diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..b74e40703 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,4 @@ +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence. +* @osbuild/osbuild-reviewers + diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 60a810c53..d1399e9e1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,16 +11,3 @@ updates: time: "04:00" open-pull-requests-limit: 5 rebase-strategy: "disabled" - - # Maintain dependencies for Go - - package-ecosystem: "gomod" - directory: "/bib" - schedule: - interval: "daily" - time: "04:00" - groups: - go-deps: - patterns: - - "*" # group all dependency updates into one PR - open-pull-requests-limit: 1 - rebase-strategy: "auto" diff --git a/.github/workflows/auto-merge-dependabot.yml b/.github/workflows/auto-merge-dependabot.yml index f5e014fbb..0626a6220 100644 --- a/.github/workflows/auto-merge-dependabot.yml +++ b/.github/workflows/auto-merge-dependabot.yml @@ -8,7 +8,7 @@ permissions: write-all jobs: dependabot: runs-on: ubuntu-latest - if: ${{ github.actor == 'dependabot[bot]' }} + if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }} steps: - name: Approve a PR run: gh pr review --approve "$PR_URL" diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index eb0244973..f01253fb6 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -2,22 +2,29 @@ name: Build containers on: pull_request: - branches: [main] + branches: + - main + - rhel-* + workflow_dispatch: # for merge queue merge_group: + push: + branches: [main] env: + REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} permissions: contents: read + packages: write jobs: build: runs-on: ubuntu-22.04 steps: - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Build image uses: redhat-actions/buildah-build@v2 @@ -25,3 +32,19 @@ jobs: image: ${{ env.IMAGE_NAME }} tags: "latest" containerfiles: Containerfile + + - name: Log in to the Container registry + if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'push') && github.ref == 'refs/heads/main' }} + uses: redhat-actions/podman-login@v1 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Push to GitHub Container Repository + if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'push') && github.ref == 'refs/heads/main' }} + uses: redhat-actions/push-to-registry@v2 + with: + image: ${{ env.IMAGE_NAME }} + tags: "latest" + registry: ${{ env.REGISTRY }} diff --git a/.github/workflows/gobump.yml b/.github/workflows/gobump.yml new file mode 100644 index 000000000..1196f6b4e --- /dev/null +++ b/.github/workflows/gobump.yml @@ -0,0 +1,57 @@ +name: "Updates Go dependencies" + +on: # yamllint disable-line rule:truthy + workflow_dispatch: + schedule: + - cron: "0 15 * * 2" + +jobs: + update-and-push: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Extract Go version from go.mod + id: go-version + run: | + VERSION=$(grep '^go ' bib/go.mod | awk '{print $2}') + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ steps.go-version.outputs.version }} + cache-dependency-path: bib/go.sum + + - name: Update go.mod and open a PR + env: + GH_TOKEN: ${{ secrets.SCHUTZBOT_GITHUB_ACCESS_TOKEN }} + run: | + pushd bib/ + echo '```' > /tmp/go.log + go get -u ./... 2>&1 | tee -a /tmp/go.log + go mod tidy 2>&1 | tee -a /tmp/go.log + echo '```' >> /tmp/go.log + popd + + if git diff --exit-code; then + echo "No changes" + exit 0 + fi + + git config user.name "schutzbot" + git config user.email "schutzbot@gmail.com" + + branch="schutz-gobump-$(date +%Y-%m-%d)" + git checkout -b "${branch}" + git add -A + git commit -m "build(deps): Update dependencies" + git push -f https://x-access-token:${GH_TOKEN}@github.com/osbuild/bootc-image-builder.git HEAD:"${branch}" + + gh pr create \ + --title "Update dependencies $(date +%Y-%m-%d)" \ + --body-file /tmp/go.log \ + --repo "osbuild/bootc-image-builder" \ + --base "main" \ + --head "${branch}" diff --git a/.github/workflows/stale-cleanup.yml b/.github/workflows/stale-cleanup.yml index e647e3184..3fa81bd5c 100644 --- a/.github/workflows/stale-cleanup.yml +++ b/.github/workflows/stale-cleanup.yml @@ -8,6 +8,7 @@ jobs: stale: runs-on: ubuntu-latest permissions: + actions: write # needed to clean up the saved action state issues: write pull-requests: write steps: diff --git a/.github/workflows/testingfarm-unit.yml b/.github/workflows/testingfarm-unit.yml index 67f900dca..e57470cd9 100644 --- a/.github/workflows/testingfarm-unit.yml +++ b/.github/workflows/testingfarm-unit.yml @@ -27,13 +27,13 @@ jobs: echo "Job originally triggered by ${{ github.actor }}" exit 1 - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.head.sha }} - name: Run the tests - uses: sclorg/testing-farm-as-github-action@v3 + uses: sclorg/testing-farm-as-github-action@v4 with: - compose: Fedora-40 + compose: Fedora-42 tmt_plan_regex: "/plans/unit-go" api_key: ${{ secrets.TF_API_KEY }} git_url: ${{ github.event.pull_request.head.repo.clone_url }} diff --git a/.github/workflows/testingfarm.yml b/.github/workflows/testingfarm.yml index 82c2c2732..58b9328b3 100644 --- a/.github/workflows/testingfarm.yml +++ b/.github/workflows/testingfarm.yml @@ -44,13 +44,13 @@ jobs: echo "Job originally triggered by ${{ github.actor }}" exit 1 - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.head.sha }} - name: Run the tests - uses: sclorg/testing-farm-as-github-action@v3 + uses: sclorg/testing-farm-as-github-action@v4 with: - compose: Fedora-40 + compose: Fedora-42 tmt_plan_regex: "/plans/integration" api_key: ${{ secrets.TF_API_KEY }} git_url: ${{ github.event.pull_request.head.repo.clone_url }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ad3cbfdbf..7a2f44d32 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,9 +13,6 @@ on: env: GO_VERSION: 1.22 - # see https://golangci-lint.run/product/changelog - # to select a version that supports the GO_VERSION given above - GOLANGCI_LINT_VERSION: v1.59.1 concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -33,7 +30,7 @@ jobs: id: go - name: Check out code into the Go module directory - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.head.sha }} @@ -44,10 +41,14 @@ jobs: - name: Install libgpgme devel package run: sudo apt install -y libgpgme-dev libbtrfs-dev libdevmapper-dev + - name: Extract golangci-lint version from Makefile + id: golangci_lint_version + run: echo "GOLANGCI_LINT_VERSION=$(awk -F '=' '/^GOLANGCI_LINT_VERSION *=/{print $2}' Makefile)" >> "$GITHUB_OUTPUT" + - name: Run golangci-lint - uses: golangci/golangci-lint-action@v6 + uses: golangci/golangci-lint-action@v9 with: - version: ${{ env.GOLANGCI_LINT_VERSION }} + version: ${{ steps.golangci_lint_version.outputs.GOLANGCI_LINT_VERSION }} args: --timeout 5m0s working-directory: bib @@ -56,9 +57,9 @@ jobs: shellcheck: name: "🐚 Shellcheck" - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.head.sha }} - name: Run ShellCheck @@ -70,22 +71,42 @@ jobs: # allow seemingly unreachable commands SHELLCHECK_OPTS: -e SC1091 -e SC2002 -e SC2317 + collect_tests: + runs-on: ubuntu-latest + outputs: + test_files: ${{ steps.collect.outputs.test_files }} + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + ref: ${{ github.event.pull_request.head.sha }} + - name: Collect test files + id: collect + run: | + TEST_FILES=$(ls test/test_*.py | sort) + JSON_FILES=$(echo "${TEST_FILES}" | jq -R | jq -cs ) + echo "test_files=${JSON_FILES}" >> $GITHUB_OUTPUT + integration: # TODO: run this also via tmt/testing-farm name: "Integration" runs-on: ubuntu-24.04 + needs: collect_tests + strategy: + matrix: + test_file: ${{ fromJson(needs.collect_tests.outputs.test_files) }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.head.sha }} - name: Setup up python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 - name: Apt update run: sudo apt update - name: Install test dependencies run: | sudo apt update - sudo apt install -y python3-pytest python3-paramiko python3-boto3 flake8 pylint libosinfo-bin squashfs-tools + sudo apt install -y python3-pytest python3-boto3 flake8 pylint libosinfo-bin squashfs-tools sshpass - name: Diskspace (before) run: | df -h @@ -124,7 +145,7 @@ jobs: - run: | mkdir -p /var/tmp/osbuild-test-store - name: Cache osbuild env - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: /var/tmp/osbuild-test-store key: no-key-needed-here @@ -136,7 +157,7 @@ jobs: # podman needs (parts of) the environment but will break when # XDG_RUNTIME_DIR is set. # TODO: figure out what exactly podman needs - sudo -E XDG_RUNTIME_DIR= pytest-3 --basetemp=/mnt/var/tmp/bib-tests + sudo -E XDG_RUNTIME_DIR= PYTHONPATH=. pytest-3 --basetemp=/mnt/var/tmp/bib-tests ${{ matrix.test_file }} - name: Diskspace (after) if: ${{ always() }} run: | diff --git a/.tekton/bootc-image-builder-pull-request.yaml b/.tekton/bootc-image-builder-pull-request.yaml index 2ff80f863..aca1eba40 100644 --- a/.tekton/bootc-image-builder-pull-request.yaml +++ b/.tekton/bootc-image-builder-pull-request.yaml @@ -46,7 +46,7 @@ spec: - name: name value: show-sbom - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-show-sbom:0.1@sha256:8e0f8cad75e6f674d72a874385b69c4651afc0c9dcc59feffe0d85844687d852 + value: quay.io/konflux-ci/tekton-catalog/task-show-sbom:0.1@sha256:beb0616db051952b4b861dd8c3e00fa1c0eccbd926feddf71194d3bb3ace9ce7 - name: kind value: task resolver: bundles @@ -65,7 +65,7 @@ spec: - name: name value: summary - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-summary:0.2@sha256:abdf426424f1331c27be80ed98a0fbcefb8422767d1724308b9d57b37f977155 + value: quay.io/konflux-ci/tekton-catalog/task-summary:0.2@sha256:3f6e8513cbd70f0416eb6c6f2766973a754778526125ff33d8e3633def917091 - name: kind value: task resolver: bundles @@ -134,9 +134,6 @@ spec: - description: "" name: CHAINS-GIT_COMMIT value: $(tasks.clone-repository-amd64.results.commit) - - description: "" - name: JAVA_COMMUNITY_DEPENDENCIES - value: $(tasks.build-container-amd64.results.JAVA_COMMUNITY_DEPENDENCIES) tasks: - name: init params: @@ -157,7 +154,7 @@ spec: - name: name value: init - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-init:0.2@sha256:596b7c11572bb94eb67d9ffb4375068426e2a8249ff2792ce04ad2a4bc593a63 + value: quay.io/konflux-ci/tekton-catalog/task-init:0.2@sha256:3ca52e1d8885fc229bd9067275f44d5b21a9a609981d0324b525ddeca909bf10 - name: kind value: task resolver: bundles @@ -174,7 +171,7 @@ spec: - name: name value: git-clone - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-git-clone:0.1@sha256:9e6c4db5a666ea0e1e747e03d63f46e5617a6b9852c26871f9d50891d778dfa2 + value: quay.io/konflux-ci/tekton-catalog/task-git-clone:0.1@sha256:90d2d42d17e6276ef45505cbb5a78598e5f5186257d0ee2260b3d4835f1c2d6b - name: kind value: task resolver: bundles @@ -202,7 +199,7 @@ spec: - name: name value: git-clone - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-git-clone:0.1@sha256:9e6c4db5a666ea0e1e747e03d63f46e5617a6b9852c26871f9d50891d778dfa2 + value: quay.io/konflux-ci/tekton-catalog/task-git-clone:0.1@sha256:90d2d42d17e6276ef45505cbb5a78598e5f5186257d0ee2260b3d4835f1c2d6b - name: kind value: task resolver: bundles @@ -230,7 +227,7 @@ spec: - name: name value: git-clone - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-git-clone:0.1@sha256:9e6c4db5a666ea0e1e747e03d63f46e5617a6b9852c26871f9d50891d778dfa2 + value: quay.io/konflux-ci/tekton-catalog/task-git-clone:0.1@sha256:90d2d42d17e6276ef45505cbb5a78598e5f5186257d0ee2260b3d4835f1c2d6b - name: kind value: task resolver: bundles @@ -256,7 +253,7 @@ spec: - name: name value: git-clone - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-git-clone:0.1@sha256:9e6c4db5a666ea0e1e747e03d63f46e5617a6b9852c26871f9d50891d778dfa2 + value: quay.io/konflux-ci/tekton-catalog/task-git-clone:0.1@sha256:90d2d42d17e6276ef45505cbb5a78598e5f5186257d0ee2260b3d4835f1c2d6b - name: kind value: task resolver: bundles @@ -293,7 +290,7 @@ spec: - name: name value: buildah - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-buildah:0.2@sha256:6b60c1130ec0df69faa82dccbc207273936a41af5ee663c736d2977580e88626 + value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.6@sha256:4a18de4811fc4b5743b0073de2154db29d323312b93419dbd28b209ce495f042 - name: kind value: task resolver: bundles @@ -330,7 +327,7 @@ spec: - name: name value: buildah-remote - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-buildah-remote:0.2@sha256:338fd01c1b4b9aa74556718c58290e7f164730ba34e80760f1a42dc2ac771a55 + value: quay.io/konflux-ci/tekton-catalog/task-buildah-remote:0.6@sha256:97c6088df2cb17239335e9722fec6de5d8bbf68a53c6489171993f55fd5be1fa - name: kind value: task resolver: bundles @@ -367,7 +364,7 @@ spec: - name: name value: buildah-remote - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-buildah-remote:0.2@sha256:338fd01c1b4b9aa74556718c58290e7f164730ba34e80760f1a42dc2ac771a55 + value: quay.io/konflux-ci/tekton-catalog/task-buildah-remote:0.6@sha256:97c6088df2cb17239335e9722fec6de5d8bbf68a53c6489171993f55fd5be1fa - name: kind value: task resolver: bundles @@ -404,7 +401,7 @@ spec: - name: name value: buildah-remote - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-buildah-remote:0.2@sha256:338fd01c1b4b9aa74556718c58290e7f164730ba34e80760f1a42dc2ac771a55 + value: quay.io/konflux-ci/tekton-catalog/task-buildah-remote:0.6@sha256:97c6088df2cb17239335e9722fec6de5d8bbf68a53c6489171993f55fd5be1fa - name: kind value: task resolver: bundles @@ -438,7 +435,7 @@ spec: - name: name value: build-image-manifest - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-build-image-manifest:0.1@sha256:399ab5004f27d7ff836f8c838b589262299e1e4bdd4670993b9d0c981b274d86 + value: quay.io/konflux-ci/tekton-catalog/task-build-image-manifest:0.1@sha256:1e49b4d7d350b8c43c284a57f3c3db789437bb3e2e28db205a990aae78c96022 - name: kind value: task resolver: bundles @@ -460,7 +457,7 @@ spec: - name: name value: inspect-image - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-inspect-image:0.1@sha256:dd639d03487d9ee2c424bcd0118a9b07064010f40168ffb1302a54e0f584603e + value: quay.io/konflux-ci/tekton-catalog/task-inspect-image:0.2@sha256:96677b43c900f1336938db3e1477bc49fb104ba3fa1e301e524a1ef704a4e754 - name: kind value: task resolver: bundles @@ -483,7 +480,7 @@ spec: - name: name value: deprecated-image-check - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-deprecated-image-check:0.4@sha256:6c389c2f670975cc0dfdd07dcb33142b1668bbfd46f6af520dd0ab736c56e7e9 + value: quay.io/konflux-ci/tekton-catalog/task-deprecated-image-check:0.5@sha256:f59175d9a0a60411738228dfe568af4684af4aa5e7e05c832927cb917801d489 - name: kind value: task resolver: bundles @@ -505,7 +502,7 @@ spec: - name: name value: clair-scan - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-clair-scan:0.1@sha256:a1bbc7354d8dc8fef41caca236bde682fc6a9230065a5537f1dc1ca4f1e39e83 + value: quay.io/konflux-ci/tekton-catalog/task-clair-scan:0.3@sha256:8ec7d7b9438ace5ef3fb03a533d9440d0fd81e51c73b0dc1eb51602fb7cd044e - name: kind value: task resolver: bundles @@ -516,18 +513,18 @@ spec: - "false" - name: sast-snyk-check params: - - name: image-digest - value: $(tasks.build-container.results.IMAGE_DIGEST) - - name: image-url - value: $(tasks.build-container.results.IMAGE_URL) + - name: image-digest + value: $(tasks.build-container.results.IMAGE_DIGEST) + - name: image-url + value: $(tasks.build-container.results.IMAGE_URL) runAfter: - - build-container + - build-container taskRef: params: - name: name value: sast-snyk-check - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-sast-snyk-check:0.1@sha256:91d32451e6e62d8a7b56d1ad389a1c0a45cdb7a35a4483e1f44224b0be2420df + value: quay.io/konflux-ci/tekton-catalog/task-sast-snyk-check:0.4@sha256:4b152eb931605b969c7a1ba15dd6a4d3c0231a20a1442ba5608e067160259e9d - name: kind value: task resolver: bundles @@ -552,7 +549,7 @@ spec: - name: name value: clamav-scan - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-clamav-scan:0.1@sha256:7e99aad37178be72a799fcf1d154007346e038fcccb222f6937df4766a2810d2 + value: quay.io/konflux-ci/tekton-catalog/task-clamav-scan:0.3@sha256:f3d2d179cddcc07d0228d9f52959a233037a3afa2619d0a8b2effbb467db80c3 - name: kind value: task resolver: bundles @@ -574,7 +571,7 @@ spec: - name: name value: sbom-json-check - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-sbom-json-check:0.1@sha256:501181e78ec76a0a9083ffc275f5307ba5653a762259412bcffaeb314f13f8ec + value: quay.io/konflux-ci/tekton-catalog/task-sbom-json-check:0.2@sha256:c9ad826b8b412bb178713c3b49aa8cbec35df0458f34fa31721fe84d645f7996 - name: kind value: task resolver: bundles @@ -587,6 +584,8 @@ spec: - name: workspace-amd64 - name: git-auth optional: true + taskRunTemplate: + serviceAccountName: build-pipeline-bootc-image-builder workspaces: - name: workspace-amd64 volumeClaimTemplate: diff --git a/.tekton/bootc-image-builder-push.yaml b/.tekton/bootc-image-builder-push.yaml index a69fcdf82..d4820c5c2 100644 --- a/.tekton/bootc-image-builder-push.yaml +++ b/.tekton/bootc-image-builder-push.yaml @@ -6,9 +6,7 @@ metadata: build.appstudio.redhat.com/commit_sha: "{{revision}}" build.appstudio.redhat.com/target_branch: "{{target_branch}}" pipelinesascode.tekton.dev/max-keep-runs: "3" - pipelinesascode.tekton.dev/on-cel-expression: - event == "push" && target_branch - == "main" + pipelinesascode.tekton.dev/on-cel-expression: event == "push" && target_branch == "main" && files.all.exists(x,!x.startsWith(".tekton/")) creationTimestamp: null labels: appstudio.openshift.io/application: bootc-image-builder @@ -43,7 +41,7 @@ spec: - name: name value: show-sbom - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-show-sbom:0.1@sha256:8e0f8cad75e6f674d72a874385b69c4651afc0c9dcc59feffe0d85844687d852 + value: quay.io/konflux-ci/tekton-catalog/task-show-sbom:0.1@sha256:beb0616db051952b4b861dd8c3e00fa1c0eccbd926feddf71194d3bb3ace9ce7 - name: kind value: task resolver: bundles @@ -62,7 +60,7 @@ spec: - name: name value: summary - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-summary:0.2@sha256:abdf426424f1331c27be80ed98a0fbcefb8422767d1724308b9d57b37f977155 + value: quay.io/konflux-ci/tekton-catalog/task-summary:0.2@sha256:3f6e8513cbd70f0416eb6c6f2766973a754778526125ff33d8e3633def917091 - name: kind value: task resolver: bundles @@ -131,9 +129,6 @@ spec: - description: "" name: CHAINS-GIT_COMMIT value: $(tasks.clone-repository-amd64.results.commit) - - description: "" - name: JAVA_COMMUNITY_DEPENDENCIES - value: $(tasks.build-container-amd64.results.JAVA_COMMUNITY_DEPENDENCIES) tasks: - name: init params: @@ -154,7 +149,7 @@ spec: - name: name value: init - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-init:0.2@sha256:596b7c11572bb94eb67d9ffb4375068426e2a8249ff2792ce04ad2a4bc593a63 + value: quay.io/konflux-ci/tekton-catalog/task-init:0.2@sha256:3ca52e1d8885fc229bd9067275f44d5b21a9a609981d0324b525ddeca909bf10 - name: kind value: task resolver: bundles @@ -171,7 +166,7 @@ spec: - name: name value: git-clone - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-git-clone:0.1@sha256:9e6c4db5a666ea0e1e747e03d63f46e5617a6b9852c26871f9d50891d778dfa2 + value: quay.io/konflux-ci/tekton-catalog/task-git-clone:0.1@sha256:90d2d42d17e6276ef45505cbb5a78598e5f5186257d0ee2260b3d4835f1c2d6b - name: kind value: task resolver: bundles @@ -199,7 +194,7 @@ spec: - name: name value: git-clone - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-git-clone:0.1@sha256:9e6c4db5a666ea0e1e747e03d63f46e5617a6b9852c26871f9d50891d778dfa2 + value: quay.io/konflux-ci/tekton-catalog/task-git-clone:0.1@sha256:90d2d42d17e6276ef45505cbb5a78598e5f5186257d0ee2260b3d4835f1c2d6b - name: kind value: task resolver: bundles @@ -227,7 +222,7 @@ spec: - name: name value: git-clone - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-git-clone:0.1@sha256:9e6c4db5a666ea0e1e747e03d63f46e5617a6b9852c26871f9d50891d778dfa2 + value: quay.io/konflux-ci/tekton-catalog/task-git-clone:0.1@sha256:90d2d42d17e6276ef45505cbb5a78598e5f5186257d0ee2260b3d4835f1c2d6b - name: kind value: task resolver: bundles @@ -253,7 +248,7 @@ spec: - name: name value: git-clone - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-git-clone:0.1@sha256:9e6c4db5a666ea0e1e747e03d63f46e5617a6b9852c26871f9d50891d778dfa2 + value: quay.io/konflux-ci/tekton-catalog/task-git-clone:0.1@sha256:90d2d42d17e6276ef45505cbb5a78598e5f5186257d0ee2260b3d4835f1c2d6b - name: kind value: task resolver: bundles @@ -278,7 +273,7 @@ spec: - name: name value: prefetch-dependencies - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-prefetch-dependencies:0.1@sha256:610ba9e81465fdc5456ed2846503c6cb6f38413d1211e5c63ba152fd1ff2c3ee + value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies:0.2@sha256:a18a33aa577ac1b8f0c9ca6cd74c4c73a30cfd48a7b959c86390bc04066d1fb1 - name: kind value: task resolver: bundles @@ -301,7 +296,7 @@ spec: - name: name value: prefetch-dependencies - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-prefetch-dependencies:0.1@sha256:610ba9e81465fdc5456ed2846503c6cb6f38413d1211e5c63ba152fd1ff2c3ee + value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies:0.2@sha256:a18a33aa577ac1b8f0c9ca6cd74c4c73a30cfd48a7b959c86390bc04066d1fb1 - name: kind value: task resolver: bundles @@ -324,7 +319,7 @@ spec: - name: name value: prefetch-dependencies - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-prefetch-dependencies:0.1@sha256:610ba9e81465fdc5456ed2846503c6cb6f38413d1211e5c63ba152fd1ff2c3ee + value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies:0.2@sha256:a18a33aa577ac1b8f0c9ca6cd74c4c73a30cfd48a7b959c86390bc04066d1fb1 - name: kind value: task resolver: bundles @@ -347,7 +342,7 @@ spec: - name: name value: prefetch-dependencies - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-prefetch-dependencies:0.1@sha256:610ba9e81465fdc5456ed2846503c6cb6f38413d1211e5c63ba152fd1ff2c3ee + value: quay.io/konflux-ci/tekton-catalog/task-prefetch-dependencies:0.2@sha256:a18a33aa577ac1b8f0c9ca6cd74c4c73a30cfd48a7b959c86390bc04066d1fb1 - name: kind value: task resolver: bundles @@ -382,7 +377,7 @@ spec: - name: name value: buildah - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-buildah:0.2@sha256:6b60c1130ec0df69faa82dccbc207273936a41af5ee663c736d2977580e88626 + value: quay.io/konflux-ci/tekton-catalog/task-buildah:0.6@sha256:4a18de4811fc4b5743b0073de2154db29d323312b93419dbd28b209ce495f042 - name: kind value: task resolver: bundles @@ -419,7 +414,7 @@ spec: - name: name value: buildah-remote - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-buildah-remote:0.2@sha256:338fd01c1b4b9aa74556718c58290e7f164730ba34e80760f1a42dc2ac771a55 + value: quay.io/konflux-ci/tekton-catalog/task-buildah-remote:0.6@sha256:97c6088df2cb17239335e9722fec6de5d8bbf68a53c6489171993f55fd5be1fa - name: kind value: task resolver: bundles @@ -456,7 +451,7 @@ spec: - name: name value: buildah-remote - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-buildah-remote:0.2@sha256:338fd01c1b4b9aa74556718c58290e7f164730ba34e80760f1a42dc2ac771a55 + value: quay.io/konflux-ci/tekton-catalog/task-buildah-remote:0.6@sha256:97c6088df2cb17239335e9722fec6de5d8bbf68a53c6489171993f55fd5be1fa - name: kind value: task resolver: bundles @@ -493,7 +488,7 @@ spec: - name: name value: buildah-remote - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-buildah-remote:0.2@sha256:338fd01c1b4b9aa74556718c58290e7f164730ba34e80760f1a42dc2ac771a55 + value: quay.io/konflux-ci/tekton-catalog/task-buildah-remote:0.6@sha256:97c6088df2cb17239335e9722fec6de5d8bbf68a53c6489171993f55fd5be1fa - name: kind value: task resolver: bundles @@ -527,7 +522,7 @@ spec: - name: name value: build-image-manifest - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-build-image-manifest:0.1@sha256:399ab5004f27d7ff836f8c838b589262299e1e4bdd4670993b9d0c981b274d86 + value: quay.io/konflux-ci/tekton-catalog/task-build-image-manifest:0.1@sha256:1e49b4d7d350b8c43c284a57f3c3db789437bb3e2e28db205a990aae78c96022 - name: kind value: task resolver: bundles @@ -549,7 +544,7 @@ spec: - name: name value: inspect-image - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-inspect-image:0.1@sha256:dd639d03487d9ee2c424bcd0118a9b07064010f40168ffb1302a54e0f584603e + value: quay.io/konflux-ci/tekton-catalog/task-inspect-image:0.2@sha256:96677b43c900f1336938db3e1477bc49fb104ba3fa1e301e524a1ef704a4e754 - name: kind value: task resolver: bundles @@ -572,7 +567,7 @@ spec: - name: name value: deprecated-image-check - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-deprecated-image-check:0.4@sha256:6c389c2f670975cc0dfdd07dcb33142b1668bbfd46f6af520dd0ab736c56e7e9 + value: quay.io/konflux-ci/tekton-catalog/task-deprecated-image-check:0.5@sha256:f59175d9a0a60411738228dfe568af4684af4aa5e7e05c832927cb917801d489 - name: kind value: task resolver: bundles @@ -594,7 +589,7 @@ spec: - name: name value: clair-scan - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-clair-scan:0.1@sha256:a1bbc7354d8dc8fef41caca236bde682fc6a9230065a5537f1dc1ca4f1e39e83 + value: quay.io/konflux-ci/tekton-catalog/task-clair-scan:0.3@sha256:8ec7d7b9438ace5ef3fb03a533d9440d0fd81e51c73b0dc1eb51602fb7cd044e - name: kind value: task resolver: bundles @@ -605,18 +600,18 @@ spec: - "false" - name: sast-snyk-check params: - - name: image-digest - value: $(tasks.build-container.results.IMAGE_DIGEST) - - name: image-url - value: $(tasks.build-container.results.IMAGE_URL) + - name: image-digest + value: $(tasks.build-container.results.IMAGE_DIGEST) + - name: image-url + value: $(tasks.build-container.results.IMAGE_URL) runAfter: - - build-container + - build-container taskRef: params: - name: name value: sast-snyk-check - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-sast-snyk-check:0.1@sha256:91d32451e6e62d8a7b56d1ad389a1c0a45cdb7a35a4483e1f44224b0be2420df + value: quay.io/konflux-ci/tekton-catalog/task-sast-snyk-check:0.4@sha256:4b152eb931605b969c7a1ba15dd6a4d3c0231a20a1442ba5608e067160259e9d - name: kind value: task resolver: bundles @@ -641,7 +636,7 @@ spec: - name: name value: clamav-scan - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-clamav-scan:0.1@sha256:7e99aad37178be72a799fcf1d154007346e038fcccb222f6937df4766a2810d2 + value: quay.io/konflux-ci/tekton-catalog/task-clamav-scan:0.3@sha256:f3d2d179cddcc07d0228d9f52959a233037a3afa2619d0a8b2effbb467db80c3 - name: kind value: task resolver: bundles @@ -663,7 +658,7 @@ spec: - name: name value: sbom-json-check - name: bundle - value: quay.io/redhat-appstudio-tekton-catalog/task-sbom-json-check:0.1@sha256:501181e78ec76a0a9083ffc275f5307ba5653a762259412bcffaeb314f13f8ec + value: quay.io/konflux-ci/tekton-catalog/task-sbom-json-check:0.2@sha256:c9ad826b8b412bb178713c3b49aa8cbec35df0458f34fa31721fe84d645f7996 - name: kind value: task resolver: bundles @@ -676,6 +671,8 @@ spec: - name: workspace-amd64 - name: git-auth optional: true + taskRunTemplate: + serviceAccountName: build-pipeline-bootc-image-builder workspaces: - name: workspace-amd64 volumeClaimTemplate: diff --git a/Containerfile b/Containerfile index 26bd03fd7..00b860307 100644 --- a/Containerfile +++ b/Containerfile @@ -1,4 +1,4 @@ -FROM registry.fedoraproject.org/fedora:41 AS builder +FROM registry.fedoraproject.org/fedora:42 AS builder RUN dnf install -y git-core golang gpgme-devel libassuan-devel && mkdir -p /build/bib COPY bib/go.mod bib/go.sum /build/bib/ ARG GOPROXY=https://proxy.golang.org,direct @@ -10,7 +10,7 @@ COPY . /build WORKDIR /build RUN ./build.sh -FROM registry.fedoraproject.org/fedora:41 +FROM registry.fedoraproject.org/fedora:42 # Fast-track osbuild so we don't depend on the "slow" Fedora release process to implement new features in bib COPY ./group_osbuild-osbuild-fedora.repo /etc/yum.repos.d/ COPY ./package-requires.txt . @@ -28,5 +28,5 @@ VOLUME /var/lib/containers/storage LABEL description="This tools allows to build and deploy disk-images from bootc container inputs." LABEL io.k8s.description="This tools allows to build and deploy disk-images from bootc container inputs." LABEL io.k8s.display-name="Bootc Image Builder" -LABEL io.openshift.tags="base fedora40" +LABEL io.openshift.tags="base fedora42" LABEL summary="A container to create disk-images from bootc container inputs" diff --git a/Makefile b/Makefile index e738d2302..aff0a97fc 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,13 @@ .PHONY: all all: build-binary build-container +GOLANGCI_LINT_VERSION=v2.1.6 +GO_BINARY?=go + +# the fallback '|| echo "golangci-lint' really expects this file +# NOT to exist! This is just a trigger to help installing golangci-lint +GOLANGCI_LINT_BIN=$(shell which golangci-lint 2>/dev/null || echo "golangci-lint") + .PHONY: help help: @echo 'Usage:' @@ -20,6 +27,7 @@ clean: ## clean all build and test artifacts .PHONY: test test: ## run all tests - Be aware that the tests take a really long time + cd bib && go test -race ./... @echo "Be aware that the tests take a really long time" @echo "Running tests as root" sudo -E pip install --user -r test/requirements.txt @@ -45,3 +53,14 @@ push-check: build-binary build-container test ## run all checks and tests befor exit 1; \ fi @echo "All looks good - congratulations" + +$(GOLANGCI_LINT_BIN): + @echo "golangci-lint does not seem to be installed" + @read -p "Press to install it or -c to abort" + $(GO_BINARY) install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) || \ + ( echo "if the go version is a problem, you can set GO_BINARY e.g. GO_BINARY=go.1.23.8 \ + after installing it e.g. go install golang.org/dl/go1.23.8@latest" ; exit 1 ) + +.PHONY: lint +lint: $(GOLANGCI_LINT_BIN) ## run the linters to check for bad code + cd bib && $(GOLANGCI_LINT_BIN) run diff --git a/README.md b/README.md index 053c3e7fb..1d932a065 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ The following command will create a QCOW2 disk image. First, create `./config.to ```bash # Ensure the image is fetched sudo podman pull quay.io/centos-bootc/centos-bootc:stream9 +mkdir output sudo podman run \ --rm \ -it \ @@ -56,7 +57,7 @@ sudo podman run \ -v /var/lib/containers/storage:/var/lib/containers/storage \ quay.io/centos-bootc/bootc-image-builder:latest \ --type qcow2 \ - --use-librepo=True \ + --use-librepo=True \ quay.io/centos-bootc/centos-bootc:stream9 ``` @@ -64,6 +65,33 @@ Note that some images (like fedora) do not have a default root filesystem type. In this case adds the switch `--rootfs `, e.g. `--rootfs btrfs`. +### Rootless + +There is *experimental* support for rootless builds in `bootc-image-builder`. To perform a rootless build KVM is used. The above example can be tried like so: + +```bash +# Ensure the image is fetched +podman pull quay.io/fedora/fedora-bootc:latest +mkdir output +podman run \ + --rm \ + -it \ + --privileged \ + --pull=newer \ + --security-opt label=type:unconfined_t \ + -v ./config.toml:/config.toml:ro \ + -v ./output:/output \ + -v ~/.local/share/containers/storage:/var/lib/containers/storage \ + quay.io/centos-bootc/bootc-image-builder:latest \ + --in-vm \ + --type qcow2 \ + --use-librepo=True \ + --rootfs ext4 \ + quay.io/fedora/fedora-bootc:latest +``` + +Note the mounting of the users container storage, addition of the `--in-vm` argument and the removal of `sudo` in the commands. + ### Running the resulting QCOW2 file on Linux (x86_64) A virtual machine can be launched using `qemu-system-x86_64` or with `virt-install` as shown below; @@ -122,6 +150,7 @@ Usage: --pull=newer \ --security-opt label=type:unconfined_t \ -v ./output:/output \ + -v /var/lib/containers/storage:/var/lib/containers/storage \ quay.io/centos-bootc/bootc-image-builder:latest \ @@ -131,7 +160,7 @@ Flags: --progress string type of progress bar to use (e.g. verbose,term) (default "auto") --rootfs string Root filesystem type. If not given, the default configured in the source container image is used. --target-arch string build for the given target architecture (experimental) - --type stringArray image types to build [ami, anaconda-iso, gce, iso, qcow2, raw, vhd, vmdk] (default [qcow2]) + --type stringArray image types to build [ami, anaconda-iso, bootc-installer, gce, iso, qcow2, raw, vhd, vmdk] (default [qcow2]) --version version for bootc-image-builder Global Flags: @@ -165,22 +194,70 @@ outputs will be produced. Note that comma or space separating the The following image types are currently available via the `--type` argument: -| Image type | Target environment | -|-----------------------|---------------------------------------------------------------------------------------| -| `ami` | [Amazon Machine Image](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AMIs.html) | -| `qcow2` **(default)** | [QEMU](https://www.qemu.org/) | -| `vmdk` | [VMDK](https://en.wikipedia.org/wiki/VMDK) usable in vSphere, among others | -| `anaconda-iso` | An unattended Anaconda installer that installs to the first disk found. | -| `raw` | Unformatted [raw disk](https://en.wikipedia.org/wiki/Rawdisk). | +| Image type | Target environment | +|-----------------------|-------------------------------------------------------------------------------------------| +| `ami` | [Amazon Machine Image](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AMIs.html) | +| `qcow2` **(default)** | [QEMU](https://www.qemu.org/) | +| `vmdk` | [VMDK](https://en.wikipedia.org/wiki/VMDK) usable in vSphere, among others | +| `bootc-installer` | An installer ISO image based on the specified bootc container image. | +| `anaconda-iso` | An unattended Anaconda installer that installs to the first disk found. Built from RPMs. | +| `raw` | Unformatted [raw disk](https://en.wikipedia.org/wiki/Rawdisk). | | `vhd` | [vhd](https://en.wikipedia.org/wiki/VHD_(file_format)) usable in Virtual PC, among others | -| `gce` | [GCE](https://cloud.google.com/compute/docs/images#custom_images) | +| `gce` | [GCE](https://cloud.google.com/compute/docs/images#custom_images) | +| `pxe-tar-xz` | A stateless image useful in PXE network boot environments | + + +## 💾 Image Type Requirements + +### pxe-tar-xz + +The container image being built must have the `dracut-live` and `squashfs-tools` packages installed as well as a rebuilding the initramfs with the 'dmsquash-live' module. See [osbuild documentation](https://github.com/osbuild/images/blob/main/data/files/pxetree/README) for more information and a sample Containerfile. + +### bootc-installer + +When building `bootc-installer` the positional container argument is expected to be a container that has Anaconda inside it; an example `Containerfile` for such a container is: + +``` +FROM your-favorite-bootc-container:latest +RUN dnf install -y \ + anaconda \ + anaconda-install-env-deps \ + anaconda-dracut \ + dracut-config-generic \ + dracut-network \ + net-tools \ + squashfs-tools \ + grub2-efi-x64-cdboot \ + python3-mako \ + lorax-templates-* \ + biosdevname \ + prefixdevname \ + && dnf clean all + +# On Fedora 42 this is necessary to get files in the right places +# RUN dnf reinstall -y shim-x64 + +# On Fedora 43 and up this is necessary to get files in the right +# places +RUN mkdir -p /boot/efi && cp -ra /usr/lib/efi/*/*/EFI /boot/efi + +# lorax wants to create a symlink in /mnt which points to /var/mnt +# on bootc but /var/mnt does not exist on some images. +# +# If https://gitlab.com/fedora/bootc/base-images/-/merge_requests/294 +# gets merged this will be no longer needed +RUN mkdir /var/mnt +``` + +You must also pass the `--bootc-installer-payload-ref` argument. This is a container reference to the payload to be installed by Anaconda. It will be embedded inside the installer and Anaconda will be configured to install it. ## 💾 Target architecture Specify the target architecture of the system on which the disk image will be installed on. By default, `bootc-image-builder` will build for the native host architecture. The target architecture must match an available architecture of the `bootc-image-builder` image you are using to build the disk image. -Currently, `amd64` and `arm64` are included in `quay.io/centos-bootc/bootc-image-builder` manifest list. +Navigate to the [centos-image-builder repository tags page](https://quay.io/repository/centos-bootc/bootc-image-builder?tab=tags) +and hover over the Tux icons to see the supported target architectures. The architecture of the bootc OCI image and the bootc-image-builder image must match. For example, when building a non-native architecture bootc OCI image, say, building for x86_64 from an arm-based Mac, it is possible to run `podman build` with the `--platform linux/amd64` flag. In this case, to then build a disk image from the same arm-based Mac, @@ -247,13 +324,14 @@ directory to the container For example: ```bash - $ sudo podman run \ +$ sudo podman run \ --rm \ -it \ --privileged \ --pull=newer \ --security-opt label=type:unconfined_t \ -v $HOME/.aws:/root/.aws:ro \ + -v /var/lib/containers/storage:/var/lib/containers/storage \ --env AWS_PROFILE=default \ quay.io/centos-bootc/bootc-image-builder:latest \ --type ami \ @@ -293,6 +371,7 @@ $ sudo podman run \ --privileged \ --pull=newer \ --security-opt label=type:unconfined_t \ + -v /var/lib/containers/storage:/var/lib/containers/storage \ --env-file=aws.secrets \ quay.io/centos-bootc/bootc-image-builder:latest \ --type ami \ @@ -314,7 +393,9 @@ The following volumes can be mounted inside the container: ## 📝 Build config -A build config is a Toml (or JSON) file with customizations for the resulting image. The config file is mapped into the container directory to `/config.toml`. The customizations are specified under a `customizations` object. +A build config is a TOML (or JSON) file with customizations for the resulting image. The config file is mapped into the container directory to `/config.toml`. The customizations are specified under a `customizations` object. + +The build config is a [Blueprint file](https://github.com/osbuild/blueprint), documented in the [osbuild.org User Guide](https://osbuild.org/docs/user-guide/blueprint-reference/). Note that not all Blueprint options are supported in bootc-image-builder. Refer to the **bootc** tab for information on whether a specific customization is supported. As an example, let's show how you can add a user to the image: @@ -339,6 +420,7 @@ sudo podman run \ --security-opt label=type:unconfined_t \ -v ./config.toml:/config.toml:ro \ -v ./output:/output \ + -v /var/lib/containers/storage:/var/lib/containers/storage \ quay.io/centos-bootc/bootc-image-builder:latest \ --type qcow2 \ quay.io/centos-bootc/centos-bootc:stream9 @@ -347,6 +429,12 @@ sudo podman run \ The configuration can also be passed in via stdin when `--config -` is used. Only JSON configuration is supported in this mode. +Additionally, images can embed a build config file, either as +`config.json` or `config.toml` in the `/usr/lib/bootc-image-builder` +directory. If this exist, and contains filesystem or disk +customizations, then these are used by default if no such +customization are specified in the regular build config. + ### Users (`user`, array) Possible fields: @@ -442,7 +530,8 @@ The `rootfs` option (or source container config, see [Detailed description of op ### Anaconda ISO (installer) options (`installer`, mapping) -Users can include kickstart file content that will be added to an ISO build to configure the installation process. +Users can include kickstart file content that will be added to an ISO build to configure the installation process. When using custom kickstart scripts the customization needs to be done via the custom kickstart script. For example using a `[customizations.user]` block alongside a `[customizations.installer.kickstart]` block is not supported. See this issue [https://github.com/osbuild/bootc-image-builder/issues/528] for additional detail. + Since multi-line strings are difficult to write and read in json, it's easier to use the toml format when adding kickstart contents: ```toml @@ -525,13 +614,23 @@ By default, the following modules are enabled for all Anaconda ISOs: - `org.fedoraproject.Anaconda.Modules.Storage` - `org.fedoraproject.Anaconda.Modules.Users` +### Anaconda ISO (media) options (`iso`, mapping) + +Users can customize the volume_id (which will be the ISO's label, used also in boot/grub.cfg). + + +```toml +[customizations.iso] +volume_id = "TheISOLabel" +application_id = "MyFancyAPP" +publisher = "ThePublisher" +``` ##### Enable vs Disable priority The `disable` list is processed after the `enable` list and therefore takes priority. In other words, adding the same module in both `enable` and `disable` will result in the module being **disabled**. Furthermore, adding a module that is enabled by default to `disable` will result in the module being **disabled**. - ## Building To build the container locally you can run diff --git a/bib/cmd/bootc-image-builder/cloud.go b/bib/cmd/bootc-image-builder/cloud.go index 17b0ab4f5..57feccc9d 100644 --- a/bib/cmd/bootc-image-builder/cloud.go +++ b/bib/cmd/bootc-image-builder/cloud.go @@ -29,15 +29,18 @@ func upload(uploader cloud.Uploader, path string, flags *pflag.FlagSet) error { if err != nil { return fmt.Errorf("cannot upload: %v", err) } + // nolint:errcheck defer file.Close() var r io.Reader = file + var size int64 if pbar != nil { st, err := file.Stat() if err != nil { return err } - pbar.SetTotal(st.Size()) + size = st.Size() + pbar.SetTotal(size) pbar.Set(pb.Bytes, true) pbar.SetWriter(osStdout) r = pbar.NewProxyReader(file) @@ -45,5 +48,5 @@ func upload(uploader cloud.Uploader, path string, flags *pflag.FlagSet) error { defer pbar.Finish() } - return uploader.UploadAndRegister(r, osStderr) + return uploader.UploadAndRegister(r, uint64(size), osStderr) } diff --git a/bib/cmd/bootc-image-builder/export_test.go b/bib/cmd/bootc-image-builder/export_test.go index 9b0e6a2c5..e174182c0 100644 --- a/bib/cmd/bootc-image-builder/export_test.go +++ b/bib/cmd/bootc-image-builder/export_test.go @@ -1,16 +1,16 @@ package main +import ( + "github.com/osbuild/images/pkg/cloud" + "github.com/osbuild/images/pkg/cloud/awscloud" +) + var ( - CanChownInPath = canChownInPath - CheckFilesystemCustomizations = checkFilesystemCustomizations - GetDistroAndRunner = getDistroAndRunner - CheckMountpoints = checkMountpoints - PartitionTables = partitionTables - UpdateFilesystemSizes = updateFilesystemSizes - GenPartitionTable = genPartitionTable - CreateRand = createRand - BuildCobraCmdline = buildCobraCmdline - CalcRequiredDirectorySizes = calcRequiredDirectorySizes + CanChownInPath = canChownInPath + GetDistroAndRunner = getDistroAndRunner + CreateRand = createRand + BuildCobraCmdline = buildCobraCmdline + HandleAWSFlags = handleAWSFlags ) func MockOsGetuid(new func() int) (restore func()) { @@ -20,3 +20,19 @@ func MockOsGetuid(new func() int) (restore func()) { osGetuid = saved } } + +func MockOsReadFile(new func(string) ([]byte, error)) (restore func()) { + saved := osReadFile + osReadFile = new + return func() { + osReadFile = saved + } +} + +func MockAwscloudNewUploader(f func(string, string, string, *awscloud.UploaderOptions) (cloud.Uploader, error)) (restore func()) { + saved := awscloudNewUploader + awscloudNewUploader = f + return func() { + awscloudNewUploader = saved + } +} diff --git a/bib/cmd/bootc-image-builder/image.go b/bib/cmd/bootc-image-builder/image.go deleted file mode 100644 index a6b1858a3..000000000 --- a/bib/cmd/bootc-image-builder/image.go +++ /dev/null @@ -1,606 +0,0 @@ -package main - -import ( - cryptorand "crypto/rand" - "errors" - "fmt" - "math" - "math/big" - "math/rand" - "strconv" - "strings" - - "github.com/osbuild/images/pkg/arch" - "github.com/osbuild/images/pkg/blueprint" - "github.com/osbuild/images/pkg/container" - "github.com/osbuild/images/pkg/customizations/anaconda" - "github.com/osbuild/images/pkg/customizations/kickstart" - "github.com/osbuild/images/pkg/customizations/users" - "github.com/osbuild/images/pkg/disk" - "github.com/osbuild/images/pkg/image" - "github.com/osbuild/images/pkg/manifest" - "github.com/osbuild/images/pkg/osbuild" - "github.com/osbuild/images/pkg/pathpolicy" - "github.com/osbuild/images/pkg/platform" - "github.com/osbuild/images/pkg/rpmmd" - "github.com/osbuild/images/pkg/runner" - "github.com/sirupsen/logrus" - - "github.com/osbuild/bootc-image-builder/bib/internal/buildconfig" - "github.com/osbuild/bootc-image-builder/bib/internal/distrodef" - "github.com/osbuild/bootc-image-builder/bib/internal/imagetypes" - "github.com/osbuild/bootc-image-builder/bib/internal/source" -) - -// TODO: Auto-detect this from container image metadata -const DEFAULT_SIZE = uint64(10 * GibiByte) - -type ManifestConfig struct { - // OCI image path (without the transport, that is always docker://) - Imgref string - - ImageTypes imagetypes.ImageTypes - - // Build config - Config *buildconfig.BuildConfig - - // CPU architecture of the image - Architecture arch.Arch - - // The minimum size required for the root fs in order to fit the container - // contents - RootfsMinsize uint64 - - // Paths to the directory with the distro definitions - DistroDefPaths []string - - // Extracted information about the source container image - SourceInfo *source.Info - - // RootFSType specifies the filesystem type for the root partition - RootFSType string - - // use librepo ad the rpm downlaod backend - UseLibrepo bool -} - -func Manifest(c *ManifestConfig) (*manifest.Manifest, error) { - rng := createRand() - - if c.ImageTypes.BuildsISO() { - return manifestForISO(c, rng) - } - return manifestForDiskImage(c, rng) -} - -var ( - // The mountpoint policy for bootc images is more restrictive than the - // ostree mountpoint policy defined in osbuild/images. It only allows / - // (for sizing the root partition) and custom mountpoints under /var but - // not /var itself. - - // Since our policy library doesn't support denying a path while allowing - // its subpaths (only the opposite), we augment the standard policy check - // with a simple search through the custom mountpoints to deny /var - // specifically. - mountpointPolicy = pathpolicy.NewPathPolicies(map[string]pathpolicy.PathPolicy{ - // allow all existing mountpoints (but no subdirs) to support size customizations - "/": {Deny: false, Exact: true}, - "/boot": {Deny: false, Exact: true}, - - // /var is not allowed, but we need to allow any subdirectories that - // are not denied below, so we allow it initially and then check it - // separately (in checkMountpoints()) - "/var": {Deny: false}, - - // /var subdir denials - "/var/home": {Deny: true}, - "/var/lock": {Deny: true}, // symlink to ../run/lock which is on tmpfs - "/var/mail": {Deny: true}, // symlink to spool/mail - "/var/mnt": {Deny: true}, - "/var/roothome": {Deny: true}, - "/var/run": {Deny: true}, // symlink to ../run which is on tmpfs - "/var/srv": {Deny: true}, - "/var/usrlocal": {Deny: true}, - }) - - mountpointMinimalPolicy = pathpolicy.NewPathPolicies(map[string]pathpolicy.PathPolicy{ - // allow all existing mountpoints to support size customizations - "/": {Deny: false, Exact: true}, - "/boot": {Deny: false, Exact: true}, - }) -) - -func checkMountpoints(filesystems []blueprint.FilesystemCustomization, policy *pathpolicy.PathPolicies) error { - errs := []error{} - for _, fs := range filesystems { - if err := policy.Check(fs.Mountpoint); err != nil { - errs = append(errs, err) - } - if fs.Mountpoint == "/var" { - // this error message is consistent with the errors returned by policy.Check() - // TODO: remove trailing space inside the quoted path when the function is fixed in osbuild/images. - errs = append(errs, fmt.Errorf(`path "/var" is not allowed`)) - } - } - if len(errs) > 0 { - return fmt.Errorf("The following errors occurred while validating custom mountpoints:\n%w", errors.Join(errs...)) - } - return nil -} - -func checkFilesystemCustomizations(fsCustomizations []blueprint.FilesystemCustomization, ptmode disk.PartitioningMode) error { - var policy *pathpolicy.PathPolicies - switch ptmode { - case disk.BtrfsPartitioningMode: - // btrfs subvolumes are not supported at build time yet, so we only - // allow / and /boot to be customized when building a btrfs disk (the - // minimal policy) - policy = mountpointMinimalPolicy - default: - policy = mountpointPolicy - } - if err := checkMountpoints(fsCustomizations, policy); err != nil { - return err - } - return nil -} - -// updateFilesystemSizes updates the size of the root filesystem customization -// based on the minRootSize. The new min size whichever is larger between the -// existing size and the minRootSize. If the root filesystem is not already -// configured, a new customization is added. -func updateFilesystemSizes(fsCustomizations []blueprint.FilesystemCustomization, minRootSize uint64) []blueprint.FilesystemCustomization { - updated := make([]blueprint.FilesystemCustomization, len(fsCustomizations), len(fsCustomizations)+1) - hasRoot := false - for idx, fsc := range fsCustomizations { - updated[idx] = fsc - if updated[idx].Mountpoint == "/" { - updated[idx].MinSize = max(updated[idx].MinSize, minRootSize) - hasRoot = true - } - } - - if !hasRoot { - // no root customization found: add it - updated = append(updated, blueprint.FilesystemCustomization{Mountpoint: "/", MinSize: minRootSize}) - } - return updated -} - -// setFSTypes sets the filesystem types for all mountable entities to match the -// selected rootfs type. -// If rootfs is 'btrfs', the function will keep '/boot' to its default. -func setFSTypes(pt *disk.PartitionTable, rootfs string) error { - if rootfs == "" { - return fmt.Errorf("root filesystem type is empty") - } - - return pt.ForEachMountable(func(mnt disk.Mountable, _ []disk.Entity) error { - switch mnt.GetMountpoint() { - case "/boot/efi": - // never change the efi partition's type - return nil - case "/boot": - // change only if we're not doing btrfs - if rootfs == "btrfs" { - return nil - } - fallthrough - default: - switch elem := mnt.(type) { - case *disk.Filesystem: - elem.Type = rootfs - case *disk.BtrfsSubvolume: - // nothing to do - default: - return fmt.Errorf("the mountable disk entity for %q of the base partition table is not an ordinary filesystem but %T", mnt.GetMountpoint(), mnt) - } - return nil - } - }) -} - -func genPartitionTable(c *ManifestConfig, customizations *blueprint.Customizations, rng *rand.Rand) (*disk.PartitionTable, error) { - fsCust := customizations.GetFilesystems() - diskCust, err := customizations.GetPartitioning() - if err != nil { - return nil, fmt.Errorf("error reading disk customizations: %w", err) - } - switch { - // XXX: move into images library - case fsCust != nil && diskCust != nil: - return nil, fmt.Errorf("cannot combine disk and filesystem customizations") - case diskCust != nil: - return genPartitionTableDiskCust(c, diskCust, rng) - default: - return genPartitionTableFsCust(c, fsCust, rng) - } -} - -// calcRequiredDirectorySizes will calculate the minimum sizes for / -// for disk customizations. We need this because with advanced partitioning -// we never grow the rootfs to the size of the disk (unlike the tranditional -// filesystem customizations). -// -// So we need to go over the customizations and ensure the min-size for "/" -// is at least rootfsMinSize. -// -// Note that a custom "/usr" is not supported in image mode so splitting -// rootfsMinSize between / and /usr is not a concern. -func calcRequiredDirectorySizes(distCust *blueprint.DiskCustomization, rootfsMinSize uint64) (map[string]uint64, error) { - // XXX: this has *way* too much low-level knowledge about the - // inner workings of blueprint.DiskCustomizations plus when - // a new type it needs to get added here too, think about - // moving into "images" instead (at least partly) - mounts := map[string]uint64{} - for _, part := range distCust.Partitions { - switch part.Type { - case "", "plain": - mounts[part.Mountpoint] = part.MinSize - case "lvm": - for _, lv := range part.LogicalVolumes { - mounts[lv.Mountpoint] = part.MinSize - } - case "btrfs": - for _, subvol := range part.Subvolumes { - mounts[subvol.Mountpoint] = part.MinSize - } - default: - return nil, fmt.Errorf("unknown disk customization type %q", part.Type) - } - } - // ensure rootfsMinSize is respected - return map[string]uint64{ - "/": max(rootfsMinSize, mounts["/"]), - }, nil -} - -func genPartitionTableDiskCust(c *ManifestConfig, diskCust *blueprint.DiskCustomization, rng *rand.Rand) (*disk.PartitionTable, error) { - if err := diskCust.ValidateLayoutConstraints(); err != nil { - return nil, fmt.Errorf("cannot use disk customization: %w", err) - } - - diskCust.MinSize = max(diskCust.MinSize, c.RootfsMinsize) - - basept, ok := partitionTables[c.Architecture.String()] - if !ok { - return nil, fmt.Errorf("pipelines: no partition tables defined for %s", c.Architecture) - } - defaultFSType, err := disk.NewFSType(c.RootFSType) - if err != nil { - return nil, err - } - requiredMinSizes, err := calcRequiredDirectorySizes(diskCust, c.RootfsMinsize) - if err != nil { - return nil, err - } - partOptions := &disk.CustomPartitionTableOptions{ - PartitionTableType: basept.Type, - // XXX: not setting/defaults will fail to boot with btrfs/lvm - BootMode: platform.BOOT_HYBRID, - DefaultFSType: defaultFSType, - RequiredMinSizes: requiredMinSizes, - Architecture: c.Architecture, - } - return disk.NewCustomPartitionTable(diskCust, partOptions, rng) -} - -func genPartitionTableFsCust(c *ManifestConfig, fsCust []blueprint.FilesystemCustomization, rng *rand.Rand) (*disk.PartitionTable, error) { - basept, ok := partitionTables[c.Architecture.String()] - if !ok { - return nil, fmt.Errorf("pipelines: no partition tables defined for %s", c.Architecture) - } - - partitioningMode := disk.RawPartitioningMode - if c.RootFSType == "btrfs" { - partitioningMode = disk.BtrfsPartitioningMode - } - if err := checkFilesystemCustomizations(fsCust, partitioningMode); err != nil { - return nil, err - } - fsCustomizations := updateFilesystemSizes(fsCust, c.RootfsMinsize) - - pt, err := disk.NewPartitionTable(&basept, fsCustomizations, DEFAULT_SIZE, partitioningMode, c.Architecture, nil, rng) - if err != nil { - return nil, err - } - - if err := setFSTypes(pt, c.RootFSType); err != nil { - return nil, fmt.Errorf("error setting root filesystem type: %w", err) - } - return pt, nil -} - -func manifestForDiskImage(c *ManifestConfig, rng *rand.Rand) (*manifest.Manifest, error) { - if c.Imgref == "" { - return nil, fmt.Errorf("pipeline: no base image defined") - } - containerSource := container.SourceSpec{ - Source: c.Imgref, - Name: c.Imgref, - Local: true, - } - - var customizations *blueprint.Customizations - if c.Config != nil { - customizations = c.Config.Customizations - } - - img := image.NewBootcDiskImage(containerSource) - img.Users = users.UsersFromBP(customizations.GetUsers()) - img.Groups = users.GroupsFromBP(customizations.GetGroups()) - // TODO: get from the bootc container instead of hardcoding it - img.SELinux = "targeted" - - img.KernelOptionsAppend = []string{ - "rw", - // TODO: Drop this as we expect kargs to come from the container image, - // xref https://github.com/CentOS/centos-bootc-layered/blob/main/cloud/usr/lib/bootc/install/05-cloud-kargs.toml - "console=tty0", - "console=ttyS0", - } - - switch c.Architecture { - case arch.ARCH_X86_64: - img.Platform = &platform.X86{ - BasePlatform: platform.BasePlatform{}, - BIOS: true, - } - case arch.ARCH_AARCH64: - img.Platform = &platform.Aarch64{ - UEFIVendor: "fedora", - BasePlatform: platform.BasePlatform{ - QCOW2Compat: "1.1", - }, - } - case arch.ARCH_S390X: - img.Platform = &platform.S390X{ - BasePlatform: platform.BasePlatform{ - QCOW2Compat: "1.1", - }, - Zipl: true, - } - case arch.ARCH_PPC64LE: - img.Platform = &platform.PPC64LE{ - BasePlatform: platform.BasePlatform{ - QCOW2Compat: "1.1", - }, - BIOS: true, - } - } - - if kopts := customizations.GetKernel(); kopts != nil && kopts.Append != "" { - img.KernelOptionsAppend = append(img.KernelOptionsAppend, kopts.Append) - } - - pt, err := genPartitionTable(c, customizations, rng) - if err != nil { - return nil, err - } - img.PartitionTable = pt - - // For the bootc-disk image, the filename is the basename and the extension - // is added automatically for each disk format - img.Filename = "disk" - - mf := manifest.New() - mf.Distro = manifest.DISTRO_FEDORA - runner := &runner.Linux{} - - if err := img.InstantiateManifestFromContainers(&mf, []container.SourceSpec{containerSource}, runner, rng); err != nil { - return nil, err - } - - return &mf, nil -} - -func labelForISO(os *source.OSRelease, arch *arch.Arch) string { - switch os.ID { - case "fedora": - return fmt.Sprintf("Fedora-S-dvd-%s-%s", arch, os.VersionID) - case "centos": - labelTemplate := "CentOS-Stream-%s-BaseOS-%s" - if os.VersionID == "8" { - labelTemplate = "CentOS-Stream-%s-%s-dvd" - } - return fmt.Sprintf(labelTemplate, os.VersionID, arch) - case "rhel": - version := strings.ReplaceAll(os.VersionID, ".", "-") - return fmt.Sprintf("RHEL-%s-BaseOS-%s", version, arch) - default: - return fmt.Sprintf("Container-Installer-%s", arch) - } -} - -func manifestForISO(c *ManifestConfig, rng *rand.Rand) (*manifest.Manifest, error) { - if c.Imgref == "" { - return nil, fmt.Errorf("pipeline: no base image defined") - } - - imageDef, err := distrodef.LoadImageDef(c.DistroDefPaths, c.SourceInfo.OSRelease.ID, c.SourceInfo.OSRelease.VersionID, "anaconda-iso") - if err != nil { - return nil, err - } - - containerSource := container.SourceSpec{ - Source: c.Imgref, - Name: c.Imgref, - Local: true, - } - - // The ref is not needed and will be removed from the ctor later - // in time - img := image.NewAnacondaContainerInstaller(containerSource, "") - img.ContainerRemoveSignatures = true - img.RootfsCompression = "zstd" - - img.Product = c.SourceInfo.OSRelease.Name - img.OSVersion = c.SourceInfo.OSRelease.VersionID - - img.ExtraBasePackages = rpmmd.PackageSet{ - Include: imageDef.Packages, - } - - img.ISOLabel = labelForISO(&c.SourceInfo.OSRelease, &c.Architecture) - - var customizations *blueprint.Customizations - if c.Config != nil { - customizations = c.Config.Customizations - } - img.FIPS = customizations.GetFIPS() - img.Kickstart, err = kickstart.New(customizations) - if err != nil { - return nil, err - } - img.Kickstart.Path = osbuild.KickstartPathOSBuild - if kopts := customizations.GetKernel(); kopts != nil && kopts.Append != "" { - img.Kickstart.KernelOptionsAppend = append(img.Kickstart.KernelOptionsAppend, kopts.Append) - } - img.Kickstart.NetworkOnBoot = true - - instCust, err := customizations.GetInstaller() - if err != nil { - return nil, err - } - if instCust != nil && instCust.Modules != nil { - img.AdditionalAnacondaModules = append(img.AdditionalAnacondaModules, instCust.Modules.Enable...) - img.DisabledAnacondaModules = append(img.DisabledAnacondaModules, instCust.Modules.Disable...) - } - img.AdditionalAnacondaModules = append(img.AdditionalAnacondaModules, - anaconda.ModuleUsers, - anaconda.ModuleServices, - anaconda.ModuleSecurity, - ) - - img.Kickstart.OSTree = &kickstart.OSTree{ - OSName: "default", - } - // use lorax-templates-rhel if the source distro is not Fedora with the exception of Fedora ELN - img.UseRHELLoraxTemplates = - c.SourceInfo.OSRelease.ID != "fedora" || c.SourceInfo.OSRelease.VersionID == "eln" - - switch c.Architecture { - case arch.ARCH_X86_64: - img.Platform = &platform.X86{ - BasePlatform: platform.BasePlatform{ - ImageFormat: platform.FORMAT_ISO, - }, - BIOS: true, - UEFIVendor: c.SourceInfo.UEFIVendor, - } - case arch.ARCH_AARCH64: - // aarch64 always uses UEFI, so let's enforce the vendor - if c.SourceInfo.UEFIVendor == "" { - return nil, fmt.Errorf("UEFI vendor must be set for aarch64 ISO") - } - img.Platform = &platform.Aarch64{ - BasePlatform: platform.BasePlatform{ - ImageFormat: platform.FORMAT_ISO, - }, - UEFIVendor: c.SourceInfo.UEFIVendor, - } - case arch.ARCH_S390X: - img.Platform = &platform.S390X{ - Zipl: true, - BasePlatform: platform.BasePlatform{ - ImageFormat: platform.FORMAT_ISO, - }, - } - case arch.ARCH_PPC64LE: - img.Platform = &platform.PPC64LE{ - BIOS: true, - BasePlatform: platform.BasePlatform{ - ImageFormat: platform.FORMAT_ISO, - }, - } - default: - return nil, fmt.Errorf("unsupported architecture %v", c.Architecture) - } - // see https://github.com/osbuild/bootc-image-builder/issues/733 - img.RootfsType = manifest.SquashfsRootfs - img.Filename = "install.iso" - - mf := manifest.New() - - foundDistro, foundRunner, err := getDistroAndRunner(c.SourceInfo.OSRelease) - if err != nil { - return nil, fmt.Errorf("failed to infer distro and runner: %w", err) - } - mf.Distro = foundDistro - - _, err = img.InstantiateManifest(&mf, nil, foundRunner, rng) - return &mf, err -} - -func getDistroAndRunner(osRelease source.OSRelease) (manifest.Distro, runner.Runner, error) { - switch osRelease.ID { - case "fedora": - version, err := strconv.ParseUint(osRelease.VersionID, 10, 64) - if err != nil { - return manifest.DISTRO_NULL, nil, fmt.Errorf("cannot parse Fedora version (%s): %w", osRelease.VersionID, err) - } - - return manifest.DISTRO_FEDORA, &runner.Fedora{ - Version: version, - }, nil - case "centos": - version, err := strconv.ParseUint(osRelease.VersionID, 10, 64) - if err != nil { - return manifest.DISTRO_NULL, nil, fmt.Errorf("cannot parse CentOS version (%s): %w", osRelease.VersionID, err) - } - r := &runner.CentOS{ - Version: version, - } - switch version { - case 9: - return manifest.DISTRO_EL9, r, nil - case 10: - return manifest.DISTRO_EL10, r, nil - default: - logrus.Warnf("Unknown CentOS version %d, using default distro for manifest generation", version) - return manifest.DISTRO_NULL, r, nil - } - - case "rhel": - versionParts := strings.Split(osRelease.VersionID, ".") - if len(versionParts) != 2 { - return manifest.DISTRO_NULL, nil, fmt.Errorf("invalid RHEL version format: %s", osRelease.VersionID) - } - major, err := strconv.ParseUint(versionParts[0], 10, 64) - if err != nil { - return manifest.DISTRO_NULL, nil, fmt.Errorf("cannot parse RHEL major version (%s): %w", versionParts[0], err) - } - minor, err := strconv.ParseUint(versionParts[1], 10, 64) - if err != nil { - return manifest.DISTRO_NULL, nil, fmt.Errorf("cannot parse RHEL minor version (%s): %w", versionParts[1], err) - } - r := &runner.RHEL{ - Major: major, - Minor: minor, - } - switch major { - case 9: - return manifest.DISTRO_EL9, r, nil - case 10: - return manifest.DISTRO_EL10, r, nil - default: - logrus.Warnf("Unknown RHEL version %d, using default distro for manifest generation", major) - return manifest.DISTRO_NULL, r, nil - } - } - - logrus.Warnf("Unknown distro %s, using default runner", osRelease.ID) - return manifest.DISTRO_NULL, &runner.Linux{}, nil -} - -func createRand() *rand.Rand { - seed, err := cryptorand.Int(cryptorand.Reader, big.NewInt(math.MaxInt64)) - if err != nil { - panic("Cannot generate an RNG seed.") - } - - // math/rand is good enough in this case - /* #nosec G404 */ - return rand.New(rand.NewSource(seed.Int64())) -} diff --git a/bib/cmd/bootc-image-builder/image_test.go b/bib/cmd/bootc-image-builder/image_test.go index b92255cb0..a204b1811 100644 --- a/bib/cmd/bootc-image-builder/image_test.go +++ b/bib/cmd/bootc-image-builder/image_test.go @@ -7,15 +7,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/osbuild/images/pkg/arch" - "github.com/osbuild/images/pkg/blueprint" - "github.com/osbuild/images/pkg/datasizes" - "github.com/osbuild/images/pkg/disk" "github.com/osbuild/images/pkg/manifest" "github.com/osbuild/images/pkg/runner" bib "github.com/osbuild/bootc-image-builder/bib/cmd/bootc-image-builder" - "github.com/osbuild/bootc-image-builder/bib/internal/source" + "github.com/osbuild/images/pkg/bib/osinfo" ) func TestGetDistroAndRunner(t *testing.T) { @@ -45,7 +41,7 @@ func TestGetDistroAndRunner(t *testing.T) { for _, c := range cases { t.Run(fmt.Sprintf("%s-%s", c.id, c.versionID), func(t *testing.T) { - osRelease := source.OSRelease{ + osRelease := osinfo.OSRelease{ ID: c.id, VersionID: c.versionID, } @@ -60,623 +56,3 @@ func TestGetDistroAndRunner(t *testing.T) { }) } } - -func TestCheckFilesystemCustomizationsValidates(t *testing.T) { - for _, tc := range []struct { - fsCust []blueprint.FilesystemCustomization - ptmode disk.PartitioningMode - expectedErr string - }{ - // happy - { - fsCust: []blueprint.FilesystemCustomization{}, - expectedErr: "", - }, - { - fsCust: []blueprint.FilesystemCustomization{}, - ptmode: disk.BtrfsPartitioningMode, - expectedErr: "", - }, - { - fsCust: []blueprint.FilesystemCustomization{ - {Mountpoint: "/"}, {Mountpoint: "/boot"}, - }, - ptmode: disk.RawPartitioningMode, - expectedErr: "", - }, - { - fsCust: []blueprint.FilesystemCustomization{ - {Mountpoint: "/"}, {Mountpoint: "/boot"}, - }, - ptmode: disk.BtrfsPartitioningMode, - expectedErr: "", - }, - { - fsCust: []blueprint.FilesystemCustomization{ - {Mountpoint: "/"}, - {Mountpoint: "/boot"}, - {Mountpoint: "/var/log"}, - {Mountpoint: "/var/data"}, - }, - expectedErr: "", - }, - // sad - { - fsCust: []blueprint.FilesystemCustomization{ - {Mountpoint: "/"}, - {Mountpoint: "/ostree"}, - }, - ptmode: disk.RawPartitioningMode, - expectedErr: "The following errors occurred while validating custom mountpoints:\npath \"/ostree\" is not allowed", - }, - { - fsCust: []blueprint.FilesystemCustomization{ - {Mountpoint: "/"}, - {Mountpoint: "/var"}, - }, - ptmode: disk.RawPartitioningMode, - expectedErr: "The following errors occurred while validating custom mountpoints:\npath \"/var\" is not allowed", - }, - { - fsCust: []blueprint.FilesystemCustomization{ - {Mountpoint: "/"}, - {Mountpoint: "/var/data"}, - }, - ptmode: disk.BtrfsPartitioningMode, - expectedErr: "The following errors occurred while validating custom mountpoints:\npath \"/var/data\" is not allowed", - }, - { - fsCust: []blueprint.FilesystemCustomization{ - {Mountpoint: "/"}, - {Mountpoint: "/boot/"}, - }, - ptmode: disk.BtrfsPartitioningMode, - expectedErr: "The following errors occurred while validating custom mountpoints:\npath \"/boot/\" must be canonical", - }, - { - fsCust: []blueprint.FilesystemCustomization{ - {Mountpoint: "/"}, - {Mountpoint: "/boot/"}, - {Mountpoint: "/opt"}, - }, - ptmode: disk.BtrfsPartitioningMode, - expectedErr: "The following errors occurred while validating custom mountpoints:\npath \"/boot/\" must be canonical\npath \"/opt\" is not allowed", - }, - } { - if tc.expectedErr == "" { - assert.NoError(t, bib.CheckFilesystemCustomizations(tc.fsCust, tc.ptmode)) - } else { - assert.ErrorContains(t, bib.CheckFilesystemCustomizations(tc.fsCust, tc.ptmode), tc.expectedErr) - } - } -} - -func TestLocalMountpointPolicy(t *testing.T) { - // extended testing of the general mountpoint policy (non-minimal) - type testCase struct { - path string - allowed bool - } - - testCases := []testCase{ - // existing mountpoints / and /boot are fine for sizing - {"/", true}, - {"/boot", true}, - - // root mountpoints are not allowed - {"/data", false}, - {"/opt", false}, - {"/stuff", false}, - {"/usr", false}, - - // /var explicitly is not allowed - {"/var", false}, - - // subdirs of /boot are not allowed - {"/boot/stuff", false}, - {"/boot/loader", false}, - - // /var subdirectories are allowed - {"/var/data", true}, - {"/var/scratch", true}, - {"/var/log", true}, - {"/var/opt", true}, - {"/var/opt/application", true}, - - // but not these - {"/var/home", false}, - {"/var/lock", false}, // symlink to ../run/lock which is on tmpfs - {"/var/mail", false}, // symlink to spool/mail - {"/var/mnt", false}, - {"/var/roothome", false}, - {"/var/run", false}, // symlink to ../run which is on tmpfs - {"/var/srv", false}, - {"/var/usrlocal", false}, - - // nor their subdirs - {"/var/run/subrun", false}, - {"/var/srv/test", false}, - {"/var/home/user", false}, - {"/var/usrlocal/bin", false}, - } - - for _, tc := range testCases { - t.Run(tc.path, func(t *testing.T) { - err := bib.CheckFilesystemCustomizations([]blueprint.FilesystemCustomization{{Mountpoint: tc.path}}, disk.RawPartitioningMode) - if err != nil && tc.allowed { - t.Errorf("expected %s to be allowed, but got error: %v", tc.path, err) - } else if err == nil && !tc.allowed { - t.Errorf("expected %s to be denied, but got no error", tc.path) - } - }) - } -} - -func TestBasePartitionTablesHaveRoot(t *testing.T) { - // make sure that all base partition tables have at least a root partition defined - for arch, pt := range bib.PartitionTables { - rootMountable := pt.FindMountable("/") - if rootMountable == nil { - t.Errorf("partition table %q does not define a root filesystem", arch) - } - _, isFS := rootMountable.(*disk.Filesystem) - if !isFS { - t.Errorf("root mountable for %q is not an ordinary filesystem", arch) - } - } - -} - -func TestUpdateFilesystemSizes(t *testing.T) { - type testCase struct { - customizations []blueprint.FilesystemCustomization - minRootSize uint64 - expected []blueprint.FilesystemCustomization - } - - testCases := map[string]testCase{ - "simple": { - customizations: nil, - minRootSize: 999, - expected: []blueprint.FilesystemCustomization{ - { - Mountpoint: "/", - MinSize: 999, - }, - }, - }, - "container-is-larger": { - customizations: []blueprint.FilesystemCustomization{ - { - Mountpoint: "/", - MinSize: 10, - }, - }, - minRootSize: 999, - expected: []blueprint.FilesystemCustomization{ - { - Mountpoint: "/", - MinSize: 999, - }, - }, - }, - "container-is-smaller": { - customizations: []blueprint.FilesystemCustomization{ - { - Mountpoint: "/", - MinSize: 1000, - }, - }, - minRootSize: 892, - expected: []blueprint.FilesystemCustomization{ - { - Mountpoint: "/", - MinSize: 1000, - }, - }, - }, - "customizations-noroot": { - customizations: []blueprint.FilesystemCustomization{ - { - Mountpoint: "/var/data", - MinSize: 1_000_000, - }, - }, - minRootSize: 9000, - expected: []blueprint.FilesystemCustomization{ - { - Mountpoint: "/var/data", - MinSize: 1_000_000, - }, - { - Mountpoint: "/", - MinSize: 9000, - }, - }, - }, - "customizations-withroot-smallcontainer": { - customizations: []blueprint.FilesystemCustomization{ - { - Mountpoint: "/var/data", - MinSize: 1_000_000, - }, - { - Mountpoint: "/", - MinSize: 2_000_000, - }, - }, - minRootSize: 9000, - expected: []blueprint.FilesystemCustomization{ - { - Mountpoint: "/var/data", - MinSize: 1_000_000, - }, - { - Mountpoint: "/", - MinSize: 2_000_000, - }, - }, - }, - "customizations-withroot-largecontainer": { - customizations: []blueprint.FilesystemCustomization{ - { - Mountpoint: "/var/data", - MinSize: 1_000_000, - }, - { - Mountpoint: "/", - MinSize: 2_000_000, - }, - }, - minRootSize: 9_000_000, - expected: []blueprint.FilesystemCustomization{ - { - Mountpoint: "/var/data", - MinSize: 1_000_000, - }, - { - Mountpoint: "/", - MinSize: 9_000_000, - }, - }, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - assert.ElementsMatch(t, bib.UpdateFilesystemSizes(tc.customizations, tc.minRootSize), tc.expected) - }) - } - -} - -func findMountableSizeableFor(pt *disk.PartitionTable, needle string) (disk.Mountable, disk.Sizeable) { - var foundMnt disk.Mountable - var foundParent disk.Sizeable - err := pt.ForEachMountable(func(mnt disk.Mountable, path []disk.Entity) error { - if mnt.GetMountpoint() == needle { - foundMnt = mnt - for idx := len(path) - 1; idx >= 0; idx-- { - if sz, ok := path[idx].(disk.Sizeable); ok { - foundParent = sz - break - } - } - } - return nil - }) - if err != nil { - panic(err) - } - return foundMnt, foundParent -} - -func TestGenPartitionTableSetsRootfsForAllFilesystemsXFS(t *testing.T) { - rng := bib.CreateRand() - - cnf := &bib.ManifestConfig{ - Architecture: arch.FromString("amd64"), - RootFSType: "xfs", - } - cus := &blueprint.Customizations{ - Filesystem: []blueprint.FilesystemCustomization{ - {Mountpoint: "/var/data", MinSize: 2_000_000}, - {Mountpoint: "/var/stuff", MinSize: 10_000_000}, - }, - } - pt, err := bib.GenPartitionTable(cnf, cus, rng) - assert.NoError(t, err) - - for _, mntPoint := range []string{"/", "/boot", "/var/data"} { - mnt, _ := findMountableSizeableFor(pt, mntPoint) - assert.Equal(t, "xfs", mnt.GetFSType()) - } - _, parent := findMountableSizeableFor(pt, "/var/data") - assert.True(t, parent.GetSize() >= 2_000_000) - - _, parent = findMountableSizeableFor(pt, "/var/stuff") - assert.True(t, parent.GetSize() >= 10_000_000) - - // ESP is always vfat - mnt, _ := findMountableSizeableFor(pt, "/boot/efi") - assert.Equal(t, "vfat", mnt.GetFSType()) -} - -func TestGenPartitionTableSetsRootfsForAllFilesystemsBtrfs(t *testing.T) { - rng := bib.CreateRand() - - cnf := &bib.ManifestConfig{ - Architecture: arch.FromString("amd64"), - RootFSType: "btrfs", - } - cus := &blueprint.Customizations{} - pt, err := bib.GenPartitionTable(cnf, cus, rng) - assert.NoError(t, err) - - mnt, _ := findMountableSizeableFor(pt, "/") - assert.Equal(t, "btrfs", mnt.GetFSType()) - - // btrfs has a default (ext4) /boot - mnt, _ = findMountableSizeableFor(pt, "/boot") - assert.Equal(t, "ext4", mnt.GetFSType()) - - // ESP is always vfat - mnt, _ = findMountableSizeableFor(pt, "/boot/efi") - assert.Equal(t, "vfat", mnt.GetFSType()) -} - -func TestGenPartitionTableDiskCustomizationRunsValidateLayoutConstraints(t *testing.T) { - rng := bib.CreateRand() - - cnf := &bib.ManifestConfig{ - Architecture: arch.FromString("amd64"), - RootFSType: "xfs", - } - cus := &blueprint.Customizations{ - Disk: &blueprint.DiskCustomization{ - Partitions: []blueprint.PartitionCustomization{ - { - Type: "lvm", - VGCustomization: blueprint.VGCustomization{}, - }, - { - Type: "lvm", - VGCustomization: blueprint.VGCustomization{}, - }, - }, - }, - } - _, err := bib.GenPartitionTable(cnf, cus, rng) - assert.EqualError(t, err, "cannot use disk customization: multiple LVM volume groups are not yet supported") -} - -func TestGenPartitionTableDiskCustomizationUnknownTypesError(t *testing.T) { - cus := &blueprint.Customizations{ - Disk: &blueprint.DiskCustomization{ - Partitions: []blueprint.PartitionCustomization{ - { - Type: "rando", - }, - }, - }, - } - _, err := bib.CalcRequiredDirectorySizes(cus.Disk, 5*datasizes.GiB) - assert.EqualError(t, err, `unknown disk customization type "rando"`) -} - -func TestGenPartitionTableDiskCustomizationSizes(t *testing.T) { - rng := bib.CreateRand() - - for _, tc := range []struct { - name string - rootfsMinSize uint64 - partitions []blueprint.PartitionCustomization - expectedMinRootSize uint64 - }{ - { - "empty disk customizaton, root expands to rootfsMinsize", - 2 * datasizes.GiB, - nil, - 2 * datasizes.GiB, - }, - // plain - { - "plain, no root minsize, expands to rootfsMinSize", - 5 * datasizes.GiB, - []blueprint.PartitionCustomization{ - { - MinSize: 10 * datasizes.GiB, - FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ - Mountpoint: "/var", - FSType: "xfs", - }, - }, - }, - 5 * datasizes.GiB, - }, - { - "plain, small root minsize, expands to rootfsMnSize", - 5 * datasizes.GiB, - []blueprint.PartitionCustomization{ - { - MinSize: 1 * datasizes.GiB, - FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ - Mountpoint: "/", - FSType: "xfs", - }, - }, - }, - 5 * datasizes.GiB, - }, - { - "plain, big root minsize", - 5 * datasizes.GiB, - []blueprint.PartitionCustomization{ - { - MinSize: 10 * datasizes.GiB, - FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ - Mountpoint: "/", - FSType: "xfs", - }, - }, - }, - 10 * datasizes.GiB, - }, - // btrfs - { - "btrfs, no root minsize, expands to rootfsMinSize", - 5 * datasizes.GiB, - []blueprint.PartitionCustomization{ - { - Type: "btrfs", - MinSize: 10 * datasizes.GiB, - BtrfsVolumeCustomization: blueprint.BtrfsVolumeCustomization{ - Subvolumes: []blueprint.BtrfsSubvolumeCustomization{ - { - Mountpoint: "/var", - Name: "varvol", - }, - }, - }, - }, - }, - 5 * datasizes.GiB, - }, - { - "btrfs, small root minsize, expands to rootfsMnSize", - 5 * datasizes.GiB, - []blueprint.PartitionCustomization{ - { - Type: "btrfs", - MinSize: 1 * datasizes.GiB, - BtrfsVolumeCustomization: blueprint.BtrfsVolumeCustomization{ - Subvolumes: []blueprint.BtrfsSubvolumeCustomization{ - { - Mountpoint: "/", - Name: "rootvol", - }, - }, - }, - }, - }, - 5 * datasizes.GiB, - }, - { - "btrfs, big root minsize", - 5 * datasizes.GiB, - []blueprint.PartitionCustomization{ - { - Type: "btrfs", - MinSize: 10 * datasizes.GiB, - BtrfsVolumeCustomization: blueprint.BtrfsVolumeCustomization{ - Subvolumes: []blueprint.BtrfsSubvolumeCustomization{ - { - Mountpoint: "/", - Name: "rootvol", - }, - }, - }, - }, - }, - 10 * datasizes.GiB, - }, - // lvm - { - "lvm, no root minsize, expands to rootfsMinSize", - 5 * datasizes.GiB, - []blueprint.PartitionCustomization{ - { - Type: "lvm", - MinSize: 10 * datasizes.GiB, - VGCustomization: blueprint.VGCustomization{ - LogicalVolumes: []blueprint.LVCustomization{ - { - MinSize: 10 * datasizes.GiB, - FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ - Mountpoint: "/var", - FSType: "xfs", - }, - }, - }, - }, - }, - }, - 5 * datasizes.GiB, - }, - { - "lvm, small root minsize, expands to rootfsMnSize", - 5 * datasizes.GiB, - []blueprint.PartitionCustomization{ - { - Type: "lvm", - MinSize: 1 * datasizes.GiB, - VGCustomization: blueprint.VGCustomization{ - LogicalVolumes: []blueprint.LVCustomization{ - { - MinSize: 1 * datasizes.GiB, - FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ - Mountpoint: "/", - FSType: "xfs", - }, - }, - }, - }, - }, - }, - 5 * datasizes.GiB, - }, - { - "lvm, big root minsize", - 5 * datasizes.GiB, - []blueprint.PartitionCustomization{ - { - Type: "lvm", - MinSize: 10 * datasizes.GiB, - VGCustomization: blueprint.VGCustomization{ - LogicalVolumes: []blueprint.LVCustomization{ - { - MinSize: 10 * datasizes.GiB, - FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ - Mountpoint: "/", - FSType: "xfs", - }, - }, - }, - }, - }, - }, - 10 * datasizes.GiB, - }, - } { - t.Run(tc.name, func(t *testing.T) { - cnf := &bib.ManifestConfig{ - Architecture: arch.FromString("amd64"), - RootFSType: "xfs", - RootfsMinsize: tc.rootfsMinSize, - } - cus := &blueprint.Customizations{ - Disk: &blueprint.DiskCustomization{ - Partitions: tc.partitions, - }, - } - pt, err := bib.GenPartitionTable(cnf, cus, rng) - assert.NoError(t, err) - - var rootSize uint64 - err = pt.ForEachMountable(func(mnt disk.Mountable, path []disk.Entity) error { - if mnt.GetMountpoint() == "/" { - for idx := len(path) - 1; idx >= 0; idx-- { - if parent, ok := path[idx].(disk.Sizeable); ok { - rootSize = parent.GetSize() - break - } - } - } - return nil - }) - assert.NoError(t, err) - // expected size is within a reasonable limit - assert.True(t, rootSize >= tc.expectedMinRootSize && rootSize < tc.expectedMinRootSize+5*datasizes.MiB) - }) - } -} diff --git a/bib/cmd/bootc-image-builder/legacy_iso.go b/bib/cmd/bootc-image-builder/legacy_iso.go new file mode 100644 index 000000000..6f5eeae1e --- /dev/null +++ b/bib/cmd/bootc-image-builder/legacy_iso.go @@ -0,0 +1,454 @@ +package main + +import ( + "fmt" + "math/rand" + "slices" + "strconv" + "strings" + + "github.com/osbuild/blueprint/pkg/blueprint" + "github.com/osbuild/images/pkg/arch" + "github.com/osbuild/images/pkg/bib/osinfo" + "github.com/osbuild/images/pkg/container" + "github.com/osbuild/images/pkg/customizations/anaconda" + "github.com/osbuild/images/pkg/customizations/kickstart" + "github.com/osbuild/images/pkg/depsolvednf" + "github.com/osbuild/images/pkg/disk" + "github.com/osbuild/images/pkg/image" + "github.com/osbuild/images/pkg/manifest" + "github.com/osbuild/images/pkg/osbuild" + "github.com/osbuild/images/pkg/platform" + "github.com/osbuild/images/pkg/rpmmd" + "github.com/osbuild/images/pkg/runner" + "github.com/sirupsen/logrus" + + podman_container "github.com/osbuild/images/pkg/bootc" + + "github.com/osbuild/bootc-image-builder/bib/internal/distrodef" +) + +// all possible locations for the bib's distro definitions +// ./data/defs and ./bib/data/defs are for development +// /usr/share/bootc-image-builder/defs is for the production, containerized version +var distroDefPaths = []string{ + "./data/defs", + "./bib/data/defs", + "/usr/share/bootc-image-builder/defs", +} + +type ManifestConfig struct { + // OCI image path (without the transport, that is always docker://) + Imgref string + BuildImgref string + + // Build config + Config *blueprint.Blueprint + + // CPU architecture of the image + Architecture arch.Arch + + // Paths to the directory with the distro definitions + DistroDefPaths []string + + // Extracted information about the source container image + SourceInfo *osinfo.Info + BuildSourceInfo *osinfo.Info + + // RootFSType specifies the filesystem type for the root partition + RootFSType string + + // use librepo ad the rpm downlaod backend + UseLibrepo bool +} + +func manifestFromCobraForLegacyISO(imgref, buildImgref, imgTypeStr, rootFs, rpmCacheRoot string, config *blueprint.Blueprint, useLibrepo bool, cntArch arch.Arch) ([]byte, *mTLSConfig, error) { + container, err := podman_container.NewContainer(imgref) + if err != nil { + return nil, nil, err + } + defer func() { + if err := container.Stop(); err != nil { + logrus.Warnf("error stopping container: %v", err) + } + }() + + var rootfsType string + if rootFs != "" { + rootfsType = rootFs + } else { + rootfsType, err = container.DefaultRootfsType() + if err != nil { + return nil, nil, fmt.Errorf("cannot get rootfs type for container: %w", err) + } + if rootfsType == "" { + return nil, nil, fmt.Errorf(`no default root filesystem type specified in container, please use "--rootfs" to set manually`) + } + } + + // Gather some data from the containers distro + sourceinfo, err := osinfo.Load(container.Root()) + if err != nil { + return nil, nil, err + } + + buildContainer := container + buildSourceinfo := sourceinfo + startedBuildContainer := false + defer func() { + if startedBuildContainer { + if err := buildContainer.Stop(); err != nil { + logrus.Warnf("error stopping container: %v", err) + } + } + }() + + if buildImgref != "" { + buildContainer, err = podman_container.NewContainer(buildImgref) + if err != nil { + return nil, nil, err + } + startedBuildContainer = true + + // Gather some data from the containers distro + buildSourceinfo, err = osinfo.Load(buildContainer.Root()) + if err != nil { + return nil, nil, err + } + } else { + buildImgref = imgref + } + + // This is needed just for RHEL and RHSM in most cases, but let's run it every time in case + // the image has some non-standard dnf plugins. + if err := buildContainer.InitDNF(); err != nil { + return nil, nil, err + } + solver, err := buildContainer.NewContainerSolver(rpmCacheRoot, cntArch, sourceinfo) + if err != nil { + return nil, nil, err + } + + manifestConfig := &ManifestConfig{ + Architecture: cntArch, + Config: config, + Imgref: imgref, + BuildImgref: buildImgref, + DistroDefPaths: distroDefPaths, + SourceInfo: sourceinfo, + BuildSourceInfo: buildSourceinfo, + RootFSType: rootfsType, + UseLibrepo: useLibrepo, + } + + manifest, repos, err := makeISOManifest(manifestConfig, solver, rpmCacheRoot) + if err != nil { + return nil, nil, err + } + + mTLS, err := extractTLSKeys(repos) + if err != nil { + return nil, nil, err + } + + return manifest, mTLS, nil +} + +func makeISOManifest(c *ManifestConfig, solver *depsolvednf.Solver, cacheRoot string) (manifest.OSBuildManifest, map[string][]rpmmd.RepoConfig, error) { + rng := createRand() + mani, err := manifestForISO(c, rng) + if err != nil { + return nil, nil, fmt.Errorf("cannot get manifest: %w", err) + } + + // depsolve packages + depsolvedSets := make(map[string]depsolvednf.DepsolveResult) + depsolvedRepos := make(map[string][]rpmmd.RepoConfig) + pkgSetChains, err := mani.GetPackageSetChains() + if err != nil { + return nil, nil, err + } + for name, pkgSet := range pkgSetChains { + res, err := solver.Depsolve(pkgSet, 0) + if err != nil { + return nil, nil, fmt.Errorf("cannot depsolve: %w", err) + } + depsolvedSets[name] = *res + depsolvedRepos[name] = res.Repos + } + + // Resolve container - the normal case is that host and target + // architecture are the same. However it is possible to build + // cross-arch images by using qemu-user. This will run everything + // (including the build-root) with the target arch then, it + // is fast enough (given that it's mostly I/O and all I/O is + // run naively via syscall translation) + + // XXX: should NewResolver() take "arch.Arch"? + resolver := container.NewResolver(c.Architecture.String()) + + containerSpecs := make(map[string][]container.Spec) + for plName, sourceSpecs := range mani.GetContainerSourceSpecs() { + for _, c := range sourceSpecs { + resolver.Add(c) + } + specs, err := resolver.Finish() + if err != nil { + return nil, nil, fmt.Errorf("cannot resolve containers: %w", err) + } + for _, spec := range specs { + if spec.Arch != c.Architecture { + return nil, nil, fmt.Errorf("image found is for unexpected architecture %q (expected %q), if that is intentional, please make sure --target-arch matches", spec.Arch, c.Architecture) + } + } + containerSpecs[plName] = specs + } + + var opts manifest.SerializeOptions + if c.UseLibrepo { + opts.RpmDownloader = osbuild.RpmDownloaderLibrepo + } + mf, err := mani.Serialize(depsolvedSets, containerSpecs, nil, &opts) + if err != nil { + return nil, nil, fmt.Errorf("[ERROR] manifest serialization failed: %s", err.Error()) + } + return mf, depsolvedRepos, nil +} + +func labelForISO(os *osinfo.OSRelease, arch *arch.Arch) string { + switch os.ID { + case "fedora": + return fmt.Sprintf("Fedora-S-dvd-%s-%s", arch, os.VersionID) + case "centos": + labelTemplate := "CentOS-Stream-%s-BaseOS-%s" + if os.VersionID == "8" { + labelTemplate = "CentOS-Stream-%s-%s-dvd" + } + return fmt.Sprintf(labelTemplate, os.VersionID, arch) + case "rhel": + version := strings.ReplaceAll(os.VersionID, ".", "-") + return fmt.Sprintf("RHEL-%s-BaseOS-%s", version, arch) + default: + return fmt.Sprintf("Container-Installer-%s", arch) + } +} + +// from:https://github.com/osbuild/images/blob/v0.207.0/data/distrodefs/rhel-10/imagetypes.yaml#L169 +var loraxRhelTemplates = []manifest.InstallerLoraxTemplate{ + manifest.InstallerLoraxTemplate{Path: "80-rhel/runtime-postinstall.tmpl"}, + manifest.InstallerLoraxTemplate{Path: "80-rhel/runtime-cleanup.tmpl", AfterDracut: true}, +} + +// from:https://github.com/osbuild/images/blob/v0.207.0/data/distrodefs/fedora/imagetypes.yaml#L408 +var loraxFedoraTemplates = []manifest.InstallerLoraxTemplate{ + manifest.InstallerLoraxTemplate{Path: "99-generic/runtime-postinstall.tmpl"}, + manifest.InstallerLoraxTemplate{Path: "99-generic/runtime-cleanup.tmpl", AfterDracut: true}, +} + +func loraxTemplates(si osinfo.OSRelease) []manifest.InstallerLoraxTemplate { + switch { + case si.ID == "rhel" || slices.Contains(si.IDLike, "rhel") || si.VersionID == "eln": + return loraxRhelTemplates + default: + return loraxFedoraTemplates + } +} + +func loraxTemplatePackage(si osinfo.OSRelease) string { + switch { + case si.ID == "rhel" || slices.Contains(si.IDLike, "rhel") || si.VersionID == "eln": + return "lorax-templates-rhel" + default: + return "lorax-templates-generic" + } +} + +func manifestForISO(c *ManifestConfig, rng *rand.Rand) (*manifest.Manifest, error) { + if c.Imgref == "" { + return nil, fmt.Errorf("pipeline: no base image defined") + } + + imageDef, err := distrodef.LoadImageDef(c.DistroDefPaths, c.SourceInfo.OSRelease.ID, c.SourceInfo.OSRelease.VersionID, "anaconda-iso") + if err != nil { + return nil, err + } + + containerSource := container.SourceSpec{ + Source: c.Imgref, + Name: c.Imgref, + Local: true, + } + + platform := &platform.Data{ + Arch: c.Architecture, + ImageFormat: platform.FORMAT_ISO, + UEFIVendor: c.SourceInfo.UEFIVendor, + } + switch c.Architecture { + case arch.ARCH_X86_64: + platform.BIOSPlatform = "i386-pc" + case arch.ARCH_AARCH64: + // aarch64 always uses UEFI, so let's enforce the vendor + if c.SourceInfo.UEFIVendor == "" { + return nil, fmt.Errorf("UEFI vendor must be set for aarch64 ISO") + } + case arch.ARCH_S390X: + platform.ZiplSupport = true + case arch.ARCH_PPC64LE: + platform.BIOSPlatform = "powerpc-ieee1275" + case arch.ARCH_RISCV64: + // nothing special needed + default: + return nil, fmt.Errorf("unsupported architecture %v", c.Architecture) + } + filename := "install.iso" + + // The ref is not needed and will be removed from the ctor later + // in time + img := image.NewAnacondaContainerInstallerLegacy(platform, filename, containerSource) + img.ContainerRemoveSignatures = true + img.RootfsCompression = "zstd" + + if c.Architecture == arch.ARCH_X86_64 { + img.ISOCustomizations.BootType = manifest.Grub2ISOBoot + } + + img.InstallerCustomizations.Product = c.SourceInfo.OSRelease.Name + img.InstallerCustomizations.OSVersion = c.SourceInfo.OSRelease.VersionID + + img.ExtraBasePackages = rpmmd.PackageSet{ + Include: imageDef.Packages, + } + + var customizations *blueprint.Customizations + if c.Config != nil { + customizations = c.Config.Customizations + } + + isoCust, err := customizations.GetISO() + if err != nil { + return nil, err + } + + if isoCust != nil && isoCust.VolumeID != "" { + img.ISOCustomizations.Label = isoCust.VolumeID + } else { + img.ISOCustomizations.Label = labelForISO(&c.SourceInfo.OSRelease, &c.Architecture) + } + img.InstallerCustomizations.FIPS = customizations.GetFIPS() + img.Kickstart, err = kickstart.New(customizations) + if err != nil { + return nil, err + } + img.Kickstart.Path = osbuild.KickstartPathOSBuild + if kopts := customizations.GetKernel(); kopts != nil && kopts.Append != "" { + img.Kickstart.KernelOptionsAppend = append(img.Kickstart.KernelOptionsAppend, kopts.Append) + } + img.Kickstart.NetworkOnBoot = true + + instCust, err := customizations.GetInstaller() + if err != nil { + return nil, err + } + if instCust != nil && instCust.Modules != nil { + img.InstallerCustomizations.EnabledAnacondaModules = append(img.InstallerCustomizations.EnabledAnacondaModules, instCust.Modules.Enable...) + img.InstallerCustomizations.DisabledAnacondaModules = append(img.InstallerCustomizations.DisabledAnacondaModules, instCust.Modules.Disable...) + } + img.InstallerCustomizations.EnabledAnacondaModules = append(img.InstallerCustomizations.EnabledAnacondaModules, + anaconda.ModuleUsers, + anaconda.ModuleServices, + anaconda.ModuleSecurity, + // XXX: get from the imagedefs + anaconda.ModuleNetwork, + anaconda.ModulePayloads, + anaconda.ModuleRuntime, + anaconda.ModuleStorage, + ) + + img.Kickstart.OSTree = &kickstart.OSTree{ + OSName: "default", + } + img.InstallerCustomizations.LoraxTemplates = loraxTemplates(c.SourceInfo.OSRelease) + img.InstallerCustomizations.LoraxTemplatePackage = loraxTemplatePackage(c.SourceInfo.OSRelease) + + // see https://github.com/osbuild/bootc-image-builder/issues/733 + img.ISOCustomizations.RootfsType = manifest.SquashfsRootfs + + installRootfsType, err := disk.NewFSType(c.RootFSType) + if err != nil { + return nil, err + } + img.InstallRootfsType = installRootfsType + + mf := manifest.New() + + foundDistro, foundRunner, err := getDistroAndRunner(c.SourceInfo.OSRelease) + if err != nil { + return nil, fmt.Errorf("failed to infer distro and runner: %w", err) + } + mf.Distro = foundDistro + + _, err = img.InstantiateManifest(&mf, nil, foundRunner, rng) + return &mf, err +} + +func getDistroAndRunner(osRelease osinfo.OSRelease) (manifest.Distro, runner.Runner, error) { + switch osRelease.ID { + case "fedora": + version, err := strconv.ParseUint(osRelease.VersionID, 10, 64) + if err != nil { + return manifest.DISTRO_NULL, nil, fmt.Errorf("cannot parse Fedora version (%s): %w", osRelease.VersionID, err) + } + + return manifest.DISTRO_FEDORA, &runner.Fedora{ + Version: version, + }, nil + case "centos": + version, err := strconv.ParseUint(osRelease.VersionID, 10, 64) + if err != nil { + return manifest.DISTRO_NULL, nil, fmt.Errorf("cannot parse CentOS version (%s): %w", osRelease.VersionID, err) + } + r := &runner.CentOS{ + Version: version, + } + switch version { + case 9: + return manifest.DISTRO_EL9, r, nil + case 10: + return manifest.DISTRO_EL10, r, nil + default: + logrus.Warnf("Unknown CentOS version %d, using default distro for manifest generation", version) + return manifest.DISTRO_NULL, r, nil + } + + case "rhel": + versionParts := strings.Split(osRelease.VersionID, ".") + if len(versionParts) != 2 { + return manifest.DISTRO_NULL, nil, fmt.Errorf("invalid RHEL version format: %s", osRelease.VersionID) + } + major, err := strconv.ParseUint(versionParts[0], 10, 64) + if err != nil { + return manifest.DISTRO_NULL, nil, fmt.Errorf("cannot parse RHEL major version (%s): %w", versionParts[0], err) + } + minor, err := strconv.ParseUint(versionParts[1], 10, 64) + if err != nil { + return manifest.DISTRO_NULL, nil, fmt.Errorf("cannot parse RHEL minor version (%s): %w", versionParts[1], err) + } + r := &runner.RHEL{ + Major: major, + Minor: minor, + } + switch major { + case 9: + return manifest.DISTRO_EL9, r, nil + case 10: + return manifest.DISTRO_EL10, r, nil + default: + logrus.Warnf("Unknown RHEL version %d, using default distro for manifest generation", major) + return manifest.DISTRO_NULL, r, nil + } + } + + logrus.Warnf("Unknown distro %s, using default runner", osRelease.ID) + return manifest.DISTRO_NULL, &runner.Linux{}, nil +} diff --git a/bib/cmd/bootc-image-builder/main.go b/bib/cmd/bootc-image-builder/main.go index 6bfeaaad7..13f121e94 100644 --- a/bib/cmd/bootc-image-builder/main.go +++ b/bib/cmd/bootc-image-builder/main.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "io" + "io/fs" "log" "os" "os/exec" @@ -17,41 +18,27 @@ import ( "github.com/spf13/pflag" "golang.org/x/exp/slices" + "github.com/osbuild/blueprint/pkg/blueprint" + repos "github.com/osbuild/images/data/repositories" "github.com/osbuild/images/pkg/arch" + "github.com/osbuild/images/pkg/bib/blueprintload" + "github.com/osbuild/images/pkg/bootc" "github.com/osbuild/images/pkg/cloud" "github.com/osbuild/images/pkg/cloud/awscloud" - "github.com/osbuild/images/pkg/container" - "github.com/osbuild/images/pkg/dnfjson" + "github.com/osbuild/images/pkg/distro" + "github.com/osbuild/images/pkg/distro/generic" + "github.com/osbuild/images/pkg/experimentalflags" "github.com/osbuild/images/pkg/manifest" - "github.com/osbuild/images/pkg/osbuild" + "github.com/osbuild/images/pkg/manifestgen" + "github.com/osbuild/images/pkg/reporegistry" "github.com/osbuild/images/pkg/rpmmd" - "github.com/osbuild/bootc-image-builder/bib/internal/buildconfig" - podman_container "github.com/osbuild/bootc-image-builder/bib/internal/container" - "github.com/osbuild/bootc-image-builder/bib/internal/imagetypes" - "github.com/osbuild/bootc-image-builder/bib/internal/setup" - "github.com/osbuild/bootc-image-builder/bib/internal/source" - "github.com/osbuild/bootc-image-builder/bib/internal/util" - "github.com/osbuild/bootc-image-builder/bib/pkg/progress" -) + "github.com/osbuild/image-builder-cli/pkg/progress" + "github.com/osbuild/image-builder-cli/pkg/setup" -const ( - // As a baseline heuristic we double the size of - // the input container to support in-place updates. - // This is planned to be more configurable in the - // future. - containerSizeToDiskSizeMultiplier = 2 + "github.com/osbuild/bootc-image-builder/bib/internal/imagetypes" ) -// all possible locations for the bib's distro definitions -// ./data/defs and ./bib/data/defs are for development -// /usr/share/bootc-image-builder/defs is for the production, containerized version -var distroDefPaths = []string{ - "./data/defs", - "./bib/data/defs", - "/usr/share/bootc-image-builder/defs", -} - var ( osGetuid = os.Getuid osGetgid = os.Getgid @@ -60,29 +47,6 @@ var ( osStderr = os.Stderr ) -// canChownInPath checks if the ownership of files can be set in a given path. -func canChownInPath(path string) (bool, error) { - info, err := os.Stat(path) - if err != nil { - return false, err - } - if !info.IsDir() { - return false, fmt.Errorf("%s is not a directory", path) - } - - checkFile, err := os.CreateTemp(path, ".writecheck") - if err != nil { - return false, err - } - defer func() { - if err := os.Remove(checkFile.Name()); err != nil { - // print the error message for info but don't error out - fmt.Fprintf(os.Stderr, "error deleting %s: %s\n", checkFile.Name(), err.Error()) - } - }() - return checkFile.Chown(osGetuid(), osGetgid()) == nil, nil -} - func inContainerOrUnknown() bool { // no systemd-detect-virt, err on the side of container if _, err := exec.LookPath("systemd-detect-virt"); err != nil { @@ -93,92 +57,13 @@ func inContainerOrUnknown() bool { return err == nil } -// getContainerSize returns the size of an already pulled container image in bytes -func getContainerSize(imgref string) (uint64, error) { - output, err := exec.Command("podman", "image", "inspect", imgref, "--format", "{{.Size}}").Output() - if err != nil { - return 0, fmt.Errorf("failed inspect image: %w", util.OutputErr(err)) - } - size, err := strconv.ParseUint(strings.TrimSpace(string(output)), 10, 64) - if err != nil { - return 0, fmt.Errorf("cannot parse image size: %w", err) - } - - logrus.Debugf("container size: %v", size) - return size, nil -} - -func makeManifest(c *ManifestConfig, solver *dnfjson.Solver, cacheRoot string) (manifest.OSBuildManifest, map[string][]rpmmd.RepoConfig, error) { - mani, err := Manifest(c) - if err != nil { - return nil, nil, fmt.Errorf("cannot get manifest: %w", err) - } - - // depsolve packages - depsolvedSets := make(map[string]dnfjson.DepsolveResult) - depsolvedRepos := make(map[string][]rpmmd.RepoConfig) - for name, pkgSet := range mani.GetPackageSetChains() { - res, err := solver.Depsolve(pkgSet, 0) - if err != nil { - return nil, nil, fmt.Errorf("cannot depsolve: %w", err) - } - depsolvedSets[name] = *res - depsolvedRepos[name] = res.Repos - } - - // Resolve container - the normal case is that host and target - // architecture are the same. However it is possible to build - // cross-arch images by using qemu-user. This will run everything - // (including the build-root) with the target arch then, it - // is fast enough (given that it's mostly I/O and all I/O is - // run naively via syscall translation) - - // XXX: should NewResolver() take "arch.Arch"? - resolver := container.NewResolver(c.Architecture.String()) - - containerSpecs := make(map[string][]container.Spec) - for plName, sourceSpecs := range mani.GetContainerSourceSpecs() { - for _, c := range sourceSpecs { - resolver.Add(c) - } - specs, err := resolver.Finish() - if err != nil { - return nil, nil, fmt.Errorf("cannot resolve containers: %w", err) - } - for _, spec := range specs { - if spec.Arch != c.Architecture { - return nil, nil, fmt.Errorf("image found is for unexpected architecture %q (expected %q), if that is intentional, please make sure --target-arch matches", spec.Arch, c.Architecture) - } - } - containerSpecs[plName] = specs - } - - var opts manifest.SerializeOptions - if c.UseLibrepo { - opts.RpmDownloader = osbuild.RpmDownloaderLibrepo - } - mf, err := mani.Serialize(depsolvedSets, containerSpecs, nil, &opts) - if err != nil { - return nil, nil, fmt.Errorf("[ERROR] manifest serialization failed: %s", err.Error()) - } - return mf, depsolvedRepos, nil -} - -func saveManifest(ms manifest.OSBuildManifest, fpath string) error { +func saveManifest(ms manifest.OSBuildManifest, fpath string) (err error) { b, err := json.MarshalIndent(ms, "", " ") if err != nil { return fmt.Errorf("failed to marshal data for %q: %s", fpath, err.Error()) } b = append(b, '\n') // add new line at end of file - fp, err := os.Create(fpath) - if err != nil { - return fmt.Errorf("failed to create output file %q: %s", fpath, err.Error()) - } - defer fp.Close() - if _, err := fp.Write(b); err != nil { - return fmt.Errorf("failed to write output file %q: %s", fpath, err.Error()) - } - return nil + return os.WriteFile(fpath, b, 0644) } // manifestFromCobra generate an osbuild manifest from a cobra commandline. @@ -200,7 +85,10 @@ func manifestFromCobra(cmd *cobra.Command, args []string, pbar progress.Progress rpmCacheRoot, _ := cmd.Flags().GetString("rpmmd") targetArch, _ := cmd.Flags().GetString("target-arch") rootFs, _ := cmd.Flags().GetString("rootfs") + buildImgref, _ := cmd.Flags().GetString("build-container") + installerPayloadRef, _ := cmd.Flags().GetString("installer-payload-ref") useLibrepo, _ := cmd.Flags().GetBool("use-librepo") + omitDefaultKernelArgs, _ := cmd.Flags().GetBool("no-default-kernel-args") // If --local was given, warn in the case of --local or --local=true (true is the default), error in the case of --local=false if cmd.Flags().Changed("local") { @@ -213,17 +101,23 @@ func manifestFromCobra(cmd *cobra.Command, args []string, pbar progress.Progress } } - if targetArch != "" && arch.FromString(targetArch) != arch.Current() { - // TODO: detect if binfmt_misc for target arch is - // available, e.g. by mounting the binfmt_misc fs into - // the container and inspects the files or by - // including tiny statically linked target-arch - // binaries inside our bib container - fmt.Fprintf(os.Stderr, "WARNING: target-arch is experimental and needs an installed 'qemu-user' package\n") - if slices.Contains(imgTypes, "iso") { - return nil, nil, fmt.Errorf("cannot build iso for different target arches yet") + if targetArch != "" { + target, err := arch.FromString(targetArch) + if err != nil { + return nil, nil, err + } + if target != arch.Current() { + // TODO: detect if binfmt_misc for target arch is + // available, e.g. by mounting the binfmt_misc fs into + // the container and inspects the files or by + // including tiny statically linked target-arch + // binaries inside our bib container + fmt.Fprintf(os.Stderr, "WARNING: target-arch is experimental and needs an installed 'qemu-user' package\n") + if slices.Contains(imgTypes, "iso") { + return nil, nil, fmt.Errorf("cannot build iso for different target arches yet") + } + cntArch = target } - cntArch = arch.FromString(targetArch) } // TODO: add "target-variant", see https://github.com/osbuild/bootc-image-builder/pull/139/files#r1467591868 @@ -235,98 +129,97 @@ func manifestFromCobra(cmd *cobra.Command, args []string, pbar progress.Progress if err != nil { return nil, nil, fmt.Errorf("cannot detect build types %v: %w", imgTypes, err) } - - config, err := buildconfig.ReadWithFallback(userConfigFile) + config, err := blueprintload.LoadWithFallback(userConfigFile) if err != nil { return nil, nil, fmt.Errorf("cannot read config: %w", err) } - pbar.SetPulseMsgf("Manifest generation step") - pbar.Start() - if err := setup.ValidateHasContainerTags(imgref); err != nil { return nil, nil, err } - cntSize, err := getContainerSize(imgref) - if err != nil { - return nil, nil, fmt.Errorf("cannot get container size: %w", err) + pbar.SetPulseMsgf("Manifest generation step") + pbar.Start() + + // Note that we only need to pass a single imgType here into the manifest generation because: + // 1. the bootc disk manifests contains exports for all supported image types + // 2. the bootc legacy types (iso, anaconda-iso) always do a single build + imgType := imgTypes[0] + if imageTypes.Legacy() { + return manifestFromCobraForLegacyISO(imgref, buildImgref, imgType, rootFs, rpmCacheRoot, config, useLibrepo, cntArch) } - container, err := podman_container.New(imgref) + return manifestFromCobraForDisk(imgref, buildImgref, installerPayloadRef, imgType, rootFs, rpmCacheRoot, config, useLibrepo, cntArch, omitDefaultKernelArgs) +} + +func manifestFromCobraForDisk(imgref, buildImgref, installerPayloadRef, imgTypeStr, rootFs, rpmCacheRoot string, config *blueprint.Blueprint, useLibrepo bool, cntArch arch.Arch, omitDefaultKernelArgs bool) ([]byte, *mTLSConfig, error) { + containerInfo, err := bootc.ResolveBootcInfo(imgref) if err != nil { return nil, nil, err } - defer func() { - if err := container.Stop(); err != nil { - logrus.Warnf("error stopping container: %v", err) - } - }() - var rootfsType string - if !imageTypes.BuildsISO() { - if rootFs != "" { - rootfsType = rootFs - } else { - rootfsType, err = container.DefaultRootfsType() - if err != nil { - return nil, nil, fmt.Errorf("cannot get rootfs type for container: %w", err) - } - if rootfsType == "" { - return nil, nil, fmt.Errorf(`no default root filesystem type specified in container, please use "--rootfs" to set manually`) - } - } + if rootFs != "" { + containerInfo.DefaultRootFs = rootFs + } - // TODO: on a cross arch build we need to be conservative, i.e. we can - // only use the default ext4 because if xfs is select we run into the - // issue that mkfs.xfs calls "ioctl(BLKBSZSET)" which is missing in - // qemu-user. - // The fix has been merged upstream https://www.mail-archive.com/qemu-devel@nongnu.org/msg1037409.html - // and is expected to be included in v9.1.0 https://github.com/qemu/qemu/commit/e6e903db6a5e960e595f9f1fd034adb942dd9508 - // Remove the following condition once we update to qemu-user v9.1.0. - if cntArch != arch.Current() && rootfsType != "ext4" { - logrus.Warningf("container preferred root filesystem %q cannot be used during cross arch build", rootfsType) - rootfsType = "ext4" - } + if buildImgref == "" { + buildImgref = imgref } - // Gather some data from the containers distro - sourceinfo, err := source.LoadInfo(container.Root()) + + distri, err := generic.NewBootc("bootc", containerInfo) if err != nil { return nil, nil, err } - // This is needed just for RHEL and RHSM in most cases, but let's run it every time in case - // the image has some non-standard dnf plugins. - if err := container.InitDNF(); err != nil { + if buildImgref != "" { + buildContainerInfo, err := bootc.ResolveBootcBuildInfo(buildImgref) + if err != nil { + return nil, nil, err + } + if err := distri.SetBuildContainer(buildContainerInfo); err != nil { + return nil, nil, err + } + } + + archi, err := distri.GetArch(cntArch.String()) + if err != nil { return nil, nil, err } - solver, err := container.NewContainerSolver(rpmCacheRoot, cntArch, sourceinfo) + imgType, err := archi.GetImageType(imgTypeStr) if err != nil { return nil, nil, err } - manifestConfig := &ManifestConfig{ - Architecture: cntArch, - Config: config, - ImageTypes: imageTypes, - Imgref: imgref, - RootfsMinsize: cntSize * containerSizeToDiskSizeMultiplier, - DistroDefPaths: distroDefPaths, - SourceInfo: sourceinfo, - RootFSType: rootfsType, - UseLibrepo: useLibrepo, + repos, err := reporegistry.New(nil, []fs.FS{repos.FS}) + if err != nil { + return nil, nil, err } - - manifest, repos, err := makeManifest(manifestConfig, solver, rpmCacheRoot) + mg, err := manifestgen.New(repos, &manifestgen.Options{ + // XXX: hack to skip repo loading for the bootc image. + // We need to add a SkipRepositories or similar to + // manifestgen instead to make this clean + OverrideRepos: []rpmmd.RepoConfig{ + { + BaseURLs: []string{"https://example.com/not-used"}, + }, + }, + // this turns (blueprint validation) warnings into + // warnings as they are visible to the user + WarningsOutput: os.Stderr, + }) if err != nil { return nil, nil, err } - - mTLS, err := extractTLSKeys(SimpleFileReader{}, repos) + imgOpts := &distro.ImageOptions{ + Bootc: &distro.BootcImageOptions{ + InstallerPayloadRef: installerPayloadRef, + OmitDefaultKernelArgs: omitDefaultKernelArgs, + }, + } + manifest, err := mg.Generate(config, imgType, imgOpts) if err != nil { return nil, nil, err } - - return manifest, mTLS, nil + return manifest, nil, nil } func cmdManifest(cmd *cobra.Command, args []string) error { @@ -345,6 +238,8 @@ func cmdManifest(cmd *cobra.Command, args []string) error { return nil } +var awscloudNewUploader = awscloud.NewUploader + func handleAWSFlags(cmd *cobra.Command) (cloud.Uploader, error) { imgTypes, _ := cmd.Flags().GetStringArray("type") region, _ := cmd.Flags().GetString("aws-region") @@ -353,17 +248,24 @@ func handleAWSFlags(cmd *cobra.Command) (cloud.Uploader, error) { } bucketName, _ := cmd.Flags().GetString("aws-bucket") imageName, _ := cmd.Flags().GetString("aws-ami-name") - targetArch, _ := cmd.Flags().GetString("target-arch") + targetArchStr, _ := cmd.Flags().GetString("target-arch") if !slices.Contains(imgTypes, "ami") { return nil, fmt.Errorf("aws flags set for non-ami image type (type is set to %s)", strings.Join(imgTypes, ",")) } - // check as many permission prerequisites as possible before starting + targetArch := arch.Current() + if targetArchStr != "" { + var err error + targetArch, err = arch.FromString(targetArchStr) + if err != nil { + return nil, err + } + } uploaderOpts := &awscloud.UploaderOptions{ TargetArch: targetArch, } - uploader, err := awscloud.NewUploader(region, bucketName, imageName, uploaderOpts) + uploader, err := awscloudNewUploader(region, bucketName, imageName, uploaderOpts) if err != nil { return nil, err } @@ -371,6 +273,7 @@ func handleAWSFlags(cmd *cobra.Command) (cloud.Uploader, error) { if logrus.GetLevel() >= logrus.InfoLevel { status = os.Stderr } + // check as many permission prerequisites as possible before starting if err := uploader.Check(status); err != nil { return nil, err } @@ -384,9 +287,10 @@ func cmdBuild(cmd *cobra.Command, args []string) error { outputDir, _ := cmd.Flags().GetString("output") targetArch, _ := cmd.Flags().GetString("target-arch") progressType, _ := cmd.Flags().GetString("progress") + runInVM, _ := cmd.Flags().GetBool("in-vm") logrus.Debug("Validating environment") - if err := setup.Validate(targetArch); err != nil { + if err := setup.Validate(targetArch, runInVM); err != nil { return fmt.Errorf("cannot validate the setup: %w", err) } logrus.Debug("Ensuring environment setup") @@ -394,7 +298,7 @@ func cmdBuild(cmd *cobra.Command, args []string) error { case false: fmt.Fprintf(os.Stderr, "WARNING: running outside a container, this is an unsupported configuration\n") case true: - if err := setup.EnsureEnvironment(osbuildStore); err != nil { + if err := setup.EnsureEnvironment(osbuildStore, runInVM); err != nil { return fmt.Errorf("cannot ensure the environment: %w", err) } } @@ -441,7 +345,7 @@ func cmdBuild(cmd *cobra.Command, args []string) error { return fmt.Errorf("cannot save manifest: %w", err) } - pbar.SetPulseMsgf("Image building step") + pbar.SetPulseMsgf("Disk image building step") pbar.SetMessagef("Building %s", manifest_fname) var osbuildEnv []string @@ -461,11 +365,17 @@ func cmdBuild(cmd *cobra.Command, args []string) error { osbuildEnv = append(osbuildEnv, envVars...) } + if experimentalflags.Bool("debug-qemu-user") { + osbuildEnv = append(osbuildEnv, "OBSBUILD_EXPERIMENAL=debug-qemu-user") + } osbuildOpts := progress.OSBuildOptions{ StoreDir: osbuildStore, OutputDir: outputDir, ExtraEnv: osbuildEnv, } + if runInVM { + osbuildOpts.InVm = []string{"image"} + } if err = progress.RunOSBuild(pbar, mf, exports, &osbuildOpts); err != nil { return fmt.Errorf("cannot run osbuild: %w", err) } @@ -499,35 +409,6 @@ func cmdBuild(cmd *cobra.Command, args []string) error { return nil } -func chownR(path string, chown string) error { - if chown == "" { - return nil - } - errFmt := "cannot parse chown: %v" - - var gid int - uidS, gidS, _ := strings.Cut(chown, ":") - uid, err := strconv.Atoi(uidS) - if err != nil { - return fmt.Errorf(errFmt, err) - } - if gidS != "" { - gid, err = strconv.Atoi(gidS) - if err != nil { - return fmt.Errorf(errFmt, err) - } - } else { - gid = osGetgid() - } - - return filepath.Walk(path, func(name string, info os.FileInfo, err error) error { - if err == nil { - err = os.Chown(name, uid, gid) - } - return err - }) -} - var rootLogLevel string func rootPreRunE(cmd *cobra.Command, _ []string) error { @@ -650,13 +531,19 @@ func buildCobraCmdline() (*cobra.Command, error) { } manifestCmd.Flags().String("rpmmd", "/rpmmd", "rpm metadata cache directory") manifestCmd.Flags().String("target-arch", "", "build for the given target architecture (experimental)") + manifestCmd.Flags().String("build-container", "", "Use a custom container for the image build") + // XXX: add --bootc-installer-payload-ref as alias to make it + // cmdline compatible with ibcli(?) + manifestCmd.Flags().String("installer-payload-ref", "", "bootc installer payload ref") manifestCmd.Flags().StringArray("type", []string{"qcow2"}, fmt.Sprintf("image types to build [%s]", imagetypes.Available())) manifestCmd.Flags().Bool("local", true, "DEPRECATED: --local is now the default behavior, make sure to pull the container image before running bootc-image-builder") if err := manifestCmd.Flags().MarkHidden("local"); err != nil { return nil, fmt.Errorf("cannot hide 'local' :%w", err) } manifestCmd.Flags().String("rootfs", "", "Root filesystem type. If not given, the default configured in the source container image is used.") - manifestCmd.Flags().Bool("use-librepo", false, "(experimenal) switch to librepo for pkg download, needs new enough osbuild") + manifestCmd.Flags().Bool("in-vm", false, "Run osbuild in a virtual machine") + manifestCmd.Flags().Bool("use-librepo", true, "switch to librepo for pkg download, needs new enough osbuild") + manifestCmd.Flags().Bool("no-default-kernel-args", false, "don't use the default kernel arguments") // --config is only useful for developers who run bib outside // of a container to generate a manifest. so hide it by // default from users. diff --git a/bib/cmd/bootc-image-builder/main_test.go b/bib/cmd/bootc-image-builder/main_test.go index 3ff836b40..90abd7c09 100644 --- a/bib/cmd/bootc-image-builder/main_test.go +++ b/bib/cmd/bootc-image-builder/main_test.go @@ -1,9 +1,9 @@ package main_test import ( - "encoding/json" - "errors" + "bytes" "fmt" + "io" "os" "strings" "testing" @@ -15,16 +15,10 @@ import ( "github.com/stretchr/testify/require" "github.com/osbuild/images/pkg/arch" - "github.com/osbuild/images/pkg/blueprint" - "github.com/osbuild/images/pkg/container" - "github.com/osbuild/images/pkg/dnfjson" - "github.com/osbuild/images/pkg/manifest" - "github.com/osbuild/images/pkg/rpmmd" + "github.com/osbuild/images/pkg/cloud" + "github.com/osbuild/images/pkg/cloud/awscloud" main "github.com/osbuild/bootc-image-builder/bib/cmd/bootc-image-builder" - "github.com/osbuild/bootc-image-builder/bib/internal/buildconfig" - "github.com/osbuild/bootc-image-builder/bib/internal/imagetypes" - "github.com/osbuild/bootc-image-builder/bib/internal/source" ) func TestCanChownInPathHappy(t *testing.T) { @@ -61,462 +55,6 @@ func TestCanChownInPathCannotChange(t *testing.T) { assert.Equal(t, canChown, false) } -type manifestTestCase struct { - config *main.ManifestConfig - imageTypes imagetypes.ImageTypes - depsolved map[string]dnfjson.DepsolveResult - containers map[string][]container.Spec - expStages map[string][]string - notExpectedStages map[string][]string - err interface{} -} - -func getBaseConfig() *main.ManifestConfig { - return &main.ManifestConfig{ - Architecture: arch.ARCH_X86_64, - Imgref: "testempty", - SourceInfo: &source.Info{ - OSRelease: source.OSRelease{ - ID: "fedora", - VersionID: "40", - Name: "Fedora Linux", - PlatformID: "platform:f40", - }, - UEFIVendor: "fedora", - }, - - // We need the real path here, because we are creating real manifests - DistroDefPaths: []string{"../../data/defs"}, - - // RootFSType is required to create a Manifest - RootFSType: "ext4", - } -} - -func getUserConfig() *main.ManifestConfig { - // add a user - pass := "super-secret-password-42" - key := "ssh-ed25519 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - return &main.ManifestConfig{ - Architecture: arch.ARCH_X86_64, - Imgref: "testuser", - Config: &buildconfig.BuildConfig{ - Customizations: &blueprint.Customizations{ - User: []blueprint.UserCustomization{ - { - Name: "tester", - Password: &pass, - Key: &key, - }, - }, - }, - }, - SourceInfo: &source.Info{ - OSRelease: source.OSRelease{ - ID: "fedora", - VersionID: "40", - Name: "Fedora Linux", - PlatformID: "platform:f40", - }, - UEFIVendor: "fedora", - }, - - // We need the real path here, because we are creating real manifests - DistroDefPaths: []string{"../../data/defs"}, - - // RootFSType is required to create a Manifest - RootFSType: "ext4", - } -} - -func TestManifestGenerationEmptyConfig(t *testing.T) { - baseConfig := getBaseConfig() - testCases := map[string]manifestTestCase{ - "ami-base": { - config: baseConfig, - imageTypes: []string{"ami"}, - }, - "raw-base": { - config: baseConfig, - imageTypes: []string{"raw"}, - }, - "qcow2-base": { - config: baseConfig, - imageTypes: []string{"qcow2"}, - }, - "iso-base": { - config: baseConfig, - imageTypes: []string{"iso"}, - }, - "empty-config": { - config: &main.ManifestConfig{}, - imageTypes: []string{"qcow2"}, - err: errors.New("pipeline: no base image defined"), - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - config := main.ManifestConfig(*tc.config) - config.ImageTypes = tc.imageTypes - _, err := main.Manifest(&config) - assert.Equal(t, err, tc.err) - }) - } -} - -func TestManifestGenerationUserConfig(t *testing.T) { - userConfig := getUserConfig() - testCases := map[string]manifestTestCase{ - "ami-user": { - config: userConfig, - imageTypes: []string{"ami"}, - }, - "raw-user": { - config: userConfig, - imageTypes: []string{"raw"}, - }, - "qcow2-user": { - config: userConfig, - imageTypes: []string{"qcow2"}, - }, - "iso-user": { - config: userConfig, - imageTypes: []string{"iso"}, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - config := main.ManifestConfig(*tc.config) - config.ImageTypes = tc.imageTypes - _, err := main.Manifest(&config) - assert.NoError(t, err) - }) - } -} - -// TODO: this tests at this layer is not ideal, it has too much knowledge -// over the implementation details of the "images" library and how an -// image.NewBootcDiskImage() works (i.e. what the pipeline names are and -// what key piplines to expect). These details should be tested in "images" -// and here we would just check (somehow) that image.NewBootcDiskImage() -// (or image.NewAnacondaContainerInstaller()) is called and the right -// customizations are passed. The existing layout makes this hard so this -// is fine for now but would be nice to revisit this. -func TestManifestSerialization(t *testing.T) { - // Tests that the manifest is generated without error and is serialized - // with expected key stages. - - // Disk images require a container for the build/image pipelines - containerSpec := container.Spec{ - Source: "test-container", - Digest: "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", - ImageID: "sha256:1111111111111111111111111111111111111111111111111111111111111111", - } - diskContainers := map[string][]container.Spec{ - "build": { - containerSpec, - }, - "image": { - containerSpec, - }, - } - - // ISOs require a container for the bootiso-tree, build packages, and packages for the anaconda-tree (with a kernel). - isoContainers := map[string][]container.Spec{ - "bootiso-tree": { - containerSpec, - }, - } - isoPackages := map[string]dnfjson.DepsolveResult{ - "build": { - Packages: []rpmmd.PackageSpec{ - { - Name: "package", - Version: "113", - Checksum: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - }, - }, - }, - "anaconda-tree": { - Packages: []rpmmd.PackageSpec{ - { - Name: "kernel", - Version: "10.11", - Checksum: "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - }, - { - Name: "package", - Version: "113", - Checksum: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - }, - }, - }, - } - - pkgsNoBuild := map[string]dnfjson.DepsolveResult{ - "anaconda-tree": { - Packages: []rpmmd.PackageSpec{ - - { - Name: "kernel", - Version: "10.11", - Checksum: "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - }, - { - Name: "package", - Version: "113", - Checksum: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - }, - }, - }, - } - - baseConfig := getBaseConfig() - userConfig := getUserConfig() - testCases := map[string]manifestTestCase{ - "ami-base": { - config: baseConfig, - imageTypes: []string{"ami"}, - containers: diskContainers, - expStages: map[string][]string{ - "build": {"org.osbuild.container-deploy"}, - "image": { - "org.osbuild.bootc.install-to-filesystem", - }, - }, - notExpectedStages: map[string][]string{ - "build": {"org.osbuild.rpm"}, - "image": { - "org.osbuild.users", - }, - }, - }, - "raw-base": { - config: baseConfig, - imageTypes: []string{"raw"}, - containers: diskContainers, - expStages: map[string][]string{ - "build": {"org.osbuild.container-deploy"}, - "image": { - "org.osbuild.bootc.install-to-filesystem", - }, - }, - notExpectedStages: map[string][]string{ - "build": {"org.osbuild.rpm"}, - "image": { - "org.osbuild.users", - }, - }, - }, - "qcow2-base": { - config: baseConfig, - imageTypes: []string{"qcow2"}, - containers: diskContainers, - expStages: map[string][]string{ - "build": {"org.osbuild.container-deploy"}, - "image": { - "org.osbuild.bootc.install-to-filesystem", - }, - }, - notExpectedStages: map[string][]string{ - "build": {"org.osbuild.rpm"}, - "image": { - "org.osbuild.users", - }, - }, - }, - "ami-user": { - config: userConfig, - imageTypes: []string{"ami"}, - containers: diskContainers, - expStages: map[string][]string{ - "build": {"org.osbuild.container-deploy"}, - "image": { - "org.osbuild.users", - "org.osbuild.bootc.install-to-filesystem", - }, - }, - notExpectedStages: map[string][]string{ - "build": {"org.osbuild.rpm"}, - }, - }, - "raw-user": { - config: userConfig, - imageTypes: []string{"raw"}, - containers: diskContainers, - expStages: map[string][]string{ - "build": {"org.osbuild.container-deploy"}, - "image": { - "org.osbuild.users", // user creation stage when we add users - "org.osbuild.bootc.install-to-filesystem", - }, - }, - notExpectedStages: map[string][]string{ - "build": {"org.osbuild.rpm"}, - }, - }, - "qcow2-user": { - config: userConfig, - imageTypes: []string{"qcow2"}, - containers: diskContainers, - expStages: map[string][]string{ - "build": {"org.osbuild.container-deploy"}, - "image": { - "org.osbuild.users", // user creation stage when we add users - "org.osbuild.bootc.install-to-filesystem", - }, - }, - notExpectedStages: map[string][]string{ - "build": {"org.osbuild.rpm"}, - }, - }, - "iso-user": { - config: userConfig, - imageTypes: []string{"iso"}, - containers: isoContainers, - depsolved: isoPackages, - expStages: map[string][]string{ - "build": {"org.osbuild.rpm"}, - "bootiso-tree": {"org.osbuild.skopeo"}, // adds the container to the ISO tree - }, - }, - "iso-nobuildpkg": { - config: userConfig, - imageTypes: []string{"iso"}, - containers: isoContainers, - depsolved: pkgsNoBuild, - err: "serialization not started", - }, - "iso-nocontainer": { - config: userConfig, - imageTypes: []string{"iso"}, - depsolved: isoPackages, - err: "missing ostree, container, or ospipeline parameters in ISO tree pipeline", - }, - "ami-nocontainer": { - config: userConfig, - imageTypes: []string{"ami"}, - // errors come from BuildrootFromContainer() - // TODO: think about better error and testing here (not the ideal layer or err msg) - err: "serialization not started", - }, - "raw-nocontainer": { - config: userConfig, - imageTypes: []string{"raw"}, - // errors come from BuildrootFromContainer() - // TODO: think about better error and testing here (not the ideal layer or err msg) - err: "serialization not started", - }, - "qcow2-nocontainer": { - config: userConfig, - imageTypes: []string{"qcow2"}, - // errors come from BuildrootFromContainer() - // TODO: think about better error and testing here (not the ideal layer or err msg) - err: "serialization not started", - }, - } - - // Use an empty config: only the imgref is required - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - assert := assert.New(t) - config := main.ManifestConfig(*tc.config) - config.ImageTypes = tc.imageTypes - mf, err := main.Manifest(&config) - assert.NoError(err) // this isn't the error we're testing for - - if tc.err != nil { - assert.PanicsWithValue(tc.err, func() { - _, err := mf.Serialize(tc.depsolved, tc.containers, nil, nil) - assert.NoError(err) - }) - } else { - manifestJson, err := mf.Serialize(tc.depsolved, tc.containers, nil, nil) - assert.NoError(err) - assert.NoError(checkStages(manifestJson, tc.expStages, tc.notExpectedStages)) - } - }) - } - - { - // this one panics with a typed error and needs to be tested separately from the above (PanicsWithError()) - t.Run("iso-nopkgs", func(t *testing.T) { - assert := assert.New(t) - config := main.ManifestConfig(*userConfig) - config.ImageTypes, _ = imagetypes.New("iso") - manifest, err := main.Manifest(&config) - assert.NoError(err) // this isn't the error we're testing for - - expError := "package \"kernel\" not found in the PackageSpec list" - assert.PanicsWithError(expError, func() { - _, err := manifest.Serialize(nil, isoContainers, nil, nil) - assert.NoError(err) - }) - }) - } -} - -// simplified representation of a manifest -type testManifest struct { - Pipelines []pipeline `json:"pipelines"` -} -type pipeline struct { - Name string `json:"name"` - Stages []stage `json:"stages"` -} -type stage struct { - Type string `json:"type"` -} - -func checkStages(serialized manifest.OSBuildManifest, pipelineStages map[string][]string, missingStages map[string][]string) error { - mf := &testManifest{} - if err := json.Unmarshal(serialized, mf); err != nil { - return err - } - pipelineMap := map[string]pipeline{} - for _, pl := range mf.Pipelines { - pipelineMap[pl.Name] = pl - } - - for plname, stages := range pipelineStages { - pl, found := pipelineMap[plname] - if !found { - return fmt.Errorf("pipeline %q not found", plname) - } - - stageMap := map[string]bool{} - for _, stage := range pl.Stages { - stageMap[stage.Type] = true - } - for _, stage := range stages { - if _, found := stageMap[stage]; !found { - return fmt.Errorf("pipeline %q - stage %q - not found", plname, stage) - } - } - } - - for plname, stages := range missingStages { - pl, found := pipelineMap[plname] - if !found { - return fmt.Errorf("pipeline %q not found", plname) - } - - stageMap := map[string]bool{} - for _, stage := range pl.Stages { - stageMap[stage.Type] = true - } - for _, stage := range stages { - if _, found := stageMap[stage]; found { - return fmt.Errorf("pipeline %q - stage %q - found (but should not be)", plname, stage) - } - } - } - - return nil -} - func mockOsArgs(new []string) (restore func()) { saved := os.Args os.Args = append([]string{"argv0"}, new...) @@ -627,3 +165,68 @@ func TestCobraCmdlineVerbose(t *testing.T) { }) } } + +type fakeAwsUploader struct { + checkCalls int + + region, bucket, ami string + opts *awscloud.UploaderOptions + + uploadAndRegisterRead bytes.Buffer + uploadAndRegisterCalls int + uploadAndRegisterErr error +} + +var _ = cloud.Uploader(&fakeAwsUploader{}) + +func (fa *fakeAwsUploader) Check(status io.Writer) error { + fa.checkCalls++ + return nil +} + +func (fa *fakeAwsUploader) UploadAndRegister(r io.Reader, size uint64, status io.Writer) error { + fa.uploadAndRegisterCalls++ + _, err := io.Copy(&fa.uploadAndRegisterRead, r) + if err != nil { + panic(err) + } + return fa.uploadAndRegisterErr +} + +func TestHandleAWSFlags(t *testing.T) { + for _, tc := range []struct { + extraArgs []string + expectedOpts *awscloud.UploaderOptions + }{ + {nil, &awscloud.UploaderOptions{TargetArch: arch.Current()}}, + {[]string{"--target-arch=aarch64"}, &awscloud.UploaderOptions{TargetArch: arch.ARCH_AARCH64}}, + } { + var fau fakeAwsUploader + t.Cleanup(main.MockAwscloudNewUploader(func(region string, bucket string, ami string, opts *awscloud.UploaderOptions) (cloud.Uploader, error) { + fau.region = region + fau.bucket = bucket + fau.ami = ami + fau.opts = opts + return &fau, nil + })) + + rootCmd, err := main.BuildCobraCmdline() + assert.NoError(t, err) + // Commands() returns commandsordered by name + buildCmd := rootCmd.Commands()[0] + assert.Equal(t, "build", buildCmd.Name()) + err = buildCmd.ParseFlags(append([]string{ + "--aws-bucket=aws-bucket", + "--aws-ami-name=aws-ami-name", + "--aws-region=aws-region", + "--type=ami", + }, tc.extraArgs...)) + assert.NoError(t, err) + + uploader, err := main.HandleAWSFlags(buildCmd) + assert.NoError(t, err) + assert.NotNil(t, uploader) + assert.Equal(t, 1, fau.checkCalls) + assert.Equal(t, tc.expectedOpts, fau.opts) + } +} diff --git a/bib/cmd/bootc-image-builder/mtls.go b/bib/cmd/bootc-image-builder/mtls.go index 101a8e45d..ebe650771 100644 --- a/bib/cmd/bootc-image-builder/mtls.go +++ b/bib/cmd/bootc-image-builder/mtls.go @@ -15,17 +15,9 @@ type mTLSConfig struct { ca []byte } -type fileReader interface { - ReadFile(string) ([]byte, error) -} - -type SimpleFileReader struct{} - -func (SimpleFileReader) ReadFile(path string) ([]byte, error) { - return os.ReadFile(path) -} +var osReadFile = os.ReadFile -func extractTLSKeys(reader fileReader, repoSets map[string][]rpmmd.RepoConfig) (*mTLSConfig, error) { +func extractTLSKeys(repoSets map[string][]rpmmd.RepoConfig) (*mTLSConfig, error) { var keyPath, certPath, caPath string for _, set := range repoSets { for _, r := range set { @@ -44,17 +36,17 @@ func extractTLSKeys(reader fileReader, repoSets map[string][]rpmmd.RepoConfig) ( return nil, nil } - key, err := reader.ReadFile(keyPath) + key, err := osReadFile(keyPath) if err != nil { return nil, fmt.Errorf("failed to read TLS client key from the container: %w", err) } - cert, err := reader.ReadFile(certPath) + cert, err := osReadFile(certPath) if err != nil { return nil, fmt.Errorf("failed to read TLS client certificate from the container: %w", err) } - ca, err := reader.ReadFile(caPath) + ca, err := osReadFile(caPath) if err != nil { return nil, fmt.Errorf("failed to read TLS CA certificate from the container: %w", err) } diff --git a/bib/cmd/bootc-image-builder/mtls_test.go b/bib/cmd/bootc-image-builder/mtls_test.go index 15a3d30a8..03fac2372 100644 --- a/bib/cmd/bootc-image-builder/mtls_test.go +++ b/bib/cmd/bootc-image-builder/mtls_test.go @@ -33,8 +33,10 @@ func TestExtractTLSKeysHappy(t *testing.T) { } fakeReader := &fakeFileReader{} + restore := MockOsReadFile(fakeReader.ReadFile) + defer restore() - mTLS, err := extractTLSKeys(fakeReader, repos) + mTLS, err := extractTLSKeys(repos) require.NoError(t, err) require.Equal(t, mTLS.ca, []byte("content of /ca")) require.Equal(t, mTLS.cert, []byte("content of /cert")) @@ -43,7 +45,7 @@ func TestExtractTLSKeysHappy(t *testing.T) { // also check that adding another repo with same keys still succeeds repos["toucan"] = repos["kingfisher"] - _, err = extractTLSKeys(fakeReader, repos) + _, err = extractTLSKeys(repos) require.NoError(t, err) require.Len(t, fakeReader.readPaths, 6) } @@ -68,8 +70,10 @@ func TestExtractTLSKeysUnhappy(t *testing.T) { } fakeReader := &fakeFileReader{} + restore := MockOsReadFile(fakeReader.ReadFile) + defer restore() - _, err := extractTLSKeys(fakeReader, repos) + _, err := extractTLSKeys(repos) require.EqualError(t, err, "multiple TLS client keys found, this is currently unsupported") } diff --git a/bib/cmd/bootc-image-builder/partition_tables.go b/bib/cmd/bootc-image-builder/partition_tables.go deleted file mode 100644 index 0d67f8705..000000000 --- a/bib/cmd/bootc-image-builder/partition_tables.go +++ /dev/null @@ -1,130 +0,0 @@ -package main - -import ( - "github.com/osbuild/images/pkg/arch" - "github.com/osbuild/images/pkg/disk" - "github.com/osbuild/images/pkg/distro" -) - -const ( - MebiByte = 1024 * 1024 // MiB - GibiByte = 1024 * 1024 * 1024 // GiB - // BootOptions defines the mountpoint options for /boot - // See https://github.com/containers/bootc/pull/341 for the rationale for - // using `ro` by default. Briefly it protects against corruption - // by non-ostree aware tools. - BootOptions = "ro" - // And we default to `ro` for the rootfs too, because we assume the input - // container image is using composefs. For more info, see - // https://github.com/containers/bootc/pull/417 and - // https://github.com/ostreedev/ostree/issues/3193 - RootOptions = "ro" -) - -// diskUuidOfUnknownOrigin is used by default for disk images, -// picked by someone in the past for unknown reasons. More in -// e.g. https://github.com/osbuild/bootc-image-builder/pull/568 and -// https://github.com/osbuild/images/pull/823 -const diskUuidOfUnknownOrigin = "D209C89E-EA5E-4FBD-B161-B461CCE297E0" - -// efiPartition defines the default ESP. See also -// https://en.wikipedia.org/wiki/EFI_system_partition -var efiPartition = disk.Partition{ - Size: 501 * MebiByte, - Type: disk.EFISystemPartitionGUID, - UUID: disk.EFISystemPartitionUUID, - Payload: &disk.Filesystem{ - Type: "vfat", - UUID: disk.EFIFilesystemUUID, - Mountpoint: "/boot/efi", - Label: "EFI-SYSTEM", - FSTabOptions: "umask=0077,shortname=winnt", - FSTabFreq: 0, - FSTabPassNo: 2, - }, -} - -// bootPartition defines a distinct filesystem for /boot -// which is needed for e.g. LVM or LUKS when using GRUB -// (which this project doesn't support today...) -// See also https://github.com/containers/bootc/pull/529/commits/e5548d8765079171e6ed39a3ab0479bc8681a1c9 -var bootPartition = disk.Partition{ - Size: 1 * GibiByte, - Type: disk.FilesystemDataGUID, - UUID: disk.DataPartitionUUID, - Payload: &disk.Filesystem{ - Type: "ext4", - Mountpoint: "/boot", - Label: "boot", - FSTabOptions: BootOptions, - FSTabFreq: 1, - FSTabPassNo: 2, - }, -} - -// rootPartition holds the root filesystem; however note -// that while the type here defines "ext4" because the data -// type requires something there, in practice we pull -// the rootfs type from the container image by default. -// See https://containers.github.io/bootc/bootc-install.html -var rootPartition = disk.Partition{ - Size: 2 * GibiByte, - Type: disk.FilesystemDataGUID, - UUID: disk.RootPartitionUUID, - Payload: &disk.Filesystem{ - Type: "ext4", - Label: "root", - Mountpoint: "/", - FSTabOptions: RootOptions, - FSTabFreq: 1, - FSTabPassNo: 1, - }, -} - -var partitionTables = distro.BasePartitionTableMap{ - arch.ARCH_X86_64.String(): disk.PartitionTable{ - UUID: diskUuidOfUnknownOrigin, - Type: disk.PT_GPT, - Partitions: []disk.Partition{ - { - Size: 1 * MebiByte, - Bootable: true, - Type: disk.BIOSBootPartitionGUID, - UUID: disk.BIOSBootPartitionUUID, - }, - efiPartition, - bootPartition, - rootPartition, - }, - }, - arch.ARCH_AARCH64.String(): disk.PartitionTable{ - UUID: diskUuidOfUnknownOrigin, - Type: disk.PT_GPT, - Partitions: []disk.Partition{ - efiPartition, - bootPartition, - rootPartition, - }, - }, - arch.ARCH_S390X.String(): disk.PartitionTable{ - UUID: diskUuidOfUnknownOrigin, - Type: disk.PT_GPT, - Partitions: []disk.Partition{ - bootPartition, - rootPartition, - }, - }, - arch.ARCH_PPC64LE.String(): disk.PartitionTable{ - UUID: diskUuidOfUnknownOrigin, - Type: disk.PT_GPT, - Partitions: []disk.Partition{ - { - Size: 4 * MebiByte, - Type: disk.PRePartitionGUID, - Bootable: true, - }, - bootPartition, - rootPartition, - }, - }, -} diff --git a/bib/cmd/bootc-image-builder/util.go b/bib/cmd/bootc-image-builder/util.go new file mode 100644 index 000000000..d6b2bd989 --- /dev/null +++ b/bib/cmd/bootc-image-builder/util.go @@ -0,0 +1,76 @@ +package main + +import ( + cryptorand "crypto/rand" + "fmt" + "math" + "math/big" + "math/rand" + "os" + "path/filepath" + "strconv" + "strings" +) + +// canChownInPath checks if the ownership of files can be set in a given path. +func canChownInPath(path string) (bool, error) { + info, err := os.Stat(path) + if err != nil { + return false, err + } + if !info.IsDir() { + return false, fmt.Errorf("%s is not a directory", path) + } + + checkFile, err := os.CreateTemp(path, ".writecheck") + if err != nil { + return false, err + } + defer func() { + if err := os.Remove(checkFile.Name()); err != nil { + // print the error message for info but don't error out + fmt.Fprintf(os.Stderr, "error deleting %s: %s\n", checkFile.Name(), err.Error()) + } + }() + return checkFile.Chown(osGetuid(), osGetgid()) == nil, nil +} + +func chownR(path string, chown string) error { + if chown == "" { + return nil + } + errFmt := "cannot parse chown: %v" + + var gid int + uidS, gidS, _ := strings.Cut(chown, ":") + uid, err := strconv.Atoi(uidS) + if err != nil { + return fmt.Errorf(errFmt, err) + } + if gidS != "" { + gid, err = strconv.Atoi(gidS) + if err != nil { + return fmt.Errorf(errFmt, err) + } + } else { + gid = osGetgid() + } + + return filepath.Walk(path, func(name string, info os.FileInfo, err error) error { + if err == nil { + err = os.Chown(name, uid, gid) + } + return err + }) +} + +func createRand() *rand.Rand { + seed, err := cryptorand.Int(cryptorand.Reader, big.NewInt(math.MaxInt64)) + if err != nil { + panic("Cannot generate an RNG seed.") + } + + // math/rand is good enough in this case + /* #nosec G404 */ + return rand.New(rand.NewSource(seed.Int64())) +} diff --git a/bib/cmd/bootc-image-builder/workload.go b/bib/cmd/bootc-image-builder/workload.go deleted file mode 100644 index d2667fa7e..000000000 --- a/bib/cmd/bootc-image-builder/workload.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import "github.com/osbuild/images/pkg/rpmmd" - -// NullWorkload implements the images Workload interface but returns only nil -// from all its methods and holds no data. -type NullWorkload struct { -} - -func (p *NullWorkload) GetRepos() []rpmmd.RepoConfig { - return nil -} - -func (p *NullWorkload) GetPackages() []string { - return nil -} - -func (p *NullWorkload) GetServices() []string { - return nil -} - -func (p *NullWorkload) GetDisabledServices() []string { - return nil -} diff --git a/bib/cmd/upload/main.go b/bib/cmd/upload/main.go index 5dfcdd08b..8f7ddccd9 100644 --- a/bib/cmd/upload/main.go +++ b/bib/cmd/upload/main.go @@ -7,6 +7,7 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/osbuild/images/pkg/arch" "github.com/osbuild/images/pkg/cloud/awscloud" ) @@ -29,9 +30,15 @@ func uploadAMI(cmd *cobra.Command, args []string) { check(err) imageName, err := flags.GetString("ami-name") check(err) - targetArch, err := flags.GetString("target-arch") + targetArchStr, err := flags.GetString("target-arch") check(err) + targetArch := arch.Current() + if targetArchStr != "" { + var err error + targetArch, err = arch.FromString(targetArchStr) + check(err) + } opts := &awscloud.UploaderOptions{ TargetArch: targetArch, } @@ -40,9 +47,10 @@ func uploadAMI(cmd *cobra.Command, args []string) { f, err := os.Open(filename) check(err) + // nolint:errcheck defer f.Close() - check(uploader.UploadAndRegister(f, os.Stderr)) + check(uploader.UploadAndRegister(f, 0, os.Stderr)) } func setupCLI() *cobra.Command { diff --git a/bib/data/defs/aurora-40.yaml b/bib/data/defs/aurora-40.yaml new file mode 120000 index 000000000..b77da5759 --- /dev/null +++ b/bib/data/defs/aurora-40.yaml @@ -0,0 +1 @@ +fedora-40.yaml \ No newline at end of file diff --git a/bib/data/defs/aurora-helium-10.yaml b/bib/data/defs/aurora-helium-10.yaml new file mode 120000 index 000000000..31ce3eb13 --- /dev/null +++ b/bib/data/defs/aurora-helium-10.yaml @@ -0,0 +1 @@ +centos-10.yaml \ No newline at end of file diff --git a/bib/data/defs/bazzite-40.yaml b/bib/data/defs/bazzite-40.yaml new file mode 120000 index 000000000..b77da5759 --- /dev/null +++ b/bib/data/defs/bazzite-40.yaml @@ -0,0 +1 @@ +fedora-40.yaml \ No newline at end of file diff --git a/bib/data/defs/bluefin-40.yaml b/bib/data/defs/bluefin-40.yaml new file mode 120000 index 000000000..b77da5759 --- /dev/null +++ b/bib/data/defs/bluefin-40.yaml @@ -0,0 +1 @@ +fedora-40.yaml \ No newline at end of file diff --git a/bib/data/defs/centos-10.yaml b/bib/data/defs/centos-10.yaml index d5956e3e8..dcf78f042 100644 --- a/bib/data/defs/centos-10.yaml +++ b/bib/data/defs/centos-10.yaml @@ -65,6 +65,7 @@ anaconda-iso: - perl-interpreter - pigz - plymouth + - prefixdevname - python3-pyatspi - rdma-core - rng-tools diff --git a/bib/data/defs/centos-9.yaml b/bib/data/defs/centos-9.yaml index f202cb580..431642ca7 100644 --- a/bib/data/defs/centos-9.yaml +++ b/bib/data/defs/centos-9.yaml @@ -73,6 +73,7 @@ anaconda-iso: - perl-interpreter - pigz - plymouth + - prefixdevname - python3-pyatspi - rdma-core - rng-tools diff --git a/bib/data/defs/fedora-40.yaml b/bib/data/defs/fedora-40.yaml index efd64d8b1..c1431a18a 100644 --- a/bib/data/defs/fedora-40.yaml +++ b/bib/data/defs/fedora-40.yaml @@ -75,6 +75,7 @@ anaconda-iso: - perl-interpreter - pigz - plymouth + - prefixdevname - python3-pyatspi - rdma-core - realtek-firmware diff --git a/bib/data/defs/fedora-42.yaml b/bib/data/defs/fedora-42.yaml new file mode 120000 index 000000000..b77da5759 --- /dev/null +++ b/bib/data/defs/fedora-42.yaml @@ -0,0 +1 @@ +fedora-40.yaml \ No newline at end of file diff --git a/bib/data/defs/rocky-10.yaml b/bib/data/defs/rocky-10.yaml new file mode 120000 index 000000000..31ce3eb13 --- /dev/null +++ b/bib/data/defs/rocky-10.yaml @@ -0,0 +1 @@ +centos-10.yaml \ No newline at end of file diff --git a/bib/data/defs/rocky-9.yaml b/bib/data/defs/rocky-9.yaml new file mode 120000 index 000000000..f09a87265 --- /dev/null +++ b/bib/data/defs/rocky-9.yaml @@ -0,0 +1 @@ +centos-9.yaml \ No newline at end of file diff --git a/bib/data/defs/stillos-10.yaml b/bib/data/defs/stillos-10.yaml new file mode 120000 index 000000000..679a5b6dd --- /dev/null +++ b/bib/data/defs/stillos-10.yaml @@ -0,0 +1 @@ +bib/data/defs/centos-10.yaml \ No newline at end of file diff --git a/bib/go.mod b/bib/go.mod index 408e28121..58863a781 100644 --- a/bib/go.mod +++ b/bib/go.mod @@ -1,130 +1,138 @@ module github.com/osbuild/bootc-image-builder/bib -go 1.22.8 +go 1.24.12 require ( - github.com/BurntSushi/toml v1.4.0 - github.com/cheggaaa/pb/v3 v3.1.6 + github.com/cheggaaa/pb/v3 v3.1.7 github.com/hashicorp/go-version v1.7.0 - github.com/mattn/go-isatty v0.0.20 - github.com/osbuild/images v0.120.0 - github.com/sirupsen/logrus v1.9.3 - github.com/spf13/cobra v1.9.1 - github.com/spf13/pflag v1.0.6 - github.com/stretchr/testify v1.10.0 - golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 - golang.org/x/sys v0.30.0 - gopkg.in/yaml.v3 v3.0.1 + github.com/osbuild/blueprint v1.26.0 + github.com/osbuild/image-builder-cli v0.0.0-20260212111125-e1480776d00e + github.com/osbuild/images v0.251.0 + github.com/sirupsen/logrus v1.9.4 + github.com/spf13/cobra v1.10.2 + github.com/spf13/pflag v1.0.10 + github.com/stretchr/testify v1.11.1 + go.yaml.in/yaml/v3 v3.0.4 + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 ) require ( - dario.cat/mergo v1.0.1 // indirect + dario.cat/mergo v1.0.2 // indirect + github.com/BurntSushi/toml v1.6.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/Microsoft/hcsshim v0.12.9 // indirect + github.com/Microsoft/hcsshim v0.13.0 // indirect github.com/VividCortex/ewma v1.2.0 // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect - github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect - github.com/aws/aws-sdk-go v1.55.6 // indirect - github.com/containerd/cgroups/v3 v3.0.3 // indirect - github.com/containerd/errdefs v0.3.0 // indirect + github.com/aws/aws-sdk-go-v2 v1.39.6 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 // indirect + github.com/aws/aws-sdk-go-v2/config v1.31.18 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.18.22 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.5 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13 // indirect + github.com/aws/aws-sdk-go-v2/service/ec2 v1.265.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.40.0 // indirect + github.com/aws/smithy-go v1.23.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clipperhouse/stringish v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.3.0 // indirect + github.com/containerd/cgroups/v3 v3.0.5 // indirect + github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect - github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.18.1 // indirect github.com/containerd/typeurl/v2 v2.2.3 // indirect - github.com/containers/common v0.62.0 // indirect - github.com/containers/image/v5 v5.34.0 // indirect + github.com/containers/common v0.64.2 // indirect + github.com/containers/image/v5 v5.36.2 // indirect github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect github.com/containers/ocicrypt v1.2.1 // indirect - github.com/containers/storage v1.57.1 // indirect - github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f // indirect - github.com/cyphar/filepath-securejoin v0.3.6 // indirect + github.com/containers/storage v1.59.1 // indirect + github.com/coreos/go-semver v0.3.1 // indirect + github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/distribution v2.8.3+incompatible // indirect - github.com/docker/docker v27.5.1+incompatible // indirect - github.com/docker/docker-credential-helpers v0.8.2 // indirect - github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/docker v28.3.3+incompatible // indirect + github.com/docker/docker-credential-helpers v0.9.4 // indirect + github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-jose/go-jose/v4 v4.0.4 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-jose/go-jose/v4 v4.1.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/analysis v0.23.0 // indirect - github.com/go-openapi/errors v0.22.0 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/loads v0.22.0 // indirect - github.com/go-openapi/runtime v0.28.0 // indirect - github.com/go-openapi/spec v0.21.0 // indirect - github.com/go-openapi/strfmt v0.23.0 // indirect - github.com/go-openapi/swag v0.23.0 // indirect - github.com/go-openapi/validate v0.24.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/go-containerregistry v0.20.2 // indirect + github.com/google/go-containerregistry v0.20.6 // indirect github.com/google/go-intervals v0.0.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 // indirect - github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/compress v1.18.1 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec // indirect - github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/mattn/go-sqlite3 v1.14.24 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/mattn/go-sqlite3 v1.14.28 // indirect github.com/miekg/pkcs11 v1.1.1 // indirect - github.com/mistifyio/go-zfs/v3 v3.0.1 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mistifyio/go-zfs/v3 v3.1.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/sys/capability v0.4.0 // indirect github.com/moby/sys/mountinfo v0.7.2 // indirect - github.com/moby/sys/user v0.3.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/oklog/ulid v1.3.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0 // indirect - github.com/opencontainers/runtime-spec v1.2.0 // indirect - github.com/opencontainers/selinux v1.11.1 // indirect - github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/opencontainers/runtime-spec v1.3.0 // indirect + github.com/opencontainers/selinux v1.12.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/proglottis/gpgme v0.1.4 // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect - github.com/sigstore/fulcio v1.6.4 // indirect - github.com/sigstore/rekor v1.3.8 // indirect - github.com/sigstore/sigstore v1.8.12 // indirect + github.com/proglottis/gpgme v0.1.5 // indirect + github.com/secure-systems-lab/go-securesystemslib v0.9.1 // indirect + github.com/sigstore/fulcio v1.8.1 // indirect + github.com/sigstore/protobuf-specs v0.5.0 // indirect + github.com/sigstore/sigstore v1.9.6-0.20250729224751-181c5d3339b3 // indirect github.com/smallstep/pkcs7 v0.1.1 // indirect github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect - github.com/sylabs/sif/v2 v2.20.2 // indirect - github.com/tchap/go-patricia/v2 v2.3.2 // indirect + github.com/sylabs/sif/v2 v2.21.1 // indirect + github.com/tchap/go-patricia/v2 v2.3.3 // indirect github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect - github.com/ulikunitz/xz v0.5.12 // indirect - github.com/vbatts/tar-split v0.11.7 // indirect - github.com/vbauerster/mpb/v8 v8.9.1 // indirect - go.mongodb.org/mongo-driver v1.14.0 // indirect + github.com/ulikunitz/xz v0.5.15 // indirect + github.com/vbatts/tar-split v0.12.2 // indirect + github.com/vbauerster/mpb/v8 v8.10.2 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect - go.opentelemetry.io/otel v1.34.0 // indirect - go.opentelemetry.io/otel/metric v1.34.0 // indirect - go.opentelemetry.io/otel/trace v1.34.0 // indirect - golang.org/x/crypto v0.33.0 // indirect - golang.org/x/net v0.35.0 // indirect - golang.org/x/sync v0.11.0 // indirect - golang.org/x/term v0.29.0 // indirect - golang.org/x/text v0.22.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 // indirect - google.golang.org/grpc v1.70.0 // indirect - google.golang.org/protobuf v1.36.5 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/grpc v1.79.3 // indirect + google.golang.org/protobuf v1.36.10 // indirect gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/bib/go.sum b/bib/go.sum index ee68d2c7b..8b65e7da7 100644 --- a/bib/go.sum +++ b/bib/go.sum @@ -1,25 +1,59 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= -dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -github.com/14rcole/gopopulate v0.0.0-20180821133914-b175b219e774 h1:SCbEWT58NSt7d2mcFdvxC9uyrdcTfvBbPLThhkDmXzg= -github.com/14rcole/gopopulate v0.0.0-20180821133914-b175b219e774/go.mod h1:6/0dYRLLXyJjbkIPeeGyoJ/eKOSI0eU6eTlCBYibgd0= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= -github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/Microsoft/hcsshim v0.12.9 h1:2zJy5KA+l0loz1HzEGqyNnjd3fyZA31ZBCGKacp6lLg= -github.com/Microsoft/hcsshim v0.12.9/go.mod h1:fJ0gkFAna6ukt0bLdKB8djt4XIJhF/vEPuoIWYVvZ8Y= +github.com/Microsoft/hcsshim v0.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWSeaA= +github.com/Microsoft/hcsshim v0.13.0/go.mod h1:9KWJ/8DgU+QzYGupX4tzMhRQE8h6w90lH6HAaclpEok= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= -github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk= +github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 h1:DHctwEM8P8iTXFxC/QK0MRjwEpWQeM9yzidCRjldUz0= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3/go.mod h1:xdCzcZEtnSTKVDOmUZs4l/j3pSV6rpo1WXl5ugNsL8Y= +github.com/aws/aws-sdk-go-v2/config v1.31.18 h1:RouG3AcF2fLFhw+Z0qbnuIl9HZ0Kh4E/U9sKwTMRpMI= +github.com/aws/aws-sdk-go-v2/config v1.31.18/go.mod h1:aXZ13mSQC8S2VEHwGfL1COMuJ1Zty6pX5xU7hyqjvCg= +github.com/aws/aws-sdk-go-v2/credentials v1.18.22 h1:hyIVGBHhQPaNP9D4BaVRwpjLMCwMMdAkHqB3gGMiykU= +github.com/aws/aws-sdk-go-v2/credentials v1.18.22/go.mod h1:B9E2qHs3/YGfeQZ4jrIE/nPvqxtyafZrJ5EQiZBG6pk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.5 h1:EDTQlpZsebBESeYoPN+TjHyU1Dher3wb3mJDG57tZ8k= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.5/go.mod h1:iRuL2scabwI/oO3KhHaqCrWlCxWiYzvmX8JGSi1iBks= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 h1:HBSI2kDkMdWz4ZM7FjwE7e/pWDEZ+nR95x8Ztet1ooY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13 h1:eg/WYAa12vqTphzIdWMzqYRVKKnCboVPRlvaybNCqPA= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13/go.mod h1:/FDdxWhz1486obGrKKC1HONd7krpk38LBt+dutLcN9k= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.265.0 h1:c3P7906uMLhQTz0L7KIjez3Sr2axS4w6kRcS6IvqOss= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.265.0/go.mod h1:NDdDLLW5PtLLXN661gKcvJvqAH5OBXsfhMlmKVu1/pY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 h1:NvMjwvv8hpGUILarKw7Z4Q0w1H9anXKsesMxtw++MA4= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4/go.mod h1:455WPHSwaGj2waRSpQp7TsnpOnBfw8iDfPfbwl7KPJE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 h1:zhBJXdhWIFZ1acfDYIhu4+LCzdUS2Vbcum7D01dXlHQ= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13/go.mod h1:JaaOeCE368qn2Hzi3sEzY6FgAZVCIYcC2nwbro2QCh8= +github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0 h1:ef6gIJR+xv/JQWwpa5FYirzoQctfSJm7tuDe3SZsUf8= +github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0/go.mod h1:+wArOOrcHUevqdto9k1tKOF5++YTe9JEcPSc9Tx2ZSw= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.1/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo= +github.com/aws/aws-sdk-go-v2/service/sts v1.40.0 h1:ZGDJVmlpPFiNFCb/I42nYVKUanJAdFUiSmUo/32AqPQ= +github.com/aws/aws-sdk-go-v2/service/sts v1.40.0/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk= +github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= +github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= @@ -27,53 +61,59 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cheggaaa/pb/v3 v3.1.6 h1:h0x+vd7EiUohAJ29DJtJy+SNAc55t/elW3jCD086EXk= -github.com/cheggaaa/pb/v3 v3.1.6/go.mod h1:urxmfVtaxT+9aWk92DbsvXFZtNSWQSO5TRAp+MJ3l1s= +github.com/cheggaaa/pb/v3 v3.1.7 h1:2FsIW307kt7A/rz/ZI2lvPO+v3wKazzE4K/0LtTWsOI= +github.com/cheggaaa/pb/v3 v3.1.7/go.mod h1:/Ji89zfVPeC/u5j8ukD0MBPHt2bzTYp74lQ7KlgFWTQ= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= +github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= +github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/containerd/cgroups/v3 v3.0.3 h1:S5ByHZ/h9PMe5IOQoN7E+nMc2UcLEM/V48DGDJ9kip0= -github.com/containerd/cgroups/v3 v3.0.3/go.mod h1:8HBe7V3aWGLFPd/k03swSIsGjZhHI2WzJmticMgVuz0= -github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4= -github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo= +github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= -github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= +github.com/containerd/stargz-snapshotter/estargz v0.18.1 h1:cy2/lpgBXDA3cDKSyEfNOFMA/c10O1axL69EU7iirO8= +github.com/containerd/stargz-snapshotter/estargz v0.18.1/go.mod h1:ALIEqa7B6oVDsrF37GkGN20SuvG/pIMm7FwP7ZmRb0Q= github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= -github.com/containers/common v0.62.0 h1:Sl9WE5h7Y/F3bejrMAA4teP1EcY9ygqJmW4iwSloZ10= -github.com/containers/common v0.62.0/go.mod h1:Yec+z8mrSq4rydHofrnDCBqAcNA/BGrSg1kfFUL6F6s= -github.com/containers/image/v5 v5.34.0 h1:HPqQaDUsox/3mC1pbOyLAIQEp0JhQqiUZ+6JiFIZLDI= -github.com/containers/image/v5 v5.34.0/go.mod h1:/WnvUSEfdqC/ahMRd4YJDBLrpYWkGl018rB77iB3FDo= +github.com/containers/common v0.64.2 h1:1xepE7QwQggUXxmyQ1Dbh6Cn0yd7ktk14sN3McSWf5I= +github.com/containers/common v0.64.2/go.mod h1:o29GfYy4tefUuShm8mOn2AiL5Mpzdio+viHI7n24KJ4= +github.com/containers/image/v5 v5.36.2 h1:GcxYQyAHRF/pLqR4p4RpvKllnNL8mOBn0eZnqJbfTwk= +github.com/containers/image/v5 v5.36.2/go.mod h1:b4GMKH2z/5t6/09utbse2ZiLK/c72GuGLFdp7K69eA4= github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 h1:Qzk5C6cYglewc+UyGf6lc8Mj2UaPTHy/iF2De0/77CA= github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY= github.com/containers/ocicrypt v1.2.1 h1:0qIOTT9DoYwcKmxSt8QJt+VzMY18onl9jUXsxpVhSmM= github.com/containers/ocicrypt v1.2.1/go.mod h1:aD0AAqfMp0MtwqWgHM1bUwe1anx0VazI108CRrSKINQ= -github.com/containers/storage v1.57.1 h1:hKPoFsuBcB3qTzBxa4IFpZMRzUuL5Xhv/BE44W0XHx8= -github.com/containers/storage v1.57.1/go.mod h1:i/Hb4lu7YgFr9G0K6BMjqW0BLJO1sFsnWQwj2UoWCUM= +github.com/containers/storage v1.59.1 h1:11Zu68MXsEQGBBd+GadPrHPpWeqjKS8hJDGiAHgIqDs= +github.com/containers/storage v1.59.1/go.mod h1:KoAYHnAjP3/cTsRS+mmWZGkufSY2GACiKQ4V3ZLQnR0= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f h1:eHnXnuK47UlSTOQexbzxAZfekVz6i+LKRdj1CU5DPaM= -github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= -github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= -github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli v27.5.1+incompatible h1:JB9cieUT9YNiMITtIsguaN55PLOHhBSz3LKVc6cqWaY= -github.com/docker/cli v27.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v28.3.2+incompatible h1:mOt9fcLE7zaACbxW1GeS65RI67wIJrTnqS3hP2huFsY= +github.com/docker/cli v28.3.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v27.5.1+incompatible h1:4PYU5dnBYqRQi0294d1FBECqT9ECWeQAIfE8q4YnPY8= -github.com/docker/docker v27.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= -github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= +github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.9.4 h1:76ItO69/AP/V4yT9V4uuuItG0B1N8hvt0T0c0NN/DzI= +github.com/docker/docker-credential-helpers v0.9.4/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -86,33 +126,13 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= -github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= -github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= -github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= -github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco= -github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs= -github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ= -github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc= -github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= -github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= -github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= -github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= -github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= -github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= @@ -121,8 +141,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -141,10 +161,11 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo= -github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU= +github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y= github.com/google/go-intervals v0.0.2 h1:FGrVEiUnTRKR8yE04qzXYaJMtnIYqobR5QbblK3ixcM= github.com/google/go-intervals v0.0.2/go.mod h1:MkaR3LNRfeKLPmqgJYs4E66z5InYjmCjbbr4TQlcT6Y= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -154,8 +175,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -165,20 +186,14 @@ github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKe github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW34dhU4az1GN0pTPADwNmvoRSeoZ6PItiqnY= -github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs= github.com/jmhodges/clock v1.2.0/go.mod h1:qKjhA7x7u/lQpPB1XAqX1b1lCI/w3/fNuYpI/ZjLynI= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -187,32 +202,32 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec h1:2tTW6cDth2TSgRbAhD7yjZzTQmcN25sDRPEeinR51yQ= github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec/go.mod h1:TmwEoGCwIti7BCeJ9hescZgRtatxRE+A72pCoPfmcfk= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= -github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= +github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/mistifyio/go-zfs/v3 v3.0.1 h1:YaoXgBePoMA12+S1u/ddkv+QqxcfiZK4prI6HPnkFiU= -github.com/mistifyio/go-zfs/v3 v3.0.1/go.mod h1:CzVgeB0RvF2EGzQnytKVvVSDwmKJXxkOTUGbNrTja/k= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mistifyio/go-zfs/v3 v3.1.0 h1:FZaylcg0hjUp27i23VcJJQiuBeAZjrC8lPqCGM1CopY= +github.com/mistifyio/go-zfs/v3 v3.1.0/go.mod h1:CzVgeB0RvF2EGzQnytKVvVSDwmKJXxkOTUGbNrTja/k= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk= github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I= github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= -github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= -github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -222,119 +237,114 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= -github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/selinux v1.11.1 h1:nHFvthhM0qY8/m+vfhJylliSshm8G1jJ2jDMcgULaH8= -github.com/opencontainers/selinux v1.11.1/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= -github.com/osbuild/images v0.120.0 h1:6zXCp59AG03qajZlg/GJ07Fr4E6z5qaZshOuWgAse7g= -github.com/osbuild/images v0.120.0/go.mod h1:Ag87vmyxooiPQBJEDILbypG8/SRIear75YA78NwLix0= -github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f h1:/UDgs8FGMqwnHagNDPGOlts35QkhAZ8by3DR7nMih7M= -github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f/go.mod h1:J6OG6YJVEWopen4avK3VNQSnALmmjvniMmni/YFYAwc= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg= +github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/selinux v1.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplUkdTrmPb8= +github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U= +github.com/osbuild/blueprint v1.26.0 h1:OIXnlrPh2wcmuw3ZKfxTuXS4T0MHbFWSWF7AarWd220= +github.com/osbuild/blueprint v1.26.0/go.mod h1:HPlJzkEl7q5g8hzaGksUk7ifFAy9QFw9LmzhuFOAVm4= +github.com/osbuild/image-builder-cli v0.0.0-20260212111125-e1480776d00e h1:y8AKA9HROboNWnAmgUhwA4YFQM9x0i7XbCPw+Peswqo= +github.com/osbuild/image-builder-cli v0.0.0-20260212111125-e1480776d00e/go.mod h1:d3rG7oIFj/SeqYVX6AWJeEbyJL6maGiIOjSfxakRhiA= +github.com/osbuild/images v0.251.0 h1:wBDQPgtjVSXN+tv0v0Q0jqxAFAvlFwNDYtA8z7uly1I= +github.com/osbuild/images v0.251.0/go.mod h1:Wq/bMjrzTBCn0S+wn6AZ0eqA3vRAy4TYOw5bXDnOlmk= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/proglottis/gpgme v0.1.4 h1:3nE7YNA70o2aLjcg63tXMOhPD7bplfE5CBdV+hLAm2M= -github.com/proglottis/gpgme v0.1.4/go.mod h1:5LoXMgpE4bttgwwdv9bLs/vwqv3qV7F4glEEZ7mRKrM= -github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= -github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/proglottis/gpgme v0.1.5 h1:KCGyOw8sQ+SI96j6G8D8YkOGn+1TwbQTT9/zQXoVlz0= +github.com/proglottis/gpgme v0.1.5/go.mod h1:5LoXMgpE4bttgwwdv9bLs/vwqv3qV7F4glEEZ7mRKrM= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.57.0 h1:Ro/rKjwdq9mZn1K5QPctzh+MA4Lp0BuYk5ZZEVhoNcY= -github.com/prometheus/common v0.57.0/go.mod h1:7uRPFSUTbfZWsJ7MHY56sqt7hLQu3bxXHDnNhl8E9qI= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= +github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/sebdah/goldie/v2 v2.5.5 h1:rx1mwF95RxZ3/83sdS4Yp7t2C5TCokvWP4TBRbAyEWY= github.com/sebdah/goldie/v2 v2.5.5/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= -github.com/secure-systems-lab/go-securesystemslib v0.9.0 h1:rf1HIbL64nUpEIZnjLZ3mcNEL9NBPB0iuVjyxvq3LZc= -github.com/secure-systems-lab/go-securesystemslib v0.9.0/go.mod h1:DVHKMcZ+V4/woA/peqr+L0joiRXbPpQ042GgJckkFgw= +github.com/secure-systems-lab/go-securesystemslib v0.9.1 h1:nZZaNz4DiERIQguNy0cL5qTdn9lR8XKHf4RUyG1Sx3g= +github.com/secure-systems-lab/go-securesystemslib v0.9.1/go.mod h1:np53YzT0zXGMv6x4iEWc9Z59uR+x+ndLwCLqPYpLXVU= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= -github.com/sigstore/fulcio v1.6.4 h1:d86obfxUAG3Y6CYwOx1pdwCZwKmROB6w6927pKOVIRY= -github.com/sigstore/fulcio v1.6.4/go.mod h1:Y6bn3i3KGhXpaHsAtYP3Z4Np0+VzCo1fLv8Ci6mbPDs= -github.com/sigstore/rekor v1.3.8 h1:B8kJI8mpSIXova4Jxa6vXdJyysRxFGsEsLKBDl0rRjA= -github.com/sigstore/rekor v1.3.8/go.mod h1:/dHFYKSuxEygfDRnEwyJ+ZD6qoVYNXQdi1mJrKvKWsI= -github.com/sigstore/sigstore v1.8.12 h1:S8xMVZbE2z9ZBuQUEG737pxdLjnbOIcFi5v9UFfkJFc= -github.com/sigstore/sigstore v1.8.12/go.mod h1:+PYQAa8rfw0QdPpBcT+Gl3egKD9c+TUgAlF12H3Nmjo= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sigstore/fulcio v1.8.1 h1:PmoQv3XmhjR2BWFWw5LcMUXJPmhyizOIL7HeYnpio58= +github.com/sigstore/fulcio v1.8.1/go.mod h1:7tP3KW9eCGlPYRj5N4MSuUOat7CkeIHuXZ2jAUQ+Rwc= +github.com/sigstore/protobuf-specs v0.5.0 h1:F8YTI65xOHw70NrvPwJ5PhAzsvTnuJMGLkA4FIkofAY= +github.com/sigstore/protobuf-specs v0.5.0/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc= +github.com/sigstore/sigstore v1.9.6-0.20250729224751-181c5d3339b3 h1:IEhSeWfhTd0kaBpHUXniWU2Tl5K5OUACN69mi1WGd+8= +github.com/sigstore/sigstore v1.9.6-0.20250729224751-181c5d3339b3/go.mod h1:JuqyPRJYnkNl6OTnQiG503EUnKih4P5EV6FUw+1B0iA= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/smallstep/pkcs7 v0.1.1 h1:x+rPdt2W088V9Vkjho4KtoggyktZJlMduZAtRHm68LU= github.com/smallstep/pkcs7 v0.1.1/go.mod h1:dL6j5AIz9GHjVEBTXtW+QliALcgM19RtXaTeyxI+AfA= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 h1:pnnLyeX7o/5aX8qUQ69P/mLojDqwda8hFOCBTmP/6hw= github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/sylabs/sif/v2 v2.20.2 h1:HGEPzauCHhIosw5o6xmT3jczuKEuaFzSfdjAsH33vYw= -github.com/sylabs/sif/v2 v2.20.2/go.mod h1:WyYryGRaR4Wp21SAymm5pK0p45qzZCSRiZMFvUZiuhc= -github.com/tchap/go-patricia/v2 v2.3.2 h1:xTHFutuitO2zqKAQ5rCROYgUb7Or/+IC3fts9/Yc7nM= -github.com/tchap/go-patricia/v2 v2.3.2/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/sylabs/sif/v2 v2.21.1 h1:GZ0b5//AFAqJEChd8wHV/uSKx/l1iuGYwjR8nx+4wPI= +github.com/sylabs/sif/v2 v2.21.1/go.mod h1:YoqEGQnb5x/ItV653bawXHZJOXQaEWpGwHsSD3YePJI= +github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc= +github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= -github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= -github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/vbatts/tar-split v0.11.7 h1:ixZ93pO/GmvaZw4Vq9OwmfZK/kc2zKdPfu0B+gYqs3U= -github.com/vbatts/tar-split v0.11.7/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= -github.com/vbauerster/mpb/v8 v8.9.1 h1:LH5R3lXPfE2e3lIGxN7WNWv3Hl5nWO6LRi2B0L0ERHw= -github.com/vbauerster/mpb/v8 v8.9.1/go.mod h1:4XMvznPh8nfe2NpnDo1QTPvW9MVkUhbG90mPWvmOzcQ= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= +github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= +github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= +github.com/vbauerster/mpb/v8 v8.10.2 h1:2uBykSHAYHekE11YvJhKxYmLATKHAGorZwFlyNw4hHM= +github.com/vbauerster/mpb/v8 v8.10.2/go.mod h1:+Ja4P92E3/CorSZgfDtK46D7AVbDqmBQRTmyTqPElo0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= -go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= -go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= -go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk= -go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= -go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= -go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= -go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= -go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= -go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= -go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= -go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= -go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94= -go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -343,11 +353,11 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= -golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 h1:9kj3STMvgqy3YA4VQXBrN7925ICMxD5wzMRcgA30588= -golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -358,8 +368,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= -golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -376,8 +386,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -390,8 +400,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -399,7 +409,6 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -408,8 +417,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -419,8 +428,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= -golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -430,10 +439,10 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= -golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -446,29 +455,30 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= -golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 h1:Pw6WnI9W/LIdRxqK7T6XGugGbHIRl5Q7q3BssH6xk4s= -google.golang.org/genproto/googleapis/api v0.0.0-20250124145028-65684f501c47 h1:5iw9XJTD4thFidQmFVvx0wi4g5yOHk76rNRUxz1ZG5g= -google.golang.org/genproto/googleapis/api v0.0.0-20250124145028-65684f501c47/go.mod h1:AfA77qWLcidQWywD0YgqfpJzf50w2VjzBml3TybHeJU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 h1:2duwAxN2+k0xLNpjnHTXoMUgnv6VPSp5fiqTuwSxjmI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= -google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -478,22 +488,17 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= -gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= -gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/bib/internal/buildconfig/config.go b/bib/internal/buildconfig/config.go deleted file mode 100644 index 1ad3fb8de..000000000 --- a/bib/internal/buildconfig/config.go +++ /dev/null @@ -1,118 +0,0 @@ -package buildconfig - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "os" - "path/filepath" - - "github.com/BurntSushi/toml" - "github.com/sirupsen/logrus" - - "github.com/osbuild/images/pkg/blueprint" -) - -// legacyBuildConfig is the json based configuration that was used in -// bootc-image-builder before PR#395. It was essentially a blueprint -// with just the extra layer of "blueprint". Supporting it still makes -// the transition of existing users/docs easier. -type legacyBuildConfig struct { - Blueprint *json.RawMessage `json:"blueprint"` -} - -type BuildConfig blueprint.Blueprint - -// configRootDir is only overriden in tests -var configRootDir = "/" - -func decodeJsonBuildConfig(r io.Reader, what string) (*BuildConfig, error) { - content, err := io.ReadAll(r) - if err != nil && err != io.EOF { - return nil, fmt.Errorf("cannot read %q: %w", what, err) - } - - // support for legacy json before 2024/05 - var legacyBC legacyBuildConfig - if err := json.Unmarshal(content, &legacyBC); err == nil { - if legacyBC.Blueprint != nil { - logrus.Warningf("Using legacy config") - content = *legacyBC.Blueprint - } - } - - dec := json.NewDecoder(bytes.NewBuffer(content)) - dec.DisallowUnknownFields() - - var conf BuildConfig - if err := dec.Decode(&conf); err != nil { - return nil, fmt.Errorf("cannot decode %q: %w", what, err) - } - if dec.More() { - return nil, fmt.Errorf("multiple configuration objects or extra data found in %q", what) - } - return &conf, nil -} - -func decodeTomlBuildConfig(r io.Reader, what string) (*BuildConfig, error) { - dec := toml.NewDecoder(r) - - var conf BuildConfig - _, err := dec.Decode(&conf) - if err != nil { - return nil, fmt.Errorf("cannot decode %q: %w", what, err) - } - - return &conf, nil -} - -var osStdin = os.Stdin - -func loadConfig(path string) (*BuildConfig, error) { - var fp *os.File - var err error - - if path == "-" { - fp = osStdin - } else { - fp, err = os.Open(path) - if err != nil { - return nil, err - } - defer fp.Close() - } - - switch { - case path == "-", filepath.Ext(path) == ".json": - return decodeJsonBuildConfig(fp, path) - case filepath.Ext(path) == ".toml": - return decodeTomlBuildConfig(fp, path) - default: - return nil, fmt.Errorf("unsupported file extension for %q", path) - } -} - -func ReadWithFallback(userConfig string) (*BuildConfig, error) { - // user asked for an explicit config - if userConfig != "" { - return loadConfig(userConfig) - } - - // check default configs - var foundConfig string - for _, dflConfigFile := range []string{"config.toml", "config.json"} { - cnfPath := filepath.Join(configRootDir, dflConfigFile) - if _, err := os.Stat(cnfPath); err == nil { - if foundConfig != "" { - return nil, fmt.Errorf("found %q and also %q, only a single one is supported", dflConfigFile, filepath.Base(foundConfig)) - } - foundConfig = cnfPath - } - } - if foundConfig == "" { - return &BuildConfig{}, nil - } - - return loadConfig(foundConfig) -} diff --git a/bib/internal/buildconfig/config_test.go b/bib/internal/buildconfig/config_test.go deleted file mode 100644 index aa56a6738..000000000 --- a/bib/internal/buildconfig/config_test.go +++ /dev/null @@ -1,185 +0,0 @@ -package buildconfig_test - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/osbuild/images/pkg/blueprint" - - "github.com/osbuild/bootc-image-builder/bib/internal/buildconfig" -) - -var expectedBuildConfig = &buildconfig.BuildConfig{ - Customizations: &blueprint.Customizations{ - User: []blueprint.UserCustomization{ - { - Name: "alice", - }, - }, - }, -} - -var fakeConfigJSON = `{ - "customizations": { - "user": [ - { - "name": "alice" - } - ] - } -}` - -var fakeConfigToml = ` -[[customizations.user]] -name = "alice" -` - -func makeFakeConfig(t *testing.T, filename, content string) string { - tmpdir := t.TempDir() - fakeCfgPath := filepath.Join(tmpdir, filename) - err := os.WriteFile(fakeCfgPath, []byte(content), 0644) - assert.NoError(t, err) - return fakeCfgPath -} - -func TestReadWithFallbackUserNoConfigNoFallack(t *testing.T) { - cfg, err := buildconfig.ReadWithFallback("") - assert.NoError(t, err) - assert.Equal(t, &buildconfig.BuildConfig{}, cfg) -} - -func TestReadWithFallbackUserProvidedConfig(t *testing.T) { - for _, tc := range []struct { - fname string - content string - }{ - {"config.toml", fakeConfigToml}, - {"config.json", fakeConfigJSON}, - } { - fakeUserCnfPath := makeFakeConfig(t, tc.fname, tc.content) - - cfg, err := buildconfig.ReadWithFallback(fakeUserCnfPath) - assert.NoError(t, err) - assert.Equal(t, expectedBuildConfig, cfg) - } -} - -func TestReadWithFallProvidedConfig(t *testing.T) { - for _, tc := range []struct { - fname string - content string - }{ - {"config.toml", fakeConfigToml}, - {"config.json", fakeConfigJSON}, - } { - fakeCnfPath := makeFakeConfig(t, tc.fname, tc.content) - restore := buildconfig.MockConfigRootDir(filepath.Dir(fakeCnfPath)) - defer restore() - - cfg, err := buildconfig.ReadWithFallback("") - assert.NoError(t, err) - assert.Equal(t, expectedBuildConfig, cfg) - } -} - -func TestReadUserConfigErrorWrongFormat(t *testing.T) { - for _, tc := range []struct { - fname, content string - expectedErr string - }{ - // wrong content, json in a toml file and vice-versa - {"config.toml", fakeConfigJSON, "cannot decode"}, - {"config.json", fakeConfigToml, "cannot decode"}, - } { - fakeCnfPath := makeFakeConfig(t, tc.fname, tc.content) - - _, err := buildconfig.ReadWithFallback(fakeCnfPath) - assert.ErrorContains(t, err, tc.expectedErr) - } -} - -func TestReadUserConfigTwoConfigsError(t *testing.T) { - tmpdir := t.TempDir() - for _, fname := range []string{"config.json", "config.toml"} { - err := os.WriteFile(filepath.Join(tmpdir, fname), nil, 0644) - assert.NoError(t, err) - } - restore := buildconfig.MockConfigRootDir(tmpdir) - defer restore() - - _, err := buildconfig.ReadWithFallback("") - assert.ErrorContains(t, err, `found "config.json" and also "config.toml", only a single one is supported`) -} - -var fakeLegacyConfigJSON = `{ - "blueprint": { - "customizations": { - "user": [ - { - "name": "alice" - } - ] - } - } -}` - -func TestReadLegacyJSONConfig(t *testing.T) { - fakeUserCnfPath := makeFakeConfig(t, "config.json", fakeLegacyConfigJSON) - cfg, err := buildconfig.ReadWithFallback(fakeUserCnfPath) - assert.NoError(t, err) - assert.Equal(t, expectedBuildConfig, cfg) -} - -func TestJsonUnknownKeysError(t *testing.T) { - fakeUserCnfPath := makeFakeConfig(t, "config.json", ` -{ - "birds": [ - { - "name": "toucan" - } - ] -} -`) - _, err := buildconfig.ReadWithFallback(fakeUserCnfPath) - - assert.ErrorContains(t, err, `json: unknown field "birds"`) -} - -func TestReadConfigIsssue655(t *testing.T) { - fakeUserCnfPath := makeFakeConfig(t, "config.toml", ` -[[customizations.filesystem]] -mountpoint = "/" -minsize = 1000 -`) - - conf, err := buildconfig.ReadWithFallback(fakeUserCnfPath) - assert.NoError(t, err) - assert.Equal(t, &buildconfig.BuildConfig{ - Customizations: &blueprint.Customizations{ - Filesystem: []blueprint.FilesystemCustomization{ - { - Mountpoint: "/", - MinSize: 1000, - }, - }, - }, - }, conf) -} - -func TestReadWithFallbackFromStdin(t *testing.T) { - fakeUserCnfPath := makeFakeConfig(t, "fake-stdin", fakeConfigJSON) - fakeStdinFp, err := os.Open(fakeUserCnfPath) - require.NoError(t, err) - defer fakeStdinFp.Close() - - restore := buildconfig.MockOsStdin(fakeStdinFp) - defer restore() - - cfg, err := buildconfig.ReadWithFallback("-") - assert.NoError(t, err) - assert.Equal(t, expectedBuildConfig, cfg) -} diff --git a/bib/internal/buildconfig/export_test.go b/bib/internal/buildconfig/export_test.go deleted file mode 100644 index 13f7d9069..000000000 --- a/bib/internal/buildconfig/export_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package buildconfig - -import ( - "os" -) - -func MockConfigRootDir(newDir string) (restore func()) { - saved := configRootDir - configRootDir = newDir - return func() { - configRootDir = saved - } -} - -func MockOsStdin(new *os.File) (restore func()) { - saved := osStdin - osStdin = new - return func() { - osStdin = saved - } -} diff --git a/bib/internal/container/container.go b/bib/internal/container/container.go deleted file mode 100644 index 454ec7109..000000000 --- a/bib/internal/container/container.go +++ /dev/null @@ -1,166 +0,0 @@ -package container - -import ( - "encoding/json" - "fmt" - "os" - "os/exec" - "strings" - - "golang.org/x/exp/slices" - - "github.com/osbuild/bootc-image-builder/bib/internal/util" -) - -// Container is a simpler wrapper around a running podman container. -// This type isn't meant as a general-purpose container management tool, but -// as an opinonated library for bootc-image-builder. -type Container struct { - id string - root string -} - -// New creates a new running container from the given image reference. -// -// NB: -// - --net host is used to make networking work in a nested container -// - /run/secrets is mounted from the host to make sure RHSM credentials are available -func New(ref string) (*Container, error) { - const secretDir = "/run/secrets" - secretVolume := fmt.Sprintf("%s:%s", secretDir, secretDir) - - args := []string{ - "run", - "--rm", - "--init", // If sleep infinity is run as PID 1, it doesn't get signals, thus we cannot easily stop the container - "--detach", - "--net", "host", // Networking in a nested container doesn't work without re-using this container's network - "--entrypoint", "sleep", // The entrypoint might be arbitrary, so let's just override it with sleep, we don't want to run anything - } - - // Re-mount the secret directory if it exists - if _, err := os.Stat(secretDir); err == nil { - args = append(args, "--volume", secretVolume) - } - - args = append(args, ref, "infinity") - - output, err := exec.Command("podman", args...).Output() - if err != nil { - if e, ok := err.(*exec.ExitError); ok { - return nil, fmt.Errorf("running %s container failed: %w\nstderr:\n%s", ref, e, e.Stderr) - } - return nil, fmt.Errorf("running %s container failed with generic error: %w", ref, err) - } - - c := &Container{} - c.id = strings.TrimSpace(string(output)) - // Ensure that the container is stopped when this function errors - defer func() { - if err != nil { - if stopErr := c.Stop(); stopErr != nil { - err = fmt.Errorf("%w\nstopping the container failed too: %s", err, stopErr) - } - c = nil - } - }() - - output, err = exec.Command("podman", "mount", c.id).Output() - if err != nil { - if err, ok := err.(*exec.ExitError); ok { - return nil, fmt.Errorf("mounting %s container failed: %w\nstderr:\n%s", ref, err, err.Stderr) - } - return nil, fmt.Errorf("mounting %s container failed with generic error: %w", ref, err) - } - c.root = strings.TrimSpace(string(output)) - - return c, err -} - -// Stop stops the container. Since New() creates a container with --rm, this -// removes the container as well. -func (c *Container) Stop() error { - if output, err := exec.Command("podman", "stop", c.id).CombinedOutput(); err != nil { - return fmt.Errorf("stopping %s container failed: %w\noutput:\n%s", c.id, err, output) - } - // when the container is stopped by podman it may not honor the "--rm" - // that was passed in `New()` so manually remove the container here if it is still available - if output, err := exec.Command("podman", "rm", "--ignore", c.id).CombinedOutput(); err != nil { - return fmt.Errorf("removing %s container failed: %w\noutput:\n%s", c.id, err, output) - } - - return nil -} - -// Root returns the root directory of the container as available on the host. -func (c *Container) Root() string { - return c.root -} - -// Reads a file from the container -func (c *Container) ReadFile(path string) ([]byte, error) { - output, err := exec.Command("podman", "exec", c.id, "cat", path).Output() - if err != nil { - if err, ok := err.(*exec.ExitError); ok { - return nil, fmt.Errorf("reading %s from %s container failed: %w\nstderr:\n%s", path, c.id, err, err.Stderr) - } - return nil, fmt.Errorf("reading %s from %s container failed with generic error: %w", path, c.id, err) - } - - return output, nil -} - -// CopyInto copies a file into the container. -func (c *Container) CopyInto(src, dest string) error { - if output, err := exec.Command("podman", "cp", src, c.id+":"+dest).CombinedOutput(); err != nil { - return fmt.Errorf("copying %s into %s container failed: %w\noutput:\n%s", src, c.id, err, output) - } - - return nil -} - -func (c *Container) ExecArgv() []string { - return []string{"podman", "exec", "-i", c.id} -} - -// DefaultRootfsType returns the default rootfs type (e.g. "ext4") as -// specified by the bootc container install configuration. An empty -// string is valid and means the container sets no default. -func (c *Container) DefaultRootfsType() (string, error) { - output, err := exec.Command("podman", "exec", c.id, "bootc", "install", "print-configuration").Output() - if err != nil { - return "", fmt.Errorf("failed to run bootc install print-configuration: %w", util.OutputErr(err)) - } - - var bootcConfig struct { - Filesystem struct { - Root struct { - Type string `json:"type"` - } `json:"root"` - } `json:"filesystem"` - } - - if err := json.Unmarshal(output, &bootcConfig); err != nil { - return "", fmt.Errorf("failed to unmarshal bootc configuration: %w", err) - } - - // filesystem.root.type is the preferred way instead of the old root-fs-type top-level key. - // See https://github.com/containers/bootc/commit/558cd4b1d242467e0ffec77fb02b35166469dcc7 - fsType := bootcConfig.Filesystem.Root.Type - // Note that these are the only filesystems that the "images" library - // knows how to handle, i.e. how to construct the required osbuild - // stages for. - // TODO: move this into a helper in "images" so that there is only - // a single place that needs updating when we add e.g. btrfs or - // bcachefs - supportedFS := []string{"ext4", "xfs", "btrfs"} - - if fsType == "" { - return "", nil - } - if !slices.Contains(supportedFS, fsType) { - return "", fmt.Errorf("unsupported root filesystem type: %s, supported: %s", fsType, strings.Join(supportedFS, ", ")) - } - - return fsType, nil -} diff --git a/bib/internal/container/container_test.go b/bib/internal/container/container_test.go deleted file mode 100644 index 2aaf218a5..000000000 --- a/bib/internal/container/container_test.go +++ /dev/null @@ -1,173 +0,0 @@ -package container - -import ( - "bytes" - "encoding/json" - "fmt" - "os" - "os/exec" - "path" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const testingImage = "registry.access.redhat.com/ubi9-micro:latest" - -type containerInfo struct { - State string `json:"State"` - Image string `json:"Image"` -} - -type invalidContainerCountError struct { - id string - count int -} - -func (e invalidContainerCountError) Error() string { - return fmt.Sprintf("expected 1 container info for %s, got %d", e.id, e.count) -} - -func getContainerInfo(id string) (containerInfo, error) { - cmd := exec.Command("podman", "ps", "--filter", "id="+id, "--format", "json") - var stdout, stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - if err := cmd.Run(); err != nil { - return containerInfo{}, fmt.Errorf("checking status of %s failed: %w\nstderr:\n%s", id, err, stderr.String()) - } - - var infos []containerInfo - if err := json.Unmarshal(stdout.Bytes(), &infos); err != nil { - return containerInfo{}, fmt.Errorf("unmarshalling %s info failed: %w\nstdout:\n%s", id, err, stdout.String()) - } - - if len(infos) != 1 { - return containerInfo{}, invalidContainerCountError{id: id, count: len(infos)} - } - - return infos[0], nil -} - -func TestNew(t *testing.T) { - if os.Geteuid() != 0 { - t.Skip("skipping test; not running as root") - } - - c, err := New(testingImage) - require.NoError(t, err) - t.Cleanup(func() { - err = c.Stop() - assert.NoError(t, err) - - // double-check that the container indeed doesn't exist - _, infoErr := getContainerInfo(c.id) - assert.ErrorIs(t, infoErr, invalidContainerCountError{id: c.id, count: 0}) - }) - - info, err := getContainerInfo(c.id) - require.NoError(t, err) - assert.Equal(t, testingImage, info.Image) - assert.Equal(t, "running", info.State) - - root := c.Root() - osRelease, err := os.ReadFile(path.Join(root, "etc/os-release")) - require.NoError(t, err) - - assert.Contains(t, string(osRelease), `ID="rhel"`) -} - -func TestReadFile(t *testing.T) { - if os.Geteuid() != 0 { - t.Skip("skipping test; not running as root") - } - - c, err := New(testingImage) - require.NoError(t, err) - t.Cleanup(func() { - err = c.Stop() - assert.NoError(t, err) - }) - - content, err := c.ReadFile("/etc/os-release") - require.NoError(t, err) - require.Contains(t, string(content), `ID="rhel"`) -} - -func TestCopyInto(t *testing.T) { - if os.Geteuid() != 0 { - t.Skip("skipping test; not running as root") - } - - tmpdir := t.TempDir() - testfile := path.Join(tmpdir, "testfile") - require.NoError(t, os.WriteFile(testfile, []byte("Hello, world!"), 0644)) - - c, err := New(testingImage) - require.NoError(t, err) - t.Cleanup(func() { - err = c.Stop() - assert.NoError(t, err) - }) - - err = c.CopyInto(testfile, "/testfile") - require.NoError(t, err) - - root := c.Root() - testfileInContainer := path.Join(root, "testfile") - testfileContent, err := os.ReadFile(testfileInContainer) - require.NoError(t, err) - require.Equal(t, "Hello, world!", string(testfileContent)) -} - -func makeFakePodman(t *testing.T, content string) { - tmpdir := t.TempDir() - t.Setenv("PATH", tmpdir+":"+os.Getenv("PATH")) - - err := os.WriteFile(filepath.Join(tmpdir, "podman"), []byte(content), 0755) - assert.NoError(t, err) -} -func TestNewFakedUnhappy(t *testing.T) { - fakePodman := `#!/bin/sh -if [ "$1" = "mount" ]; then - >&2 echo "forced-crash" - exit 2 -fi -exec /usr/bin/podman "$@" -` - makeFakePodman(t, fakePodman) - _, err := New(testingImage) - assert.ErrorContains(t, err, fmt.Sprintf("mounting %s container failed: ", testingImage)) - assert.ErrorContains(t, err, "stderr:\nforced-crash") -} - -func TestRootfsTypeHappy(t *testing.T) { - for _, tc := range []string{"", "ext4", "xfs"} { - jsonStr := "{}" - if tc != "" { - jsonStr = fmt.Sprintf(`{"filesystem": {"root": {"type": "%s"}}}`, tc) - } - makeFakePodman(t, fmt.Sprintf(`#!/bin/sh -echo '%s' -`, jsonStr)) - cnt := Container{} - rootfs, err := cnt.DefaultRootfsType() - assert.NoError(t, err) - assert.Equal(t, tc, rootfs) - } -} - -func TestRootfsTypeSad(t *testing.T) { - for _, tc := range []string{"ext1"} { - jsonStr := fmt.Sprintf(`{"filesystem": {"root": {"type": "%s"}}}`, tc) - makeFakePodman(t, fmt.Sprintf(`#!/bin/sh -echo '%s' -`, jsonStr)) - cnt := Container{} - _, err := cnt.DefaultRootfsType() - assert.ErrorContains(t, err, "unsupported root filesystem type: ext1, supported: ") - } -} diff --git a/bib/internal/container/solver.go b/bib/internal/container/solver.go deleted file mode 100644 index 9381b92ba..000000000 --- a/bib/internal/container/solver.go +++ /dev/null @@ -1,125 +0,0 @@ -package container - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - - "github.com/osbuild/images/pkg/arch" - "github.com/osbuild/images/pkg/dnfjson" - - "github.com/osbuild/bootc-image-builder/bib/internal/source" -) - -func forceSymlink(symlinkPath, target string) error { - if output, err := exec.Command("ln", "-sf", target, symlinkPath).CombinedOutput(); err != nil { - return fmt.Errorf("cannot run ln: %w, output:\n%s", err, output) - } - return nil -} - -// InitDNF initializes dnf in the container. This is necessary when -// the caller wants to read the image's dnf repositories, but they are -// not static, but rather configured by dnf dynamically. The primaru -// use-case for this is RHEL and subscription-manager. -// -// The implementation is simple: We just run plain `dnf` in the -// container so that the subscription-manager gets initialized. For -// compatibility with both dnf and dnf5 we cannot just run "dnf" as -// dnf5 will error and do nothing in this case. So we use "dnf check -// --duplicates" as this is fast on both dnf4/dnf5 (just doing "dnf5 -// check" without arguments takes around 25s so that is not a great -// option). -func (c *Container) InitDNF() error { - if output, err := exec.Command("podman", "exec", c.id, "dnf", "check", "--duplicates").CombinedOutput(); err != nil { - return fmt.Errorf("initializing dnf in %s container failed: %w\noutput:\n%s", c.id, err, string(output)) - } - - return nil -} - -func (cnt *Container) hasRunSecrets() bool { - _, err := os.Stat(filepath.Join(cnt.root, "/run/secrets/redhat.repo")) - return err == nil -} - -// setupRunSecretsBindMount will synthesise a /run/secrets dir -// in the container root -func (cnt *Container) setupRunSecrets() error { - if cnt.hasRunSecrets() { - return nil - } - dst := filepath.Join(cnt.root, "/run/secrets") - if err := os.MkdirAll(dst, 0755); err != nil { - return err - } - - // We cannot just bind mount here because - // /usr/share/rhel/secrets contains a bunch of relative symlinks - // that will point to the container root not the host when resolved - // from the outside (via the host container mount). - // - // So instead of bind mounting we create a copy of the - // /run/secrets/ - they are static so that should be fine. - // - // We want to support /usr/share/rhel/secrets too to be able - // to run "bootc-image-builder manifest" directly on the host - // (which is useful for e.g. composer). - for _, src := range []string{"/run/secrets", "/usr/share/rhel/secrets"} { - if st, err := os.Stat(src); err != nil || !st.IsDir() { - continue - } - - dents, err := filepath.Glob(src + "/*") - if err != nil { - return err - } - for _, ent := range dents { - // Check if the target file actually exists (i.e. for - // symlinks that they are valid) and only copy if so. - // This covers unsubscribed machines. - if _, err := os.Stat(ent); err != nil { - continue - } - - // Note the use of "-L" here to dereference/copy links - if output, err := exec.Command("cp", "-rvL", ent, dst).CombinedOutput(); err != nil { - return fmt.Errorf("failed to setup /run/secrets: %w, output:\n%s", err, string(output)) - } - } - } - - // workaround broken containers (like f41) that use absolute symlinks - // to point to the entitlements-host and rhsm-host, they need to be - // relative so that the "SetRootdir()" from the resolver works, i.e. - // they need to point into the mounted container. - symlink := filepath.Join(cnt.root, "/etc/pki/entitlement-host") - target := "../../run/secrets/etc-pki-entitlement" - if err := forceSymlink(symlink, target); err != nil { - return err - } - symlink = filepath.Join(cnt.root, "/etc/rhsm-host") - target = "../run/secrets/rhsm" - if err := forceSymlink(symlink, target); err != nil { - return err - } - return nil -} - -func (cnt *Container) NewContainerSolver(cacheRoot string, architecture arch.Arch, sourceInfo *source.Info) (*dnfjson.Solver, error) { - solver := dnfjson.NewSolver( - sourceInfo.OSRelease.PlatformID, - sourceInfo.OSRelease.VersionID, - architecture.String(), - fmt.Sprintf("%s-%s", sourceInfo.OSRelease.ID, sourceInfo.OSRelease.VersionID), - cacheRoot) - - // we copy the data directly into the cnt.root, no need to - // cleanup here because podman stop will remove the dir - if err := cnt.setupRunSecrets(); err != nil { - return nil, err - } - solver.SetRootDir(cnt.root) - return solver, nil -} diff --git a/bib/internal/container/solver_test.go b/bib/internal/container/solver_test.go deleted file mode 100644 index ce5e72d79..000000000 --- a/bib/internal/container/solver_test.go +++ /dev/null @@ -1,198 +0,0 @@ -package container_test - -import ( - "os" - "os/exec" - "path/filepath" - "runtime" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/osbuild/images/pkg/arch" - "github.com/osbuild/images/pkg/rpmmd" - - "github.com/osbuild/bootc-image-builder/bib/internal/container" - "github.com/osbuild/bootc-image-builder/bib/internal/source" -) - -const ( - dnfTestingImageRHEL = "registry.access.redhat.com/ubi9:latest" - dnfTestingImageCentos = "quay.io/centos/centos:stream9" - dnfTestingImageFedoraLatest = "registry.fedoraproject.org/fedora:latest" -) - -func ensureCanRunDNFJsonTests(t *testing.T) { - if os.Geteuid() != 0 { - t.Skip("skipping test; not running as root") - } - if _, err := os.Stat("/usr/libexec/osbuild-depsolve-dnf"); err != nil { - t.Skip("cannot find /usr/libexec/osbuild-depsolve-dnf") - } -} - -func ensureAMD64(t *testing.T) { - if runtime.GOARCH != "amd64" { - t.Skip("skipping test; only runs on x86_64") - } -} - -func TestDNFJsonWorks(t *testing.T) { - ensureCanRunDNFJsonTests(t) - - cacheRoot := t.TempDir() - - cnt, err := container.New(dnfTestingImageCentos) - require.NoError(t, err) - defer func() { - assert.NoError(t, cnt.Stop()) - }() - - err = cnt.InitDNF() - require.NoError(t, err) - - sourceInfo, err := source.LoadInfo(cnt.Root()) - require.NoError(t, err) - solver, err := cnt.NewContainerSolver(cacheRoot, arch.Current(), sourceInfo) - require.NoError(t, err) - res, err := solver.Depsolve([]rpmmd.PackageSet{ - { - Include: []string{"coreutils"}, - }, - }, 0) - require.NoError(t, err) - assert.True(t, len(res.Packages) > 0) -} - -func subscribeMachine(t *testing.T) (restore func()) { - if _, err := exec.LookPath("subscription-manager"); err != nil { - t.Skip("no subscription-manager found") - return func() {} - } - - matches, err := filepath.Glob("/etc/pki/entitlement/*.pem") - if err == nil && len(matches) > 0 { - return func() {} - } - - rhsmOrg := os.Getenv("RHSM_ORG") - rhsmActivationKey := os.Getenv("RHSM_ACTIVATION_KEY") - if rhsmOrg == "" || rhsmActivationKey == "" { - t.Skip("no RHSM_{ORG,ACTIVATION_KEY} env vars found") - return func() {} - } - - err = exec.Command("subscription-manager", "register", - "--org", rhsmOrg, - "--activationkey", rhsmActivationKey).Run() - require.NoError(t, err) - - return func() { - err := exec.Command("subscription-manager", "unregister").Run() - require.NoError(t, err) - } -} - -func TestDNFInitGivesAccessToSubscribedContent(t *testing.T) { - if os.Geteuid() != 0 { - t.Skip("skipping test; not running as root") - } - ensureAMD64(t) - - restore := subscribeMachine(t) - defer restore() - - cnt, err := container.New(dnfTestingImageRHEL) - require.NoError(t, err) - err = cnt.InitDNF() - require.NoError(t, err) - - content, err := cnt.ReadFile("/etc/yum.repos.d/redhat.repo") - require.NoError(t, err) - assert.Contains(t, string(content), "rhel-9-for-x86_64-baseos-rpms") -} - -func TestDNFJsonWorkWithSubscribedContent(t *testing.T) { - ensureCanRunDNFJsonTests(t) - ensureAMD64(t) - cacheRoot := t.TempDir() - - restore := subscribeMachine(t) - defer restore() - - cnt, err := container.New(dnfTestingImageRHEL) - require.NoError(t, err) - defer func() { - assert.NoError(t, cnt.Stop()) - }() - - err = cnt.InitDNF() - require.NoError(t, err) - - sourceInfo, err := source.LoadInfo(cnt.Root()) - require.NoError(t, err) - solver, err := cnt.NewContainerSolver(cacheRoot, arch.ARCH_X86_64, sourceInfo) - require.NoError(t, err) - - res, err := solver.Depsolve([]rpmmd.PackageSet{ - { - Include: []string{"coreutils"}, - }, - }, 0) - require.NoError(t, err) - assert.True(t, len(res.Packages) > 0) -} - -func runCmd(t *testing.T, args ...string) { - cmd := exec.Command(args[0], args[1:]...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - err := cmd.Run() - require.NoError(t, err) -} - -func TestDNFJsonWorkWithSubscribedContentNestedContainers(t *testing.T) { - ensureCanRunDNFJsonTests(t) - ensureAMD64(t) - tmpdir := t.TempDir() - - restore := subscribeMachine(t) - defer restore() - - // build a test binary from the existing - // TestDNFJsonWorkWithSubscribedContent that is then - // transfered and run *inside* the centos container - testBinary := filepath.Join(tmpdir, "dnftest") - runCmd(t, "go", "test", - "-c", - "-o", testBinary, - "-run", "^TestDNFJsonWorkWithSubscribedContent$") - - output, err := exec.Command( - "podman", "run", "--rm", - "--privileged", - "--init", - "--detach", - "--entrypoint", "sleep", - // use a fedora container as intermediate so that we - // always have the latest glibc (we cannot fully - // static link the test) - dnfTestingImageFedoraLatest, - "infinity", - ).Output() - require.NoError(t, err, string(output)) - cntID := strings.TrimSpace(string(output)) - defer func() { - err := exec.Command("podman", "stop", cntID).Run() - assert.NoError(t, err) - }() - - runCmd(t, "podman", "cp", testBinary, cntID+":/dnftest") - // we need these test dependencies inside the container - runCmd(t, "podman", "exec", cntID, "dnf", "install", "-y", - "gpgme", "podman") - // run the test - runCmd(t, "podman", "exec", cntID, "/dnftest") -} diff --git a/bib/internal/distrodef/distrodef.go b/bib/internal/distrodef/distrodef.go index c121d2910..ce5b11948 100644 --- a/bib/internal/distrodef/distrodef.go +++ b/bib/internal/distrodef/distrodef.go @@ -6,8 +6,8 @@ import ( "path/filepath" "strings" + "go.yaml.in/yaml/v3" "golang.org/x/exp/maps" - "gopkg.in/yaml.v3" "github.com/hashicorp/go-version" ) diff --git a/bib/internal/imagetypes/imagetypes.go b/bib/internal/imagetypes/imagetypes.go index 8e788ae6b..b401d910f 100644 --- a/bib/internal/imagetypes/imagetypes.go +++ b/bib/internal/imagetypes/imagetypes.go @@ -10,17 +10,25 @@ import ( type imageType struct { Export string ISO bool + Legacy bool } var supportedImageTypes = map[string]imageType{ - "ami": imageType{Export: "image"}, - "qcow2": imageType{Export: "qcow2"}, - "raw": imageType{Export: "image"}, - "vmdk": imageType{Export: "vmdk"}, - "vhd": imageType{Export: "vpc"}, - "gce": imageType{Export: "gce"}, - "anaconda-iso": imageType{Export: "bootiso", ISO: true}, - "iso": imageType{Export: "bootiso", ISO: true}, + // XXX: ideally we would look how to consolidate all + // knownledge about disk based image types into the images + // library + "ami": imageType{Export: "image"}, + "qcow2": imageType{Export: "qcow2"}, + "raw": imageType{Export: "image"}, + "vmdk": imageType{Export: "vmdk"}, + "vhd": imageType{Export: "vpc"}, + "gce": imageType{Export: "gce"}, + "ova": imageType{Export: "archive"}, + "bootc-installer": imageType{Export: "bootiso", ISO: true}, + "pxe-tar-xz": imageType{Export: "bootc-pxe-tree"}, + // the iso image types are RPM based and legacy/deprecated + "anaconda-iso": imageType{Export: "bootiso", ISO: true, Legacy: true}, + "iso": imageType{Export: "bootiso", ISO: true, Legacy: true}, } // Available() returns a comma-separated list of supported image types @@ -86,3 +94,12 @@ func (it ImageTypes) BuildsISO() bool { // XXX: this assumes a valid ImagTypes object return supportedImageTypes[it[0]].ISO } + +func (it ImageTypes) Legacy() bool { + for _, name := range it { + if supportedImageTypes[name].Legacy { + return true + } + } + return false +} diff --git a/bib/internal/imagetypes/imagetypes_test.go b/bib/internal/imagetypes/imagetypes_test.go index fe36ea0f6..c0fedd79c 100644 --- a/bib/internal/imagetypes/imagetypes_test.go +++ b/bib/internal/imagetypes/imagetypes_test.go @@ -53,25 +53,34 @@ func TestImageTypes(t *testing.T) { expectedExports: []string{"bootiso"}, expectISO: true, }, + "bootc-pxe-tree": { + imageTypes: []string{"pxe-tar-xz"}, + expectedExports: []string{"bootc-pxe-tree"}, + expectISO: false, + }, "bad-mix": { imageTypes: []string{"vmdk", "anaconda-iso"}, expectedErr: errors.New("cannot mix ISO/disk images in request [vmdk anaconda-iso]"), }, - "bad-mix-part-2": { + "bad-mix-2": { + imageTypes: []string{"vmdk", "bootc-installer"}, + expectedErr: errors.New("cannot mix ISO/disk images in request [vmdk bootc-installer]"), + }, + "bad-mix-3": { imageTypes: []string{"ami", "iso"}, expectedErr: errors.New("cannot mix ISO/disk images in request [ami iso]"), }, "bad-image-type": { imageTypes: []string{"bad"}, - expectedErr: errors.New(`unsupported image type "bad", valid types are ami, anaconda-iso, gce, iso, qcow2, raw, vhd, vmdk`), + expectedErr: errors.New(`unsupported image type "bad", valid types are ami, anaconda-iso, bootc-installer, gce, iso, ova, pxe-tar-xz, qcow2, raw, vhd, vmdk`), }, "bad-in-good": { imageTypes: []string{"ami", "raw", "vmdk", "qcow2", "something-else-what-is-this"}, - expectedErr: errors.New(`unsupported image type "something-else-what-is-this", valid types are ami, anaconda-iso, gce, iso, qcow2, raw, vhd, vmdk`), + expectedErr: errors.New(`unsupported image type "something-else-what-is-this", valid types are ami, anaconda-iso, bootc-installer, gce, iso, ova, pxe-tar-xz, qcow2, raw, vhd, vmdk`), }, "all-bad": { imageTypes: []string{"bad1", "bad2", "bad3", "bad4", "bad5", "bad42"}, - expectedErr: errors.New(`unsupported image type "bad1", valid types are ami, anaconda-iso, gce, iso, qcow2, raw, vhd, vmdk`), + expectedErr: errors.New(`unsupported image type "bad1", valid types are ami, anaconda-iso, bootc-installer, gce, iso, ova, pxe-tar-xz, qcow2, raw, vhd, vmdk`), }, } diff --git a/bib/internal/podmanutil/podmanutils.go b/bib/internal/podmanutil/podmanutils.go deleted file mode 100644 index ac6c48524..000000000 --- a/bib/internal/podmanutil/podmanutils.go +++ /dev/null @@ -1,38 +0,0 @@ -package podmanutil - -import ( - "bufio" - "bytes" - "errors" - "fmt" - "io/fs" - "os" -) - -// envPath is written by podman -const envPath = "/run/.containerenv" - -// rootlessKey is set when we are rootless -const rootlessKey = "rootless=1" - -// IsRootless detects if we are running rootless in podman; -// other situations (e.g. docker) will successfuly return false. -func IsRootless() (bool, error) { - buf, err := os.ReadFile(envPath) - if err != nil { - if errors.Is(err, fs.ErrNotExist) { - return false, nil - } - return false, err - } - scanner := bufio.NewScanner(bytes.NewReader(buf)) - for scanner.Scan() { - if scanner.Text() == rootlessKey { - return true, nil - } - } - if err := scanner.Err(); err != nil { - return false, fmt.Errorf("parsing %s: %w", envPath, err) - } - return false, nil -} diff --git a/bib/internal/setup/export_test.go b/bib/internal/setup/export_test.go deleted file mode 100644 index 2e5088961..000000000 --- a/bib/internal/setup/export_test.go +++ /dev/null @@ -1,3 +0,0 @@ -package setup - -var ValidateCanRunTargetArch = validateCanRunTargetArch diff --git a/bib/internal/setup/setup.go b/bib/internal/setup/setup.go deleted file mode 100644 index 27009a57d..000000000 --- a/bib/internal/setup/setup.go +++ /dev/null @@ -1,167 +0,0 @@ -package setup - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - "runtime" - "strings" - - "golang.org/x/sys/unix" - - "github.com/sirupsen/logrus" - - "github.com/osbuild/bootc-image-builder/bib/internal/podmanutil" - "github.com/osbuild/bootc-image-builder/bib/internal/util" -) - -// EnsureEnvironment mutates external filesystem state as necessary -// to run in a container environment. This function is idempotent. -func EnsureEnvironment(storePath string) error { - osbuildPath := "/usr/bin/osbuild" - if util.IsMountpoint(osbuildPath) { - return nil - } - - // Forcibly label the store to ensure we're not grabbing container labels - rootType := "system_u:object_r:root_t:s0" - // This papers over the lack of ensuring correct labels for the /ostree root - // in the existing pipeline - if err := util.RunCmdSync("chcon", rootType, storePath); err != nil { - return err - } - - // A hardcoded security label from Fedora derivatives for osbuild - // TODO: Avoid hardcoding this by using either host policy lookup - // Or eventually depend on privileged containers just having this capability. - // - // We need this in order to get `install_t` that has `CAP_MAC_ADMIN` for creating SELinux - // labels unknown to the host. - // - // Note that the transition to `install_t` must happen at this point. Osbuild stages run in `bwrap` that creates - // a nosuid, no_new_privs environment. In such an environment, we cannot transition from `unconfined_t` to `install_t`, - // because we would get more privileges. - installType := "system_u:object_r:install_exec_t:s0" - // Where we dump temporary files; this must be an overlayfs as we cannot - // write security contexts on overlayfs. - runTmp := "/run/osbuild/" - - if err := os.MkdirAll(runTmp, 0o755); err != nil { - return err - } - if !util.IsMountpoint(runTmp) { - if err := util.RunCmdSync("mount", "-t", "tmpfs", "tmpfs", runTmp); err != nil { - return err - } - } - destPath := filepath.Join(runTmp, "osbuild") - if err := util.RunCmdSync("cp", "-p", "/usr/bin/osbuild", destPath); err != nil { - return err - } - if err := util.RunCmdSync("chcon", installType, destPath); err != nil { - return err - } - - // Ensure we have devfs inside the container to get dynamic loop - // loop devices inside the container. - if err := util.RunCmdSync("mount", "-t", "devtmpfs", "devtmpfs", "/dev"); err != nil { - return err - } - - // Create a bind mount into our target location; we can't copy it because - // again we have to perserve the SELinux label. - if err := util.RunCmdSync("mount", "--bind", destPath, osbuildPath); err != nil { - return err - } - // NOTE: Don't add new code here, do it before the bind mount which acts as the final success indicator - - return nil -} - -// Validate checks that the environment is supported (e.g. caller set up the -// container correctly) -func Validate(targetArch string) error { - isRootless, err := podmanutil.IsRootless() - if err != nil { - return fmt.Errorf("checking rootless: %w", err) - } - if isRootless { - return fmt.Errorf("this command must be run in rootful (not rootless) podman") - } - - // Having /sys be writable is an easy to check proxy for privileges; more effective - // is really looking for CAP_SYS_ADMIN, but that involves more Go libraries. - var stvfsbuf unix.Statfs_t - if err := unix.Statfs("/sys", &stvfsbuf); err != nil { - return fmt.Errorf("failed to stat /sys: %w", err) - } - if (stvfsbuf.Flags & unix.ST_RDONLY) > 0 { - return fmt.Errorf("this command requires a privileged container") - } - - // Try to run the cross arch binary - if err := validateCanRunTargetArch(targetArch); err != nil { - return fmt.Errorf("cannot run binary in target arch: %w", err) - } - - return nil -} - -// ValidateHasContainerStorageMounted checks that the hostcontainer storage -// is mounted inside the container -func ValidateHasContainerStorageMounted() error { - // Just look for the overlay backend, which we expect by default. - // In theory, one could be using a different backend, but we don't - // really need to worry about this right now. If it turns out - // we do need to care, then we can probably handle this by - // just trying to query the image. - overlayPath := "/var/lib/containers/storage/overlay" - if _, err := os.Stat(overlayPath); err != nil { - if os.IsNotExist(err) { - return fmt.Errorf("cannot find %q (missing -v /var/lib/containers/storage:/var/lib/containers/storage mount?)", overlayPath) - } - return fmt.Errorf("failed to stat %q: %w", overlayPath, err) - } - return nil -} - -func validateCanRunTargetArch(targetArch string) error { - if targetArch == runtime.GOARCH || targetArch == "" { - return nil - } - - canaryCmd := fmt.Sprintf("bib-canary-%s", targetArch) - if _, err := exec.LookPath(canaryCmd); err != nil { - // we could error here but in principle with a working qemu-user - // any arch should work so let's just warn. the common case - // (arm64/amd64) is covered properly - logrus.Warningf("cannot check architecture support for %v: no canary binary found", targetArch) - return nil - } - output, err := exec.Command(canaryCmd).CombinedOutput() - if err != nil { - return fmt.Errorf("cannot run canary binary for %q, do you have 'qemu-user-static' installed?\n%s", targetArch, err) - } - if string(output) != "ok\n" { - return fmt.Errorf("internal error: unexpected output from cross-architecture canary: %q", string(output)) - } - - return nil -} - -func ValidateHasContainerTags(imgref string) error { - output, err := exec.Command("podman", "image", "inspect", imgref, "--format", "{{.Labels}}").Output() - if err != nil { - return fmt.Errorf(`failed to inspect the image: %w -bootc-image-builder no longer pulls images, make sure to pull it before running bootc-image-builder: - sudo podman pull %s`, util.OutputErr(err), imgref) - } - - tags := string(output) - if !strings.Contains(tags, "containers.bootc:1") { - return fmt.Errorf("image %s is not a bootc image", imgref) - } - - return nil -} diff --git a/bib/internal/setup/setup_test.go b/bib/internal/setup/setup_test.go deleted file mode 100644 index d8b36951d..000000000 --- a/bib/internal/setup/setup_test.go +++ /dev/null @@ -1,113 +0,0 @@ -package setup_test - -import ( - "bytes" - "fmt" - "os" - "path/filepath" - "runtime" - "testing" - - "github.com/sirupsen/logrus" - "github.com/stretchr/testify/assert" - - "github.com/osbuild/bootc-image-builder/bib/internal/setup" -) - -func TestValidateCanRunTargetArchTrivial(t *testing.T) { - for _, arch := range []string{runtime.GOARCH, ""} { - err := setup.ValidateCanRunTargetArch(arch) - assert.NoError(t, err) - } -} - -func TestValidateCanRunTargetArchUnsupportedCanary(t *testing.T) { - var logbuf bytes.Buffer - logrus.SetOutput(&logbuf) - - err := setup.ValidateCanRunTargetArch("unsupported-arch") - assert.NoError(t, err) - assert.Contains(t, logbuf.String(), `level=warning msg="cannot check architecture support for unsupported-arch: no canary binary found"`) -} - -func makeFakeBinary(t *testing.T, binary, content string) { - tmpdir := t.TempDir() - t.Setenv("PATH", tmpdir+":"+os.Getenv("PATH")) - err := os.WriteFile(filepath.Join(tmpdir, binary), []byte(content), 0o755) - assert.NoError(t, err) -} - -func makeFakeCanary(t *testing.T, content string) { - makeFakeBinary(t, "bib-canary-fakearch", content) -} - -func TestValidateCanRunTargetArchHappy(t *testing.T) { - var logbuf bytes.Buffer - logrus.SetOutput(&logbuf) - - makeFakeCanary(t, "#!/bin/sh\necho ok") - - err := setup.ValidateCanRunTargetArch("fakearch") - assert.NoError(t, err) - assert.Equal(t, "", logbuf.String()) -} - -func TestValidateCanRunTargetArchExecFormatError(t *testing.T) { - makeFakeCanary(t, "") - - err := setup.ValidateCanRunTargetArch("fakearch") - assert.ErrorContains(t, err, `cannot run canary binary for "fakearch", do you have 'qemu-user-static' installed?`) - assert.ErrorContains(t, err, `: exec format error`) -} - -func TestValidateCanRunTargetArchUnexpectedOutput(t *testing.T) { - makeFakeCanary(t, "#!/bin/sh\necho xxx") - - err := setup.ValidateCanRunTargetArch("fakearch") - assert.ErrorContains(t, err, `internal error: unexpected output`) -} - -var ( - fakePodmanOutputCentosBootc = `map[containers.bootc:1 io.buildah.version:1.29.1 org.opencontainers.image.version:stream9.20240319.0 ostree.bootable:true ostree.commit:97d619eae2a5474a9c363c78e3ad6caec14acba54a0b077c7cb69d00a4f800a5 ostree.final-diffid:sha256:12787d84fa137cd5649a9005efe98ec9d05ea46245fdc50aecb7dd007f2035b1 ostree.linux:5.14.0-430.el9.x86_64 redhat.compose-id:CentOS-Stream-9-20240304.d.0 redhat.id:centos redhat.version-id:9 rpmostree.inputhash:a5c67fd4e9465e47e01922171c6ab8edf261d2d381e590b5cd7fed81ea8d4dbe]` - - fakePodmanOutputCentos = `map[io.buildah.version:1.33.7 org.label-schema.build-date:20240618 org.label-schema.license:GPLv2 org.label-schema.name:CentOS Stream 9 Base Image org.label-schema.schema-version:1.0 org.label-schema.vendor:CentOS]` - - emptyPodmanOutput = `map[]` -) - -func TestValidateTags(t *testing.T) { - for _, tc := range []struct { - imageref string - fakeOutput string - expectedErr string - }{ - { - "quay.io/centos-bootc/centos-bootc:stream9", - fakePodmanOutputCentosBootc, - "", - }, - { - "quay.io/centos/centos:stream9", - fakePodmanOutputCentos, - "image quay.io/centos/centos:stream9 is not a bootc image", - }, - { - "fake/image", - emptyPodmanOutput, - "image fake/image is not a bootc image", - }, - } { - podmanArgsFile := filepath.Join(t.TempDir(), "args.txt") - fakePodman := fmt.Sprintf(`#!/bin/sh -e -echo "$@" > '%s' -echo '%s' -`, podmanArgsFile, tc.fakeOutput) - makeFakeBinary(t, "podman", fakePodman) - err := setup.ValidateHasContainerTags(tc.imageref) - if tc.expectedErr == "" { - assert.NoError(t, err) - } else { - assert.EqualError(t, err, tc.expectedErr) - } - } -} diff --git a/bib/internal/source/source.go b/bib/internal/source/source.go deleted file mode 100644 index f2508e64a..000000000 --- a/bib/internal/source/source.go +++ /dev/null @@ -1,84 +0,0 @@ -package source - -import ( - "fmt" - "os" - "path" - - "github.com/sirupsen/logrus" - - "github.com/osbuild/images/pkg/distro" -) - -type OSRelease struct { - PlatformID string - ID string - VersionID string - Name string - VariantID string -} - -type Info struct { - OSRelease OSRelease - UEFIVendor string -} - -func validateOSRelease(osrelease map[string]string) error { - // VARIANT_ID is optional - for _, key := range []string{"ID", "VERSION_ID", "NAME", "PLATFORM_ID"} { - if _, ok := osrelease[key]; !ok { - return fmt.Errorf("missing %s in os-release", key) - } - } - return nil -} - -func uefiVendor(root string) (string, error) { - bootupdEfiDir := path.Join(root, "usr/lib/bootupd/updates/EFI") - l, err := os.ReadDir(bootupdEfiDir) - if err != nil { - return "", fmt.Errorf("cannot read bootupd EFI directory %s: %w", bootupdEfiDir, err) - } - - // best-effort search: return the first directory that's not "BOOT" - for _, entry := range l { - if !entry.IsDir() { - continue - } - - if entry.Name() == "BOOT" { - continue - } - - return entry.Name(), nil - } - - return "", fmt.Errorf("cannot find UEFI vendor in %s", bootupdEfiDir) -} - -func LoadInfo(root string) (*Info, error) { - osrelease, err := distro.ReadOSReleaseFromTree(root) - if err != nil { - return nil, err - } - if err := validateOSRelease(osrelease); err != nil { - return nil, err - } - - vendor, err := uefiVendor(root) - if err != nil { - logrus.Debugf("cannot read UEFI vendor: %v, setting it to none", err) - } - - return &Info{ - OSRelease: OSRelease{ - ID: osrelease["ID"], - VersionID: osrelease["VERSION_ID"], - Name: osrelease["NAME"], - PlatformID: osrelease["PLATFORM_ID"], - VariantID: osrelease["VARIANT_ID"], - }, - - UEFIVendor: vendor, - }, nil -} diff --git a/bib/internal/source/source_test.go b/bib/internal/source/source_test.go deleted file mode 100644 index 8f7c7c28a..000000000 --- a/bib/internal/source/source_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package source - -import ( - "os" - "path" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func writeOSRelease(root, id, versionID, name, platformID, variantID string) error { - err := os.MkdirAll(path.Join(root, "etc"), 0755) - if err != nil { - return err - } - - var buf string - if id != "" { - buf += "ID=" + id + "\n" - } - if versionID != "" { - buf += "VERSION_ID=" + versionID + "\n" - } - if name != "" { - buf += "NAME=" + name + "\n" - } - if platformID != "" { - buf += "PLATFORM_ID=" + platformID + "\n" - } - if variantID != "" { - buf += "VARIANT_ID=" + variantID + "\n" - } - - return os.WriteFile(path.Join(root, "etc/os-release"), []byte(buf), 0644) -} - -func createBootupdEFI(root, uefiVendor string) error { - err := os.MkdirAll(path.Join(root, "usr/lib/bootupd/updates/EFI/BOOT"), 0755) - if err != nil { - return err - } - return os.Mkdir(path.Join(root, "usr/lib/bootupd/updates/EFI", uefiVendor), 0755) -} - -func TestLoadInfo(t *testing.T) { - cases := []struct { - desc string - id string - versionID string - name string - uefiVendor string - platformID string - variantID string - errorStr string - }{ - {"happy", "fedora", "40", "Fedora Linux", "fedora", "platform:f40", "coreos", ""}, - {"happy-no-uefi", "fedora", "40", "Fedora Linux", "", "platform:f40", "coreos", ""}, - {"happy-no-variant_id", "fedora", "40", "Fedora Linux", "", "platform:f40", "", ""}, - {"sad-no-id", "", "40", "Fedora Linux", "fedora", "platform:f40", "", "missing ID in os-release"}, - {"sad-no-id", "fedora", "", "Fedora Linux", "fedora", "platform:f40", "", "missing VERSION_ID in os-release"}, - {"sad-no-id", "fedora", "40", "", "fedora", "platform:f40", "", "missing NAME in os-release"}, - {"sad-no-id", "fedora", "40", "Fedora Linux", "fedora", "", "", "missing PLATFORM_ID in os-release"}, - } - - for _, c := range cases { - t.Run(c.desc, func(t *testing.T) { - root := t.TempDir() - require.NoError(t, writeOSRelease(root, c.id, c.versionID, c.name, c.platformID, c.variantID)) - if c.uefiVendor != "" { - require.NoError(t, createBootupdEFI(root, c.uefiVendor)) - - } - - info, err := LoadInfo(root) - - if c.errorStr != "" { - require.Equal(t, c.errorStr, err.Error()) - return - } - require.NoError(t, err) - assert.Equal(t, c.id, info.OSRelease.ID) - assert.Equal(t, c.versionID, info.OSRelease.VersionID) - assert.Equal(t, c.name, info.OSRelease.Name) - assert.Equal(t, c.uefiVendor, info.UEFIVendor) - assert.Equal(t, c.platformID, info.OSRelease.PlatformID) - assert.Equal(t, c.variantID, info.OSRelease.VariantID) - - }) - } -} diff --git a/bib/internal/util/util.go b/bib/internal/util/util.go deleted file mode 100644 index 04e1c60d4..000000000 --- a/bib/internal/util/util.go +++ /dev/null @@ -1,37 +0,0 @@ -package util - -import ( - "fmt" - "os" - "os/exec" - "strings" - - "github.com/sirupsen/logrus" -) - -// IsMountpoint checks if the target path is a mount point -func IsMountpoint(path string) bool { - return exec.Command("mountpoint", path).Run() == nil -} - -// Synchronously invoke a command, propagating stdout and stderr -// to the current process's stdout and stderr -func RunCmdSync(cmdName string, args ...string) error { - logrus.Debugf("Running: %s %s", cmdName, strings.Join(args, " ")) - cmd := exec.Command(cmdName, args...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - return fmt.Errorf("error running %s %s: %w", cmdName, strings.Join(args, " "), err) - } - return nil -} - -// OutputErr takes an error from exec.Command().Output() and tries -// generate an error with stderr details -func OutputErr(err error) error { - if err, ok := err.(*exec.ExitError); ok { - return fmt.Errorf("%w, stderr:\n%s", err, err.Stderr) - } - return err -} diff --git a/bib/internal/util/util_test.go b/bib/internal/util/util_test.go deleted file mode 100644 index f72c2a718..000000000 --- a/bib/internal/util/util_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package util_test - -import ( - "fmt" - "os/exec" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/osbuild/bootc-image-builder/bib/internal/util" -) - -func TestOutputErrPassthrough(t *testing.T) { - err := fmt.Errorf("boom") - assert.Equal(t, util.OutputErr(err), err) -} - -func TestOutputErrExecError(t *testing.T) { - _, err := exec.Command("bash", "-c", ">&2 echo some-stderr; exit 1").Output() - assert.Equal(t, "exit status 1, stderr:\nsome-stderr\n", util.OutputErr(err).Error()) -} diff --git a/bib/pkg/progress/export_test.go b/bib/pkg/progress/export_test.go deleted file mode 100644 index 26d0c57c2..000000000 --- a/bib/pkg/progress/export_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package progress - -import ( - "io" -) - -type ( - TerminalProgressBar = terminalProgressBar - DebugProgressBar = debugProgressBar - VerboseProgressBar = verboseProgressBar -) - -var ( - NewSyncedWriter = newSyncedWriter -) - -func MockOsStdout(w io.Writer) (restore func()) { - saved := osStdout - osStdout = func() io.Writer { return w } - return func() { - osStdout = saved - } -} - -func MockOsStderr(w io.Writer) (restore func()) { - saved := osStderr - osStderr = func() io.Writer { return w } - return func() { - osStderr = saved - } -} - -func MockIsattyIsTerminal(fn func(uintptr) bool) (restore func()) { - saved := isattyIsTerminal - isattyIsTerminal = fn - return func() { - isattyIsTerminal = saved - } -} - -func MockOsbuildCmd(s string) (restore func()) { - saved := osbuildCmd - osbuildCmd = s - return func() { - osbuildCmd = saved - } -} diff --git a/bib/pkg/progress/progress.go b/bib/pkg/progress/progress.go deleted file mode 100644 index 371ba3627..000000000 --- a/bib/pkg/progress/progress.go +++ /dev/null @@ -1,506 +0,0 @@ -package progress - -import ( - "bytes" - "errors" - "fmt" - "io" - "os" - "os/exec" - "strings" - "sync" - "syscall" - "time" - - "github.com/cheggaaa/pb/v3" - "github.com/mattn/go-isatty" - "github.com/sirupsen/logrus" - - "github.com/osbuild/images/pkg/osbuild" -) - -var ( - // This is only needed because pb.Pool require a real terminal. - // It sets it into "raw-mode" but there is really no need for - // this (see "func render()" below) so once this is fixed - // upstream we should remove this. - ESC = "\x1b" - ERASE_LINE = ESC + "[2K" - CURSOR_HIDE = ESC + "[?25l" - CURSOR_SHOW = ESC + "[?25h" -) - -// Used for testing, this must be a function (instead of the usual -// "var osStderr = os.Stderr" so that higher level libraries can test -// this code by replacing "os.Stderr", e.g. testutil.CaptureStdio() -var osStdout = func() io.Writer { - return os.Stdout -} -var osStderr = func() io.Writer { - return os.Stderr -} - -func cursorUp(i int) string { - return fmt.Sprintf("%s[%dA", ESC, i) -} - -// ProgressBar is an interface for progress reporting when there is -// an arbitrary amount of sub-progress information (like osbuild) -type ProgressBar interface { - // SetProgress sets the progress details at the given "level". - // Levels should start with "0" and increase as the nesting - // gets deeper. - // - // Note that reducing depth is currently not supported, once - // a sub-progress is added it cannot be removed/hidden - // (but if required it can be added, its a SMOP) - SetProgress(level int, msg string, done int, total int) error - - // The high-level message that is displayed in a spinner - // that contains the current top level step, for bib this - // is really just "Manifest generation step" and - // "Image generation step". We could map this to a three-level - // progress as well but we spend 90% of the time in the - // "Image generation step" so the UI looks a bit odd. - SetPulseMsgf(fmt string, args ...interface{}) - - // A high level message with the last operation status. - // For us this usually comes from the stages and has information - // like "Starting module org.osbuild.selinux" - SetMessagef(fmt string, args ...interface{}) - - // Start will start rendering the progress information - Start() - - // Stop will stop rendering the progress information, the - // screen is not cleared, the last few lines will be visible - Stop() -} - -var isattyIsTerminal = isatty.IsTerminal - -// New creates a new progressbar based on the requested type -func New(typ string) (ProgressBar, error) { - switch typ { - case "", "auto": - // autoselect based on if we are on an interactive - // terminal, use verbose progress for scripts - if isattyIsTerminal(os.Stdin.Fd()) { - return NewTerminalProgressBar() - } - return NewVerboseProgressBar() - case "verbose": - return NewVerboseProgressBar() - case "term": - return NewTerminalProgressBar() - case "debug": - return NewDebugProgressBar() - default: - return nil, fmt.Errorf("unknown progress type: %q", typ) - } -} - -type terminalProgressBar struct { - spinnerPb *pb.ProgressBar - msgPb *pb.ProgressBar - subLevelPbs []*pb.ProgressBar - - shutdownCh chan bool - - out io.Writer -} - -// NewTerminalProgressBar creates a new default pb3 based progressbar suitable for -// most terminals. -func NewTerminalProgressBar() (ProgressBar, error) { - b := &terminalProgressBar{ - out: osStderr(), - } - b.spinnerPb = pb.New(0) - b.spinnerPb.SetTemplate(`[{{ (cycle . "|" "/" "-" "\\") }}] {{ string . "spinnerMsg" }}`) - b.msgPb = pb.New(0) - b.msgPb.SetTemplate(`Message: {{ string . "msg" }}`) - return b, nil -} - -func (b *terminalProgressBar) SetProgress(subLevel int, msg string, done int, total int) error { - // auto-add as needed, requires sublevels to get added in order - // i.e. adding 0 and then 2 will fail - switch { - case subLevel == len(b.subLevelPbs): - apb := pb.New(0) - progressBarTmpl := `[{{ counters . }}] {{ string . "prefix" }} {{ bar .}} {{ percent . }}` - apb.SetTemplateString(progressBarTmpl) - if err := apb.Err(); err != nil { - return fmt.Errorf("error setting the progressbarTemplat: %w", err) - } - // workaround bug when running tests in tmt - if apb.Width() == 0 { - // this is pb.defaultBarWidth - apb.SetWidth(100) - } - b.subLevelPbs = append(b.subLevelPbs, apb) - case subLevel > len(b.subLevelPbs): - return fmt.Errorf("sublevel added out of order, have %v sublevels but want level %v", len(b.subLevelPbs), subLevel) - } - apb := b.subLevelPbs[subLevel] - apb.SetTotal(int64(total) + 1) - apb.SetCurrent(int64(done) + 1) - apb.Set("prefix", msg) - return nil -} - -func shorten(msg string) string { - msg = strings.Replace(msg, "\n", " ", -1) - // XXX: make this smarter - if len(msg) > 60 { - return msg[:60] + "..." - } - return msg -} - -func (b *terminalProgressBar) SetPulseMsgf(msg string, args ...interface{}) { - b.spinnerPb.Set("spinnerMsg", shorten(fmt.Sprintf(msg, args...))) -} - -func (b *terminalProgressBar) SetMessagef(msg string, args ...interface{}) { - b.msgPb.Set("msg", shorten(fmt.Sprintf(msg, args...))) -} - -func (b *terminalProgressBar) render() { - var renderedLines int - fmt.Fprintf(b.out, "%s%s\n", ERASE_LINE, b.spinnerPb.String()) - renderedLines++ - for _, prog := range b.subLevelPbs { - fmt.Fprintf(b.out, "%s%s\n", ERASE_LINE, prog.String()) - renderedLines++ - } - fmt.Fprintf(b.out, "%s%s\n", ERASE_LINE, b.msgPb.String()) - renderedLines++ - fmt.Fprint(b.out, cursorUp(renderedLines)) -} - -// Workaround for the pb.Pool requiring "raw-mode" - see here how to avoid -// it. Once fixes upstream we should remove this. -func (b *terminalProgressBar) renderLoop() { - for { - select { - case <-b.shutdownCh: - b.render() - // finally move cursor down again - fmt.Fprint(b.out, CURSOR_SHOW) - fmt.Fprint(b.out, strings.Repeat("\n", 2+len(b.subLevelPbs))) - // close last to avoid race with b.out - close(b.shutdownCh) - return - case <-time.After(200 * time.Millisecond): - // break to redraw the screen - } - b.render() - } -} - -func (b *terminalProgressBar) Start() { - // render() already running - if b.shutdownCh != nil { - return - } - fmt.Fprintf(b.out, "%s", CURSOR_HIDE) - b.shutdownCh = make(chan bool) - go b.renderLoop() -} - -func (b *terminalProgressBar) Err() error { - var errs []error - if err := b.spinnerPb.Err(); err != nil { - errs = append(errs, fmt.Errorf("error on spinner progressbar: %w", err)) - } - if err := b.msgPb.Err(); err != nil { - errs = append(errs, fmt.Errorf("error on spinner progressbar: %w", err)) - } - for _, pb := range b.subLevelPbs { - if err := pb.Err(); err != nil { - errs = append(errs, fmt.Errorf("error on spinner progressbar: %w", err)) - } - } - return errors.Join(errs...) -} - -func (b *terminalProgressBar) Stop() { - if b.shutdownCh == nil { - return - } - // request shutdown - b.shutdownCh <- true - // wait for ack - select { - case <-b.shutdownCh: - // shudown complete - case <-time.After(1 * time.Second): - // I cannot think of how this could happen, i.e. why - // closing would not work but lets be conservative - - // without a timeout we hang here forever - logrus.Warnf("no progress channel shutdown after 1sec") - } - b.shutdownCh = nil - // This should never happen but be paranoid, this should - // never happen but ensure we did not accumulate error while - // running - if err := b.Err(); err != nil { - fmt.Fprintf(b.out, "error from pb.ProgressBar: %v", err) - } -} - -type verboseProgressBar struct { - w io.Writer -} - -// NewVerboseProgressBar starts a new "verbose" progressbar that will just -// prints message but does not show any progress. -func NewVerboseProgressBar() (ProgressBar, error) { - b := &verboseProgressBar{w: osStderr()} - return b, nil -} - -func (b *verboseProgressBar) SetPulseMsgf(msg string, args ...interface{}) { - fmt.Fprintf(b.w, msg, args...) - fmt.Fprintf(b.w, "\n") -} - -func (b *verboseProgressBar) SetMessagef(msg string, args ...interface{}) { - fmt.Fprintf(b.w, msg, args...) - fmt.Fprintf(b.w, "\n") -} - -func (b *verboseProgressBar) Start() { -} - -func (b *verboseProgressBar) Stop() { -} - -func (b *verboseProgressBar) SetProgress(subLevel int, msg string, done int, total int) error { - return nil -} - -type debugProgressBar struct { - w io.Writer -} - -// NewDebugProgressBar will create a progressbar aimed to debug the -// lower level osbuild/images message. It will never clear the screen -// so "glitches/weird" messages from the lower-layers can be inspected -// easier. -func NewDebugProgressBar() (ProgressBar, error) { - b := &debugProgressBar{w: osStderr()} - return b, nil -} - -func (b *debugProgressBar) SetPulseMsgf(msg string, args ...interface{}) { - fmt.Fprintf(b.w, "pulse: ") - fmt.Fprintf(b.w, msg, args...) - fmt.Fprintf(b.w, "\n") -} - -func (b *debugProgressBar) SetMessagef(msg string, args ...interface{}) { - fmt.Fprintf(b.w, "msg: ") - fmt.Fprintf(b.w, msg, args...) - fmt.Fprintf(b.w, "\n") -} - -func (b *debugProgressBar) Start() { - fmt.Fprintf(b.w, "Start progressbar\n") -} - -func (b *debugProgressBar) Stop() { - fmt.Fprintf(b.w, "Stop progressbar\n") -} - -func (b *debugProgressBar) SetProgress(subLevel int, msg string, done int, total int) error { - fmt.Fprintf(b.w, "%s[%v / %v] %s", strings.Repeat(" ", subLevel), done, total, msg) - fmt.Fprintf(b.w, "\n") - return nil -} - -type OSBuildOptions struct { - StoreDir string - OutputDir string - ExtraEnv []string - - // BuildLog writes the osbuild output to the given writer - BuildLog io.Writer -} - -// XXX: merge variant back into images/pkg/osbuild/osbuild-exec.go -func RunOSBuild(pb ProgressBar, manifest []byte, exports []string, opts *OSBuildOptions) error { - if opts == nil { - opts = &OSBuildOptions{} - } - - // To keep maximum compatibility keep the old behavior to run osbuild - // directly and show all messages unless we have a "real" progress bar. - // - // This should ensure that e.g. "podman bootc" keeps working as it - // is currently expecting the raw osbuild output. Once we double - // checked with them we can remove the runOSBuildNoProgress() and - // just run with the new runOSBuildWithProgress() helper. - switch pb.(type) { - case *terminalProgressBar, *debugProgressBar: - return runOSBuildWithProgress(pb, manifest, exports, opts) - default: - return runOSBuildNoProgress(pb, manifest, exports, opts) - } -} - -func runOSBuildNoProgress(pb ProgressBar, manifest []byte, exports []string, opts *OSBuildOptions) error { - var stdout, stderr io.Writer - - var writeMu sync.Mutex - if opts.BuildLog == nil { - // No external build log requested and we won't need an - // internal one because all output goes directly to - // stdout/stderr. This is for maximum compatibility with - // the existing bootc-image-builder in "verbose" mode - // where stdout, stderr come directly from osbuild. - stdout = osStdout() - stderr = osStderr() - } else { - // There is a slight wrinkle here: when requesting a - // buildlog we can no longer write to separate - // stdout/stderr streams without being racy and give - // potential out-of-order output (which is very bad - // and confusing in a log). The reason is that if - // cmd.Std{out,err} are different "go" will start two - // go-routine to monitor/copy those are racy when both - // stdout,stderr output happens close together - // (TestRunOSBuildWithBuildlog demos that). We cannot - // have our cake and eat it so here we need to combine - // osbuilds stderr into our stdout. - mw := newSyncedWriter(&writeMu, io.MultiWriter(osStdout(), opts.BuildLog)) - stdout = mw - stderr = mw - } - - cmd := exec.Command( - osbuildCmd, - "--store", opts.StoreDir, - "--output-directory", opts.OutputDir, - "-", - ) - for _, export := range exports { - cmd.Args = append(cmd.Args, "--export", export) - } - - cmd.Env = append(os.Environ(), opts.ExtraEnv...) - cmd.Stdin = bytes.NewBuffer(manifest) - cmd.Stdout = stdout - cmd.Stderr = stderr - if err := cmd.Run(); err != nil { - return fmt.Errorf("error running osbuild: %w", err) - } - return nil -} - -var osbuildCmd = "osbuild" - -func runOSBuildWithProgress(pb ProgressBar, manifest []byte, exports []string, opts *OSBuildOptions) (err error) { - rp, wp, err := os.Pipe() - if err != nil { - return fmt.Errorf("cannot create pipe for osbuild: %w", err) - } - defer rp.Close() - defer wp.Close() - - cmd := exec.Command( - osbuildCmd, - "--store", opts.StoreDir, - "--output-directory", opts.OutputDir, - "--monitor=JSONSeqMonitor", - "--monitor-fd=3", - "-", - ) - for _, export := range exports { - cmd.Args = append(cmd.Args, "--export", export) - } - - var stdio bytes.Buffer - var mw, buildLog io.Writer - var writeMu sync.Mutex - if opts.BuildLog != nil { - mw = newSyncedWriter(&writeMu, io.MultiWriter(&stdio, opts.BuildLog)) - buildLog = newSyncedWriter(&writeMu, opts.BuildLog) - } else { - mw = &stdio - buildLog = io.Discard - } - - cmd.Env = append(os.Environ(), opts.ExtraEnv...) - cmd.Stdin = bytes.NewBuffer(manifest) - cmd.Stdout = mw - cmd.Stderr = mw - cmd.ExtraFiles = []*os.File{wp} - - osbuildStatus := osbuild.NewStatusScanner(rp) - if err := cmd.Start(); err != nil { - return fmt.Errorf("error starting osbuild: %v", err) - } - wp.Close() - defer func() { - // Try to stop osbuild if we exit early, we are gentle - // here to give osbuild the chance to release its - // resources (like mounts in the buildroot). This is - // best effort only (but also a pretty uncommon error - // condition). If ProcessState is set the process has - // already exited and we have nothing to do. - if err != nil && cmd.Process != nil && cmd.ProcessState == nil { - sigErr := cmd.Process.Signal(syscall.SIGINT) - err = errors.Join(err, sigErr) - } - }() - - var tracesMsgs []string - for { - st, err := osbuildStatus.Status() - if err != nil { - // This should never happen but if it does we try - // to be helpful. We need to exit here (and kill - // osbuild in the defer) or we would appear to be - // handing as cmd.Wait() would wait to finish but - // no progress or other message is reported. We - // can also not (in the general case) recover as - // the underlying osbuildStatus.scanner maybe in - // an unrecoverable state (like ErrTooBig). - return fmt.Errorf(`error parsing osbuild status, please report a bug and try with "--progress=verbose": %w`, err) - } - if st == nil { - break - } - i := 0 - for p := st.Progress; p != nil; p = p.SubProgress { - if err := pb.SetProgress(i, p.Message, p.Done, p.Total); err != nil { - logrus.Warnf("cannot set progress: %v", err) - } - i++ - } - // forward to user - if st.Message != "" { - pb.SetMessagef(st.Message) - } - - // keep internal log for error reporting, forward to - // external build log - if st.Message != "" { - tracesMsgs = append(tracesMsgs, st.Message) - fmt.Fprintln(buildLog, st.Message) - } - if st.Trace != "" { - tracesMsgs = append(tracesMsgs, st.Trace) - fmt.Fprintln(buildLog, st.Trace) - } - } - - if err := cmd.Wait(); err != nil { - return fmt.Errorf("error running osbuild: %w\nBuildLog:\n%s\nOutput:\n%s", err, strings.Join(tracesMsgs, "\n"), stdio.String()) - } - - return nil -} diff --git a/bib/pkg/progress/progress_test.go b/bib/pkg/progress/progress_test.go deleted file mode 100644 index f1621c789..000000000 --- a/bib/pkg/progress/progress_test.go +++ /dev/null @@ -1,265 +0,0 @@ -package progress_test - -import ( - "bytes" - "fmt" - "io" - "os" - "path/filepath" - "reflect" - "testing" - "time" - - "github.com/stretchr/testify/assert" - - "github.com/osbuild/bootc-image-builder/bib/pkg/progress" -) - -func TestProgressNew(t *testing.T) { - for _, tc := range []struct { - typ string - expected interface{} - expectedErr string - }{ - {"term", &progress.TerminalProgressBar{}, ""}, - {"debug", &progress.DebugProgressBar{}, ""}, - {"verbose", &progress.VerboseProgressBar{}, ""}, - // unknown progress type - {"bad", nil, `unknown progress type: "bad"`}, - } { - pb, err := progress.New(tc.typ) - if tc.expectedErr == "" { - assert.NoError(t, err) - assert.Equal(t, reflect.TypeOf(pb), reflect.TypeOf(tc.expected), fmt.Sprintf("[%v] %T not the expected %T", tc.typ, pb, tc.expected)) - } else { - assert.EqualError(t, err, tc.expectedErr) - } - } -} - -func TestVerboseProgress(t *testing.T) { - var buf bytes.Buffer - restore := progress.MockOsStderr(&buf) - defer restore() - - // verbose progress never generates progress output - pbar, err := progress.NewVerboseProgressBar() - assert.NoError(t, err) - err = pbar.SetProgress(0, "set-progress", 1, 100) - assert.NoError(t, err) - assert.Equal(t, "", buf.String()) - - // but it shows the messages - pbar.SetPulseMsgf("pulse") - assert.Equal(t, "pulse\n", buf.String()) - buf.Reset() - - pbar.SetMessagef("message") - assert.Equal(t, "message\n", buf.String()) - buf.Reset() - - pbar.Start() - assert.Equal(t, "", buf.String()) - pbar.Stop() - assert.Equal(t, "", buf.String()) -} - -func TestDebugProgress(t *testing.T) { - var buf bytes.Buffer - restore := progress.MockOsStderr(&buf) - defer restore() - - pbar, err := progress.NewDebugProgressBar() - assert.NoError(t, err) - err = pbar.SetProgress(0, "set-progress-msg", 1, 100) - assert.NoError(t, err) - assert.Equal(t, "[1 / 100] set-progress-msg\n", buf.String()) - buf.Reset() - - pbar.SetPulseMsgf("pulse-msg") - assert.Equal(t, "pulse: pulse-msg\n", buf.String()) - buf.Reset() - - pbar.SetMessagef("some-message") - assert.Equal(t, "msg: some-message\n", buf.String()) - buf.Reset() - - pbar.Start() - assert.Equal(t, "Start progressbar\n", buf.String()) - buf.Reset() - - pbar.Stop() - assert.Equal(t, "Stop progressbar\n", buf.String()) - buf.Reset() -} - -func TestTermProgress(t *testing.T) { - var buf bytes.Buffer - restore := progress.MockOsStderr(&buf) - defer restore() - - pbar, err := progress.NewTerminalProgressBar() - assert.NoError(t, err) - - pbar.Start() - pbar.SetPulseMsgf("pulse-msg") - pbar.SetMessagef("some-message") - err = pbar.SetProgress(0, "set-progress-msg", 0, 5) - assert.NoError(t, err) - pbar.Stop() - assert.NoError(t, pbar.(*progress.TerminalProgressBar).Err()) - - assert.Contains(t, buf.String(), "[1 / 6] set-progress-msg") - assert.Contains(t, buf.String(), "[|] pulse-msg\n") - assert.Contains(t, buf.String(), "Message: some-message\n") - // check shutdown - assert.Contains(t, buf.String(), progress.CURSOR_SHOW) -} - -func TestProgressNewAutoselect(t *testing.T) { - for _, tc := range []struct { - onTerm bool - expected interface{} - }{ - {false, &progress.VerboseProgressBar{}}, - {true, &progress.TerminalProgressBar{}}, - } { - restore := progress.MockIsattyIsTerminal(func(uintptr) bool { - return tc.onTerm - }) - defer restore() - - pb, err := progress.New("auto") - assert.NoError(t, err) - assert.Equal(t, reflect.TypeOf(pb), reflect.TypeOf(tc.expected), fmt.Sprintf("[%v] %T not the expected %T", tc.onTerm, pb, tc.expected)) - } -} - -func makeFakeOsbuild(t *testing.T, content string) string { - p := filepath.Join(t.TempDir(), "fake-osbuild") - err := os.WriteFile(p, []byte("#!/bin/sh\n"+content), 0755) - assert.NoError(t, err) - return p -} - -func TestRunOSBuildWithProgressErrorReporting(t *testing.T) { - restore := progress.MockOsStderr(io.Discard) - defer restore() - - restore = progress.MockOsbuildCmd(makeFakeOsbuild(t, ` ->&3 echo '{"message": "osbuild-stage-message"}' - -echo osbuild-stdout-output ->&2 echo osbuild-stderr-output -exit 112 -`)) - defer restore() - - pbar, err := progress.New("debug") - assert.NoError(t, err) - err = progress.RunOSBuild(pbar, []byte(`{"fake":"manifest"}`), nil, nil) - assert.EqualError(t, err, `error running osbuild: exit status 112 -BuildLog: -osbuild-stage-message -Output: -osbuild-stdout-output -osbuild-stderr-output -`) -} - -func TestRunOSBuildWithProgressIncorrectJSON(t *testing.T) { - signalDeliveredMarkerPath := filepath.Join(t.TempDir(), "sigint-delivered") - - restore := progress.MockOsbuildCmd(makeFakeOsbuild(t, fmt.Sprintf(` -trap 'touch "%s";exit 2' INT - ->&3 echo invalid-json - -# we cannot sleep infinity here or the shell script trap is never run -while true; do - sleep 0.1 -done -`, signalDeliveredMarkerPath))) - defer restore() - - pbar, err := progress.New("debug") - assert.NoError(t, err) - err = progress.RunOSBuild(pbar, []byte(`{"fake":"manifest"}`), nil, nil) - assert.EqualError(t, err, `error parsing osbuild status, please report a bug and try with "--progress=verbose": cannot scan line "invalid-json": invalid character 'i' looking for beginning of value`) - - // ensure the SIGINT got delivered - var pathExists = func(p string) bool { - _, err := os.Stat(p) - return err == nil - } - for i := 0; i < 20; i++ { - time.Sleep(100 * time.Millisecond) - if pathExists(signalDeliveredMarkerPath) { - break - } - } - assert.True(t, pathExists(signalDeliveredMarkerPath)) -} - -func TestRunOSBuildWithBuildlogTerm(t *testing.T) { - restore := progress.MockOsbuildCmd(makeFakeOsbuild(t, ` -echo osbuild-stdout-output ->&2 echo osbuild-stderr-output - -# without the sleep this is racy as two different go routines poll -# this does not matter (much) in practise because osbuild output and -# stage output are using the syncedMultiWriter so output is not garbled -sleep 0.1 ->&3 echo '{"message": "osbuild-stage-message"}' -`)) - defer restore() - - var fakeStdout, fakeStderr bytes.Buffer - restore = progress.MockOsStdout(&fakeStdout) - defer restore() - restore = progress.MockOsStderr(&fakeStderr) - defer restore() - - pbar, err := progress.New("term") - assert.NoError(t, err) - - var buildLog bytes.Buffer - opts := &progress.OSBuildOptions{ - BuildLog: &buildLog, - } - err = progress.RunOSBuild(pbar, []byte(`{"fake":"manifest"}`), nil, opts) - assert.NoError(t, err) - expectedOutput := `osbuild-stdout-output -osbuild-stderr-output -osbuild-stage-message -` - assert.Equal(t, expectedOutput, buildLog.String()) -} - -func TestRunOSBuildWithBuildlogVerbose(t *testing.T) { - restore := progress.MockOsbuildCmd(makeFakeOsbuild(t, ` -echo osbuild-stdout-output ->&2 echo osbuild-stderr-output -`)) - defer restore() - - var fakeStdout, fakeStderr bytes.Buffer - restore = progress.MockOsStdout(&fakeStdout) - defer restore() - restore = progress.MockOsStderr(&fakeStderr) - defer restore() - - pbar, err := progress.New("verbose") - assert.NoError(t, err) - - var buildLog bytes.Buffer - opts := &progress.OSBuildOptions{ - BuildLog: &buildLog, - } - err = progress.RunOSBuild(pbar, []byte(`{"fake":"manifest"}`), nil, opts) - assert.NoError(t, err) - expectedOutput := `osbuild-stdout-output -osbuild-stderr-output -` - assert.Equal(t, expectedOutput, buildLog.String()) -} diff --git a/bib/pkg/progress/syncwriter.go b/bib/pkg/progress/syncwriter.go deleted file mode 100644 index f9ca783a8..000000000 --- a/bib/pkg/progress/syncwriter.go +++ /dev/null @@ -1,22 +0,0 @@ -package progress - -import ( - "io" - "sync" -) - -type syncedWriter struct { - mu *sync.Mutex - w io.Writer -} - -func newSyncedWriter(mu *sync.Mutex, w io.Writer) io.Writer { - return &syncedWriter{mu: mu, w: w} -} - -func (sw *syncedWriter) Write(p []byte) (n int, err error) { - sw.mu.Lock() - defer sw.mu.Unlock() - - return sw.w.Write(p) -} diff --git a/bib/pkg/progress/syncwriter_test.go b/bib/pkg/progress/syncwriter_test.go deleted file mode 100644 index 32c37570b..000000000 --- a/bib/pkg/progress/syncwriter_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package progress_test - -import ( - "bufio" - "bytes" - "fmt" - "strings" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - - "github.com/osbuild/bootc-image-builder/bib/pkg/progress" -) - -func TestSyncWriter(t *testing.T) { - var mu sync.Mutex - var buf bytes.Buffer - var wg sync.WaitGroup - - for id := 0; id < 100; id++ { - wg.Add(1) - w := progress.NewSyncedWriter(&mu, &buf) - go func(id int) { - defer wg.Done() - for i := 0; i < 500; i++ { - fmt.Fprintln(w, strings.Repeat(fmt.Sprintf("%v", id%10), 60)) - time.Sleep(10 * time.Nanosecond) - } - }(id) - } - wg.Wait() - - scanner := bufio.NewScanner(&buf) - for { - if !scanner.Scan() { - break - } - line := scanner.Text() - assert.True(t, len(line) == 60, fmt.Sprintf("len %v: line: %v", len(line), line)) - } - assert.NoError(t, scanner.Err()) -} diff --git a/devel/Containerfile b/devel/Containerfile index 044213934..2254010ee 100644 --- a/devel/Containerfile +++ b/devel/Containerfile @@ -1,4 +1,4 @@ -FROM registry.fedoraproject.org/fedora:40 AS osbuild-builder +FROM registry.fedoraproject.org/fedora:42 AS osbuild-builder # build osbuild RPMs RUN dnf install -y rpm-build dnf-plugins-core git-core COPY --from=osbuild . /build @@ -8,7 +8,7 @@ RUN git config --global --add safe.directory /build RUN make rpm -FROM registry.fedoraproject.org/fedora:40 AS bib-builder +FROM registry.fedoraproject.org/fedora:42 AS bib-builder # replace osbuild/images dependency and build bib RUN dnf install -y git-core golang gpgme-devel libassuan-devel COPY --from=images . /build/images @@ -22,7 +22,7 @@ WORKDIR /build RUN ./build.sh -FROM registry.fedoraproject.org/fedora:40 +FROM registry.fedoraproject.org/fedora:42 COPY --from=osbuild-builder /build/rpmbuild/RPMS/noarch/*.rpm /rpms/ COPY ./package-requires.txt . RUN grep -vE '^#' package-requires.txt | xargs dnf install -y && rm -f package-requires.txt && dnf install -y /rpms/*.rpm && dnf clean all diff --git a/package-requires.txt b/package-requires.txt index e6c92fa65..84d325eeb 100644 --- a/package-requires.txt +++ b/package-requires.txt @@ -8,7 +8,7 @@ osbuild osbuild-ostree osbuild-depsolve-dnf osbuild-lvm2 podman # Image building dependencies -qemu-img +qemu-kvm-core virtiofsd qemu-img # rpm-ostree wants these for packages selinux-policy-targeted distribution-gpg-keys diff --git a/plans/integration.fmf b/plans/integration.fmf index c43795dfa..16a4d21f3 100644 --- a/plans/integration.fmf +++ b/plans/integration.fmf @@ -1,24 +1,28 @@ summary: Run all tests inside a VM environment provision: how: virtual - image: fedora:40 + image: fedora:42 hardware: virtualization: is-supported: true disk: - size: '>= 120 GB' + memory: ">= 8 GB" prepare: how: install package: - edk2-aarch64 + - git - osbuild-depsolve-dnf + - osbuild-lvm2 + - osbuild-ostree - podman - pytest - python3-boto3 - python3-flake8 - - python3-paramiko - python3-pip - skopeo + - sshpass - qemu-kvm - qemu-system-aarch64 - qemu-user-static @@ -31,7 +35,8 @@ execute: echo "Install test requirements" pip install --user -r test/requirements.txt echo "Run tests" - pytest --force-aws-upload + # mvo:2025-07-14: disabled AWS upload test until we add back the credentials + pytest # --force-aws-upload duration: 4h finish: how: shell diff --git a/plans/unit-go.fmf b/plans/unit-go.fmf index f0f075cc5..ee79d0b26 100644 --- a/plans/unit-go.fmf +++ b/plans/unit-go.fmf @@ -1,7 +1,7 @@ summary: Run all tests inside a VM environment provision: how: virtual - image: fedora:40 + image: fedora:42 prepare: how: install package: diff --git a/test/conftest.py b/test/conftest.py index 4db68ad67..acdfb3937 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,6 +1,8 @@ import pytest +# pylint: disable=wrong-import-order from testcases import TestCase +from vmtest.util import get_free_port def pytest_addoption(parser): @@ -20,3 +22,8 @@ def pytest_make_parametrize_id(config, val): # pylint: disable=W0613 if isinstance(val, TestCase): return f"{val}" return None + + +@pytest.fixture(name="free_port") +def free_port_fixture(): + return get_free_port() diff --git a/test/containerbuild.py b/test/containerbuild.py index b762a8d94..0e0d48b85 100644 --- a/test/containerbuild.py +++ b/test/containerbuild.py @@ -1,4 +1,5 @@ import os +import pathlib import platform import random import string @@ -25,6 +26,7 @@ def make_container(container_path, arch=None): subprocess.check_call([ "podman", "build", + "--cache-ttl=1h", "-t", container_tag, "--arch", arch, container_path], encoding="utf8") @@ -41,12 +43,57 @@ def build_container_fixture(): container_tag = "bootc-image-builder-test" subprocess.check_call([ "podman", "build", + "--cache-ttl=1h", "-f", "Containerfile", "-t", container_tag, ]) return container_tag +@pytest.fixture(name="pxe_container", scope="session") +def pxe_container_fixture(tmpdir_factory): + """ + Build a PXE-capable bootc image (dracut-live, squashfs-tools, + dmsquash-live initramfs) with a dedicated tag for PXE tests. + Uses the same base as other tests (centos-bootc:stream9). + """ + if tag_from_env := os.getenv("BIB_TEST_PXE_CONTAINER_TAG"): + return tag_from_env + + tmp_path = pathlib.Path(tmpdir_factory.mktemp("build-pxe-container")) + containerfile = tmp_path / "Containerfile" + # Use echo/printf instead of heredoc so we avoid delimiter-at-line-start + # issues when the content is written via textwrap.dedent. + containerfile.write_text(textwrap.dedent("""\ + FROM quay.io/centos-bootc/centos-bootc:stream9 + RUN dnf -y install dracut-live squashfs-tools && dnf clean all + # Override using composefs for ostree (incompatible with squashfs rootfs) + RUN echo '[composefs]' > /usr/lib/ostree/prepare-root.conf && \\ + echo 'enabled = no' >> /usr/lib/ostree/prepare-root.conf && \\ + echo '[sysroot]' >> /usr/lib/ostree/prepare-root.conf && \\ + echo 'readonly = true' >> /usr/lib/ostree/prepare-root.conf + + # Include the dmsquash-live module in the initramfs + RUN echo 'compress="xz"' > /usr/lib/dracut/dracut.conf.d/40-pxe.conf && \\ + echo 'add_dracutmodules+=" qemu qemu-net livenet dmsquash-live "' >> \\ + /usr/lib/dracut/dracut.conf.d/40-pxe.conf && \\ + echo 'early_microcode="no"' >> /usr/lib/dracut/dracut.conf.d/40-pxe.conf + + # Rebuild the initrd + RUN set -xe; kver=$(ls /usr/lib/modules); \\ + env DRACUT_NO_XATTR=1 dracut -vf /usr/lib/modules/$kver/initramfs.img "$kver" + """), encoding="utf8") + pxe_container_tag = "localhost/bootc-image-builder-test-pxe" + subprocess.check_call([ + "podman", "build", + "--cache-ttl=1h", + "-f", str(containerfile), + "-t", pxe_container_tag, + str(tmp_path), + ]) + return pxe_container_tag + + @pytest.fixture(name="build_fake_container", scope="session") def build_fake_container_fixture(tmpdir_factory, build_container): """Build a container with a fake osbuild and returns the name""" diff --git a/test/requirements.txt b/test/requirements.txt index 9be09ce71..5a58554d4 100644 --- a/test/requirements.txt +++ b/test/requirements.txt @@ -1,6 +1,6 @@ pytest==7.4.3 flake8==6.1.0 -paramiko==2.12.0 boto3==1.33.13 qmp==1.1.0 pylint==3.2.5 +vmtest @ git+https://github.com/osbuild/images.git diff --git a/test/test_build_cross.py b/test/test_build_cross.py new file mode 100644 index 000000000..12b89eebd --- /dev/null +++ b/test/test_build_cross.py @@ -0,0 +1,23 @@ +import platform + +import pytest + +from testcases import gen_testcases + +from test_build_disk import ( # pylint: disable=unused-import + assert_disk_image_boots, + build_container_fixture, + gpg_conf_fixture, + image_type_fixture, + registry_conf_fixture, + shared_tmpdir_fixture, +) + + +# This testcase is not part of "test_build_disk.py:test_image_boots" +# because it takes ~30min on the GH runners so moving it into a +# separate file ensures it is run in parallel on GH. +@pytest.mark.skipif(platform.system() != "Linux", reason="boot test only runs on linux right now") +@pytest.mark.parametrize("image_type", gen_testcases("qemu-cross"), indirect=["image_type"]) +def test_image_boots_cross(image_type): + assert_disk_image_boots(image_type) diff --git a/test/test_build.py b/test/test_build_disk.py similarity index 86% rename from test/test_build.py rename to test/test_build_disk.py index b8d367b62..7672bd80f 100644 --- a/test/test_build.py +++ b/test/test_build_disk.py @@ -2,8 +2,10 @@ import os import pathlib import platform +import random import re import shutil +import string import subprocess import tempfile import uuid @@ -16,7 +18,8 @@ import testutil from containerbuild import build_container_fixture # pylint: disable=unused-import from testcases import CLOUD_BOOT_IMAGE_TYPES, DISK_IMAGE_TYPES, gen_testcases -from vm import AWS, QEMU +import vmtest.util +from vmtest.vm import AWS_REGION, AWS, QEMU if not testutil.has_executable("podman"): pytest.skip("no podman, skipping integration tests that required podman", allow_module_level=True) @@ -35,6 +38,7 @@ class ImageBuildResult(NamedTuple): img_path: str img_arch: str container_ref: str + build_container_ref: str rootfs: str disk_config: str username: str @@ -110,7 +114,7 @@ def registry_conf_fixture(shared_tmpdir, request): {local_registry}: lookaside: file:///{sigstore_dir} """ - registry_port = testutil.get_free_port() + registry_port = vmtest.util.get_free_port() # We cannot use localhost as we need to access the registry from both # the host system and the bootc-image-builder container. default_ip = testutil.get_ip_from_default_route() @@ -137,7 +141,8 @@ def registry_conf_fixture(shared_tmpdir, request): "-p", f"{registry_port}:5000", "--restart", "always", "--name", registry_container_name, - "registry:2" + # We use a copy of docker.io registry to avoid running into docker.io pull rate limits + "ghcr.io/osbuild/bootc-image-builder/registry:2" ], check=True) registry_container_state = subprocess.run([ @@ -254,7 +259,9 @@ def build_images(shared_tmpdir, build_container, request, force_aws_upload, gpg_ image_types = request.param.image.split("+") username = "test" - password = "password" + # use 18 char random password + password = "".join( + random.choices(string.ascii_uppercase + string.digits, k=18)) kargs = "systemd.journald.forward_to_console=1" container_ref = tc.container_ref @@ -313,7 +320,7 @@ def build_images(shared_tmpdir, build_container, request, force_aws_upload, gpg_ bib_output = bib_output_path.read_text(encoding="utf8") results.append(ImageBuildResult( image_type, generated_img, tc.target_arch, - container_ref, tc.rootfs, tc.disk_config, + container_ref, tc.build_container_ref, tc.rootfs, tc.disk_config, username, password, ssh_keyfile_private_path, kargs, bib_output, journal_output)) @@ -358,20 +365,40 @@ def build_images(shared_tmpdir, build_container, request, force_aws_upload, gpg_ "kernel": { "append": kargs, }, + "files": [ + { + "path": "/etc/some-file", + "data": "some-data", + }, + ], + "directories": [ + { + "path": "/etc/some-dir", + }, + ], }, } testutil.maybe_create_filesystem_customizations(cfg, tc) testutil.maybe_create_disk_customizations(cfg, tc) - print(f"config for {output_path} {tc=}: {cfg=}") + # if we build an iso we cannot have the "home" customization for + # user root or images will panic(), c.f. + # https://github.com/osbuild/images/pull/1806 + if not image_types[0] in DISK_IMAGE_TYPES: + del cfg["customizations"]["user"][0]["home"] config_json_path = output_path / "config.json" config_json_path.write_text(json.dumps(cfg), encoding="utf-8") + # mask pw + for user in cfg["customizations"]["user"]: + user["password"] = "***" + print(f"config for {output_path} {tc=}: {cfg=}") cursor = testutil.journal_cursor() upload_args = [] creds_args = [] target_arch_args = [] + build_container_args = [] if tc.target_arch: target_arch_args = ["--target-arch", tc.target_arch] @@ -384,7 +411,7 @@ def build_images(shared_tmpdir, build_container, request, force_aws_upload, gpg_ upload_args = [ f"--aws-ami-name=bootc-image-builder-test-{str(uuid.uuid4())}", - f"--aws-region={testutil.AWS_REGION}", + f"--aws-region={AWS_REGION}", "--aws-bucket=bootc-image-builder-ci", ] elif force_aws_upload: @@ -395,6 +422,7 @@ def build_images(shared_tmpdir, build_container, request, force_aws_upload, gpg_ if image_types[0] in DISK_IMAGE_TYPES: types_arg = [f"--type={it}" for it in DISK_IMAGE_TYPES] else: + # building an iso types_arg = [f"--type={image_types[0]}"] # run container to deploy an image into a bootable disk and upload to a cloud service if applicable @@ -405,6 +433,11 @@ def build_images(shared_tmpdir, build_container, request, force_aws_upload, gpg_ "-v", "/var/tmp/osbuild-test-store:/store", # share the cache between builds "-v", "/var/lib/containers/storage:/var/lib/containers/storage", # mount the host's containers storage ] + if tc.target_arch: + # help debug cross-arch issues by making qemu-user print + cmd.extend( + ["--env", "OSBUILD_EXPERIMENTAL=debug-qemu-user"]) + if tc.podman_terminal: cmd.append("-t") @@ -421,10 +454,16 @@ def build_images(shared_tmpdir, build_container, request, force_aws_upload, gpg_ # Pull the signed image testutil.pull_container(container_ref, tls_verify=False) + if tc.build_container_ref: + build_container_args = [ + "--build-container", tc.build_container_ref, + ] + cmd.extend([ *creds_args, build_container, container_ref, + *build_container_args, *types_arg, *upload_args, *target_arch_args, @@ -454,7 +493,7 @@ def build_images(shared_tmpdir, build_container, request, force_aws_upload, gpg_ metadata["ami_id"] = parse_ami_id_from_log(journal_output) def del_ami(): - testutil.deregister_ami(metadata["ami_id"]) + testutil.deregister_ami(metadata["ami_id"], AWS_REGION) request.addfinalizer(del_ami) journal_log_path.write_text(journal_output, encoding="utf8") @@ -464,7 +503,7 @@ def del_ami(): for image_type in image_types: results.append(ImageBuildResult( image_type, artifact[image_type], tc.target_arch, - container_ref, tc.rootfs, tc.disk_config, + container_ref, tc.build_container_ref, tc.rootfs, tc.disk_config, username, password, ssh_keyfile_private_path, kargs, bib_output, journal_output, metadata)) yield results @@ -498,9 +537,15 @@ def test_image_is_generated(image_type): f"content: {os.listdir(os.fspath(image_type.img_path))}" +@pytest.mark.parametrize("image_type", gen_testcases("build-container"), indirect=["image_type"]) +def test_build_container_works(image_type): + assert image_type.img_path.exists(), "output file missing, dir "\ + f"content: {os.listdir(os.fspath(image_type.img_path))}" + + def assert_kernel_args(test_vm, image_type): - exit_status, kcmdline = test_vm.run("cat /proc/cmdline", user=image_type.username, password=image_type.password) - assert exit_status == 0 + ret = test_vm.run(["cat", "/proc/cmdline"], user=image_type.username, password=image_type.password) + kcmdline = ret.stdout # the kernel arg string must have a space as the prefix and either a space # as suffix or be the last element of the kernel commandline assert re.search(f" {re.escape(image_type.kargs)}( |$)", kcmdline) @@ -509,26 +554,34 @@ def assert_kernel_args(test_vm, image_type): @pytest.mark.skipif(platform.system() != "Linux", reason="boot test only runs on linux right now") @pytest.mark.parametrize("image_type", gen_testcases("qemu-boot"), indirect=["image_type"]) def test_image_boots(image_type): + assert_disk_image_boots(image_type) + + +def assert_disk_image_boots(image_type): with QEMU(image_type.img_path, arch=image_type.img_arch) as test_vm: # user/password login works - exit_status, _ = test_vm.run("true", user=image_type.username, password=image_type.password) - assert exit_status == 0 + test_vm.run("true", user=image_type.username, password=image_type.password) # root/ssh login also works - exit_status, output = test_vm.run("id", user="root", keyfile=image_type.ssh_keyfile_private_path) - assert exit_status == 0 - assert "uid=0" in output + ret = test_vm.run("id", user="root", keyfile=image_type.ssh_keyfile_private_path) + assert "uid=0" in ret.stdout # check generic image options assert_kernel_args(test_vm, image_type) # ensure bootc points to the right image - _, output = test_vm.run("bootc status", user="root", keyfile=image_type.ssh_keyfile_private_path) + ret = test_vm.run(["bootc", "status"], user="root", keyfile=image_type.ssh_keyfile_private_path) # XXX: read the fully yaml instead? - assert f"image: {image_type.container_ref}" in output + assert f"image: {image_type.container_ref}" in ret.stdout if image_type.disk_config: assert_disk_customizations(image_type, test_vm) else: assert_fs_customizations(image_type, test_vm) + # check file/dir customizations + ret = test_vm.run(["stat", "/etc/some-file"], user=image_type.username, password=image_type.password) + assert "File: /etc/some-file" in ret.stdout + ret = test_vm.run(["stat", "/etc/some-dir"], user=image_type.username, password=image_type.password) + assert "File: /etc/some-dir" in ret.stdout + @pytest.mark.parametrize("image_type", gen_testcases("ami-boot"), indirect=["image_type"]) def test_ami_boots_in_aws(image_type, force_aws_upload): @@ -542,11 +595,9 @@ def test_ami_boots_in_aws(image_type, force_aws_upload): # 4.30 GiB / 10.00 GiB [------------>____________] 43.02% 58.04 MiB p/s assert "] 100.00%" in image_type.bib_output with AWS(image_type.metadata["ami_id"]) as test_vm: - exit_status, _ = test_vm.run("true", user=image_type.username, password=image_type.password) - assert exit_status == 0 - exit_status, output = test_vm.run("echo hello", user=image_type.username, password=image_type.password) - assert exit_status == 0 - assert "hello" in output + test_vm.run("true", user=image_type.username, password=image_type.password) + ret = test_vm.run(["echo", "hello"], user=image_type.username, password=image_type.password) + assert "hello" in ret.stdout def log_has_osbuild_selinux_denials(log): @@ -585,63 +636,14 @@ def has_selinux(): @pytest.mark.skipif(not has_selinux(), reason="selinux not enabled") @pytest.mark.parametrize("image_type", gen_testcases("qemu-boot"), indirect=["image_type"]) def test_image_build_without_se_linux_denials(image_type): + pytest.skip("skip until https://github.com/osbuild/bootc-image-builder/issues/645 is resolved") + # the journal always contains logs from the image building assert image_type.journal_output != "" assert not log_has_osbuild_selinux_denials(image_type.journal_output), \ f"denials in log {image_type.journal_output}" -@pytest.mark.skipif(platform.system() != "Linux", reason="boot test only runs on linux right now") -@pytest.mark.parametrize("image_type", gen_testcases("anaconda-iso"), indirect=["image_type"]) -def test_iso_installs(image_type): - installer_iso_path = image_type.img_path - test_disk_path = installer_iso_path.with_name("test-disk.img") - with open(test_disk_path, "w", encoding="utf8") as fp: - fp.truncate(10_1000_1000_1000) - # install to test disk - with QEMU(test_disk_path, cdrom=installer_iso_path) as vm: - vm.start(wait_event="qmp:RESET", snapshot=False, use_ovmf=True) - vm.force_stop() - # boot test disk and do extremly simple check - with QEMU(test_disk_path) as vm: - vm.start(use_ovmf=True) - exit_status, _ = vm.run("true", user=image_type.username, password=image_type.password) - assert exit_status == 0 - assert_kernel_args(vm, image_type) - - -def osinfo_for(it: ImageBuildResult, arch: str) -> str: - base = "Media is an installer for OS" - if it.container_ref.endswith("/centos-bootc/centos-bootc:stream9"): - return f"{base} 'CentOS Stream 9 ({arch})'\n" - if it.container_ref.endswith("/centos-bootc/centos-bootc:stream10"): - # XXX: uncomment once - # https://gitlab.com/libosinfo/osinfo-db/-/commit/fc811ba5a792967e22a0108de5a245b23da3cc66 - # gets released - # return f"CentOS Stream 10 ({arch})" - return "" - if "/fedora/fedora-bootc:" in it.container_ref: - ver = it.container_ref.rsplit(":", maxsplit=1)[1] - return f"{base} 'Fedora Server {ver} ({arch})'\n" - raise ValueError(f"unknown osinfo string for '{it.container_ref}'") - - -@pytest.mark.skipif(platform.system() != "Linux", reason="osinfo detect test only runs on linux right now") -@pytest.mark.parametrize("image_type", gen_testcases("anaconda-iso"), indirect=["image_type"]) -def test_iso_os_detection(image_type): - installer_iso_path = image_type.img_path - arch = image_type.img_arch - if not arch: - arch = platform.machine() - result = subprocess.run([ - "osinfo-detect", - installer_iso_path, - ], capture_output=True, text=True, check=True) - osinfo_output = result.stdout - expected_output = f"Media is bootable.\n{osinfo_for(image_type, arch)}" - assert osinfo_output == expected_output - - @pytest.mark.skipif(platform.system() != "Linux", reason="osinfo detect test only runs on linux right now") @pytest.mark.skipif(not testutil.has_executable("unsquashfs"), reason="need unsquashfs") @pytest.mark.parametrize("image_type", gen_testcases("anaconda-iso"), indirect=["image_type"]) @@ -678,13 +680,15 @@ def assert_fs_customizations(image_type, test_vm): """ # check the minsize specified in the build configuration for each mountpoint against the sizes in the image # TODO: replace 'df' call with 'parted --json' and find the partition size for each mountpoint - exit_status, output = test_vm.run("df --output=target,size", user="root", - keyfile=image_type.ssh_keyfile_private_path) - assert exit_status == 0 + ret = test_vm.run(["df", "--all", "--output=target,size"], user="root", + keyfile=image_type.ssh_keyfile_private_path) # parse the output of 'df' to a mountpoint -> size dict for convenience mountpoint_sizes = {} - for line in output.splitlines()[1:]: + for line in ret.stdout.splitlines()[1:]: fields = line.split() + # some filesystems to not report a size with --all + if fields[1] == "-": + continue # Note that df output is in 1k blocks, not bytes mountpoint_sizes[fields[0]] = int(fields[1]) * 2 ** 10 # in bytes @@ -701,13 +705,12 @@ def assert_fs_customizations(image_type, test_vm): def assert_disk_customizations(image_type, test_vm): - exit_status, output = test_vm.run("findmnt --json", user="root", - keyfile=image_type.ssh_keyfile_private_path) - assert exit_status == 0 - findmnt = json.loads(output) - exit_status, swapon_output = test_vm.run("swapon --show", user="root", - keyfile=image_type.ssh_keyfile_private_path) - assert exit_status == 0 + ret = test_vm.run(["findmnt", "--json"], user="root", + keyfile=image_type.ssh_keyfile_private_path) + findmnt = json.loads(ret.stdout) + swapon_ret = test_vm.run(["swapon", "--show"], user="root", + keyfile=image_type.ssh_keyfile_private_path) + swapon_output = swapon_ret.stdout if dc := image_type.disk_config: if dc == "lvm": mnts = [mnt for mnt in findmnt["filesystems"][0]["children"] diff --git a/test/test_build_iso.py b/test/test_build_iso.py new file mode 100644 index 000000000..f66a8e847 --- /dev/null +++ b/test/test_build_iso.py @@ -0,0 +1,205 @@ +import os +import random +import json +import platform +import string +import subprocess +import textwrap +from contextlib import ExitStack + +import pytest +# local test utils +import testutil +from containerbuild import build_container_fixture, make_container # pylint: disable=unused-import +from testcases import gen_testcases +from test_build_disk import ( + assert_kernel_args, + ImageBuildResult, +) +from test_build_disk import ( # pylint: disable=unused-import + gpg_conf_fixture, + image_type_fixture, + registry_conf_fixture, + shared_tmpdir_fixture, +) +from vmtest.vm import QEMU + + +ISO_BOOT_TIMEOUT = 1800 + + +@pytest.mark.skipif(platform.system() != "Linux", reason="boot test only runs on linux right now") +@pytest.mark.parametrize("image_type", gen_testcases("anaconda-iso"), indirect=["image_type"]) +def test_iso_installs(image_type): + installer_iso_path = image_type.img_path + test_disk_path = installer_iso_path.with_name("test-disk.img") + with open(test_disk_path, "w", encoding="utf8") as fp: + fp.truncate(10_1000_1000_1000) + # install to test disk + with QEMU(test_disk_path, cdrom=installer_iso_path) as vm: + vm.start(wait_event="qmp:RESET", snapshot=False, use_ovmf=True, timeout_sec=ISO_BOOT_TIMEOUT) + vm.force_stop() + # boot test disk and do extremly simple check + with QEMU(test_disk_path) as vm: + vm.start(use_ovmf=True) + vm.run("true", user=image_type.username, password=image_type.password) + assert_kernel_args(vm, image_type) + + +def osinfo_for(it: ImageBuildResult, arch: str) -> str: + base = "Media is an installer for OS" + if it.container_ref.endswith("/centos-bootc/centos-bootc:stream9"): + return f"{base} 'CentOS Stream 9 ({arch})'\n" + if it.container_ref.endswith("/centos-bootc/centos-bootc:stream10"): + return f"Media is an installer for OS 'CentOS Stream 10 ({arch})'\n" + if "/fedora/fedora-bootc:" in it.container_ref: + ver = it.container_ref.rsplit(":", maxsplit=1)[1] + return f"{base} 'Fedora Server {ver} ({arch})'\n" + raise ValueError(f"unknown osinfo string for '{it.container_ref}'") + + +@pytest.mark.skipif(platform.system() != "Linux", reason="osinfo detect test only runs on linux right now") +@pytest.mark.parametrize("image_type", gen_testcases("anaconda-iso"), indirect=["image_type"]) +def test_iso_os_detection(image_type): + installer_iso_path = image_type.img_path + arch = image_type.img_arch + if not arch: + arch = platform.machine() + result = subprocess.run([ + "osinfo-detect", + installer_iso_path, + ], capture_output=True, text=True, check=True) + osinfo_output = result.stdout + expected_output = f"Media is bootable.\n{osinfo_for(image_type, arch)}" + assert osinfo_output == expected_output + + +@pytest.mark.skipif(platform.system() != "Linux", reason="osinfo detect test only runs on linux right now") +@pytest.mark.skipif(not testutil.has_executable("unsquashfs"), reason="need unsquashfs") +@pytest.mark.parametrize("image_type", gen_testcases("anaconda-iso"), indirect=["image_type"]) +def test_iso_install_img_is_squashfs(tmp_path, image_type): + installer_iso_path = image_type.img_path + with ExitStack() as cm: + mount_point = tmp_path / "cdrom" + mount_point.mkdir() + subprocess.check_call(["mount", installer_iso_path, os.fspath(mount_point)]) + cm.callback(subprocess.check_call, ["umount", os.fspath(mount_point)]) + # ensure install.img is the "flat" squashfs, before PR#777 the content + # was an intermediate ext4 image "squashfs-root/LiveOS/rootfs.img" + output = subprocess.check_output(["unsquashfs", "-ls", mount_point / "images/install.img"], text=True) + assert "usr/bin/bootc" in output + + +@pytest.mark.skipif(platform.system() != "Linux", reason="boot test only runs on linux right now") +@pytest.mark.parametrize("container_ref", [ + "quay.io/centos-bootc/centos-bootc:stream10", + "quay.io/fedora/fedora-bootc:42", + "quay.io/centos-bootc/centos-bootc:stream9", +]) +# pylint: disable=too-many-locals +def test_bootc_installer_iso_installs(tmp_path, build_container, container_ref): + # XXX: duplicated from test_build_disk.py + username = "test" + password = "".join( + random.choices(string.ascii_uppercase + string.digits, k=18)) + ssh_keyfile_private_path = tmp_path / "ssh-keyfile" + ssh_keyfile_public_path = ssh_keyfile_private_path.with_suffix(".pub") + if not ssh_keyfile_private_path.exists(): + subprocess.run([ + "ssh-keygen", + "-N", "", + # be very conservative with keys for paramiko + "-b", "2048", + "-t", "rsa", + "-f", os.fspath(ssh_keyfile_private_path), + ], check=True) + ssh_pubkey = ssh_keyfile_public_path.read_text(encoding="utf8").strip() + cfg = { + "customizations": { + "user": [ + { + "name": "root", + "key": ssh_pubkey, + # note that we have no "home" here for ISOs + }, { + "name": username, + "password": password, + "groups": ["wheel"], + }, + ], + "kernel": { + # XXX: we need https://github.com/osbuild/images/pull/1786 or no kargs are added to anaconda + # XXX2: drop a bunch of the debug flags + # + # Use console=ttyS0 so that we see output in our debug + # logs. by default anaconda prints to the last console= + # from the kernel commandline + "append": "systemd.debug-shell=1 rd.systemd.debug-shell=1 inst.debug console=ttyS0", + }, + }, + } + config_json_path = tmp_path / "config.json" + config_json_path.write_text(json.dumps(cfg), encoding="utf-8") + # create anaconda iso from base + cntf_path = tmp_path / "Containerfile" + cntf_path.write_text(textwrap.dedent(f"""\n + FROM {container_ref} + RUN dnf install -y \ + anaconda-core \ + anaconda-dracut \ + anaconda-install-img-deps \ + biosdevname \ + grub2-efi-x64-cdboot \ + net-tools \ + prefixdevname \ + python3-mako \ + lorax-templates-* \ + squashfs-tools \ + && dnf clean all + # shim-x64 is marked installed but the files are not in the expected + # place for https://github.com/osbuild/osbuild/blob/v160/stages/org.osbuild.grub2.iso#L91, see + # workaround via reinstall, we could add a config to the grub2.iso + # stage to allow a different prefix that then would be used by + # anaconda. + # If https://github.com/osbuild/osbuild/pull/2204 would get merged we + # can update images/ to set the correct efi_src_dirs and this can + # be removed (but its rather ugly). + # See also https://bugzilla.redhat.com/show_bug.cgi?id=1750708 + RUN dnf reinstall -y shim-x64 + # lorax wants to create a symlink in /mnt which points to /var/mnt + # on bootc but /var/mnt does not exist on some images. + # + # If https://gitlab.com/fedora/bootc/base-images/-/merge_requests/294 + # gets merged this will be no longer needed + RUN mkdir /var/mnt + """), encoding="utf8") + output_path = tmp_path / "output" + output_path.mkdir() + with make_container(tmp_path) as container_tag: + cmd = [ + *testutil.podman_run_common, + "-v", f"{config_json_path}:/config.json:ro", + "-v", f"{output_path}:/output", + "-v", "/var/tmp/osbuild-test-store:/store", # share the cache between builds + "-v", "/var/lib/containers/storage:/var/lib/containers/storage", + build_container, + "--type", "bootc-installer", + "--rootfs", "ext4", + "--installer-payload-ref", container_ref, + f"localhost/{container_tag}", + ] + subprocess.check_call(cmd) + installer_iso_path = output_path / "bootiso" / "install.iso" + test_disk_path = installer_iso_path.with_name("test-disk.img") + with open(test_disk_path, "w", encoding="utf8") as fp: + fp.truncate(10_1000_1000_1000) + # install to test disk + with QEMU(test_disk_path, cdrom=installer_iso_path) as vm: + vm.start(wait_event="qmp:RESET", snapshot=False, use_ovmf=True, timeout_sec=ISO_BOOT_TIMEOUT) + vm.force_stop() + # boot test disk and do extremly simple check + with QEMU(test_disk_path) as vm: + vm.start(use_ovmf=True) + vm.run("true", user=username, password=password) + ret = vm.run(["bootc", "status"], user="root", keyfile=ssh_keyfile_private_path) + assert f"image: {container_ref}" in ret.stdout diff --git a/test/test_manifest.py b/test/test_manifest.py index 32fb90ef2..24b3a3f05 100644 --- a/test/test_manifest.py +++ b/test/test_manifest.py @@ -1,3 +1,5 @@ +# pylint: disable=too-many-lines + import base64 import hashlib import json @@ -10,6 +12,7 @@ import testutil from containerbuild import build_container_fixture # pylint: disable=unused-import +from containerbuild import pxe_container_fixture # pylint: disable=unused-import from containerbuild import make_container from testcases import gen_testcases @@ -51,14 +54,41 @@ def test_manifest_smoke(build_container, tc): @pytest.mark.parametrize("tc", gen_testcases("anaconda-iso")) -def test_iso_manifest_smoke(build_container, tc): +def test_rpm_iso_manifest_smoke(build_container, tc): testutil.pull_container(tc.container_ref, tc.target_arch) output = subprocess.check_output([ *testutil.podman_run_common, build_container, "manifest", - "--type=anaconda-iso", f"{tc.container_ref}", + *tc.bib_rootfs_args(), + "--type=anaconda-iso", + f"{tc.container_ref}", + ]) + manifest = json.loads(output) + # just some basic validation + expected_pipeline_names = ["build", "anaconda-tree", "efiboot-tree", "bootiso-tree", "bootiso"] + assert manifest["version"] == "2" + assert [pipeline["name"] for pipeline in manifest["pipelines"]] == expected_pipeline_names + + +def test_bootc_iso_manifest_smoke(build_container): + container_ref = "quay.io/centos-bootc/centos-bootc:stream9" + # Note that this is not a realistic ref, a generic bootc + # image does not contain anaconda so this won't produce a + # working installer. For the purpose of the test to validate + # that we get a manifest with the right refs its good enough. + installer_payload_ref = "quay.io/centos-bootc/centos-bootc:stream10" + testutil.pull_container(container_ref) + testutil.pull_container(installer_payload_ref) + + output = subprocess.check_output([ + *testutil.podman_run_common, + build_container, + "manifest", + "--type=bootc-installer", + f"{container_ref}", + f"--installer-payload-ref={installer_payload_ref}", ]) manifest = json.loads(output) # just some basic validation @@ -67,6 +97,21 @@ def test_iso_manifest_smoke(build_container, tc): assert [pipeline["name"] for pipeline in manifest["pipelines"]] == expected_pipeline_names +def test_pxe_tar_xz_manifest_smoke(pxe_container, build_container): + output = subprocess.check_output([ + *testutil.podman_run_common, + build_container, + "manifest", + "--type=pxe-tar-xz", + pxe_container, + ]) + manifest = json.loads(output) + pipeline_names = [pipeline["name"] for pipeline in manifest["pipelines"]] + assert manifest["version"] == "2" + assert "build" in pipeline_names + assert "bootc-pxe-tree" in pipeline_names + + @pytest.mark.parametrize("tc", gen_testcases("manifest")) def test_manifest_disksize(tmp_path, build_container, tc): testutil.pull_container(tc.container_ref, tc.target_arch) @@ -149,7 +194,8 @@ def test_manifest_cross_arch_check(tmp_path, build_container): "manifest", "--target-arch=aarch64", f"localhost/{container_tag}" ], check=True, capture_output=True, encoding="utf8") - assert 'image found is for unexpected architecture "x86_64"' in exc.value.stderr + assert ('cannot generate manifest: requested bootc arch "aarch64" ' + 'does not match available arches [x86_64]') in exc.value.stderr def find_rootfs_type_from(manifest_str): @@ -304,7 +350,7 @@ def test_mount_ostree_error(tmpdir_factory, build_container): "manifest", f"{container_ref}", "--config", "/output/config.json", ], stderr=subprocess.PIPE, encoding="utf8") - assert 'The following errors occurred while validating custom mountpoints:\npath "/ostree" is not allowed' \ + assert 'the following errors occurred while validating custom mountpoints:\npath "/ostree" is not allowed' \ in exc.value.stderr @@ -405,15 +451,36 @@ def test_manifest_anaconda_module_customizations(tmpdir_factory, build_container assert "org.fedoraproject.Anaconda.Modules.Timezone" not in st["options"]["activatable-modules"] -def find_fstab_stage_from(manifest_str): +def find_fs_mount_info_from(manifest_str): manifest = json.loads(manifest_str) + mount_stages = [] + # normally there should be only one swap partition, but there's no technical reason you can't have multiple + swap_stages = [] for pipeline in manifest["pipelines"]: - # the fstab stage in cross-arch manifests is in the "ostree-deployment" pipeline + # the mount unit stages in cross-arch manifests are in the "ostree-deployment" pipeline if pipeline["name"] in ("image", "ostree-deployment"): for st in pipeline["stages"]: - if st["type"] == "org.osbuild.fstab": - return st - raise ValueError(f"cannot find fstab stage in manifest:\n{manifest_str}") + if st["type"] == "org.osbuild.systemd.unit.create": + options = st["options"] + if options["filename"].endswith(".mount"): + mount_stages.append(st) + elif options["filename"].endswith(".swap"): + swap_stages.append(st) + + if not mount_stages: + raise ValueError(f"cannot find mount unit creation stages in manifest:\n{manifest_str}") + + mounts = [] + for stage in mount_stages: + options = stage["options"]["config"] + mounts.append(options["Mount"]) + + swaps = [] + for stage in swap_stages: + options = stage["options"]["config"] + swaps.append(options["Swap"]) + + return mounts, swaps @pytest.mark.parametrize("fscustomizations,rootfs", [ @@ -480,25 +547,23 @@ def test_manifest_fs_customizations_smoke_toml(tmp_path, build_container): def assert_fs_customizations(customizations, fstype, manifest): - # use the fstab stage to get filesystem types for each mountpoint - fstab_stage = find_fstab_stage_from(manifest) - filesystems = fstab_stage["options"]["filesystems"] + mounts, _ = find_fs_mount_info_from(manifest) manifest_mountpoints = set() - for fs in filesystems: - manifest_mountpoints.add(fs["path"]) - if fs["path"] == "/boot/efi": - assert fs["vfs_type"] == "vfat" + for mount in mounts: + manifest_mountpoints.add(mount["Where"]) + if mount["Where"] == "/boot/efi": + assert mount["Type"] == "vfat" continue - if fstype == "btrfs" and fs["path"] == "/boot": + if fstype == "btrfs" and mount["Where"] == "/boot": # /boot keeps its default fstype when using btrfs - assert fs["vfs_type"] == "ext4" + assert mount["Type"] == "ext4" continue - assert fs["vfs_type"] == fstype, f"incorrect filesystem type for {fs['path']}" + assert mount["Type"] == fstype, f"incorrect filesystem type for {mount['Where']}" - # check that all fs customizations appear in fstab + # check that all fs customizations appear in the manifest for custom_mountpoint in customizations: assert custom_mountpoint in manifest_mountpoints @@ -537,8 +602,7 @@ def test_manifest_fs_customizations_xarch(tmp_path, build_container, fscustomiza "manifest", f"{container_ref}", ]) - # cross-arch builds only support ext4 (for now) - assert_fs_customizations(fscustomizations, "ext4", output) + assert_fs_customizations(fscustomizations, rootfs, output) def find_grub2_iso_stage_from(manifest_str): @@ -589,33 +653,23 @@ def test_manifest_disk_customization_lvm(tmp_path, build_container): container_ref = "quay.io/centos-bootc/centos-bootc:stream9" testutil.pull_container(container_ref) - config = { - "customizations": { - "disk": { - "partitions": [ - { - "type": "lvm", - "minsize": "10 GiB", - "logical_volumes": [ - { - "minsize": "10 GiB", - "fs_type": "ext4", - "mountpoint": "/", - } - ] - } - ] - } - } - } - config_path = tmp_path / "config.json" - with config_path.open("w") as config_file: - json.dump(config, config_file) + config = textwrap.dedent("""\ + [[customizations.disk.partitions]] + type = "lvm" + minsize = "10 GiB" + + [[customizations.disk.partitions.logical_volumes]] + minsize = "10 GiB" + fs_type = "ext4" + mountpoint = "/" + """) + config_path = tmp_path / "config.toml" + config_path.write_text(config) testutil.pull_container(container_ref) output = subprocess.check_output([ *testutil.podman_run_common, - "-v", f"{config_path}:/config.json:ro", + "-v", f"{config_path}:/config.toml:ro", build_container, "manifest", f"{container_ref}", ]) @@ -623,6 +677,28 @@ def test_manifest_disk_customization_lvm(tmp_path, build_container): assert st["devices"]["rootlv"]["type"] == "org.osbuild.lvm2.lv" +def test_manifest_disk_customization_dos(tmp_path, build_container): + container_ref = "quay.io/centos-bootc/centos-bootc:stream9" + testutil.pull_container(container_ref) + + config = textwrap.dedent("""\ + [customizations.disk] + type = "dos" + """) + config_path = tmp_path / "config.toml" + config_path.write_text(config) + + testutil.pull_container(container_ref) + output = subprocess.check_output([ + *testutil.podman_run_common, + "-v", f"{config_path}:/config.toml:ro", + build_container, + "manifest", f"{container_ref}", + ]) + st = find_stage_options_from(output, "org.osbuild.sfdisk") + assert st["label"] == "dos" + + def test_manifest_disk_customization_btrfs(tmp_path, build_container): container_ref = "quay.io/centos-bootc/centos-bootc:stream9" @@ -699,14 +775,12 @@ def test_manifest_disk_customization_swap(tmp_path, build_container): mkswap_stage = find_mkswap_stage_from(output) assert mkswap_stage["options"].get("uuid") swap_uuid = mkswap_stage["options"]["uuid"] - fstab_stage = find_fstab_stage_from(output) - filesystems = fstab_stage["options"]["filesystems"] + _, swaps = find_fs_mount_info_from(output) + what_node = f"/dev/disk/by-uuid/{swap_uuid}" assert { - 'uuid': swap_uuid, - "vfs_type": "swap", - "path": "none", - "options": "defaults", - } in filesystems + "What": what_node, + "Options": "defaults", + } in swaps def test_manifest_disk_customization_lvm_swap(tmp_path, build_container): @@ -744,14 +818,12 @@ def test_manifest_disk_customization_lvm_swap(tmp_path, build_container): mkswap_stage = find_mkswap_stage_from(output) assert mkswap_stage["options"].get("uuid") swap_uuid = mkswap_stage["options"]["uuid"] - fstab_stage = find_fstab_stage_from(output) - filesystems = fstab_stage["options"]["filesystems"] + _, swaps = find_fs_mount_info_from(output) + what_node = f"/dev/disk/by-uuid/{swap_uuid}" assert { - 'uuid': swap_uuid, - "vfs_type": "swap", - "path": "none", - "options": "defaults", - } in filesystems + "What": what_node, + "Options": "defaults", + } in swaps # run osbuild schema validation, see gh#748 if not testutil.has_executable("osbuild"): pytest.skip("no osbuild executable") @@ -779,3 +851,268 @@ def test_iso_manifest_use_librepo(build_container, use_librepo): assert "org.osbuild.librepo" in manifest["sources"] else: assert "org.osbuild.curl" in manifest["sources"] + + +def test_manifest_customization_custom_file_smoke(tmp_path, build_container): + # no need to parameterize this test, toml is the same for all containers + container_ref = "quay.io/centos-bootc/centos-bootc:stream9" + testutil.pull_container(container_ref) + + cfg = { + "blueprint": { + "customizations": { + "files": [ + { + "path": "/etc/custom_file", + "data": "hello world" + }, + ], + "directories": [ + { + "path": "/etc/custom_dir", + }, + ], + }, + }, + } + + output_path = tmp_path / "output" + output_path.mkdir(exist_ok=True) + config_json_path = output_path / "config.json" + config_json_path.write_text(json.dumps(cfg), encoding="utf-8") + + output = subprocess.check_output([ + *testutil.podman_run_common, + "-v", f"{output_path}:/output", + build_container, + "manifest", f"{container_ref}", + "--config", "/output/config.json", + ], stderr=subprocess.PIPE, encoding="utf8") + json.loads(output) + assert '"to":"tree:///etc/custom_file"' in output + assert ('{"type":"org.osbuild.mkdir","options":{"paths":' + '[{"path":"/etc/custom_dir","exist_ok":true}]},' + '"devices":{"disk":{"type":"org.osbuild.loopback"' + ',"options":{"filename":"disk.raw"') in output + + +def find_stage_options_from(manifest_str, stage_type): + manifest = json.loads(manifest_str) + for pipl in manifest["pipelines"]: + for st in pipl["stages"]: + if st["type"] == stage_type: + return st["options"] + raise ValueError(f"cannot find {stage_type} stage manifest:\n{manifest_str}") + + +def test_manifest_image_customize_filesystem(tmp_path, build_container): + # no need to parameterize this test, overrides behaves same for all containers + container_ref = "quay.io/centos-bootc/centos-bootc:stream9" + testutil.pull_container(container_ref) + + cfg = { + "blueprint": { + "customizations": { + "filesystem": [ + { + "mountpoint": "/boot", + "minsize": "3GiB" + } + ] + }, + }, + } + + config_json_path = tmp_path / "config.json" + config_json_path.write_text(json.dumps(cfg), encoding="utf-8") + + # create derrived container with filesystem customization + cntf_path = tmp_path / "Containerfile" + cntf_path.write_text(textwrap.dedent(f"""\n + FROM {container_ref} + RUN mkdir -p -m 0755 /usr/lib/bootc-image-builder + COPY config.json /usr/lib/bootc-image-builder/ + """), encoding="utf8") + + print(f"building filesystem customize container from {container_ref}") + with make_container(tmp_path) as container_tag: + print(f"using {container_tag}") + manifest_str = subprocess.check_output([ + *testutil.podman_run_common, + build_container, + "manifest", + f"localhost/{container_tag}", + ], encoding="utf8") + sfdisk_options = find_stage_options_from(manifest_str, "org.osbuild.sfdisk") + assert sfdisk_options["partitions"][2]["size"] == 3 * 1024 * 1024 * 1024 / 512 + + +def test_manifest_image_customize_disk(tmp_path, build_container): + # no need to parameterize this test, overrides behaves same for all containers + container_ref = "quay.io/centos-bootc/centos-bootc:stream9" + testutil.pull_container(container_ref) + + cfg = { + "blueprint": { + "customizations": { + "disk": { + "partitions": [ + { + "label": "var", + "mountpoint": "/var", + "fs_type": "ext4", + "minsize": "3 GiB", + }, + ], + }, + }, + }, + } + + config_json_path = tmp_path / "config.json" + config_json_path.write_text(json.dumps(cfg), encoding="utf-8") + + # create derrived container with disk customization + cntf_path = tmp_path / "Containerfile" + cntf_path.write_text(textwrap.dedent(f"""\n + FROM {container_ref} + RUN mkdir -p -m 0755 /usr/lib/bootc-image-builder + COPY config.json /usr/lib/bootc-image-builder/ + """), encoding="utf8") + + print(f"building filesystem customize container from {container_ref}") + with make_container(tmp_path) as container_tag: + print(f"using {container_tag}") + manifest_str = subprocess.check_output([ + *testutil.podman_run_common, + build_container, + "manifest", + f"localhost/{container_tag}", + ], encoding="utf8") + sfdisk_options = find_stage_options_from(manifest_str, "org.osbuild.sfdisk") + assert sfdisk_options["partitions"][2]["size"] == 3 * 1024 * 1024 * 1024 / 512 + + +def test_manifest_image_disk_yaml(tmp_path, build_container): + # no need to parameterize this test, overrides behaves same for all containers + container_ref = "quay.io/centos-bootc/centos-bootc:stream9" + testutil.pull_container(container_ref) + + disk_yaml = textwrap.dedent("""--- + #enabled once https://github.com/osbuild/images/pull/1834 is in + #mount_configuration: none + partition_table: + size: '8589934592' + partitions: + - bootable: true + size: 1 MiB + type: 21686148-6449-6E6F-744E-656564454649 + uuid: fac7f1fb-3e8d-4137-a512-961de09a5549 + - bootable: false + label: efi + payload: + label: ESP + mountpoint: /boot/efi + type: vfat + payload_type: filesystem + size: '104857600' + type: c12a7328-f81f-11d2-ba4b-00a0c93ec93b + uuid: 68b2905b-df3e-4fb3-80fa-49d1e773aa33 + - label: ukiboot_a + size: '134217728' + type: df331e4d-be00-463f-b4a7-8b43e18fb53a + uuid: CD3B4BE3-0139-4A63-8060-658554C7273B + payload_type: raw + payload: + source_path: /usr/lib/modules/5.0-x86_64/aboot.img + - label: ukiboot_b + size: '134217728' + type: df331e4d-be00-463f-b4a7-8b43e18fb53a + uuid: E4D4DA50-7050-41AE-A5F9-DEF12B94DFB5 + - label: ukibootctl + size: '1048576' + type: fefd9070-346f-4c9a-85e6-17f07f922773 + uuid: 5A6F3ADE-EEB0-11EF-A838-E89C256C3906 + - label: root + payload: + label: root + mountpoint: / + type: ext4 + payload_type: filesystem + type: b921b045-1df0-41c3-af44-4c6f280d3fae + uuid: 6264d520-3fb9-423f-8ab8-7a0a8e3d3562 + """) + + disk_yaml_path = tmp_path / "disk.yaml" + disk_yaml_path.write_text(disk_yaml, encoding="utf-8") + + testdata_path = tmp_path / "fake-aboot.img" + testdata_path.write_text("fake aboot.img content", encoding="utf-8") + + # Create derived container with the custom partitioning with an aboot + # partition and a kernel module dir with an aboot.img file + cntf_path = tmp_path / "Containerfile" + cntf_path.write_text(textwrap.dedent(f"""\n + FROM {container_ref} + RUN mkdir -p -m 0755 /usr/lib/bootc-image-builder + COPY disk.yaml /usr/lib/bootc-image-builder/ + # add a preditable aboot.img for the write-device tes + RUN mkdir -p -m 0755 /usr/lib/modules/5.0-x86_64/ + COPY fake-aboot.img /usr/lib/modules/5.0-x86_64/aboot.img + """), encoding="utf8") + + print(f"building filesystem customize container from {container_ref}") + with make_container(tmp_path) as container_tag: + print(f"using {container_tag}") + manifest_str = subprocess.check_output([ + *testutil.podman_run_common, + build_container, + "manifest", + f"localhost/{container_tag}", + ], encoding="utf8") + write_device_options = find_stage_options_from(manifest_str, "org.osbuild.write-device") + assert write_device_options["from"] == "input://tree/usr/lib/modules/5.0-x86_64/aboot.img" + + +@pytest.mark.parametrize("tc", gen_testcases("anaconda-iso")) +def test_ova_manifest_smoke(build_container, tc): + testutil.pull_container(tc.container_ref, tc.target_arch) + + output = subprocess.check_output([ + *testutil.podman_run_common, + build_container, + "manifest", + *tc.bib_rootfs_args(), + "--type=ova", + f"{tc.container_ref}", + ]) + # just some basic validation that we generate a ova + assert find_stage_options_from(output, "org.osbuild.tar") == { + "filename": "image.ova", + "format": "ustar", + "paths": [ + "image.ovf", + "image.mf", + "image.vmdk" + ] + } + + +def test_manifest_warns_on_unsupported(tmp_path, build_container): + # no need to parameterize this test, toml is the same for all containers + container_ref = "quay.io/centos-bootc/centos-bootc:stream9" + testutil.pull_container(container_ref) + + config_toml_path = tmp_path / "config.toml" + config_toml_path.write_text(textwrap.dedent("""\ + [[customizations.repositories]] + id = "foo" + """)) + res = subprocess.run([ + *testutil.podman_run_common, + "-v", f"{config_toml_path}:/config.toml:ro", + build_container, + "manifest", f"{container_ref}", + ], check=True, capture_output=True, text=True) + assert ('blueprint validation failed for image type "qcow2": ' + 'customizations.repositories: not supported' in res.stderr) diff --git a/test/test_progress.py b/test/test_progress.py index 3b7a7a2b8..559f5026e 100644 --- a/test/test_progress.py +++ b/test/test_progress.py @@ -13,6 +13,9 @@ def test_progress_debug(tmp_path, build_fake_container): + container_ref = "quay.io/centos-bootc/centos-bootc:stream9" + testutil.pull_container(container_ref) + output_path = tmp_path / "output" output_path.mkdir(exist_ok=True) @@ -21,12 +24,12 @@ def test_progress_debug(tmp_path, build_fake_container): build_fake_container, "build", "--progress=debug", - "quay.io/centos-bootc/centos-bootc:stream9", + container_ref, ] res = subprocess.run(cmdline, capture_output=True, check=True, text=True) assert res.stderr.count("Start progressbar") == 1 assert res.stderr.count("Manifest generation step") == 1 - assert res.stderr.count("Image building step") == 1 + assert res.stderr.count("Disk image building step") == 1 assert res.stderr.count("Build complete") == 1 assert res.stderr.count("Stop progressbar") == 1 assert res.stdout.strip() == "" @@ -54,25 +57,6 @@ def test_progress_term_works_without_tty(tmp_path, build_fake_container): assert "[|] Manifest generation step" in res.stderr -def test_progress_term_autoselect(tmp_path, build_fake_container): - output_path = tmp_path / "output" - output_path.mkdir(exist_ok=True) - - cmdline = [ - *testutil.podman_run_common, - # we have a terminal - "-t", - build_fake_container, - "build", - # note that we do not select a --progress here so auto-select is used - "quay.io/centos-bootc/centos-bootc:stream9", - ] - res = subprocess.run(cmdline, capture_output=True, text=True, check=False) - assert res.returncode == 0 - # its curious that we get the output on stdout here, podman weirdness? - assert "[|] Manifest generation step" in res.stdout - - @pytest.mark.skipif(not testutil.can_start_rootful_containers, reason="require a rootful containers (try: sudo)") @pytest.mark.parametrize("progress", ["term", "verbose"]) def test_progress_error_reporting(tmp_path, build_erroring_container, progress): diff --git a/test/testcases.py b/test/testcases.py index 88ed8cd82..d6e9fddf1 100644 --- a/test/testcases.py +++ b/test/testcases.py @@ -14,6 +14,8 @@ class TestCase: # container_ref to the bootc image, e.g. quay.io/fedora/fedora-bootc:40 container_ref: str = "" + # optional build_container_ref to the bootc image, e.g. quay.io/fedora/fedora-bootc:40 + build_container_ref: str = "" # image is the image type, e.g. "ami" image: str = "" # target_arch is the target archicture, empty means current arch @@ -45,14 +47,14 @@ def __str__(self): @dataclasses.dataclass class TestCaseFedora(TestCase): - container_ref: str = "quay.io/fedora/fedora-bootc:40" + container_ref: str = "quay.io/fedora/fedora-bootc:42" rootfs: str = "btrfs" use_librepo: bool = True @dataclasses.dataclass -class TestCaseFedora42(TestCase): - container_ref: str = "quay.io/fedora/fedora-bootc:42" +class TestCaseFedora43(TestCase): + container_ref: str = "quay.io/fedora/fedora-bootc:43" rootfs: str = "btrfs" use_librepo: bool = True @@ -84,7 +86,7 @@ def test_testcase_nameing(): assert f"{tc}" == expected, f"{tc} != {expected}" -def gen_testcases(what): # pylint: disable=too-many-return-statements +def gen_testcases(what): # pylint: disable=too-many-return-statements disable=too-many-branches if what == "manifest": return [TestCaseC9S(), TestCaseFedora(), TestCaseC10S()] if what == "default-rootfs": @@ -94,29 +96,35 @@ def gen_testcases(what): # pylint: disable=too-many-return-statements return [TestCaseC9S(image="ami"), TestCaseFedora(image="ami")] if what == "anaconda-iso": return [ - # 2024-12-19: disabled for now until the mirror situation becomes - # a bit more stable - # TestCaseFedora(image="anaconda-iso", sign=True), + TestCaseFedora(image="anaconda-iso", sign=True), TestCaseC9S(image="anaconda-iso"), TestCaseC10S(image="anaconda-iso"), ] + if what == "pxe-tar-xz": + return [ + TestCaseC9S(image="pxe-tar-xz"), + ] + if what == "qemu-cross": + test_cases = [] + if platform.machine() == "x86_64": + # 2025-09-19: disabled because CI hangs, see + # https://github.com/osbuild/bootc-image-builder/actions/runs/17821609665 + # test_cases.append( + # TestCaseC9S(image="raw", target_arch="arm64")) + pass + elif platform.machine() == "arm64": + # TODO: add arm64->x86_64 cross build test too + pass + return test_cases if what == "qemu-boot": - test_cases = [ + return [ # test default partitioning TestCaseFedora(image="qcow2"), # test with custom disk configs TestCaseC9S(image="qcow2", disk_config="swap"), - TestCaseFedora(image="raw", disk_config="btrfs"), + TestCaseFedora43(image="raw", disk_config="btrfs"), TestCaseC9S(image="raw", disk_config="lvm"), ] - # do a cross arch test too - if platform.machine() == "x86_64": - test_cases.append( - TestCaseC9S(image="raw", target_arch="arm64")) - elif platform.machine() == "arm64": - # TODO: add arm64->x86_64 cross build test too - pass - return test_cases if what == "all": return [ klass(image=img) @@ -135,8 +143,11 @@ def gen_testcases(what): # pylint: disable=too-many-return-statements if what == "target-arch-smoke": return [ TestCaseC9S(target_arch="arm64"), - # TODO: merge with TestCaseFedora once the arches are build there - TestCaseFedora42(target_arch="ppc64le"), - TestCaseFedora42(target_arch="s390x"), + TestCaseFedora(target_arch="ppc64le"), + TestCaseFedora(target_arch="s390x"), + ] + if what == "build-container": + return [ + TestCaseC9S(build_container_ref="quay.io/centos-bootc/centos-bootc:stream10", image="qcow2"), ] raise ValueError(f"unknown test-case type {what}") diff --git a/test/testutil.py b/test/testutil.py index e1700078b..096d8f661 100644 --- a/test/testutil.py +++ b/test/testutil.py @@ -2,15 +2,11 @@ import pathlib import platform import shutil -import socket import subprocess -import time import boto3 from botocore.exceptions import ClientError -AWS_REGION = "us-east-1" - def run_journalctl(*args): pre = [] @@ -35,28 +31,6 @@ def has_executable(name): return shutil.which(name) is not None -def get_free_port() -> int: - # this is racy but there is no race-free way to do better with the qemu CLI - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind(("localhost", 0)) - return s.getsockname()[1] - - -def wait_ssh_ready(address, port, sleep, max_wait_sec): - for _ in range(int(max_wait_sec / sleep)): - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.settimeout(sleep) - try: - s.connect((address, port)) - data = s.recv(256) - if b"OpenSSH" in data: - return - except (ConnectionRefusedError, ConnectionResetError, TimeoutError): - pass - time.sleep(sleep) - raise ConnectionRefusedError(f"cannot connect to port {port} after {max_wait_sec}s") - - def has_x86_64_v3_cpu(): # x86_64-v3 has multiple features, see # https://en.wikipedia.org/wiki/X86-64#Microarchitecture_levels @@ -95,8 +69,8 @@ def write_aws_creds(path): return True -def deregister_ami(ami_id): - ec2 = boto3.resource("ec2", region_name=AWS_REGION) +def deregister_ami(ami_id, aws_region): + ec2 = boto3.resource("ec2", region_name=aws_region) try: print(f"Deregistering image {ami_id}") ami = ec2.Image(ami_id) diff --git a/test/testutil_test.py b/test/testutil_test.py deleted file mode 100644 index a1b2f0d26..000000000 --- a/test/testutil_test.py +++ /dev/null @@ -1,58 +0,0 @@ -import contextlib -import platform -import subprocess -from unittest.mock import call, patch - -import pytest -from testutil import get_free_port, has_executable, wait_ssh_ready - - -def test_get_free_port(): - port_nr = get_free_port() - assert 1024 < port_nr < 65535 - - -@pytest.fixture(name="free_port") -def free_port_fixture(): - return get_free_port() - - -@patch("time.sleep") -def test_wait_ssh_ready_sleeps_no_connection(mocked_sleep, free_port): - with pytest.raises(ConnectionRefusedError): - wait_ssh_ready("localhost", free_port, sleep=0.1, max_wait_sec=0.35) - assert mocked_sleep.call_args_list == [call(0.1), call(0.1), call(0.1)] - - -@pytest.mark.skipif(not has_executable("nc"), reason="needs nc") -def test_wait_ssh_ready_sleeps_wrong_reply(free_port): - with contextlib.ExitStack() as cm: - with subprocess.Popen( - f"echo not-ssh | nc -vv -l -p {free_port}", - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - encoding="utf-8", - ) as p: - cm.callback(p.kill) - # wait for nc to be ready - while True: - # netcat tranditional uses "listening", others "Listening" - # so just omit the first char - if "istening " in p.stdout.readline(): - break - # now connect - with patch("time.sleep") as mocked_sleep: - with pytest.raises(ConnectionRefusedError): - wait_ssh_ready("localhost", free_port, sleep=0.1, max_wait_sec=0.55) - assert mocked_sleep.call_args_list == [ - call(0.1), call(0.1), call(0.1), call(0.1), call(0.1)] - - -@pytest.mark.skipif(platform.system() == "Darwin", reason="hangs on macOS") -@pytest.mark.skipif(not has_executable("nc"), reason="needs nc") -def test_wait_ssh_ready_integration(free_port): - with contextlib.ExitStack() as cm: - with subprocess.Popen(f"echo OpenSSH | nc -l -p {free_port}", shell=True) as p: - cm.callback(p.kill) - wait_ssh_ready("localhost", free_port, sleep=0.1, max_wait_sec=10) diff --git a/test/vm.py b/test/vm.py deleted file mode 100644 index a1be56a52..000000000 --- a/test/vm.py +++ /dev/null @@ -1,310 +0,0 @@ -import abc -import os -import pathlib -import platform -import subprocess -import sys -import time -import uuid -from io import StringIO - -import boto3 -import paramiko -from botocore.exceptions import ClientError -from paramiko.client import AutoAddPolicy, SSHClient -from testutil import AWS_REGION, get_free_port, wait_ssh_ready - - -class VM(abc.ABC): - - def __init__(self): - self._ssh_port = None - self._address = None - - def __del__(self): - self.force_stop() - - @abc.abstractmethod - def start(self): - """ - Start the VM. This method will be called automatically if it is not called explicitly before calling run(). - """ - - def _log(self, msg): - # XXX: use a proper logger - sys.stdout.write(msg.rstrip("\n") + "\n") - - def wait_ssh_ready(self): - wait_ssh_ready(self._address, self._ssh_port, sleep=1, max_wait_sec=600) - - @abc.abstractmethod - def force_stop(self): - """ - Stop the VM and clean up any resources that were created when setting up and starting the machine. - """ - - def run(self, cmd, user, password="", keyfile=None): - """ - Run a command on the VM via SSH using the provided credentials. - """ - if not self.running(): - self.start() - client = SSHClient() - client.set_missing_host_key_policy(AutoAddPolicy) - # workaround, see https://github.com/paramiko/paramiko/issues/2048 - pkey = None - if keyfile: - pkey = paramiko.RSAKey.from_private_key_file(keyfile) - client.connect( - self._address, self._ssh_port, - user, password, pkey=pkey, - allow_agent=False, look_for_keys=False) - chan = client.get_transport().open_session() - chan.get_pty() - chan.exec_command(cmd) - stdout_f = chan.makefile() - output = StringIO() - while True: - out = stdout_f.readline() - if not out: - break - self._log(out) - output.write(out) - exit_status = stdout_f.channel.recv_exit_status() - return exit_status, output.getvalue() - - @abc.abstractmethod - def running(self): - """ - True if the VM is running. - """ - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - self.force_stop() - - -# needed as each distro puts the OVMF.fd in a different location -def find_ovmf(): - for p in [ - "/usr/share/ovmf/OVMF.fd", # Debian - "/usr/share/OVMF/OVMF_CODE.fd", # Fedora - ]: - if os.path.exists(p): - return p - raise ValueError("cannot find a OVMF bios") - - -class QEMU(VM): - MEM = "2000" - - def __init__(self, img, arch="", snapshot=True, cdrom=None): - super().__init__() - self._img = pathlib.Path(img) - self._qmp_socket = self._img.with_suffix(".qemp-socket") - self._qemu_p = None - self._snapshot = snapshot - self._cdrom = cdrom - self._ssh_port = None - if not arch: - arch = platform.machine() - self._arch = arch - - def __del__(self): - self.force_stop() - - def _gen_qemu_cmdline(self, snapshot, use_ovmf): - if self._arch in ("arm64", "aarch64"): - qemu_cmdline = [ - "qemu-system-aarch64", - "-machine", "virt", - "-cpu", "cortex-a57", - "-smp", "2", - "-bios", "/usr/share/AAVMF/AAVMF_CODE.fd", - ] - elif self._arch in ("amd64", "x86_64"): - qemu_cmdline = [ - "qemu-system-x86_64", - "-M", "accel=kvm", - # get "illegal instruction" inside the VM otherwise - "-cpu", "host", - ] - if use_ovmf: - qemu_cmdline.extend(["-bios", find_ovmf()]) - else: - raise ValueError(f"unsupported architecture {self._arch}") - - # common part - qemu_cmdline += [ - "-m", self.MEM, - "-serial", "stdio", - "-monitor", "none", - "-netdev", f"user,id=net.0,hostfwd=tcp::{self._ssh_port}-:22", - "-device", "rtl8139,netdev=net.0", - "-qmp", f"unix:{self._qmp_socket},server,nowait", - ] - if not os.environ.get("OSBUILD_TEST_QEMU_GUI"): - qemu_cmdline.append("-nographic") - if self._cdrom: - qemu_cmdline.extend(["-cdrom", self._cdrom]) - if snapshot: - qemu_cmdline.append("-snapshot") - qemu_cmdline.append(self._img) - return qemu_cmdline - - # XXX: move args to init() so that __enter__ can use them? - def start(self, wait_event="ssh", snapshot=True, use_ovmf=False): - if self.running(): - return - self._ssh_port = get_free_port() - self._address = "localhost" - - # XXX: use systemd-run to ensure cleanup? - # pylint: disable=consider-using-with - self._qemu_p = subprocess.Popen( - self._gen_qemu_cmdline(snapshot, use_ovmf), - stdout=sys.stdout, - stderr=sys.stderr, - ) - # XXX: also check that qemu is working and did not crash - ev = wait_event.split(":") - if ev == ["ssh"]: - self.wait_ssh_ready() - self._log(f"vm ready at port {self._ssh_port}") - elif ev[0] == "qmp": - qmp_event = ev[1] - self.wait_qmp_event(qmp_event) - self._log(f"qmp event {qmp_event}") - else: - raise ValueError(f"unsupported wait_event {wait_event}") - - def _wait_qmp_socket(self, timeout_sec): - for _ in range(timeout_sec): - if os.path.exists(self._qmp_socket): - return True - time.sleep(1) - raise TimeoutError(f"no {self._qmp_socket} after {timeout_sec} seconds") - - def wait_qmp_event(self, qmp_event): - # import lazy to avoid requiring it for all operations - import qmp # pylint: disable=import-outside-toplevel - self._wait_qmp_socket(30) - mon = qmp.QEMUMonitorProtocol(os.fspath(self._qmp_socket)) - mon.connect() - while True: - event = mon.pull_event(wait=True) - self._log(f"DEBUG: got event {event}") - if event["event"] == qmp_event: - return - - def force_stop(self): - if self._qemu_p: - self._qemu_p.kill() - self._qemu_p = None - self._address = None - self._ssh_port = None - - def running(self): - return self._qemu_p is not None - - -class AWS(VM): - - _instance_type = "t3.medium" # set based on architecture when we add arm tests - - def __init__(self, ami_id): - super().__init__() - self._ssh_port = 22 - self._ami_id = ami_id - self._ec2_instance = None - self._ec2_security_group = None - self._ec2_resource = boto3.resource("ec2", region_name=AWS_REGION) - - def start(self): - if self.running(): - return - sec_group_ids = [] - if not self._ec2_security_group: - self._set_ssh_security_group() - sec_group_ids = [self._ec2_security_group.id] - try: - self._log(f"Creating ec2 instance from {self._ami_id}") - instances = self._ec2_resource.create_instances( - ImageId=self._ami_id, - InstanceType=self._instance_type, - SecurityGroupIds=sec_group_ids, - MinCount=1, MaxCount=1 - ) - self._ec2_instance = instances[0] - self._log(f"Waiting for instance {self._ec2_instance.id} to start") - self._ec2_instance.wait_until_running() - self._ec2_instance.reload() # make sure the instance info is up to date - self._address = self._ec2_instance.public_ip_address - self._log(f"Instance is running at {self._address}") - self.wait_ssh_ready() - self._log("SSH is ready") - except ClientError as err: - err_code = err.response["Error"]["Code"] - err_msg = err.response["Error"]["Message"] - self._log(f"Couldn't create instance with image {self._ami_id} and type {self._instance_type}.") - self._log(f"Error {err_code}: {err_msg}") - raise - - def _set_ssh_security_group(self): - group_name = f"bootc-image-builder-test-{str(uuid.uuid4())}" - group_desc = "bootc-image-builder test security group: SSH rule" - try: - self._log(f"Creating security group {group_name}") - self._ec2_security_group = self._ec2_resource.create_security_group(GroupName=group_name, - Description=group_desc) - ip_permissions = [ - { - "IpProtocol": "tcp", - "FromPort": self._ssh_port, - "ToPort": self._ssh_port, - "IpRanges": [{"CidrIp": "0.0.0.0/0"}], - } - ] - self._log(f"Authorizing inbound rule for {group_name} ({self._ec2_security_group})") - self._ec2_security_group.authorize_ingress(IpPermissions=ip_permissions) - self._log("Security group created") - except ClientError as err: - err_code = err.response["Error"]["Code"] - err_msg = err.response["Error"]["Message"] - self._log(f"Couldn't create security group {group_name} or authorize inbound rule.") - self._log(f"Error {err_code}: {err_msg}") - raise - - def force_stop(self): - if self._ec2_instance: - self._log(f"Terminating instance {self._ec2_instance.id}") - try: - self._ec2_instance.terminate() - self._ec2_instance.wait_until_terminated() - self._ec2_instance = None - self._address = None - except ClientError as err: - err_code = err.response["Error"]["Code"] - err_msg = err.response["Error"]["Message"] - self._log(f"Couldn't terminate instance {self._ec2_instance.id}.") - self._log(f"Error {err_code}: {err_msg}") - else: - self._log("No EC2 instance defined. Skipping termination.") - - if self._ec2_security_group: - self._log(f"Deleting security group {self._ec2_security_group.id}") - try: - self._ec2_security_group.delete() - self._ec2_security_group = None - except ClientError as err: - err_code = err.response["Error"]["Code"] - err_msg = err.response["Error"]["Message"] - self._log(f"Couldn't delete security group {self._ec2_security_group.id}.") - self._log(f"Error {err_code}: {err_msg}") - else: - self._log("No security group defined. Skipping deletion.") - - def running(self): - return self._ec2_instance is not None