diff --git a/.github/config/golangci.yaml b/.github/config/golangci.yaml index a2d6efa90f..8352fbc2a3 100644 --- a/.github/config/golangci.yaml +++ b/.github/config/golangci.yaml @@ -91,6 +91,9 @@ linters: # Disabled because of deprecation - execinquery + # Seriously, who wants every comment to end in a period??? + - godot + linters-settings: gci: sections: diff --git a/.github/config/goreleaser.yaml b/.github/config/goreleaser.yaml index 60696bf9f3..5f8e6ebec5 100644 --- a/.github/config/goreleaser.yaml +++ b/.github/config/goreleaser.yaml @@ -2,6 +2,9 @@ # However, latest builds (equivalent to nightlies based on the main branch), uses latest.yml version: 2 +release: + prerelease: auto + before: hooks: - go mod tidy @@ -90,18 +93,6 @@ changelog: - '^docs:' - '^test:' -brews: - - name: ocm - repository: - owner: open-component-model - name: homebrew-tap - token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" - directory: Formula - homepage: "https://ocm.software/" - description: "The OCM CLI makes it easy to create component versions and embed them in build processes." - test: | - system "#{bin}/ocm --version" - nfpms: - id: debian package_name: ocm-cli diff --git a/.github/config/wordlist.txt b/.github/config/wordlist.txt index 7a2e1647a7..81e7bd3d34 100644 --- a/.github/config/wordlist.txt +++ b/.github/config/wordlist.txt @@ -236,6 +236,7 @@ repocpi repositoryimpl repositoryspec resendbuffer +resmgmt resolvers resourcereference routings diff --git a/.github/workflows/blackduck_scan_scheduled.yaml b/.github/workflows/blackduck_scan_scheduled.yaml index 2dbff357dd..9491840c0d 100644 --- a/.github/workflows/blackduck_scan_scheduled.yaml +++ b/.github/workflows/blackduck_scan_scheduled.yaml @@ -23,6 +23,28 @@ jobs: uses: actions/setup-go@v5 with: go-version-file: '${{ github.workspace }}/go.mod' + cache: false + + - name: Get go environment for use with cache + run: | + echo "go_cache=$(go env GOCACHE)" >> $GITHUB_ENV + echo "go_modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV + # This step will only reuse the go mod and build cache from main made during the Build, + # see push_ocm.yaml => "ocm-cli-latest" Job + # This means it never caches by itself and PRs cannot cause cache pollution / thrashing + # This is because we have huge storage requirements for our cache because of the mass of dependencies + - name: Restore / Reuse Cache from central build + id: cache-golang-restore + uses: actions/cache/restore@v4 # Only Restore, not build another cache (too big) + with: + path: | + ${{ env.go_cache }} + ${{ env.go_modcache }} + key: ${{ env.cache_name }}-${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}-${{ hashFiles('**/go.mod') }} + restore-keys: | + ${{ env.cache_name }}-${{ runner.os }}-go- + env: + cache_name: ocm-cli-latest-go-cache # needs to be the same key in the end as in the build step - name: Blackduck Full Scan uses: mercedesbenzio/detect-action@v2 diff --git a/.github/workflows/buildcomponents.yaml b/.github/workflows/buildcomponents.yaml deleted file mode 100644 index d86805b513..0000000000 --- a/.github/workflows/buildcomponents.yaml +++ /dev/null @@ -1,69 +0,0 @@ -name: BuildComponents - -on: - workflow_dispatch: - inputs: - ocm_push: - type: boolean - description: "Push to OCM Repository" - default: false - -jobs: - components: - name: Trigger component build - runs-on: large_runner - permissions: - contents: write - id-token: write - packages: write - repository-projects: read - steps: - - name: Self Hosted Runner Post Job Cleanup Action - uses: TooMuch4U/actions-clean@v2.2 - - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version-file: '${{ github.workspace }}/go.mod' - cache: false - - - name: Get go environment for use with cache - run: | - echo "go_cache=$(go env GOCACHE)" >> $GITHUB_ENV - echo "go_modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV - - name: Set up cache - # https://github.com/actions/setup-go/issues/358 - cache is not working with setup-go for multiple jobs - uses: actions/cache@v4 - with: - path: | - ${{ env.go_cache }} - ${{ env.go_modcache }} - key: ${{ env.cache_name }}-${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}-${{ hashFiles('**/go.mod') }} - restore-keys: | - ${{ env.cache_name }}-${{ runner.os }}-go- - env: - cache_name: buildcomponents-go-cache - - - name: Push OCM Components - if: inputs.ocm_push == true - env: - GITHUBORG: ${{ github.repository_owner }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - make push - - - name: Build OCM Components - if: inputs.ocm_push == false - env: - GITHUBORG: ${{ github.repository_owner }} - run: | - make ctf - - - name: Upload OCM Archive - uses: actions/upload-artifact@v4 - with: - name: ocm.ctf - path: gen/ctf diff --git a/.github/workflows/check_diff_action.yaml b/.github/workflows/check_diff_action.yaml index d3439e427c..30cfeeb60b 100644 --- a/.github/workflows/check_diff_action.yaml +++ b/.github/workflows/check_diff_action.yaml @@ -21,9 +21,14 @@ jobs: run: | echo "go_cache=$(go env GOCACHE)" >> $GITHUB_ENV echo "go_modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV - - name: Set up cache - # https://github.com/actions/setup-go/issues/358 - cache is not working with setup-go for multiple jobs - uses: actions/cache@v4 + + # This step will only reuse the go mod and build cache from main made during the Build, + # see lint_and_test.yaml => "test" Job + # This means it never caches by itself and PRs cannot cause cache pollution / thrashing + # This is because we have huge storage requirements for our cache because of the mass of dependencies + - name: Restore / Reuse Cache from central build + id: cache-golang-restore + uses: actions/cache/restore@v4 # Only Restore, not build another cache (too big) with: path: | ${{ env.go_cache }} @@ -32,7 +37,7 @@ jobs: restore-keys: | ${{ env.cache_name }}-${{ runner.os }}-go- env: - cache_name: diff-check-go-cache + cache_name: run-tests-go-cache # needs to be the same key in the end as in the build step - name: Make generate and deepcopy run: | diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0e1964b0cd..7f05fae76d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -27,7 +27,8 @@ jobs: # - https://gh.io/supported-runners-and-hardware-resources # - https://gh.io/using-larger-runners (GitHub.com only) # Consider using larger runners or machines with greater resources for possible analysis time improvements. - runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + # - ADDENDUM: We moved this to a larger runner for faster analysis + runs-on: large_runner timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} permissions: # required for all workflows @@ -45,7 +46,7 @@ jobs: matrix: include: - language: go - build-mode: autobuild + build-mode: manual # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' # Use `c-cpp` to analyze code written in C, C++ or both # Use 'java-kotlin' to analyze code written in Java, Kotlin or both @@ -61,7 +62,39 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - # Initializes the CodeQL tools for scanning. + + - name: Setup Go + uses: actions/setup-go@v5 + if: matrix.build-mode == 'manual' + with: + go-version-file: '${{ github.workspace }}/go.mod' + cache: false + + - name: Get go environment for use with cache + if: matrix.build-mode == 'manual' + run: | + echo "go_cache=$(go env GOCACHE)" >> $GITHUB_ENV + echo "go_modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV + + # This step will only reuse the go mod and build cache from main made during the Build, + # see push_ocm.yaml => "ocm-cli-latest" Job + # This means it never caches by itself and PRs cannot cause cache pollution / thrashing + # This is because we have huge storage requirements for our cache because of the mass of dependencies + - name: Restore / Reuse Cache from central build + if: matrix.build-mode == 'manual' + id: cache-golang-restore + uses: actions/cache/restore@v4 # Only Restore, not build another cache (too big) + with: + path: | + ${{ env.go_cache }} + ${{ env.go_modcache }} + key: ${{ env.cache_name }}-${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}-${{ hashFiles('**/go.mod') }} + restore-keys: | + ${{ env.cache_name }}-${{ runner.os }}-go- + env: + cache_name: ocm-cli-latest-go-cache # needs to be the same key in the end as in the build step + + # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: @@ -74,20 +107,9 @@ jobs: # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality - # If the analyze step fails for one of the languages you are analyzing with - # "We were unable to automatically build your code", modify the matrix above - # to set the build mode to "manual" for that language. Then modify this step - # to build your code. - # ℹī¸ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - if: matrix.build-mode == 'manual' - run: | - echo 'If you are using a "manual" build mode for one or more of the' \ - 'languages you are analyzing, replace this with the commands to build' \ - 'your code, for example:' - echo ' make bootstrap' - echo ' make release' - exit 1 + - name: Build + if: matrix.build-mode == 'manual' + run: make build -j - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/components.yaml b/.github/workflows/components.yaml index 88958fc9ed..ff7f6b9af4 100644 --- a/.github/workflows/components.yaml +++ b/.github/workflows/components.yaml @@ -1,4 +1,4 @@ -name: component CTFs +name: Components on: pull_request: @@ -11,79 +11,33 @@ permissions: contents: read pull-requests: read -jobs: - build-cli: - name: Build CLI - runs-on: large_runner - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version-file: '${{ github.workspace }}/go.mod' - cache: false - - - name: Get go environment for use with cache - run: | - echo "go_cache=$(go env GOCACHE)" >> $GITHUB_ENV - echo "go_modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV - - name: Set up cache - # https://github.com/actions/setup-go/issues/358 - cache is not working with setup-go for multiple jobs - uses: actions/cache@v4 - with: - path: | - ${{ env.go_cache }} - ${{ env.go_modcache }} - key: ${{ env.cache_name }}-${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}-${{ hashFiles('**/go.mod') }} - restore-keys: | - ${{ env.cache_name }}-${{ runner.os }}-go- - env: - cache_name: cli-go-cache - - - name: CTF - run: | - cd components/ocmcli - PATH=$PATH:$(go env GOPATH)/bin make ctf +env: + CTF_TYPE: directory + components: '["ocmcli", "helminstaller", "helmdemo", "subchartsdemo", "ecrplugin"]' - build-helminstaller: - name: Build HelmInstaller - runs-on: large_runner +jobs: + define-matrix: + runs-on: ubuntu-latest + outputs: + components: ${{ steps.componentMatrix.outputs.matrix }} steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version-file: '${{ github.workspace }}/go.mod' - cache: false - - - name: Get go environment for use with cache + - id: componentMatrix + name: Set Components to be used for Build run: | - echo "go_cache=$(go env GOCACHE)" >> $GITHUB_ENV - echo "go_modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV - - name: Set up cache - # https://github.com/actions/setup-go/issues/358 - cache is not working with setup-go for multiple jobs - uses: actions/cache@v4 - with: - path: | - ${{ env.go_cache }} - ${{ env.go_modcache }} - key: ${{ env.cache_name }}-${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}-${{ hashFiles('**/go.mod') }} - restore-keys: | - ${{ env.cache_name }}-${{ runner.os }}-go- + echo "matrix=$input" >> $GITHUB_OUTPUT env: - cache_name: helminstaller-go-cache - - - name: CTF - run: | - cd components/helminstaller - PATH=$PATH:$(go env GOPATH)/bin make ctf - - build-helmdemo: - name: Build HelmDemo + input: ${{ env.components }} + + build: + name: "Build" + needs: define-matrix + strategy: + matrix: + component: ${{fromJSON(needs.define-matrix.outputs.components)}} runs-on: large_runner steps: + - name: Self Hosted Runner Post Job Cleanup Action + uses: TooMuch4U/actions-clean@v2.2 - name: Checkout uses: actions/checkout@v4 - name: Setup Go @@ -91,14 +45,17 @@ jobs: with: go-version-file: '${{ github.workspace }}/go.mod' cache: false - - name: Get go environment for use with cache run: | echo "go_cache=$(go env GOCACHE)" >> $GITHUB_ENV echo "go_modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV - - name: Set up cache - # https://github.com/actions/setup-go/issues/358 - cache is not working with setup-go for multiple jobs - uses: actions/cache@v4 + # This step will only reuse the go mod and build cache from main made during the Build, + # see push_ocm.yaml => "ocm-cli-latest" Job + # This means it never caches by itself and PRs cannot cause cache pollution / thrashing + # This is because we have huge storage requirements for our cache because of the mass of dependencies + - name: Restore / Reuse Cache from central build + id: cache-golang-restore + uses: actions/cache/restore@v4 # Only Restore, not build another cache (too big) with: path: | ${{ env.go_cache }} @@ -107,80 +64,84 @@ jobs: restore-keys: | ${{ env.cache_name }}-${{ runner.os }}-go- env: - cache_name: helmdemo-go-cache - + cache_name: ocm-cli-latest-go-cache # needs to be the same key in the end as in the build step - name: CTF run: | - cd components/helmdemo - PATH=$PATH:$(go env GOPATH)/bin make ctf - - build-subchartsdemo: - name: Build Helm SubChartsDemo - runs-on: large_runner - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version-file: '${{ github.workspace }}/go.mod' - cache: false - - - name: Get go environment for use with cache - run: | - echo "go_cache=$(go env GOCACHE)" >> $GITHUB_ENV - echo "go_modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV - - name: Set up cache - # https://github.com/actions/setup-go/issues/358 - cache is not working with setup-go for multiple jobs - uses: actions/cache@v4 + cd components/${{ matrix.component }} + PATH=$PATH:$(go env GOPATH)/bin CTF_TYPE=${{ env.CTF_TYPE }} make ctf descriptor describe + - name: Upload CTF + uses: actions/upload-artifact@v4 with: - path: | - ${{ env.go_cache }} - ${{ env.go_modcache }} - key: ${{ env.cache_name }}-${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}-${{ hashFiles('**/go.mod') }} - restore-keys: | - ${{ env.cache_name }}-${{ runner.os }}-go- - env: - cache_name: helm-subchart-go-cache - - - name: CTF - run: | - cd components/subchartsdemo - PATH=$PATH:$(go env GOPATH)/bin make ctf - - build-ecrplugin: - name: Build ECR Plugin + if-no-files-found: error + overwrite: true + retention-days: 1 + name: ctf-component-${{ matrix.component }} + path: gen/${{ matrix.component }}/ctf + + aggregate: + name: "Aggregate" runs-on: large_runner + needs: [build, define-matrix] + env: + components: ${{ join(fromJSON(needs.define-matrix.outputs.components), ' ') }} steps: - name: Self Hosted Runner Post Job Cleanup Action uses: TooMuch4U/actions-clean@v2.2 - - name: Checkout uses: actions/checkout@v4 - - name: Setup Go - uses: actions/setup-go@v5 + - name: Download CTFs + uses: actions/download-artifact@v4 with: - go-version-file: '${{ github.workspace }}/go.mod' - cache: false - - - name: Get go environment for use with cache + pattern: 'ctf-component-*' + path: gen/downloaded-ctfs + - name: Move CTFs into correct directory for aggregation run: | - echo "go_cache=$(go env GOCACHE)" >> $GITHUB_ENV - echo "go_modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV - - name: Set up cache - # https://github.com/actions/setup-go/issues/358 - cache is not working with setup-go for multiple jobs - uses: actions/cache@v4 - with: - path: | - ${{ env.go_cache }} - ${{ env.go_modcache }} - key: ${{ env.cache_name }}-${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}-${{ hashFiles('**/go.mod') }} - restore-keys: | - ${{ env.cache_name }}-${{ runner.os }}-go- - env: - cache_name: ecr-plugin-go-cache - - - name: CTF + IFS=" " read -a COMPONENTS <<< "${{ env.components }}" + for i in "${COMPONENTS[@]}"; do + mkdir -p ${{ github.workspace }}/gen/${i} + mv ${{ github.workspace }}/gen/downloaded-ctfs/ctf-component-${i} ${{ github.workspace }}/gen/${i}/ctf + ls -R ${{ github.workspace }}/gen/${i} + done + - name: Extract OCM Binary from CTF to avoid OCM Inception + id: extract-ocm run: | - cd components/ecrplugin - PATH=$PATH:$(go env GOPATH)/bin make ctf + ocm_binary=$(bash ./hack/get_bare_resource_from_ctf.sh \ + "ocm.software/ocmcli" \ + "" \ + "ocmcli" \ + $(go env GOARCH) \ + $(go env GOOS) \ + "application/octet-stream" \ + ${{ github.workspace }}/gen/ocmcli/ctf) + + new_loc=${{ github.workspace }}/bin/ocm + mkdir -p $(dirname $new_loc) + ln -s $ocm_binary $new_loc + chmod +x $new_loc + echo "OCM binary linked to $new_loc" + echo "binary=$new_loc" >> $GITHUB_OUTPUT + - name: Create aggregated CTF + run: | + for i in ${{ env.components }}; do + echo "transfering component $i..." + ${{ steps.extract-ocm.outputs.binary }} transfer cv \ + --type ${{ env.CTF_TYPE }} -V \ + ${{ github.workspace }}/gen/$i/ctf \ + ${{ github.workspace }}/gen/ctf + done + - name: Upload aggregated CTF + # TODO This is currently permanently disabled, + # until we integrate it with the release build, in which it would be reused + if: false + uses: actions/upload-artifact@v4 + with: + if-no-files-found: error + overwrite: true + retention-days: 60 + name: ctf-aggregated + path: gen/ctf + - name: Delete old CTFs that lead up to aggregation + uses: geekyeggo/delete-artifact@v5 + with: + name: | + ctf-component-* \ No newline at end of file diff --git a/.github/workflows/flake_vendorhash.yaml b/.github/workflows/flake_vendorhash.yaml index 1c8696ebf5..9b296efcd3 100644 --- a/.github/workflows/flake_vendorhash.yaml +++ b/.github/workflows/flake_vendorhash.yaml @@ -25,7 +25,7 @@ jobs: with: token: ${{ steps.generate_token.outputs.token }} - name: Install Nix - uses: DeterminateSystems/nix-installer-action@v14 + uses: DeterminateSystems/nix-installer-action@v16 - name: Update ocm vendor hash run: nix run .#nixpkgs.nix-update -- --flake --version=skip ocm - name: Check diff and create action summary diff --git a/.github/workflows/integration-test.yaml b/.github/workflows/integration-test.yaml new file mode 100644 index 0000000000..384748e96c --- /dev/null +++ b/.github/workflows/integration-test.yaml @@ -0,0 +1,27 @@ +name: Integration Tests + +on: + push: + branches: + - main + pull_request_target: + branches: + - main + workflow_dispatch: + +permissions: + # Necessary to write the branch + # TODO: Remove once https://github.com/open-component-model/ocm-integrationtest/blob/main/.github/workflows/integrationtest.yaml#L41 is not needed anymore + contents: write + +jobs: + test: + name: Run tests + uses: open-component-model/ocm-integrationtest/.github/workflows/integrationtest.yaml@main + permissions: + contents: write + id-token: write + packages: write + secrets: inherit + with: + ref: ${{ github.ref }} \ No newline at end of file diff --git a/.github/workflows/lint_and_test.yaml b/.github/workflows/lint_and_test.yaml index a728855768..20bfddb4a1 100644 --- a/.github/workflows/lint_and_test.yaml +++ b/.github/workflows/lint_and_test.yaml @@ -30,9 +30,17 @@ jobs: run: | echo "go_cache=$(go env GOCACHE)" >> $GITHUB_ENV echo "go_modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV - - name: Set up cache - # https://github.com/actions/setup-go/issues/358 - cache is not working with setup-go for multiple jobs - uses: actions/cache@v4 + + # This step will only reuse the go mod and build cache from main made during the Build, + # see lint_and_test.yaml => "test" Job + # This means it never caches by itself and PRs cannot cause cache pollution / thrashing + # This is because we have huge storage requirements for our cache because of the mass of dependencies + # + # NOTE: This is different from our regular build cache (which contains all archs and is built in a different job) + # This is because it requires caching of test dependencies, which are compiled only for linux-amd64 for test runs in CI. + - name: Restore / Reuse Cache from central build + id: cache-golang-restore + uses: actions/cache/restore@v4 # Only Restore, not build another cache (too big) with: path: | ${{ env.go_cache }} @@ -41,10 +49,27 @@ jobs: restore-keys: | ${{ env.cache_name }}-${{ runner.os }}-go- env: - cache_name: run-tests-go-cache + cache_name: run-tests-go-cache # needs to be the same key in the end as in the build step + - name: Build + run: make build -j - name: Test - run: make build install-requirements test + run: make install-requirements test + + # NOTE: This is different from our regular build cache (which contains all archs and is built in a different job) + # This is because it requires caching of test dependencies, which are compiled only for linux-amd64 for test runs in CI. + - name: Save Cache of Build (only on main) + id: cache-golang-save + if: github.ref == 'refs/heads/main' # Only run on main, never in PR + uses: actions/cache/save@v4 # Only saves cache build-test (linux-amd64) + with: + path: | + ${{ env.go_cache }} + ${{ env.go_modcache }} + key: ${{ env.cache_name }}-${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}-${{ hashFiles('**/go.mod') }} + upload-chunk-size: 256000000 # default of 32MB is not really optimal for our large cache, choose 256MB instead + env: + cache_name: run-tests-go-cache # needs to be the same key in the end as in the build step go-lint: name: Lint Golang @@ -64,9 +89,14 @@ jobs: run: | echo "go_cache=$(go env GOCACHE)" >> $GITHUB_ENV echo "go_modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV - - name: Set up cache - # https://github.com/actions/setup-go/issues/358 - cache is not working with setup-go for multiple jobs - uses: actions/cache@v4 + + # This step will only reuse the go mod and build cache from main made during the Build, + # see push_ocm.yaml => "ocm-cli-latest" Job + # This means it never caches by itself and PRs cannot cause cache pollution / thrashing + # This is because we have huge storage requirements for our cache because of the mass of dependencies + - name: Restore / Reuse Cache from central build + id: cache-golang-restore + uses: actions/cache/restore@v4 # Only Restore, not build another cache (too big) with: path: | ${{ env.go_cache }} @@ -75,7 +105,7 @@ jobs: restore-keys: | ${{ env.cache_name }}-${{ runner.os }}-go- env: - cache_name: golint-go-cache + cache_name: ocm-cli-latest-go-cache # needs to be the same key in the end as in the build step - name: Install goimports run: go install golang.org/x/tools/cmd/goimports@latest diff --git a/.github/workflows/mend_scan.yaml b/.github/workflows/mend_scan.yaml index 2de6a8063c..fc59902887 100644 --- a/.github/workflows/mend_scan.yaml +++ b/.github/workflows/mend_scan.yaml @@ -60,7 +60,7 @@ jobs: cache_name: mend-scan-go-cache - name: 'Setup jq' - uses: dcarbone/install-jq-action@v2.1.0 + uses: dcarbone/install-jq-action@v3.0.1 with: version: '1.7' @@ -198,7 +198,7 @@ jobs: - name: Comment Mend Status on PR if: ${{ github.event_name != 'schedule' && steps.pr_exists.outputs.pr_found == 'true' }} - uses: thollander/actions-comment-pull-request@v3.0.0 + uses: thollander/actions-comment-pull-request@v3.0.1 with: message: | ## Mend Scan Summary: :${{ steps.report.outputs.status }}: diff --git a/.github/workflows/publish-latest.yaml b/.github/workflows/publish-latest.yaml index 7d2ee728b2..9d60f1dd52 100644 --- a/.github/workflows/publish-latest.yaml +++ b/.github/workflows/publish-latest.yaml @@ -108,9 +108,14 @@ jobs: run: | echo "go_cache=$(go env GOCACHE)" >> $GITHUB_ENV echo "go_modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV - - name: Set up cache - # https://github.com/actions/setup-go/issues/358 - cache is not working with setup-go for multiple jobs - uses: actions/cache@v4 + + # This step will only reuse the go mod and build cache from main made during the Build, + # see push_ocm.yaml => "ocm-cli-latest" Job + # This means it never caches by itself and PRs cannot cause cache pollution / thrashing + # This is because we have huge storage requirements for our cache because of the mass of dependencies + - name: Restore / Reuse Cache from central build + id: cache-golang-restore + uses: actions/cache/restore@v4 # Only Restore, not build another cache (too big) with: path: | ${{ env.go_cache }} @@ -119,7 +124,7 @@ jobs: restore-keys: | ${{ env.cache_name }}-${{ runner.os }}-go- env: - cache_name: ocm-cli-latest-go-cache + cache_name: ocm-cli-latest-go-cache # needs to be the same key in the end as in the build step - name: Goreleaser release snapshot uses: goreleaser/goreleaser-action@v6 @@ -149,3 +154,16 @@ jobs: skipIfReleaseExists: false body: | holds always the latest ocm-cli binaries + + # This step is actually responsible for populating our build cache for the next runs in PRs or on main. + - name: Save Cache of Build (only on main) + id: cache-golang-save + uses: actions/cache/save@v4 # Only save build cache once + with: + path: | + ${{ env.go_cache }} + ${{ env.go_modcache }} + key: ${{ env.cache_name }}-${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}-${{ hashFiles('**/go.mod') }} + upload-chunk-size: 256000000 # default of 32MB is not really optimal for our large cache, choose 256MB instead + env: + cache_name: ocm-cli-latest-go-cache # needs to be the same key in the end as in the build step \ No newline at end of file diff --git a/.github/workflows/publish-to-other-than-github.yaml b/.github/workflows/publish-to-other-than-github.yaml index 4e3d951398..012821ff04 100644 --- a/.github/workflows/publish-to-other-than-github.yaml +++ b/.github/workflows/publish-to-other-than-github.yaml @@ -8,34 +8,77 @@ name: Publish Release to other package registries than Github on: - workflow_dispatch: - inputs: - version: - type: string - description: 'Version of the latest release (e.g. v0.42.0)' - required: false - default: '' repository_dispatch: - types: [ocm-cli-release] + types: [publish-ocm-cli] jobs: + push-to-brew-tap: + name: Update Homebrew Tap + if: github.event.client_payload.push-to-brew-tap && github.event.client_payload.version != '' + runs-on: ubuntu-latest + env: + REPO: open-component-model/homebrew-tap + steps: + - name: Ensure proper version + run: echo "RELEASE_VERSION=$(echo ${{ github.event.client_payload.version }} | tr -d ['v'])" >> $GITHUB_ENV + - name: Generate token + id: generate_token + uses: tibdex/github-app-token@v2 + with: + app_id: ${{ secrets.OCMBOT_APP_ID }} + private_key: ${{ secrets.OCMBOT_PRIV_KEY }} + - name: Checkout + uses: actions/checkout@v4 + with: + path: tap + repository: ${{ env.REPO }} + token: ${{ steps.generate_token.outputs.token }} + - name: Get Update Script + uses: actions/checkout@v4 + with: + path: scripts + sparse-checkout: | + hack/brew + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: ${{ github.workspace }}/scripts/hack/brew/go.mod + cache: false + - name: Build Script + working-directory: ${{ github.workspace }}/scripts/hack/brew + run: go build -o script + - name: Update Homebrew Tap + run: | + formula=$(${{ github.workspace }}/scripts/hack/brew/script \ + --version ${{ env.RELEASE_VERSION }} \ + --template ${{ github.workspace }}/scripts/hack/brew/internal/ocm_formula_template.rb.tpl \ + --outputDirectory ${{ github.workspace }}/tap/Formula) + mkdir -p ${{ github.workspace }}/tap/Aliases + cd ${{ github.workspace }}/tap/Aliases + ln -s ../Formula/$(basename $formula) ./ocm + - name: Create Pull Request + uses: peter-evans/create-pull-request@v7 + with: + path: tap + token: ${{ steps.generate_token.outputs.token }} + title: "chore: update OCM CLI to v${{ env.RELEASE_VERSION }}" + commit-message: "[github-actions] update OCM CLI to v${{ env.RELEASE_VERSION }}" + branch: chore/update-ocm-cli/${{ env.RELEASE_VERSION }} + delete-branch: true + sign-commits: true + add-paths: | + Formula/* + Aliases/* + body: | + Update OCM CLI to v${{ env.RELEASE_VERSION }}. push-to-aur: name: Update Arch Linux User Repository + if: github.event.client_payload.push-to-aur && github.event.client_payload.version != '' runs-on: ubuntu-latest steps: - name: Ensure proper version - run: | - if [ -n "${{ github.event.inputs.version }}" ]; then - echo "RELEASE_VERSION=$(echo ${{ github.event.inputs.version }} | tr -d ['v'])" >> $GITHUB_ENV - exit 0 - fi - if [ -n "${{ github.event.client_payload.version }}" ]; then - echo "RELEASE_VERSION=$(echo ${{ github.event.client_payload.version }} | tr -d ['v'])" >> $GITHUB_ENV - exit 0 - fi - echo "Version not provided" - exit 1 + run: echo "RELEASE_VERSION=$(echo ${{ github.event.client_payload.version }} | tr -d ['v'])" >> $GITHUB_ENV - name: Install SSH key uses: shimataro/ssh-key-action@v2 with: @@ -56,24 +99,13 @@ jobs: push-to-chocolatey: name: Update Chocolatey + if: github.event.client_payload.push-to-chocolatey && github.event.client_payload.version != '' runs-on: windows-latest steps: - name: Ensure proper version run: | - $workflow_version = "${{ github.event.inputs.version }}" - $repository_version = "${{ github.event.client_payload.version }}" - if (-not ([string]::IsNullOrEmpty($workflow_version))) { - $workflow_version = "$workflow_version" -replace 'v' - echo "RELEASE_VERSION=$workflow_version" | Out-File $env:GITHUB_ENV - exit 0 - } - if (-not ([string]::IsNullOrEmpty($repository_version))) { - $repository_version = "$repository_version" -replace 'v' - echo "RELEASE_VERSION=($repository_version -replace 'v')" | Out-File $env:GITHUB_ENV - exit 0 - } - Write-Host "Version not provided" - exit 1 + $version = "${{ github.event.client_payload.version }}" -replace 'v' + echo "RELEASE_VERSION=$version" | Out-File $env:GITHUB_ENV - name: Generate token id: generate_token uses: tibdex/github-app-token@v2 @@ -91,24 +123,13 @@ jobs: push-to-winget: name: Update Winget + if: github.event.client_payload.push-to-winget && github.event.client_payload.version != '' runs-on: windows-latest steps: - name: Ensure proper version run: | - $workflow_version = "${{ github.event.inputs.version }}" - $repository_version = "${{ github.event.client_payload.version }}" - if (-not ([string]::IsNullOrEmpty($workflow_version))) { - $workflow_version = "$workflow_version" -replace 'v' - echo "RELEASE_VERSION=$workflow_version" | Out-File $env:GITHUB_ENV - exit 0 - } - if (-not ([string]::IsNullOrEmpty($repository_version))) { - $repository_version = "$repository_version" -replace 'v' - echo "RELEASE_VERSION=$repository_version" | Out-File $env:GITHUB_ENV - exit 0 - } - Write-Host "Version not provided" - exit 1 + $version = "${{ github.event.client_payload.version }}" -replace 'v' + echo "RELEASE_VERSION=$version" | Out-File $env:GITHUB_ENV - name: Generate token id: generate_token uses: tibdex/github-app-token@v2 diff --git a/.github/workflows/release-branch.yaml b/.github/workflows/release-branch.yaml index 42a666e321..f63d279c5f 100644 --- a/.github/workflows/release-branch.yaml +++ b/.github/workflows/release-branch.yaml @@ -1,75 +1,102 @@ - -name: Release Branch Creation +# This creates a new release branch from the main branch. +# It serves as the cutoff point for the next minor release. +# From this point onward only bug fixes and critical changes will be accepted onto the release +# branch as backports from main. At the same time, the main branch will be open for new features +# and changes for the next minor release. +name: Release Branch Cutoff on: workflow_dispatch: - inputs: - tag: - type: string - description: "Tag name (if other than execution base)" - required: false - default: "" + +permissions: + # Necessary to write the branch + contents: write jobs: - check-and-create: + cutoff-preconditions: runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + repository-projects: read + outputs: + minor: ${{ steps.get-minor.outputs.minor }} + branch: ${{ steps.verify-branch.outputs.branch }} + steps: + - name: Generate token + id: generate_token + uses: tibdex/github-app-token@v2 + with: + app_id: ${{ secrets.OCMBOT_APP_ID }} + private_key: ${{ secrets.OCMBOT_PRIV_KEY }} + - name: Checkout + uses: actions/checkout@v4 + with: + ref: main + fetch-depth: 0 + token: ${{ steps.generate_token.outputs.token }} + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: '${{ github.workspace }}/go.mod' + cache: false + - name: Get Minor + id: get-minor + run: | + set -e + minor="$(go run ./api/version/generate print-major-minor)" + echo "minor=$minor" >> $GITHUB_OUTPUT + echo "Current Major-Minor Version: $minor" + - name: Verify Branch does not exist + id: verify-branch + run: | + set -e + minor="v${{ steps.get-minor.outputs.minor }}" + branch="releases/$minor" + if git ls-remote --exit-code origin refs/heads/$branch ; then + >&2 echo "branch $branch already exists, aborting" + exit 1 + fi + echo "branch $branch does not exist" + echo "branch=$branch" >> $GITHUB_OUTPUT + + create-branch: + runs-on: ubuntu-latest + needs: cutoff-preconditions permissions: contents: write id-token: write repository-projects: read steps: - - name: Generate token - id: generate_token - uses: tibdex/github-app-token@v2 - with: - app_id: ${{ secrets.OCMBOT_APP_ID }} - private_key: ${{ secrets.OCMBOT_PRIV_KEY }} - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ steps.generate_token.outputs.token }} + - name: Generate token + id: generate_token + uses: tibdex/github-app-token@v2 + with: + app_id: ${{ secrets.OCMBOT_APP_ID }} + private_key: ${{ secrets.OCMBOT_PRIV_KEY }} + - name: Checkout + uses: actions/checkout@v4 + with: + ref: main + fetch-depth: 0 + token: ${{ steps.generate_token.outputs.token }} - - name: Create Release Branch - run: | - set -e - git config --global user.name github-actions - git config --global user.email '${GITHUB_ACTOR}@users.noreply.github.com' + - name: Create Release Branch + run: | + set -e + branch=${{ needs.cutoff-preconditions.outputs.branch }} + git checkout -b "$branch" + git push origin $branch - tag="${{github.event.inputs.tag}}" - if [ -n "$tag" ]; then - if ! git ls-remote --tags --exit-code origin "$tag" >/dev/null; then - >&2 echo "tag $tag not found" - exit 1 - fi - git fetch origin "$tag" - git checkout "$tag" - else - if [ "${{ github.ref_type }}" != "tag" ]; then - >&2 echo "please run workflow on desired tag to create a release branch for or specify a tag as input" - exit 1 - fi - - tag="${{ github.ref_name }}" - fi - - if ! [[ "$tag" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - >&2 echo "no valid non-pre-release tag $tag" - exit 1 - fi - if [ "$tag" == "${tag%.0}" ]; then - >&2 echo "please use a non-patch tag" - exit 1 - fi - if git ls-remote --exit-code origin refs/heads/releases/$tag ; then - >&2 echo "branch releases/$tag already exists" - exit 1 - fi - echo "creating release branch for $tag" - n="releases/$tag" - git checkout -b "$n" - v="$(go run ./api/version/generate bump-patch)" - echo "$v" > VERSION - git add VERSION - git commit -m "Prepare Development of v$v" - git push origin "$n" + # Make sure main contains the next minor after cutoff + bump-main-pr: + uses: ./.github/workflows/release-bump-version.yaml + needs: create-branch + permissions: + contents: write + id-token: write + packages: write + secrets: inherit + with: + bump-type: minor + ref: ${{ github.ref }} \ No newline at end of file diff --git a/.github/workflows/release-bump-version.yaml b/.github/workflows/release-bump-version.yaml new file mode 100644 index 0000000000..1286cda04f --- /dev/null +++ b/.github/workflows/release-bump-version.yaml @@ -0,0 +1,85 @@ +name: Bump VERSION + +on: + workflow_call: + inputs: + ref: + description: "The branch to bump, use the branch the workflow is called on by default" + required: true + default: "" + type: string + bump-type: + description: "The type of bump to perform, one of 'minor' or 'patch'" + required: true + default: "patch" + type: string + +jobs: + create-bump-pr: + name: "Pull Request" + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + packages: write + env: + REF: ${{ inputs.ref == '' && github.ref || inputs.ref }} + steps: + - name: Validate Input + run: | + set -e + if [[ ${{ inputs.bump-type }} != "minor" && ${{ inputs.bump-type }} != "patch" ]]; then + >&2 echo "Invalid bump type: ${{ inputs.bump-type }}" + exit 1 + fi + - name: Generate token + id: generate_token + uses: tibdex/github-app-token@v2 + with: + app_id: ${{ secrets.OCMBOT_APP_ID }} + private_key: ${{ secrets.OCMBOT_PRIV_KEY }} + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ env.REF }} + sparse-checkout: | + api/version + VERSION + go.mod + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: '${{ github.workspace }}/go.mod' + cache: 'false' + - name: Version Bump + id: version-bump + run: | + set -e + + echo "determining next version" + version=$(go run ./api/version/generate bump-${{ inputs.bump-type }}) + + echo "bumping main branch to $version" + echo $version > VERSION + + echo "version=$version" >> $GITHUB_OUTPUT + echo "version after bump: $version" + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ steps.generate_token.outputs.token }} + title: "chore: bump VERSION to ${{ steps.version-bump.outputs.version }}" + commit-message: "[github-actions] Bump to ${{ steps.version-bump.outputs.version }}" + branch: "chore/bump-${{ inputs.bump-type }}/v${{ steps.version-bump.outputs.version }}" + delete-branch: true + sign-commits: true + add-paths: | + VERSION + body: | + Update OCM Version to ${{ steps.version-bump.outputs.version }} + + This makes sure that the branch contains the next valid version. + + ${{ inputs.bump-type == 'minor' && 'This is a minor bump, the next release will be a new minor version and signals opening of the development branch for new features.' || '' }} + ${{ inputs.bump-type == 'patch' && 'This is a patch bump, intended to allow creation of the next patch release without manually incrementing the VERSION.' || '' }} \ No newline at end of file diff --git a/.github/workflows/release-drafter.yaml b/.github/workflows/release-drafter.yaml index efa4b7c86c..1901afcea0 100644 --- a/.github/workflows/release-drafter.yaml +++ b/.github/workflows/release-drafter.yaml @@ -3,7 +3,6 @@ name: Release Drafter on: push: branches: - - main - releases/* permissions: @@ -11,43 +10,26 @@ permissions: # The release-drafter action adds PR titles to the release notes once these are merged to main. # A draft release is kept up-to-date listing the changes for the next minor release version. jobs: + release-version: + name: Release Version + uses: ./.github/workflows/release-version.yaml + with: + # the draft release notes do not need to be done by release candidate + # instead we can continously maintain them throughout the candidates + release_candidate: false + permissions: + contents: read + repository-projects: read update_release_draft: + needs: release-version permissions: contents: write runs-on: ubuntu-latest + env: + RELEASE_VERSION: ${{ needs.release-version.outputs.version }} steps: - name: Checkout uses: actions/checkout@v4 - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version-file: '${{ github.workspace }}/go.mod' - cache: false - - - name: Get go environment for use with cache - run: | - echo "go_cache=$(go env GOCACHE)" >> $GITHUB_ENV - echo "go_modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV - - name: Set up cache - # https://github.com/actions/setup-go/issues/358 - cache is not working with setup-go for multiple jobs - uses: actions/cache@v4 - with: - path: | - ${{ env.go_cache }} - ${{ env.go_modcache }} - key: ${{ env.cache_name }}-${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}-${{ hashFiles('**/go.mod') }} - restore-keys: | - ${{ env.cache_name }}-${{ runner.os }}-go- - env: - cache_name: release-draft-go-cache - - - name: Set Version - run: | - RELEASE_VERSION=v$(go run $GITHUB_WORKSPACE/api/version/generate print-version) - echo "release version is $RELEASE_VERSION" - echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_ENV - - name: Drafter uses: release-drafter/release-drafter@v6 env: diff --git a/.github/workflows/release-version.yaml b/.github/workflows/release-version.yaml new file mode 100644 index 0000000000..4bf084bece --- /dev/null +++ b/.github/workflows/release-version.yaml @@ -0,0 +1,71 @@ +# This workflow can be used to resolve the combination of the inputs candidate and candidate name +# to a release version. The release version is then used in the subsequent steps of the release workflow. +# The release version base is fetched from the VERSION file in the repository root. +name: Derive Release Version from VERSION file + +on: + workflow_call: + inputs: + release_candidate: + type: boolean + description: "Release Candidate" + required: false + default: true + release_candidate_name: + type: string + description: "Release Candidate Name, adjust after every succinct release candidate (e.g. to rc.2, rc.3...)" + required: false + default: "rc.1" + outputs: + version: + description: "The release version to use, e.g. v0.18.0" + value: ${{ jobs.get-release-version.outputs.release-version }} + version_no_prefix: + description: "The release version to use without the 'v' prefix, e.g. v0.18.0 => 0.18.0" + value: ${{ jobs.get-release-version.outputs.release-version-no-prefix }} + version_no_suffix: + description: "The base version to use, without any suffix, e.g. v0.18.0-rc.1 => v0.18.0" + value: ${{ jobs.get-release-version.outputs.base-version }} + +jobs: + get-release-version: + name: Get Release Version + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + base-version: ${{ steps.set-base-version.outputs.BASE_VERSION }} + release-version: ${{ steps.export-version.outputs.RELEASE_VERSION }} + release-version-no-prefix: ${{ steps.export-version.outputs.RELEASE_VERSION_NO_PREFIX }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: '${{ github.workspace }}/go.mod' + cache: false + + - name: Generate Base Version + id: set-base-version + run: | + BASE_VERSION=v$(go run $GITHUB_WORKSPACE/api/version/generate print-version) + echo "BASE_VERSION=$BASE_VERSION" >> $GITHUB_ENV + echo "BASE_VERSION=$BASE_VERSION" >> $GITHUB_OUTPUT + + - name: Set Version for Release Candidate + if: inputs.release_candidate == true + run: | + RELEASE_VERSION=v$(go run $GITHUB_WORKSPACE/api/version/generate --no-dev print-rc-version ${{ inputs.release_candidate_name }}) + echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_ENV + - name: Set Version + if: inputs.release_candidate == false + run: | + RELEASE_VERSION=${{env.BASE_VERSION}} + echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_ENV + + - name: Export Version + id: export-version + run: | + echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_OUTPUT + echo "RELEASE_VERSION_NO_PREFIX=${RELEASE_VERSION#v}" >> $GITHUB_OUTPUT \ No newline at end of file diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b1a60d85f5..5abef69138 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -8,68 +8,40 @@ on: description: "Release Candidate" required: true default: true - create_branch: - type: boolean - description: "Create Release Branch (on failure or if already existing, set to false to ensure a successful run)" - required: true - default: false - prerelease: + release_candidate_name: type: string description: "Release Candidate Name, adjust after every succinct release candidate (e.g. to rc.2, rc.3...)" required: true default: "rc.1" jobs: + release-version: + name: Release Version + uses: ./.github/workflows/release-version.yaml + with: + release_candidate: ${{ inputs.release_candidate }} + release_candidate_name: ${{ inputs.release_candidate_name }} + permissions: + contents: read + repository-projects: read check: name: Check Release Preconditions - runs-on: large_runner + runs-on: ubuntu-latest permissions: - contents: write - id-token: write + contents: read repository-projects: read + needs: release-version + env: + RELEASE_VERSION: ${{ needs.release-version.outputs.version }} + RELEASE_VERSION_NO_SUFFIX: ${{ needs.release-version.outputs.version_no_suffix }} + REF: ${{ github.ref }} + outputs: + draft-release-notes: ${{ steps.release-notes.outputs.json }} steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - - - name: Job Settings - run: | - echo "Release Job Arguments" - if ${{ github.event.inputs.release_candidate }}; then - v="v$(go run $GITHUB_WORKSPACE/api/version/generate --no-dev print-rc-version ${{ github.event.inputs.prerelease }})" - if [ -n "${{ github.event.inputs.prerelease }}" ]; then - echo "Candidate: $v" - else - echo "Candidate: $v (taken from source)" - fi - else - v="v$(go run $GITHUB_WORKSPACE/api/version/generate print-version)" - echo "Final Release: $v" - if ${{ github.event.inputs.create_branch }}; then - echo "with release branch creation" - else - echo "without release branch creation" - fi - fi - - - name: Set Base Version - run: | - BASE_VERSION=v$(go run $GITHUB_WORKSPACE/api/version/generate print-version) - echo "BASE_VERSION=$BASE_VERSION" >> $GITHUB_ENV - - - name: Set Pre-Release Version - if: inputs.release_candidate == true - run: | - RELEASE_VERSION=v$(go run $GITHUB_WORKSPACE/api/version/generate --no-dev print-rc-version ${{ github.event.inputs.prerelease }}) - echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_ENV - - - name: Set Version - if: inputs.release_candidate == false - run: | - RELEASE_VERSION=${{env.BASE_VERSION}} - echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_ENV - - name: Check Tag run: | set -e @@ -77,32 +49,35 @@ jobs: >&2 echo "tag ${{ env.RELEASE_VERSION }} already exists" exit 1 fi - - - name: Check Branch - if: inputs.release_candidate == false && inputs.create_branch && github.ref == 'refs/heads/main' + - name: Check if release is running on release branch run: | - set -e - if git ls-remote --exit-code origin refs/heads/releases/${{ env.RELEASE_VERSION }} ; then - >&2 echo "branch releases/${{ env.RELEASE_VERSION }} already exists" - exit 1 + if [[ ${{ env.REF }} != *"releases/"* ]]; then + echo "The branch ${{ env.REF }} is not a valid release branch and cannot be used for a release" + exit 1 fi - - - name: Get Draft Release Notes + echo "Branch ${{ env.REF }} is a valid release branch" + - name: Generate token + id: generate_token + uses: tibdex/github-app-token@v2 + with: + app_id: ${{ secrets.OCMBOT_APP_ID }} + private_key: ${{ secrets.OCMBOT_PRIV_KEY }} + - name: Ensure existing Draft Release Notes exist id: release-notes - uses: cardinalby/git-get-release-action@v1 + shell: bash env: - GITHUB_TOKEN: ${{ github.token }} - with: - draft: true - releaseName: ${{ env.BASE_VERSION }} - - lint-and-test: - name: Lint and Unit Tests - uses: ./.github/workflows/lint_and_test.yaml - needs: check - permissions: - contents: read - pull-requests: read + GH_TOKEN: ${{ steps.generate_token.outputs.token }} + run: | + RELEASE_JSON=$( \ + gh api /repos/${{ github.repository }}/releases \ + -q '.[] | select(.name == "${{ env.RELEASE_VERSION_NO_SUFFIX }}" and .draft == true)' \ + ) + echo "json=${RELEASE_JSON}" >> $GITHUB_OUTPUT + # if no draft release notes are found, we cannot continue + if [ -z "${RELEASE_JSON}" ]; then + echo "No draft release notes found for ${{ env.RELEASE_VERSION_NO_SUFFIX }}" + exit 1 + fi components: name: Component CTF Builds uses: ./.github/workflows/components.yaml @@ -110,24 +85,23 @@ jobs: permissions: contents: read pull-requests: read - diff-check-manifests: - name: Check for diff after go mod tidy and generated targets - uses: ./.github/workflows/check_diff_action.yaml - needs: check - permissions: - contents: read - pull-requests: read + release: -# needs: -# - lint-and-test -# - components + needs: + # run check before actual release to make sure we succeed + # they will be skipped from the needs check + - check + - release-version name: Release Build runs-on: large_runner permissions: contents: write id-token: write packages: write + env: + RELEASE_VERSION: ${{ needs.release-version.outputs.version }} + RELEASE_NOTES: ${{ fromJSON(needs.check.outputs.draft-release-notes).body }} steps: - name: Self Hosted Runner Post Job Cleanup Action uses: TooMuch4U/actions-clean@v2.2 @@ -140,35 +114,12 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: + # fetch all history so we can calculate the version and tagging fetch-depth: 0 token: ${{ steps.generate_token.outputs.token }} - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version-file: '${{ github.workspace }}/go.mod' - check-latest: false - cache: false - - - name: Get go environment for use with cache - run: | - echo "go_cache=$(go env GOCACHE)" >> $GITHUB_ENV - echo "go_modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV - - name: Set up cache - # https://github.com/actions/setup-go/issues/358 - cache is not working with setup-go for multiple jobs - uses: actions/cache@v4 - with: - path: | - ${{ env.go_cache }} - ${{ env.go_modcache }} - key: ${{ env.cache_name }}-${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}-${{ hashFiles('**/go.mod') }} - restore-keys: | - ${{ env.cache_name }}-${{ runner.os }}-go- - env: - cache_name: release-go-cache - - name: Setup Syft - uses: anchore/sbom-action/download-syft@8d0a6505bf28ced3e85154d13dc6af83299e13f1 # v0.17.4 + uses: anchore/sbom-action/download-syft@fc46e51fd3cb168ffb36c6d1915723c47db58abb # v0.17.7 - name: Setup Cosign uses: sigstore/cosign-installer@v3.7.0 @@ -178,37 +129,8 @@ jobs: git config user.name "GitHub Actions Bot" git config user.email "<41898282+github-actions[bot]@users.noreply.github.com>" - - name: Set Base Version - run: | - BASE_VERSION=v$(go run $GITHUB_WORKSPACE/api/version/generate print-version) - echo "BASE_VERSION=$BASE_VERSION" >> $GITHUB_ENV - - - name: Set Pre-Release Version - if: inputs.release_candidate == true - run: | - RELEASE_VERSION=v$(go run $GITHUB_WORKSPACE/api/version/generate --no-dev print-rc-version ${{ github.event.inputs.prerelease }}) - echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_ENV - echo "release name is $RELEASE_VERSION" - - - name: Set Version - if: inputs.release_candidate == false - run: | - RELEASE_VERSION=${{env.BASE_VERSION}} - echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_ENV - echo "release name is $RELEASE_VERSION" - - - name: Get Draft Release Notes - id: release-notes - uses: cardinalby/git-get-release-action@v1 - env: - GITHUB_TOKEN: ${{ github.token }} - with: - draft: true - releaseName: ${{ env.BASE_VERSION }} - - name: Update Release Notes File env: - RELEASE_NOTES: ${{ steps.release-notes.outputs.body }} GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} run: | if git ls-remote --exit-code origin refs/tags/${{ env.RELEASE_VERSION }}; then @@ -220,13 +142,13 @@ jobs: if [ ! -f "$f" ]; then echo "# Release ${{ env.RELEASE_VERSION }}" > "$f" echo "$RELEASE_NOTES" | tail -n +2 >> "$f" - echo "RELEASE_NOTES_FILE=$f" >> $GITHUB_ENV git add "$f" git commit -m "ReleaseNotes for $RELEASE_VERSION" git push origin ${GITHUB_REF#refs/heads/} else echo "Using release notes file $f from code base" fi + echo "RELEASE_NOTES_FILE=$f" >> $GITHUB_ENV - name: Create and Push Release env: @@ -254,41 +176,19 @@ jobs: env: GITHUBORG: ${{ github.repository_owner }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - HOMEBREW_TAP_GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} GORELEASER_CURRENT_TAG: ${{ env.RELEASE_VERSION }} NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + - name: Remove GPG Token file + run: | + rm ocm-releases-key.gpg + - name: Push OCM Components env: GITHUBORG: ${{ github.repository_owner }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: make plain-push - - name: Create Release Branch - if: inputs.release_candidate == false && inputs.create_branch && github.ref == 'refs/heads/main' - run: | - n="releases/${{env.RELEASE_VERSION}}" - git checkout -b "$n" - v="$(go run ./api/version/generate bump-patch)" - echo "$v" > VERSION - git add VERSION - git commit -m "Prepare Development of v$v" - git push origin "$n" - - - name: Bump Version File - if: inputs.release_candidate == false - run: | - set -e - git checkout ${GITHUB_REF#refs/heads/} - v="$(go run ./api/version/generate bump-version)" - echo "$v" > VERSION - # Trigger a bump of any potential files that depend on a new version - make generate - git add --all - git commit -m "Update version to $v" - git push origin ${GITHUB_REF#refs/heads/} - echo "Next branch version is $v" - - name: Publish Release Event if: inputs.release_candidate == false uses: peter-evans/repository-dispatch@v3 @@ -305,6 +205,20 @@ jobs: uses: peter-evans/repository-dispatch@v3 with: token: ${{ steps.generate_token.outputs.token }} - repository: open-component-model/ocm - event-type: ocm-cli-release - client-payload: '{"version": "${{ env.RELEASE_VERSION }}"}' \ No newline at end of file + repository: ${{ github.repository_owner }}/ocm + event-type: publish-ocm-cli + client-payload: '{"version":"${{ env.RELEASE_VERSION }}","push-to-aur":true,"push-to-chocolatey":true,"push-to-winget":true}' + + # make sure that the branch contains the next valid patch + bump-release-branch-pr: + if: inputs.release_candidate == false + uses: ./.github/workflows/release-bump-version.yaml + needs: release + permissions: + contents: write + id-token: write + packages: write + secrets: inherit + with: + bump-type: patch + ref: ${{ github.ref }} \ No newline at end of file diff --git a/.github/workflows/releasenotes.yaml b/.github/workflows/releasenotes.yaml index 9988dd047c..bec485b73f 100644 --- a/.github/workflows/releasenotes.yaml +++ b/.github/workflows/releasenotes.yaml @@ -27,9 +27,14 @@ jobs: run: | echo "go_cache=$(go env GOCACHE)" >> $GITHUB_ENV echo "go_modcache=$(go env GOMODCACHE)" >> $GITHUB_ENV - - name: Set up cache - # https://github.com/actions/setup-go/issues/358 - cache is not working with setup-go for multiple jobs - uses: actions/cache@v4 + + # This step will only reuse the go mod and build cache from main made during the Build, + # see push_ocm.yaml => "ocm-cli-latest" Job + # This means it never caches by itself and PRs cannot cause cache pollution / thrashing + # This is because we have huge storage requirements for our cache because of the mass of dependencies + - name: Restore / Reuse Cache from central build + id: cache-golang-restore + uses: actions/cache/restore@v4 # Only Restore, not build another cache (too big) with: path: | ${{ env.go_cache }} @@ -38,7 +43,7 @@ jobs: restore-keys: | ${{ env.cache_name }}-${{ runner.os }}-go- env: - cache_name: releasenotes-go-cache + cache_name: ocm-cli-latest-go-cache # needs to be the same key in the end as in the build step - name: Setup git config run: | diff --git a/.github/workflows/retrigger-publish-to-other.yaml b/.github/workflows/retrigger-publish-to-other.yaml new file mode 100644 index 0000000000..57219180d2 --- /dev/null +++ b/.github/workflows/retrigger-publish-to-other.yaml @@ -0,0 +1,65 @@ +name: Manually retrigger the publishing of ocm-cli to other repositories + +on: + workflow_dispatch: + inputs: + version: + type: string + description: Which version (e.g. v0.42.0) do you want to publish? + required: true + default: '' + push-to-aur: + type: boolean + description: Do you want to push to the Arch Linux User Repository? + required: false + default: false + push-to-chocolatey: + type: boolean + description: Do you want to push to Chocolatey? + required: false + default: false + push-to-winget: + type: boolean + description: Do you want to push to Winget? + required: false + default: false + push-to-brew-tap: + type: boolean + description: Do you want to push to the Homebrew Tap at https://github.com/open-component-model/homebrew-tap? + required: false + default: false + +jobs: + retrigger: + name: Create new "Release Publish Event" + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + packages: write + steps: + - name: Generate token + id: generate_token + uses: tibdex/github-app-token@v2 + with: + app_id: ${{ secrets.OCMBOT_APP_ID }} + private_key: ${{ secrets.OCMBOT_PRIV_KEY }} + - name: Ensure proper version + run: | + curl -sSL -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${{ steps.generate_token.outputs.token }}" -H "X-GitHub-Api-Version: 2022-11-28" https://api.github.com/repos/open-component-model/ocm/releases > releases.json + jq -r '.[] | .tag_name' releases.json | grep -v -E '.*-rc|latest' > versions.txt + if grep -Fxq '${{ github.event.inputs.version }}' versions.txt; then + echo "Version (${{ github.event.inputs.version }}) found!" + else + echo "Version (${{ github.event.inputs.version }}) not found! This are the availble ones:" + cat versions.txt + exit 1 + fi + echo "RELEASE_VERSION=$(echo ${{ github.event.inputs.version }} )" >> $GITHUB_ENV + - name: Publish Event + uses: peter-evans/repository-dispatch@v3 + with: + token: ${{ steps.generate_token.outputs.token }} + repository: ${{ github.repository_owner }}/ocm + event-type: publish-ocm-cli + client-payload: '{"version":"${{ env.RELEASE_VERSION }}","push-to-aur":${{ github.event.inputs.push-to-aur }},"push-to-chocolatey":${{ github.event.inputs.push-to-chocolatey }},"push-to-winget":${{ github.event.inputs.push-to-winget }},"push-to-brew-tap":${{ github.event.inputs.push-to-brew-tap }}}' diff --git a/.github/workflows/reuse_helper_tool.yaml b/.github/workflows/reuse_helper_tool.yaml index ffd49ec236..ad08e35803 100644 --- a/.github/workflows/reuse_helper_tool.yaml +++ b/.github/workflows/reuse_helper_tool.yaml @@ -8,4 +8,4 @@ jobs: steps: - uses: actions/checkout@v4 - name: REUSE Compliance Check - uses: fsfe/reuse-action@v4 \ No newline at end of file + uses: fsfe/reuse-action@v5 \ No newline at end of file diff --git a/Makefile b/Makefile index c9c129d764..405434ff37 100644 --- a/Makefile +++ b/Makefile @@ -13,8 +13,8 @@ CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen PLATFORMS = windows/amd64 darwin/arm64 darwin/amd64 linux/amd64 linux/arm64 CREDS ?= -OCM := go run $(REPO_ROOT)/cmds/ocm $(CREDS) -CTF_TYPE ?= tgz +OCM := bin/ocm $(CREDS) +CTF_TYPE ?= directory GEN := $(REPO_ROOT)/gen @@ -27,26 +27,44 @@ BUILD_FLAGS := "-s -w \ -X ocm.software/ocm/api/version.gitTreeState=$(GIT_TREE_STATE) \ -X ocm.software/ocm/api/version.gitCommit=$(COMMIT) \ -X ocm.software/ocm/api/version.buildDate=$(NOW)" +CGO_ENABLED := 0 COMPONENTS ?= ocmcli helminstaller demoplugin ecrplugin helmdemo subchartsdemo -.PHONY: build -build: ${SOURCES} +.PHONY: build bin +build: bin bin/ocm bin/helminstaller bin/demo bin/cliplugin bin/ecrplugin + +bin: mkdir -p bin + +bin/ocm: bin $(SOURCES) + CGO_ENABLED=$(CGO_ENABLED) go build -ldflags $(BUILD_FLAGS) -o bin/ocm ./cmds/ocm + +bin/helminstaller: bin $(SOURCES) + CGO_ENABLED=$(CGO_ENABLED) go build -ldflags $(BUILD_FLAGS) -o bin/helminstaller ./cmds/helminstaller + +bin/demo: bin $(SOURCES) + CGO_ENABLED=$(CGO_ENABLED) go build -ldflags $(BUILD_FLAGS) -o bin/demo ./cmds/demoplugin + +bin/cliplugin: bin $(SOURCES) + CGO_ENABLED=$(CGO_ENABLED) go build -ldflags $(BUILD_FLAGS) -o bin/cliplugin ./cmds/cliplugin + +bin/ecrplugin: bin $(SOURCES) + CGO_ENABLED=$(CGO_ENABLED) go build -ldflags $(BUILD_FLAGS) -o bin/ecrplugin ./cmds/ecrplugin + +api: $(SOURCES) go build ./api/... + +examples: $(SOURCES) go build ./examples/... - CGO_ENABLED=0 go build -ldflags $(BUILD_FLAGS) -o bin/ocm ./cmds/ocm - CGO_ENABLED=0 go build -ldflags $(BUILD_FLAGS) -o bin/helminstaller ./cmds/helminstaller - CGO_ENABLED=0 go build -ldflags $(BUILD_FLAGS) -o bin/demo ./cmds/demoplugin - CGO_ENABLED=0 go build -ldflags $(BUILD_FLAGS) -o bin/cliplugin ./cmds/cliplugin - CGO_ENABLED=0 go build -ldflags $(BUILD_FLAGS) -o bin/ecrplugin ./cmds/ecrplugin build-platforms: $(GEN)/.exists $(SOURCES) @for i in $(PLATFORMS); do \ echo GOARCH=$$(basename $$i) GOOS=$$(dirname $$i); \ - GOARCH=$$(basename $$i) GOOS=$$(dirname $$i) CGO_ENABLED=0 go build ./cmds/ocm ./cmds/helminstaller ./cmds/ecrplugin; \ - done + GOARCH=$$(basename $$i) GOOS=$$(dirname $$i) CGO_ENABLED=$(CGO_ENABLED) go build ./cmds/ocm ./cmds/helminstaller ./cmds/ecrplugin & \ + done; \ + wait .PHONY: install-requirements install-requirements: @@ -72,17 +90,18 @@ check-and-fix: .PHONY: force-test force-test: - @go test --count=1 $(EFFECTIVE_DIRECTORIES) + @go test -vet=off --count=1 $(EFFECTIVE_DIRECTORIES) +TESTFLAGS = -vet=off --tags=integration .PHONY: test test: @echo "> Run Tests" - @go test --tags=integration $(EFFECTIVE_DIRECTORIES) + go test $(TESTFLAGS) $(EFFECTIVE_DIRECTORIES) .PHONY: unit-test unit-test: @echo "> Run Unit Tests" - @go test $(EFFECTIVE_DIRECTORIES) + @go test -vet=off $(EFFECTIVE_DIRECTORIES) .PHONY: generate generate: @@ -146,7 +165,7 @@ $(GEN)/.comps: $(GEN)/.exists .PHONY: ctf ctf: $(GEN)/ctf -$(GEN)/ctf: $(GEN)/.exists $(GEN)/.comps +$(GEN)/ctf: $(GEN)/.exists $(GEN)/.comps bin/ocm @rm -rf "$(GEN)"/ctf @for i in $(COMPONENTS); do \ echo "transfering component $$i..."; \ @@ -155,19 +174,27 @@ $(GEN)/ctf: $(GEN)/.exists $(GEN)/.comps done @touch $@ +.PHONY: describe +describe: $(GEN)/ctf bin/ocm + $(OCM) get resources --lookup $(OCMREPO) -r -o treewide $(GEN)/ctf + +.PHONY: descriptor +descriptor: $(GEN)/ctf bin/ocm + $(OCM) get component -S v3alpha1 -o yaml $(GEN)/ctf + .PHONY: push push: $(GEN)/ctf $(GEN)/.push.$(NAME) -$(GEN)/.push.$(NAME): $(GEN)/ctf +$(GEN)/.push.$(NAME): $(GEN)/ctf bin/ocm $(OCM) transfer ctf -f $(GEN)/ctf $(OCMREPO) @touch $@ .PHONY: plain-push -plain-push: $(GEN) +plain-push: $(GEN) bin/ocm $(OCM) transfer ctf -f $(GEN)/ctf $(OCMREPO) .PHONY: plain-ctf -plain-ctf: $(GEN) +plain-ctf: $(GEN) bin/ocm @rm -rf "$(GEN)"/ctf @for i in $(COMPONENTS); do \ echo "transfering component $$i..."; \ diff --git a/README.md b/README.md index 576b4399ae..d0025805a8 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,7 @@ More comprehensive examples can be taken from the [`components`](components) con ## GPG Public Key -The authenticity of released packages that have been uploaded to public repositories can be verified using our GPG public key. You can find the key in the file [OCM-RELEASES-PUBLIC.gpg](https://ocm.software/OCM-RELEASES-PUBLIC.gpg) on our website. +The authenticity of released packages that have been uploaded to public repositories can be verified using our GPG public key. You can find the current key in the file [OCM-RELEASES-PUBLIC-CURRENT.gpg](https://ocm.software/gpg/OCM-RELEASES-PUBLIC-CURRENT.gpg) on our website. You can find the old keys in the website github repository [here](https://github.com/open-component-model/ocm-website/tree/main/static/gpg). ## Contributing diff --git a/VERSION b/VERSION index a0073758b8..7120f981c5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.17.0-dev +0.19.0-dev diff --git a/api/credentials/internal/identity.go b/api/credentials/internal/identity.go index d8b3e84d91..6743754550 100644 --- a/api/credentials/internal/identity.go +++ b/api/credentials/internal/identity.go @@ -93,6 +93,10 @@ func orMatcher(list []IdentityMatcher) IdentityMatcher { // ConsumerIdentity describes the identity of a credential consumer. type ConsumerIdentity map[string]string +// UnmarshalJSON allows a yaml specification containing a data type other +// string, e.g. a hostpath spec with a port. Previously, it would error if the +// user specified `port: 5000` and instead, the user had to specify +// `port: "5000"`. func (c *ConsumerIdentity) UnmarshalJSON(data []byte) error { var m map[string]interface{} err := runtime.DefaultJSONEncoding.Unmarshal(data, &m) @@ -100,15 +104,21 @@ func (c *ConsumerIdentity) UnmarshalJSON(data []byte) error { return err } + if len(m) == 0 { + return nil + } *c = make(map[string]string, len(m)) for k, v := range m { switch v.(type) { + case nil: + (*c)[k] = "" case map[string]interface{}: return fmt.Errorf("cannot unmarshal complex type into consumer identity") case []interface{}: return fmt.Errorf("cannot unmarshal complex type into consumer identity") + default: + (*c)[k] = fmt.Sprintf("%v", v) } - (*c)[k] = fmt.Sprintf("%v", v) } return nil } diff --git a/api/credentials/internal/identity_test.go b/api/credentials/internal/identity_test.go index 990a19997a..66633df813 100644 --- a/api/credentials/internal/identity_test.go +++ b/api/credentials/internal/identity_test.go @@ -70,4 +70,19 @@ port: cid := internal.ConsumerIdentity{} Expect(yaml.Unmarshal([]byte(data), &cid)).NotTo(Succeed()) }) + It("with nil", func() { + data := ` +scheme: http +hostname: 127.0.0.1 +port: +` + id := internal.ConsumerIdentity{ + "scheme": "http", + "hostname": "127.0.0.1", + "port": "", + } + cid := internal.ConsumerIdentity{} + Expect(yaml.Unmarshal([]byte(data), &cid)).To(Succeed()) + Expect(cid).To(Equal(id)) + }) }) diff --git a/api/datacontext/action/api/registry.go b/api/datacontext/action/api/registry.go index bf2ceee6df..24049f3401 100644 --- a/api/datacontext/action/api/registry.go +++ b/api/datacontext/action/api/registry.go @@ -5,6 +5,7 @@ import ( "sync" "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/goutils/maputils" "golang.org/x/exp/slices" "ocm.software/ocm/api/utils" @@ -26,6 +27,7 @@ type ActionTypeRegistry interface { DecodeActionResult(data []byte, unmarshaler runtime.Unmarshaler) (ActionResult, error) EncodeActionResult(spec ActionResult, marshaler runtime.Marshaler) ([]byte, error) + GetActionNames() []string GetAction(name string) Action SupportedActionVersions(name string) []string @@ -161,6 +163,12 @@ func (r *actionRegistry) RegisterActionType(typ ActionType) error { return nil } +func (r *actionRegistry) GetActionNames() []string { + r.lock.Lock() + defer r.lock.Unlock() + return maputils.OrderedKeys(r.actions) +} + func (r *actionRegistry) GetAction(name string) Action { r.lock.Lock() defer r.lock.Unlock() diff --git a/api/datacontext/action/api/utils.go b/api/datacontext/action/api/utils.go index 52535f92f8..a6602c45f9 100644 --- a/api/datacontext/action/api/utils.go +++ b/api/datacontext/action/api/utils.go @@ -1,6 +1,8 @@ package api import ( + "ocm.software/ocm/api/utils" + common "ocm.software/ocm/api/utils/misc" "ocm.software/ocm/api/utils/runtime" ) @@ -36,3 +38,26 @@ func (a *actionType) SpecificationType() ActionSpecType { func (a *actionType) ResultType() ActionResultType { return a.restype } + +func Usage(reg ActionTypeRegistry) string { + p, buf := common.NewBufferedPrinter() + for _, n := range reg.GetActionNames() { + a := reg.GetAction(n) + p.Printf("- Name: %s\n", n) + if a.Description() != "" { + p.Printf("%s\n", utils.IndentLines(a.Description(), " ")) + } + if a.Usage() != "" { + p.Printf("\n%s\n", utils.IndentLines(a.Usage(), " ")) + } + p := p.AddGap(" ") + + if len(a.ConsumerAttributes()) > 0 { + p.Printf("Possible Consumer Attributes:\n") + for _, a := range a.ConsumerAttributes() { + p.Printf("- %s\n", a) + } + } + } + return buf.String() +} diff --git a/api/helper/builder/ocm_identity.go b/api/helper/builder/ocm_identity.go index f77719a7e3..ec302ee622 100644 --- a/api/helper/builder/ocm_identity.go +++ b/api/helper/builder/ocm_identity.go @@ -1,5 +1,9 @@ package builder +import ( + metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" +) + const T_OCMMETA = "element with metadata" //////////////////////////////////////////////////////////////////////////////// @@ -10,6 +14,18 @@ func (b *Builder) ExtraIdentity(name string, value string) { b.ocm_meta.ExtraIdentity.Set(name, value) } +func (b *Builder) ExtraIdentities(extras ...string) { + b.expect(b.ocm_meta, T_OCMMETA) + + id := metav1.NewExtraIdentity(extras...) + if b.ocm_meta.ExtraIdentity == nil { + b.ocm_meta.ExtraIdentity = metav1.Identity{} + } + for k, v := range id { + b.ocm_meta.ExtraIdentity.Set(k, v) + } +} + //////////////////////////////////////////////////////////////////////////////// func (b *Builder) RemoveExtraIdentity(name string) { diff --git a/api/helper/builder/ocm_reference.go b/api/helper/builder/ocm_reference.go index 93fc02f932..8f3d2b92f7 100644 --- a/api/helper/builder/ocm_reference.go +++ b/api/helper/builder/ocm_reference.go @@ -1,6 +1,7 @@ package builder import ( + "ocm.software/ocm/api/ocm" "ocm.software/ocm/api/ocm/compdesc" ) @@ -22,7 +23,7 @@ func (r *ocmReference) Set() { } func (r *ocmReference) Close() error { - return r.ocm_vers.SetReference(&r.meta) + return r.ocm_vers.SetReference(&r.meta, ocm.ModifyElement()) } //////////////////////////////////////////////////////////////////////////////// diff --git a/api/helper/builder/ocm_resource.go b/api/helper/builder/ocm_resource.go index 9b3ffe30dc..fb29c85b65 100644 --- a/api/helper/builder/ocm_resource.go +++ b/api/helper/builder/ocm_resource.go @@ -42,9 +42,9 @@ func (r *ocmResource) Close() error { } switch { case r.access != nil: - return r.Builder.ocm_vers.SetResource(&r.meta, r.access, r.opts.ApplyModificationOptions((ocm.ModifyResource()))) + return r.Builder.ocm_vers.SetResource(&r.meta, r.access, r.opts.ApplyModificationOptions((ocm.ModifyElement()))) case r.blob != nil: - return r.Builder.ocm_vers.SetResourceBlob(&r.meta, r.blob, r.hint, nil, r.opts.ApplyModificationOptions((ocm.ModifyResource()))) + return r.Builder.ocm_vers.SetResourceBlob(&r.meta, r.blob, r.hint, nil, r.opts.ApplyModificationOptions((ocm.ModifyElement()))) } return errors.New("access or blob required") } diff --git a/api/oci/art_test.go b/api/oci/art_test.go index d118fb22ab..5305930dc4 100644 --- a/api/oci/art_test.go +++ b/api/oci/art_test.go @@ -15,7 +15,7 @@ func CheckArt(ref string, exp *oci.ArtSpec) { Expect(err).To(HaveOccurred()) } else { Expect(err).To(Succeed()) - Expect(spec).To(Equal(*exp)) + Expect(spec).To(Equal(exp)) } } @@ -26,9 +26,9 @@ var _ = Describe("art parsing", func() { It("succeeds", func() { CheckArt("ubuntu", &oci.ArtSpec{Repository: "ubuntu"}) CheckArt("ubuntu/test", &oci.ArtSpec{Repository: "ubuntu/test"}) - CheckArt("ubuntu/test@"+digest.String(), &oci.ArtSpec{Repository: "ubuntu/test", Digest: &digest}) - CheckArt("ubuntu/test:"+tag, &oci.ArtSpec{Repository: "ubuntu/test", Tag: &tag}) - CheckArt("ubuntu/test:"+tag+"@"+digest.String(), &oci.ArtSpec{Repository: "ubuntu/test", Digest: &digest, Tag: &tag}) + CheckArt("ubuntu/test@"+digest.String(), &oci.ArtSpec{Repository: "ubuntu/test", ArtVersion: oci.ArtVersion{Digest: &digest}}) + CheckArt("ubuntu/test:"+tag, &oci.ArtSpec{Repository: "ubuntu/test", ArtVersion: oci.ArtVersion{Tag: &tag}}) + CheckArt("ubuntu/test:"+tag+"@"+digest.String(), &oci.ArtSpec{Repository: "ubuntu/test", ArtVersion: oci.ArtVersion{Digest: &digest, Tag: &tag}}) }) It("fails", func() { diff --git a/api/oci/extensions/repositories/artifactset/utils_synthesis.go b/api/oci/extensions/repositories/artifactset/utils_synthesis.go index 2a6bd5641b..74165c1154 100644 --- a/api/oci/extensions/repositories/artifactset/utils_synthesis.go +++ b/api/oci/extensions/repositories/artifactset/utils_synthesis.go @@ -11,6 +11,7 @@ import ( "ocm.software/ocm/api/oci/artdesc" "ocm.software/ocm/api/oci/cpi" + "ocm.software/ocm/api/oci/ociutils" "ocm.software/ocm/api/oci/tools/transfer" "ocm.software/ocm/api/oci/tools/transfer/filters" "ocm.software/ocm/api/utils/accessio" @@ -92,14 +93,19 @@ func SynthesizeArtifactBlobForArtifact(art cpi.ArtifactAccess, ref string, filte return nil, err } + vers, err := ociutils.ParseVersion(ref) + if err != nil { + return nil, err + } + return SythesizeArtifactSet(func(set *ArtifactSet) (string, error) { dig, err := transfer.TransferArtifactWithFilter(art, set, filters.And(filter...)) if err != nil { return "", fmt.Errorf("failed to transfer artifact: %w", err) } - if ok, _ := artdesc.IsDigest(ref); !ok { - err = set.AddTags(*dig, ref) + if ok := vers.IsTagged(); ok { + err = set.AddTags(*dig, vers.GetTag()) if err != nil { return "", fmt.Errorf("failed to add tag: %w", err) } diff --git a/api/oci/extensions/repositories/ocireg/blobs.go b/api/oci/extensions/repositories/ocireg/blobs.go new file mode 100644 index 0000000000..0ecf1b299d --- /dev/null +++ b/api/oci/extensions/repositories/ocireg/blobs.go @@ -0,0 +1,109 @@ +package ocireg + +import ( + "sync" + + "github.com/containerd/containerd/remotes" + "github.com/mandelsoft/goutils/errors" + "github.com/opencontainers/go-digest" + "github.com/sirupsen/logrus" + + "ocm.software/ocm/api/oci/cpi" + "ocm.software/ocm/api/oci/extensions/attrs/cacheattr" + "ocm.software/ocm/api/tech/docker/resolve" + "ocm.software/ocm/api/utils/accessio" + "ocm.software/ocm/api/utils/blobaccess/blobaccess" +) + +type BlobContainer interface { + GetBlobData(digest digest.Digest) (int64, cpi.DataAccess, error) + AddBlob(blob cpi.BlobAccess) (int64, digest.Digest, error) + Unref() error +} + +type blobContainer struct { + accessio.StaticAllocatable + fetcher resolve.Fetcher + pusher resolve.Pusher + mime string +} + +type BlobContainers struct { + lock sync.Mutex + cache accessio.BlobCache + fetcher resolve.Fetcher + pusher resolve.Pusher + mimes map[string]BlobContainer +} + +func NewBlobContainers(ctx cpi.Context, fetcher remotes.Fetcher, pusher resolve.Pusher) *BlobContainers { + return &BlobContainers{ + cache: cacheattr.Get(ctx), + fetcher: fetcher, + pusher: pusher, + mimes: map[string]BlobContainer{}, + } +} + +func (c *BlobContainers) Get(mime string) (BlobContainer, error) { + c.lock.Lock() + defer c.lock.Unlock() + + found := c.mimes[mime] + if found == nil { + container, err := NewBlobContainer(c.cache, mime, c.fetcher, c.pusher) + if err != nil { + return nil, err + } + c.mimes[mime] = container + + return container, nil + } + + return found, nil +} + +func (c *BlobContainers) Release() error { + c.lock.Lock() + defer c.lock.Unlock() + list := errors.ErrListf("releasing mime block caches") + for _, b := range c.mimes { + list.Add(b.Unref()) + } + return list.Result() +} + +func newBlobContainer(mime string, fetcher resolve.Fetcher, pusher resolve.Pusher) *blobContainer { + return &blobContainer{ + mime: mime, + fetcher: fetcher, + pusher: pusher, + } +} + +func NewBlobContainer(cache accessio.BlobCache, mime string, fetcher resolve.Fetcher, pusher resolve.Pusher) (BlobContainer, error) { + c := newBlobContainer(mime, fetcher, pusher) + + if cache == nil { + return c, nil + } + r, err := accessio.CachedAccess(c, c, cache) + if err != nil { + return nil, err + } + return r, nil +} + +func (n *blobContainer) GetBlobData(digest digest.Digest) (int64, cpi.DataAccess, error) { + logrus.Debugf("orig get %s %s\n", n.mime, digest) + acc, err := NewDataAccess(n.fetcher, digest, n.mime, false) + return blobaccess.BLOB_UNKNOWN_SIZE, acc, err +} + +func (n *blobContainer) AddBlob(blob cpi.BlobAccess) (int64, digest.Digest, error) { + err := push(dummyContext, n.pusher, blob) + if err != nil { + return blobaccess.BLOB_UNKNOWN_SIZE, blobaccess.BLOB_UNKNOWN_DIGEST, err + } + return blob.Size(), blob.Digest(), err +} diff --git a/api/oci/extensions/repositories/ocireg/namespace.go b/api/oci/extensions/repositories/ocireg/namespace.go index cd46662e7c..9ef8239979 100644 --- a/api/oci/extensions/repositories/ocireg/namespace.go +++ b/api/oci/extensions/repositories/ocireg/namespace.go @@ -4,15 +4,15 @@ import ( "context" "fmt" + "github.com/containerd/errdefs" "github.com/mandelsoft/goutils/errors" "github.com/opencontainers/go-digest" - "oras.land/oras-go/v2/errdef" - "oras.land/oras-go/v2/registry" "ocm.software/ocm/api/oci/artdesc" "ocm.software/ocm/api/oci/cpi" "ocm.software/ocm/api/oci/cpi/support" "ocm.software/ocm/api/oci/extensions/actions/oci-repository-prepare" + "ocm.software/ocm/api/tech/docker/resolve" "ocm.software/ocm/api/utils/accessio" "ocm.software/ocm/api/utils/blobaccess/blobaccess" "ocm.software/ocm/api/utils/logging" @@ -20,108 +20,153 @@ import ( ) type NamespaceContainer struct { - impl support.NamespaceAccessImpl - repo *RepositoryImpl - checked bool - ociRepo registry.Repository + impl support.NamespaceAccessImpl + repo *RepositoryImpl + resolver resolve.Resolver + lister resolve.Lister + fetcher resolve.Fetcher + pusher resolve.Pusher + blobs *BlobContainers + checked bool } var _ support.NamespaceContainer = (*NamespaceContainer)(nil) func NewNamespace(repo *RepositoryImpl, name string) (cpi.NamespaceAccess, error) { ref := repo.GetRef(name, "") - ociRepo, err := repo.getResolver(ref, name) + resolver, err := repo.getResolver(name) + if err != nil { + return nil, err + } + fetcher, err := resolver.Fetcher(context.Background(), ref) + if err != nil { + return nil, err + } + pusher, err := resolver.Pusher(context.Background(), ref) + if err != nil { + return nil, err + } + lister, err := resolver.Lister(context.Background(), ref) if err != nil { return nil, err } - c := &NamespaceContainer{ - repo: repo, - ociRepo: ociRepo, + repo: repo, + resolver: resolver, + lister: lister, + fetcher: fetcher, + pusher: pusher, + blobs: NewBlobContainers(repo.GetContext(), fetcher, pusher), } return support.NewNamespaceAccess(name, c, repo) } func (n *NamespaceContainer) Close() error { - return n.repo.Close() + return n.blobs.Release() } func (n *NamespaceContainer) SetImplementation(impl support.NamespaceAccessImpl) { n.impl = impl } +func (n *NamespaceContainer) getPusher(vers string) (resolve.Pusher, error) { + err := n.assureCreated() + if err != nil { + return nil, err + } + + ref := n.repo.GetRef(n.impl.GetNamespace(), vers) + resolver := n.resolver + + n.repo.GetContext().Logger().Trace("get pusher", "ref", ref) + if ok, _ := artdesc.IsDigest(vers); !ok { + var err error + + resolver, err = n.repo.getResolver(n.impl.GetNamespace()) + if err != nil { + return nil, fmt.Errorf("unable get resolver: %w", err) + } + } + + return resolver.Pusher(dummyContext, ref) +} + +func (n *NamespaceContainer) push(vers string, blob cpi.BlobAccess) error { + p, err := n.getPusher(vers) + if err != nil { + return fmt.Errorf("unable to get pusher: %w", err) + } + n.repo.GetContext().Logger().Trace("pushing", "version", vers) + return push(dummyContext, p, blob) +} + func (n *NamespaceContainer) IsReadOnly() bool { return n.repo.IsReadOnly() } func (n *NamespaceContainer) GetBlobData(digest digest.Digest) (int64, cpi.DataAccess, error) { n.repo.GetContext().Logger().Debug("getting blob", "digest", digest) - - acc, err := NewDataAccess(n.ociRepo, digest, false) + blob, err := n.blobs.Get("") if err != nil { - return -1, nil, fmt.Errorf("failed to construct data access: %w", err) + return -1, nil, fmt.Errorf("failed to retrieve blob data: %w", err) } - - n.repo.GetContext().Logger().Debug("getting blob done", "digest", digest, "size", blobaccess.BLOB_UNKNOWN_SIZE, "error", logging.ErrorMessage(err)) - return blobaccess.BLOB_UNKNOWN_SIZE, acc, err + size, acc, err := blob.GetBlobData(digest) + n.repo.GetContext().Logger().Debug("getting blob done", "digest", digest, "size", size, "error", logging.ErrorMessage(err)) + return size, acc, err } func (n *NamespaceContainer) AddBlob(blob cpi.BlobAccess) error { log := n.repo.GetContext().Logger() log.Debug("adding blob", "digest", blob.Digest()) - - if err := n.assureCreated(); err != nil { - return err + blobData, err := n.blobs.Get("") + if err != nil { + return fmt.Errorf("failed to retrieve blob data: %w", err) } - - if err := push(dummyContext, n.ociRepo, blob); err != nil { + err = n.assureCreated() + if err != nil { return err } - + if _, _, err := blobData.AddBlob(blob); err != nil { + log.Debug("adding blob failed", "digest", blob.Digest(), "error", err.Error()) + return fmt.Errorf("unable to add blob (OCI repository %s): %w", n.impl.GetNamespace(), err) + } log.Debug("adding blob done", "digest", blob.Digest()) return nil } func (n *NamespaceContainer) ListTags() ([]string, error) { - var result []string - if err := n.ociRepo.Tags(dummyContext, "", func(tags []string) error { - result = append(result, tags...) - - return nil - }); err != nil { - return nil, err - } - - return result, nil + return n.lister.List(dummyContext) } func (n *NamespaceContainer) GetArtifact(i support.NamespaceAccessImpl, vers string) (cpi.ArtifactAccess, error) { ref := n.repo.GetRef(n.impl.GetNamespace(), vers) n.repo.GetContext().Logger().Debug("get artifact", "ref", ref) - desc, err := n.ociRepo.Resolve(context.Background(), ref) + _, desc, err := n.resolver.Resolve(context.Background(), ref) n.repo.GetContext().Logger().Debug("done", "digest", desc.Digest, "size", desc.Size, "mimetype", desc.MediaType, "error", logging.ErrorMessage(err)) if err != nil { - if errors.Is(err, errdef.ErrNotFound) { + if errdefs.IsNotFound(err) { return nil, errors.ErrNotFound(cpi.KIND_OCIARTIFACT, ref, n.impl.GetNamespace()) } return nil, err } - - acc, err := NewDataAccess(n.ociRepo, desc.Digest, false) + blobData, err := n.blobs.Get(desc.MediaType) if err != nil { - return nil, fmt.Errorf("failed to construct data access: %w", err) + return nil, fmt.Errorf("failed to retrieve blob data, blob data was empty: %w", err) + } + _, acc, err := blobData.GetBlobData(desc.Digest) + if err != nil { + return nil, err } - return support.NewArtifactForBlob(i, blobaccess.ForDataAccess(desc.Digest, desc.Size, desc.MediaType, acc)) } func (n *NamespaceContainer) HasArtifact(vers string) (bool, error) { ref := n.repo.GetRef(n.impl.GetNamespace(), vers) n.repo.GetContext().Logger().Debug("check artifact", "ref", ref) - desc, err := n.ociRepo.Resolve(context.Background(), ref) + _, desc, err := n.resolver.Resolve(context.Background(), ref) n.repo.GetContext().Logger().Debug("done", "digest", desc.Digest, "size", desc.Size, "mimetype", desc.MediaType, "error", logging.ErrorMessage(err)) if err != nil { - if errors.Is(err, errdef.ErrNotFound) { + if errdefs.IsNotFound(err) { return false, nil } return false, err @@ -159,15 +204,20 @@ func (n *NamespaceContainer) AddArtifact(artifact cpi.Artifact, tags ...string) } n.repo.GetContext().Logger().Debug("adding artifact", "digest", blob.Digest(), "mimetype", blob.MimeType()) + blobData, err := n.blobs.Get(blob.MimeType()) + if err != nil { + return nil, fmt.Errorf("failed to retrieve blob data: %w", err) + } - if err := n.assureCreated(); err != nil { + _, _, err = blobData.AddBlob(blob) + if err != nil { return nil, err } if len(tags) > 0 { for _, tag := range tags { - if err := n.pushTag(blob, tag); err != nil { - return nil, fmt.Errorf("failed to push tag %s: %w", tag, err) + if err := n.push(tag, blob); err != nil { + return nil, err } } } @@ -175,52 +225,22 @@ func (n *NamespaceContainer) AddArtifact(artifact cpi.Artifact, tags ...string) return blob, err } -func (n *NamespaceContainer) pushTag(blob blobaccess.BlobAccess, tag string) error { - reader, err := blob.Reader() - if err != nil { - return err - } - - expectedDescriptor := *artdesc.DefaultBlobDescriptor(blob) - // If the descriptor exists, we are adding the blob to the descriptor as is. - if err := n.ociRepo.PushReference(context.Background(), expectedDescriptor, reader, tag); err != nil { - // If the manifest is unknown to the registry, which can occur with Docker, - // we might be able to push the entire blob instead of a reference. - // We can't assert the error because docker returns Manifest Unknown, - // while ghcr.io or quay work with PushReference out of the box. - // Meanwhile, we need to get the reader again, because PushReference exhausted it. - reader, err = blob.Reader() - if err != nil { - return err - } - - // If any other error arises, pushing the blob would also fail. - return n.ociRepo.Blobs().Push(context.Background(), expectedDescriptor, reader) - } - - return nil -} - func (n *NamespaceContainer) AddTags(digest digest.Digest, tags ...string) error { - ref := n.repo.GetRef(n.impl.GetNamespace(), digest.String()) - desc, err := n.ociRepo.Resolve(context.Background(), ref) + _, desc, err := n.resolver.Resolve(context.Background(), n.repo.GetRef(n.impl.GetNamespace(), digest.String())) if err != nil { return fmt.Errorf("unable to resolve: %w", err) } - acc, err := NewDataAccess(n.ociRepo, desc.Digest, false) + acc, err := NewDataAccess(n.fetcher, desc.Digest, desc.MediaType, false) if err != nil { return fmt.Errorf("error creating new data access: %w", err) } - if err := n.assureCreated(); err != nil { - return err - } - blob := blobaccess.ForDataAccess(desc.Digest, desc.Size, desc.MediaType, acc) for _, tag := range tags { - if err := n.pushTag(blob, tag); err != nil { - return fmt.Errorf("failed to push tag %s: %w", tag, err) + err := n.push(tag, blob) + if err != nil { + return fmt.Errorf("unable to push: %w", err) } } diff --git a/api/oci/extensions/repositories/ocireg/repository.go b/api/oci/extensions/repositories/ocireg/repository.go index 061879a0d2..1bae127a71 100644 --- a/api/oci/extensions/repositories/ocireg/repository.go +++ b/api/oci/extensions/repositories/ocireg/repository.go @@ -4,23 +4,20 @@ import ( "context" "crypto/tls" "crypto/x509" - "fmt" - "net/http" "path" "strings" + "github.com/containerd/containerd/remotes/docker/config" + "github.com/containerd/errdefs" "github.com/mandelsoft/goutils/errors" "github.com/mandelsoft/logging" - "oras.land/oras-go/v2/errdef" - "oras.land/oras-go/v2/registry" - "oras.land/oras-go/v2/registry/remote" - "oras.land/oras-go/v2/registry/remote/auth" - "oras.land/oras-go/v2/registry/remote/retry" "ocm.software/ocm/api/credentials" "ocm.software/ocm/api/datacontext/attrs/rootcertsattr" "ocm.software/ocm/api/oci/artdesc" "ocm.software/ocm/api/oci/cpi" + "ocm.software/ocm/api/tech/docker" + "ocm.software/ocm/api/tech/docker/resolve" "ocm.software/ocm/api/tech/oci/identity" "ocm.software/ocm/api/utils" ocmlog "ocm.software/ocm/api/utils/logging" @@ -115,7 +112,7 @@ func (r *RepositoryImpl) getCreds(comp string) (credentials.Credentials, error) return identity.GetCredentials(r.GetContext(), r.info.Locator, comp) } -func (r *RepositoryImpl) getResolver(ref string, comp string) (registry.Repository, error) { +func (r *RepositoryImpl) getResolver(comp string) (resolve.Resolver, error) { creds, err := r.getCreds(comp) if err != nil { if !errors.IsErrUnknownKind(err, credentials.KIND_CONSUMER) { @@ -126,59 +123,53 @@ func (r *RepositoryImpl) getResolver(ref string, comp string) (registry.Reposito if creds == nil { logger.Trace("no credentials") } - repo, err := remote.NewRepository(ref) - if err != nil { - return nil, fmt.Errorf("error creating oci repository: %w", err) - } - - authCreds := auth.Credential{} - if creds != nil { - pass := creds.GetProperty(credentials.ATTR_IDENTITY_TOKEN) - if pass == "" { - pass = creds.GetProperty(credentials.ATTR_PASSWORD) - } - authCreds.Username = creds.GetProperty(credentials.ATTR_USERNAME) - authCreds.Password = pass - } - client := http.DefaultClient - if r.info.Scheme == "https" { - // set up TLS - //nolint:gosec // used like the default, there are OCI servers (quay.io) not working with min version. - conf := &tls.Config{ - // MinVersion: tls.VersionTLS13, - RootCAs: func() *x509.CertPool { - var rootCAs *x509.CertPool + opts := docker.ResolverOptions{ + Hosts: docker.ConvertHosts(config.ConfigureHosts(context.Background(), config.HostOptions{ + Credentials: func(host string) (string, string, error) { if creds != nil { - c := creds.GetProperty(credentials.ATTR_CERTIFICATE_AUTHORITY) - if c != "" { - rootCAs = x509.NewCertPool() - rootCAs.AppendCertsFromPEM([]byte(c)) + p := creds.GetProperty(credentials.ATTR_IDENTITY_TOKEN) + if p == "" { + p = creds.GetProperty(credentials.ATTR_PASSWORD) } + pw := "" + if p != "" { + pw = "***" + } + logger.Trace("query credentials", ocmlog.ATTR_USER, creds.GetProperty(credentials.ATTR_USERNAME), "pass", pw) + return creds.GetProperty(credentials.ATTR_USERNAME), p, nil + } + logger.Trace("no credentials") + return "", "", nil + }, + DefaultScheme: r.info.Scheme, + //nolint:gosec // used like the default, there are OCI servers (quay.io) not working with min version. + DefaultTLS: func() *tls.Config { + if r.info.Scheme == "http" { + return nil } - if rootCAs == nil { - rootCAs = rootcertsattr.Get(r.GetContext()).GetRootCertPool(true) + return &tls.Config{ + // MinVersion: tls.VersionTLS13, + RootCAs: func() *x509.CertPool { + var rootCAs *x509.CertPool + if creds != nil { + c := creds.GetProperty(credentials.ATTR_CERTIFICATE_AUTHORITY) + if c != "" { + rootCAs = x509.NewCertPool() + rootCAs.AppendCertsFromPEM([]byte(c)) + } + } + if rootCAs == nil { + rootCAs = rootcertsattr.Get(r.GetContext()).GetRootCertPool(true) + } + return rootCAs + }(), } - return rootCAs }(), - } - - client = &http.Client{ - Transport: retry.NewTransport(&http.Transport{ - TLSClientConfig: conf, - }), - } - } else { - repo.PlainHTTP = true - } - - repo.Client = &auth.Client{ - Client: client, - Cache: auth.NewCache(), - Credential: auth.StaticCredential(r.info.HostPort(), authCreds), + })), } - return repo, nil + return docker.NewResolver(opts), nil } func (r *RepositoryImpl) GetRef(comp, vers string) string { @@ -197,14 +188,14 @@ func (r *RepositoryImpl) GetBaseURL() string { } func (r *RepositoryImpl) ExistsArtifact(name string, version string) (bool, error) { - ref := r.GetRef(name, version) - res, err := r.getResolver(ref, name) + res, err := r.getResolver(name) if err != nil { return false, err } - - if _, err = res.Resolve(context.Background(), ref); err != nil { - if errors.Is(err, errdef.ErrNotFound) { + ref := r.GetRef(name, version) + _, _, err = res.Resolve(context.Background(), ref) + if err != nil { + if errdefs.IsNotFound(err) { return false, nil } return false, err diff --git a/api/oci/extensions/repositories/ocireg/utils.go b/api/oci/extensions/repositories/ocireg/utils.go index 828582f064..17a96f040a 100644 --- a/api/oci/extensions/repositories/ocireg/utils.go +++ b/api/oci/extensions/repositories/ocireg/utils.go @@ -2,21 +2,19 @@ package ocireg import ( "context" - "errors" "fmt" "io" "sync" "github.com/containerd/containerd/remotes" + "github.com/containerd/errdefs" "github.com/containerd/log" "github.com/opencontainers/go-digest" "github.com/sirupsen/logrus" - "oras.land/oras-go/v2/content" - "oras.land/oras-go/v2/errdef" - "oras.land/oras-go/v2/registry" "ocm.software/ocm/api/oci/artdesc" "ocm.software/ocm/api/oci/cpi" + "ocm.software/ocm/api/tech/docker/resolve" "ocm.software/ocm/api/utils/accessio" "ocm.software/ocm/api/utils/blobaccess/blobaccess" "ocm.software/ocm/api/utils/logging" @@ -26,40 +24,32 @@ import ( type dataAccess struct { accessio.NopCloser - lock sync.Mutex - repo registry.Repository - desc artdesc.Descriptor - reader io.ReadCloser + lock sync.Mutex + fetcher remotes.Fetcher + desc artdesc.Descriptor + reader io.ReadCloser } var _ cpi.DataAccess = (*dataAccess)(nil) -func NewDataAccess(repo registry.Repository, digest digest.Digest, delayed bool) (*dataAccess, error) { +func NewDataAccess(fetcher remotes.Fetcher, digest digest.Digest, mimeType string, delayed bool) (*dataAccess, error) { var reader io.ReadCloser - // First, we try to resolve a blob if a blob was already provided, this will work. - desc, err := repo.Blobs().Resolve(dummyContext, digest.String()) - if err != nil { - if errors.Is(err, errdef.ErrNotFound) { - // If the provided digest was that of a manifest, the second try will find - // the manifest, because the first one didn't find the blob. - desc, err = repo.Resolve(dummyContext, digest.String()) - if err != nil { - return nil, err - } - } else { - return nil, fmt.Errorf("failed to resolve descriptor with digest %s: %w", digest.String(), err) - } + var err error + desc := artdesc.Descriptor{ + MediaType: mimeType, + Digest: digest, + Size: blobaccess.BLOB_UNKNOWN_SIZE, } if !delayed { - reader, err = repo.Fetch(dummyContext, desc) + reader, err = fetcher.Fetch(dummyContext, desc) if err != nil { - return nil, fmt.Errorf("failed to fetch descriptor: %w", err) + return nil, err } } return &dataAccess{ - repo: repo, - desc: desc, - reader: reader, + fetcher: fetcher, + desc: desc, + reader: reader, }, nil } @@ -75,7 +65,7 @@ func (d *dataAccess) Reader() (io.ReadCloser, error) { if reader != nil { return reader, nil } - return d.repo.Fetch(dummyContext, d.desc) + return d.fetcher.Fetch(dummyContext, d.desc) } func readAll(reader io.ReadCloser, err error) ([]byte, error) { @@ -91,32 +81,28 @@ func readAll(reader io.ReadCloser, err error) ([]byte, error) { return data, nil } -func push(ctx context.Context, p content.Pusher, blob blobaccess.BlobAccess) error { +func push(ctx context.Context, p resolve.Pusher, blob blobaccess.BlobAccess) error { desc := *artdesc.DefaultBlobDescriptor(blob) return pushData(ctx, p, desc, blob) } -func pushData(ctx context.Context, p content.Pusher, desc artdesc.Descriptor, data blobaccess.DataAccess) error { +func pushData(ctx context.Context, p resolve.Pusher, desc artdesc.Descriptor, data blobaccess.DataAccess) error { key := remotes.MakeRefKey(ctx, desc) if desc.Size == 0 { desc.Size = -1 } logging.Logger().Debug("*** push blob", "mediatype", desc.MediaType, "digest", desc.Digest, "key", key) - reader, err := data.Reader() + req, err := p.Push(ctx, desc, data) if err != nil { - return err - } - - if err := p.Push(ctx, desc, reader); err != nil { - if errors.Is(err, errdef.ErrAlreadyExists) { + if errdefs.IsAlreadyExists(err) { logging.Logger().Debug("blob already exists", "mediatype", desc.MediaType, "digest", desc.Digest) return nil } return fmt.Errorf("failed to push: %w", err) } - return nil + return req.Commit(ctx, desc.Size, desc.Digest) } var dummyContext = nologger() diff --git a/api/oci/ociutils/ref.go b/api/oci/ociutils/ref.go new file mode 100644 index 0000000000..b419e30a3d --- /dev/null +++ b/api/oci/ociutils/ref.go @@ -0,0 +1,116 @@ +package ociutils + +import ( + "strings" + + "github.com/mandelsoft/goutils/generics" + "github.com/opencontainers/go-digest" +) + +// ParseVersion parses the version part of an OCI reference consisting +// of an optional tag and/or digest. +func ParseVersion(vers string) (*ArtVersion, error) { + if strings.HasPrefix(vers, "@") { + dig, err := digest.Parse(vers[1:]) + if err != nil { + return nil, err + } + return &ArtVersion{ + Digest: &dig, + }, nil + } + + i := strings.Index(vers, "@") + if i > 0 { + dig, err := digest.Parse(vers[i+1:]) + if err != nil { + return nil, err + } + return &ArtVersion{ + Tag: generics.Pointer(vers[:i]), + Digest: &dig, + }, nil + } + if vers == "" { + return &ArtVersion{}, nil + } + return &ArtVersion{ + Tag: &vers, + }, nil +} + +// ArtVersion is the version part of an OCI reference consisting of an +// optional tag and/or digest. Both parts may be nil, if a reference +// does not include a version part. +// Such objects are sub objects of (oci.)ArtSpec, which has be moved +// to separate package to avoid package cycles. The methods are +// derived from ArtSpec. +type ArtVersion struct { + // +optional + Tag *string `json:"tag,omitempty"` + // +optional + Digest *digest.Digest `json:"digest,omitempty"` +} + +func (v *ArtVersion) VersionSpec() string { + if v == nil { + return "" + } + + vers := "" + if v.Tag != nil { + vers = *v.Tag + } + + if v.Digest != nil { + vers += "@" + string(*v.Digest) + } + if vers == "" { + return "latest" + } + return vers +} + +// IsVersion returns true, if the object ref is given +// and describes a dedicated version, either by tag or digest. +// As part of the ArtSpec type in oci, it might describe +// no version part. THis method indicates, whether a version part +// is present. +func (v *ArtVersion) IsVersion() bool { + if v == nil { + return false + } + return v.Tag != nil || v.Digest != nil +} + +func (v *ArtVersion) IsTagged() bool { + return v != nil && v.Tag != nil +} + +func (v *ArtVersion) IsDigested() bool { + return v != nil && v.Digest != nil +} + +func (v *ArtVersion) GetTag() string { + if v != nil && v.Tag != nil { + return *v.Tag + } + return "" +} + +func (v *ArtVersion) GetDigest() digest.Digest { + if v != nil && v.Digest != nil { + return *v.Digest + } + return "" +} + +func (r *ArtVersion) Version() string { + if r.Digest != nil { + return "@" + string(*r.Digest) + } + if r.Tag != nil { + return *r.Tag + } + return "latest" +} diff --git a/api/oci/ociutils/ref_test.go b/api/oci/ociutils/ref_test.go new file mode 100644 index 0000000000..1e5075447a --- /dev/null +++ b/api/oci/ociutils/ref_test.go @@ -0,0 +1,70 @@ +package ociutils_test + +import ( + . "github.com/mandelsoft/goutils/testutils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/opencontainers/go-digest" + "ocm.software/ocm/api/oci/ociutils" + "ocm.software/ocm/api/oci/testhelper" +) + +var _ = Describe("Ref Test Environment", func() { + dig := "sha256:" + testhelper.H_OCIARCHMANIFEST1 + + type expect struct { + yaml string + versionSpec string + isVersion bool + version string + isTag bool + tag string + isDigested bool + digest string + } + + DescribeTable("parsing", func(src string, e expect) { + v := Must(ociutils.ParseVersion(src)) + Expect(v).NotTo(BeNil()) + Expect(v).To(YAMLEqual(e.yaml)) + Expect(v.VersionSpec()).To(Equal(e.versionSpec)) + Expect(v.IsVersion()).To(Equal(e.isVersion)) + Expect(v.Version()).To(Equal(e.version)) + Expect(v.IsTagged()).To(Equal(e.isTag)) + Expect(v.GetTag()).To(Equal(e.tag)) + Expect(v.IsDigested()).To(Equal(e.isDigested)) + Expect(v.GetDigest()).To(Equal(digest.Digest(e.digest))) + }, + Entry("empty", "", expect{ + yaml: "{}", + versionSpec: "latest", + version: "latest", + }), + Entry("tag", "tag", expect{ + yaml: "{\"tag\":\"tag\"}", + versionSpec: "tag", + isVersion: true, + version: "tag", + isTag: true, + tag: "tag", + }), + Entry("digest", "@"+dig, expect{ + yaml: "{\"digest\":\"" + dig + "\"}", + versionSpec: "@" + dig, + isVersion: true, + version: "@" + dig, + isDigested: true, + digest: dig, + }), + Entry("tag@digest", "tag@"+dig, expect{ + yaml: "{\"tag\":\"tag\",\"digest\":\"" + dig + "\"}", + versionSpec: "tag@" + dig, + isVersion: true, + version: "@" + dig, + isTag: true, + tag: "tag", + isDigested: true, + digest: dig, + }), + ) +}) diff --git a/api/oci/ociutils/suite_test.go b/api/oci/ociutils/suite_test.go new file mode 100644 index 0000000000..bf4c1257f7 --- /dev/null +++ b/api/oci/ociutils/suite_test.go @@ -0,0 +1,13 @@ +package ociutils_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "OCI Utils Test Suite") +} diff --git a/api/oci/ref.go b/api/oci/ref.go index 457eef3f1e..0d64f407b0 100644 --- a/api/oci/ref.go +++ b/api/oci/ref.go @@ -8,6 +8,7 @@ import ( "github.com/opencontainers/go-digest" "ocm.software/ocm/api/oci/grammar" + "ocm.software/ocm/api/oci/ociutils" ) // to find a suitable secret for images on Docker Hub, we need its two domains to do matching. @@ -224,11 +225,18 @@ func (r RefSpec) DeepCopy() RefSpec { //////////////////////////////////////////////////////////////////////////////// -func ParseArt(art string) (ArtSpec, error) { +// ParseVersion parses an OCI version part of an OCI reference. +// It has to be placed in a utils package to avoid package cycles +// for particular users. +func ParseVersion(vers string) (*ArtVersion, error) { + return ociutils.ParseVersion(vers) +} + +func ParseArt(art string) (*ArtSpec, error) { match := grammar.AnchoredArtifactVersionRegexp.FindSubmatch([]byte(art)) if match == nil { - return ArtSpec{}, errors.ErrInvalid(KIND_ARETEFACT_REFERENCE, art) + return nil, errors.ErrInvalid(KIND_ARETEFACT_REFERENCE, art) } var tag *string var dig *digest.Digest @@ -241,57 +249,37 @@ func ParseArt(art string) (ArtSpec, error) { t := string(match[3]) d, err := digest.Parse(t) if err != nil { - return ArtSpec{}, errors.ErrInvalidWrap(err, KIND_ARETEFACT_REFERENCE, art) + return nil, errors.ErrInvalidWrap(err, KIND_ARETEFACT_REFERENCE, art) } dig = &d } - return ArtSpec{ + return &ArtSpec{ Repository: string(match[1]), - Tag: tag, - Digest: dig, + ArtVersion: ArtVersion{ + Tag: tag, + Digest: dig, + }, }, nil } +type ArtVersion = ociutils.ArtVersion + // ArtSpec is a go internal representation of a oci reference. type ArtSpec struct { // Repository is the part of a reference without its hostname Repository string `json:"repository"` - // +optional - Tag *string `json:"tag,omitempty"` - // +optional - Digest *digest.Digest `json:"digest,omitempty"` -} - -func (r *ArtSpec) Version() string { - if r.Digest != nil { - return "@" + string(*r.Digest) - } - if r.Tag != nil { - return *r.Tag - } - return "latest" + // artifact version + ArtVersion `json:",inline"` } func (r *ArtSpec) IsRegistry() bool { return r.Repository == "" } -func (r *ArtSpec) IsVersion() bool { - return r.Tag != nil || r.Digest != nil -} - -func (r *ArtSpec) IsTagged() bool { - return r.Tag != nil -} - -func (r *ArtSpec) GetTag() string { - if r.Tag != nil { - return *r.Tag - } - return "" -} - func (r *ArtSpec) String() string { + if r == nil { + return "" + } s := r.Repository if r.Tag != nil { s += fmt.Sprintf(":%s", *r.Tag) diff --git a/api/oci/ref_test.go b/api/oci/ref_test.go index a62abd3c18..c95850ef03 100644 --- a/api/oci/ref_test.go +++ b/api/oci/ref_test.go @@ -7,6 +7,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/mandelsoft/goutils/generics" godigest "github.com/opencontainers/go-digest" "ocm.software/ocm/api/oci" @@ -143,8 +144,7 @@ var _ = Describe("ref parsing", func() { }, ArtSpec: oci.ArtSpec{ Repository: r, - Tag: Pointer([]byte(uv)), - Digest: Dig([]byte(ud)), + ArtVersion: oci.ArtVersion{Tag: Pointer([]byte(uv)), Digest: Dig([]byte(ud))}, }, }) }) @@ -192,8 +192,10 @@ var _ = Describe("ref parsing", func() { }, ArtSpec: oci.ArtSpec{ Repository: r, - Tag: Pointer([]byte(uv)), - Digest: Dig([]byte(ud)), + ArtVersion: oci.ArtVersion{ + Tag: Pointer([]byte(uv)), + Digest: Dig([]byte(ud)), + }, }, }) }) @@ -253,8 +255,10 @@ var _ = Describe("ref parsing", func() { }, ArtSpec: oci.ArtSpec{ Repository: r, - Tag: Pointer([]byte(uv)), - Digest: Dig([]byte(ud)), + ArtVersion: oci.ArtVersion{ + Tag: Pointer([]byte(uv)), + Digest: Dig([]byte(ud)), + }, }, }) }) @@ -291,8 +295,10 @@ var _ = Describe("ref parsing", func() { }, ArtSpec: oci.ArtSpec{ Repository: r, - Tag: Pointer([]byte(uv)), - Digest: Dig([]byte(ud)), + ArtVersion: oci.ArtVersion{ + Tag: Pointer([]byte(uv)), + Digest: Dig([]byte(ud)), + }, }, }) }) @@ -341,8 +347,10 @@ var _ = Describe("ref parsing", func() { }, ArtSpec: oci.ArtSpec{ Repository: r, - Tag: Pointer([]byte(uv)), - Digest: Dig([]byte(ud)), + ArtVersion: oci.ArtVersion{ + Tag: Pointer([]byte(uv)), + Digest: Dig([]byte(ud)), + }, }, }) }) @@ -380,8 +388,10 @@ var _ = Describe("ref parsing", func() { }, ArtSpec: oci.ArtSpec{ Repository: "library/" + r, - Tag: Pointer([]byte(uv)), - Digest: Dig([]byte(ud)), + ArtVersion: oci.ArtVersion{ + Tag: Pointer([]byte(uv)), + Digest: Dig([]byte(ud)), + }, }, }) }) @@ -416,8 +426,10 @@ var _ = Describe("ref parsing", func() { }, ArtSpec: oci.ArtSpec{ Repository: r, - Tag: Pointer([]byte(uv)), - Digest: Dig([]byte(ud)), + ArtVersion: oci.ArtVersion{ + Tag: Pointer([]byte(uv)), + Digest: Dig([]byte(ud)), + }, }, }) }) @@ -565,20 +577,20 @@ var _ = Describe("ref parsing", func() { }) It("succeeds", func() { CheckRef("ubuntu", &oci.RefSpec{UniformRepositorySpec: docker, ArtSpec: oci.ArtSpec{Repository: "library/ubuntu"}}) - CheckRef("ubuntu:v1", &oci.RefSpec{UniformRepositorySpec: docker, ArtSpec: oci.ArtSpec{Repository: "library/ubuntu", Tag: &tag}}) + CheckRef("ubuntu:v1", &oci.RefSpec{UniformRepositorySpec: docker, ArtSpec: oci.ArtSpec{Repository: "library/ubuntu", ArtVersion: oci.ArtVersion{Tag: &tag}}}) CheckRef("test/ubuntu", &oci.RefSpec{UniformRepositorySpec: docker, ArtSpec: oci.ArtSpec{Repository: "test/ubuntu"}}) CheckRef("test_test/ubuntu", &oci.RefSpec{UniformRepositorySpec: docker, ArtSpec: oci.ArtSpec{Repository: "test_test/ubuntu"}}) CheckRef("test__test/ubuntu", &oci.RefSpec{UniformRepositorySpec: docker, ArtSpec: oci.ArtSpec{Repository: "test__test/ubuntu"}}) CheckRef("test-test/ubuntu", &oci.RefSpec{UniformRepositorySpec: docker, ArtSpec: oci.ArtSpec{Repository: "test-test/ubuntu"}}) CheckRef("test--test/ubuntu", &oci.RefSpec{UniformRepositorySpec: docker, ArtSpec: oci.ArtSpec{Repository: "test--test/ubuntu"}}) CheckRef("test-----test/ubuntu", &oci.RefSpec{UniformRepositorySpec: docker, ArtSpec: oci.ArtSpec{Repository: "test-----test/ubuntu"}}) - CheckRef("test/ubuntu:v1", &oci.RefSpec{UniformRepositorySpec: docker, ArtSpec: oci.ArtSpec{Repository: "test/ubuntu", Tag: &tag}}) + CheckRef("test/ubuntu:v1", &oci.RefSpec{UniformRepositorySpec: docker, ArtSpec: oci.ArtSpec{Repository: "test/ubuntu", ArtVersion: oci.ArtVersion{Tag: &tag}}}) CheckRef("ghcr.io/test/ubuntu", &oci.RefSpec{UniformRepositorySpec: ghcr, ArtSpec: oci.ArtSpec{Repository: "test/ubuntu"}}) CheckRef("ghcr.io/test", &oci.RefSpec{UniformRepositorySpec: ghcr, ArtSpec: oci.ArtSpec{Repository: "test"}}) CheckRef("ghcr.io:8080/test/ubuntu", &oci.RefSpec{UniformRepositorySpec: oci.UniformRepositorySpec{Host: "ghcr.io:8080"}, ArtSpec: oci.ArtSpec{Repository: "test/ubuntu"}}) - CheckRef("ghcr.io/test/ubuntu:v1", &oci.RefSpec{UniformRepositorySpec: ghcr, ArtSpec: oci.ArtSpec{Repository: "test/ubuntu", Tag: &tag}}) - CheckRef("ghcr.io/test/ubuntu@sha256:3d05e105e350edf5be64fe356f4906dd3f9bf442a279e4142db9879bba8e677a", &oci.RefSpec{UniformRepositorySpec: ghcr, ArtSpec: oci.ArtSpec{Repository: "test/ubuntu", Digest: &digest}}) - CheckRef("ghcr.io/test/ubuntu:v1@sha256:3d05e105e350edf5be64fe356f4906dd3f9bf442a279e4142db9879bba8e677a", &oci.RefSpec{UniformRepositorySpec: ghcr, ArtSpec: oci.ArtSpec{Repository: "test/ubuntu", Tag: &tag, Digest: &digest}}) + CheckRef("ghcr.io/test/ubuntu:v1", &oci.RefSpec{UniformRepositorySpec: ghcr, ArtSpec: oci.ArtSpec{Repository: "test/ubuntu", ArtVersion: oci.ArtVersion{Tag: &tag}}}) + CheckRef("ghcr.io/test/ubuntu@sha256:3d05e105e350edf5be64fe356f4906dd3f9bf442a279e4142db9879bba8e677a", &oci.RefSpec{UniformRepositorySpec: ghcr, ArtSpec: oci.ArtSpec{Repository: "test/ubuntu", ArtVersion: oci.ArtVersion{Digest: &digest}}}) + CheckRef("ghcr.io/test/ubuntu:v1@sha256:3d05e105e350edf5be64fe356f4906dd3f9bf442a279e4142db9879bba8e677a", &oci.RefSpec{UniformRepositorySpec: ghcr, ArtSpec: oci.ArtSpec{Repository: "test/ubuntu", ArtVersion: oci.ArtVersion{Tag: &tag, Digest: &digest}}}) CheckRef("test___test/ubuntu", &oci.RefSpec{ UniformRepositorySpec: oci.UniformRepositorySpec{ Info: "test___test/ubuntu", @@ -594,8 +606,10 @@ var _ = Describe("ref parsing", func() { }, ArtSpec: oci.ArtSpec{ Repository: "repo/repo", - Tag: &tag, - Digest: &digest, + ArtVersion: oci.ArtVersion{ + Tag: &tag, + Digest: &digest, + }, }, }) CheckRef("http://127.0.0.1:443/repo/repo:v1@"+digest.String(), &oci.RefSpec{ @@ -607,8 +621,10 @@ var _ = Describe("ref parsing", func() { }, ArtSpec: oci.ArtSpec{ Repository: "repo/repo", - Tag: &tag, - Digest: &digest, + ArtVersion: oci.ArtVersion{ + Tag: &tag, + Digest: &digest, + }, }, }) CheckRef("directory::a/b", &oci.RefSpec{ @@ -695,7 +711,7 @@ var _ = Describe("ref parsing", func() { }, ArtSpec: oci.ArtSpec{ Repository: "mandelsoft/test", - Tag: &tag, + ArtVersion: oci.ArtVersion{Tag: &tag}, }, }) CheckRef("/tmp/ctf", &oci.RefSpec{ @@ -722,7 +738,7 @@ var _ = Describe("ref parsing", func() { }, ArtSpec: oci.ArtSpec{ Repository: "repo", - Tag: &tag, + ArtVersion: oci.ArtVersion{Tag: &tag}, }, }) ref := Must(oci.ParseRef("OCIRegistry::{\"type\":\"OCIRegistry\", \"baseUrl\": \"test.com\"}//repo:1.0.0")) @@ -745,7 +761,7 @@ var _ = Describe("ref parsing", func() { }, ArtSpec: oci.ArtSpec{ Repository: "repo", - Tag: &tag, + ArtVersion: oci.ArtVersion{Tag: &tag}, }, }) ref := Must(oci.ParseRef("oci::{\"type\":\"OCIRegistry\", \"baseUrl\": \"test.com\"}//repo:1.0.0")) @@ -826,4 +842,33 @@ var _ = Describe("ref parsing", func() { spec := Must(ctx.MapUniformRepositorySpec(&ref)) Expect(spec).To(Equal(Must(ctf.NewRepositorySpec(accessobj.ACC_WRITABLE, "./file/path")))) }) + + Context("version", func() { + It("parses tag", func() { + v := Must(oci.ParseVersion("tag")) + + Expect(v).To(Equal(&oci.ArtVersion{ + Tag: generics.Pointer("tag"), + Digest: nil, + })) + }) + + It("parses digest", func() { + v := Must(oci.ParseVersion("@sha256:3d05e105e350edf5be64fe356f4906dd3f9bf442a279e4142db9879bba8e677a")) + + Expect(v).To(Equal(&oci.ArtVersion{ + Tag: nil, + Digest: generics.Pointer(godigest.Digest("sha256:3d05e105e350edf5be64fe356f4906dd3f9bf442a279e4142db9879bba8e677a")), + })) + }) + + It("parses tag+digest", func() { + v := Must(oci.ParseVersion("tag@sha256:3d05e105e350edf5be64fe356f4906dd3f9bf442a279e4142db9879bba8e677a")) + + Expect(v).To(Equal(&oci.ArtVersion{ + Tag: generics.Pointer("tag"), + Digest: generics.Pointer(godigest.Digest("sha256:3d05e105e350edf5be64fe356f4906dd3f9bf442a279e4142db9879bba8e677a")), + })) + }) + }) }) diff --git a/api/oci/testhelper/manifests.go b/api/oci/testhelper/manifests.go index 824ab871fd..a6506db65a 100644 --- a/api/oci/testhelper/manifests.go +++ b/api/oci/testhelper/manifests.go @@ -65,6 +65,7 @@ func OCIArtifactResource1(env *builder.Builder, name string, host string, funcs const ( D_OCIMANIFEST1 = "0c4abdb72cf59cb4b77f4aacb4775f9f546ebc3face189b2224a966c8826ca9f" H_OCIARCHMANIFEST1 = "b0692bcec00e0a875b6b280f3209d6776f3eca128adcb7e81e82fd32127c0c62" + // H_OCIARCHMANIFEST1 = "818fb6a69a5f55e8b3dbc921a61fdd000b9445a745b587ba753a811b02426326". ) var DS_OCIMANIFEST1 = &metav1.DigestSpec{ @@ -124,6 +125,7 @@ func OCIManifest2For(env *builder.Builder, ns, tag string, nested ...func()) (*a const ( D_OCIMANIFEST2 = "c2d2dca275c33c1270dea6168a002d67c0e98780d7a54960758139ae19984bd7" H_OCIARCHMANIFEST2 = "cb85cd58b10e36343971691abbfe40200cb645c6e95f0bdabd111a30cf794708" + // H_OCIARCHMANIFEST2 = "2aaf6f8857dcbfa04a72fb98dd53f649b46e5d81aa4fb17330df74b0ffc30839". ) func HashManifest2(fmt string) string { diff --git a/api/ocm/add_test.go b/api/ocm/add_test.go index 9d27a6c0de..1c33309f7d 100644 --- a/api/ocm/add_test.go +++ b/api/ocm/add_test.go @@ -179,13 +179,13 @@ var _ = Describe("add resources", func() { Context("references", func() { It("adds reference", func() { ref := ocm.NewComponentReference("test", COMPONENT+"/sub", "v1") - MustBeSuccessful(cv.SetReference(ref)) + MustBeSuccessful(cv.SetReference(ref, ocm.ModifyElement())) Expect(len(cv.GetDescriptor().References)).To(Equal(1)) }) It("replaces reference", func() { ref := ocm.NewComponentReference("test", COMPONENT+"/sub", "v1") - MustBeSuccessful(cv.SetReference(ref)) + MustBeSuccessful(cv.SetReference(ref, ocm.ModifyElement())) MustBeSuccessful(cv.SetReference(ref.WithVersion("v1"))) Expect(len(Must(cv.SelectReferences(selectors.Name("test"))))).To(Equal(1)) @@ -193,7 +193,7 @@ var _ = Describe("add resources", func() { It("replaces source (enforced)", func() { ref := ocm.NewComponentReference("test", COMPONENT+"/sub", "v1") - MustBeSuccessful(cv.SetReference(ref)) + MustBeSuccessful(cv.SetReference(ref, ocm.ModifyElement())) MustBeSuccessful(cv.SetReference(ref.WithVersion("v2"))) Expect(len(Must(cv.SelectReferences(selectors.Name("test"))))).To(Equal(1)) @@ -201,7 +201,7 @@ var _ = Describe("add resources", func() { It("fails replace non-existent source)", func() { ref := ocm.NewComponentReference("test", COMPONENT+"/sub", "v1") - MustBeSuccessful(cv.SetReference(ref)) + MustBeSuccessful(cv.SetReference(ref, ocm.ModifyElement())) Expect(cv.SetReference(ref.WithExtraIdentity("attr", "value"), ocm.UpdateElement)).To( MatchError("element \"attr\"=\"value\",\"name\"=\"test\" not found")) @@ -209,21 +209,21 @@ var _ = Describe("add resources", func() { It("adds duplicate reference with different version", func() { ref := ocm.NewComponentReference("test", COMPONENT+"/sub", "v1") - MustBeSuccessful(cv.SetReference(ref)) + MustBeSuccessful(cv.SetReference(ref, ocm.ModifyElement())) MustBeSuccessful(cv.SetReference(ref.WithVersion("v2"), ocm.AppendElement)) Expect(len(Must(cv.SelectReferences(selectors.Name("test"))))).To(Equal(2)) }) It("rejects duplicate reference with same version", func() { ref := ocm.NewComponentReference("test", COMPONENT+"/sub", "v1") - MustBeSuccessful(cv.SetReference(ref)) + MustBeSuccessful(cv.SetReference(ref, ocm.ModifyElement())) Expect(cv.SetReference(ref.WithVersion("v1"), ocm.AppendElement)). To(MatchError("adding a new reference with same base identity requires different version")) }) It("rejects duplicate reference with extra identity", func() { ref := ocm.NewComponentReference("test", COMPONENT+"/sub", "v1").WithExtraIdentity("attr", "value") - MustBeSuccessful(cv.SetReference(ref)) + MustBeSuccessful(cv.SetReference(ref, ocm.ModifyElement())) Expect(cv.SetReference(ref, ocm.AppendElement)). To(MatchError("adding a new reference with same base identity requires different version")) }) diff --git a/api/ocm/compdesc/componentdescriptor.go b/api/ocm/compdesc/componentdescriptor.go index 02feb8d26c..8638dfdb7c 100644 --- a/api/ocm/compdesc/componentdescriptor.go +++ b/api/ocm/compdesc/componentdescriptor.go @@ -238,18 +238,21 @@ func (o *ElementMeta) GetIdentity(accessor ElementListAccessor) metav1.Identity identity = metav1.Identity{} } identity[SystemIdentityName] = o.Name - if accessor != nil { + if identity.Get(SystemIdentityVersion) == "" && accessor != nil { found := false l := accessor.Len() for i := 0; i < l; i++ { m := accessor.Get(i).GetMeta() - if m.GetName() == o.Name && m.GetExtraIdentity().Equals(o.ExtraIdentity) { - if found { - identity[SystemIdentityVersion] = o.Version - - break + if m.GetName() == o.Name { + mid := m.GetExtraIdentity() + mid.Remove(SystemIdentityVersion) + if mid.Equals(o.ExtraIdentity) { + if found { + identity[SystemIdentityVersion] = o.Version + break + } + found = true } - found = true } } } diff --git a/api/ocm/compdesc/default.go b/api/ocm/compdesc/default.go index af504034f2..22c3359152 100644 --- a/api/ocm/compdesc/default.go +++ b/api/ocm/compdesc/default.go @@ -27,9 +27,16 @@ func DefaultComponent(component *ComponentDescriptor) *ComponentDescriptor { return component } +func DefaultElements(component *ComponentDescriptor) { + DefaultResources(component) + DefaultSources(component) + DefaultReferences(component) +} + // DefaultResources defaults a list of resources. // The version of the component is defaulted for local resources that do not contain a version. // adds the version as identity if the resource identity would clash otherwise. +// The version is added to an extraIdentity, if it is not unique without it. func DefaultResources(component *ComponentDescriptor) { for i, res := range component.Resources { if res.Relation == v1.LocalRelation && len(res.Version) == 0 { @@ -39,7 +46,45 @@ func DefaultResources(component *ComponentDescriptor) { id := res.GetIdentity(component.Resources) if v, ok := id[SystemIdentityVersion]; ok { if res.ExtraIdentity == nil { - res.ExtraIdentity = v1.Identity{ + component.Resources[i].ExtraIdentity = v1.Identity{ + SystemIdentityVersion: v, + } + } else { + if _, ok := res.ExtraIdentity[SystemIdentityVersion]; !ok { + res.ExtraIdentity[SystemIdentityVersion] = v + } + } + } + } +} + +// DefaultSources defaults a list of sources. +// The version is added to an extraIdentity, if it is not unique without it. +func DefaultSources(component *ComponentDescriptor) { + for i, res := range component.Sources { + id := res.GetIdentity(component.Resources) + if v, ok := id[SystemIdentityVersion]; ok { + if res.ExtraIdentity == nil { + component.Sources[i].ExtraIdentity = v1.Identity{ + SystemIdentityVersion: v, + } + } else { + if _, ok := res.ExtraIdentity[SystemIdentityVersion]; !ok { + res.ExtraIdentity[SystemIdentityVersion] = v + } + } + } + } +} + +// DefaultReferences defaults a list of references. +// The version is added to an extraIdentity, if it is not unique without it. +func DefaultReferences(component *ComponentDescriptor) { + for i, res := range component.References { + id := res.GetIdentity(component.Resources) + if v, ok := id[SystemIdentityVersion]; ok { + if res.ExtraIdentity == nil { + component.References[i].ExtraIdentity = v1.Identity{ SystemIdentityVersion: v, } } else { diff --git a/api/ocm/compdesc/versions/ocm.software/v3alpha1/default.go b/api/ocm/compdesc/versions/ocm.software/v3alpha1/default.go index 0a03ce49ab..979c2b09ea 100644 --- a/api/ocm/compdesc/versions/ocm.software/v3alpha1/default.go +++ b/api/ocm/compdesc/versions/ocm.software/v3alpha1/default.go @@ -1,7 +1,6 @@ package v3alpha1 import ( - v1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" "ocm.software/ocm/api/utils/runtime" ) @@ -20,30 +19,5 @@ func (cd *ComponentDescriptor) Default() error { cd.Spec.Resources = make([]Resource, 0) } - DefaultResources(cd) return nil } - -// DefaultResources defaults a list of resources. -// The version of the component is defaulted for local resources that do not contain a version. -// adds the version as identity if the resource identity would clash otherwise. -func DefaultResources(component *ComponentDescriptor) { - for i, res := range component.Spec.Resources { - if res.Relation == v1.LocalRelation && len(res.Version) == 0 { - component.Spec.Resources[i].Version = component.GetVersion() - } - - id := res.GetIdentity(component.Spec.Resources) - if v, ok := id[SystemIdentityVersion]; ok { - if res.ExtraIdentity == nil { - res.ExtraIdentity = v1.Identity{ - SystemIdentityVersion: v, - } - } else { - if _, ok := res.ExtraIdentity[SystemIdentityVersion]; !ok { - res.ExtraIdentity[SystemIdentityVersion] = v - } - } - } - } -} diff --git a/api/ocm/compdesc/versions/v2/default.go b/api/ocm/compdesc/versions/v2/default.go index ce4aec2201..08ea24a1a3 100644 --- a/api/ocm/compdesc/versions/v2/default.go +++ b/api/ocm/compdesc/versions/v2/default.go @@ -1,7 +1,6 @@ package v2 import ( - v1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" "ocm.software/ocm/api/utils/runtime" ) @@ -20,30 +19,5 @@ func (cd *ComponentDescriptor) Default() error { cd.Resources = make([]Resource, 0) } - DefaultResources(cd) return nil } - -// DefaultResources defaults a list of resources. -// The version of the component is defaulted for local resources that do not contain a version. -// adds the version as identity if the resource identity would clash otherwise. -func DefaultResources(component *ComponentDescriptor) { - for i, res := range component.Resources { - if res.Relation == v1.LocalRelation && len(res.Version) == 0 { - component.Resources[i].Version = component.GetVersion() - } - - id := res.GetIdentity(component.Resources) - if v, ok := id[SystemIdentityVersion]; ok { - if res.ExtraIdentity == nil { - res.ExtraIdentity = v1.Identity{ - SystemIdentityVersion: v, - } - } else { - if _, ok := res.ExtraIdentity[SystemIdentityVersion]; !ok { - res.ExtraIdentity[SystemIdentityVersion] = v - } - } - } - } -} diff --git a/api/ocm/cpi/dummy.go b/api/ocm/cpi/dummy.go index 7181a35c05..fa45b54aa4 100644 --- a/api/ocm/cpi/dummy.go +++ b/api/ocm/cpi/dummy.go @@ -164,19 +164,19 @@ func (d *DummyComponentVersionAccess) SetResourceByAccess(art ResourceAccess, mo return errors.ErrNotSupported("resource modification") } -func (d *DummyComponentVersionAccess) SetSourceBlob(meta *SourceMeta, blob BlobAccess, refname string, global AccessSpec, opts ...TargetOption) error { +func (d *DummyComponentVersionAccess) SetSourceBlob(meta *SourceMeta, blob BlobAccess, refname string, global AccessSpec, opts ...TargetElementOption) error { return errors.ErrNotSupported("source modification") } -func (d *DummyComponentVersionAccess) SetSource(meta *SourceMeta, spec compdesc.AccessSpec, opts ...TargetOption) error { +func (d *DummyComponentVersionAccess) SetSource(meta *SourceMeta, spec compdesc.AccessSpec, opts ...TargetElementOption) error { return errors.ErrNotSupported("source modification") } -func (d *DummyComponentVersionAccess) SetSourceByAccess(art SourceAccess, opts ...TargetOption) error { +func (d *DummyComponentVersionAccess) SetSourceByAccess(art SourceAccess, opts ...TargetElementOption) error { return errors.ErrNotSupported() } -func (d *DummyComponentVersionAccess) SetReference(ref *ComponentReference, opts ...TargetOption) error { +func (d *DummyComponentVersionAccess) SetReference(ref *ComponentReference, opts ...ElementModificationOption) error { return errors.ErrNotSupported() } diff --git a/api/ocm/cpi/modopts.go b/api/ocm/cpi/modopts.go index 1666c0355b..ebdfeab00d 100644 --- a/api/ocm/cpi/modopts.go +++ b/api/ocm/cpi/modopts.go @@ -9,9 +9,12 @@ import ( ) type ( - TargetElement = internal.TargetElement - TargetOption = internal.TargetOption - TargetOptions = internal.TargetOptions + TargetElement = internal.TargetElement + TargetElementOption = internal.TargetElementOption + TargetElementOptions = internal.TargetElementOptions + + ElementModificationOption = internal.ElementModificationOption + ElementModificationOptions = internal.ElementModificationOptions ModificationOption = internal.ModificationOption ModificationOptions = internal.ModificationOptions @@ -26,8 +29,8 @@ type ( AddVersionOptions = internal.AddVersionOptions ) -func NewTargetOptions(list ...TargetOption) *TargetOptions { - var m TargetOptions +func NewTargetElementOptions(list ...TargetElementOption) *TargetElementOptions { + var m TargetElementOptions m.ApplyTargetOptions(list...) return &m } @@ -65,6 +68,10 @@ func NewModificationOptions(list ...ModificationOption) *ModificationOptions { return internal.NewModificationOptions(list...) } +func NewElementModificationOptions(list ...ElementModificationOption) *ElementModificationOptions { + return internal.NewElementModificationOptions(list...) +} + func TargetIndex(idx int) internal.TargetIndex { return internal.TargetIndex(-1) } @@ -77,10 +84,15 @@ func TargetIdentity(id v1.Identity) internal.TargetIdentity { return internal.TargetIdentity(id) } +// Deprecated: use ModifyElement. func ModifyResource(flag ...bool) internal.ModOptionImpl { return internal.ModifyResource(flag...) } +func ModifyElement(flag ...bool) internal.ElemModOptionImpl { + return internal.ModifyElement(flag...) +} + func AcceptExistentDigests(flag ...bool) internal.ModOptionImpl { return internal.AcceptExistentDigests(flag...) } diff --git a/api/ocm/cpi/repocpi/view_cv.go b/api/ocm/cpi/repocpi/view_cv.go index 98e43e2aa6..2c36d65db8 100644 --- a/api/ocm/cpi/repocpi/view_cv.go +++ b/api/ocm/cpi/repocpi/view_cv.go @@ -249,7 +249,7 @@ func (c *componentVersionAccessView) SetResourceBlob(meta *cpi.ResourceMeta, blo return fmt.Errorf("unable to add blob (component %s:%s resource %s): %w", c.GetName(), c.GetVersion(), meta.GetName(), err) } - if err := c.SetResource(meta, acc, eff, cpi.ModifyResource()); err != nil { + if err := c.SetResource(meta, acc, eff, cpi.ModifyElement()); err != nil { return fmt.Errorf("unable to set resource: %w", err) } @@ -264,7 +264,7 @@ func (c *componentVersionAccessView) AdjustSourceAccess(meta *cpi.SourceMeta, ac return errors.ErrUnknown(cpi.KIND_RESOURCE, meta.GetIdentity(cd.Resources).String()) } -func (c *componentVersionAccessView) SetSourceBlob(meta *cpi.SourceMeta, blob cpi.BlobAccess, refName string, global cpi.AccessSpec, modopts ...cpi.TargetOption) error { +func (c *componentVersionAccessView) SetSourceBlob(meta *cpi.SourceMeta, blob cpi.BlobAccess, refName string, global cpi.AccessSpec, modopts ...cpi.TargetElementOption) error { cpi.Logger(c).Debug("adding source blob", "source", meta.Name) if err := utils.ValidateObject(blob); err != nil { return err @@ -384,7 +384,7 @@ func (c *componentVersionAccessView) SetResource(meta *cpi.ResourceMeta, acc com cd := c.bridge.GetDescriptor() - idx, err := c.getElementIndex("resource", cd.Resources, res, &opts.TargetOptions) + idx, err := c.getElementIndex("resource", cd.Resources, res, &opts.TargetElementOptions) if err != nil { return err } @@ -393,7 +393,7 @@ func (c *componentVersionAccessView) SetResource(meta *cpi.ResourceMeta, acc com } if old == nil { - if !opts.IsModifyResource() && c.bridge.IsPersistent() { + if !opts.IsModifyElement() && c.bridge.IsPersistent() { return fmt.Errorf("new resource would invalidate signature") } } @@ -456,7 +456,7 @@ func (c *componentVersionAccessView) SetResource(meta *cpi.ResourceMeta, acc com if old != nil { eq := res.Equivalent(old) if !eq.IsLocalHashEqual() && c.bridge.IsPersistent() { - if !opts.IsModifyResource() { + if !opts.IsModifyElement() { return fmt.Errorf("resource would invalidate signature") } cd.Signatures = nil @@ -468,6 +468,10 @@ func (c *componentVersionAccessView) SetResource(meta *cpi.ResourceMeta, acc com } else { cd.Resources[idx] = *res } + if opts.IsModifyElement() { + // default handling for completing an extra identity for modifications, only. + compdesc.DefaultResources(cd) + } return c.bridge.Update(false) }) } @@ -499,7 +503,7 @@ func (c *componentVersionAccessView) evaluateResourceDigest(res, old *compdesc.R if !old.Digest.IsNone() { digester.HashAlgorithm = old.Digest.HashAlgorithm digester.NormalizationAlgorithm = old.Digest.NormalisationAlgorithm - if opts.IsAcceptExistentDigests() && !opts.IsModifyResource() && c.bridge.IsPersistent() { + if opts.IsAcceptExistentDigests() && !opts.IsModifyElement() && c.bridge.IsPersistent() { res.Digest = old.Digest value = old.Digest.Value } @@ -508,7 +512,7 @@ func (c *componentVersionAccessView) evaluateResourceDigest(res, old *compdesc.R return hashAlgo, digester, value } -func (c *componentVersionAccessView) SetSourceByAccess(art cpi.SourceAccess, optslist ...cpi.TargetOption) error { +func (c *componentVersionAccessView) SetSourceByAccess(art cpi.SourceAccess, optslist ...cpi.TargetElementOption) error { return setAccess(c, "source", art, func(meta *cpi.SourceMeta, acc compdesc.AccessSpec) error { return c.SetSource(meta, acc, optslist...) @@ -518,7 +522,7 @@ func (c *componentVersionAccessView) SetSourceByAccess(art cpi.SourceAccess, opt }) } -func (c *componentVersionAccessView) SetSource(meta *cpi.SourceMeta, acc compdesc.AccessSpec, optlist ...cpi.TargetOption) error { +func (c *componentVersionAccessView) SetSource(meta *cpi.SourceMeta, acc compdesc.AccessSpec, optlist ...cpi.TargetElementOption) error { if c.bridge.IsReadOnly() { return accessio.ErrReadOnly } @@ -544,33 +548,51 @@ func (c *componentVersionAccessView) SetSource(meta *cpi.SourceMeta, acc compdes } else { cd.Sources[idx] = *res } + compdesc.DefaultSources(cd) return c.bridge.Update(false) }) } -func (c *componentVersionAccessView) SetReference(ref *cpi.ComponentReference, optlist ...cpi.TargetOption) error { +func (c *componentVersionAccessView) SetReference(ref *cpi.ComponentReference, optlist ...cpi.ElementModificationOption) error { + opts := cpi.NewElementModificationOptions(optlist...) + moddef := false + return c.Execute(func() error { cd := c.bridge.GetDescriptor() if ref.Version == "" { return fmt.Errorf("version required for component version reference") } - idx, err := c.getElementIndex("reference", cd.References, ref, optlist...) + idx, err := c.getElementIndex("reference", cd.References, ref, &opts.TargetElementOptions) if err != nil { return err } if idx < 0 { + if !opts.IsModifyElement(moddef) { + return fmt.Errorf("adding reference would invalidate signature") + } cd.References = append(cd.References, *ref) } else { + eq := ref.Equivalent(&cd.References[idx]) + if !eq.IsEquivalent() && c.bridge.IsPersistent() { + if !opts.IsModifyElement(moddef) { + return fmt.Errorf("reference would invalidate signature") + } + cd.Signatures = nil + } + cd.References[idx].Equivalent(ref) cd.References[idx] = *ref } + if opts.IsModifyElement(moddef) { + compdesc.DefaultReferences(cd) + } return c.bridge.Update(false) }) } -func (c *componentVersionAccessView) getElementIndex(kind string, acc compdesc.ElementListAccessor, prov compdesc.ElementMetaProvider, optlist ...cpi.TargetOption) (int, error) { - opts := internal.NewTargetOptions(optlist...) +func (c *componentVersionAccessView) getElementIndex(kind string, acc compdesc.ElementListAccessor, prov compdesc.ElementMetaProvider, optlist ...cpi.TargetElementOption) (int, error) { + opts := internal.NewTargetElementOptions(optlist...) curidx := compdesc.ElementIndex(acc, prov) meta := prov.GetMeta() var idx int diff --git a/api/ocm/extensions/accessmethods/ociartifact/method.go b/api/ocm/extensions/accessmethods/ociartifact/method.go index b31ca9d611..38fef2b5ef 100644 --- a/api/ocm/extensions/accessmethods/ociartifact/method.go +++ b/api/ocm/extensions/accessmethods/ociartifact/method.go @@ -227,7 +227,7 @@ func (m *accessMethod) eval(relto oci.Repository) error { ocictx := m.ctx.OCIContext() spec := ocictx.GetAlias(ref.Host) if spec == nil { - spec = ocireg.NewRepositorySpec(ref.Host) + spec = ocireg.NewRepositorySpec(ref.RepositoryRef()) } repo, err := ocictx.RepositoryForSpec(spec) if err != nil { @@ -247,7 +247,7 @@ func (m *accessMethod) eval(relto oci.Repository) error { } ref = oci.RefSpec{ UniformRepositorySpec: *repo.GetSpecification().UniformRepositorySpec(), - ArtSpec: art, + ArtSpec: *art, } m.repo = repo } @@ -355,7 +355,7 @@ func (m *accessMethod) getBlob() (artifactset.ArtifactBlob, error) { } logger := Logger(WrapContextProvider(m.ctx)) logger.Info("synthesize artifact blob", "ref", m.reference) - m.blob, err = artifactset.SynthesizeArtifactBlobForArtifact(m.art, m.ref.Version()) + m.blob, err = artifactset.SynthesizeArtifactBlobForArtifact(m.art, m.ref.VersionSpec()) logger.Info("synthesize artifact blob done", "ref", m.reference, "error", logging.ErrorMessage(err)) if err != nil { m.err = err diff --git a/api/ocm/extensions/attrs/ociuploadattr/attr.go b/api/ocm/extensions/attrs/ociuploadattr/attr.go index eb3a07e318..529c1472f2 100644 --- a/api/ocm/extensions/attrs/ociuploadattr/attr.go +++ b/api/ocm/extensions/attrs/ociuploadattr/attr.go @@ -55,7 +55,7 @@ func (a AttributeType) Decode(data []byte, unmarshaller runtime.Unmarshaler) (in return &value, nil } if value.Ref == "" { - return nil, errors.ErrInvalidWrap(errors.Newf("missing repository or ref"), oci.KIND_OCI_REFERENCE, string(data)) + return nil, errors.ErrInvalidWrap(errors.Newf("missing repository or ociRef"), oci.KIND_OCI_REFERENCE, string(data)) } data = []byte(value.Ref) } diff --git a/api/ocm/extensions/blobhandler/config/type.go b/api/ocm/extensions/blobhandler/config/type.go index 7d36e228ba..01a2054412 100644 --- a/api/ocm/extensions/blobhandler/config/type.go +++ b/api/ocm/extensions/blobhandler/config/type.go @@ -64,7 +64,13 @@ func (a *Config) ApplyTo(ctx cfgcpi.Context, target interface{}) error { } reg := blobhandler.For(t) for _, h := range a.Registrations { - accepted, err := reg.RegisterByName(h.Name, t, h.Config, &h.HandlerOptions) + opts := h.HandlerOptions + if opts.Priority == 0 { + // config objects have higher prio than builtin defaults + // CLI options get even higher prio. + opts.Priority = blobhandler.DEFAULT_BLOBHANDLER_PRIO * 2 + } + accepted, err := reg.RegisterByName(h.Name, t, h.Config, &opts) if err != nil { return errors.Wrapf(err, "registering upload handler %q[%s]", h.Name, h.Description) } @@ -75,14 +81,15 @@ func (a *Config) ApplyTo(ctx cfgcpi.Context, target interface{}) error { return nil } -const usage = ` +var usage = ` The config type ` + ConfigType + ` can be used to define a list -of preconfigured upload handler registrations (see ocm ocm-uploadhandlers): +of preconfigured upload handler registrations (see ocm ocm-uploadhandlers), +the default priority is ` + fmt.Sprintf("%d", download.DEFAULT_BLOBHANDLER_PRIO*2) + `:
     type: ` + ConfigType + `
     description: "my standard upload handler configuration"
-    handlers:
+    registrations:
       - name: oci/artifact
         artifactType: ociImage
         config:
diff --git a/api/ocm/extensions/blobhandler/handlers/oci/ocirepo/handler_test.go b/api/ocm/extensions/blobhandler/handlers/oci/ocirepo/handler_test.go
index 66fec9df12..6fc5155f25 100644
--- a/api/ocm/extensions/blobhandler/handlers/oci/ocirepo/handler_test.go
+++ b/api/ocm/extensions/blobhandler/handlers/oci/ocirepo/handler_test.go
@@ -125,7 +125,7 @@ var _ = Describe("oci artifact transfer", func() {
 			data := Must(json.Marshal(comp.GetDescriptor().Resources[1].Access))
 
 			fmt.Printf("%s\n", string(data))
-			Expect(string(data)).To(StringEqualWithContext(`{"globalAccess":{"imageReference":"baseurl.io/ocm/value:v2.0@sha256:` + D_OCIMANIFEST1 + `","type":"ociArtifact"},"localReference":"sha256:b0692bcec00e0a875b6b280f3209d6776f3eca128adcb7e81e82fd32127c0c62","mediaType":"application/vnd.oci.image.manifest.v1+tar+gzip","referenceName":"ocm/value:v2.0","type":"localBlob"}`))
+			Expect(string(data)).To(StringEqualWithContext(`{"globalAccess":{"imageReference":"baseurl.io/ocm/value:v2.0@sha256:` + D_OCIMANIFEST1 + `","type":"ociArtifact"},"localReference":"sha256:` + H_OCIARCHMANIFEST1 + `","mediaType":"application/vnd.oci.image.manifest.v1+tar+gzip","referenceName":"ocm/value:v2.0","type":"localBlob"}`))
 			ocirepo := genericocireg.GetOCIRepository(tgt)
 			Expect(ocirepo).NotTo(BeNil())
 
diff --git a/api/ocm/extensions/blobhandler/registration.go b/api/ocm/extensions/blobhandler/registration.go
index 83a1763517..308a06765c 100644
--- a/api/ocm/extensions/blobhandler/registration.go
+++ b/api/ocm/extensions/blobhandler/registration.go
@@ -4,8 +4,11 @@ import (
 	"fmt"
 
 	"ocm.software/ocm/api/ocm/cpi"
+	"ocm.software/ocm/api/ocm/internal"
 )
 
+const DEFAULT_BLOBHANDLER_PRIO = internal.DEFAULT_BLOBHANDLER_PRIO
+
 func RegisterHandlerByName(ctx cpi.ContextProvider, name string, config HandlerConfig, opts ...HandlerOption) error {
 	o, err := For(ctx).RegisterByName(name, ctx.OCMContext(), config, opts...)
 	if err != nil {
diff --git a/api/ocm/extensions/download/config/registration_test.go b/api/ocm/extensions/download/config/registration_test.go
new file mode 100644
index 0000000000..f68f6efb41
--- /dev/null
+++ b/api/ocm/extensions/download/config/registration_test.go
@@ -0,0 +1,39 @@
+package config_test
+
+import (
+	"encoding/json"
+
+	. "github.com/mandelsoft/goutils/testutils"
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+
+	"ocm.software/ocm/api/ocm"
+	"ocm.software/ocm/api/ocm/extensions/download"
+	me "ocm.software/ocm/api/ocm/extensions/download/config"
+	"ocm.software/ocm/api/ocm/ocmutils"
+	"ocm.software/ocm/api/tech/helm"
+)
+
+var _ = Describe("Download Handler regigistration", func() {
+	It("register by ocm config", func() {
+		ctx := ocm.New()
+
+		cfg := me.New()
+		cfg.AddRegistration(me.Registration{
+			Name:        "helm/artifact",
+			Description: "some registration",
+			HandlerOptions: download.HandlerOptions{
+				HandlerKey: download.HandlerKey{
+					ArtifactType: "someType",
+				},
+			},
+			Config: nil,
+		})
+
+		data := Must(json.Marshal(cfg))
+		ocmutils.ConfigureByData(ctx, data, "manual")
+
+		h := download.For(ctx).LookupHandler("someType", helm.ChartMediaType)
+		Expect(h.Len()).To(Equal(1))
+	})
+})
diff --git a/api/ocm/extensions/download/config/suite_test.go b/api/ocm/extensions/download/config/suite_test.go
new file mode 100644
index 0000000000..c580e9c0ec
--- /dev/null
+++ b/api/ocm/extensions/download/config/suite_test.go
@@ -0,0 +1,13 @@
+package config_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+func TestConfig(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "cdownload handler config Test Suite")
+}
diff --git a/api/ocm/extensions/download/config/type.go b/api/ocm/extensions/download/config/type.go
index 7f6aca2ce5..868fe46a28 100644
--- a/api/ocm/extensions/download/config/type.go
+++ b/api/ocm/extensions/download/config/type.go
@@ -63,7 +63,13 @@ func (a *Config) ApplyTo(ctx cfgcpi.Context, target interface{}) error {
 	}
 	reg := download.For(t)
 	for _, h := range a.Registrations {
-		accepted, err := reg.RegisterByName(h.Name, t, h.Config, &h.HandlerOptions)
+		opts := h.HandlerOptions
+		if opts.Priority == 0 {
+			// config objects have higher prio than builtin defaults
+			// CLI options get even higher prio.
+			opts.Priority = download.DEFAULT_BLOBHANDLER_PRIO * 2
+		}
+		accepted, err := reg.RegisterByName(h.Name, t, h.Config, &opts)
 		if err != nil {
 			return errors.Wrapf(err, "registering download handler %q[%s]", h.Name, h.Description)
 		}
@@ -74,17 +80,20 @@ func (a *Config) ApplyTo(ctx cfgcpi.Context, target interface{}) error {
 	return nil
 }
 
-const usage = `
+var usage = `
 The config type ` + ConfigType + ` can be used to define a list
-of preconfigured download handler registrations (see ocm ocm-downloadhandlers):
+of preconfigured download handler registrations (see ocm ocm-downloadhandlers),
+the default priority is ` + fmt.Sprintf("%d", download.DEFAULT_BLOBHANDLER_PRIO*2) + `:
 
 
     type: ` + ConfigType + `
     description: "my standard download handler configuration"
-    handlers:
+    registrations:
       - name: oci/artifact
         artifactType: ociImage
-        mimeType:
+        mimeType: ...
+        description: ...
+        priority: ...
         config: ...
       ...
 
diff --git a/api/ocm/extensions/download/handlers/ocirepo/handler.go b/api/ocm/extensions/download/handlers/ocirepo/handler.go index 269a9bb0ab..b6b8dcad66 100644 --- a/api/ocm/extensions/download/handlers/ocirepo/handler.go +++ b/api/ocm/extensions/download/handlers/ocirepo/handler.go @@ -78,7 +78,7 @@ func (h *handler) Download(p common.Printer, racc cpi.ResourceAccess, path strin ocictx := ctx.OCIContext() - var artspec oci.ArtSpec + var artspec *oci.ArtSpec var prefix string var result oci.RefSpec @@ -101,7 +101,7 @@ func (h *handler) Download(p common.Printer, racc cpi.ResourceAccess, path strin return true, "", err } finalize.Close(repo, "repository for downloading OCI artifact") - artspec = ref.ArtSpec + artspec = &ref.ArtSpec } else { log.Debug("evaluating config") if path != "" { @@ -117,16 +117,19 @@ func (h *handler) Download(p common.Printer, racc cpi.ResourceAccess, path strin } result.UniformRepositorySpec = *us } - log.Debug("using artifact spec", "spec", artspec.String()) - if artspec.Digest != nil { - return true, "", fmt.Errorf("digest not possible for target") - } - if artspec.Repository != "" { - namespace = artspec.Repository - } - if artspec.IsTagged() { - tag = *artspec.Tag + if artspec != nil { + log.Debug("using artifact spec", "spec", artspec.String()) + if artspec.IsDigested() { + return true, "", fmt.Errorf("digest not possible for target") + } + + if artspec.Repository != "" { + namespace = artspec.Repository + } + if artspec.IsTagged() { + tag = *artspec.Tag + } } if prefix != "" && namespace != "" { diff --git a/api/ocm/extensions/pubsub/providers/ocireg/provider.go b/api/ocm/extensions/pubsub/providers/ocireg/provider.go index d2e0e99b25..58c5b435b3 100644 --- a/api/ocm/extensions/pubsub/providers/ocireg/provider.go +++ b/api/ocm/extensions/pubsub/providers/ocireg/provider.go @@ -5,8 +5,8 @@ import ( "fmt" "path" + containererr "github.com/containerd/containerd/remotes/errors" "github.com/mandelsoft/goutils/errors" - "oras.land/oras-go/v2/registry/remote/errcode" "ocm.software/ocm/api/ocm/cpi" "ocm.software/ocm/api/ocm/cpi/repocpi" @@ -45,18 +45,10 @@ func (p *Provider) GetPubSubSpec(repo repocpi.Repository) (pubsub.PubSubSpec, er ocirepo := path.Join(gen.Meta().SubPath, componentmapping.ComponentDescriptorNamespace) acc, err := gen.OCIRepository().LookupArtifact(ocirepo, META) - - // Dirty workaround until fix is ready for https://github.com/open-component-model/ocm/issues/872 - errCode := errcode.Error{} - if errors.As(err, &errCode) { - if errCode.Code == errcode.ErrorCodeDenied { - return nil, nil - } - } - - if errors.IsErrNotFound(err) || errors.IsErrUnknown(err) { + if errors.IsErrNotFound(err) || errors.IsErrUnknown(err) || errors.IsA(err, containererr.ErrUnexpectedStatus{}) { return nil, nil } + if err != nil { return nil, errors.Wrapf(err, "cannot access meta data manifest version") } diff --git a/api/ocm/extensions/repositories/genericocireg/annotation_test.go b/api/ocm/extensions/repositories/genericocireg/annotation_test.go index 60ec0db94b..4d06e4fc61 100644 --- a/api/ocm/extensions/repositories/genericocireg/annotation_test.go +++ b/api/ocm/extensions/repositories/genericocireg/annotation_test.go @@ -5,6 +5,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "ocm.software/ocm/api/helper/builder" + "ocm.software/ocm/api/oci/extensions/repositories/ctf" metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes" diff --git a/api/ocm/internal/accesstypes.go b/api/ocm/internal/accesstypes.go index 707c223e64..42f87e6891 100644 --- a/api/ocm/internal/accesstypes.go +++ b/api/ocm/internal/accesstypes.go @@ -97,7 +97,7 @@ type AccessMethod interface { // AsBlobAccess maps a method object into a // basic blob access interface. // It does not provide a separate reference, - // closing the blob access with close the + // closing the blob access will close the // access method. AsBlobAccess() BlobAccess } diff --git a/api/ocm/internal/modopts.go b/api/ocm/internal/modopts.go index 41826374bc..3732c59434 100644 --- a/api/ocm/internal/modopts.go +++ b/api/ocm/internal/modopts.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/mandelsoft/goutils/general" + "github.com/mandelsoft/goutils/generics" "github.com/mandelsoft/goutils/optionutils" "ocm.software/ocm/api/ocm/compdesc" @@ -100,32 +101,36 @@ type TargetElement interface { } type TargetOptionImpl interface { - TargetOption + TargetElementOption ModificationOption BlobModificationOption } -type TargetOptions struct { +type TargetElementOptions struct { TargetElement TargetElement } -type TargetOption interface { - ApplyTargetOption(options *TargetOptions) +type TargetElementOption interface { + ApplyTargetOption(options *TargetElementOptions) } -func (m *TargetOptions) ApplyBlobModificationOption(opts *BlobModificationOptions) { - m.ApplyTargetOption(&opts.TargetOptions) +func (m *TargetElementOptions) ApplyBlobModificationOption(opts *BlobModificationOptions) { + m.ApplyTargetOption(&opts.TargetElementOptions) } -func (m *TargetOptions) ApplyModificationOption(opts *ModificationOptions) { - m.ApplyTargetOption(&opts.TargetOptions) +func (m *TargetElementOptions) ApplyModificationOption(opts *ModificationOptions) { + m.ApplyTargetOption(&opts.TargetElementOptions) } -func (m *TargetOptions) ApplyTargetOption(opts *TargetOptions) { +func (m *TargetElementOptions) ApplyElementModificationOption(opts *ElementModificationOptions) { + m.ApplyTargetOption(&opts.TargetElementOptions) +} + +func (m *TargetElementOptions) ApplyTargetOption(opts *TargetElementOptions) { optionutils.Transfer(&opts.TargetElement, m.TargetElement) } -func (m *TargetOptions) ApplyTargetOptions(list ...TargetOption) *TargetOptions { +func (m *TargetElementOptions) ApplyTargetOptions(list ...TargetElementOption) *TargetElementOptions { for _, o := range list { if o != nil { o.ApplyTargetOption(m) @@ -134,12 +139,55 @@ func (m *TargetOptions) ApplyTargetOptions(list ...TargetOption) *TargetOptions return m } -func NewTargetOptions(list ...TargetOption) *TargetOptions { - var m TargetOptions +func NewTargetElementOptions(list ...TargetElementOption) *TargetElementOptions { + var m TargetElementOptions m.ApplyTargetOptions(list...) return &m } +type ElementModificationOption interface { + ApplyElementModificationOption(opts *ElementModificationOptions) +} + +type ElementModificationOptions struct { + TargetElementOptions + + // ModifyElement disables the modification of signature relevant + // resource parts. + ModifyElement *bool +} + +func (m *ElementModificationOptions) ApplyBlobModificationOption(opts *BlobModificationOptions) { + m.ApplyElementModificationOption(&opts.ElementModificationOptions) +} + +func (m *ElementModificationOptions) ApplyModificationOption(opts *ModificationOptions) { + m.ApplyElementModificationOption(&opts.ElementModificationOptions) +} + +func (m *ElementModificationOptions) ApplyElementModificationOption(opts *ElementModificationOptions) { + optionutils.Transfer(&opts.ModifyElement, m.ModifyElement) +} + +func (m *ElementModificationOptions) ApplyElementModificationOptions(list ...ElementModificationOption) *ElementModificationOptions { + for _, o := range list { + if o != nil { + o.ApplyElementModificationOption(m) + } + } + return m +} + +func (m *ElementModificationOptions) IsModifyElement(def ...bool) bool { + return utils.AsBool(m.ModifyElement, def...) +} + +func NewElementModificationOptions(list ...ElementModificationOption) *ElementModificationOptions { + var m ElementModificationOptions + m.ApplyElementModificationOptions(list...) + return &m +} + type ModificationOption interface { ApplyModificationOption(opts *ModificationOptions) } @@ -149,12 +197,14 @@ type ModOptionImpl interface { BlobModificationOption } -type ModificationOptions struct { - TargetOptions +type ElemModOptionImpl interface { + ElementModificationOption + ModificationOption + BlobModificationOption +} - // ModifyResource disables the modification of signature releveant - // resource parts. - ModifyResource *bool +type ModificationOptions struct { + ElementModificationOptions // AcceptExistentDigests don't validate/recalculate the content digest // of resources. @@ -173,10 +223,6 @@ type ModificationOptions struct { SkipDigest *bool } -func (m *ModificationOptions) IsModifyResource() bool { - return utils.AsBool(m.ModifyResource) -} - func (m *ModificationOptions) IsAcceptExistentDigests() bool { return utils.AsBool(m.AcceptExistentDigests) } @@ -203,8 +249,8 @@ func (m *ModificationOptions) ApplyBlobModificationOption(opts *BlobModification } func (m *ModificationOptions) ApplyModificationOption(opts *ModificationOptions) { - m.TargetOptions.ApplyTargetOption(&opts.TargetOptions) - optionutils.Transfer(&opts.ModifyResource, m.ModifyResource) + m.TargetElementOptions.ApplyTargetOption(&opts.TargetElementOptions) + optionutils.Transfer(&opts.ModifyElement, m.ModifyElement) optionutils.Transfer(&opts.AcceptExistentDigests, m.AcceptExistentDigests) optionutils.Transfer(&opts.SkipDigest, m.SkipDigest) optionutils.Transfer(&opts.SkipVerify, m.SkipVerify) @@ -238,10 +284,17 @@ func (m TargetIndex) ApplyBlobModificationOption(opts *BlobModificationOptions) } func (m TargetIndex) ApplyModificationOption(opts *ModificationOptions) { - m.ApplyTargetOption(&opts.TargetOptions) + m.ApplyTargetOption(&opts.TargetElementOptions) } -func (m TargetIndex) ApplyTargetOption(opts *TargetOptions) { +func (m TargetIndex) ApplyElementModificationOption(opts *ElementModificationOptions) { + if m < 0 { + opts.ModifyElement = generics.Pointer(true) + } + m.ApplyTargetOption(&opts.TargetElementOptions) +} + +func (m TargetIndex) ApplyTargetOption(opts *TargetElementOptions) { opts.TargetElement = m } @@ -259,10 +312,14 @@ func (m TargetIdentityOrAppend) ApplyBlobModificationOption(opts *BlobModificati } func (m TargetIdentityOrAppend) ApplyModificationOption(opts *ModificationOptions) { - m.ApplyTargetOption(&opts.TargetOptions) + m.ApplyTargetOption(&opts.TargetElementOptions) +} + +func (m TargetIdentityOrAppend) ApplyElementModificationOption(opts *ElementModificationOptions) { + m.ApplyTargetOption(&opts.TargetElementOptions) } -func (m TargetIdentityOrAppend) ApplyTargetOption(opts *TargetOptions) { +func (m TargetIdentityOrAppend) ApplyTargetOption(opts *TargetElementOptions) { opts.TargetElement = m } @@ -285,10 +342,14 @@ func (m TargetIdentity) ApplyBlobModificationOption(opts *BlobModificationOption } func (m TargetIdentity) ApplyModificationOption(opts *ModificationOptions) { - m.ApplyTargetOption(&opts.TargetOptions) + m.ApplyTargetOption(&opts.TargetElementOptions) } -func (m TargetIdentity) ApplyTargetOption(opts *TargetOptions) { +func (m TargetIdentity) ApplyElementModificationOption(opts *ElementModificationOptions) { + m.ApplyTargetOption(&opts.TargetElementOptions) +} + +func (m TargetIdentity) ApplyTargetOption(opts *TargetElementOptions) { opts.TargetElement = m } @@ -313,27 +374,39 @@ func (m replaceElement) ApplyBlobModificationOption(opts *BlobModificationOption } func (m replaceElement) ApplyModificationOption(opts *ModificationOptions) { - m.ApplyTargetOption(&opts.TargetOptions) + m.ApplyTargetOption(&opts.TargetElementOptions) } -func (m replaceElement) ApplyTargetOption(opts *TargetOptions) { +func (m replaceElement) ApplyElementModificationOption(opts *ElementModificationOptions) { + m.ApplyTargetOption(&opts.TargetElementOptions) +} + +func (m replaceElement) ApplyTargetOption(opts *TargetElementOptions) { opts.TargetElement = m } //////////////////////////////////////////////////////////////////////////////// -type modifyresource bool +type modifyelement bool -func (m modifyresource) ApplyBlobModificationOption(opts *BlobModificationOptions) { +func (m modifyelement) ApplyBlobModificationOption(opts *BlobModificationOptions) { m.ApplyModificationOption(&opts.ModificationOptions) } -func (m modifyresource) ApplyModificationOption(opts *ModificationOptions) { - opts.ModifyResource = utils.BoolP(m) +func (m modifyelement) ApplyModificationOption(opts *ModificationOptions) { + opts.ModifyElement = utils.BoolP(m) +} + +func (m modifyelement) ApplyElementModificationOption(opts *ElementModificationOptions) { + opts.ModifyElement = utils.BoolP(m) } func ModifyResource(flag ...bool) ModOptionImpl { - return modifyresource(utils.OptionalDefaultedBool(true, flag...)) + return modifyelement(utils.OptionalDefaultedBool(true, flag...)) +} + +func ModifyElement(flag ...bool) ElemModOptionImpl { + return modifyelement(utils.OptionalDefaultedBool(true, flag...)) } //////////////////////////////////////////////////////////////////////////////// diff --git a/api/ocm/internal/repository.go b/api/ocm/internal/repository.go index 4aa4008afc..7ed53168f7 100644 --- a/api/ocm/internal/repository.go +++ b/api/ocm/internal/repository.go @@ -144,10 +144,10 @@ type ComponentVersionAccess interface { // SetSource updates or sets anew source. The options only use the // target options. All other options are ignored. - SetSource(*SourceMeta, compdesc.AccessSpec, ...TargetOption) error + SetSource(*SourceMeta, compdesc.AccessSpec, ...TargetElementOption) error // SetSourceByAccess updates or sets anew source. The options only use the // target options. All other options are ignored. - SetSourceByAccess(art SourceAccess, opts ...TargetOption) error + SetSourceByAccess(art SourceAccess, opts ...TargetElementOption) error GetReference(meta metav1.Identity) (ComponentReference, error) GetReferenceIndex(meta metav1.Identity) int @@ -155,7 +155,10 @@ type ComponentVersionAccess interface { GetReferences() []ComponentReference SelectReferences(sel ...refsel.Selector) ([]ComponentReference, error) - SetReference(ref *ComponentReference, opts ...TargetOption) error + // SetReference adds or updates a reference. By default, it does not allow for + // signature relevant changes. If such operations should be possible + // the option ModifyElement() has to be passed as option. + SetReference(ref *ComponentReference, opts ...ElementModificationOption) error // AddBlob adds a local blob and returns an appropriate local access spec. AddBlob(blob BlobAccess, artType, refName string, global AccessSpec, opts ...BlobUploadOption) (AccessSpec, error) @@ -166,7 +169,7 @@ type ComponentVersionAccess interface { AdjustSourceAccess(meta *SourceMeta, acc compdesc.AccessSpec) error // SetSourceBlob updates or sets anew source. The options only use the // target options. All other options are ignored. - SetSourceBlob(meta *SourceMeta, blob BlobAccess, refname string, global AccessSpec, opts ...TargetOption) error + SetSourceBlob(meta *SourceMeta, blob BlobAccess, refname string, global AccessSpec, opts ...TargetElementOption) error // AccessMethod provides an access method implementation for // an access spec. This might be a repository local implementation diff --git a/api/ocm/modopts.go b/api/ocm/modopts.go index 5c271f0e6b..a980ae0485 100644 --- a/api/ocm/modopts.go +++ b/api/ocm/modopts.go @@ -9,9 +9,12 @@ import ( ) type ( - TargetElement = internal.TargetElement - TargetOption = internal.TargetOption - TargetOptions = internal.TargetOptions + TargetElement = internal.TargetElement + TargetElementOption = internal.TargetElementOption + TargetElementOptions = internal.TargetElementOptions + + ElementModificationOption = internal.ElementModificationOption + ElementModificationOptions = internal.ElementModificationOptions ModificationOption = internal.ModificationOption ModificationOptions = internal.ModificationOptions @@ -60,7 +63,7 @@ func NewModificationOptions(list ...ModificationOption) *ModificationOptions { } func TargetIndex(idx int) internal.TargetOptionImpl { - return internal.TargetIndex(-1) + return internal.TargetIndex(idx) } const AppendElement = internal.TargetIndex(-1) @@ -75,10 +78,15 @@ func TargetIdentityOrCreate(id v1.Identity) internal.TargetOptionImpl { return internal.TargetIdentityOrAppend(id) } +// Deprecated: use ModifyElement. func ModifyResource(flag ...bool) internal.ModOptionImpl { return internal.ModifyResource(flag...) } +func ModifyElement(flag ...bool) internal.ElemModOptionImpl { + return internal.ModifyElement(flag...) +} + func AcceptExistentDigests(flag ...bool) internal.ModOptionImpl { return internal.AcceptExistentDigests(flag...) } diff --git a/api/ocm/tools/signing/handler_test.go b/api/ocm/tools/signing/handler_test.go index 6707f84a8f..4b7984543c 100644 --- a/api/ocm/tools/signing/handler_test.go +++ b/api/ocm/tools/signing/handler_test.go @@ -58,18 +58,28 @@ var _ = Describe("Simple signing handlers", func() { meta := ocm.NewResourceMeta("blob", resourcetypes.PLAIN_TEXT, v1.LocalRelation) meta.Version = "v1" - meta.ExtraIdentity = map[string]string{} MustBeSuccessful(cv.SetResourceBlob(meta, blobaccess.ForString(mime.MIME_TEXT, "test data"), "", nil)) + meta.ExtraIdentity = map[string]string{} meta.Version = "v2" MustBeSuccessful(cv.SetResourceBlob(meta, blobaccess.ForString(mime.MIME_TEXT, "other test data"), "", nil, ocm.TargetIndex(-1))) }) - It("signs without modification", func() { + It("signs without modification (compatibility)", func() { Must(signing.SignComponentVersion(cv, "signature", signing.PrivateKey("signature", priv))) cd := cv.GetDescriptor() + cd.Resources[0].ExtraIdentity = v1.Identity{} + cd.Resources[1].ExtraIdentity = v1.Identity{} Expect(len(cd.Resources)).To(Equal(2)) Expect(len(cd.Resources[0].ExtraIdentity)).To(Equal(0)) Expect(len(cd.Resources[1].ExtraIdentity)).To(Equal(0)) }) + + It("signs defaulted", func() { + Must(signing.SignComponentVersion(cv, "signature", signing.PrivateKey("signature", priv))) + cd := cv.GetDescriptor() + Expect(len(cd.Resources)).To(Equal(2)) + Expect(len(cd.Resources[0].ExtraIdentity)).To(Equal(1)) + Expect(len(cd.Resources[1].ExtraIdentity)).To(Equal(1)) + }) }) }) diff --git a/api/ocm/tools/signing/signing_test.go b/api/ocm/tools/signing/signing_test.go index f374ec0528..a54e91c9bb 100644 --- a/api/ocm/tools/signing/signing_test.go +++ b/api/ocm/tools/signing/signing_test.go @@ -1286,6 +1286,159 @@ applying to version "github.com/mandelsoft/ref2:v1"[github.com/mandelsoft/ref2:v CheckStore(store, common.NewNameVersion(COMPONENTA, VERSION)) }) }) + + Context("handle extra identity", func() { + BeforeEach(func() { + env.OCMCommonTransport(ARCH, accessio.FormatDirectory, func() { + env.ComponentVersion(COMPONENTA, VERSION, func() { + env.Provider(PROVIDER) + env.Resource("test", "v1", resourcetypes.PLAIN_TEXT, metav1.ExternalRelation, func() { + env.BlobStringData(mime.MIME_TEXT, "test data") + }) + env.Resource("test", "v2", resourcetypes.PLAIN_TEXT, metav1.ExternalRelation, func() { + env.BlobStringData(mime.MIME_TEXT, "extended test data") + env.ModificationOptions(ocm.AppendElement) + env.ExtraIdentities() + }) + }) + }) + }) + + It("signs version with non-unique resource names", func() { + session := datacontext.NewSession() + defer session.Close() + + src := Must(ctf.Open(env.OCMContext(), accessobj.ACC_WRITABLE, ARCH, 0, env)) + archcloser := session.AddCloser(src) + + cv := Must(src.LookupComponentVersion(COMPONENTA, VERSION)) + closer := session.AddCloser(cv) + + cd := cv.GetDescriptor() + + Expect(cd.Resources[0].GetIdentity(cv.GetDescriptor().Resources)).To(YAMLEqual(` +name: test +version: v1 +`)) + Expect(cd.Resources[0].ExtraIdentity).To(YAMLEqual(` +version: v1 +`)) + Expect(cd.Resources[1].GetIdentity(cv.GetDescriptor().Resources)).To(YAMLEqual(` +name: test +version: v2 +`)) + Expect(cd.Resources[1].ExtraIdentity).To(YAMLEqual(` +version: v2 +`)) + data := Must(compdesc.Encode(cd, compdesc.DefaultYAMLCodec)) + Expect(string(data)).To(YAMLEqual(` + component: + componentReferences: [] + name: github.com/mandelsoft/test + provider: mandelsoft + repositoryContexts: [] + resources: + - access: + localReference: sha256:916f0027a575074ce72a331777c3478d6513f786a591bd892da1a577bf2335f9 + mediaType: text/plain + type: localBlob + digest: + hashAlgorithm: SHA-256 + normalisationAlgorithm: genericBlobDigest/v1 + value: 916f0027a575074ce72a331777c3478d6513f786a591bd892da1a577bf2335f9 + name: test + relation: external + type: plainText + version: v1 + extraIdentity: + version: v1 + - access: + localReference: sha256:920ce99fb13b43ca0408caee6e61f6335ea5156d79aa98e733e1ed2393e0f649 + mediaType: text/plain + type: localBlob + digest: + hashAlgorithm: SHA-256 + normalisationAlgorithm: genericBlobDigest/v1 + value: 920ce99fb13b43ca0408caee6e61f6335ea5156d79aa98e733e1ed2393e0f649 + name: test + relation: external + type: plainText + version: v2 + extraIdentity: + version: v2 + sources: [] + version: v1 + meta: + schemaVersion: v2 +`)) + + digest := "70c1b7f5e2260a283e24788c81ea7f8f6e9a70a8544dbf62d6f3a27285f6b633" + + pr, buf := common.NewBufferedPrinter() + // key taken from signing attr + dig := Must(SignComponentVersion(cv, SIGNATURE, SignerByAlgo(SIGN_ALGO), Printer(pr))) + Expect(closer.Close()).To(Succeed()) + Expect(archcloser.Close()).To(Succeed()) + Expect(dig.Value).To(StringEqualWithContext(digest)) + + Expect(buf.String()).To(StringEqualTrimmedWithContext(` +applying to version "github.com/mandelsoft/test:v1"[github.com/mandelsoft/test:v1]... + resource 0: "name"="test","version"="v1": digest SHA-256:916f0027a575074ce72a331777c3478d6513f786a591bd892da1a577bf2335f9[genericBlobDigest/v1] + resource 1: "name"="test","version"="v2": digest SHA-256:920ce99fb13b43ca0408caee6e61f6335ea5156d79aa98e733e1ed2393e0f649[genericBlobDigest/v1] +`)) + + src = Must(ctf.Open(env.OCMContext(), accessobj.ACC_READONLY, ARCH, 0, env)) + session.AddCloser(src) + cv = Must(src.LookupComponentVersion(COMPONENTA, VERSION)) + session.AddCloser(cv) + + cd = cv.GetDescriptor().Copy() + Expect(len(cd.Signatures)).To(Equal(1)) + cd.Signatures = nil // for comparison + data = Must(compdesc.Encode(cd, compdesc.DefaultYAMLCodec)) + + Expect(string(data)).To(YAMLEqual(` + component: + componentReferences: [] + name: github.com/mandelsoft/test + provider: mandelsoft + repositoryContexts: [] + resources: + - access: + localReference: sha256:916f0027a575074ce72a331777c3478d6513f786a591bd892da1a577bf2335f9 + mediaType: text/plain + type: localBlob + digest: + hashAlgorithm: SHA-256 + normalisationAlgorithm: genericBlobDigest/v1 + value: 916f0027a575074ce72a331777c3478d6513f786a591bd892da1a577bf2335f9 + name: test + relation: external + type: plainText + version: v1 + extraIdentity: + version: v1 + - access: + localReference: sha256:920ce99fb13b43ca0408caee6e61f6335ea5156d79aa98e733e1ed2393e0f649 + mediaType: text/plain + type: localBlob + digest: + hashAlgorithm: SHA-256 + normalisationAlgorithm: genericBlobDigest/v1 + value: 920ce99fb13b43ca0408caee6e61f6335ea5156d79aa98e733e1ed2393e0f649 + name: test + relation: external + type: plainText + version: v2 + extraIdentity: + version: v2 + sources: [] + version: v1 + meta: + schemaVersion: v2 +`)) + }) + }) }) func CheckStore(store VerifiedStore, ve common.VersionedElement) { diff --git a/api/ocm/tools/signing/transport_test.go b/api/ocm/tools/signing/transport_test.go index 85323ce2bd..c5294e2d1d 100644 --- a/api/ocm/tools/signing/transport_test.go +++ b/api/ocm/tools/signing/transport_test.go @@ -168,7 +168,7 @@ var _ = Describe("transport and signing", func() { ra := desc.GetResourceIndexByIdentity(metav1.NewIdentity("image")) Expect(ra).To(BeNumerically(">=", 0)) // indeed, the artifact set archive hash seems to be reproducible - desc.Resources[ra].Access = localblob.New("sha256:b0692bcec00e0a875b6b280f3209d6776f3eca128adcb7e81e82fd32127c0c62", "ocm/value:v2.0", "application/vnd.oci.image.manifest.v1+tar+gzip", nil) + desc.Resources[ra].Access = localblob.New("sha256:"+H_OCIARCHMANIFEST1, "ocm/value:v2.0", "application/vnd.oci.image.manifest.v1+tar+gzip", nil) Expect(tcv.GetDescriptor()).To(YAMLEqual(desc)) descSigned := tcv.GetDescriptor().Copy() diff --git a/api/ocm/tools/transfer/transfer.go b/api/ocm/tools/transfer/transfer.go index 8490138868..ad6121728e 100644 --- a/api/ocm/tools/transfer/transfer.go +++ b/api/ocm/tools/transfer/transfer.go @@ -273,7 +273,7 @@ func copyVersion(printer common.Printer, log logging.Logger, hist common.History err = handler.HandleTransferResource(r, m, hint, t) } else { if err == nil { // old resource found -> keep current access method - t.SetResource(r.Meta(), old.Access, ocm.ModifyResource(), ocm.SkipVerify()) + t.SetResource(r.Meta(), old.Access, ocm.ModifyElement(), ocm.SkipVerify()) } notifyArtifactInfo(printer, log, "resource", i, r.Meta(), hint, "already present") } diff --git a/api/tech/docker/README.md b/api/tech/docker/README.md new file mode 100644 index 0000000000..096a9c1e18 --- /dev/null +++ b/api/tech/docker/README.md @@ -0,0 +1,4 @@ +# containerd + +Taken from github.com/containerd/containerd remotes/docker to add list endpoints +Fix retry of requests with ResendBuffer diff --git a/api/tech/docker/errors/errors.go b/api/tech/docker/errors/errors.go new file mode 100644 index 0000000000..a158f75b5a --- /dev/null +++ b/api/tech/docker/errors/errors.go @@ -0,0 +1,58 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package errors + +import ( + "fmt" + "io" + "net/http" +) + +var _ error = ErrUnexpectedStatus{} + +// ErrUnexpectedStatus is returned if a registry API request returned with unexpected HTTP status +type ErrUnexpectedStatus struct { + Status string + StatusCode int + Body []byte + RequestURL, RequestMethod string +} + +func (e ErrUnexpectedStatus) Error() string { + if len(e.Body) > 0 { + return fmt.Sprintf("unexpected status from %s request to %s: %s: %s", e.RequestMethod, e.RequestURL, e.Status, string(e.Body)) + } + return fmt.Sprintf("unexpected status from %s request to %s: %s", e.RequestMethod, e.RequestURL, e.Status) +} + +// NewUnexpectedStatusErr creates an ErrUnexpectedStatus from HTTP response +func NewUnexpectedStatusErr(resp *http.Response) error { + var b []byte + if resp.Body != nil { + b, _ = io.ReadAll(io.LimitReader(resp.Body, 64000)) // 64KB + } + err := ErrUnexpectedStatus{ + Body: b, + Status: resp.Status, + StatusCode: resp.StatusCode, + RequestMethod: resp.Request.Method, + } + if resp.Request.URL != nil { + err.RequestURL = resp.Request.URL.String() + } + return err +} diff --git a/api/tech/docker/fetcher.go b/api/tech/docker/fetcher.go new file mode 100644 index 0000000000..4a2eec584e --- /dev/null +++ b/api/tech/docker/fetcher.go @@ -0,0 +1,202 @@ +package docker + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/containerd/containerd/errdefs" + "github.com/containerd/containerd/images" + "github.com/containerd/containerd/log" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" + + "ocm.software/ocm/api/utils/accessio" +) + +type dockerFetcher struct { + *dockerBase +} + +func (r dockerFetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error) { + ctx = log.WithLogger(ctx, log.G(ctx).WithField("digest", desc.Digest)) + + hosts := r.filterHosts(HostCapabilityPull) + if len(hosts) == 0 { + return nil, errors.Wrap(errdefs.ErrNotFound, "no pull hosts") + } + + ctx, err := ContextWithRepositoryScope(ctx, r.refspec, false) + if err != nil { + return nil, err + } + + return newHTTPReadSeeker(desc.Size, func(offset int64) (io.ReadCloser, error) { + // firstly try fetch via external urls + for _, us := range desc.URLs { + ctx = log.WithLogger(ctx, log.G(ctx).WithField("url", us)) + + u, err := url.Parse(us) + if err != nil { + log.G(ctx).WithError(err).Debug("failed to parse") + continue + } + if u.Scheme != "http" && u.Scheme != "https" { + log.G(ctx).Debug("non-http(s) alternative url is unsupported") + continue + } + log.G(ctx).Debug("trying alternative url") + + // Try this first, parse it + host := RegistryHost{ + Client: http.DefaultClient, + Host: u.Host, + Scheme: u.Scheme, + Path: u.Path, + Capabilities: HostCapabilityPull, + } + req := r.request(host, http.MethodGet) + // Strip namespace from base + req.path = u.Path + if u.RawQuery != "" { + req.path = req.path + "?" + u.RawQuery + } + + rc, err := r.open(ctx, req, desc.MediaType, offset) + if err != nil { + if errdefs.IsNotFound(err) { + continue // try one of the other urls. + } + + return nil, err + } + + return rc, nil + } + + // Try manifests endpoints for manifests types + switch desc.MediaType { + case images.MediaTypeDockerSchema2Manifest, images.MediaTypeDockerSchema2ManifestList, + images.MediaTypeDockerSchema1Manifest, + ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex: + + var firstErr error + for _, host := range r.hosts { + req := r.request(host, http.MethodGet, "manifests", desc.Digest.String()) + if err := req.addNamespace(r.refspec.Hostname()); err != nil { + return nil, err + } + + rc, err := r.open(ctx, req, desc.MediaType, offset) + if err != nil { + // Store the error for referencing later + if firstErr == nil { + firstErr = err + } + continue // try another host + } + + return rc, nil + } + + return nil, firstErr + } + + // Finally use blobs endpoints + var firstErr error + for _, host := range r.hosts { + req := r.request(host, http.MethodGet, "blobs", desc.Digest.String()) + if err := req.addNamespace(r.refspec.Hostname()); err != nil { + return nil, err + } + + rc, err := r.open(ctx, req, desc.MediaType, offset) + if err != nil { + // Store the error for referencing later + if firstErr == nil { + firstErr = err + } + continue // try another host + } + + return rc, nil + } + + if errdefs.IsNotFound(firstErr) { + firstErr = errors.Wrapf(errdefs.ErrNotFound, + "could not fetch content descriptor %v (%v) from remote", + desc.Digest, desc.MediaType) + } + + return nil, firstErr + }) +} + +func (r dockerFetcher) open(ctx context.Context, req *request, mediatype string, offset int64) (_ io.ReadCloser, retErr error) { + mt := "*/*" + if mediatype != "" { + mt = mediatype + ", " + mt + } + req.header.Set("Accept", mt) + + if offset > 0 { + // Note: "Accept-Ranges: bytes" cannot be trusted as some endpoints + // will return the header without supporting the range. The content + // range must always be checked. + req.header.Set("Range", fmt.Sprintf("bytes=%d-", offset)) + } + + resp, err := req.doWithRetries(ctx, nil) + if err != nil { + return nil, accessio.RetriableError(err) + } + defer func() { + if retErr != nil { + resp.Body.Close() + } + }() + + if resp.StatusCode > 299 { + // TODO(stevvooe): When doing a offset specific request, we should + // really distinguish between a 206 and a 200. In the case of 200, we + // can discard the bytes, hiding the seek behavior from the + // implementation. + + if resp.StatusCode == http.StatusNotFound { + return nil, errors.Wrapf(errdefs.ErrNotFound, "content at %v not found", req.String()) + } + var registryErr Errors + if err := json.NewDecoder(resp.Body).Decode(®istryErr); err != nil || registryErr.Len() < 1 { + return nil, errors.Errorf("unexpected status code %v: %v", req.String(), resp.Status) + } + return nil, errors.Errorf("unexpected status code %v: %s - Server message: %s", req.String(), resp.Status, registryErr.Error()) + } + if offset > 0 { + cr := resp.Header.Get("content-range") + if cr != "" { + if !strings.HasPrefix(cr, fmt.Sprintf("bytes %d-", offset)) { + return nil, errors.Errorf("unhandled content range in response: %v", cr) + } + } else { + // TODO: Should any cases where use of content range + // without the proper header be considered? + // 206 responses? + + // Discard up to offset + // Could use buffer pool here but this case should be rare + n, err := io.Copy(io.Discard, io.LimitReader(resp.Body, offset)) + if err != nil { + return nil, errors.Wrap(err, "failed to discard to offset") + } + if n != offset { + return nil, errors.Errorf("unable to discard to offset") + } + } + } + + return resp.Body, nil +} diff --git a/api/tech/docker/handler.go b/api/tech/docker/handler.go new file mode 100644 index 0000000000..0ff9959ad3 --- /dev/null +++ b/api/tech/docker/handler.go @@ -0,0 +1,136 @@ +package docker + +import ( + "context" + "fmt" + "net/url" + "strings" + + "github.com/containerd/containerd/content" + "github.com/containerd/containerd/images" + "github.com/containerd/containerd/labels" + "github.com/containerd/containerd/log" + "github.com/containerd/containerd/reference" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +// labelDistributionSource describes the source blob comes from. +var labelDistributionSource = "containerd.io/distribution.source" + +// AppendDistributionSourceLabel updates the label of blob with distribution source. +func AppendDistributionSourceLabel(manager content.Manager, ref string) (images.HandlerFunc, error) { + refspec, err := reference.Parse(ref) + if err != nil { + return nil, err + } + + u, err := url.Parse("dummy://" + refspec.Locator) + if err != nil { + return nil, err + } + + source, repo := u.Hostname(), strings.TrimPrefix(u.Path, "/") + return func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { + info, err := manager.Info(ctx, desc.Digest) + if err != nil { + return nil, err + } + + key := distributionSourceLabelKey(source) + + originLabel := "" + if info.Labels != nil { + originLabel = info.Labels[key] + } + value := appendDistributionSourceLabel(originLabel, repo) + + // The repo name has been limited under 256 and the distribution + // label might hit the limitation of label size, when blob data + // is used as the very, very common layer. + if err := labels.Validate(key, value); err != nil { + log.G(ctx).Warnf("skip to append distribution label: %s", err) + return nil, nil + } + + info = content.Info{ + Digest: desc.Digest, + Labels: map[string]string{ + key: value, + }, + } + _, err = manager.Update(ctx, info, fmt.Sprintf("labels.%s", key)) + return nil, err + }, nil +} + +func appendDistributionSourceLabel(originLabel, repo string) string { + repos := []string{} + if originLabel != "" { + repos = strings.Split(originLabel, ",") + } + repos = append(repos, repo) + + // use empty string to present duplicate items + for i := 1; i < len(repos); i++ { + tmp, j := repos[i], i-1 + for ; j >= 0 && repos[j] >= tmp; j-- { + if repos[j] == tmp { + tmp = "" + } + repos[j+1] = repos[j] + } + repos[j+1] = tmp + } + + i := 0 + for ; i < len(repos) && repos[i] == ""; i++ { + } + + return strings.Join(repos[i:], ",") +} + +func distributionSourceLabelKey(source string) string { + return fmt.Sprintf("%s.%s", labelDistributionSource, source) +} + +// selectRepositoryMountCandidate will select the repo which has longest +// common prefix components as the candidate. +func selectRepositoryMountCandidate(refspec reference.Spec, sources map[string]string) string { + u, err := url.Parse("dummy://" + refspec.Locator) + if err != nil { + // NOTE: basically, it won't be error here + return "" + } + + source, target := u.Hostname(), strings.TrimPrefix(u.Path, "/") + repoLabel, ok := sources[distributionSourceLabelKey(source)] + if !ok || repoLabel == "" { + return "" + } + + n, match := 0, "" + components := strings.Split(target, "/") + for _, repo := range strings.Split(repoLabel, ",") { + // the target repo is not a candidate + if repo == target { + continue + } + + if l := commonPrefixComponents(components, repo); l >= n { + n, match = l, repo + } + } + return match +} + +func commonPrefixComponents(components []string, target string) int { + targetComponents := strings.Split(target, "/") + + i := 0 + for ; i < len(components) && i < len(targetComponents); i++ { + if components[i] != targetComponents[i] { + break + } + } + return i +} diff --git a/api/tech/docker/httpreadseeker.go b/api/tech/docker/httpreadseeker.go new file mode 100644 index 0000000000..c6b803810b --- /dev/null +++ b/api/tech/docker/httpreadseeker.go @@ -0,0 +1,157 @@ +package docker + +import ( + "bytes" + "io" + + "github.com/containerd/containerd/errdefs" + "github.com/containerd/containerd/log" + "github.com/pkg/errors" +) + +const maxRetry = 3 + +type httpReadSeeker struct { + size int64 + offset int64 + rc io.ReadCloser + open func(offset int64) (io.ReadCloser, error) + closed bool + + errsWithNoProgress int +} + +func newHTTPReadSeeker(size int64, open func(offset int64) (io.ReadCloser, error)) (io.ReadCloser, error) { + return &httpReadSeeker{ + size: size, + open: open, + }, nil +} + +func (hrs *httpReadSeeker) Read(p []byte) (n int, err error) { + if hrs.closed { + return 0, io.EOF + } + + rd, err := hrs.reader() + if err != nil { + return 0, err + } + + n, err = rd.Read(p) + hrs.offset += int64(n) + if n > 0 || err == nil { + hrs.errsWithNoProgress = 0 + } + + if !errors.Is(err, io.ErrUnexpectedEOF) { + return + } + // connection closed unexpectedly. try reconnecting. + if n == 0 { + hrs.errsWithNoProgress++ + if hrs.errsWithNoProgress > maxRetry { + return // too many retries for this offset with no progress + } + } + + if hrs.rc != nil { + if clsErr := hrs.rc.Close(); clsErr != nil { + log.L.WithError(clsErr).Errorf("httpReadSeeker: failed to close ReadCloser") + } + hrs.rc = nil + } + + if _, err2 := hrs.reader(); err2 == nil { + return n, nil + } + + return n, err +} + +func (hrs *httpReadSeeker) Close() error { + if hrs.closed { + return nil + } + hrs.closed = true + if hrs.rc != nil { + return hrs.rc.Close() + } + + return nil +} + +func (hrs *httpReadSeeker) Seek(offset int64, whence int) (int64, error) { + if hrs.closed { + return 0, errors.Wrap(errdefs.ErrUnavailable, "Fetcher.Seek: closed") + } + + abs := hrs.offset + switch whence { + case io.SeekStart: + abs = offset + case io.SeekCurrent: + abs += offset + case io.SeekEnd: + if hrs.size == -1 { + return 0, errors.Wrap(errdefs.ErrUnavailable, "Fetcher.Seek: unknown size, cannot seek from end") + } + abs = hrs.size + offset + default: + return 0, errors.Wrap(errdefs.ErrInvalidArgument, "Fetcher.Seek: invalid whence") + } + + if abs < 0 { + return 0, errors.Wrapf(errdefs.ErrInvalidArgument, "Fetcher.Seek: negative offset") + } + + if abs != hrs.offset { + if hrs.rc != nil { + if err := hrs.rc.Close(); err != nil { + log.L.WithError(err).Errorf("Fetcher.Seek: failed to close ReadCloser") + } + + hrs.rc = nil + } + + hrs.offset = abs + } + + return hrs.offset, nil +} + +func (hrs *httpReadSeeker) reader() (io.Reader, error) { + if hrs.rc != nil { + return hrs.rc, nil + } + + if hrs.size == -1 || hrs.offset < hrs.size { + // only try to reopen the body request if we are seeking to a value + // less than the actual size. + if hrs.open == nil { + return nil, errors.Wrapf(errdefs.ErrNotImplemented, "cannot open") + } + + rc, err := hrs.open(hrs.offset) + if err != nil { + return nil, errors.Wrapf(err, "httpReadSeeker: failed open") + } + + if hrs.rc != nil { + if err := hrs.rc.Close(); err != nil { + log.L.WithError(err).Errorf("httpReadSeeker: failed to close ReadCloser") + } + } + hrs.rc = rc + } else { + // There is an edge case here where offset == size of the content. If + // we seek, we will probably get an error for content that cannot be + // sought (?). In that case, we should err on committing the content, + // as the length is already satisfied but we just return the empty + // reader instead. + + hrs.rc = io.NopCloser(bytes.NewReader([]byte{})) + } + + return hrs.rc, nil +} diff --git a/api/tech/docker/lister.go b/api/tech/docker/lister.go new file mode 100644 index 0000000000..efd3b8e1e2 --- /dev/null +++ b/api/tech/docker/lister.go @@ -0,0 +1,130 @@ +package docker + +import ( + "context" + "encoding/json" + "io" + "net/http" + + "github.com/containerd/containerd/errdefs" + "github.com/containerd/containerd/log" + "github.com/pkg/errors" + + "ocm.software/ocm/api/tech/docker/resolve" +) + +var ErrObjectNotRequired = errors.New("object not required") + +type TagList struct { + Name string `json:"name"` + Tags []string `json:"tags"` +} + +type dockerLister struct { + dockerBase *dockerBase +} + +func (r *dockerResolver) Lister(ctx context.Context, ref string) (resolve.Lister, error) { + base, err := r.resolveDockerBase(ref) + if err != nil { + return nil, err + } + if base.refspec.Object != "" { + return nil, ErrObjectNotRequired + } + + return &dockerLister{ + dockerBase: base, + }, nil +} + +func (r *dockerLister) List(ctx context.Context) ([]string, error) { + refspec := r.dockerBase.refspec + base := r.dockerBase + var ( + firstErr error + paths [][]string + caps = HostCapabilityPull + ) + + // turns out, we have a valid digest, make a url. + paths = append(paths, []string{"tags/list"}) + caps |= HostCapabilityResolve + + hosts := base.filterHosts(caps) + if len(hosts) == 0 { + return nil, errors.Wrap(errdefs.ErrNotFound, "no list hosts") + } + + ctx, err := ContextWithRepositoryScope(ctx, refspec, false) + if err != nil { + return nil, err + } + + for _, u := range paths { + for _, host := range hosts { + ctxWithLogger := log.WithLogger(ctx, log.G(ctx).WithField("host", host.Host)) + + req := base.request(host, http.MethodGet, u...) + if err := req.addNamespace(base.refspec.Hostname()); err != nil { + return nil, err + } + + req.header["Accept"] = []string{"application/json"} + + log.G(ctxWithLogger).Debug("listing") + resp, err := req.doWithRetries(ctxWithLogger, nil) + if err != nil { + if errors.Is(err, ErrInvalidAuthorization) { + err = errors.Wrapf(err, "pull access denied, repository does not exist or may require authorization") + } + // Store the error for referencing later + if firstErr == nil { + firstErr = err + } + log.G(ctxWithLogger).WithError(err).Info("trying next host") + continue // try another host + } + + if resp.StatusCode > 299 { + resp.Body.Close() + if resp.StatusCode == http.StatusNotFound { + log.G(ctxWithLogger).Info("trying next host - response was http.StatusNotFound") + continue + } + if resp.StatusCode > 399 { + // Set firstErr when encountering the first non-404 status code. + if firstErr == nil { + firstErr = errors.Errorf("pulling from host %s failed with status code %v: %v", host.Host, u, resp.Status) + } + continue // try another host + } + return nil, errors.Errorf("taglist from host %s failed with unexpected status code %v: %v", host.Host, u, resp.Status) + } + + data, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return nil, err + } + + tags := &TagList{} + + err = json.Unmarshal(data, tags) + if err != nil { + return nil, err + } + return tags.Tags, nil + } + } + + // If above loop terminates without return, then there was an error. + // "firstErr" contains the first non-404 error. That is, "firstErr == nil" + // means that either no registries were given or each registry returned 404. + + if firstErr == nil { + firstErr = errors.Wrap(errdefs.ErrNotFound, base.refspec.Locator) + } + + return nil, firstErr +} diff --git a/api/tech/docker/orig.go b/api/tech/docker/orig.go new file mode 100644 index 0000000000..c9b2468fba --- /dev/null +++ b/api/tech/docker/orig.go @@ -0,0 +1,44 @@ +package docker + +import ( + "github.com/containerd/containerd/remotes/docker" +) + +var ( + ContextWithRepositoryScope = docker.ContextWithRepositoryScope + ContextWithAppendPullRepositoryScope = docker.ContextWithAppendPullRepositoryScope + NewInMemoryTracker = docker.NewInMemoryTracker + NewDockerAuthorizer = docker.NewDockerAuthorizer + WithAuthClient = docker.WithAuthClient + WithAuthHeader = docker.WithAuthHeader + WithAuthCreds = docker.WithAuthCreds +) + +type ( + Errors = docker.Errors + StatusTracker = docker.StatusTracker + Status = docker.Status + StatusTrackLocker = docker.StatusTrackLocker +) + +func ConvertHosts(hosts docker.RegistryHosts) RegistryHosts { + return func(host string) ([]RegistryHost, error) { + list, err := hosts(host) + if err != nil { + return nil, err + } + result := make([]RegistryHost, len(list)) + for i, v := range list { + result[i] = RegistryHost{ + Client: v.Client, + Authorizer: v.Authorizer, + Host: v.Host, + Scheme: v.Scheme, + Path: v.Path, + Capabilities: HostCapabilities(v.Capabilities), + Header: v.Header, + } + } + return result, nil + } +} diff --git a/api/tech/docker/pusher.go b/api/tech/docker/pusher.go new file mode 100644 index 0000000000..708ad0f349 --- /dev/null +++ b/api/tech/docker/pusher.go @@ -0,0 +1,433 @@ +package docker + +import ( + "context" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/containerd/containerd/content" + "github.com/containerd/containerd/errdefs" + "github.com/containerd/containerd/images" + "github.com/containerd/containerd/log" + "github.com/containerd/containerd/remotes" + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + remoteserrors "ocm.software/ocm/api/tech/docker/errors" + "ocm.software/ocm/api/tech/docker/resolve" + "ocm.software/ocm/api/utils/accessio" +) + +func init() { + l := logrus.New() + l.Level = logrus.WarnLevel + log.L = logrus.NewEntry(l) +} + +type dockerPusher struct { + *dockerBase + object string + + // TODO: namespace tracker + tracker StatusTracker +} + +func (p dockerPusher) Push(ctx context.Context, desc ocispec.Descriptor, src resolve.Source) (resolve.PushRequest, error) { + return p.push(ctx, desc, src, remotes.MakeRefKey(ctx, desc), false) +} + +func (p dockerPusher) push(ctx context.Context, desc ocispec.Descriptor, src resolve.Source, ref string, unavailableOnFail bool) (resolve.PushRequest, error) { + if l, ok := p.tracker.(StatusTrackLocker); ok { + l.Lock(ref) + defer l.Unlock(ref) + } + ctx, err := ContextWithRepositoryScope(ctx, p.refspec, true) + if err != nil { + return nil, err + } + status, err := p.tracker.GetStatus(ref) + if err == nil { + if status.Committed && status.Offset == status.Total { + return nil, errors.Wrapf(errdefs.ErrAlreadyExists, "ref %v", ref) + } + if unavailableOnFail { + // Another push of this ref is happening elsewhere. The rest of function + // will continue only when `errdefs.IsNotFound(err) == true` (i.e. there + // is no actively-tracked ref already). + return nil, errors.Wrap(errdefs.ErrUnavailable, "push is on-going") + } + // TODO: Handle incomplete status + } else if !errdefs.IsNotFound(err) { + return nil, errors.Wrap(err, "failed to get status") + } + + hosts := p.filterHosts(HostCapabilityPush) + if len(hosts) == 0 { + return nil, errors.Wrap(errdefs.ErrNotFound, "no push hosts") + } + + var ( + isManifest bool + existCheck []string + host = hosts[0] + ) + + switch desc.MediaType { + case images.MediaTypeDockerSchema2Manifest, images.MediaTypeDockerSchema2ManifestList, + ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex: + isManifest = true + existCheck = getManifestPath(p.object, desc.Digest) + default: + existCheck = []string{"blobs", desc.Digest.String()} + } + + req := p.request(host, http.MethodHead, existCheck...) + req.header.Set("Accept", strings.Join([]string{desc.MediaType, `*/*`}, ", ")) + + log.G(ctx).WithField("url", req.String()).Debugf("checking and pushing to") + + headResp, err := req.doWithRetries(ctx, nil) + if err != nil { + if !errors.Is(err, ErrInvalidAuthorization) { + return nil, err + } + log.G(ctx).WithError(err).Debugf("Unable to check existence, continuing with push") + } else { + defer headResp.Body.Close() + + if headResp.StatusCode == http.StatusOK { + var exists bool + if isManifest && existCheck[1] != desc.Digest.String() { + dgstHeader := digest.Digest(headResp.Header.Get("Docker-Content-Digest")) + if dgstHeader == desc.Digest { + exists = true + } + } else { + exists = true + } + + if exists { + p.tracker.SetStatus(ref, Status{ + Committed: true, + Status: content.Status{ + Ref: ref, + Total: desc.Size, + Offset: desc.Size, + // TODO: Set updated time? + }, + }) + + return nil, errors.Wrapf(errdefs.ErrAlreadyExists, "content %v on remote", desc.Digest) + } + } else if headResp.StatusCode != http.StatusNotFound { + err := remoteserrors.NewUnexpectedStatusErr(headResp) + + var statusError remoteserrors.ErrUnexpectedStatus + if errors.As(err, &statusError) { + log.G(ctx). + WithField("resp", headResp). + WithField("body", string(statusError.Body)). + Debug("unexpected response") + } + + return nil, accessio.RetriableError(err) + } + } + + if isManifest { + putPath := getManifestPath(p.object, desc.Digest) + req = p.request(host, http.MethodPut, putPath...) + req.header.Add("Content-Type", desc.MediaType) + } else { + // Start upload request + req = p.request(host, http.MethodPost, "blobs", "uploads/") + + var resp *http.Response + if fromRepo := selectRepositoryMountCandidate(p.refspec, desc.Annotations); fromRepo != "" { + preq := requestWithMountFrom(req, desc.Digest.String(), fromRepo) + pctx := ContextWithAppendPullRepositoryScope(ctx, fromRepo) + + // NOTE: the fromRepo might be private repo and + // auth service still can grant token without error. + // but the post request will fail because of 401. + // + // for the private repo, we should remove mount-from + // query and send the request again. + resp, err = preq.doWithRetries(pctx, nil) + if err != nil { + return nil, accessio.RetriableError(err) + } + + if resp.StatusCode == http.StatusUnauthorized { + log.G(ctx).Debugf("failed to mount from repository %s", fromRepo) + + resp.Body.Close() + resp = nil + } + } + + if resp == nil { + resp, err = req.doWithRetries(ctx, nil) + if err != nil { + return nil, accessio.RetriableError(err) + } + } + defer resp.Body.Close() + + switch resp.StatusCode { + case http.StatusOK, http.StatusAccepted, http.StatusNoContent: + case http.StatusCreated: + p.tracker.SetStatus(ref, Status{ + Committed: true, + Status: content.Status{ + Ref: ref, + Total: desc.Size, + Offset: desc.Size, + }, + }) + return nil, errors.Wrapf(errdefs.ErrAlreadyExists, "content %v on remote", desc.Digest) + default: + err := remoteserrors.NewUnexpectedStatusErr(resp) + + var statusError remoteserrors.ErrUnexpectedStatus + if errors.As(err, &statusError) { + log.G(ctx). + WithField("resp", resp). + WithField("body", string(statusError.Body)). + Debug("unexpected response") + } + + return nil, err + } + + var ( + location = resp.Header.Get("Location") + lurl *url.URL + lhost = host + ) + // Support paths without host in location + if strings.HasPrefix(location, "/") { + lurl, err = url.Parse(lhost.Scheme + "://" + lhost.Host + location) + if err != nil { + return nil, errors.Wrapf(err, "unable to parse location %v", location) + } + } else { + if !strings.Contains(location, "://") { + location = lhost.Scheme + "://" + location + } + lurl, err = url.Parse(location) + if err != nil { + return nil, errors.Wrapf(err, "unable to parse location %v", location) + } + + if lurl.Host != lhost.Host || lhost.Scheme != lurl.Scheme { + lhost.Scheme = lurl.Scheme + lhost.Host = lurl.Host + log.G(ctx).WithField("host", lhost.Host).WithField("scheme", lhost.Scheme).Debug("upload changed destination") + + // Strip authorizer if change to host or scheme + lhost.Authorizer = nil + } + } + q := lurl.Query() + q.Add("digest", desc.Digest.String()) + + req = p.request(lhost, http.MethodPut) + req.header.Set("Content-Type", "application/octet-stream") + req.path = lurl.Path + "?" + q.Encode() + } + p.tracker.SetStatus(ref, Status{ + Status: content.Status{ + Ref: ref, + Total: desc.Size, + Expected: desc.Digest, + StartedAt: time.Now(), + }, + }) + + // TODO: Support chunked upload + + respC := make(chan response, 1) + + preq := &pushRequest{ + base: p.dockerBase, + ref: ref, + responseC: respC, + source: src, + isManifest: isManifest, + expected: desc.Digest, + tracker: p.tracker, + } + + req.body = preq.Reader + req.size = desc.Size + + go func() { + defer close(respC) + resp, err := req.doWithRetries(ctx, nil) + if err != nil { + respC <- response{err: err} + return + } + + switch resp.StatusCode { + case http.StatusOK, http.StatusCreated, http.StatusNoContent: + default: + err := remoteserrors.NewUnexpectedStatusErr(resp) + + var statusError remoteserrors.ErrUnexpectedStatus + if errors.As(err, &statusError) { + log.G(ctx). + WithField("resp", resp). + WithField("body", string(statusError.Body)). + Debug("unexpected response") + } + } + respC <- response{Response: resp} + }() + + return preq, nil +} + +func getManifestPath(object string, dgst digest.Digest) []string { + if i := strings.IndexByte(object, '@'); i >= 0 { + if object[i+1:] != dgst.String() { + // use digest, not tag + object = "" + } else { + // strip @ for registry path to make tag + object = object[:i] + } + } + + if object == "" { + return []string{"manifests", dgst.String()} + } + + return []string{"manifests", object} +} + +type response struct { + *http.Response + err error +} + +type pushRequest struct { + base *dockerBase + ref string + + responseC <-chan response + source resolve.Source + isManifest bool + + expected digest.Digest + tracker StatusTracker +} + +func (pw *pushRequest) Status() (content.Status, error) { + status, err := pw.tracker.GetStatus(pw.ref) + if err != nil { + return content.Status{}, err + } + return status.Status, nil +} + +func (pw *pushRequest) Commit(ctx context.Context, size int64, expected digest.Digest, opts ...content.Opt) error { + // TODO: timeout waiting for response + resp := <-pw.responseC + if resp.err != nil { + return resp.err + } + defer resp.Response.Body.Close() + + // 201 is specified return status, some registries return + // 200, 202 or 204. + switch resp.StatusCode { + case http.StatusOK, http.StatusCreated, http.StatusNoContent, http.StatusAccepted: + default: + return remoteserrors.NewUnexpectedStatusErr(resp.Response) + } + + status, err := pw.tracker.GetStatus(pw.ref) + if err != nil { + return errors.Wrap(err, "failed to get status") + } + + if size > 0 && size != status.Offset { + return errors.Errorf("unexpected size %d, expected %d", status.Offset, size) + } + + if expected == "" { + expected = status.Expected + } + + actual, err := digest.Parse(resp.Header.Get("Docker-Content-Digest")) + if err != nil { + return errors.Wrap(err, "invalid content digest in response") + } + + if actual != expected { + return errors.Errorf("got digest %s, expected %s", actual, expected) + } + + status.Committed = true + status.UpdatedAt = time.Now() + pw.tracker.SetStatus(pw.ref, status) + + return nil +} + +func (pw *pushRequest) Reader() (io.ReadCloser, error) { + status, err := pw.tracker.GetStatus(pw.ref) + if err != nil { + return nil, err + } + status.Offset = 0 + status.UpdatedAt = time.Now() + pw.tracker.SetStatus(pw.ref, status) + + r, err := pw.source.Reader() + if err != nil { + return nil, err + } + return &sizeTrackingReader{pw, r}, nil +} + +type sizeTrackingReader struct { + pw *pushRequest + io.ReadCloser +} + +func (t *sizeTrackingReader) Read(in []byte) (int, error) { + // fmt.Printf("reading next...\n") + n, err := t.ReadCloser.Read(in) + if n > 0 { + status, err := t.pw.tracker.GetStatus(t.pw.ref) + // fmt.Printf("read %d[%d] bytes\n", n, status.Offset) + if err != nil { + return n, err + } + status.Offset += int64(n) + status.UpdatedAt = time.Now() + t.pw.tracker.SetStatus(t.pw.ref, status) + } + return n, err +} + +func requestWithMountFrom(req *request, mount, from string) *request { + creq := *req + + sep := "?" + if strings.Contains(creq.path, sep) { + sep = "&" + } + + creq.path = creq.path + sep + "mount=" + mount + "&from=" + from + + return &creq +} diff --git a/api/tech/docker/registry.go b/api/tech/docker/registry.go new file mode 100644 index 0000000000..795dd6e244 --- /dev/null +++ b/api/tech/docker/registry.go @@ -0,0 +1,234 @@ +package docker + +import ( + "net" + "net/http" + + "github.com/pkg/errors" +) + +// HostCapabilities represent the capabilities of the registry +// host. This also represents the set of operations for which +// the registry host may be trusted to perform. +// +// For example pushing is a capability which should only be +// performed on an upstream source, not a mirror. +// Resolving (the process of converting a name into a digest) +// must be considered a trusted operation and only done by +// a host which is trusted (or more preferably by secure process +// which can prove the provenance of the mapping). A public +// mirror should never be trusted to do a resolve action. +// +// | Registry Type | Pull | Resolve | Push | +// |------------------|------|---------|------| +// | Public Registry | yes | yes | yes | +// | Private Registry | yes | yes | yes | +// | Public Mirror | yes | no | no | +// | Private Mirror | yes | yes | no |. +type HostCapabilities uint8 + +const ( + // HostCapabilityPull represents the capability to fetch manifests + // and blobs by digest. + HostCapabilityPull HostCapabilities = 1 << iota + + // HostCapabilityResolve represents the capability to fetch manifests + // by name. + HostCapabilityResolve + + // HostCapabilityPush represents the capability to push blobs and + // manifests. + HostCapabilityPush + + // Reserved for future capabilities (i.e. search, catalog, remove). +) + +// Has checks whether the capabilities list has the provide capability. +func (c HostCapabilities) Has(t HostCapabilities) bool { + return c&t == t +} + +// RegistryHost represents a complete configuration for a registry +// host, representing the capabilities, authorizations, connection +// configuration, and location. +type RegistryHost struct { + Client *http.Client + Authorizer Authorizer + Host string + Scheme string + Path string + Capabilities HostCapabilities + Header http.Header +} + +const ( + dockerHostname = "docker.io" + dockerRegistryHostname = "registry-1.docker.io" +) + +func (h RegistryHost) isProxy(refhost string) bool { + if refhost != h.Host { + if refhost != dockerHostname || h.Host != dockerRegistryHostname { + return true + } + } + return false +} + +// RegistryHosts fetches the registry hosts for a given namespace, +// provided by the host component of an distribution image reference. +type RegistryHosts func(string) ([]RegistryHost, error) + +// Registries joins multiple registry configuration functions, using the same +// order as provided within the arguments. When an empty registry configuration +// is returned with a nil error, the next function will be called. +// NOTE: This function will not join configurations, as soon as a non-empty +// configuration is returned from a configuration function, it will be returned +// to the caller. +func Registries(registries ...RegistryHosts) RegistryHosts { + return func(host string) ([]RegistryHost, error) { + for _, registry := range registries { + config, err := registry(host) + if err != nil { + return config, err + } + if len(config) > 0 { + return config, nil + } + } + return nil, nil + } +} + +type registryOpts struct { + authorizer Authorizer + plainHTTP func(string) (bool, error) + host func(string) (string, error) + client *http.Client +} + +// RegistryOpt defines a registry default option. +type RegistryOpt func(*registryOpts) + +// WithPlainHTTP configures registries to use plaintext http scheme +// for the provided host match function. +func WithPlainHTTP(f func(string) (bool, error)) RegistryOpt { + return func(opts *registryOpts) { + opts.plainHTTP = f + } +} + +// WithAuthorizer configures the default authorizer for a registry. +func WithAuthorizer(a Authorizer) RegistryOpt { + return func(opts *registryOpts) { + opts.authorizer = a + } +} + +// WithHostTranslator defines the default translator to use for registry hosts. +func WithHostTranslator(h func(string) (string, error)) RegistryOpt { + return func(opts *registryOpts) { + opts.host = h + } +} + +// WithClient configures the default http client for a registry. +func WithClient(c *http.Client) RegistryOpt { + return func(opts *registryOpts) { + opts.client = c + } +} + +// ConfigureDefaultRegistries is used to create a default configuration for +// registries. For more advanced configurations or per-domain setups, +// the RegistryHosts interface should be used directly. +// NOTE: This function will always return a non-empty value or error. +func ConfigureDefaultRegistries(ropts ...RegistryOpt) RegistryHosts { + var opts registryOpts + for _, opt := range ropts { + opt(&opts) + } + + return func(host string) ([]RegistryHost, error) { + config := RegistryHost{ + Client: opts.client, + Authorizer: opts.authorizer, + Host: host, + Scheme: "https", + Path: "/v2", + Capabilities: HostCapabilityPull | HostCapabilityResolve | HostCapabilityPush, + } + + if config.Client == nil { + config.Client = http.DefaultClient + } + + if opts.plainHTTP != nil { + match, err := opts.plainHTTP(host) + if err != nil { + return nil, err + } + if match { + config.Scheme = "http" + } + } + + if opts.host != nil { + var err error + config.Host, err = opts.host(config.Host) + if err != nil { + return nil, err + } + } else if host == dockerHostname { + config.Host = dockerRegistryHostname + } + + return []RegistryHost{config}, nil + } +} + +// MatchAllHosts is a host match function which is always true. +func MatchAllHosts(string) (bool, error) { + return true, nil +} + +// MatchLocalhost is a host match function which returns true for +// localhost. +// +// Note: this does not handle matching of ip addresses in octal, +// decimal or hex form. +func MatchLocalhost(host string) (bool, error) { + switch { + case host == "::1": + return true, nil + case host == "[::1]": + return true, nil + } + h, p, err := net.SplitHostPort(host) + + // addrError helps distinguish between errors of form + // "no colon in address" and "too many colons in address". + // The former is fine as the host string need not have a + // port. Latter needs to be handled. + addrError := &net.AddrError{ + Err: "missing port in address", + Addr: host, + } + if err != nil { + if err.Error() != addrError.Error() { + return false, err + } + // host string without any port specified + h = host + } else if len(p) == 0 { + return false, errors.New("invalid host name format") + } + + // use ipv4 dotted decimal for further checking + if h == "localhost" { + h = "127.0.0.1" + } + ip := net.ParseIP(h) + + return ip.IsLoopback(), nil +} diff --git a/api/tech/docker/resolve/interface.go b/api/tech/docker/resolve/interface.go new file mode 100644 index 0000000000..8476000012 --- /dev/null +++ b/api/tech/docker/resolve/interface.go @@ -0,0 +1,75 @@ +package resolve + +import ( + "context" + "io" + + "github.com/containerd/containerd/content" + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +// all new and modified + +type Source interface { + Reader() (io.ReadCloser, error) +} + +// Resolver provides remotes based on a locator. +type Resolver interface { + // Resolve attempts to resolve the reference into a name and descriptor. + // + // The argument `ref` should be a scheme-less URI representing the remote. + // Structurally, it has a host and path. The "host" can be used to directly + // reference a specific host or be matched against a specific handler. + // + // The returned name should be used to identify the referenced entity. + // Dependending on the remote namespace, this may be immutable or mutable. + // While the name may differ from ref, it should itself be a valid ref. + // + // If the resolution fails, an error will be returned. + Resolve(ctx context.Context, ref string) (name string, desc ocispec.Descriptor, err error) + + // Fetcher returns a new fetcher for the provided reference. + // All content fetched from the returned fetcher will be + // from the namespace referred to by ref. + Fetcher(ctx context.Context, ref string) (Fetcher, error) + + // Pusher returns a new pusher for the provided reference + // The returned Pusher should satisfy content.Ingester and concurrent attempts + // to push the same blob using the Ingester API should result in ErrUnavailable. + Pusher(ctx context.Context, ref string) (Pusher, error) + + Lister(ctx context.Context, ref string) (Lister, error) +} + +// Fetcher fetches content. +type Fetcher interface { + // Fetch the resource identified by the descriptor. + Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error) +} + +// Pusher pushes content +// don't use write interface of containerd remotes.Pusher. +type Pusher interface { + // Push returns a push request for the given resource identified + // by the descriptor and the given data source. + Push(ctx context.Context, d ocispec.Descriptor, src Source) (PushRequest, error) +} + +type Lister interface { + List(context.Context) ([]string, error) +} + +// PushRequest handles the result of a push request +// replaces containerd content.Writer. +type PushRequest interface { + // Commit commits the blob (but no roll-back is guaranteed on an error). + // size and expected can be zero-value when unknown. + // Commit always closes the writer, even on error. + // ErrAlreadyExists aborts the writer. + Commit(ctx context.Context, size int64, expected digest.Digest, opts ...content.Opt) error + + // Status returns the current state of write + Status() (content.Status, error) +} diff --git a/api/tech/docker/resolver.go b/api/tech/docker/resolver.go new file mode 100644 index 0000000000..292df03ae3 --- /dev/null +++ b/api/tech/docker/resolver.go @@ -0,0 +1,656 @@ +package docker + +import ( + "context" + "fmt" + "io" + "net/http" + "net/url" + "path" + "strings" + + "github.com/containerd/containerd/errdefs" + "github.com/containerd/containerd/images" + "github.com/containerd/containerd/log" + "github.com/containerd/containerd/reference" + "github.com/containerd/containerd/remotes/docker/schema1" + "github.com/containerd/containerd/version" + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "golang.org/x/net/context/ctxhttp" + + "ocm.software/ocm/api/tech/docker/resolve" + "ocm.software/ocm/api/utils/accessio" +) + +var ( + // ErrInvalidAuthorization is used when credentials are passed to a server but + // those credentials are rejected. + ErrInvalidAuthorization = errors.New("authorization failed") + + // MaxManifestSize represents the largest size accepted from a registry + // during resolution. Larger manifests may be accepted using a + // resolution method other than the registry. + // + // NOTE: The max supported layers by some runtimes is 128 and individual + // layers will not contribute more than 256 bytes, making a + // reasonable limit for a large image manifests of 32K bytes. + // 4M bytes represents a much larger upper bound for images which may + // contain large annotations or be non-images. A proper manifest + // design puts large metadata in subobjects, as is consistent the + // intent of the manifest design. + MaxManifestSize int64 = 4 * 1048 * 1048 +) + +// Authorizer is used to authorize HTTP requests based on 401 HTTP responses. +// An Authorizer is responsible for caching tokens or credentials used by +// requests. +type Authorizer interface { + // Authorize sets the appropriate `Authorization` header on the given + // request. + // + // If no authorization is found for the request, the request remains + // unmodified. It may also add an `Authorization` header as + // "bearer " + // "basic " + Authorize(context.Context, *http.Request) error + + // AddResponses adds a 401 response for the authorizer to consider when + // authorizing requests. The last response should be unauthorized and + // the previous requests are used to consider redirects and retries + // that may have led to the 401. + // + // If response is not handled, returns `ErrNotImplemented` + AddResponses(context.Context, []*http.Response) error +} + +// ResolverOptions are used to configured a new Docker register resolver. +type ResolverOptions struct { + // Hosts returns registry host configurations for a namespace. + Hosts RegistryHosts + + // Headers are the HTTP request header fields sent by the resolver + Headers http.Header + + // Tracker is used to track uploads to the registry. This is used + // since the registry does not have upload tracking and the existing + // mechanism for getting blob upload status is expensive. + Tracker StatusTracker + + // Authorizer is used to authorize registry requests + // Deprecated: use Hosts + Authorizer Authorizer + + // Credentials provides username and secret given a host. + // If username is empty but a secret is given, that secret + // is interpreted as a long lived token. + // Deprecated: use Hosts + Credentials func(string) (string, string, error) + + // Host provides the hostname given a namespace. + // Deprecated: use Hosts + Host func(string) (string, error) + + // PlainHTTP specifies to use plain http and not https + // Deprecated: use Hosts + PlainHTTP bool + + // Client is the http client to used when making registry requests + // Deprecated: use Hosts + Client *http.Client +} + +// DefaultHost is the default host function. +func DefaultHost(ns string) (string, error) { + if ns == "docker.io" { + return "registry-1.docker.io", nil + } + return ns, nil +} + +type dockerResolver struct { + hosts RegistryHosts + header http.Header + resolveHeader http.Header + tracker StatusTracker +} + +// NewResolver returns a new resolver to a Docker registry. +func NewResolver(options ResolverOptions) resolve.Resolver { + if options.Tracker == nil { + options.Tracker = NewInMemoryTracker() + } + + if options.Headers == nil { + options.Headers = make(http.Header) + } + if _, ok := options.Headers["User-Agent"]; !ok { + options.Headers.Set("User-Agent", "containerd/"+version.Version) + } + + resolveHeader := http.Header{} + if _, ok := options.Headers["Accept"]; !ok { + // set headers for all the types we support for resolution. + resolveHeader.Set("Accept", strings.Join([]string{ + images.MediaTypeDockerSchema2Manifest, + images.MediaTypeDockerSchema2ManifestList, + ocispec.MediaTypeImageManifest, + ocispec.MediaTypeImageIndex, "*/*", + }, ", ")) + } else { + resolveHeader["Accept"] = options.Headers["Accept"] + delete(options.Headers, "Accept") + } + + if options.Hosts == nil { + opts := []RegistryOpt{} + if options.Host != nil { + opts = append(opts, WithHostTranslator(options.Host)) + } + + if options.Authorizer == nil { + options.Authorizer = NewDockerAuthorizer( + WithAuthClient(options.Client), + WithAuthHeader(options.Headers), + WithAuthCreds(options.Credentials)) + } + opts = append(opts, WithAuthorizer(options.Authorizer)) + + if options.Client != nil { + opts = append(opts, WithClient(options.Client)) + } + if options.PlainHTTP { + opts = append(opts, WithPlainHTTP(MatchAllHosts)) + } else { + opts = append(opts, WithPlainHTTP(MatchLocalhost)) + } + options.Hosts = ConfigureDefaultRegistries(opts...) + } + return &dockerResolver{ + hosts: options.Hosts, + header: options.Headers, + resolveHeader: resolveHeader, + tracker: options.Tracker, + } +} + +func getManifestMediaType(resp *http.Response) string { + // Strip encoding data (manifests should always be ascii JSON) + contentType := resp.Header.Get("Content-Type") + if sp := strings.IndexByte(contentType, ';'); sp != -1 { + contentType = contentType[0:sp] + } + + // As of Apr 30 2019 the registry.access.redhat.com registry does not specify + // the content type of any data but uses schema1 manifests. + if contentType == "text/plain" { + contentType = images.MediaTypeDockerSchema1Manifest + } + return contentType +} + +type countingReader struct { + reader io.Reader + bytesRead int64 +} + +func (r *countingReader) Read(p []byte) (int, error) { + n, err := r.reader.Read(p) + r.bytesRead += int64(n) + return n, err +} + +var _ resolve.Resolver = &dockerResolver{} + +func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocispec.Descriptor, error) { + base, err := r.resolveDockerBase(ref) + if err != nil { + return "", ocispec.Descriptor{}, err + } + refspec := base.refspec + if refspec.Object == "" { + return "", ocispec.Descriptor{}, reference.ErrObjectRequired + } + + var ( + firstErr error + paths [][]string + dgst = refspec.Digest() + caps = HostCapabilityPull + ) + + if dgst != "" { + if err := dgst.Validate(); err != nil { + // need to fail here, since we can't actually resolve the invalid + // digest. + return "", ocispec.Descriptor{}, err + } + + // turns out, we have a valid digest, make a url. + paths = append(paths, []string{"manifests", dgst.String()}) + + // fallback to blobs on not found. + paths = append(paths, []string{"blobs", dgst.String()}) + } else { + // Add + paths = append(paths, []string{"manifests", refspec.Object}) + caps |= HostCapabilityResolve + } + + hosts := base.filterHosts(caps) + if len(hosts) == 0 { + return "", ocispec.Descriptor{}, errors.Wrap(errdefs.ErrNotFound, "no resolve hosts") + } + + ctx, err = ContextWithRepositoryScope(ctx, refspec, false) + if err != nil { + return "", ocispec.Descriptor{}, err + } + + for _, u := range paths { + for _, host := range hosts { + ctxWithLogger := log.WithLogger(ctx, log.G(ctx).WithField("host", host.Host)) + + req := base.request(host, http.MethodHead, u...) + if err := req.addNamespace(base.refspec.Hostname()); err != nil { + return "", ocispec.Descriptor{}, err + } + + for key, value := range r.resolveHeader { + req.header[key] = append(req.header[key], value...) + } + + log.G(ctxWithLogger).Debug("resolving") + resp, err := req.doWithRetries(ctxWithLogger, nil) + if err != nil { + if errors.Is(err, ErrInvalidAuthorization) { + err = errors.Wrapf(err, "pull access denied, repository does not exist or may require authorization") + } else { + err = accessio.RetriableError(err) + } + // Store the error for referencing later + if firstErr == nil { + firstErr = err + } + log.G(ctxWithLogger).WithError(err).Info("trying next host") + continue // try another host + } + resp.Body.Close() // don't care about body contents. + + if resp.StatusCode > 299 { + if resp.StatusCode == http.StatusNotFound { + // log.G(ctxWithLogger).Info("trying next host - response was http.StatusNotFound") + continue + } + if resp.StatusCode > 399 { + // Set firstErr when encountering the first non-404 status code. + if firstErr == nil { + firstErr = errors.Errorf("pulling from host %s failed with status code %v: %v", host.Host, u, resp.Status) + } + continue // try another host + } + return "", ocispec.Descriptor{}, errors.Errorf("pulling from host %s failed with unexpected status code %v: %v", host.Host, u, resp.Status) + } + size := resp.ContentLength + contentType := getManifestMediaType(resp) + + // if no digest was provided, then only a resolve + // trusted registry was contacted, in this case use + // the digest header (or content from GET) + if dgst == "" { + // this is the only point at which we trust the registry. we use the + // content headers to assemble a descriptor for the name. when this becomes + // more robust, we mostly get this information from a secure trust store. + dgstHeader := digest.Digest(resp.Header.Get("Docker-Content-Digest")) + + if dgstHeader != "" && size != -1 { + if err := dgstHeader.Validate(); err != nil { + return "", ocispec.Descriptor{}, errors.Wrapf(err, "%q in header not a valid digest", dgstHeader) + } + dgst = dgstHeader + } + } + if dgst == "" || size == -1 { + log.G(ctxWithLogger).Debug("no Docker-Content-Digest header, fetching manifest instead") + + req = base.request(host, http.MethodGet, u...) + if err := req.addNamespace(base.refspec.Hostname()); err != nil { + return "", ocispec.Descriptor{}, err + } + + for key, value := range r.resolveHeader { + req.header[key] = append(req.header[key], value...) + } + + resp, err := req.doWithRetries(ctxWithLogger, nil) + if err != nil { + return "", ocispec.Descriptor{}, accessio.RetriableError(err) + } + defer resp.Body.Close() + + bodyReader := countingReader{reader: resp.Body} + + contentType = getManifestMediaType(resp) + if dgst == "" { + if contentType == images.MediaTypeDockerSchema1Manifest { + b, err := schema1.ReadStripSignature(&bodyReader) + if err != nil { + return "", ocispec.Descriptor{}, accessio.RetriableError(err) + } + + dgst = digest.FromBytes(b) + } else { + dgst, err = digest.FromReader(&bodyReader) + if err != nil { + return "", ocispec.Descriptor{}, accessio.RetriableError(err) + } + } + } else if _, err := io.Copy(io.Discard, &bodyReader); err != nil { + return "", ocispec.Descriptor{}, accessio.RetriableError(err) + } + size = bodyReader.bytesRead + } + // Prevent resolving to excessively large manifests + if size > MaxManifestSize { + if firstErr == nil { + firstErr = errors.Wrapf(errdefs.ErrNotFound, "rejecting %d byte manifest for %s", size, ref) + } + continue + } + + desc := ocispec.Descriptor{ + Digest: dgst, + MediaType: contentType, + Size: size, + } + + log.G(ctxWithLogger).WithField("desc.digest", desc.Digest).Debug("resolved") + return ref, desc, nil + } + } + + // If above loop terminates without return, then there was an error. + // "firstErr" contains the first non-404 error. That is, "firstErr == nil" + // means that either no registries were given or each registry returned 404. + + if firstErr == nil { + firstErr = errors.Wrap(errdefs.ErrNotFound, ref) + } + + return "", ocispec.Descriptor{}, firstErr +} + +func (r *dockerResolver) Fetcher(ctx context.Context, ref string) (resolve.Fetcher, error) { + base, err := r.resolveDockerBase(ref) + if err != nil { + return nil, err + } + + return dockerFetcher{ + dockerBase: base, + }, nil +} + +func (r *dockerResolver) Pusher(ctx context.Context, ref string) (resolve.Pusher, error) { + base, err := r.resolveDockerBase(ref) + if err != nil { + return nil, err + } + + return dockerPusher{ + dockerBase: base, + object: base.refspec.Object, + tracker: r.tracker, + }, nil +} + +func (r *dockerResolver) resolveDockerBase(ref string) (*dockerBase, error) { + refspec, err := reference.Parse(ref) + if err != nil { + return nil, err + } + + return r.base(refspec) +} + +type dockerBase struct { + refspec reference.Spec + repository string + hosts []RegistryHost + header http.Header +} + +func (r *dockerResolver) base(refspec reference.Spec) (*dockerBase, error) { + host := refspec.Hostname() + hosts, err := r.hosts(host) + if err != nil { + return nil, err + } + return &dockerBase{ + refspec: refspec, + repository: strings.TrimPrefix(refspec.Locator, host+"/"), + hosts: hosts, + header: r.header, + }, nil +} + +func (r *dockerBase) filterHosts(caps HostCapabilities) (hosts []RegistryHost) { + for _, host := range r.hosts { + if host.Capabilities.Has(caps) { + hosts = append(hosts, host) + } + } + return +} + +func (r *dockerBase) request(host RegistryHost, method string, ps ...string) *request { + header := r.header.Clone() + if header == nil { + header = http.Header{} + } + + for key, value := range host.Header { + header[key] = append(header[key], value...) + } + parts := append([]string{"/", host.Path, r.repository}, ps...) + p := path.Join(parts...) + // Join strips trailing slash, re-add ending "/" if included + if len(parts) > 0 && strings.HasSuffix(parts[len(parts)-1], "/") { + p += "/" + } + return &request{ + method: method, + path: p, + header: header, + host: host, + } +} + +func (r *request) authorize(ctx context.Context, req *http.Request) error { + // Check if has header for host + if r.host.Authorizer != nil { + if err := r.host.Authorizer.Authorize(ctx, req); err != nil { + return err + } + } + + return nil +} + +func (r *request) addNamespace(ns string) (err error) { + if !r.host.isProxy(ns) { + return nil + } + var q url.Values + // Parse query + if i := strings.IndexByte(r.path, '?'); i > 0 { + r.path = r.path[:i+1] + q, err = url.ParseQuery(r.path[i+1:]) + if err != nil { + return + } + } else { + r.path += "?" + q = url.Values{} + } + q.Add("ns", ns) + + r.path += q.Encode() + + return +} + +type request struct { + method string + path string + header http.Header + host RegistryHost + body func() (io.ReadCloser, error) + size int64 +} + +func (r *request) do(ctx context.Context) (*http.Response, error) { + u := r.host.Scheme + "://" + r.host.Host + r.path + req, err := http.NewRequestWithContext(ctx, r.method, u, nil) + if err != nil { + return nil, err + } + req.Header = http.Header{} // headers need to be copied to avoid concurrent map access + for k, v := range r.header { + req.Header[k] = v + } + if r.body != nil { + body, err := r.body() + if err != nil { + return nil, err + } + req.Body = body + req.GetBody = r.body + if r.size > 0 { + req.ContentLength = r.size + } + defer body.Close() + } + + ctx = log.WithLogger(ctx, log.G(ctx).WithField("url", u)) + log.G(ctx).WithFields(sanitizedRequestFields(req)).Debug("do request") + if err := r.authorize(ctx, req); err != nil { + return nil, errors.Wrap(err, "failed to authorize") + } + + client := &http.Client{} + if r.host.Client != nil { + *client = *r.host.Client + } + if client.CheckRedirect == nil { + client.CheckRedirect = func(req *http.Request, via []*http.Request) error { + if len(via) >= 10 { + return errors.New("stopped after 10 redirects") + } + return errors.Wrap(r.authorize(ctx, req), "failed to authorize redirect") + } + } + + resp, err := ctxhttp.Do(ctx, client, req) + if err != nil { + return nil, errors.Wrap(err, "failed to do request") + } + log.G(ctx).WithFields(responseFields(resp)).Debug("fetch response received") + return resp, nil +} + +func (r *request) doWithRetries(ctx context.Context, responses []*http.Response) (*http.Response, error) { + resp, err := r.do(ctx) + if err != nil { + return nil, err + } + + responses = append(responses, resp) + retry, err := r.retryRequest(ctx, responses) + if err != nil { + resp.Body.Close() + return nil, err + } + if retry { + resp.Body.Close() + return r.doWithRetries(ctx, responses) + } + return resp, err +} + +func (r *request) retryRequest(ctx context.Context, responses []*http.Response) (bool, error) { + if len(responses) > 5 { + return false, nil + } + last := responses[len(responses)-1] + switch last.StatusCode { + case http.StatusUnauthorized: + log.G(ctx).WithField("header", last.Header.Get("WWW-Authenticate")).Debug("Unauthorized") + if r.host.Authorizer != nil { + if err := r.host.Authorizer.AddResponses(ctx, responses); err == nil { + return true, nil + } else if !errdefs.IsNotImplemented(err) { + return false, err + } + } + + return false, nil + case http.StatusMethodNotAllowed: + // Support registries which have not properly implemented the HEAD method for + // manifests endpoint + if r.method == http.MethodHead && strings.Contains(r.path, "/manifests/") { + r.method = http.MethodGet + return true, nil + } + case http.StatusRequestTimeout, http.StatusTooManyRequests: + return true, nil + } + + // TODO: Handle 50x errors accounting for attempt history + return false, nil +} + +func (r *request) String() string { + return r.host.Scheme + "://" + r.host.Host + r.path +} + +func sanitizedRequestFields(req *http.Request) logrus.Fields { + fields := map[string]interface{}{ + "request.method": req.Method, + } + for k, vals := range req.Header { + k = strings.ToLower(k) + if k == "authorization" { + continue + } + for i, v := range vals { + field := "request.header." + k + if i > 0 { + field = fmt.Sprintf("%s.%d", field, i) + } + fields[field] = v + } + } + + return logrus.Fields(fields) +} + +func responseFields(resp *http.Response) logrus.Fields { + fields := map[string]interface{}{ + "response.status": resp.Status, + } + for k, vals := range resp.Header { + k = strings.ToLower(k) + for i, v := range vals { + field := "response.header." + k + if i > 0 { + field = fmt.Sprintf("%s.%d", field, i) + } + fields[field] = v + } + } + + return logrus.Fields(fields) +} diff --git a/api/version/generate/release_generate.go b/api/version/generate/release_generate.go index d93f2c6a6c..c5549205d8 100644 --- a/api/version/generate/release_generate.go +++ b/api/version/generate/release_generate.go @@ -84,6 +84,8 @@ func main() { switch cmd { case "print-semver": fmt.Print(nonpre) + case "print-major-minor": + fmt.Printf("%d.%d", nonpre.Major(), nonpre.Minor()) case "print-version": fmt.Print(v) case "print-rc-version": @@ -92,14 +94,8 @@ func main() { } else { fmt.Printf("%s-%s", v, pre) } - case "bump-version": - var next string - if nonpre.Patch() > 0 { - next = nonpre.IncPatch().String() - } else { - next = nonpre.IncMinor().String() - } - next += "-dev" + case "bump-minor": + next := nonpre.IncMinor().String() + "-dev" fmt.Printf("%s", next) case "bump-patch": next := nonpre.IncPatch().String() + "-dev" diff --git a/cmds/ocm/commands/controllercmds/uninstall/cmd.go b/cmds/ocm/commands/controllercmds/uninstall/cmd.go index 4261b22772..ef4148bb55 100644 --- a/cmds/ocm/commands/controllercmds/uninstall/cmd.go +++ b/cmds/ocm/commands/controllercmds/uninstall/cmd.go @@ -57,7 +57,7 @@ func (o *Command) ForName(name string) *cobra.Command { // AddFlags for the known item to delete. func (o *Command) AddFlags(set *pflag.FlagSet) { set.StringVarP(&o.Version, "version", "v", "latest", "the version of the controller to install") - set.StringVarP(&o.BaseURL, "base-url", "u", "https://ocm.software/ocm-controller/releases", "the base url to the ocm-controller's release page") + set.StringVarP(&o.BaseURL, "base-url", "u", "https://github.com/ocm-controller/releases", "the base url to the ocm-controller's release page") set.StringVarP(&o.ReleaseAPIURL, "release-api-url", "a", "https://api.github.com/repos/open-component-model/ocm-controller/releases", "the base url to the ocm-controller's API release page") set.StringVar(&o.CertManagerBaseURL, "cert-manager-base-url", "https://github.com/cert-manager/cert-manager/releases", "the base url to the cert-manager's release page") set.StringVar(&o.CertManagerReleaseAPIURL, "cert-manager-release-api-url", "https://api.github.com/repos/cert-manager/cert-manager/releases", "the base url to the cert-manager's API release page") diff --git a/cmds/ocm/commands/misccmds/action/execute/cmd.go b/cmds/ocm/commands/misccmds/action/execute/cmd.go index b669e2c73f..a781ed6511 100644 --- a/cmds/ocm/commands/misccmds/action/execute/cmd.go +++ b/cmds/ocm/commands/misccmds/action/execute/cmd.go @@ -11,6 +11,7 @@ import ( clictx "ocm.software/ocm/api/cli" "ocm.software/ocm/api/credentials" "ocm.software/ocm/api/datacontext/action" + "ocm.software/ocm/api/datacontext/action/api" utils2 "ocm.software/ocm/api/utils" common "ocm.software/ocm/api/utils/misc" "ocm.software/ocm/api/utils/out" @@ -54,11 +55,13 @@ func (o *Command) ForName(name string) *cobra.Command { Args: cobra.MinimumNArgs(1), Long: ` Execute an action extension for a given action specification. The specification -show be a JSON or YAML argument. +should be a JSON or YAML argument. Additional properties settings can be used to describe a consumer id to retrieve credentials for. -`, + +The following actions are supported: +` + api.Usage(api.DefaultRegistry()), Example: ` $ ocm execute action '{ "type": "oci.repository.prepare/v1", "hostname": "...", "repository": "..."}' `, diff --git a/cmds/ocm/commands/ocicmds/common/handlers/artifacthdlr/closure.go b/cmds/ocm/commands/ocicmds/common/handlers/artifacthdlr/closure.go index 978515dbe3..129a018a27 100644 --- a/cmds/ocm/commands/ocicmds/common/handlers/artifacthdlr/closure.go +++ b/cmds/ocm/commands/ocicmds/common/handlers/artifacthdlr/closure.go @@ -52,7 +52,7 @@ func traverse(hist common.History, o *Object, octx out.Context) []output.Object UniformRepositorySpec: o.Spec.UniformRepositorySpec, ArtSpec: oci.ArtSpec{ Repository: o.Spec.Repository, - Digest: &ref.Digest, + ArtVersion: oci.ArtVersion{Digest: &ref.Digest}, }, }, Namespace: o.Namespace, diff --git a/cmds/ocm/commands/ocicmds/common/handlers/artifacthdlr/typehandler.go b/cmds/ocm/commands/ocicmds/common/handlers/artifacthdlr/typehandler.go index aa8aa34801..5d027b3fd8 100644 --- a/cmds/ocm/commands/ocicmds/common/handlers/artifacthdlr/typehandler.go +++ b/cmds/ocm/commands/ocicmds/common/handlers/artifacthdlr/typehandler.go @@ -192,7 +192,7 @@ func (h *TypeHandler) get(repo oci.Repository, elemspec utils.ElemSpec) ([]outpu return result, nil } } else { - art := oci.ArtSpec{Repository: ""} + art := &oci.ArtSpec{Repository: ""} if name != "" { art, err = oci.ParseArt(name) if err != nil { diff --git a/cmds/ocm/commands/ocmcmds/common/addhdlrs/base.go b/cmds/ocm/commands/ocmcmds/common/addhdlrs/base.go index f772472acc..3927286a42 100644 --- a/cmds/ocm/commands/ocmcmds/common/addhdlrs/base.go +++ b/cmds/ocm/commands/ocmcmds/common/addhdlrs/base.go @@ -11,7 +11,10 @@ type ResourceSpecHandlerBase struct { options options.OptionSet } -var _ options.Options = (*ResourceSpecHandlerBase)(nil) +var ( + _ options.Options = (*ResourceSpecHandlerBase)(nil) + _ options.OptionSetProvider = (*ResourceSpecHandlerBase)(nil) +) func NewBase(opts ...options.Options) ResourceSpecHandlerBase { return ResourceSpecHandlerBase{options: opts} @@ -26,7 +29,7 @@ func (h *ResourceSpecHandlerBase) WithCLIOptions(opts ...options.Options) Resour return *h } -func (h *ResourceSpecHandlerBase) GetOptions() options.OptionSet { +func (h *ResourceSpecHandlerBase) AsOptionSet() options.OptionSet { return h.options } @@ -34,6 +37,10 @@ func (h *ResourceSpecHandlerBase) AddFlags(opts *pflag.FlagSet) { h.options.AddFlags(opts) } -func (h *ResourceSpecHandlerBase) GetTargetOpts() []ocm.TargetOption { - return options.FindOptions[ocm.TargetOption](h.options) +func (h *ResourceSpecHandlerBase) GetTargetOpts() []ocm.TargetElementOption { + return options.FindOptions[ocm.TargetElementOption](h.options) +} + +func (h *ResourceSpecHandlerBase) GetElementModificationOpts() []ocm.ElementModificationOption { + return options.FindOptions[ocm.ElementModificationOption](h.options) } diff --git a/cmds/ocm/commands/ocmcmds/common/addhdlrs/comp/components.go b/cmds/ocm/commands/ocmcmds/common/addhdlrs/comp/components.go index 7f423468fc..05d6139249 100644 --- a/cmds/ocm/commands/ocmcmds/common/addhdlrs/comp/components.go +++ b/cmds/ocm/commands/ocmcmds/common/addhdlrs/comp/components.go @@ -1,6 +1,8 @@ package comp import ( + "fmt" + "github.com/mandelsoft/goutils/errors" "github.com/mandelsoft/goutils/finalizer" "github.com/mandelsoft/goutils/set" @@ -21,9 +23,13 @@ func ProcessComponents(ctx clictx.Context, ictx inputs.Context, repo ocm.Reposit for _, elem := range elems { if r, ok := elem.Spec().(*ResourceSpec); ok { + if len(r.References) > 0 && len(r.OldReferences) > 0 { + return fmt.Errorf("only field references or componentReferences (deprecated) is possible") + } list.Add(addhdlrs.ValidateElementSpecIdentities("resource", elem.Source().String(), sliceutils.Convert[addhdlrs.ElementSpec](r.Resources))) list.Add(addhdlrs.ValidateElementSpecIdentities("source", elem.Source().String(), sliceutils.Convert[addhdlrs.ElementSpec](r.Sources))) list.Add(addhdlrs.ValidateElementSpecIdentities("reference", elem.Source().String(), sliceutils.Convert[addhdlrs.ElementSpec](r.References))) + list.Add(addhdlrs.ValidateElementSpecIdentities("reference", elem.Source().String(), sliceutils.Convert[addhdlrs.ElementSpec](r.OldReferences))) } } if err := list.Result(); err != nil { diff --git a/cmds/ocm/commands/ocmcmds/common/addhdlrs/comp/elements.go b/cmds/ocm/commands/ocmcmds/common/addhdlrs/comp/elements.go index bd47976aae..c5fdf9a3b3 100644 --- a/cmds/ocm/commands/ocmcmds/common/addhdlrs/comp/elements.go +++ b/cmds/ocm/commands/ocmcmds/common/addhdlrs/comp/elements.go @@ -21,6 +21,7 @@ import ( "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/addhdlrs/rscs" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/addhdlrs/srcs" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/inputs" + "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/schemaoption" "ocm.software/ocm/cmds/ocm/common/options" "ocm.software/ocm/cmds/ocm/common/utils" ) @@ -34,7 +35,7 @@ type ResourceSpecHandler struct { srchandler *srcs.ResourceSpecHandler refhandler *refs.ResourceSpecHandler version string - schema string + schema *schemaoption.Option } var ( @@ -42,20 +43,25 @@ var ( _ options.Options = (*ResourceSpecHandler)(nil) ) -func New(v string, schema string, opts ...ocm.ModificationOption) *ResourceSpecHandler { +func New(opts ...ocm.ModificationOption) *ResourceSpecHandler { return &ResourceSpecHandler{ rschandler: rscs.New(opts...), srchandler: srcs.New(), refhandler: refs.New(), - version: v, - schema: schema, + schema: schemaoption.New(compdesc.DefaultSchemeVersion), } } +func (h *ResourceSpecHandler) AsOptionSet() options.OptionSet { + return options.OptionSet{h.rschandler.AsOptionSet(), h.srchandler.AsOptionSet(), h.refhandler.AsOptionSet(), h.schema} +} + func (h *ResourceSpecHandler) AddFlags(fs *pflag.FlagSet) { h.rschandler.AddFlags(fs) h.srchandler.AddFlags(fs) h.refhandler.AddFlags(fs) + fs.StringVarP(&h.version, "version", "v", "", "default version for components") + h.schema.AddFlags(fs) } func (h *ResourceSpecHandler) WithCLIOptions(opts ...options.Options) *ResourceSpecHandler { @@ -111,14 +117,14 @@ func (h *ResourceSpecHandler) Add(ctx clictx.Context, ictx inputs.Context, elem cd := cv.GetDescriptor() - opts := h.srchandler.GetOptions()[0].(*addhdlrs.Options) + opts := h.srchandler.AsOptionSet()[0].(*addhdlrs.Options) if !opts.Replace { cd.Resources = nil cd.Sources = nil cd.References = nil } - schema := h.schema + schema := h.schema.Schema if r.Meta.ConfiguredVersion != "" { schema = r.Meta.ConfiguredVersion } @@ -143,10 +149,18 @@ func (h *ResourceSpecHandler) Add(ctx clictx.Context, ictx inputs.Context, elem if err != nil { return err } + + if len(r.References) > 0 && len(r.OldReferences) > 0 { + return fmt.Errorf("only field references or componentReferences (deprecated) is possible") + } err = handle(ctx, ictx, elem.Source(), cv, r.References, h.refhandler) if err != nil { return err } + err = handle(ctx, ictx, elem.Source(), cv, r.OldReferences, h.refhandler) + if err != nil { + return err + } return comp.AddVersion(cv) } @@ -169,7 +183,10 @@ type ResourceSpec struct { // Sources defines sources that produced the component Sources []*srcs.ResourceSpec `json:"sources"` // References references component dependencies that can be resolved in the current context. - References []*refs.ResourceSpec `json:"componentReferences"` + References []*refs.ResourceSpec `json:"references"` + // OldReferences references component dependencies that can be resolved in the current context. + // Deprecated: use field References. + OldReferences []*refs.ResourceSpec `json:"componentReferences"` // Resources defines all resources that are created by the component and by a third party. Resources []*rscs.ResourceSpec `json:"resources"` } diff --git a/cmds/ocm/commands/ocmcmds/common/addhdlrs/options.go b/cmds/ocm/commands/ocmcmds/common/addhdlrs/options.go index e9614db216..2297e16847 100644 --- a/cmds/ocm/commands/ocmcmds/common/addhdlrs/options.go +++ b/cmds/ocm/commands/ocmcmds/common/addhdlrs/options.go @@ -8,30 +8,59 @@ import ( ) type Options struct { + // Replace enables to replace existing elements (same raw identity) with a different version instead + // of appending a new element. Replace bool + // PreserveSignature disables the modification of signature relevant information. + PreserveSignature bool } -var _ ocm.ModificationOption = (*Options)(nil) +var ( + _ ocm.ModificationOption = (*Options)(nil) + _ ocm.ElementModificationOption = (*Options)(nil) + _ ocm.BlobModificationOption = (*Options)(nil) + _ ocm.TargetElementOption = (*Options)(nil) +) func (o *Options) AddFlags(fs *pflag.FlagSet) { - f := fs.Lookup("replace") + o.addBoolFlag(fs, &o.Replace, "replace", "R", false, "replace existing elements") + o.addBoolFlag(fs, &o.PreserveSignature, "preserve-signature", "P", false, "preserve existing signatures") +} + +func (o *Options) addBoolFlag(fs *pflag.FlagSet, p *bool, long string, short string, def bool, usage string) { + f := fs.Lookup(long) if f != nil { - if bp := generics.Cast[*bool](f.Value); bp != nil { - return + if f.Value.Type() != "bool" { + f = nil } } - fs.BoolVarP(&o.Replace, "replace", "R", false, "replace existing elements") + if f == nil { + fs.BoolVarP(p, long, short, def, usage) + } +} + +func (o *Options) applyPreserve(opts *ocm.ElementModificationOptions) { + if !o.PreserveSignature { + opts.ModifyElement = generics.Pointer(true) + } } func (o *Options) ApplyBlobModificationOption(opts *ocm.BlobModificationOptions) { - o.ApplyTargetOption(&opts.TargetOptions) + o.applyPreserve(&opts.ElementModificationOptions) + o.ApplyTargetOption(&opts.TargetElementOptions) } func (o *Options) ApplyModificationOption(opts *ocm.ModificationOptions) { - o.ApplyTargetOption(&opts.TargetOptions) + o.applyPreserve(&opts.ElementModificationOptions) + o.ApplyTargetOption(&opts.TargetElementOptions) } -func (o *Options) ApplyTargetOption(opts *ocm.TargetOptions) { +func (o *Options) ApplyElementModificationOption(opts *ocm.ElementModificationOptions) { + o.applyPreserve(opts) + o.ApplyTargetOption(&opts.TargetElementOptions) +} + +func (o *Options) ApplyTargetOption(opts *ocm.TargetElementOptions) { if !o.Replace { opts.TargetElement = ocm.AppendElement } @@ -41,6 +70,9 @@ func (o *Options) Description() string { return ` The --replace option allows users to specify whether adding an element with the same name and extra identity but different version as an -existing element append (false) or replace (true) the existing element. +existing element, append (false) or replace (true) the existing element. + +The --preserve-signature option prohibits changes of signature +relevant elements. ` } diff --git a/cmds/ocm/commands/ocmcmds/common/addhdlrs/refs/elements.go b/cmds/ocm/commands/ocmcmds/common/addhdlrs/refs/elements.go index 3c529accd7..ddab09974b 100644 --- a/cmds/ocm/commands/ocmcmds/common/addhdlrs/refs/elements.go +++ b/cmds/ocm/commands/ocmcmds/common/addhdlrs/refs/elements.go @@ -66,7 +66,8 @@ func (h *ResourceSpecHandler) Set(v ocm.ComponentVersionAccess, r addhdlrs.Eleme }, ComponentName: spec.ComponentName, } - return v.SetReference(meta, h.GetTargetOpts()...) + + return v.SetReference(meta, h.GetElementModificationOpts()...) } //////////////////////////////////////////////////////////////////////////////// diff --git a/cmds/ocm/commands/ocmcmds/common/addhdlrs/rscs/elements.go b/cmds/ocm/commands/ocmcmds/common/addhdlrs/rscs/elements.go index 1cd98d2392..c51fa9d12e 100644 --- a/cmds/ocm/commands/ocmcmds/common/addhdlrs/rscs/elements.go +++ b/cmds/ocm/commands/ocmcmds/common/addhdlrs/rscs/elements.go @@ -48,7 +48,7 @@ func (h *ResourceSpecHandler) WithCLIOptions(opts ...options.Options) *ResourceS } func (h *ResourceSpecHandler) getModOpts() []ocm.ModificationOption { - opts := options.FindOptions[ocm.ModificationOption](h.GetOptions()) + opts := options.FindOptions[ocm.ModificationOption](h.AsOptionSet()) if h.opts != nil { opts = append(opts, h.opts) } @@ -63,7 +63,7 @@ func (*ResourceSpecHandler) RequireInputs() bool { return true } -func (ResourceSpecHandler) Decode(data []byte) (addhdlrs.ElementSpec, error) { +func (*ResourceSpecHandler) Decode(data []byte) (addhdlrs.ElementSpec, error) { var desc ResourceSpec err := runtime.DefaultYAMLEncoding.Unmarshal(data, &desc) if err != nil { @@ -72,7 +72,7 @@ func (ResourceSpecHandler) Decode(data []byte) (addhdlrs.ElementSpec, error) { return &desc, nil } -func (h ResourceSpecHandler) Set(v ocm.ComponentVersionAccess, r addhdlrs.Element, acc compdesc.AccessSpec) error { +func (h *ResourceSpecHandler) Set(v ocm.ComponentVersionAccess, r addhdlrs.Element, acc compdesc.AccessSpec) error { spec, ok := r.Spec().(*ResourceSpec) if !ok { return fmt.Errorf("element spec is not a valid resource spec, failed to assert type %T to ResourceSpec", r.Spec()) @@ -102,9 +102,14 @@ func (h ResourceSpecHandler) Set(v ocm.ComponentVersionAccess, r addhdlrs.Elemen SourceRefs: compdescv2.ConvertSourcerefsTo(spec.SourceRefs), } opts := h.getModOpts() - if ocm.IsIntermediate(v.Repository().GetSpecification()) { - opts = append(opts, ocm.ModifyResource()) + if spec.SkipDigestGeneration { + opts = append(opts, ocm.SkipDigest()) //nolint:staticcheck // skip digest still used for tests } + /* + if ocm.IsIntermediate(v.Repository().GetSpecification()) { + opts = append(opts, ocm.ModifyElement()) + } + */ return v.SetResource(meta, acc, opts...) } @@ -126,6 +131,11 @@ type ResourceSpec struct { SourceRefs []compdescv2.SourceRef `json:"srcRefs"` addhdlrs.ResourceInput `json:",inline"` + + // additional process related options + + // SkipDigestGeneration omits the digest generation. + SkipDigestGeneration bool `json:"skipDigestGeneration,omitempty"` } var _ addhdlrs.ElementSpec = (*ResourceSpec)(nil) diff --git a/cmds/ocm/commands/ocmcmds/common/addhdlrs/srcs/elements.go b/cmds/ocm/commands/ocmcmds/common/addhdlrs/srcs/elements.go index 6ab83fca1d..9d8dced1dc 100644 --- a/cmds/ocm/commands/ocmcmds/common/addhdlrs/srcs/elements.go +++ b/cmds/ocm/commands/ocmcmds/common/addhdlrs/srcs/elements.go @@ -29,11 +29,11 @@ func New(opts ...options.Options) *ResourceSpecHandler { return &ResourceSpecHandler{addhdlrs.NewBase(opts...)} } -func (ResourceSpecHandler) Key() string { +func (*ResourceSpecHandler) Key() string { return "source" } -func (ResourceSpecHandler) RequireInputs() bool { +func (*ResourceSpecHandler) RequireInputs() bool { return true } diff --git a/cmds/ocm/commands/ocmcmds/common/options/downloaderoption/option.go b/cmds/ocm/commands/ocmcmds/common/options/downloaderoption/option.go index 3c214aa4b7..0552c854c1 100644 --- a/cmds/ocm/commands/ocmcmds/common/options/downloaderoption/option.go +++ b/cmds/ocm/commands/ocmcmds/common/options/downloaderoption/option.go @@ -31,7 +31,7 @@ type Option struct { func (o *Option) Register(ctx ocm.ContextProvider) error { for _, s := range o.Registrations { err := download.RegisterHandlerByName(ctx.OCMContext(), s.Name, s.Config, - download.ForArtifactType(s.ArtifactType), download.ForMimeType(s.MediaType)) + download.ForArtifactType(s.ArtifactType), download.ForMimeType(s.MediaType), download.WithPrio(s.GetPriority(download.DEFAULT_BLOBHANDLER_PRIO*3))) if err != nil { return err } diff --git a/cmds/ocm/commands/ocmcmds/common/options/downloaderoption/option_test.go b/cmds/ocm/commands/ocmcmds/common/options/downloaderoption/option_test.go new file mode 100644 index 0000000000..6cdfa5a10c --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/common/options/downloaderoption/option_test.go @@ -0,0 +1,80 @@ +package downloaderoption_test + +import ( + . "github.com/mandelsoft/goutils/testutils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/mandelsoft/goutils/generics" + "github.com/spf13/pflag" + + "ocm.software/ocm/api/ocm" + me "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/downloaderoption" +) + +var _ = Describe("Downloader Option Test Environment", func() { + var o *me.Option + var fs *pflag.FlagSet + + BeforeEach(func() { + o = me.New(ocm.DefaultContext()) + fs = &pflag.FlagSet{} + o.AddFlags(fs) + }) + + It("handles all parts", func() { + MustBeSuccessful(fs.Parse([]string{"--downloader", `bla/blub:a:b:10={"k":"v"}`})) + MustBeSuccessful(o.Configure(nil)) + Expect(len(o.Registrations)).To(Equal(1)) + Expect(o.Registrations[0].Prio).To(Equal(generics.Pointer(10))) + Expect(o.Registrations[0].Name).To(Equal("bla/blub")) + Expect(o.Registrations[0].ArtifactType).To(Equal("a")) + Expect(o.Registrations[0].MediaType).To(Equal("b")) + Expect(o.Registrations[0].Config).To(Equal([]byte(`{"k":"v"}`))) + Expect("").To(Equal("")) + }) + + It("handles empty parts", func() { + MustBeSuccessful(fs.Parse([]string{"--downloader", `bla/blub:::10={"k":"v"}`})) + MustBeSuccessful(o.Configure(nil)) + Expect(len(o.Registrations)).To(Equal(1)) + Expect(o.Registrations[0].Prio).To(Equal(generics.Pointer(10))) + Expect(o.Registrations[0].Name).To(Equal("bla/blub")) + Expect(o.Registrations[0].ArtifactType).To(Equal("")) + Expect(o.Registrations[0].MediaType).To(Equal("")) + Expect(o.Registrations[0].Config).To(Equal([]byte(`{"k":"v"}`))) + }) + + It("handles art/media/config", func() { + MustBeSuccessful(fs.Parse([]string{"--downloader", `bla/blub:a:b={"k":"v"}`})) + MustBeSuccessful(o.Configure(nil)) + Expect(len(o.Registrations)).To(Equal(1)) + Expect(o.Registrations[0].Prio).To(BeNil()) + Expect(o.Registrations[0].Name).To(Equal("bla/blub")) + Expect(o.Registrations[0].ArtifactType).To(Equal("a")) + Expect(o.Registrations[0].MediaType).To(Equal("b")) + Expect(o.Registrations[0].Config).To(Equal([]byte(`{"k":"v"}`))) + }) + + It("handles art/media/empty config", func() { + MustBeSuccessful(fs.Parse([]string{"--downloader", `bla/blub:a:b`})) + MustBeSuccessful(o.Configure(nil)) + Expect(len(o.Registrations)).To(Equal(1)) + Expect(o.Registrations[0].Prio).To(BeNil()) + Expect(o.Registrations[0].Name).To(Equal("bla/blub")) + Expect(o.Registrations[0].ArtifactType).To(Equal("a")) + Expect(o.Registrations[0].MediaType).To(Equal("b")) + Expect(o.Registrations[0].Config).To(BeNil()) + }) + + It("handles empty config", func() { + MustBeSuccessful(fs.Parse([]string{"--downloader", `bla/blub`})) + MustBeSuccessful(o.Configure(nil)) + Expect(len(o.Registrations)).To(Equal(1)) + Expect(o.Registrations[0].Prio).To(BeNil()) + Expect(o.Registrations[0].Name).To(Equal("bla/blub")) + Expect(o.Registrations[0].ArtifactType).To(Equal("")) + Expect(o.Registrations[0].MediaType).To(Equal("")) + Expect(o.Registrations[0].Config).To(BeNil()) + }) +}) diff --git a/cmds/ocm/commands/ocmcmds/common/options/downloaderoption/suite_test.go b/cmds/ocm/commands/ocmcmds/common/options/downloaderoption/suite_test.go new file mode 100644 index 0000000000..7774df74a1 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/common/options/downloaderoption/suite_test.go @@ -0,0 +1,13 @@ +package downloaderoption_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "downloader option Test Suite") +} diff --git a/cmds/ocm/commands/ocmcmds/common/options/optutils/reg_test.go b/cmds/ocm/commands/ocmcmds/common/options/optutils/reg_test.go index edca672cdb..829b42c17e 100644 --- a/cmds/ocm/commands/ocmcmds/common/options/optutils/reg_test.go +++ b/cmds/ocm/commands/ocmcmds/common/options/optutils/reg_test.go @@ -1,8 +1,6 @@ package optutils_test import ( - "encoding/json" - . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -37,7 +35,7 @@ var _ = Describe("registration options", func() { Name: "plugin/name", ArtifactType: "art", MediaType: "media", - Config: json.RawMessage(`{"name":"Name"}`), + Config: []byte(`{"name":"Name"}`), }})) }) @@ -49,7 +47,7 @@ var _ = Describe("registration options", func() { Name: "plugin/name", ArtifactType: "art", MediaType: "", - Config: json.RawMessage(`{"name":"Name"}`), + Config: []byte(`{"name":"Name"}`), }})) }) @@ -61,7 +59,7 @@ var _ = Describe("registration options", func() { Name: "plugin/name", ArtifactType: "", MediaType: "", - Config: json.RawMessage(`{"name":"Name"}`), + Config: []byte(`{"name":"Name"}`), }})) }) @@ -73,7 +71,7 @@ var _ = Describe("registration options", func() { Name: "plugin/name", ArtifactType: "", MediaType: "", - Config: json.RawMessage(`{"name":"Name"}`), + Config: []byte(`{"name":"Name"}`), }})) }) @@ -85,7 +83,7 @@ var _ = Describe("registration options", func() { Name: "plugin/name", ArtifactType: "", MediaType: "", - Config: json.RawMessage(`"Name"`), + Config: []byte(`"Name"`), }})) }) @@ -101,12 +99,12 @@ var _ = Describe("registration options", func() { Name: "plugin/name", ArtifactType: "", MediaType: "", - Config: json.RawMessage(`{"name":"Name"}`), + Config: []byte(`{"name":"Name"}`), }})) }) It("fails", func() { MustBeSuccessful(flags.Parse([]string{`--test`, `plugin/name:::=Name`})) - MustFailWithMessage(opt.Configure(ctx), "invalid test registration plugin/name::: must be of "+optutils.RegistrationFormat) + MustFailWithMessage(opt.Configure(ctx), "invalid test registration plugin/name::: (invalid priority) must be of "+optutils.RegistrationFormat) }) }) diff --git a/cmds/ocm/commands/ocmcmds/common/options/optutils/registration.go b/cmds/ocm/commands/ocmcmds/common/options/optutils/registration.go index aaa7040419..44b0da8344 100644 --- a/cmds/ocm/commands/ocmcmds/common/options/optutils/registration.go +++ b/cmds/ocm/commands/ocmcmds/common/options/optutils/registration.go @@ -3,9 +3,11 @@ package optutils import ( "encoding/json" "fmt" + "strconv" "strings" "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/goutils/generics" "github.com/spf13/pflag" "sigs.k8s.io/yaml" @@ -18,7 +20,15 @@ type Registration struct { Name string ArtifactType string MediaType string - Config json.RawMessage + Prio *int + Config interface{} +} + +func (r *Registration) GetPriority(def int) int { + if r.Prio != nil { + return *r.Prio + } + return def } func NewRegistrationOption(name, short, desc, usage string) RegistrationOption { @@ -34,7 +44,7 @@ type RegistrationOption struct { Registrations []*Registration } -const RegistrationFormat = "[:[:]]== 0 { art = nam[i+1:] nam = nam[:i] - i = strings.Index(art, ":") - if i >= 0 { - med = art[i+1:] - art = art[:i] - i = strings.Index(med, ":") - if i >= 0 { - return fmt.Errorf("invalid %s registration %s must be of %s", o.name, n, RegistrationFormat) - } + } + i = strings.Index(art, ":") + if i >= 0 { + med = art[i+1:] + art = art[:i] + } + i = strings.Index(med, ":") + if i >= 0 { + p := med[i+1:] + med = med[:i] + + v, err := strconv.ParseInt(p, 10, 32) + if err != nil { + return fmt.Errorf("invalid %s registration %s (invalid priority) must be of %s", o.name, n, RegistrationFormat) } + prio = generics.Pointer(int(v)) + } + i = strings.Index(med, ":") + if i >= 0 { + return fmt.Errorf("invalid %s registration %s must be of %s", o.name, n, RegistrationFormat) } - var data json.RawMessage + var data interface{} var raw []byte var err error if strings.HasPrefix(v, "@") { @@ -73,7 +95,9 @@ func (o *RegistrationOption) Configure(ctx clictx.Context) error { return errors.Wrapf(err, "cannot read %s config from %q", o.name, v[1:]) } } else { - raw = []byte(v) + if v != "" { + raw = []byte(v) + } } if len(raw) > 0 { @@ -91,6 +115,7 @@ func (o *RegistrationOption) Configure(ctx clictx.Context) error { Name: nam, ArtifactType: art, MediaType: med, + Prio: prio, Config: data, }) } diff --git a/cmds/ocm/commands/ocmcmds/common/options/uploaderoption/option.go b/cmds/ocm/commands/ocmcmds/common/options/uploaderoption/option.go index 4386201744..a5fa584f05 100644 --- a/cmds/ocm/commands/ocmcmds/common/options/uploaderoption/option.go +++ b/cmds/ocm/commands/ocmcmds/common/options/uploaderoption/option.go @@ -29,7 +29,7 @@ type Option struct { func (o *Option) Register(ctx ocm.ContextProvider) error { for _, s := range o.Registrations { err := blobhandler.RegisterHandlerByName(ctx.OCMContext(), s.Name, s.Config, - blobhandler.ForArtifactType(s.ArtifactType), blobhandler.ForMimeType(s.MediaType)) + blobhandler.ForArtifactType(s.ArtifactType), blobhandler.ForMimeType(s.MediaType), blobhandler.WithPrio(s.GetPriority(blobhandler.DEFAULT_BLOBHANDLER_PRIO*3))) if err != nil { return err } diff --git a/cmds/ocm/commands/ocmcmds/common/options/uploaderoption/option_test.go b/cmds/ocm/commands/ocmcmds/common/options/uploaderoption/option_test.go new file mode 100644 index 0000000000..91a4fd78c9 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/common/options/uploaderoption/option_test.go @@ -0,0 +1,80 @@ +package uploaderoption_test + +import ( + . "github.com/mandelsoft/goutils/testutils" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/mandelsoft/goutils/generics" + "github.com/spf13/pflag" + + "ocm.software/ocm/api/ocm" + me "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/uploaderoption" +) + +var _ = Describe("Downloader Option Test Environment", func() { + var o *me.Option + var fs *pflag.FlagSet + + BeforeEach(func() { + o = me.New(ocm.DefaultContext()) + fs = &pflag.FlagSet{} + o.AddFlags(fs) + }) + + It("handles all parts", func() { + MustBeSuccessful(fs.Parse([]string{"--uploader", `bla/blub:a:b:10={"k":"v"}`})) + MustBeSuccessful(o.Configure(nil)) + Expect(len(o.Registrations)).To(Equal(1)) + Expect(o.Registrations[0].Prio).To(Equal(generics.Pointer(10))) + Expect(o.Registrations[0].Name).To(Equal("bla/blub")) + Expect(o.Registrations[0].ArtifactType).To(Equal("a")) + Expect(o.Registrations[0].MediaType).To(Equal("b")) + Expect(o.Registrations[0].Config).To(Equal([]byte(`{"k":"v"}`))) + Expect("").To(Equal("")) + }) + + It("handles empty parts", func() { + MustBeSuccessful(fs.Parse([]string{"--uploader", `bla/blub:::10={"k":"v"}`})) + MustBeSuccessful(o.Configure(nil)) + Expect(len(o.Registrations)).To(Equal(1)) + Expect(o.Registrations[0].Prio).To(Equal(generics.Pointer(10))) + Expect(o.Registrations[0].Name).To(Equal("bla/blub")) + Expect(o.Registrations[0].ArtifactType).To(Equal("")) + Expect(o.Registrations[0].MediaType).To(Equal("")) + Expect(o.Registrations[0].Config).To(Equal([]byte(`{"k":"v"}`))) + }) + + It("handles art/media/config", func() { + MustBeSuccessful(fs.Parse([]string{"--uploader", `bla/blub:a:b={"k":"v"}`})) + MustBeSuccessful(o.Configure(nil)) + Expect(len(o.Registrations)).To(Equal(1)) + Expect(o.Registrations[0].Prio).To(BeNil()) + Expect(o.Registrations[0].Name).To(Equal("bla/blub")) + Expect(o.Registrations[0].ArtifactType).To(Equal("a")) + Expect(o.Registrations[0].MediaType).To(Equal("b")) + Expect(o.Registrations[0].Config).To(Equal([]byte(`{"k":"v"}`))) + }) + + It("handles art/media/empty config", func() { + MustBeSuccessful(fs.Parse([]string{"--uploader", `bla/blub:a:b`})) + MustBeSuccessful(o.Configure(nil)) + Expect(len(o.Registrations)).To(Equal(1)) + Expect(o.Registrations[0].Prio).To(BeNil()) + Expect(o.Registrations[0].Name).To(Equal("bla/blub")) + Expect(o.Registrations[0].ArtifactType).To(Equal("a")) + Expect(o.Registrations[0].MediaType).To(Equal("b")) + Expect(o.Registrations[0].Config).To(BeNil()) + }) + + It("handles empty config", func() { + MustBeSuccessful(fs.Parse([]string{"--uploader", `bla/blub`})) + MustBeSuccessful(o.Configure(nil)) + Expect(len(o.Registrations)).To(Equal(1)) + Expect(o.Registrations[0].Prio).To(BeNil()) + Expect(o.Registrations[0].Name).To(Equal("bla/blub")) + Expect(o.Registrations[0].ArtifactType).To(Equal("")) + Expect(o.Registrations[0].MediaType).To(Equal("")) + Expect(o.Registrations[0].Config).To(BeNil()) + }) +}) diff --git a/cmds/ocm/commands/ocmcmds/common/options/uploaderoption/uploader_test.go b/cmds/ocm/commands/ocmcmds/common/options/uploaderoption/uploader_test.go index 8dbd4404e7..5942457560 100644 --- a/cmds/ocm/commands/ocmcmds/common/options/uploaderoption/uploader_test.go +++ b/cmds/ocm/commands/ocmcmds/common/options/uploaderoption/uploader_test.go @@ -1,8 +1,6 @@ package uploaderoption import ( - "encoding/json" - . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -33,7 +31,7 @@ var _ = Describe("uploader option", func() { Name: "plugin/name", ArtifactType: "art", MediaType: "media", - Config: json.RawMessage(`{"name":"Name"}`), + Config: []byte(`{"name":"Name"}`), }})) }) @@ -45,7 +43,7 @@ var _ = Describe("uploader option", func() { Name: "plugin/name", ArtifactType: "art", MediaType: "", - Config: json.RawMessage(`{"name":"Name"}`), + Config: []byte(`{"name":"Name"}`), }})) }) @@ -57,7 +55,7 @@ var _ = Describe("uploader option", func() { Name: "plugin/name", ArtifactType: "", MediaType: "", - Config: json.RawMessage(`{"name":"Name"}`), + Config: []byte(`{"name":"Name"}`), }})) }) @@ -69,7 +67,7 @@ var _ = Describe("uploader option", func() { Name: "plugin/name", ArtifactType: "", MediaType: "", - Config: json.RawMessage(`{"name":"Name"}`), + Config: []byte(`{"name":"Name"}`), }})) }) @@ -81,12 +79,12 @@ var _ = Describe("uploader option", func() { Name: "plugin/name", ArtifactType: "", MediaType: "", - Config: json.RawMessage(`"Name"`), + Config: []byte(`"Name"`), }})) }) It("fails", func() { - MustBeSuccessful(flags.Parse([]string{`--uploader`, `plugin/name:::=Name`})) - MustFailWithMessage(opt.Configure(ctx), "invalid uploader registration plugin/name::: must be of "+optutils.RegistrationFormat) + MustBeSuccessful(flags.Parse([]string{`--uploader`, `plugin/name:::0:=Name`})) + MustFailWithMessage(opt.Configure(ctx), "invalid uploader registration plugin/name:::0: (invalid priority) must be of "+optutils.RegistrationFormat) }) }) diff --git a/cmds/ocm/commands/ocmcmds/common/utils.go b/cmds/ocm/commands/ocmcmds/common/utils.go index 5537d634b1..b5324d114c 100644 --- a/cmds/ocm/commands/ocmcmds/common/utils.go +++ b/cmds/ocm/commands/ocmcmds/common/utils.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/mandelsoft/goutils/errors" + "github.com/mandelsoft/goutils/general" clictx "ocm.software/ocm/api/cli" "ocm.software/ocm/api/ocm" @@ -66,20 +67,29 @@ func MapArgsToIdentityPattern(args ...string) (metav1.Identity, error) { //////////////////////////////////////////////////////////////////////////////// +// OptionWithSessionCompleter describes the interface for option objects requiring +// a completion with a session. type OptionWithSessionCompleter interface { CompleteWithSession(ctx clictx.OCM, session ocm.Session) error } -func CompleteOptionsWithSession(ctx clictx.Context, session ocm.Session) options.OptionsProcessor { +// CompleteOptionsWithSession provides an options.OptionsProcessor completing +// options by passing a session object using the OptionWithSessionCompleter interface. +// If an optional argument true is given, it also tries the other standard completion +// methods possible for an options object. +func CompleteOptionsWithSession(ctx clictx.Context, session ocm.Session, all ...bool) options.OptionsProcessor { + otherCompleters := general.Optional(all...) return func(opt options.Options) error { if c, ok := opt.(OptionWithSessionCompleter); ok { return c.CompleteWithSession(ctx.OCM(), session) } - if c, ok := opt.(options.OptionWithCLIContextCompleter); ok { - return c.Configure(ctx) - } - if c, ok := opt.(options.SimpleOptionCompleter); ok { - return c.Complete() + if otherCompleters { + if c, ok := opt.(options.OptionWithCLIContextCompleter); ok { + return c.Configure(ctx) + } + if c, ok := opt.(options.SimpleOptionCompleter); ok { + return c.Complete() + } } return nil } diff --git a/cmds/ocm/commands/ocmcmds/components/add/cmd.go b/cmds/ocm/commands/ocmcmds/components/add/cmd.go index cc2309a940..5032ea8cec 100644 --- a/cmds/ocm/commands/ocmcmds/components/add/cmd.go +++ b/cmds/ocm/commands/ocmcmds/components/add/cmd.go @@ -11,7 +11,6 @@ import ( clictx "ocm.software/ocm/api/cli" "ocm.software/ocm/api/ocm" - "ocm.software/ocm/api/ocm/compdesc" "ocm.software/ocm/api/ocm/extensions/repositories/ctf" "ocm.software/ocm/api/ocm/tools/transfer/transferhandler/standard" "ocm.software/ocm/api/utils/accessio" @@ -25,7 +24,6 @@ import ( "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/fileoption" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/lookupoption" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/rscbyvalueoption" - "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/schemaoption" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/templateroption" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/options/uploaderoption" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/names" @@ -48,23 +46,23 @@ type Command struct { FormatHandler ctf.FormatHandler Format string - Version string + Handler *comp.ResourceSpecHandler Envs []string Archive string - Options addhdlrs.Options - Elements []addhdlrs.ElementSource } func NewCommand(ctx clictx.Context, names ...string) *cobra.Command { + hdlr := comp.New().WithCLIOptions(&addhdlrs.Options{}) return utils.SetupCommand(&Command{ + Handler: hdlr, BaseCommand: utils.NewBaseCommand(ctx, + hdlr, formatoption.New(ctf.GetFormats()...), fileoption.New("transport-archive"), - schemaoption.New(compdesc.DefaultSchemeVersion), templateroption.New(""), dryrunoption.New("evaluate and print component specifications", true), lookupoption.New(), @@ -155,12 +153,10 @@ Various elements support to add arbitrary information by using labels func (o *Command) AddFlags(fs *pflag.FlagSet) { o.BaseCommand.AddFlags(fs) - o.Options.AddFlags(fs) fs.BoolVarP(&o.Force, "force", "f", false, "remove existing content") fs.BoolVarP(&o.Create, "create", "c", false, "(re)create archive") fs.BoolVarP(&o.Closure, "complete", "C", false, "include all referenced component version") fs.StringArrayVarP(&o.Envs, "settings", "s", nil, "settings file with variable settings (yaml)") - fs.StringVarP(&o.Version, "version", "v", "", "default version for components") } func (o *Command) Complete(args []string) error { @@ -209,8 +205,7 @@ func (o *Command) Run() error { printer := common2.NewPrinter(o.Context.StdOut()) fs := o.Context.FileSystem() - h := comp.New(o.Version, schemaoption.From(o).Schema).WithCLIOptions(&o.Options) - elems, ictx, err := addhdlrs.ProcessDescriptions(o.Context, printer, templateroption.From(o).Options, h, o.Elements) + elems, ictx, err := addhdlrs.ProcessDescriptions(o.Context, printer, templateroption.From(o).Options, o.Handler, o.Elements) if err != nil { return err } @@ -250,7 +245,7 @@ func (o *Command) Run() error { } if err == nil { - err = comp.ProcessComponents(o.Context, ictx, repo, general.Conditional(o.Closure, lookupoption.From(o).Resolver, nil), thdlr, h, elems) + err = comp.ProcessComponents(o.Context, ictx, repo, general.Conditional(o.Closure, lookupoption.From(o).Resolver, nil), thdlr, o.Handler, elems) cerr := repo.Close() if err == nil { err = cerr diff --git a/cmds/ocm/commands/ocmcmds/components/add/cmd_test.go b/cmds/ocm/commands/ocmcmds/components/add/cmd_test.go index 22435d1afb..df8269cda5 100644 --- a/cmds/ocm/commands/ocmcmds/components/add/cmd_test.go +++ b/cmds/ocm/commands/ocmcmds/components/add/cmd_test.go @@ -1,6 +1,7 @@ package add_test import ( + "github.com/mandelsoft/goutils/general" . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -16,7 +17,7 @@ import ( "ocm.software/ocm/api/ocm/extensions/accessmethods/ociartifact" resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes" "ocm.software/ocm/api/ocm/extensions/repositories/ctf" - ocmutils "ocm.software/ocm/api/ocm/ocmutils" + "ocm.software/ocm/api/ocm/ocmutils" "ocm.software/ocm/api/ocm/valuemergehandler/handlers/defaultmerge" "ocm.software/ocm/api/utils/accessio" "ocm.software/ocm/api/utils/accessobj" @@ -35,7 +36,7 @@ const ( OUT = "/tmp/res" ) -func CheckComponent(env *TestEnv, handler func(ocm.Repository)) { +func CheckComponent(env *TestEnv, handler func(ocm.Repository), tests ...func(cv ocm.ComponentVersionAccess)) { repo := Must(ctf.Open(env.OCMContext(), accessobj.ACC_READONLY, ARCH, 0, env)) defer Close(repo) cv := Must(repo.LookupComponentVersion("ocm.software/demo/test", "1.0.0")) @@ -73,6 +74,10 @@ func CheckComponent(env *TestEnv, handler func(ocm.Repository)) { if handler != nil { handler(repo) } + + for _, t := range tests { + t(cv) + } } var _ = Describe("Test Environment", func() { @@ -92,12 +97,29 @@ var _ = Describe("Test Environment", func() { CheckComponent(env, nil) }) + It("creates ctf and adds component (deprecated)", func() { + Expect(env.Execute("add", "c", "-fc", "--file", ARCH, "testdata/component-constructor-old.yaml")).To(Succeed()) + Expect(env.DirExists(ARCH)).To(BeTrue()) + CheckComponent(env, nil) + }) + It("creates ctf and adds components", func() { Expect(env.Execute("add", "c", "-fc", "--file", ARCH, "--version", "1.0.0", "testdata/component-constructor.yaml")).To(Succeed()) Expect(env.DirExists(ARCH)).To(BeTrue()) CheckComponent(env, nil) }) + It("creates ctf and adds components without digests", func() { + Expect(env.Execute("add", "c", "--skip-digest-generation", "-fc", "--file", ARCH, "--version", "1.0.0", "testdata/component-constructor.yaml")).To(Succeed()) + Expect(env.DirExists(ARCH)).To(BeTrue()) + CheckComponent(env, nil, noDigest("data"), noDigest("text")) + }) + It("creates ctf and adds components without digest for one resource", func() { + Expect(env.Execute("add", "c", "-fc", "--file", ARCH, "--version", "1.0.0", "testdata/component-constructor-skip.yaml")).To(Succeed()) + Expect(env.DirExists(ARCH)).To(BeTrue()) + CheckComponent(env, nil, noDigest("data", false), noDigest("text")) + }) + Context("failures", func() { It("rejects adding duplicate components", func() { ExpectError(env.Execute("add", "c", "-fc", "--file", ARCH, "--version", "1.0.0", "testdata/components-dup.yaml")).To( @@ -170,3 +192,15 @@ var _ = Describe("Test Environment", func() { }) }) }) + +func noDigest(name string, skips ...bool) func(cv ocm.ComponentVersionAccess) { + skip := general.OptionalDefaultedBool(true, skips...) + return func(cv ocm.ComponentVersionAccess) { + r := MustWithOffset(1, Calling(cv.GetResource(metav1.Identity{"name": name}))) + if skip { + ExpectWithOffset(1, r.Meta().Digest).To(BeNil()) + } else { + ExpectWithOffset(1, r.Meta().Digest).NotTo(BeNil()) + } + } +} diff --git a/cmds/ocm/commands/ocmcmds/components/add/testdata/component-constructor-old.yaml b/cmds/ocm/commands/ocmcmds/components/add/testdata/component-constructor-old.yaml new file mode 100644 index 0000000000..fe09093721 --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/components/add/testdata/component-constructor-old.yaml @@ -0,0 +1,34 @@ +name: ocm.software/demo/test +version: 1.0.0 +provider: + name: ocm.software + labels: + - name: city + value: Karlsruhe +labels: + - name: purpose + value: test + +resources: + - name: text + type: PlainText + labels: + - name: city + value: Karlsruhe + merge: + algorithm: default + config: + overwrite: inbound + input: + type: file + path: testdata + - name: data + type: PlainText + input: + type: binary + data: IXN0cmluZ2RhdGE= + +componentReferences: + - name: ref + version: v1 + componentName: github.com/mandelsoft/test2 diff --git a/cmds/ocm/commands/ocmcmds/components/add/testdata/component-constructor-skip.yaml b/cmds/ocm/commands/ocmcmds/components/add/testdata/component-constructor-skip.yaml new file mode 100644 index 0000000000..bd8c88c4dd --- /dev/null +++ b/cmds/ocm/commands/ocmcmds/components/add/testdata/component-constructor-skip.yaml @@ -0,0 +1,35 @@ +name: ocm.software/demo/test +version: 1.0.0 +provider: + name: ocm.software + labels: + - name: city + value: Karlsruhe +labels: + - name: purpose + value: test + +resources: + - name: text + type: PlainText + skipDigestGeneration: true + labels: + - name: city + value: Karlsruhe + merge: + algorithm: default + config: + overwrite: inbound + input: + type: file + path: testdata + - name: data + type: PlainText + input: + type: binary + data: IXN0cmluZ2RhdGE= + +references: + - name: ref + version: v1 + componentName: github.com/mandelsoft/test2 diff --git a/cmds/ocm/commands/ocmcmds/components/add/testdata/component-constructor.yaml b/cmds/ocm/commands/ocmcmds/components/add/testdata/component-constructor.yaml index fe09093721..5145f331ed 100644 --- a/cmds/ocm/commands/ocmcmds/components/add/testdata/component-constructor.yaml +++ b/cmds/ocm/commands/ocmcmds/components/add/testdata/component-constructor.yaml @@ -28,7 +28,7 @@ resources: type: binary data: IXN0cmluZ2RhdGE= -componentReferences: +references: - name: ref version: v1 componentName: github.com/mandelsoft/test2 diff --git a/cmds/ocm/commands/ocmcmds/components/add/testdata/component-dup-ref.yaml b/cmds/ocm/commands/ocmcmds/components/add/testdata/component-dup-ref.yaml index e8bd8c4a6c..c1969bf374 100644 --- a/cmds/ocm/commands/ocmcmds/components/add/testdata/component-dup-ref.yaml +++ b/cmds/ocm/commands/ocmcmds/components/add/testdata/component-dup-ref.yaml @@ -28,7 +28,7 @@ resources: type: binary data: IXN0cmluZ2RhdGE= -componentReferences: +references: - name: ref version: v1 componentName: github.com/mandelsoft/test2 diff --git a/cmds/ocm/commands/ocmcmds/components/add/testdata/component-dup-res.yaml b/cmds/ocm/commands/ocmcmds/components/add/testdata/component-dup-res.yaml index e7349b9876..b314503dac 100644 --- a/cmds/ocm/commands/ocmcmds/components/add/testdata/component-dup-res.yaml +++ b/cmds/ocm/commands/ocmcmds/components/add/testdata/component-dup-res.yaml @@ -33,7 +33,7 @@ resources: type: binary data: IXN0cmluZ2RhdGE= -componentReferences: +references: - name: ref version: v1 componentName: github.com/mandelsoft/test2 diff --git a/cmds/ocm/commands/ocmcmds/components/add/testdata/component-dup-src.yaml b/cmds/ocm/commands/ocmcmds/components/add/testdata/component-dup-src.yaml index 5179cb01ff..05444e9c9b 100644 --- a/cmds/ocm/commands/ocmcmds/components/add/testdata/component-dup-src.yaml +++ b/cmds/ocm/commands/ocmcmds/components/add/testdata/component-dup-src.yaml @@ -40,7 +40,7 @@ resources: type: binary data: IXN0cmluZ2RhdGE= -componentReferences: +references: - name: ref version: v1 componentName: github.com/mandelsoft/test2 diff --git a/cmds/ocm/commands/ocmcmds/components/add/testdata/components-dup.yaml b/cmds/ocm/commands/ocmcmds/components/add/testdata/components-dup.yaml index 7b48dec46f..d887b96007 100644 --- a/cmds/ocm/commands/ocmcmds/components/add/testdata/components-dup.yaml +++ b/cmds/ocm/commands/ocmcmds/components/add/testdata/components-dup.yaml @@ -27,7 +27,7 @@ components: input: type: binary data: IXN0cmluZ2RhdGE= - componentReferences: + references: - name: ref version: v1 componentName: github.com/mandelsoft/test2 diff --git a/cmds/ocm/commands/ocmcmds/components/add/testdata/components.yaml b/cmds/ocm/commands/ocmcmds/components/add/testdata/components.yaml index c19b40404e..3933e6a4fd 100644 --- a/cmds/ocm/commands/ocmcmds/components/add/testdata/components.yaml +++ b/cmds/ocm/commands/ocmcmds/components/add/testdata/components.yaml @@ -27,7 +27,7 @@ components: input: type: binary data: IXN0cmluZ2RhdGE= - componentReferences: + references: - name: ref version: v1 componentName: github.com/mandelsoft/test2 \ No newline at end of file diff --git a/cmds/ocm/commands/ocmcmds/components/hash/cmd.go b/cmds/ocm/commands/ocmcmds/components/hash/cmd.go index 43922b6ff0..b2ef1f9932 100644 --- a/cmds/ocm/commands/ocmcmds/components/hash/cmd.go +++ b/cmds/ocm/commands/ocmcmds/components/hash/cmd.go @@ -13,6 +13,7 @@ import ( "ocm.software/ocm/api/ocm" "ocm.software/ocm/api/ocm/compdesc" common "ocm.software/ocm/api/utils/misc" + "ocm.software/ocm/api/utils/out" "ocm.software/ocm/cmds/ocm/commands/common/options/closureoption" ocmcommon "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common" "ocm.software/ocm/cmds/ocm/commands/ocmcmds/common/handlers/comphdlr" @@ -180,17 +181,26 @@ func (h *action) Close() error { func (h *action) Out() error { if len(h.norms) > 1 { - dir := h.mode.outfile - dir = strings.TrimSuffix(dir, ".ncd") - err := h.ctx.FileSystem().Mkdir(dir, 0o755) - if err != nil { - return fmt.Errorf("cannot create output dir %s", dir) - } - for k, n := range h.norms { - p := filepath.Join(dir, k.String()) - err := h.write(p+".ncd", n) + if h.mode.outfile == "" || h.mode.outfile == "-" { + for _, n := range h.norms { + err := h.write(h.mode.outfile, n) + if err != nil { + return err + } + } + } else { + dir := h.mode.outfile + dir = strings.TrimSuffix(dir, ".ncd") + err := h.ctx.FileSystem().Mkdir(dir, 0o755) if err != nil { - return err + return fmt.Errorf("cannot create output dir %s", dir) + } + for k, n := range h.norms { + p := filepath.Join(dir, k.String()) + err := h.write(p+".ncd", n) + if err != nil { + return err + } } } } else { @@ -202,12 +212,17 @@ func (h *action) Out() error { } func (h *action) write(p, n string) error { - dir := filepath.Dir(p) - err := h.ctx.FileSystem().MkdirAll(dir, 0o755) - if err != nil { - return fmt.Errorf("cannot create dir %s", dir) + if p == "" || p == "-" { + out.Outln(h.ctx, n) + return nil + } else { + dir := filepath.Dir(p) + err := h.ctx.FileSystem().MkdirAll(dir, 0o755) + if err != nil { + return fmt.Errorf("cannot create dir %s", dir) + } + return vfs.WriteFile(h.ctx.FileSystem(), p, []byte(n), 0o644) } - return vfs.WriteFile(h.ctx.FileSystem(), p, []byte(n), 0o644) } ///////// diff --git a/cmds/ocm/commands/ocmcmds/components/hash/cmd_test.go b/cmds/ocm/commands/ocmcmds/components/hash/cmd_test.go index b0a4396a20..f79e2c1c05 100644 --- a/cmds/ocm/commands/ocmcmds/components/hash/cmd_test.go +++ b/cmds/ocm/commands/ocmcmds/components/hash/cmd_test.go @@ -2,17 +2,19 @@ package hash_test import ( "bytes" + "crypto/sha256" + "encoding/hex" "fmt" . "github.com/mandelsoft/goutils/testutils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "ocm.software/ocm/api/ocm/extensions/repositories/ctf" . "ocm.software/ocm/cmds/ocm/testhelper" "ocm.software/ocm/api/ocm/compdesc" metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1" resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes" + "ocm.software/ocm/api/ocm/extensions/repositories/ctf" "ocm.software/ocm/api/utils/accessio" "ocm.software/ocm/api/utils/mime" ) @@ -50,6 +52,48 @@ test.de/x v1 37f7f500d87f4b0a8765649f7c047db382e272b73e042805131df57279991b `)) }) + It("normalize component archive v1", func() { + env.ComponentArchive(ARCH, accessio.FormatDirectory, COMP, VERSION, func() { + env.Provider(PROVIDER) + }) + + buf := bytes.NewBuffer(nil) + Expect(env.CatchOutput(buf).Execute("hash", "components", ARCH, "-O", "-", "-o", "norm")).To(Succeed()) + Expect(buf.String()).To(Equal(`[{"component":[{"componentReferences":[]},{"name":"test.de/x"},{"provider":"mandelsoft"},{"resources":[]},{"version":"v1"}]},{"meta":[{"schemaVersion":"v2"}]}] +`)) + }) + + It("normalize component archive v2", func() { + env.ComponentArchive(ARCH, accessio.FormatDirectory, COMP, VERSION, func() { + env.Provider(PROVIDER) + }) + + buf := bytes.NewBuffer(nil) + Expect(env.CatchOutput(buf).Execute("hash", "components", ARCH, "-N", "jsonNormalisation/v2", "-o", "norm")).To(Succeed()) + Expect(buf.String()).To(StringEqualTrimmedWithContext(`{"component":{"componentReferences":[],"name":"test.de/x","provider":{"name":"mandelsoft"},"resources":[],"sources":[],"version":"v1"}} +`)) + }) + + It("check hash", func() { + env.ComponentArchive(ARCH, accessio.FormatDirectory, COMP, VERSION, func() { + env.Provider(PROVIDER) + }) + + buf := bytes.NewBuffer(nil) + Expect(env.CatchOutput(buf).Execute("hash", "components", ARCH, "-o", "yaml")).To(Succeed()) + Expect(buf.String()).To(StringEqualTrimmedWithContext(` +--- +component: test.de/x +context: [] +hash: 37f7f500d87f4b0a8765649f7c047db382e272b73e042805131df57279991b2b +normalized: '[{"component":[{"componentReferences":[]},{"name":"test.de/x"},{"provider":"mandelsoft"},{"resources":[]},{"version":"v1"}]},{"meta":[{"schemaVersion":"v2"}]}]' +version: v1 +`)) + + h := sha256.Sum256([]byte(`[{"component":[{"componentReferences":[]},{"name":"test.de/x"},{"provider":"mandelsoft"},{"resources":[]},{"version":"v1"}]},{"meta":[{"schemaVersion":"v2"}]}]`)) + Expect(hex.EncodeToString(h[:])).To(Equal("37f7f500d87f4b0a8765649f7c047db382e272b73e042805131df57279991b2b")) + }) + It("hash component archive with resources", func() { env.ComponentArchive(ARCH, accessio.FormatDirectory, COMP, VERSION, func() { env.Provider(PROVIDER) diff --git a/cmds/ocm/commands/ocmcmds/components/hash/options.go b/cmds/ocm/commands/ocmcmds/components/hash/options.go index d3165c732b..99346e88a2 100644 --- a/cmds/ocm/commands/ocmcmds/components/hash/options.go +++ b/cmds/ocm/commands/ocmcmds/components/hash/options.go @@ -33,7 +33,7 @@ func (o *Option) AddFlags(fs *pflag.FlagSet) { fs.BoolVarP(&o.Actual, "actual", "", false, "use actual component descriptor") fs.BoolVarP(&o.Update, "update", "U", false, "update digests in component version") fs.BoolVarP(&o.Verify, "verify", "V", false, "verify digests found in component version") - fs.StringVarP(&o.outfile, "outfile", "O", "norm.ncd", "Output file for normalized component descriptor") + fs.StringVarP(&o.outfile, "outfile", "O", "-", "Output file for normalized component descriptor") } func (o *Option) Complete(cmd *Command) error { diff --git a/cmds/ocm/commands/ocmcmds/resources/download/cmd_test.go b/cmds/ocm/commands/ocmcmds/resources/download/cmd_test.go index e3aea958ca..6aab6733e6 100644 --- a/cmds/ocm/commands/ocmcmds/resources/download/cmd_test.go +++ b/cmds/ocm/commands/ocmcmds/resources/download/cmd_test.go @@ -78,6 +78,28 @@ var _ = Describe("Test Environment", func() { Expect(env.ReadFile(OUT)).To(Equal([]byte("testdata"))) }) + It("registers download handler without config", func() { + env.OCMCommonTransport(ARCH, accessio.FormatDirectory, func() { + env.Component(COMP, func() { + env.Version(VERSION, func() { + env.Provider(PROVIDER) + env.Resource("testdata", "", "PlainText", metav1.LocalRelation, func() { + env.BlobStringData(mime.MIME_TEXT, "testdata") + }) + }) + }) + }) + + buf := bytes.NewBuffer(nil) + Expect(env.CatchOutput(buf).Execute("download", "resources", "--downloader", "helm/artifact:helm/v1", "-O", OUT, ARCH)).To(Succeed()) + Expect(buf.String()).To(StringEqualTrimmedWithContext( + ` +/tmp/res: 8 byte(s) written +`)) + Expect(env.FileExists(OUT)).To(BeTrue()) + Expect(env.ReadFile(OUT)).To(Equal([]byte("testdata"))) + }) + Context("with closure", func() { BeforeEach(func() { env.OCMCommonTransport(ARCH, accessio.FormatDirectory, func() { diff --git a/cmds/ocm/commands/verbs/install/cmd.go b/cmds/ocm/commands/verbs/install/cmd.go index ea1ca8661f..87cbb3dbb3 100644 --- a/cmds/ocm/commands/verbs/install/cmd.go +++ b/cmds/ocm/commands/verbs/install/cmd.go @@ -12,7 +12,7 @@ import ( // NewCommand creates a new command. func NewCommand(ctx clictx.Context) *cobra.Command { cmd := utils.MassageCommand(&cobra.Command{ - Short: "Install elements.", + Short: "Install new OCM CLI components ", }, verbs.Install) cmd.AddCommand(plugins.NewCommand(ctx)) return cmd diff --git a/cmds/ocm/common/options/interfaces.go b/cmds/ocm/common/options/interfaces.go index e48cbad36c..68caba0413 100644 --- a/cmds/ocm/common/options/interfaces.go +++ b/cmds/ocm/common/options/interfaces.go @@ -10,16 +10,24 @@ import ( "ocm.software/ocm/api/utils/out" ) +// OptionsProcessor is handler used to process all +// option found in a set of options. type OptionsProcessor func(Options) error +// SimpleOptionCompleter describes the interface for an option object +// requirung completion without any further information. type SimpleOptionCompleter interface { Complete() error } +// OptionWithOutputContextCompleter describes the interface for an option object +// requirung completion with an output context. type OptionWithOutputContextCompleter interface { Complete(ctx out.Context) error } +// OptionWithCLIContextCompleter describes the interface for an option object +// requirung completion with a CLI context. type OptionWithCLIContextCompleter interface { Configure(ctx clictx.Context) error } @@ -144,6 +152,8 @@ func (s OptionSet) Get(proto interface{}) bool { return false } +// ProcessOnOptions processes all options found in the option set +// woth a given OptionsProcessor. func (s OptionSet) ProcessOnOptions(f OptionsProcessor) error { for _, n := range s { var err error diff --git a/components/demoplugin/Makefile b/components/demoplugin/Makefile index b10d4e36d3..43b8b58034 100644 --- a/components/demoplugin/Makefile +++ b/components/demoplugin/Makefile @@ -4,6 +4,7 @@ GITHUBORG ?= open-component-model COMPONENT = $(PROVIDER)/plugins/$(NAME) OCMREPO ?= ghcr.io/$(GITHUBORG)/ocm PLATFORMS = linux/amd64 linux/arm64 +CTF_TYPE ?= directory REPO_ROOT := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))/../.. VERSION = $(shell go run ../../api/version/generate/release_generate.go print-rc-version $(CANDIDATE)) @@ -12,13 +13,25 @@ EFFECTIVE_VERSION = $(VERSION)+$(COMMIT) GIT_TREE_STATE := $(shell [ -z "$$(git status --porcelain 2>/dev/null)" ] && echo clean || echo dirty) CMDSRCS=$(shell find $(REPO_ROOT)/cmds/$(NAME) -type f) -OCMSRCS=$(shell find $(REPO_ROOT)/pkg -type f) $(REPO_ROOT)/go.* +OCMSRCS=$(shell find $(REPO_ROOT)/api -type f) $(REPO_ROOT)/go.* CREDS ?= -OCM = go run $(REPO_ROOT)/cmds/ocm $(CREDS) +# Define the path to the binary +OCM_BIN = $(REPO_ROOT)/bin/ocm + +# Rule to build the binary if it doesn't exist or if the source code has changed +$(OCM_BIN): $(REPO_ROOT)/cmds/ocm/main.go + mkdir -p $(REPO_ROOT)/bin + go build -ldflags $(BUILD_FLAGS) -o $(OCM_BIN) $(REPO_ROOT)/cmds/ocm + +# Use the binary for the OCM command +OCM = $(OCM_BIN) $(CREDS) GEN = $(REPO_ROOT)/gen/$(NAME) + $(GEN): + @mkdir -p $(GEN) + NOW := $(shell date -u +%FT%T%z) BUILD_FLAGS := "-s -w \ -X ocm.software/ocm/api/version.gitVersion=$(EFFECTIVE_VERSION) \ @@ -26,53 +39,56 @@ BUILD_FLAGS := "-s -w \ -X ocm.software/ocm/api/version.gitCommit=$(COMMIT) \ -X ocm.software/ocm/api/version.buildDate=$(NOW)" - .PHONY: build build: $(GEN)/build -$(GEN)/build: $(CMDSRCS) $(OCMSRCS) +$(GEN)/build: $(GEN) $(CMDSRCS) $(OCMSRCS) @for i in $(PLATFORMS); do \ tag=$$(echo $$i | sed -e s:/:-:g); \ echo GOARCH=$$(basename $$i) GOOS=$$(dirname $$i) CGO_ENABLED=0 go build -ldflags $(BUILD_FLAGS) -o $(GEN)/$(NAME).$$tag ../../cmds/$(NAME); \ - GOARCH=$$(basename $$i) GOOS=$$(dirname $$i) CGO_ENABLED=0 go build -ldflags $(BUILD_FLAGS) -o $(GEN)/$(NAME).$$tag ../../cmds/$(NAME); \ - done + GOARCH=$$(basename $$i) GOOS=$$(dirname $$i) CGO_ENABLED=0 go build -ldflags $(BUILD_FLAGS) -o $(GEN)/$(NAME).$$tag ../../cmds/$(NAME) & \ + done; \ + wait @touch $(GEN)/build - .PHONY: ctf ctf: $(GEN)/ctf -$(GEN)/ctf: $(GEN)/ca.done +$(GEN)/ctf: $(OCM_BIN) $(GEN)/.exists $(GEN)/build component-constructor.yaml $(CHARTSRCS) @rm -rf "$(GEN)/ctf" - $(OCM) transfer ca $(GEN)/ca $(GEN)/ctf + $(OCM) add componentversions \ + --create \ + --file $(GEN)/ctf \ + --type $(CTF_TYPE) \ + --templater=spiff \ + COMPONENT="$(COMPONENT)" \ + NAME="$(NAME)" \ + VERSION="$(VERSION)" \ + PROVIDER="$(PROVIDER)" \ + COMMIT="$(COMMIT)" \ + GEN="$(GEN)" \ + PLATFORMS="$(PLATFORMS)" \ + component-constructor.yaml touch "$(GEN)/ctf" .PHONY: version version: @echo $(VERSION) -.PHONY: ca -ca: $(GEN)/ca.done - -$(GEN)/ca.done: $(GEN)/.exists $(GEN)/build resources.yaml $(CHARTSRCS) - $(OCM) create ca -f $(COMPONENT) "$(VERSION)" --provider $(PROVIDER) --file $(GEN)/ca - $(OCM) add resources --templater=spiff --file $(GEN)/ca NAME="$(NAME)" VERSION="$(VERSION)" COMMIT="$(COMMIT)" GEN="$(GEN)" PLATFORMS="$(PLATFORMS)" resources.yaml - @touch $(GEN)/ca.done - .PHONY: push push: $(GEN)/ctf $(GEN)/push.$(NAME) -$(GEN)/push.$(NAME): $(GEN)/ctf +$(GEN)/push.$(NAME): $(GEN)/ctf $(OCM_BIN) $(OCM) transfer ctf -f $(GEN)/ctf $(OCMREPO) @touch $(GEN)/push.$(NAME) .PHONY: plain-push -plain-push: $(GEN) +plain-push: $(GEN) $(OCM_BIN) $(OCM) transfer ctf -f $(GEN)/ctf $(OCMREPO) @touch $(GEN)/push.$(NAME) .PHONY: transport -transport: +transport: $(OCM_BIN) ifneq ($(TARGETREPO),) $(OCM) transfer component -Vc $(OCMREPO)//$(COMPONENT):$(VERSION) $(TARGETREPO) endif @@ -88,12 +104,12 @@ info: @echo "COMMIT; $(COMMIT)" .PHONY: describe -describe: $(GEN)/ctf - ocm get resources --lookup $(OCMREPO) -c -o treewide $(GEN)/ctf +describe: $(GEN)/ctf $(OCM_BIN) + $(OCM) get resources --lookup $(OCMREPO) -r -o treewide $(GEN)/ctf .PHONY: descriptor -descriptor: $(GEN)/ctf - ocm get component -S v3alpha1 -o yaml $(GEN)/ctf +descriptor: $(GEN)/ctf $(OCM_BIN) + $(OCM) get component -S v3alpha1 -o yaml $(GEN)/ctf .PHONY: clean clean: diff --git a/components/demoplugin/component-constructor.yaml b/components/demoplugin/component-constructor.yaml new file mode 100644 index 0000000000..253566936f --- /dev/null +++ b/components/demoplugin/component-constructor.yaml @@ -0,0 +1,23 @@ +--- +helper: + <<<: (( &temporary )) + executable: + <<<: (( &template )) + name: demo + type: ocmPlugin + version: (( values.VERSION )) + extraIdentity: + os: ((dirname(p) )) + architecture: (( basename(p) )) + input: + type: file + # Generate the path to the plugin binary by looking into the base path and encoding the platform + path: (( values.GEN "/" values.NAME "." replace(p,"/","-") )) + +components: + - name: (( values.COMPONENT)) + version: (( values.VERSION)) + provider: + name: (( values.PROVIDER)) + # use all platforms and create a resource for each + resources: (( map[split(" ", values.PLATFORMS)|p|-> *helper.executable] )) \ No newline at end of file diff --git a/components/demoplugin/resources.yaml b/components/demoplugin/resources.yaml deleted file mode 100644 index ed51a5f1e9..0000000000 --- a/components/demoplugin/resources.yaml +++ /dev/null @@ -1,18 +0,0 @@ ---- -helper: - <<<: (( &temporary )) - executable: - <<<: (( &template )) - name: demo - type: ocmPlugin - version: (( values.VERSION )) - extraIdentity: - os: ((dirname(p) )) - architecture: (( basename(p) )) - input: - type: file - path: (( values.GEN "/" values.NAME "." replace(p,"/","-") )) - - -resources: (( map[split(" ", values.PLATFORMS)|p|-> *helper.executable] )) - diff --git a/components/ecrplugin/Makefile b/components/ecrplugin/Makefile index 3f037f8f21..ff96ab485f 100644 --- a/components/ecrplugin/Makefile +++ b/components/ecrplugin/Makefile @@ -4,6 +4,7 @@ GITHUBORG ?= open-component-model COMPONENT = $(PROVIDER)/plugins/$(NAME) OCMREPO ?= ghcr.io/$(GITHUBORG)/ocm PLATFORMS = linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 +CTF_TYPE ?= directory REPO_ROOT := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))/../.. @@ -13,13 +14,25 @@ EFFECTIVE_VERSION = $(VERSION)+$(COMMIT) GIT_TREE_STATE := $(shell [ -z "$$(git status --porcelain 2>/dev/null)" ] && echo clean || echo dirty) CMDSRCS=$(shell find $(REPO_ROOT)/cmds/$(NAME) -type f) -OCMSRCS=$(shell find $(REPO_ROOT)/pkg -type f) $(REPO_ROOT)/go.* +OCMSRCS=$(shell find $(REPO_ROOT)/api -type f) $(REPO_ROOT)/go.* CREDS ?= -OCM = go run $(REPO_ROOT)/cmds/ocm $(CREDS) +# Define the path to the binary +OCM_BIN = $(REPO_ROOT)/bin/ocm + +# Rule to build the binary if it doesn't exist or if the source code has changed +$(OCM_BIN): $(REPO_ROOT)/cmds/ocm/main.go + mkdir -p $(REPO_ROOT)/bin + go build -ldflags $(BUILD_FLAGS) -o $(OCM_BIN) $(REPO_ROOT)/cmds/ocm + +# Use the binary for the OCM command +OCM = $(OCM_BIN) $(CREDS) GEN = $(REPO_ROOT)/gen/$(NAME) + $(GEN): + @mkdir -p $(GEN) + NOW := $(shell date -u +%FT%T%z) BUILD_FLAGS := "-s -w \ -X ocm.software/ocm/api/version.gitVersion=$(EFFECTIVE_VERSION) \ @@ -27,58 +40,57 @@ BUILD_FLAGS := "-s -w \ -X ocm.software/ocm/api/version.gitCommit=$(COMMIT) \ -X ocm.software/ocm/api/version.buildDate=$(NOW)" - .PHONY: build build: $(GEN)/build -$(GEN)/build: $(CMDSRCS) $(OCMSRCS) +$(GEN)/build: $(GEN) $(CMDSRCS) $(OCMSRCS) @for i in $(PLATFORMS); do \ tag=$$(echo $$i | sed -e s:/:-:g); \ echo GOARCH=$$(basename $$i) GOOS=$$(dirname $$i) CGO_ENABLED=0 go build -ldflags $(BUILD_FLAGS) -o $(GEN)/$(NAME).$$tag ../../cmds/$(NAME); \ - GOARCH=$$(basename $$i) GOOS=$$(dirname $$i) CGO_ENABLED=0 go build -ldflags $(BUILD_FLAGS) -o $(GEN)/$(NAME).$$tag ../../cmds/$(NAME); \ - done + GOARCH=$$(basename $$i) GOOS=$$(dirname $$i) CGO_ENABLED=0 go build -ldflags $(BUILD_FLAGS) -o $(GEN)/$(NAME).$$tag ../../cmds/$(NAME) & \ + done; \ + wait @touch $(GEN)/build .PHONY: ctf ctf: $(GEN)/ctf -$(GEN)/ctf: $(GEN)/ca.done +$(GEN)/ctf: $(OCM_BIN) $(GEN)/.exists $(GEN)/build component-constructor.yaml $(CHARTSRCS) @rm -rf "$(GEN)/ctf" - $(OCM) transfer ca $(GEN)/ca $(GEN)/ctf + $(OCM) add componentversions \ + --create \ + --file $(GEN)/ctf \ + --type $(CTF_TYPE) \ + --templater=spiff \ + COMPONENT="$(COMPONENT)" \ + NAME="$(NAME)" \ + VERSION="$(VERSION)" \ + PROVIDER="$(PROVIDER)" \ + COMMIT="$(COMMIT)" \ + GEN="$(GEN)" \ + PLATFORMS="$(PLATFORMS)" \ + component-constructor.yaml touch "$(GEN)/ctf" .PHONY: version version: @echo $(VERSION) -.PHONY: ca -ca: $(GEN)/ca.done - -$(GEN)/ca.done: $(GEN)/.exists $(GEN)/build resources.yaml $(CHARTSRCS) - $(OCM) create ca -f $(COMPONENT) "$(VERSION)" --provider $(PROVIDER) --file $(GEN)/ca - $(OCM) add resources --templater=spiff --file $(GEN)/ca NAME="$(NAME)" VERSION="$(VERSION)" COMMIT="$(COMMIT)" GEN="$(GEN)" PLATFORMS="$(PLATFORMS)" resources.yaml - @touch $(GEN)/ca.done - -.PHONY: plain-ca -plain-ca: $(GEN)/.exists resources.yaml $(CHARTSRCS) - $(OCM) create ca -f $(COMPONENT) "$(VERSION)" --provider $(PROVIDER) --file $(GEN)/ca - $(OCM) add resources --templater=spiff --file $(GEN)/ca NAME="$(NAME)" VERSION="$(VERSION)" COMMIT="$(COMMIT)" GEN="$(GEN)" PLATFORMS="$(PLATFORMS)" resources.yaml - .PHONY: push push: $(GEN)/ctf $(GEN)/push.$(NAME) -$(GEN)/push.$(NAME): $(GEN)/ctf +$(GEN)/push.$(NAME): $(GEN)/ctf $(OCM_BIN) $(OCM) transfer ctf -f $(GEN)/ctf $(OCMREPO) @touch $(GEN)/push.$(NAME) .PHONY: plain-push -plain-push: $(GEN) +plain-push: $(GEN) $(OCM_BIN) $(OCM) transfer ctf -f $(GEN)/ctf $(OCMREPO) @touch $(GEN)/push.$(NAME) .PHONY: transport -transport: +transport: $(OCM_BIN) ifneq ($(TARGETREPO),) $(OCM) transfer component -Vc $(OCMREPO)//$(COMPONENT):$(VERSION) $(TARGETREPO) endif @@ -94,12 +106,12 @@ info: @echo "COMMIT; $(COMMIT)" .PHONY: describe -describe: $(GEN)/ctf - ocm get resources --lookup $(OCMREPO) -c -o treewide $(GEN)/ctf +describe: $(GEN)/ctf $(OCM_BIN) + $(OCM) get resources --lookup $(OCMREPO) -r -o treewide $(GEN)/ctf .PHONY: descriptor -descriptor: $(GEN)/ctf - ocm get component -S v3alpha1 -o yaml $(GEN)/ctf +descriptor: $(GEN)/ctf $(OCM_BIN) + $(OCM) get component -S v3alpha1 -o yaml $(GEN)/ctf .PHONY: clean clean: diff --git a/components/ecrplugin/component-constructor.yaml b/components/ecrplugin/component-constructor.yaml new file mode 100644 index 0000000000..253566936f --- /dev/null +++ b/components/ecrplugin/component-constructor.yaml @@ -0,0 +1,23 @@ +--- +helper: + <<<: (( &temporary )) + executable: + <<<: (( &template )) + name: demo + type: ocmPlugin + version: (( values.VERSION )) + extraIdentity: + os: ((dirname(p) )) + architecture: (( basename(p) )) + input: + type: file + # Generate the path to the plugin binary by looking into the base path and encoding the platform + path: (( values.GEN "/" values.NAME "." replace(p,"/","-") )) + +components: + - name: (( values.COMPONENT)) + version: (( values.VERSION)) + provider: + name: (( values.PROVIDER)) + # use all platforms and create a resource for each + resources: (( map[split(" ", values.PLATFORMS)|p|-> *helper.executable] )) \ No newline at end of file diff --git a/components/ecrplugin/resources.yaml b/components/ecrplugin/resources.yaml deleted file mode 100644 index 69e59a7b55..0000000000 --- a/components/ecrplugin/resources.yaml +++ /dev/null @@ -1,18 +0,0 @@ ---- -helper: - <<<: (( &temporary )) - executable: - <<<: (( &template )) - name: (( values.NAME )) - type: ocmPlugin - version: (( values.VERSION )) - extraIdentity: - os: ((dirname(p) )) - architecture: (( basename(p) )) - input: - type: file - path: (( values.GEN "/" values.NAME "." replace(p,"/","-") )) - - -resources: (( map[split(" ", values.PLATFORMS)|p|-> *helper.executable] )) - diff --git a/components/helmdemo/Makefile b/components/helmdemo/Makefile index 6268331544..4ceba662ff 100644 --- a/components/helmdemo/Makefile +++ b/components/helmdemo/Makefile @@ -3,6 +3,7 @@ PROVIDER ?= ocm.software GITHUBORG ?= open-component-model COMPONENT = $(PROVIDER)/toi/demo/$(NAME) OCMREPO ?= ghcr.io/$(GITHUBORG)/ocm +CTF_TYPE ?= directory HELMINSTCOMP = $(PROVIDER)/toi/installers/helminstaller @@ -13,47 +14,65 @@ EFFECTIVE_VERSION = $(VERSION)-$(COMMIT) HELMINSTVERSION ?= $(VERSION) CREDS ?= -OCM = go run $(REPO_ROOT)/cmds/ocm $(CREDS) +# Define the path to the binary +OCM_BIN = $(REPO_ROOT)/bin/ocm + +# Rule to build the binary if it doesn't exist or if the source code has changed +$(OCM_BIN): $(REPO_ROOT)/cmds/ocm/main.go + mkdir -p $(REPO_ROOT)/bin + go build -ldflags $(BUILD_FLAGS) -o $(OCM_BIN) $(REPO_ROOT)/cmds/ocm + +# Use the binary for the OCM command +OCM = $(OCM_BIN) $(CREDS) GEN := $(REPO_ROOT)/gen/$(NAME) +$(GEN): + @mkdir -p $(GEN) + +NOW := $(shell date -u +%FT%T%z) +BUILD_FLAGS := "-s -w \ + -X ocm.software/ocm/api/version.gitVersion=$(EFFECTIVE_VERSION) \ + -X ocm.software/ocm/api/version.gitTreeState=$(GIT_TREE_STATE) \ + -X ocm.software/ocm/api/version.gitCommit=$(COMMIT) \ + -X ocm.software/ocm/api/version.buildDate=$(NOW)" + CHARTSRCS=$(shell find echoserver -type f) .PHONY: ctf ctf: $(GEN)/ctf -$(GEN)/ctf: $(GEN)/ca +$(GEN)/ctf: $(OCM_BIN) $(GEN)/.exists $(CHARTSRCS) $(GEN) component-constructor.yaml packagespec.yaml examples/* helmconfig.yaml @rm -rf $(GEN)/ctf - $(OCM) -X keeplocalblob=true transfer ca $(GEN)/ca $(GEN)/ctf + $(OCM) add componentversions \ + --create \ + --file $(GEN)/ctf \ + --type $(CTF_TYPE) \ + --templater=spiff \ + COMPONENT="$(COMPONENT)" \ + NAME="$(NAME)" \ + VERSION="$(VERSION)" \ + PROVIDER="$(PROVIDER)" \ + COMMIT="$(COMMIT)" \ + GEN="$(GEN)" \ + HELMINSTCOMP=$(HELMINSTCOMP) \ + HELMINSTVERSION=$(HELMINSTVERSION) \ + component-constructor.yaml touch $(GEN)/ctf .PHONY: version version: @echo $(VERSION) -.PHONY: ca -ca: $(GEN)/ca - -$(GEN)/ca: $(GEN)/.exists sources.yaml resources.yaml references.yaml $(CHARTSRCS) packagespec.yaml examples/* helmconfig.yaml - $(OCM) create ca -f $(COMPONENT) "$(VERSION)" --provider $(PROVIDER) --file $(GEN)/ca - $(OCM) add sources $(GEN)/ca VERSION="$(VERSION)" COMMIT="$(COMMIT)" sources.yaml - $(OCM) add resources $(GEN)/ca VERSION="$(VERSION)" COMMIT="$(COMMIT)" resources.yaml - $(OCM) add references $(GEN)/ca VERSION="$(VERSION)" COMMIT="$(COMMIT)" HELMINSTCOMP=$(HELMINSTCOMP) HELMINSTVERSION=$(HELMINSTVERSION) references.yaml - @touch $(GEN)/ca - -.PHONY: eval-resources -eval-resources: - $(OCM) add resources --dry-run VERSION="$(VERSION)" COMMIT="$(COMMIT)" resources.yaml -O "$(GEN)/resources.yaml" - .PHONY: push push: $(GEN)/ctf $(GEN)/push.$(NAME) -$(GEN)/push.$(NAME): $(GEN)/ctf +$(GEN)/push.$(NAME): $(GEN)/ctf $(OCM_BIN) $(OCM) -X keeplocalblob=true transfer ctf -f $(GEN)/ctf $(OCMREPO) @touch $(GEN)/push.$(NAME) .PHONY: plain-push -plain-push: $(GEN) +plain-push: $(GEN) $(OCM_BIN) $(OCM) -X keeplocalblob=true transfer ctf -f $(GEN)/ctf $(OCMREPO) @touch $(GEN)/push.$(NAME) @@ -76,11 +95,11 @@ info: @echo "version for helminstaller: $(HELMINSTVERSION)" .PHONY: describe -describe: $(GEN)/ctf +describe: $(GEN)/ctf $(OCM_BIN) $(OCM) get resources --lookup $(OCMREPO) -r -o treewide $(GEN)/ctf .PHONY: descriptor -descriptor: $(GEN)/ctf +descriptor: $(GEN)/ctf $(OCM_BIN) $(OCM) get component -S v3alpha1 -o yaml $(GEN)/ctf .PHONY: clean diff --git a/components/helmdemo/component-constructor.yaml b/components/helmdemo/component-constructor.yaml new file mode 100644 index 0000000000..36725845e8 --- /dev/null +++ b/components/helmdemo/component-constructor.yaml @@ -0,0 +1,58 @@ +components: + - name: (( values.COMPONENT)) + version: (( values.VERSION)) + provider: + name: (( values.PROVIDER)) + # use all platforms and create a resource for each +# ADD back once https://github.com/open-component-model/ocm/issues/1041 is fixed + references: + - name: installer + componentName: (( values.HELMINSTCOMP )) + version: (( values.HELMINSTVERSION )) + sources: + - name: source + type: filesytem + access: + type: github + repoUrl: github.com/open-component-model/ocm + commit: (( values.COMMIT )) + version: (( values.VERSION )) + resources: + - name: creds-example + type: yaml + labels: + - name: commit + value: (( values.COMMIT )) + input: + type: file + mediaType: application/vnd.toi.ocm.software.credentials.v1+yaml + path: examples/creds.yaml + - name: config-example + type: yaml + labels: + - name: commit + value: (( values.COMMIT )) + input: + type: file + mediaType: application/vnd.toi.ocm.software.config.v1+yaml + path: examples/config.yaml + - name: image + type: ociImage + version: "1.0" + access: + type: ociArtifact + imageReference: gcr.io/google-containers/echoserver:1.10 + - name: chart + type: helmChart + input: + type: helm + path: echoserver + - name: package + type: toiPackage + labels: + - name: commit + value: (( values.COMMIT )) + input: + type: spiff + mediaType: application/vnd.toi.ocm.software.package.v1+yaml + path: packagespec.yaml \ No newline at end of file diff --git a/components/helmdemo/references.yaml b/components/helmdemo/references.yaml deleted file mode 100644 index 6c0cf353b5..0000000000 --- a/components/helmdemo/references.yaml +++ /dev/null @@ -1,4 +0,0 @@ ---- -name: installer -componentName: ${HELMINSTCOMP} -version: ${HELMINSTVERSION} diff --git a/components/helmdemo/resources.yaml b/components/helmdemo/resources.yaml deleted file mode 100644 index cec2758956..0000000000 --- a/components/helmdemo/resources.yaml +++ /dev/null @@ -1,43 +0,0 @@ ---- -name: package -type: toiPackage -labels: - - name: commit - value: ${COMMIT} -input: - type: spiff - mediaType: application/vnd.toi.ocm.software.package.v1+yaml - path: packagespec.yaml ---- -name: chart -type: helmChart -input: - type: helm - path: echoserver ---- -name: image -type: ociImage -version: "1.0" -access: - type: ociArtifact - imageReference: gcr.io/google-containers/echoserver:1.10 ---- -name: config-example -type: yaml -labels: - - name: commit - value: ${COMMIT} -input: - type: file - mediaType: application/vnd.toi.ocm.software.config.v1+yaml - path: examples/config.yaml ---- -name: creds-example -type: yaml -labels: - - name: commit - value: ${COMMIT} -input: - type: file - mediaType: application/vnd.toi.ocm.software.credentials.v1+yaml - path: examples/creds.yaml diff --git a/components/helmdemo/sources.yaml b/components/helmdemo/sources.yaml deleted file mode 100644 index dcc9c5f896..0000000000 --- a/components/helmdemo/sources.yaml +++ /dev/null @@ -1,7 +0,0 @@ -name: source -type: filesytem -access: - type: github - repoUrl: github.com/open-component-model/ocm - commit: ${COMMIT} -version: ${VERSION} diff --git a/components/helminstaller/Dockerfile b/components/helminstaller/Dockerfile index 008f78d481..f0d8aa97e4 100644 --- a/components/helminstaller/Dockerfile +++ b/components/helminstaller/Dockerfile @@ -8,8 +8,8 @@ COPY api api COPY cmds cmds COPY hack/generate-docs hack/generate-docs #COPY go/api api -RUN --mount=type=cache,target=/root/.cache/go-build go get -d ./... -RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \ +RUN go get -d ./... +RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \ go build -o /main -ldflags "-s -w \ -X ocm.software/ocm/api/version.gitVersion=$EFFECTIVE_VERSION \ -X ocm.software/ocm/api/version.gitTreeState=$GIT_TREE_STATE \ diff --git a/components/helminstaller/Makefile b/components/helminstaller/Makefile index ccc438c2f1..ed3d368daa 100644 --- a/components/helminstaller/Makefile +++ b/components/helminstaller/Makefile @@ -6,6 +6,7 @@ COMPONENT := $(PROVIDER)/toi/installers/$(NAME) OCMREPO ?= ghcr.io/$(GITHUBORG)/ocm MULTI ?= true PLATFORMS ?= linux/amd64 linux/arm64 +CTF_TYPE ?= directory REPO_ROOT := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))/../.. VERSION := $(shell go run ../../api/version/generate/release_generate.go print-rc-version $(CANDIDATE)) @@ -13,17 +14,36 @@ COMMIT := $(shell git rev-parse --verify EFFECTIVE_VERSION := $(VERSION)-$(COMMIT) GIT_TREE_STATE := $(shell [ -z "$$(git status --porcelain 2>/dev/null)" ] && echo clean || echo dirty) PLATFORM := $(shell go env GOOS)/$(shell go env GOARCH) +CACHE_DIR := $(shell go env GOCACHE) +MOD_CACHE_DIR := $(shell go env GOMODCACHE) CREDS ?= -OCM = go run $(REPO_ROOT)/cmds/ocm $(CREDS) +# Define the path to the binary +OCM_BIN = $(REPO_ROOT)/bin/ocm + +# Rule to build the binary if it doesn't exist or if the source code has changed +$(OCM_BIN): $(REPO_ROOT)/cmds/ocm/main.go + mkdir -p $(REPO_ROOT)/bin + go build -ldflags $(BUILD_FLAGS) -o $(OCM_BIN) $(REPO_ROOT)/cmds/ocm + +# Use the binary for the OCM command +OCM = $(OCM_BIN) $(CREDS) GEN = $(REPO_ROOT)/gen/$(NAME) +$(GEN): + @mkdir -p $(GEN) + +NOW := $(shell date -u +%FT%T%z) +BUILD_FLAGS := "-s -w \ + -X ocm.software/ocm/api/version.gitVersion=$(EFFECTIVE_VERSION) \ + -X ocm.software/ocm/api/version.gitTreeState=$(GIT_TREE_STATE) \ + -X ocm.software/ocm/api/version.gitCommit=$(COMMIT) \ + -X ocm.software/ocm/api/version.buildDate=$(NOW)" + CMDSRCS=$(shell find $(REPO_ROOT)/cmds/$(NAME) -type f) OCMSRCS=$(shell find $(REPO_ROOT)/pkg -type f) $(REPO_ROOT)/go.* -ATTRIBUTES = VERSION="$(VERSION)" COMMIT="$(COMMIT)" IMAGE="$(IMAGE):$(VERSION)" PLATFORMS="$(PLATFORMS)" MULTI=$(MULTI) - ifeq ($(MULTI),true) FLAGSUF = .multi endif @@ -31,47 +51,39 @@ endif .PHONY: ctf ctf: $(GEN)/ctf -$(GEN)/ctf: $(GEN)/ca +$(GEN)/ctf: $(OCM_BIN) $(GEN)/.exists $(GEN)/image.$(NAME)$(FLAGSUF) component-constructor.yaml executorspec.yaml @rm -rf "$(GEN)/ctf" - $(OCM) transfer ca $(GEN)/ca $(GEN)/ctf - touch $(GEN)/ctf - -.PHONY: plain-ctf -plain-ctf: - $(OCM) transfer ca $(GEN)/ca $(GEN)/ctf - touch $(GEN)/ctf + $(OCM) add componentversions \ + --create \ + --file $(GEN)/ctf \ + --type $(CTF_TYPE) \ + --templater=spiff \ + COMPONENT="$(COMPONENT)" \ + NAME="$(NAME)" \ + VERSION="$(VERSION)" \ + PROVIDER="$(PROVIDER)" \ + COMMIT="$(COMMIT)" \ + GEN="$(GEN)" \ + PLATFORMS="$(PLATFORMS)" \ + MULTI="$(MULTI)" \ + IMAGE="$(IMAGE):$(VERSION)" \ + component-constructor.yaml + touch "$(GEN)/ctf" .PHONY: version version: @echo $(VERSION) -.PHONY: ca -ca: $(GEN)/ca - -$(GEN)/ca: $(GEN)/.exists $(GEN)/image.$(NAME)$(FLAGSUF) resources.yaml executorspec.yaml - $(OCM) create ca -f $(COMPONENT) "$(VERSION)" --provider $(PROVIDER) --file $(GEN)/ca - $(OCM) add resources --templater spiff $(GEN)/ca $(ATTRIBUTES) resources.yaml - @touch $(GEN)/ca - - -.PHONY: plain-ca -plain-ca: $(GEN)/.exists - $(OCM) create ca -f $(COMPONENT) "$(VERSION)" --provider $(PROVIDER) --file $(GEN)/ca - $(OCM) add resources --templater spiff $(GEN)/ca $(ATTRIBUTES) resources.yaml - @touch $(GEN)/ca - -.PHONY: eval-resources -eval-resources: - $(OCM) add resources --dry-run --templater spiff $(ATTRIBUTES) resources.yaml - .PHONY: build build: $(GEN)/image.$(NAME)$(FLAGSUF) $(GEN)/image.$(NAME): $(GEN)/.exists Dockerfile $(CMDSRCS) $(OCMSRCS) docker buildx build -t $(IMAGE):$(VERSION) --platform $(PLATFORM) --file Dockerfile $(REPO_ROOT) \ --build-arg COMMIT=$(COMMIT) \ + --build-arg CACHE_DIR=$(CACHE_DIR) \ + --build-arg MOD_CACHE_DIR=$(MOD_CACHE_DIR) \ --build-arg EFFECTIVE_VERSION=$(EFFECTIVE_VERSION) \ - --build-arg GIT_TREE_STATE=$(GIT_TREE_STATE) + --build-arg GIT_TREE_STATE=$(GIT_TREE_STATE); \ @touch $(GEN)/image.$(NAME) push-image: @@ -87,6 +99,8 @@ $(GEN)/image.$(NAME).multi: $(GEN)/.exists Dockerfile $(CMDSRCS) $(OCMSRCS) echo building platform $$i; \ docker buildx build --load -t $(IMAGE):$(VERSION)-$$tag --platform $$i --file Dockerfile $(REPO_ROOT) \ --build-arg COMMIT=$(COMMIT) \ + --build-arg CACHE_DIR=$(CACHE_DIR) \ + --build-arg MOD_CACHE_DIR=$(MOD_CACHE_DIR) \ --build-arg EFFECTIVE_VERSION=$(EFFECTIVE_VERSION) \ --build-arg GIT_TREE_STATE=$(GIT_TREE_STATE); \ done @@ -95,17 +109,17 @@ $(GEN)/image.$(NAME).multi: $(GEN)/.exists Dockerfile $(CMDSRCS) $(OCMSRCS) .PHONY: push push: $(GEN)/ctf $(GEN)/push.$(NAME) -$(GEN)/push.$(NAME): $(GEN)/ctf +$(GEN)/push.$(NAME): $(GEN)/ctf $(OCM_BIN) $(OCM) transfer ctf -f $(GEN)/ctf $(OCMREPO) @touch $(GEN)/push.$(NAME) .PHONY: plain-push -plain-push: $(GEN)/.exists +plain-push: $(GEN)/.exists $(OCM_BIN) $(OCM) transfer ctf -f $(GEN)/ctf $(OCMREPO) @touch $(GEN)/push.$(NAME) .PHONY: transport -transport: +transport: $(OCM_BIN) ifneq ($(TARGETREPO),) $(OCM) transfer component -Vr $(OCMREPO)//$(COMPONENT):$(VERSION) $(TARGETREPO) endif @@ -123,12 +137,12 @@ info: @echo "PATFORM: $(PLATFORM)" .PHONY: describe -describe: $(GEN)/ctf - ocm get resources --lookup $(OCMREPO) -r -o treewide $(GEN)/ctf +describe: $(OCM_BIN) $(GEN)/ctf + $(OCM) get resources --lookup $(OCMREPO) -r -o treewide $(GEN)/ctf .PHONY: descriptor -descriptor: $(GEN)/ctf - ocm get component -S v3alpha1 -o yaml $(GEN)/ctf +descriptor: $(OCM_BIN) $(GEN)/ctf + $(OCM) get component -S v3alpha1 -o yaml $(GEN)/ctf .PHONY: setup setup: diff --git a/components/helminstaller/a.yaml b/components/helminstaller/a.yaml deleted file mode 100644 index 21ceec38d8..0000000000 --- a/components/helminstaller/a.yaml +++ /dev/null @@ -1 +0,0 @@ -flag: (( flag ? "yes" :"no" )) diff --git a/components/helminstaller/component-constructor.yaml b/components/helminstaller/component-constructor.yaml new file mode 100644 index 0000000000..a2223653ad --- /dev/null +++ b/components/helminstaller/component-constructor.yaml @@ -0,0 +1,25 @@ +--- +components: + - name: (( values.COMPONENT)) + version: (( values.VERSION)) + provider: + name: (( values.PROVIDER)) + # use all platforms and create a resource for each + resources: + - name: toiexecutor + type: toiExecutor + labels: + - name: commit + value: (( values.COMMIT )) + input: + type: file + mediaType: application/x-yaml + path: executorspec.yaml + - name: toiimage + type: ociImage + version: (( values.VERSION )) + input: + type: (( bool(values.MULTI) ? "dockermulti" :"docker" )) + repository: (( index(values.IMAGE, ":") >= 0 ? substr(values.IMAGE,0,index(values.IMAGE,":")) :values.IMAGE )) + variants: (( bool(values.MULTI) ? map[split(" ", values.PLATFORMS)|v|-> values.IMAGE "-" replace(v,"/","-")] :~~ )) + path: (( !bool(values.MULTI) ? values.IMAGE :~~ )) \ No newline at end of file diff --git a/components/helminstaller/resources.yaml b/components/helminstaller/resources.yaml deleted file mode 100644 index 9a6f2ef0eb..0000000000 --- a/components/helminstaller/resources.yaml +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: toiimage -type: ociImage -version: (( values.VERSION )) -input: - type: (( bool(values.MULTI) ? "dockermulti" :"docker" )) - repository: (( index(values.IMAGE, ":") >= 0 ? substr(values.IMAGE,0,index(values.IMAGE,":")) :values.IMAGE )) - variants: (( bool(values.MULTI) ? map[split(" ", values.PLATFORMS)|v|-> values.IMAGE "-" replace(v,"/","-")] :~~ )) - path: (( !bool(values.MULTI) ? values.IMAGE :~~ )) ---- -name: toiexecutor -type: toiExecutor -labels: - - name: commit - value: (( values.COMMIT )) -input: - type: file - mediaType: application/x-yaml - path: executorspec.yaml - diff --git a/components/ocmcli/Makefile b/components/ocmcli/Makefile index 66979a20bc..e972f91037 100644 --- a/components/ocmcli/Makefile +++ b/components/ocmcli/Makefile @@ -8,6 +8,7 @@ OCMREPO ?= ghcr.io/$(GITHUBORG)/ocm MULTI ?= true IMAGE_PLATFORMS ?= linux/amd64 linux/arm64 PLATFORMS = $(IMAGE_PLATFORMS) darwin/arm64 darwin/amd64 windows/amd64 +CTF_TYPE ?= directory REPO_ROOT := $(dir $(realpath $(lastword $(MAKEFILE_LIST))))../.. GIT_TREE_STATE = $(shell [ -z "$$(git status --porcelain 2>/dev/null)" ] && echo clean || echo dirty) @@ -18,18 +19,28 @@ PLATFORM_OS := $(shell go env GOOS) PLATFORM_ARCH := $(shell go env GOARCH) CMDSRCS=$(shell find $(REPO_ROOT)/cmds/$(CMD) -type f) Makefile -OCMSRCS=$(shell find $(REPO_ROOT)/pkg -type f) $(REPO_ROOT)/go.* - -ATTRIBUTES = VERSION="$(VERSION)" NAME="$(NAME)" COMMIT="$(COMMIT)" IMAGE="$(IMAGE):$(VERSION)" PLATFORMS="$(PLATFORMS)" IMAGE_PLATFORMS="$(IMAGE_PLATFORMS)" GEN="$(GEN)" MULTI=$(MULTI) +OCMSRCS=$(shell find $(REPO_ROOT)/api -type f) $(REPO_ROOT)/go.* ifeq ($(MULTI),true) FLAGSUF = .multi endif CREDS ?= -OCM = go run $(REPO_ROOT)/cmds/ocm $(CREDS) +# Define the path to the binary +OCM_BIN = $(REPO_ROOT)/bin/ocm + +# Rule to build the binary if it doesn't exist or if the source code has changed +$(OCM_BIN): $(REPO_ROOT)/cmds/ocm/main.go + mkdir -p $(REPO_ROOT)/bin + go build -ldflags $(BUILD_FLAGS) -o $(OCM_BIN) $(REPO_ROOT)/cmds/ocm -GEN = $(REPO_ROOT)/gen/$(shell basename $(realpath .)) +# Use the binary for the OCM command +OCM = $(OCM_BIN) $(CREDS) + +GEN = $(REPO_ROOT)/gen/$(NAME) + + $(GEN): + @mkdir -p $(GEN) NOW := $(shell date -u +%FT%T%z) BUILD_FLAGS := "-s -w \ @@ -43,12 +54,13 @@ ALPINE_LATEST_VER=$(shell curl -s https://registry.hub.docker.com/v2/repositorie .PHONY: build build: $(GEN)/build -$(GEN)/build: $(GEN)/.exists $(CMDSRCS) $(OCMSRCS) +$(GEN)/build: $(GEN) $(GEN)/.exists $(CMDSRCS) $(OCMSRCS) @for i in $(PLATFORMS); do \ - tag=$$(echo $$i | sed -e s:/:-:g); \ - echo GOARCH=$$(basename $$i) GOOS=$$(dirname $$i) CGO_ENABLED=0 go build -ldflags $(BUILD_FLAGS) -o $(GEN)/$(NAME).$$tag ../../cmds/$(CMD); \ - GOARCH=$$(basename $$i) GOOS=$$(dirname $$i) CGO_ENABLED=0 go build -ldflags $(BUILD_FLAGS) -o $(GEN)/$(NAME).$$tag ../../cmds/$(CMD); \ - done + tag=$$(echo $$i | sed -e s:/:-:g); \ + echo GOARCH=$$(basename $$i) GOOS=$$(dirname $$i) CGO_ENABLED=0 go build -ldflags $(BUILD_FLAGS) -o $(GEN)/$(NAME).$$tag ../../cmds/$(CMD); \ + GOARCH=$$(basename $$i) GOOS=$$(dirname $$i) CGO_ENABLED=0 go build -ldflags $(BUILD_FLAGS) -o $(GEN)/$(NAME).$$tag ../../cmds/$(CMD) & \ + done; \ + wait @touch $(GEN)/build .PHONY: image @@ -89,38 +101,44 @@ $(GEN)/image.multi: Dockerfile $(GEN)/build .PHONY: ctf ctf: $(GEN)/ctf -$(GEN)/ctf: $(GEN)/ca.done +$(GEN)/ctf: $(OCM_BIN) $(GEN)/.exists $(GEN)/build $(GEN)/image$(FLAGSUF) component-constructor.yaml $(CHARTSRCS) Makefile @rm -rf "$(GEN)/ctf" - $(OCM) transfer ca $(GEN)/ca $(GEN)/ctf - touch $(GEN)/ctf + $(OCM) add componentversions \ + --create \ + --file $(GEN)/ctf \ + --type $(CTF_TYPE) \ + --templater=spiff \ + COMPONENT="$(COMPONENT)" \ + NAME="$(NAME)" \ + VERSION="$(VERSION)" \ + PROVIDER="$(PROVIDER)" \ + COMMIT="$(COMMIT)" \ + GEN="$(GEN)" \ + PLATFORMS="$(PLATFORMS)" \ + IMAGE_PLATFORMS="$(IMAGE_PLATFORMS)" \ + MULTI=$(MULTI) \ + IMAGE="$(IMAGE):$(VERSION)" \ + component-constructor.yaml + touch "$(GEN)/ctf" .PHONY: version version: @echo $(VERSION) -.PHONY: ca -ca: $(GEN)/ca.done - -$(GEN)/ca.done: $(GEN)/.exists $(GEN)/build $(GEN)/image$(FLAGSUF) resources.yaml $(CHARTSRCS) Makefile - $(OCM) create ca -f $(COMPONENT) "$(VERSION)" --provider $(PROVIDER) --file $(GEN)/ca - $(OCM) add resources --templater=spiff --file $(GEN)/ca $(ATTRIBUTES) resources.yaml - $(OCM) add sources $(GEN)/ca VERSION="$(VERSION)" COMMIT="$(COMMIT)" sources.yaml - @touch $(GEN)/ca.done - .PHONY: push push: $(GEN)/ctf $(GEN)/push.$(NAME) -$(GEN)/push.$(NAME): $(GEN)/ctf +$(GEN)/push.$(NAME): $(GEN)/ctf $(OCM_BIN) $(OCM) transfer ctf -f $(GEN)/ctf $(OCMREPO) @touch $(GEN)/push.$(NAME) .PHONY: plain-push -plain-push: $(GEN) +plain-push: $(GEN) $(OCM_BIN) $(OCM) transfer ctf -f $(GEN)/ctf $(OCMREPO) @touch $(GEN)/push.$(NAME) .PHONY: transport -transport: +transport: $(OCM_BIN) ifneq ($(TARGETREPO),) $(OCM) transfer component -Vc $(OCMREPO)//$(COMPONENT):$(VERSION) $(TARGETREPO) endif @@ -137,12 +155,12 @@ info: @echo "GIT_TREE: $(GIT_TREE_STATE)" .PHONY: describe -describe: $(GEN)/ctf - ocm get resources --lookup $(OCMREPO) -c -o treewide $(GEN)/ctf +describe: $(GEN)/ctf $(OCM_BIN) + $(OCM) get resources --lookup $(OCMREPO) -r -o treewide $(GEN)/ctf .PHONY: descriptor -descriptor: $(GEN)/ctf - ocm get component -S v3alpha1 -o yaml $(GEN)/ctf +descriptor: $(GEN)/ctf $(OCM_BIN) + $(OCM) get component -S v3alpha1 -o yaml $(GEN)/ctf .PHONY: clean clean: diff --git a/components/ocmcli/resources.yaml b/components/ocmcli/component-constructor.yaml similarity index 63% rename from components/ocmcli/resources.yaml rename to components/ocmcli/component-constructor.yaml index ac152b9b83..11eb4bcecb 100644 --- a/components/ocmcli/resources.yaml +++ b/components/ocmcli/component-constructor.yaml @@ -26,6 +26,21 @@ helper: variants: (( bool(values.MULTI) ? map[split(" ", values.IMAGE_PLATFORMS)|v|-> values.IMAGE "-" replace(v,"/","-")] :~~ )) path: (( !bool(values.MULTI) ? values.IMAGE :~~ )) +components: + - name: (( values.COMPONENT)) + version: (( values.VERSION)) + provider: + name: (( values.PROVIDER)) + # use all platforms and create a resource for each + resources: (( map[split(" ", values.PLATFORMS)|p|-> *helper.executable] *helper.image )) + sources: + - name: source + type: filesytem + access: + type: github + repoUrl: github.com/open-component-model/ocm + commit: (( values.COMMIT )) + version: (( values.VERSION )) + -resources: (( map[split(" ", values.PLATFORMS)|p|-> *helper.executable] *helper.image )) diff --git a/components/ocmcli/sources.yaml b/components/ocmcli/sources.yaml deleted file mode 100644 index dcc9c5f896..0000000000 --- a/components/ocmcli/sources.yaml +++ /dev/null @@ -1,7 +0,0 @@ -name: source -type: filesytem -access: - type: github - repoUrl: github.com/open-component-model/ocm - commit: ${COMMIT} -version: ${VERSION} diff --git a/components/subchartsdemo/Makefile b/components/subchartsdemo/Makefile index 67a3af7da6..93ae7b583b 100644 --- a/components/subchartsdemo/Makefile +++ b/components/subchartsdemo/Makefile @@ -12,13 +12,30 @@ PODINFO_VERSION = 6.3.5 PODINFO_CHART_VERSION = 6.3.5 REPO_ROOT := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))/../.. +GIT_TREE_STATE = $(shell [ -z "$$(git status --porcelain 2>/dev/null)" ] && echo clean || echo dirty) VERSION = $(shell go run $(REPO_ROOT)/api/version/generate/release_generate.go print-rc-version $(CANDIDATE)) COMMIT = $(shell git rev-parse HEAD) EFFECTIVE_VERSION = $(VERSION)-$(COMMIT) HELMINSTVERSION ?= $(VERSION) CREDS ?= -OCM = go run $(REPO_ROOT)/cmds/ocm $(CREDS) +# Define the path to the binary +OCM_BIN = $(REPO_ROOT)/bin/ocm + +# Rule to build the binary if it doesn't exist or if the source code has changed +$(OCM_BIN): $(REPO_ROOT)/cmds/ocm/main.go + mkdir -p $(REPO_ROOT)/bin + go build -ldflags $(BUILD_FLAGS) -o $(OCM_BIN) $(REPO_ROOT)/cmds/ocm + +# Use the binary for the OCM command +OCM = $(OCM_BIN) $(CREDS) + +NOW := $(shell date -u +%FT%T%z) +BUILD_FLAGS := "-s -w \ + -X ocm.software/ocm/api/version.gitVersion=$(EFFECTIVE_VERSION) \ + -X ocm.software/ocm/api/version.gitTreeState=$(GIT_TREE_STATE) \ + -X ocm.software/ocm/api/version.gitCommit=$(COMMIT) \ + -X ocm.software/ocm/api/version.buildDate=$(NOW)" GEN = $(REPO_ROOT)/gen/subchartsdemo @@ -31,7 +48,7 @@ ctf: $(GEN)/ctf version: @echo $(VERSION) -$(GEN)/ctf: $(GEN)/.exists component-constructor.yaml $(ECHOCHARTSRCS) packagespec.yaml podinfo/podinfo-$(PODINFO_CHART_VERSION).tgz +$(GEN)/ctf: $(GEN)/.exists component-constructor.yaml $(ECHOCHARTSRCS) packagespec.yaml podinfo/podinfo-$(PODINFO_CHART_VERSION).tgz $(OCM_BIN) @rm -rf $(GEN)/ctf $(OCM) add componentversions --create VERSION="$(VERSION)" COMMIT="$(COMMIT)" COMPONENT_PREFIX=$(COMPONENT_PREFIX) PROVIDER=$(PROVIDER) PODINFO_VERSION=$(PODINFO_VERSION) PODINFO_CHART_VERSION=$(PODINFO_CHART_VERSION) HELMINSTCOMP=$(HELMINSTCOMP) HELMINSTVERSION=$(HELMINSTVERSION) ECHO_VERSION=$(ECHO_VERSION) ECHO_CHART_VERSION=$(ECHO_CHART_VERSION) --file $(GEN)/ctf component-constructor.yaml @touch $(GEN)/ctf @@ -39,12 +56,12 @@ $(GEN)/ctf: $(GEN)/.exists component-constructor.yaml $(ECHOCHARTSRCS) packagesp .PHONY: push push: $(GEN)/ctf $(GEN)/push.$(NAME) -$(GEN)/push.$(NAME): $(GEN)/ctf +$(GEN)/push.$(NAME): $(GEN)/ctf $(OCM_BIN) $(OCM) -X keeplocalblob=true transfer ctf --copy-resources -f $(GEN)/ctf $(OCMREPO) @touch $(GEN)/push.$(NAME) .PHONY: plain-push -plain-push: $(GEN) +plain-push: $(GEN) $(OCM_BIN) $(OCM) -X keeplocalblob=true transfer ctf --copy-resources -f $(GEN)/ctf $(OCMREPO) @touch $(GEN)/push.$(NAME) @@ -60,12 +77,12 @@ info: @echo "CREDS: $(CREDS)" .PHONY: describe -describe: $(GEN)/ctf - ocm get resources --lookup $(OCMREPO) -o treewide $(GEN)/ctf +describe: $(GEN)/ctf $(OCM_BIN) + $(OCM) get resources --lookup $(OCMREPO) -r -o treewide $(GEN)/ctf .PHONY: descriptor -descriptor: $(GEN)/ctf - ocm get component -S v3alpha1 -o yaml $(GEN)/ctf +descriptor: $(GEN)/ctf $(OCM_BIN) + $(OCM) get component -S v3alpha1 -o yaml $(GEN)/ctf .PHONY: clean clean: diff --git a/components/subchartsdemo/component-constructor.yaml b/components/subchartsdemo/component-constructor.yaml index 7f0a7ec51f..8a469048d7 100644 --- a/components/subchartsdemo/component-constructor.yaml +++ b/components/subchartsdemo/component-constructor.yaml @@ -7,7 +7,7 @@ components: version: ${VERSION} provider: name: ${PROVIDER} - componentReferences: + references: - name: echoserver componentName: ${COMPONENT_PREFIX}/echoserver version: "${ECHO_VERSION}" diff --git a/docs/reference/ocm.md b/docs/reference/ocm.md index c88be56806..f99a049602 100644 --- a/docs/reference/ocm.md +++ b/docs/reference/ocm.md @@ -369,7 +369,7 @@ by a certificate delivered with the signature. * [ocm execute](ocm_execute.md) — Execute an element. * [ocm get](ocm_get.md) — Get information about artifacts and components * [ocm hash](ocm_hash.md) — Hash and normalization operations -* [ocm install](ocm_install.md) — Install elements. +* [ocm install](ocm_install.md) — Install new OCM CLI components * [ocm list](ocm_list.md) — List information about components * [ocm set](ocm_set.md) — Set information about OCM repositories * [ocm show](ocm_show.md) — Show tags or versions diff --git a/docs/reference/ocm_add_componentversions.md b/docs/reference/ocm_add_componentversions.md index 91ae988a4a..b400223c54 100644 --- a/docs/reference/ocm_add_componentversions.md +++ b/docs/reference/ocm_add_componentversions.md @@ -26,12 +26,14 @@ componentversions, componentversion, cv, components, component, comps, comp, c -h, --help help for componentversions --lookup stringArray repository name or spec for closure lookup fallback -O, --output string output file for dry-run + -P, --preserve-signature preserve existing signatures -R, --replace replace existing elements -S, --scheme string schema version (default "v2") -s, --settings stringArray settings file with variable settings (yaml) + --skip-digest-generation skip digest creation --templater string templater to use (go, none, spiff, subst) (default "subst") -t, --type string archive format (directory, tar, tgz) (default "directory") - --uploader = repository uploader ([:[:]]== repository uploader ([:[:[:]]]=) (default []) -v, --version string default version for components ``` @@ -54,7 +56,10 @@ components will be added by value. The --replace option allows users to specify whether adding an element with the same name and extra identity but different version as an -existing element append (false) or replace (true) the existing element. +existing element, append (false) or replace (true) the existing element. + +The --preserve-signature option prohibits changes of signature +relevant elements. The source, resource and reference list can be composed according to the commands @@ -85,13 +90,6 @@ archive does not exist yet. The following formats are supported: The default format is directory. -If the option --scheme is given, the specified component descriptor format is used/generated. - -The following schema versions are supported for explicit conversions: - - ocm.software/v3alpha1 - - v2 (default) - - All yaml/json defined resources can be templated. Variables are specified as regular arguments following the syntax <name>=<value>. Additionally settings can be specified by a yaml file using the --settings diff --git a/docs/reference/ocm_add_references.md b/docs/reference/ocm_add_references.md index 6af5dfca9c..dfc6834cb1 100644 --- a/docs/reference/ocm_add_references.md +++ b/docs/reference/ocm_add_references.md @@ -20,6 +20,7 @@ references, reference, refs -F, --file string target file/directory (default "component-archive") -h, --help help for references -O, --output string output file for dry-run + -P, --preserve-signature preserve existing signatures -R, --replace replace existing elements -s, --settings stringArray settings file with variable settings (yaml) --templater string templater to use (go, none, spiff, subst) (default "subst") @@ -112,7 +113,10 @@ There are several templaters that can be selected by the --templater--replace option allows users to specify whether adding an element with the same name and extra identity but different version as an -existing element append (false) or replace (true) the existing element. +existing element, append (false) or replace (true) the existing element. + +The --preserve-signature option prohibits changes of signature +relevant elements. All yaml/json defined resources can be templated. diff --git a/docs/reference/ocm_add_resources.md b/docs/reference/ocm_add_resources.md index 7a0602a33c..3e9754bd01 100644 --- a/docs/reference/ocm_add_resources.md +++ b/docs/reference/ocm_add_resources.md @@ -20,6 +20,7 @@ resources, resource, res, r -F, --file string target file/directory (default "component-archive") -h, --help help for resources -O, --output string output file for dry-run + -P, --preserve-signature preserve existing signatures -R, --replace replace existing elements -s, --settings stringArray settings file with variable settings (yaml) --skip-digest-generation skip digest creation @@ -1023,7 +1024,10 @@ shown below. The --replace option allows users to specify whether adding an element with the same name and extra identity but different version as an -existing element append (false) or replace (true) the existing element. +existing element, append (false) or replace (true) the existing element. + +The --preserve-signature option prohibits changes of signature +relevant elements. All yaml/json defined resources can be templated. diff --git a/docs/reference/ocm_add_sources.md b/docs/reference/ocm_add_sources.md index 41f7c6b51a..e6af2717b9 100644 --- a/docs/reference/ocm_add_sources.md +++ b/docs/reference/ocm_add_sources.md @@ -20,6 +20,7 @@ sources, source, src, s -F, --file string target file/directory (default "component-archive") -h, --help help for sources -O, --output string output file for dry-run + -P, --preserve-signature preserve existing signatures -R, --replace replace existing elements -s, --settings stringArray settings file with variable settings (yaml) --templater string templater to use (go, none, spiff, subst) (default "subst") @@ -1021,7 +1022,10 @@ shown below. The --replace option allows users to specify whether adding an element with the same name and extra identity but different version as an -existing element append (false) or replace (true) the existing element. +existing element, append (false) or replace (true) the existing element. + +The --preserve-signature option prohibits changes of signature +relevant elements. All yaml/json defined resources can be templated. diff --git a/docs/reference/ocm_configfile.md b/docs/reference/ocm_configfile.md index 4cccc01e1e..ae9e51920d 100644 --- a/docs/reference/ocm_configfile.md +++ b/docs/reference/ocm_configfile.md @@ -61,15 +61,18 @@ The following configuration types are supported:
- downloader.ocm.config.ocm.software The config type downloader.ocm.config.ocm.software can be used to define a list - of preconfigured download handler registrations (see [ocm ocm-downloadhandlers](ocm_ocm-downloadhandlers.md)): + of preconfigured download handler registrations (see [ocm ocm-downloadhandlers](ocm_ocm-downloadhandlers.md)), + the default priority is 200:
       type: downloader.ocm.config.ocm.software
       description: "my standard download handler configuration"
-      handlers:
+      registrations:
         - name: oci/artifact
           artifactType: ociImage
-          mimeType:
+          mimeType: ...
+          description: ...
+          priority: ...
           config: ...
         ...
   
@@ -312,12 +315,13 @@ The following configuration types are supported: - uploader.ocm.config.ocm.software The config type uploader.ocm.config.ocm.software can be used to define a list - of preconfigured upload handler registrations (see [ocm ocm-uploadhandlers](ocm_ocm-uploadhandlers.md)): + of preconfigured upload handler registrations (see [ocm ocm-uploadhandlers](ocm_ocm-uploadhandlers.md)), + the default priority is 200:
       type: uploader.ocm.config.ocm.software
       description: "my standard upload handler configuration"
-      handlers:
+      registrations:
         - name: oci/artifact
           artifactType: ociImage
           config:
diff --git a/docs/reference/ocm_controller_uninstall.md b/docs/reference/ocm_controller_uninstall.md
index 43e1fd4537..65386338e2 100644
--- a/docs/reference/ocm_controller_uninstall.md
+++ b/docs/reference/ocm_controller_uninstall.md
@@ -9,7 +9,7 @@ ocm controller uninstall controller
 ### Options
 
 ```text
-  -u, --base-url string                       the base url to the ocm-controller's release page (default "https://ocm.software/ocm-controller/releases")
+  -u, --base-url string                       the base url to the ocm-controller's release page (default "https://github.com/ocm-controller/releases")
       --cert-manager-base-url string          the base url to the cert-manager's release page (default "https://github.com/cert-manager/cert-manager/releases")
       --cert-manager-release-api-url string   the base url to the cert-manager's API release page (default "https://api.github.com/repos/cert-manager/cert-manager/releases")
       --cert-manager-version string           version for cert-manager (default "v1.13.2")
diff --git a/docs/reference/ocm_download_resources.md b/docs/reference/ocm_download_resources.md
index 1e81643411..fd14528178 100644
--- a/docs/reference/ocm_download_resources.md
+++ b/docs/reference/ocm_download_resources.md
@@ -18,7 +18,7 @@ resources, resource, res, r
       --check-verified              enable verification store
   -c, --constraints constraints     version constraint
   -d, --download-handlers           use download handler if possible
-      --downloader =   artifact downloader ([:[:]]==   artifact downloader ([:[:[:]]]=) (default [])
   -x, --executable                  download executable for local platform
   -h, --help                        help for resources
       --latest                      restrict component versions to latest
diff --git a/docs/reference/ocm_execute_action.md b/docs/reference/ocm_execute_action.md
index e051e59a29..0d709936df 100644
--- a/docs/reference/ocm_execute_action.md
+++ b/docs/reference/ocm_execute_action.md
@@ -18,11 +18,27 @@ ocm execute action []  {=}
 ### Description
 
 Execute an action extension for a given action specification. The specification
-show be a JSON or YAML argument.
+should be a JSON or YAML argument.
 
 Additional properties settings can be used to describe a consumer id
 to retrieve credentials for.
 
+The following actions are supported:
+- Name: oci.repository.prepare
+    Prepare the usage of a repository in an OCI registry.
+
+    The hostname of the target repository is used as selector. The action should
+    assure, that the requested repository is available on the target OCI registry.
+
+    Spec version v1 uses the following specification fields:
+    - hostname *string*: The  hostname of the OCI registry.
+    - repository *string*: The OCI repository name.
+
+  Possible Consumer Attributes:
+  - hostname
+  - port
+  - pathprefix
+
 ### Examples
 
 ```bash
diff --git a/docs/reference/ocm_hash_componentversions.md b/docs/reference/ocm_hash_componentversions.md
index a00153f15a..561a9c16c9 100644
--- a/docs/reference/ocm_hash_componentversions.md
+++ b/docs/reference/ocm_hash_componentversions.md
@@ -22,7 +22,7 @@ componentversions, componentversion, cv, components, component, comps, comp, c
       --latest                    restrict component versions to latest
       --lookup stringArray        repository name or spec for closure lookup fallback
   -N, --normalization string      normalization algorithm (default "jsonNormalisation/v1")
-  -O, --outfile string            Output file for normalized component descriptor (default "norm.ncd")
+  -O, --outfile string            Output file for normalized component descriptor (default "-")
   -o, --output string             output mode (JSON, json, norm, wide, yaml)
   -r, --recursive                 follow component reference nesting
       --repo string               repository name or spec
diff --git a/docs/reference/ocm_install.md b/docs/reference/ocm_install.md
index fc01db3356..3646334332 100644
--- a/docs/reference/ocm_install.md
+++ b/docs/reference/ocm_install.md
@@ -1,4 +1,4 @@
-## ocm install — Install Elements.
+## ocm install — Install New OCM CLI Components
 
 ### Synopsis
 
diff --git a/docs/reference/ocm_install_plugins.md b/docs/reference/ocm_install_plugins.md
index 9e501520c0..9c20720c97 100644
--- a/docs/reference/ocm_install_plugins.md
+++ b/docs/reference/ocm_install_plugins.md
@@ -87,6 +87,6 @@ $ ocm install plugin -r demo
 
 #### Parents
 
-* [ocm install](ocm_install.md)	 — Install elements.
+* [ocm install](ocm_install.md)	 — Install new OCM CLI components
 * [ocm](ocm.md)	 — Open Component Model command line client
 
diff --git a/docs/reference/ocm_transfer_commontransportarchive.md b/docs/reference/ocm_transfer_commontransportarchive.md
index 17ec4d89e6..799b4d1d35 100644
--- a/docs/reference/ocm_transfer_commontransportarchive.md
+++ b/docs/reference/ocm_transfer_commontransportarchive.md
@@ -29,7 +29,7 @@ commontransportarchive, ctf
   -s, --scriptFile string           filename of transfer handler script
   -E, --stop-on-existing            stop on existing component version in target repository
   -t, --type string                 archive format (directory, tar, tgz) (default "directory")
-      --uploader =     repository uploader ([:[:]]==     repository uploader ([:[:[:]]]=) (default [])
 ```
 
 ### Description
diff --git a/docs/reference/ocm_transfer_componentversions.md b/docs/reference/ocm_transfer_componentversions.md
index f335ae5629..86af0d1002 100644
--- a/docs/reference/ocm_transfer_componentversions.md
+++ b/docs/reference/ocm_transfer_componentversions.md
@@ -34,7 +34,7 @@ componentversions, componentversion, cv, components, component, comps, comp, c
   -s, --scriptFile string           filename of transfer handler script
   -E, --stop-on-existing            stop on existing component version in target repository
   -t, --type string                 archive format (directory, tar, tgz) (default "directory")
-      --uploader =     repository uploader ([:[:]]==     repository uploader ([:[:[:]]]=) (default [])
 ```
 
 ### Description
diff --git a/examples/lib/tour/01-getting-started/README.md b/examples/lib/tour/01-getting-started/README.md
index eef92cf8c4..fd23f76256 100644
--- a/examples/lib/tour/01-getting-started/README.md
+++ b/examples/lib/tour/01-getting-started/README.md
@@ -128,7 +128,9 @@ Now, we have a look at the latest version. It is
 the last one in the list.
 
 ```go
-	cv, err := c.LookupVersion(versions[len(versions)-1])
+	// to retrieve the latest version use
+	// cv, err := c.LookupVersion(versions[len(versions)-1])
+	cv, err := c.LookupVersion("0.17.0")
 	if err != nil {
 		return errors.Wrapf(err, "cannot get latest version")
 	}
@@ -168,32 +170,32 @@ differ, because the code always describes the latest version):
 
 ```text
 resources of the latest version:
-  version:  0.16.2
+  version:  0.17.0
   provider: ocm.software
    1: name:           ocmcli
       extra identity: "architecture"="amd64","os"="linux"
       resource type:  executable
-      access:         Local blob sha256:b199a1e6558af64898cf0af5245907a12ff3ce152926e30a1a446c8aa6f85fec[]
+      access:         Local blob sha256:03a45dcde67ba565fe806cb5db67da3387f772f7c50af711a0edd6f802570c04[]
    2: name:           ocmcli
       extra identity: "architecture"="arm64","os"="linux"
       resource type:  executable
-      access:         Local blob sha256:b942839e4e86286ad702a9bfbdc30100c970e6ed1d6e45de3a06b67998f746bf[]
+      access:         Local blob sha256:5a622634ae43cf03eac91079389d83266891d1f9b2d8a3884cef6fe639180324[]
    3: name:           ocmcli
       extra identity: "architecture"="arm64","os"="darwin"
       resource type:  executable
-      access:         Local blob sha256:827662585090c8fe0ffb0e3380b2219a0f1ef65d1e320767b2bdf5723bf0ac31[]
+      access:         Local blob sha256:1482fe5b764e3a86cf96704d7a839ad7e53dcbfd4f5fce5405abffb1962153dd[]
    4: name:           ocmcli
       extra identity: "architecture"="amd64","os"="darwin"
       resource type:  executable
-      access:         Local blob sha256:c33865145684d2bf45fd50a80dd5435f3bf3f7153058126589e8a92f369205a4[]
+      access:         Local blob sha256:805f181aff48511eea12c699ed1bbcee8bdc4c5168924e81058aff8715946875[]
    5: name:           ocmcli
       extra identity: "architecture"="amd64","os"="windows"
       resource type:  executable
-      access:         Local blob sha256:97057f0822fb137b872c94f81c24618a3df25ea14bd956fea85464c6e8465661[]
+      access:         Local blob sha256:20839c68bf0c4cf99444d78ebb93f53358fa9e95fe806f186220bd21d520efa7[]
    6: name:           ocmcli-image
       extra identity: 
       resource type:  ociImage
-      access:         OCI artifact ghcr.io/open-component-model/ocm/ocm.software/ocmcli/ocmcli-image:0.16.2@sha256:491c08697c6a0be0f0a2d468377eff4deb271b68169a8cf8eb992c553da32df8
+      access:         OCI artifact ghcr.io/open-component-model/ocm/ocm.software/ocmcli/ocmcli-image:0.17.0@sha256:16fb52a1cb11c867bd058f4124dea53fbab94229842cc14b52653c2e80b1cede
 ```
 
 Resources have some metadata, like their identity and a resource type.
diff --git a/examples/lib/tour/01-getting-started/example.go b/examples/lib/tour/01-getting-started/example.go
index 25b5b34236..c84fe58f06 100644
--- a/examples/lib/tour/01-getting-started/example.go
+++ b/examples/lib/tour/01-getting-started/example.go
@@ -103,7 +103,9 @@ func GettingStarted() error {
 	// Now, we have a look at the latest version. it is
 	// the last one in the list.
 	// --- begin lookup version ---
-	cv, err := c.LookupVersion(versions[len(versions)-1])
+	// to retrieve the latest version use
+	// cv, err := c.LookupVersion(versions[len(versions)-1])
+	cv, err := c.LookupVersion("0.17.0")
 	if err != nil {
 		return errors.Wrapf(err, "cannot get latest version")
 	}
diff --git a/examples/lib/tour/07-resource-management/README.md b/examples/lib/tour/07-resource-management/README.md
new file mode 100644
index 0000000000..06cda25194
--- /dev/null
+++ b/examples/lib/tour/07-resource-management/README.md
@@ -0,0 +1,297 @@
+
+
+
+# Resource Management
+
+
+
+This tour illustrates the basic contract to
+correctly work with closeable object references used
+in the library.
+
+Many objects provided by the library offer some kind of resource management. In the [first example](../01-getting-started/README.md#getting-started), this is an
+OCM repository, the OCM component, component version and the access method.
+Another important kind of objects are the `BlobAccess` implementations.
+
+Those objects may use external resources, like temporary file system content or caches. To get rid of those resources again, they offer a `Close` method.
+
+To achieve the possibility to pass those objects around in non-functional call contexts they feature some kind of resource management. It allows to handle
+the life cycle of the resource in a completely local manner. To do so, a second method `Dup` is offered, which provides an independent reference to the original resources, which can be closed separately.
+The possible externally held resource are released with the close of the last reference.
+
+This offers a simple contract to handle resources in functions or object methods:
+
+1. a function creating such an object is responsible for the life cycle of its reference
+
+    - if the object is returned, this responsibility is passed to its caller
+
+      ```go
+      func f() (Object, error) {
+          o, err:= Create()
+          if err != nil {
+              return nil, err
+          }
+          o.DoSomeThing()
+          DoSomeThingOther(o)
+          return o, nil
+      }
+      ```
+
+    - otherwise, it must be closed at the end of the function (or if it is not used anymore)
+
+      ```go
+      func f() error {
+          o, err:= Create()
+          if err != nil {
+              return err
+          }
+          defer o.Close()
+          o.DoSomeThing()
+          DoSomeThingOther(o)
+      }
+      ```
+
+   The object may be passed to any called function without bothering what this function does with this reference.
+
+2. a function receiving such an object from a function as result it inherits   the responsibility to close it again (see case 1)
+
+3. a function receiving such an object as an argument can freely use  it and a pass it around.
+
+    ```go
+    func f(o Object) {
+        o.DoSomeThing()
+        DoSomeThingOther(o)
+    }
+    ```
+
+   If it decides to store the reference in some state, it must use an own reference for this, obtained by a call to `Dup`. After obtaining an own reference the used storage context is responsible to close it again. It should never close the obtained reference, because the caller is responsible for this.
+
+    ```go
+    func (r *State) f(o Object) (err error) {
+        r.obj, err = o.Dup()
+        return err
+    }
+   
+    func (r *State) Close() error {
+        if r.obj == nil {
+            return nil
+        }
+        return r.obj.Close()
+    }
+    ```
+
+## Running the example
+
+You can call the main program without any argument.
+
+## Walkthrough
+
+The example is based on the initial [getting started scenario](../01-getting-started/README.md#getting-started).
+It separates the resource gathering from the handling of the found resources.
+
+```go
+	// gathering resources, this is completely hidden
+	// behind an implementation.
+	resources, err := GatherResources(ctx, CachingFactory{})
+	if err != nil {
+		return err
+	}
+
+	var list errors.ErrorList
+
+	list.Add(HandleResources(resources))
+
+	// we are done, so close the resources, again.
+	for i, r := range resources {
+		list.Addf(nil, r.Close(), "closing resource %d", i)
+	}
+	return list.Result()
+```
+
+The resources are provided by an array of the interface `Resource`:
+
+```go
+type Resource interface {
+	GetIdentity() metav1.Identity
+	GetType() string
+	GetAccess() string
+	GetData() ([]byte, error)
+
+	SetError(s string)
+	AddDataFromMethod(ctx ocm.ContextProvider, m ocm.AccessMethod) error
+
+	Close() error
+}
+
+```
+
+It encapsulates the technical resource handling
+and offers a `Close` method, also, to release potential local resources.
+
+The example provides one implementation, using the original access method
+to cache the data to avoid additional copies.
+
+```go
+// resource is a Resource implementation using
+// the original access method to cache the content.
+type resource struct {
+	Identity     metav1.Identity
+	ArtifactType string
+	Access       string
+	Data         blobaccess.BlobAccess
+}
+
+var _ Resource = (*resource)(nil)
+
+func (r *resource) AddDataFromMethod(ctx ocm.ContextProvider, m ocm.AccessMethod) error {
+	// provide an own reference to the method
+	// to store this in the provided resource object.
+	priv, err := m.Dup()
+	if err != nil {
+		return err
+	}
+
+	// release a possible former cache entry
+	if r.Data != nil {
+		r.Data.Close()
+	}
+	r.Data = priv.AsBlobAccess()
+	// release obsolete blob access
+	r.Access = m.AccessSpec().Describe(ctx.OCMContext())
+	return nil
+}
+
+// Close releases the cached access.
+func (r *resource) Close() error {
+	c := r.Data
+	if c == nil {
+		return nil
+	}
+	r.Data = nil
+	return c.Close()
+}
+
+```
+
+The `AddDataFromMethod` uses `Dup` to provide an own reference to the
+access method, which is stored in the provided resource object.
+It implements the `Close` method to release this cached content, again.
+The responsibility for this reference is taken by the `resource`object.
+
+In the `GatherResources` function, a repository access is created.
+It is not forwarded, and therefore closed, again, in this function.
+
+```go
+	repo, err := ctx.RepositoryForSpec(spec)
+	if err != nil {
+		return nil, errors.Wrapf(err, "cannot setup repository")
+	}
+
+	// to release potentially allocated temporary resources,
+	// many objects must be closed, if they should not be used
+	// anymore.
+	// This is typically done by a `defer` statement placed after a
+	// successful object retrieval.
+	defer repo.Close()
+```
+
+The same is done for the component version lookup.
+
+```go
+	c, err := repo.LookupComponent("ocm.software/ocmcli")
+	if err != nil {
+		return nil, errors.Wrapf(err, "cannot lookup component")
+	}
+	defer c.Close()
+```
+
+Then the resource `factory` is used to create the `Resource` objects for
+the resources found in the component version.
+
+```go
+	for _, r := range cv.GetResources() {
+		res := factory.Create(
+			r.Meta().GetIdentity(cv.GetDescriptor().Resources),
+			r.Meta().GetType(),
+		)
+		acc, err := r.Access()
+		if err != nil {
+			res.SetError(err.Error())
+		} else {
+			m, err := acc.AccessMethod(cv)
+			if err == nil {
+				// delegate data handling to target
+				// we don't know, how this is implemented.
+				err = res.AddDataFromMethod(ctx, m)
+				if err != nil {
+					res.SetError(err.Error())
+				}
+				// release local usage of the access method object
+				m.Close()
+			} else {
+				res.SetError(err.Error())
+			}
+		}
+		resources = append(resources, res)
+	}
+```
+
+Because the function cannot know what happens behind the call to
+`AddDataFromMethod`, it just closes everything what is created
+in the function, this also includes the access method (`m`).
+
+Finally, it returns the resource array after all locally created
+references are correctly closed.
+The provided `Resource` objects have taken the responsibility for
+keeping their own references.
+
+The resource handling function just uses the resources.
+
+```go
+func HandleResources(resources []Resource) error {
+	var list errors.ErrorList
+	fmt.Printf("*** resources:\n")
+	for i, r := range resources {
+		fmt.Printf("  %2d: extra identity: %s\n", i+1, r.GetIdentity())
+		fmt.Printf("      resource type:  %s\n", r.GetType())
+		fmt.Printf("      access:         %s\n", r.GetAccess())
+	}
+
+	return list.Result()
+}
+
+```
+
+The responsibility for closing the resources has been passed to
+the `ResourceManagement` functions, which calls the gather and
+the handling function. Therefore, it calls the `Resource.Close`
+function before finishing.
+
+The final output of this example looks like:
+
+```yaml
+versions for component ocm.software/ocmcli: 0.1.0-alpha.2, 0.1.0-dev, 0.3.0-dev, 0.3.0-rc.2, 0.3.0-rc.3, 0.3.0, 0.4.0-dev, 0.4.0, 0.4.1, 0.4.2, 0.4.3, 0.5.0, 0.6.0, 0.7.0, 0.8.0, 0.9.0, 0.10.0, 0.11.0, 0.12.0, 0.12.1, 0.13.0, 0.14.0, 0.15.0, 0.16.0, 0.16.1, 0.16.2, 0.17.0-rc.1, 0.17.0, 0.18.0-rc.1, 0.18.0-rc.2
+looking up resources of the latest version:
+  version:  0.17.0
+  provider: ocm.software
+*** resources:
+   1: extra identity: "architecture"="amd64","name"="ocmcli","os"="linux"
+      resource type:  executable
+      access:         Local blob sha256:03a45dcde67ba565fe806cb5db67da3387f772f7c50af711a0edd6f802570c04[]
+   2: extra identity: "architecture"="arm64","name"="ocmcli","os"="linux"
+      resource type:  executable
+      access:         Local blob sha256:5a622634ae43cf03eac91079389d83266891d1f9b2d8a3884cef6fe639180324[]
+   3: extra identity: "architecture"="arm64","name"="ocmcli","os"="darwin"
+      resource type:  executable
+      access:         Local blob sha256:1482fe5b764e3a86cf96704d7a839ad7e53dcbfd4f5fce5405abffb1962153dd[]
+   4: extra identity: "architecture"="amd64","name"="ocmcli","os"="darwin"
+      resource type:  executable
+      access:         Local blob sha256:805f181aff48511eea12c699ed1bbcee8bdc4c5168924e81058aff8715946875[]
+   5: extra identity: "architecture"="amd64","name"="ocmcli","os"="windows"
+      resource type:  executable
+      access:         Local blob sha256:20839c68bf0c4cf99444d78ebb93f53358fa9e95fe806f186220bd21d520efa7[]
+   6: extra identity: "name"="ocmcli-image"
+      resource type:  ociImage
+      access:         OCI artifact ghcr.io/open-component-model/ocm/ocm.software/ocmcli/ocmcli-image:0.17.0@sha256:16fb52a1cb11c867bd058f4124dea53fbab94229842cc14b52653c2e80b1cede
+
+```
diff --git a/examples/lib/tour/07-resource-management/example.go b/examples/lib/tour/07-resource-management/example.go
new file mode 100644
index 0000000000..d4b46e909d
--- /dev/null
+++ b/examples/lib/tour/07-resource-management/example.go
@@ -0,0 +1,262 @@
+package main
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/mandelsoft/goutils/errors"
+
+	"ocm.software/ocm/api/utils/blobaccess/blobaccess"
+
+	"ocm.software/ocm/api/ocm"
+	metav1 "ocm.software/ocm/api/ocm/compdesc/meta/v1"
+	"ocm.software/ocm/api/ocm/extensions/repositories/ocireg"
+	"ocm.software/ocm/api/utils/semverutils"
+)
+
+// --- begin resource interface ---
+type Resource interface {
+	GetIdentity() metav1.Identity
+	GetType() string
+	GetAccess() string
+	GetData() ([]byte, error)
+
+	SetError(s string)
+	AddDataFromMethod(ctx ocm.ContextProvider, m ocm.AccessMethod) error
+
+	Close() error
+}
+
+// --- end resource interface ---
+
+// --- begin resource factory ---
+// ResourceFactory is used to create a particular resource object.
+type ResourceFactory interface {
+	Create(id metav1.Identity, typ string) Resource
+}
+
+// --- end resource factory ---
+
+// --- begin resource implementation ---
+// resource is a Resource implementation using
+// the original access method to cache the content.
+type resource struct {
+	Identity     metav1.Identity
+	ArtifactType string
+	Access       string
+	Data         blobaccess.BlobAccess
+}
+
+var _ Resource = (*resource)(nil)
+
+func (r *resource) AddDataFromMethod(ctx ocm.ContextProvider, m ocm.AccessMethod) error {
+	// provide an own reference to the method
+	// to store this in the provided resource object.
+	priv, err := m.Dup()
+	if err != nil {
+		return err
+	}
+
+	// release a possible former cache entry
+	if r.Data != nil {
+		r.Data.Close()
+	}
+	r.Data = priv.AsBlobAccess()
+	// release obsolete blob access
+	r.Access = m.AccessSpec().Describe(ctx.OCMContext())
+	return nil
+}
+
+// Close releases the cached access.
+func (r *resource) Close() error {
+	c := r.Data
+	if c == nil {
+		return nil
+	}
+	r.Data = nil
+	return c.Close()
+}
+
+// --- end resource implementation ---
+
+// --- begin caching factory ---
+// CachingFactory provides resource inmplementations
+// using the original access as cache.
+type CachingFactory struct {
+}
+
+func (c CachingFactory) Create(id metav1.Identity, typ string) Resource {
+	return &resource{
+		Identity:     id,
+		ArtifactType: typ,
+	}
+}
+
+// --- end caching factory ---
+
+func (r *resource) GetIdentity() metav1.Identity {
+	return r.Identity
+}
+func (r *resource) GetType() string {
+	return r.ArtifactType
+}
+
+func (r *resource) GetAccess() string {
+	return r.Access
+}
+
+func (r *resource) SetError(s string) {
+	r.Access = "error: " + s
+}
+
+func (r *resource) GetData() ([]byte, error) {
+	if r.Data == nil {
+		return nil, fmt.Errorf("no data set")
+	}
+	return r.Data.Get()
+}
+
+func ResourceManagement() error {
+	// get the default context providing
+	// all OCM entry point registrations, like
+	// access method, repository types, etc.
+	// The context bundles all registrations and
+	// configuration settings, like credentials,
+	// which should be used when working with the OCM
+	// ecosystem.
+	ctx := ocm.DefaultContext()
+
+	// --- begin decouple ---
+	// gathering resources, this is completely hidden
+	// behind an implementation.
+	resources, err := GatherResources(ctx, CachingFactory{})
+	if err != nil {
+		return err
+	}
+
+	var list errors.ErrorList
+
+	list.Add(HandleResources(resources))
+
+	// we are done, so close the resources, again.
+	for i, r := range resources {
+		list.Addf(nil, r.Close(), "closing resource %d", i)
+	}
+	return list.Result()
+	// --- end decouple ---
+}
+
+// --- begin handle ---
+func HandleResources(resources []Resource) error {
+	var list errors.ErrorList
+	fmt.Printf("*** resources:\n")
+	for i, r := range resources {
+		fmt.Printf("  %2d: extra identity: %s\n", i+1, r.GetIdentity())
+		fmt.Printf("      resource type:  %s\n", r.GetType())
+		fmt.Printf("      access:         %s\n", r.GetAccess())
+	}
+
+	return list.Result()
+}
+
+// --- end handle ---
+
+func GatherResources(ctx ocm.Context, factory ResourceFactory) ([]Resource, error) {
+	var resources []Resource
+
+	spec := ocireg.NewRepositorySpec("ghcr.io/open-component-model/ocm")
+
+	// And the context can now be used to map the descriptor
+	// into a repository object, which then provides access
+	// to the OCM elements stored in this repository.
+	// --- begin repository ---
+	repo, err := ctx.RepositoryForSpec(spec)
+	if err != nil {
+		return nil, errors.Wrapf(err, "cannot setup repository")
+	}
+
+	// to release potentially allocated temporary resources,
+	// many objects must be closed, if they should not be used
+	// anymore.
+	// This is typically done by a `defer` statement placed after a
+	// successful object retrieval.
+	defer repo.Close()
+	// --- end repository ---
+
+	// Now, we look up the OCM CLI component.
+	// All kinds of repositories, regardless of their type
+	// feature the same interface to work with OCM content.
+	// --- begin lookup component ---
+	c, err := repo.LookupComponent("ocm.software/ocmcli")
+	if err != nil {
+		return nil, errors.Wrapf(err, "cannot lookup component")
+	}
+	defer c.Close()
+	// --- end lookup component ---
+
+	// Now we look for the versions of the component
+	// available in this repository.
+	versions, err := c.ListVersions()
+	if err != nil {
+		return nil, errors.Wrapf(err, "cannot query version names")
+	}
+
+	// OCM version names must follow the SemVer rules.
+	// Therefore, we can simply order the versions and print them.
+	err = semverutils.SortVersions(versions)
+	if err != nil {
+		return nil, errors.Wrapf(err, "cannot sort versions")
+	}
+	fmt.Printf("versions for component ocm.software/ocmcli: %s\n", strings.Join(versions, ", "))
+
+	// Now, we have a look at the latest version. it is
+	// the last one in the list.
+	// --- begin lookup version ---
+	// to retrieve the latest version use
+	// cv, err := c.LookupVersion(versions[len(versions)-1])
+	cv, err := c.LookupVersion("0.17.0")
+	if err != nil {
+		return nil, errors.Wrapf(err, "cannot get latest version")
+	}
+	defer cv.Close()
+	// --- end lookup version ---
+
+	cd := cv.GetDescriptor()
+	fmt.Printf("looking up resources of the latest version:\n")
+	fmt.Printf("  version:  %s\n", cv.GetVersion())
+	fmt.Printf("  provider: %s\n", cd.Provider.Name)
+
+	// and list all the included resources.
+	// Resources have some metadata, like the resource identity and a resource type.
+	// And they describe how the content of the resource (as blob) can be accessed.
+	// This is done by an *access specification*, again a serializable descriptor,
+	// like the repository specification.
+	// --- begin resources ---
+	for _, r := range cv.GetResources() {
+		res := factory.Create(
+			r.Meta().GetIdentity(cv.GetDescriptor().Resources),
+			r.Meta().GetType(),
+		)
+		acc, err := r.Access()
+		if err != nil {
+			res.SetError(err.Error())
+		} else {
+			m, err := acc.AccessMethod(cv)
+			if err == nil {
+				// delegate data handling to target
+				// we don't know, how this is implemented.
+				err = res.AddDataFromMethod(ctx, m)
+				if err != nil {
+					res.SetError(err.Error())
+				}
+				// release local usage of the access method object
+				m.Close()
+			} else {
+				res.SetError(err.Error())
+			}
+		}
+		resources = append(resources, res)
+	}
+	// --- end resources ---
+	return resources, nil
+}
diff --git a/examples/lib/tour/07-resource-management/main.go b/examples/lib/tour/07-resource-management/main.go
new file mode 100644
index 0000000000..75a437c009
--- /dev/null
+++ b/examples/lib/tour/07-resource-management/main.go
@@ -0,0 +1,14 @@
+package main
+
+import (
+	"fmt"
+	"os"
+)
+
+func main() {
+	err := ResourceManagement()
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Error: %s\n", err)
+		os.Exit(1)
+	}
+}
diff --git a/examples/lib/tour/README.md b/examples/lib/tour/README.md
index 6931b81dd0..9ea044220c 100644
--- a/examples/lib/tour/README.md
+++ b/examples/lib/tour/README.md
@@ -16,3 +16,4 @@ of extension points of the library.
 - [Working with Configuration](04-working-with-config/README.md#config)
 - [Transporting Component Versions](05-transporting-component-versions/README.md#transport)
 - [Signing Component Versions](06-signing-component-versions/README.md#signing)
+- [Resource Management](07-resource-management/README.md#resmgmt)
diff --git a/examples/lib/tour/docsrc/07-resource-management/README.md b/examples/lib/tour/docsrc/07-resource-management/README.md
new file mode 100644
index 0000000000..5e964d5118
--- /dev/null
+++ b/examples/lib/tour/docsrc/07-resource-management/README.md
@@ -0,0 +1,158 @@
+# Resource Management
+
+{{resmgmt}}
+
+This tour illustrates the basic contract to
+correctly work with closeable object references used
+in the library.
+
+Many objects provided by the library offer some kind of resource management. In the [first example]({{getting-started}}), this is an
+OCM repository, the OCM component, component version and the access method.
+Another important kind of objects are the `BlobAccess` implementations.
+
+Those objects may use external resources, like temporary file system content or caches. To get rid of those resources again, they offer a `Close` method.
+
+To achieve the possibility to pass those objects around in non-functional call contexts they feature some kind of resource management. It allows to handle
+the life cycle of the resource in a completely local manner. To do so, a second method `Dup` is offered, which provides an independent reference to the original resources, which can be closed separately.
+The possible externally held resource are released with the close of the last reference.
+
+This offers a simple contract to handle resources in functions or object methods:
+
+1. a function creating such an object is responsible for the life cycle of its reference
+
+    - if the object is returned, this responsibility is passed to its caller
+
+      ```go
+      func f() (Object, error) {
+          o, err:= Create()
+          if err != nil {
+              return nil, err
+          }
+          o.DoSomeThing()
+          DoSomeThingOther(o)
+          return o, nil
+      }
+      ```
+
+    - otherwise, it must be closed at the end of the function (or if it is not used anymore)
+
+      ```go
+      func f() error {
+          o, err:= Create()
+          if err != nil {
+              return err
+          }
+          defer o.Close()
+          o.DoSomeThing()
+          DoSomeThingOther(o)
+      }
+      ```
+
+   The object may be passed to any called function without bothering what this function does with this reference.
+
+2. a function receiving such an object from a function as result it inherits   the responsibility to close it again (see case 1)
+
+3. a function receiving such an object as an argument can freely use  it and a pass it around.
+
+    ```go
+    func f(o Object) {
+        o.DoSomeThing()
+        DoSomeThingOther(o)
+    }
+    ```
+
+   If it decides to store the reference in some state, it must use an own reference for this, obtained by a call to `Dup`. After obtaining an own reference the used storage context is responsible to close it again. It should never close the obtained reference, because the caller is responsible for this.
+
+    ```go
+    func (r *State) f(o Object) (err error) {
+        r.obj, err = o.Dup()
+        return err
+    }
+   
+    func (r *State) Close() error {
+        if r.obj == nil {
+            return nil
+        }
+        return r.obj.Close()
+    }
+    ```
+
+## Running the example
+
+You can call the main program without any argument.
+
+## Walkthrough
+
+The example is based on the initial [getting started scenario]({{getting-started}}).
+It separates the resource gathering from the handling of the found resources.
+
+```go
+{{include}{../../07-resource-management/example.go}{decouple}}
+```
+
+The resources are provided by an array of the interface `Resource`:
+
+```go
+{{include}{../../07-resource-management/example.go}{resource interface}}
+```
+
+It encapsulates the technical resource handling
+and offers a `Close` method, also, to release potential local resources.
+
+The example provides one implementation, using the original access method
+to cache the data to avoid additional copies.
+
+```go
+{{include}{../../07-resource-management/example.go}{resource implementation}}
+```
+
+The `AddDataFromMethod` uses `Dup` to provide an own reference to the
+access method, which is stored in the provided resource object.
+It implements the `Close` method to release this cached content, again.
+The responsibility for this reference is taken by the `resource`object.
+
+In the `GatherResources` function, a repository access is created.
+It is not forwarded, and therefore closed, again, in this function.
+
+```go
+{{include}{../../07-resource-management/example.go}{repository}}
+```
+
+The same is done for the component version lookup.
+
+```go
+{{include}{../../07-resource-management/example.go}{lookup component}}
+```
+
+Then the resource `factory` is used to create the `Resource` objects for
+the resources found in the component version.
+
+```go
+{{include}{../../07-resource-management/example.go}{resources}}
+```
+
+Because the function cannot know what happens behind the call to
+`AddDataFromMethod`, it just closes everything what is created
+in the function, this also includes the access method (`m`).
+
+Finally, it returns the resource array after all locally created
+references are correctly closed.
+The provided `Resource` objects have taken the responsibility for
+keeping their own references.
+
+The resource handling function just uses the resources.
+
+```go
+{{include}{../../07-resource-management/example.go}{handle}}
+```
+
+The responsibility for closing the resources has been passed to
+the `ResourceManagement` functions, which calls the gather and
+the handling function. Therefore, it calls the `Resource.Close`
+function before finishing.
+
+The final output of this example looks like:
+
+```yaml
+{{execute}{go}{run}{../../07-resource-management}}
+```
diff --git a/examples/lib/tour/docsrc/README.md b/examples/lib/tour/docsrc/README.md
index 52ad1a7a7e..91de1b496c 100644
--- a/examples/lib/tour/docsrc/README.md
+++ b/examples/lib/tour/docsrc/README.md
@@ -13,3 +13,4 @@ of extension points of the library.
 - [Working with Configuration]({{config}})
 - [Transporting Component Versions]({{transport}})
 - [Signing Component Versions]({{signing}})
+- [Resource Management]({{resmgmt}})
diff --git a/flake.lock b/flake.lock
index 39d7a094aa..4249bfa306 100644
--- a/flake.lock
+++ b/flake.lock
@@ -2,16 +2,16 @@
   "nodes": {
     "nixpkgs": {
       "locked": {
-        "lastModified": 1722221733,
-        "narHash": "sha256-sga9SrrPb+pQJxG1ttJfMPheZvDOxApFfwXCFO0H9xw=",
+        "lastModified": 1729665710,
+        "narHash": "sha256-AlcmCXJZPIlO5dmFzV3V2XF6x/OpNWUV8Y/FMPGd8Z4=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "12bf09802d77264e441f48e25459c10c93eada2e",
+        "rev": "2768c7d042a37de65bb1b5b3268fc987e534c49d",
         "type": "github"
       },
       "original": {
         "owner": "NixOS",
-        "ref": "nixos-24.05",
+        "ref": "nixos-unstable",
         "repo": "nixpkgs",
         "type": "github"
       }
diff --git a/flake.nix b/flake.nix
index 0a4bd6b392..de809a651c 100644
--- a/flake.nix
+++ b/flake.nix
@@ -35,7 +35,7 @@
             state = if (self ? rev) then "clean" else "dirty";
 
             # This vendorHash represents a derivative of all go.mod dependencies and needs to be adjusted with every change
-            vendorHash = "sha256-pfnq3+5xmybYvevMrWOP2UmMnN1lApTcq/oaq91Yrs0=";
+            vendorHash = "sha256-g7zvy5tMww/sqiYz93/WsR3Wol/Un3EHZp1WbGDpBAI=";
 
             src = ./.;
 
diff --git a/go.mod b/go.mod
index 83aa2666d8..dbff4b9ccb 100644
--- a/go.mod
+++ b/go.mod
@@ -8,16 +8,17 @@ require (
 	github.com/DataDog/gostackparse v0.7.0
 	github.com/InfiniteLoopSpace/go_S-MIME v0.0.0-20181221134359-3f58f9a4b2b6
 	github.com/Masterminds/semver/v3 v3.3.0
-	github.com/aws/aws-sdk-go-v2 v1.32.2
-	github.com/aws/aws-sdk-go-v2/config v1.28.0
-	github.com/aws/aws-sdk-go-v2/credentials v1.17.41
-	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.33
-	github.com/aws/aws-sdk-go-v2/service/ecr v1.36.2
-	github.com/aws/aws-sdk-go-v2/service/s3 v1.66.0
+	github.com/aws/aws-sdk-go-v2 v1.32.4
+	github.com/aws/aws-sdk-go-v2/config v1.28.4
+	github.com/aws/aws-sdk-go-v2/credentials v1.17.45
+	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.38
+	github.com/aws/aws-sdk-go-v2/service/ecr v1.36.5
+	github.com/aws/aws-sdk-go-v2/service/s3 v1.67.0
 	github.com/cloudflare/cfssl v1.6.5
 	github.com/containerd/containerd v1.7.23
+	github.com/containerd/errdefs v1.0.0
 	github.com/containerd/log v0.1.0
-	github.com/containers/image/v5 v5.32.2
+	github.com/containers/image/v5 v5.33.0
 	github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f
 	github.com/distribution/reference v0.6.0
 	github.com/docker/cli v27.3.1+incompatible
@@ -48,12 +49,12 @@ require (
 	github.com/mandelsoft/spiff v1.7.0-beta-6
 	github.com/mandelsoft/vfs v0.4.4
 	github.com/marstr/guid v1.1.0
-	github.com/mikefarah/yq/v4 v4.44.3
+	github.com/mikefarah/yq/v4 v4.44.5
 	github.com/mitchellh/copystructure v1.2.0
 	github.com/mittwald/go-helm-client v0.12.14
 	github.com/modern-go/reflect2 v1.0.2
-	github.com/onsi/ginkgo/v2 v2.20.2
-	github.com/onsi/gomega v1.34.2
+	github.com/onsi/ginkgo/v2 v2.21.0
+	github.com/onsi/gomega v1.35.1
 	github.com/opencontainers/go-digest v1.0.0
 	github.com/opencontainers/image-spec v1.1.0
 	github.com/pkg/errors v0.9.1
@@ -72,19 +73,18 @@ require (
 	github.com/xeipuuv/gojsonschema v1.2.0
 	golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
 	golang.org/x/lint v0.0.0-20210508222113-6edffad5e616
-	golang.org/x/net v0.30.0
-	golang.org/x/oauth2 v0.23.0
-	golang.org/x/text v0.19.0
+	golang.org/x/net v0.31.0
+	golang.org/x/oauth2 v0.24.0
+	golang.org/x/text v0.20.0
 	gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473
 	gopkg.in/yaml.v3 v3.0.1
-	helm.sh/helm/v3 v3.16.2
-	k8s.io/api v0.31.1
-	k8s.io/apiextensions-apiserver v0.31.1
-	k8s.io/apimachinery v0.31.1
-	k8s.io/cli-runtime v0.31.1
-	k8s.io/client-go v0.31.1
-	oras.land/oras-go/v2 v2.5.0
-	sigs.k8s.io/controller-runtime v0.19.0
+	helm.sh/helm/v3 v3.16.3
+	k8s.io/api v0.31.2
+	k8s.io/apiextensions-apiserver v0.31.2
+	k8s.io/apimachinery v0.31.2
+	k8s.io/cli-runtime v0.31.2
+	k8s.io/client-go v0.31.2
+	sigs.k8s.io/controller-runtime v0.19.1
 	sigs.k8s.io/yaml v1.4.0
 )
 
@@ -130,19 +130,19 @@ require (
 	github.com/aliyun/credentials-go v1.3.10 // indirect
 	github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
 	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 // indirect
-	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21 // indirect
+	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.21 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.23 // indirect
 	github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.27.2 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.2 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.2 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sso v1.24.2 // indirect
-	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sts v1.32.2 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.4 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.4 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sso v1.24.5 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sts v1.33.0 // indirect
 	github.com/aws/smithy-go v1.22.0 // indirect
 	github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20241009180534-e718692eec62 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
@@ -159,12 +159,11 @@ require (
 	github.com/clbanning/mxj/v2 v2.7.0 // indirect
 	github.com/cloudflare/circl v1.5.0 // indirect
 	github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect
-	github.com/containerd/errdefs v0.3.0 // indirect
 	github.com/containerd/platforms v0.2.1 // indirect
 	github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect
 	github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect
 	github.com/containers/ocicrypt v1.2.0 // indirect
-	github.com/containers/storage v1.55.0 // indirect
+	github.com/containers/storage v1.56.0 // indirect
 	github.com/coreos/go-oidc/v3 v3.11.0 // indirect
 	github.com/cyphar/filepath-securejoin v0.3.4 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
@@ -184,7 +183,7 @@ require (
 	github.com/evanphx/json-patch v5.9.0+incompatible // indirect
 	github.com/evanphx/json-patch/v5 v5.9.0 // indirect
 	github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
-	github.com/fatih/color v1.17.0 // indirect
+	github.com/fatih/color v1.18.0 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
 	github.com/fsnotify/fsnotify v1.7.0 // indirect
 	github.com/fvbommel/sortorder v1.1.0 // indirect
@@ -206,9 +205,9 @@ require (
 	github.com/go-openapi/validate v0.24.0 // indirect
 	github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
 	github.com/goccy/go-json v0.10.3 // indirect
-	github.com/goccy/go-yaml v1.12.0 // indirect
+	github.com/goccy/go-yaml v1.13.0 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
-	github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
+	github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/golang/snappy v0.0.4 // indirect
@@ -220,7 +219,7 @@ require (
 	github.com/google/go-github/v55 v55.0.0 // indirect
 	github.com/google/go-querystring v1.1.0 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
-	github.com/google/pprof v0.0.0-20241009165004-a3522334989c // indirect
+	github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect
 	github.com/google/s2a-go v0.1.8 // indirect
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
 	github.com/google/uuid v1.6.0 // indirect
@@ -267,6 +266,7 @@ require (
 	github.com/moby/docker-image-spec v1.3.1 // indirect
 	github.com/moby/locker v1.0.1 // indirect
 	github.com/moby/spdystream v0.5.0 // indirect
+	github.com/moby/sys/capability v0.3.0 // indirect
 	github.com/moby/sys/mountinfo v0.7.2 // indirect
 	github.com/moby/sys/sequential v0.6.0 // indirect
 	github.com/moby/sys/user v0.3.0 // indirect
@@ -283,6 +283,7 @@ require (
 	github.com/opencontainers/runtime-spec v1.2.0 // indirect
 	github.com/opentracing/opentracing-go v1.2.0 // indirect
 	github.com/pborman/uuid v1.2.1 // indirect
+	github.com/pelletier/go-toml v1.9.5 // indirect
 	github.com/pelletier/go-toml/v2 v2.2.3 // indirect
 	github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
 	github.com/pjbgf/sha1cd v0.3.0 // indirect
@@ -314,7 +315,6 @@ require (
 	github.com/spf13/viper v1.19.0 // indirect
 	github.com/spiffe/go-spiffe/v2 v2.4.0 // indirect
 	github.com/subosito/gotenv v1.6.0 // indirect
-	github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect
 	github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
 	github.com/thales-e-security/pool v0.0.2 // indirect
 	github.com/theupdateframework/go-tuf v0.7.0 // indirect
@@ -348,14 +348,13 @@ require (
 	go.step.sm/crypto v0.54.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	go.uber.org/zap v1.27.0 // indirect
-	golang.org/x/crypto v0.28.0 // indirect
+	golang.org/x/crypto v0.29.0 // indirect
 	golang.org/x/mod v0.21.0 // indirect
-	golang.org/x/sync v0.8.0 // indirect
-	golang.org/x/sys v0.26.0 // indirect
-	golang.org/x/term v0.25.0 // indirect
+	golang.org/x/sync v0.9.0 // indirect
+	golang.org/x/sys v0.27.0 // indirect
+	golang.org/x/term v0.26.0 // indirect
 	golang.org/x/time v0.7.0 // indirect
 	golang.org/x/tools v0.26.0 // indirect
-	golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
 	google.golang.org/api v0.200.0 // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect
@@ -366,8 +365,8 @@ require (
 	gopkg.in/ini.v1 v1.67.0 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
-	k8s.io/apiserver v0.31.1 // indirect
-	k8s.io/component-base v0.31.1 // indirect
+	k8s.io/apiserver v0.31.2 // indirect
+	k8s.io/component-base v0.31.2 // indirect
 	k8s.io/klog/v2 v2.130.1 // indirect
 	k8s.io/kube-openapi v0.0.0-20241009091222-67ed5848f094 // indirect
 	k8s.io/kubectl v0.31.1 // indirect
@@ -381,3 +380,10 @@ require (
 )
 
 retract [v0.16.0, v0.16.9] // Retract all from v0.16 due to https://github.com/open-component-model/ocm-project/issues/293
+
+// crypto/tls: Client Hello is always sent in 2 TCP frames if GODEBUG=tlskyber=1 (default) which causes
+// issues with various enterprise network gateways such as Palo Alto Networks. We have been reported issues
+// such as https://github.com/open-component-model/ocm/issues/1027 and do not want to pin our crypto/tls version.
+// As such we have decided to globally override tlskyber=0
+// For more info, see https://github.com/golang/go/issues/70047
+godebug tlskyber=0
diff --git a/go.sum b/go.sum
index f4ec6afdc8..43243efdc8 100644
--- a/go.sum
+++ b/go.sum
@@ -87,8 +87,8 @@ github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA4
 github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
 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.5 h1:bpTInLlDy/nDRWFVcefDZZ1+U8tS+rz3MxjKgu9boo0=
-github.com/Microsoft/hcsshim v0.12.5/go.mod h1:tIUGego4G1EN5Hb6KC90aDYiUI2dqLSTTOCjVNpOgZ8=
+github.com/Microsoft/hcsshim v0.12.9 h1:2zJy5KA+l0loz1HzEGqyNnjd3fyZA31ZBCGKacp6lLg=
+github.com/Microsoft/hcsshim v0.12.9/go.mod h1:fJ0gkFAna6ukt0bLdKB8djt4XIJhF/vEPuoIWYVvZ8Y=
 github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
 github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
 github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
@@ -168,48 +168,48 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d
 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.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
 github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
-github.com/aws/aws-sdk-go-v2 v1.32.2 h1:AkNLZEyYMLnx/Q/mSKkcMqwNFXMAvFto9bNsHqcTduI=
-github.com/aws/aws-sdk-go-v2 v1.32.2/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo=
+github.com/aws/aws-sdk-go-v2 v1.32.4 h1:S13INUiTxgrPueTmrm5DZ+MiAo99zYzHEFh1UNkOxNE=
+github.com/aws/aws-sdk-go-v2 v1.32.4/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6 h1:pT3hpW0cOHRJx8Y0DfJUEQuqPild8jRGmSFmBgvydr0=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.6/go.mod h1:j/I2++U0xX+cr44QjHay4Cvxj6FUbnxrgmqN3H1jTZA=
-github.com/aws/aws-sdk-go-v2/config v1.28.0 h1:FosVYWcqEtWNxHn8gB/Vs6jOlNwSoyOCA/g/sxyySOQ=
-github.com/aws/aws-sdk-go-v2/config v1.28.0/go.mod h1:pYhbtvg1siOOg8h5an77rXle9tVG8T+BWLWAo7cOukc=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.41 h1:7gXo+Axmp+R4Z+AK8YFQO0ZV3L0gizGINCOWxSLY9W8=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.41/go.mod h1:u4Eb8d3394YLubphT4jLEwN1rLNq2wFOlT6OuxFwPzU=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17 h1:TMH3f/SCAWdNtXXVPPu5D6wrr4G5hI1rAxbcocKfC7Q=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.17/go.mod h1:1ZRXLdTpzdJb9fwTMXiLipENRxkGMTn1sfKexGllQCw=
-github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.33 h1:X+4YY5kZRI/cOoSMVMGTqFXHAMg1bvvay7IBcqHpybQ=
-github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.33/go.mod h1:DPynzu+cn92k5UQ6tZhX+wfTB4ah6QDU/NgdHqatmvk=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21 h1:UAsR3xA31QGf79WzpG/ixT9FZvQlh5HY1NRqSHBNOCk=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.21/go.mod h1:JNr43NFf5L9YaG3eKTm7HQzls9J+A9YYcGI5Quh1r2Y=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21 h1:6jZVETqmYCadGFvrYEQfC5fAQmlo80CeL5psbno6r0s=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.21/go.mod h1:1SR0GbLlnN3QUmYaflZNiH1ql+1qrSiB2vwcJ+4UM60=
+github.com/aws/aws-sdk-go-v2/config v1.28.4 h1:qgD0MKmkIzZR2DrAjWJcI9UkndjR+8f6sjUQvXh0mb0=
+github.com/aws/aws-sdk-go-v2/config v1.28.4/go.mod h1:LgnWnNzHZw4MLplSyEGia0WgJ/kCGD86zGCjvNpehJs=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.45 h1:DUgm5lFso57E7150RBgu1JpVQoF8fAPretiDStIuVjg=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.45/go.mod h1:dnBpENcPC1ekZrGpSWspX+ZRGzhkvqngT2Qp5xBR1dY=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19 h1:woXadbf0c7enQ2UGCi8gW/WuKmE0xIzxBF/eD94jMKQ=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.19/go.mod h1:zminj5ucw7w0r65bP6nhyOd3xL6veAUMc3ElGMoLVb4=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.38 h1:xN0PViSptTHJ7QIKyWeWntuTCZoejutTPfhsZIoMDy0=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.38/go.mod h1:orUzUoWBICDyc+hz49KpySb3sa2Tw3c0IaFqrH4c4dg=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23 h1:A2w6m6Tmr+BNXjDsr7M90zkWjsu4JXHwrzPg235STs4=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.23/go.mod h1:35EVp9wyeANdujZruvHiQUAo9E3vbhnIO1mTCAxMlY0=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23 h1:pgYW9FCabt2M25MoHYCfMrVY2ghiiBKYWUVXfwZs+sU=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.23/go.mod h1:c48kLgzO19wAu3CPkDWC28JbaJ+hfQlsdl7I2+oqIbk=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
-github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.21 h1:7edmS3VOBDhK00b/MwGtGglCm7hhwNYnjJs/PgFdMQE=
-github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.21/go.mod h1:Q9o5h4HoIWG8XfzxqiuK/CGUbepCJ8uTlaE3bAbxytQ=
-github.com/aws/aws-sdk-go-v2/service/ecr v1.36.2 h1:VDQaVwGOokbd3VUbHF+wupiffdrbAZPdQnr5XZMJqrs=
-github.com/aws/aws-sdk-go-v2/service/ecr v1.36.2/go.mod h1:lvUlMghKYmSxSfv0vU7pdU/8jSY+s0zpG8xXhaGKCw0=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.23 h1:1SZBDiRzzs3sNhOMVApyWPduWYGAX0imGy06XiBnCAM=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.23/go.mod h1:i9TkxgbZmHVh2S0La6CAXtnyFhlCX/pJ0JsOvBAS6Mk=
+github.com/aws/aws-sdk-go-v2/service/ecr v1.36.5 h1:FMF/uaTcIdhvOwZXJfzpwanx2m4Dd6IcN4vDnAn7NAA=
+github.com/aws/aws-sdk-go-v2/service/ecr v1.36.5/go.mod h1:xhf509Ba+rG5whtO7w46O0raVzu1Og3Aba80LSvHbbQ=
 github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.27.2 h1:Zru9Iy2JPM5+uRnFnoqeOZzi8JIVIHJ0ua6JdeDHcyg=
 github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.27.2/go.mod h1:PtQC3XjutCYFCn1+i8+wtpDaXvEK+vXF2gyLIKAmh4A=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ=
-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.2 h1:4FMHqLfk0efmTqhXVRL5xYRqlEBNBiRI7N6w4jsEdd4=
-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.2/go.mod h1:LWoqeWlK9OZeJxsROW2RqrSPvQHKTpp69r/iDjwsSaw=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2 h1:s7NA1SOw8q/5c0wr8477yOPp0z+uBaXBnLE0XYb0POA=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.2/go.mod h1:fnjjWyAW/Pj5HYOxl9LJqWtEwS7W2qgcRLWP+uWbss0=
-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.2 h1:t7iUP9+4wdc5lt3E41huP+GvQZJD38WLsgVp4iOtAjg=
-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.2/go.mod h1:/niFCtmuQNxqx9v8WAPq5qh7EH25U4BF6tjoyq9bObM=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.4 h1:aaPpoG15S2qHkWm4KlEyF01zovK1nW4BBbyXuHNSE90=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.4/go.mod h1:eD9gS2EARTKgGr/W5xwgY/ik9z/zqpW+m/xOQbVxrMk=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4 h1:tHxQi/XHPK0ctd/wdOw0t7Xrc2OxcRCnVzv8lwWPu0c=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.4/go.mod h1:4GQbF1vJzG60poZqWatZlhP31y8PGCCVTvIGPdaaYJ0=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.4 h1:E5ZAVOmI2apR8ADb72Q63KqwwwdW1XcMeXIlrZ1Psjg=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.4/go.mod h1:wezzqVUOVVdk+2Z/JzQT4NxAU0NbhRe5W8pIE72jsWI=
 github.com/aws/aws-sdk-go-v2/service/kms v1.37.0 h1:ovrHGOiNu4S0GSMeexZlsMhBkUb3bCE3iOktFZ7rmBU=
 github.com/aws/aws-sdk-go-v2/service/kms v1.37.0/go.mod h1:YLqfMkq9GWbICgqT5XMIzT8I2+MxVKodTnNBo3BONgE=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.66.0 h1:xA6XhTF7PE89BCNHJbQi8VvPzcgMtmGC5dr8S8N7lHk=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.66.0/go.mod h1:cB6oAuus7YXRZhWCc1wIwPywwZ1XwweNp2TVAEGYeB8=
-github.com/aws/aws-sdk-go-v2/service/sso v1.24.2 h1:bSYXVyUzoTHoKalBmwaZxs97HU9DWWI3ehHSAMa7xOk=
-github.com/aws/aws-sdk-go-v2/service/sso v1.24.2/go.mod h1:skMqY7JElusiOUjMJMOv1jJsP7YUg7DrhgqZZWuzu1U=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2 h1:AhmO1fHINP9vFYUE0LHzCWg/LfUWUF+zFPEcY9QXb7o=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.2/go.mod h1:o8aQygT2+MVP0NaV6kbdE1YnnIM8RRVQzoeUH45GOdI=
-github.com/aws/aws-sdk-go-v2/service/sts v1.32.2 h1:CiS7i0+FUe+/YY1GvIBLLrR/XNGZ4CtM1Ll0XavNuVo=
-github.com/aws/aws-sdk-go-v2/service/sts v1.32.2/go.mod h1:HtaiBI8CjYoNVde8arShXb94UbQQi9L4EMr6D+xGBwo=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.67.0 h1:SwaJ0w0MOp0pBTIKTamLVeTKD+iOWyNJRdJ2KCQRg6Q=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.67.0/go.mod h1:TMhLIyRIyoGVlaEMAt+ITMbwskSTpcGsCPDq91/ihY0=
+github.com/aws/aws-sdk-go-v2/service/sso v1.24.5 h1:HJwZwRt2Z2Tdec+m+fPjvdmkq2s9Ra+VR0hjF7V2o40=
+github.com/aws/aws-sdk-go-v2/service/sso v1.24.5/go.mod h1:wrMCEwjFPms+V86TCQQeOxQF/If4vT44FGIOFiMC2ck=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4 h1:zcx9LiGWZ6i6pjdcoE9oXAB6mUdeyC36Ia/QEiIvYdg=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.4/go.mod h1:Tp/ly1cTjRLGBBmNccFumbZ8oqpZlpdhFf80SrRh4is=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.0 h1:s7LRgBqhwLaxcocnAniBJp7gaAB+4I4vHzqUqjH18yc=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.0/go.mod h1:9XEUty5v5UAsMiFOBJrNibZgwCeOma73jgGwwhgffa8=
 github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM=
 github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
 github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20241009180534-e718692eec62 h1:T5b8GwBFIlqQzAbqTNcyLvzcAvJ09MXrF6zyUlIic8A=
@@ -287,22 +287,27 @@ github.com/containerd/containerd v1.7.23 h1:H2CClyUkmpKAGlhQp95g2WXHfLYc7whAuvZG
 github.com/containerd/containerd v1.7.23/go.mod h1:7QUzfURqZWCZV7RLNEn1XjUCQLEf0bkaK4GjUaZehxw=
 github.com/containerd/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG023MDM=
 github.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
-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/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/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
 github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
 github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU=
 github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk=
-github.com/containers/image/v5 v5.32.2 h1:SzNE2Y6sf9b1GJoC8qjCuMBXwQrACFp4p0RK15+4gmQ=
-github.com/containers/image/v5 v5.32.2/go.mod h1:v1l73VeMugfj/QtKI+jhYbwnwFCFnNGckvbST3rQ5Hk=
+github.com/containerd/typeurl v1.0.2 h1:Chlt8zIieDbzQFzXzAeBEF92KhExuE4p9p92/QmY7aY=
+github.com/containerd/typeurl/v2 v2.2.0 h1:6NBDbQzr7I5LHgp34xAXYF5DOTQDn05X58lsPEmzLso=
+github.com/containerd/typeurl/v2 v2.2.0/go.mod h1:8XOOxnyatxSWuG8OfsZXVnAF4iZfedjS/8UHSPJnX4g=
+github.com/containers/image/v5 v5.33.0 h1:6oPEFwTurf7pDTGw7TghqGs8K0+OvPtY/UyzU0B2DfE=
+github.com/containers/image/v5 v5.33.0/go.mod h1:T7HpASmvnp2H1u4cyckMvCzLuYgpD18dSmabSw0AcHk=
 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.0 h1:X14EgRK3xNFvJEfI5O4Qn4T3E25ANudSOZz/sirVuPM=
 github.com/containers/ocicrypt v1.2.0/go.mod h1:ZNviigQajtdlxIZGibvblVuIFBKIuUI2M0QM12SD31U=
-github.com/containers/storage v1.55.0 h1:wTWZ3YpcQf1F+dSP4KxG9iqDfpQY1otaUXjPpffuhgg=
-github.com/containers/storage v1.55.0/go.mod h1:28cB81IDk+y7ok60Of6u52RbCeBRucbFOeLunhER1RQ=
+github.com/containers/storage v1.56.0 h1:DZ9KSkj6M2tvj/4bBoaJu3QDHRl35BwsZ4kmLJS97ZI=
+github.com/containers/storage v1.56.0/go.mod h1:c6WKowcAlED/DkWGNuL9bvGYqIWCVy7isRMdCSKWNjk=
 github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
 github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
 github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
@@ -386,8 +391,8 @@ github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0
 github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
 github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4=
 github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc=
-github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
-github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
+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/fluxcd/cli-utils v0.36.0-flux.9 h1:RITKdwIAqT3EFKXl7B91mj6usVjxcy7W8PJZlxqUa84=
@@ -488,8 +493,8 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
 github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
 github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
 github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
-github.com/goccy/go-yaml v1.12.0 h1:/1WHjnMsI1dlIBQutrvSMGZRQufVO3asrHfTwfACoPM=
-github.com/goccy/go-yaml v1.12.0/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU=
+github.com/goccy/go-yaml v1.13.0 h1:0Wtp0FZLd7Sm8gERmR9S6Iczzb3vItJj7NaHmFg8pTs=
+github.com/goccy/go-yaml v1.13.0/go.mod h1:IjYwxUiJDoqpx2RmbdjMUceGHZwYLon3sfOGl5Hi9lc=
 github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
 github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -498,8 +503,9 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
 github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
-github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
 github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
+github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
 github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
@@ -559,8 +565,8 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
 github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
 github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20241009165004-a3522334989c h1:NDovD0SMpBYXlE1zJmS1q55vWB/fUQBcPAqAboZSccA=
-github.com/google/pprof v0.0.0-20241009165004-a3522334989c/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
+github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
+github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
 github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
 github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
@@ -734,8 +740,9 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
 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.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
-github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
 github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+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/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
 github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
@@ -743,8 +750,8 @@ github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WT
 github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
 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/mikefarah/yq/v4 v4.44.3 h1:3zxHntH67maSHr6ynCjM44htw7LZNINmTzYn3tM2t+I=
-github.com/mikefarah/yq/v4 v4.44.3/go.mod h1:1pm9sJoyZLDql3OqgklvRCkD0XIIHMZV38jKZgAuxwY=
+github.com/mikefarah/yq/v4 v4.44.5 h1:/Xm1dM1BfyDJMg+yIpnl2AgpmLFQg3Lcm/kuyYgHEXE=
+github.com/mikefarah/yq/v4 v4.44.5/go.mod h1:rpn3xGVz+2pDuLJTlCvzatCwTmmUeHcm7MbkbtHdvkc=
 github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
@@ -764,6 +771,8 @@ github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
 github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
 github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
 github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
+github.com/moby/sys/capability v0.3.0 h1:kEP+y6te0gEXIaeQhIi0s7vKs/w0RPoH1qPa6jROcVg=
+github.com/moby/sys/capability v0.3.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/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
@@ -810,15 +819,15 @@ github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vv
 github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
 github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
 github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
-github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4=
-github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag=
+github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
+github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
 github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
 github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
-github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8=
-github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc=
+github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
+github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
 github.com/open-component-model/cobra v0.0.0-20230329075350-b1fd876abfb9 h1:b2cJvZ8nWAVvCqvPhUaFl26Wht4nM4mqfl2ksY9lVzU=
 github.com/open-component-model/cobra v0.0.0-20230329075350-b1fd876abfb9/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
 github.com/open-policy-agent/opa v0.68.0 h1:Jl3U2vXRjwk7JrHmS19U3HZO5qxQRinQbJ2eCJYSqJQ=
@@ -996,8 +1005,6 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
-github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI=
-github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
 github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs=
 github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48=
 github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes=
@@ -1140,8 +1147,8 @@ golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU
 golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
 golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
-golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
-golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
+golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
+golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
 golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
@@ -1185,11 +1192,11 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
-golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
-golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
+golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
+golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
-golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
+golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
 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=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1201,8 +1208,8 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
-golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
+golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1240,8 +1247,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
-golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
+golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 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=
@@ -1252,8 +1259,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
 golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
 golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
-golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
-golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
+golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
+golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -1264,8 +1271,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
-golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
+golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
 golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
 golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -1290,8 +1297,6 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
 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=
 golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
-golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
-golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
 google.golang.org/api v0.200.0 h1:0ytfNWn101is6e9VBoct2wrGDjOi5vn7jw5KtaQgDrU=
 google.golang.org/api v0.200.0/go.mod h1:Tc5u9kcbjO7A8SwGlYj4IiVifJU01UqXtEgDMYmBmV8=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
@@ -1366,24 +1371,24 @@ 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/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
 gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
-helm.sh/helm/v3 v3.16.2 h1:Y9v7ry+ubQmi+cb5zw1Llx8OKHU9Hk9NQ/+P+LGBe2o=
-helm.sh/helm/v3 v3.16.2/go.mod h1:SyTXgKBjNqi2NPsHCW5dDAsHqvGIu0kdNYNH9gQaw70=
+helm.sh/helm/v3 v3.16.3 h1:kb8bSxMeRJ+knsK/ovvlaVPfdis0X3/ZhYCSFRP+YmY=
+helm.sh/helm/v3 v3.16.3/go.mod h1:zeVWGDR4JJgiRbT3AnNsjYaX8OTJlIE9zC+Q7F7iUSU=
 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=
-k8s.io/api v0.31.1 h1:Xe1hX/fPW3PXYYv8BlozYqw63ytA92snr96zMW9gWTU=
-k8s.io/api v0.31.1/go.mod h1:sbN1g6eY6XVLeqNsZGLnI5FwVseTrZX7Fv3O26rhAaI=
-k8s.io/apiextensions-apiserver v0.31.1 h1:L+hwULvXx+nvTYX/MKM3kKMZyei+UiSXQWciX/N6E40=
-k8s.io/apiextensions-apiserver v0.31.1/go.mod h1:tWMPR3sgW+jsl2xm9v7lAyRF1rYEK71i9G5dRtkknoQ=
-k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U=
-k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
-k8s.io/apiserver v0.31.1 h1:Sars5ejQDCRBY5f7R3QFHdqN3s61nhkpaX8/k1iEw1c=
-k8s.io/apiserver v0.31.1/go.mod h1:lzDhpeToamVZJmmFlaLwdYZwd7zB+WYRYIboqA1kGxM=
-k8s.io/cli-runtime v0.31.1 h1:/ZmKhmZ6hNqDM+yf9s3Y4KEYakNXUn5sod2LWGGwCuk=
-k8s.io/cli-runtime v0.31.1/go.mod h1:pKv1cDIaq7ehWGuXQ+A//1OIF+7DI+xudXtExMCbe9U=
-k8s.io/client-go v0.31.1 h1:f0ugtWSbWpxHR7sjVpQwuvw9a3ZKLXX0u0itkFXufb0=
-k8s.io/client-go v0.31.1/go.mod h1:sKI8871MJN2OyeqRlmA4W4KM9KBdBUpDLu/43eGemCg=
-k8s.io/component-base v0.31.1 h1:UpOepcrX3rQ3ab5NB6g5iP0tvsgJWzxTyAo20sgYSy8=
-k8s.io/component-base v0.31.1/go.mod h1:WGeaw7t/kTsqpVTaCoVEtillbqAhF2/JgvO0LDOMa0w=
+k8s.io/api v0.31.2 h1:3wLBbL5Uom/8Zy98GRPXpJ254nEFpl+hwndmk9RwmL0=
+k8s.io/api v0.31.2/go.mod h1:bWmGvrGPssSK1ljmLzd3pwCQ9MgoTsRCuK35u6SygUk=
+k8s.io/apiextensions-apiserver v0.31.2 h1:W8EwUb8+WXBLu56ser5IudT2cOho0gAKeTOnywBLxd0=
+k8s.io/apiextensions-apiserver v0.31.2/go.mod h1:i+Geh+nGCJEGiCGR3MlBDkS7koHIIKWVfWeRFiOsUcM=
+k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw=
+k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
+k8s.io/apiserver v0.31.2 h1:VUzOEUGRCDi6kX1OyQ801m4A7AUPglpsmGvdsekmcI4=
+k8s.io/apiserver v0.31.2/go.mod h1:o3nKZR7lPlJqkU5I3Ove+Zx3JuoFjQobGX1Gctw6XuE=
+k8s.io/cli-runtime v0.31.2 h1:7FQt4C4Xnqx8V1GJqymInK0FFsoC+fAZtbLqgXYVOLQ=
+k8s.io/cli-runtime v0.31.2/go.mod h1:XROyicf+G7rQ6FQJMbeDV9jqxzkWXTYD6Uxd15noe0Q=
+k8s.io/client-go v0.31.2 h1:Y2F4dxU5d3AQj+ybwSMqQnpZH9F30//1ObxOKlTI9yc=
+k8s.io/client-go v0.31.2/go.mod h1:NPa74jSVR/+eez2dFsEIHNa+3o09vtNaWwWwb1qSxSs=
+k8s.io/component-base v0.31.2 h1:Z1J1LIaC0AV+nzcPRFqfK09af6bZ4D1nAOpWsy9owlA=
+k8s.io/component-base v0.31.2/go.mod h1:9PeyyFN/drHjtJZMCTkSpQJS3U9OXORnHQqMLDz0sUQ=
 k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
 k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
 k8s.io/kube-openapi v0.0.0-20241009091222-67ed5848f094 h1:MErs8YA0abvOqJ8gIupA1Tz6PKXYUw34XsGlA7uSL1k=
@@ -1394,10 +1399,8 @@ k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 h1:MDF6h2H/h4tbzmtIKTuctcwZmY0tY
 k8s.io/utils v0.0.0-20240921022957-49e7df575cb6/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 oras.land/oras-go v1.2.6 h1:z8cmxQXBU8yZ4mkytWqXfo6tZcamPwjsuxYU81xJ8Lk=
 oras.land/oras-go v1.2.6/go.mod h1:OVPc1PegSEe/K8YiLfosrlqlqTN9PUyFvOw5Y9gwrT8=
-oras.land/oras-go/v2 v2.5.0 h1:o8Me9kLY74Vp5uw07QXPiitjsw7qNXi8Twd+19Zf02c=
-oras.land/oras-go/v2 v2.5.0/go.mod h1:z4eisnLP530vwIOUOJeBIj0aGI0L1C3d53atvCBqZHg=
-sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q=
-sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4=
+sigs.k8s.io/controller-runtime v0.19.1 h1:Son+Q40+Be3QWb+niBXAg2vFiYWolDjjRfO8hn/cxOk=
+sigs.k8s.io/controller-runtime v0.19.1/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4=
 sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
 sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
 sigs.k8s.io/kustomize/api v0.18.0 h1:hTzp67k+3NEVInwz5BHyzc9rGxIauoXferXyjv5lWPo=
diff --git a/hack/Makefile b/hack/Makefile
index d40a41f49c..297864f875 100644
--- a/hack/Makefile
+++ b/hack/Makefile
@@ -59,7 +59,7 @@ VAULT := $(shell ($(LOCALBIN)/vault --version 2>/dev/null || echo 0.0) | sed 's/
 ifneq ($(VAULT), $(VAULT_VERSION))
 	deps += vault
 endif
-OCI_REGISTRY_VERSION := 3.0.0-alpha.1
+OCI_REGISTRY_VERSION := 3.0.0-beta.1
 OCI_REGISTRY := $(shell (registry --version 2>/dev/null || echo 0.0) | sed 's/.* v\([0-9a-z\.\-]*\).*/\1/')
 ifneq ($(OCI_REGISTRY), $(OCI_REGISTRY_VERSION))
 	deps += oci-registry
@@ -97,18 +97,18 @@ go-bindata:
 
 .PHONY: vault
 vault:
-	@if [ "$(VAULT)" != "$(VAULT_VERSION)" ]; then \
+ifneq ($(VAULT), $(VAULT_VERSION))
 		curl -o $(LOCALBIN)/vault.zip https://releases.hashicorp.com/vault/$(VAULT_VERSION)/vault_$(VAULT_VERSION)_$(OS_ARCH).zip; \
 		unzip -o $(LOCALBIN)/vault.zip -d $(LOCALBIN); \
 		rm $(LOCALBIN)/vault.zip; \
-		chmod a+x $(LOCALBIN)/vault;\
-	fi
+		chmod a+x $(LOCALBIN)/vault;
+endif
 
 .PHONY: oci-registry
 oci-registry:
-	@if [ "$(OCI_REGISTRY)" != "$(OCI_REGISTRY_VERSION)" ]; then \
-		go install -v github.com/distribution/distribution/v3/cmd/registry@v3.0.0-alpha.1; \
-	fi
+ifeq (,$(findstring $(OCI_REGISTRY_VERSION), $(OCI_REGISTRY)))
+	go install -v github.com/distribution/distribution/v3/cmd/registry@v$(OCI_REGISTRY_VERSION)
+endif
 
 $(GOPATH)/bin/goimports:
 	go install -v golang.org/x/tools/cmd/goimports@latest
diff --git a/hack/brew/go.mod b/hack/brew/go.mod
new file mode 100644
index 0000000000..e68d38213a
--- /dev/null
+++ b/hack/brew/go.mod
@@ -0,0 +1,3 @@
+module ocm.software/ocm/hack/brew
+
+go 1.23.2
\ No newline at end of file
diff --git a/hack/brew/internal/generate.go b/hack/brew/internal/generate.go
new file mode 100644
index 0000000000..2c9f68d1a9
--- /dev/null
+++ b/hack/brew/internal/generate.go
@@ -0,0 +1,116 @@
+package internal
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+	"path/filepath"
+	"strings"
+	"text/template"
+)
+
+const ClassName = "Ocm"
+
+// GenerateVersionedHomebrewFormula generates a Homebrew formula for a specific version,
+// architecture, and operating system. It fetches the SHA256 digest for each combination
+// and uses a template to create the formula file.
+func GenerateVersionedHomebrewFormula(
+	version string,
+	architectures []string,
+	operatingSystems []string,
+	releaseURL string,
+	templateFile string,
+	outputDir string,
+	writer io.Writer,
+) error {
+	values := map[string]string{
+		"ReleaseURL": releaseURL,
+		"Version":    version,
+	}
+
+	for _, targetOs := range operatingSystems {
+		for _, arch := range architectures {
+			digest, err := FetchDigestFromGithubRelease(releaseURL, version, targetOs, arch)
+			if err != nil {
+				return fmt.Errorf("failed to fetch digest for %s/%s: %w", targetOs, arch, err)
+			}
+			values[fmt.Sprintf("%s_%s_sha256", targetOs, arch)] = digest
+		}
+	}
+
+	if err := GenerateFormula(templateFile, outputDir, version, values, writer); err != nil {
+		return fmt.Errorf("failed to generate formula: %w", err)
+	}
+
+	return nil
+}
+
+// FetchDigestFromGithubRelease retrieves the SHA256 digest for a specific version, operating system, and architecture
+// from the given release URL.
+func FetchDigestFromGithubRelease(releaseURL, version, targetOs, arch string) (_ string, err error) {
+	url := fmt.Sprintf("%s/v%s/ocm-%s-%s-%s.tar.gz.sha256", releaseURL, version, version, targetOs, arch)
+	resp, err := http.Get(url)
+	if err != nil {
+		return "", fmt.Errorf("failed to get digest: %w", err)
+	}
+	defer func() {
+		err = errors.Join(err, resp.Body.Close())
+	}()
+
+	digestBytes, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return "", fmt.Errorf("failed to read digest: %w", err)
+	}
+
+	return strings.TrimSpace(string(digestBytes)), nil
+}
+
+// GenerateFormula generates the Homebrew formula file using the provided template and values.
+func GenerateFormula(templateFile, outputDir, version string, values map[string]string, writer io.Writer) error {
+	tmpl, err := template.New(filepath.Base(templateFile)).Funcs(template.FuncMap{
+		"classname": func() string {
+			return fmt.Sprintf("%sAT%s", ClassName, strings.ReplaceAll(version, ".", ""))
+		},
+	}).ParseFiles(templateFile)
+	if err != nil {
+		return fmt.Errorf("failed to parse template: %w", err)
+	}
+
+	outputFile := fmt.Sprintf("ocm@%s.rb", version)
+	if err := ensureDirectory(outputDir); err != nil {
+		return err
+	}
+
+	versionedFormula, err := os.Create(filepath.Join(outputDir, outputFile))
+	if err != nil {
+		return fmt.Errorf("failed to create output file: %w", err)
+	}
+	defer versionedFormula.Close()
+
+	if err := tmpl.Execute(versionedFormula, values); err != nil {
+		return fmt.Errorf("failed to execute template: %w", err)
+	}
+
+	if _, err := io.WriteString(writer, versionedFormula.Name()); err != nil {
+		return fmt.Errorf("failed to write output file path: %w", err)
+	}
+
+	return nil
+}
+
+// ensureDirectory checks if a directory exists and creates it if it does not.
+func ensureDirectory(dir string) error {
+	fi, err := os.Stat(dir)
+	if os.IsNotExist(err) {
+		if err := os.MkdirAll(dir, 0755); err != nil {
+			return fmt.Errorf("failed to create directory: %w", err)
+		}
+	} else if err != nil {
+		return fmt.Errorf("failed to stat directory: %w", err)
+	} else if !fi.IsDir() {
+		return fmt.Errorf("path is not a directory")
+	}
+	return nil
+}
diff --git a/hack/brew/internal/generate_test.go b/hack/brew/internal/generate_test.go
new file mode 100644
index 0000000000..02fc0620e7
--- /dev/null
+++ b/hack/brew/internal/generate_test.go
@@ -0,0 +1,145 @@
+package internal
+
+import (
+	"bytes"
+	_ "embed"
+	"fmt"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+)
+
+//go:embed ocm_formula_template.rb.tpl
+var tplFile []byte
+
+//go:embed testdata/expected_formula.rb
+var expectedResolved []byte
+
+func TestGenerateVersionedHomebrewFormula(t *testing.T) {
+	version := "1.0.0"
+	architectures := []string{"amd64", "arm64"}
+	operatingSystems := []string{"darwin", "linux"}
+	outputDir := t.TempDir()
+
+	templateFile := filepath.Join(outputDir, "ocm_formula_template.rb.tpl")
+	if err := os.WriteFile(templateFile, tplFile, os.ModePerm); err != nil {
+		t.Fatalf("failed to write template file: %v", err)
+	}
+
+	dummyDigest := "dummy-digest"
+	// Mock server to simulate fetching digests
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Write([]byte(dummyDigest))
+	}))
+	defer server.Close()
+	expectedResolved = bytes.ReplaceAll(expectedResolved, []byte("$$TEST_SERVER$$"), []byte(server.URL))
+
+	var buf bytes.Buffer
+
+	err := GenerateVersionedHomebrewFormula(
+		version,
+		architectures,
+		operatingSystems,
+		server.URL,
+		templateFile,
+		outputDir,
+		&buf,
+	)
+	if err != nil {
+		t.Fatalf("expected no error, got %v", err)
+	}
+
+	file := buf.String()
+
+	fi, err := os.Stat(file)
+	if err != nil {
+		t.Fatalf("expected no error, got %v", err)
+	}
+	if fi.Size() == 0 {
+		t.Fatalf("expected file to be non-empty")
+	}
+	if filepath.Ext(file) != ".rb" {
+		t.Fatalf("expected file to have .rb extension")
+	}
+	if !strings.Contains(file, version) {
+		t.Fatalf("expected file to contain version")
+	}
+
+	data, err := os.ReadFile(file)
+	if err != nil {
+		t.Fatalf("expected no error, got %v", err)
+	}
+
+	if string(data) != string(expectedResolved) {
+		t.Fatalf("expected %s, got %s", string(expectedResolved), string(data))
+	}
+}
+
+func TestFetchDigest(t *testing.T) {
+	expectedDigest := "dummy-digest"
+	version := "1.0.0"
+	targetOS, arch := "linux", "amd64"
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.Path != "/v1.0.0/ocm-1.0.0-linux-amd64.tar.gz.sha256" {
+			t.Fatalf("expected path %s, got %s", fmt.Sprintf("/v%[1]s/ocm-%[1]s-%s-%s.tar.gz.sha256", version, targetOS, arch), r.URL.Path)
+		}
+		w.Write([]byte(expectedDigest))
+	}))
+	defer server.Close()
+
+	digest, err := FetchDigestFromGithubRelease(server.URL, version, targetOS, arch)
+	if err != nil {
+		t.Fatalf("expected no error, got %v", err)
+	}
+	if digest != expectedDigest {
+		t.Fatalf("expected %s, got %s", expectedDigest, digest)
+	}
+}
+
+func TestGenerateFormula(t *testing.T) {
+	templateContent := `class {{ classname }} < Formula
+version "{{ .Version }}"
+end`
+	templateFile := "test_template.rb.tpl"
+	if err := os.WriteFile(templateFile, []byte(templateContent), 0644); err != nil {
+		t.Fatalf("failed to write template file: %v", err)
+	}
+	defer os.Remove(templateFile)
+
+	outputDir := t.TempDir()
+	values := map[string]string{"Version": "1.0.0"}
+
+	var buf bytes.Buffer
+
+	if err := GenerateFormula(templateFile, outputDir, "1.0.0", values, &buf); err != nil {
+		t.Fatalf("expected no error, got %v", err)
+	}
+
+	if buf.String() == "" {
+		t.Fatalf("expected non-empty output")
+	}
+
+	outputFile := filepath.Join(outputDir, "ocm@1.0.0.rb")
+	if _, err := os.Stat(outputFile); os.IsNotExist(err) {
+		t.Fatalf("expected output file to exist")
+	}
+}
+
+func TestEnsureDirectory(t *testing.T) {
+	dir := t.TempDir()
+	if err := ensureDirectory(dir); err != nil {
+		t.Fatalf("expected no error, got %v", err)
+	}
+
+	nonDirFile := filepath.Join(dir, "file")
+	if err := os.WriteFile(nonDirFile, []byte("content"), 0644); err != nil {
+		t.Fatalf("failed to write file: %v", err)
+	}
+
+	if err := ensureDirectory(nonDirFile); err == nil {
+		t.Fatalf("expected error, got nil")
+	}
+}
diff --git a/hack/brew/internal/ocm_formula_template.rb.tpl b/hack/brew/internal/ocm_formula_template.rb.tpl
new file mode 100644
index 0000000000..60a7f9013c
--- /dev/null
+++ b/hack/brew/internal/ocm_formula_template.rb.tpl
@@ -0,0 +1,55 @@
+{{- /* Go template for Homebrew Formula */ -}}
+# typed: false
+# frozen_string_literal: true
+
+class {{ classname }} < Formula
+  desc "The OCM CLI makes it easy to create component versions and embed them in build processes."
+  homepage "https://ocm.software/"
+  version "{{ .Version }}"
+
+  on_macos do
+    on_intel do
+      url "{{ .ReleaseURL }}/v{{ .Version }}/ocm-{{ .Version }}-darwin-amd64.tar.gz"
+      sha256 "{{ .darwin_amd64_sha256 }}"
+
+      def install
+        bin.install "ocm"
+      end
+    end
+    on_arm do
+      url "{{ .ReleaseURL }}/v{{ .Version }}/ocm-{{ .Version }}-darwin-arm64.tar.gz"
+      sha256 "{{ .darwin_arm64_sha256 }}"
+
+      def install
+        bin.install "ocm"
+      end
+    end
+  end
+
+  on_linux do
+    on_intel do
+      if Hardware::CPU.is_64_bit?
+        url "{{ .ReleaseURL }}/v{{ .Version }}/ocm-{{ .Version }}-linux-amd64.tar.gz"
+        sha256 "{{ .linux_amd64_sha256 }}"
+
+        def install
+          bin.install "ocm"
+        end
+      end
+    end
+    on_arm do
+      if Hardware::CPU.is_64_bit?
+        url "{{ .ReleaseURL }}/v{{ .Version }}/ocm-{{ .Version }}-linux-arm64.tar.gz"
+        sha256 "{{ .linux_arm64_sha256 }}"
+
+        def install
+          bin.install "ocm"
+        end
+      end
+    end
+  end
+
+  test do
+    system "#{bin}/ocm --version"
+  end
+end
diff --git a/hack/brew/internal/testdata/expected_formula.rb b/hack/brew/internal/testdata/expected_formula.rb
new file mode 100644
index 0000000000..4adf158cb6
--- /dev/null
+++ b/hack/brew/internal/testdata/expected_formula.rb
@@ -0,0 +1,54 @@
+# typed: false
+# frozen_string_literal: true
+
+class OcmAT100 < Formula
+  desc "The OCM CLI makes it easy to create component versions and embed them in build processes."
+  homepage "https://ocm.software/"
+  version "1.0.0"
+
+  on_macos do
+    on_intel do
+      url "$$TEST_SERVER$$/v1.0.0/ocm-1.0.0-darwin-amd64.tar.gz"
+      sha256 "dummy-digest"
+
+      def install
+        bin.install "ocm"
+      end
+    end
+    on_arm do
+      url "$$TEST_SERVER$$/v1.0.0/ocm-1.0.0-darwin-arm64.tar.gz"
+      sha256 "dummy-digest"
+
+      def install
+        bin.install "ocm"
+      end
+    end
+  end
+
+  on_linux do
+    on_intel do
+      if Hardware::CPU.is_64_bit?
+        url "$$TEST_SERVER$$/v1.0.0/ocm-1.0.0-linux-amd64.tar.gz"
+        sha256 "dummy-digest"
+
+        def install
+          bin.install "ocm"
+        end
+      end
+    end
+    on_arm do
+      if Hardware::CPU.is_64_bit?
+        url "$$TEST_SERVER$$/v1.0.0/ocm-1.0.0-linux-arm64.tar.gz"
+        sha256 "dummy-digest"
+
+        def install
+          bin.install "ocm"
+        end
+      end
+    end
+  end
+
+  test do
+    system "#{bin}/ocm --version"
+  end
+end
diff --git a/hack/brew/main.go b/hack/brew/main.go
new file mode 100644
index 0000000000..825cb925bc
--- /dev/null
+++ b/hack/brew/main.go
@@ -0,0 +1,41 @@
+package main
+
+import (
+	"flag"
+	"log"
+	"os"
+	"strings"
+
+	"ocm.software/ocm/hack/brew/internal"
+)
+
+const DefaultReleaseURL = "https://github.com/open-component-model/ocm/releases/download"
+const DefaultFormulaTemplate = "hack/brew/internal/ocm_formula_template.rb.tpl"
+const DefaultArchitectures = "amd64,arm64"
+const DefaultOperatingSystems = "darwin,linux"
+
+func main() {
+	version := flag.String("version", "", "version of the OCM formula")
+	outputDir := flag.String("outputDirectory", ".", "path to the output directory")
+	templateFile := flag.String("template", DefaultFormulaTemplate, "path to the template file")
+	architecturesRaw := flag.String("arch", DefaultArchitectures, "comma-separated list of architectures")
+	operatingSystemsRaw := flag.String("os", DefaultOperatingSystems, "comma-separated list of operating systems")
+	releaseURL := flag.String("releaseURL", DefaultReleaseURL, "URL to fetch the release from")
+
+	flag.Parse()
+
+	if *version == "" {
+		log.Fatalf("version is required")
+	}
+
+	if err := internal.GenerateVersionedHomebrewFormula(*version,
+		strings.Split(*architecturesRaw, ","),
+		strings.Split(*operatingSystemsRaw, ","),
+		*releaseURL,
+		*templateFile,
+		*outputDir,
+		os.Stdout,
+	); err != nil {
+		log.Fatalf("failed to generate formula: %v", err)
+	}
+}
diff --git a/hack/get_bare_resource_from_ctf.sh b/hack/get_bare_resource_from_ctf.sh
new file mode 100644
index 0000000000..242c85da98
--- /dev/null
+++ b/hack/get_bare_resource_from_ctf.sh
@@ -0,0 +1,59 @@
+#!/bin/bash
+
+set -e
+
+# This script is used to get a bare resource from a CTF file.
+# It can be used in case the OCM CLI is not available to extract the resource from a CTF.
+# A typical use case for this is the "OCM Inception" in which a CTF containing the CLI needs to be extracted
+# to run the CLI to extract the resource.
+#
+# In this case one can use this script to extract the correct OCM CLI without having to rely on the CLI being
+# already available.
+#
+# By default the script will look for the OCM CLI component with any version (the first encountered will be used)
+# and will extract the resource "ocmcli" for amd64/linux as a filepath. This path can then be used to run the CLI,
+# but only after allowing to execute it, e.g with `chmod +x `.
+
+COMPONENT=${1:-"ocm.software/ocmcli"}
+COMPONENT_VERSION=${2:-""}
+RESOURCE=${3:-"ocmcli"}
+ARCHITECTURE=${4:-"amd64"}
+OS=${5:-"linux"}
+MEDIA_TYPE=${6:-"application/octet-stream"}
+PATH_TO_CTF=${7:-"./gen/ctf"}
+
+INDEX=$( \
+yq -r ".artifacts | filter(.repository == \"component-descriptors/${COMPONENT}\" and (.tag | contains(\"${COMPONENT_VERSION}\")))[0].digest" \
+  "${PATH_TO_CTF}"/artifact-index.json | \
+  sed 's/:/./g' \
+)
+
+if [ -z "${INDEX}" ]; then
+  echo "No index found for ${COMPONENT}"
+  exit 1
+fi
+
+RESOURCE=$( \
+yq ".layers | filter(
+    (
+      .annotations.\"software.ocm.artifact\" |
+      from_json |
+      .[0]
+    ) as \$artifact |
+    (
+      \$artifact.identity.name == \"$RESOURCE\" and
+      \$artifact.identity.architecture == \"$ARCHITECTURE\" and
+      \$artifact.identity.os == \"$OS\" and
+      .mediaType == \"$MEDIA_TYPE\"
+    )
+  )[0].digest" "${PATH_TO_CTF}"/blobs/"${INDEX}" | sed 's/:/./g' \
+)
+
+if [ -z "${RESOURCE}" ]; then
+  echo "No resource found for ${COMPONENT}"
+  exit 1
+fi
+
+RESOURCE=$PATH_TO_CTF/blobs/$RESOURCE
+
+echo "$RESOURCE"