pax_global_header00006660000000000000000000000064152242346410014515gustar00rootroot0000000000000052 comment=210747c29c1d38732b3194878dfd8b5a6b9ad7eb oras-1.3.3/000077500000000000000000000000001522423464100124655ustar00rootroot00000000000000oras-1.3.3/.github/000077500000000000000000000000001522423464100140255ustar00rootroot00000000000000oras-1.3.3/.github/.codecov.yml000066400000000000000000000014451522423464100162540ustar00rootroot00000000000000# Copyright The ORAS 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. coverage: status: project: default: target: 75% if_ci_failed: error patch: default: target: 80% if_ci_failed: error ignore: - "test/**" - "docs/**" - "internal/testutils/**"oras-1.3.3/.github/ISSUE_TEMPLATE/000077500000000000000000000000001522423464100162105ustar00rootroot00000000000000oras-1.3.3/.github/ISSUE_TEMPLATE/bug-report.yaml000066400000000000000000000041021522423464100211570ustar00rootroot00000000000000# Copyright The ORAS 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. name: Bug Report description: File a bug report labels: [bug, triage] body: - type: markdown id: preface attributes: value: | Thank you for reporting bugs to ORAS! - type: textarea id: environment validations: required: true attributes: label: What happened in your environment? description: "Please also attach logs here using `--debug` flag." - type: textarea id: expect attributes: label: "What did you expect to happen?" - type: textarea id: reproduce validations: required: true attributes: label: "How can we reproduce it?" description: "Please tell us your environment information as minimally and precisely as possible." - type: textarea id: version validations: required: true attributes: label: What is the version of your ORAS CLI? description: "You can use the command `oras version` to get it." - type: input id: os validations: required: true attributes: label: What is your OS environment? description: "e.g. Ubuntu 16.04" - type: checkboxes id: idea attributes: label: "Are you willing to submit PRs to fix it?" description: "This is absolutely not required, but we are happy to guide you in the contribution process especially when you already have a good proposal or understanding of how to implement it. Join us at https://slack.cncf.io/ and choose #oras channel." options: - label: Yes, I am willing to fix it.oras-1.3.3/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000014301522423464100201760ustar00rootroot00000000000000# Copyright The ORAS 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. blank_issues_enabled: true contact_links: - name: Ask a question or request support in the community url: https://github.com/oras-project/oras/discussions/ about: Ask a question or request support for using ORASoras-1.3.3/.github/ISSUE_TEMPLATE/feature-request.yaml000066400000000000000000000033301522423464100222140ustar00rootroot00000000000000# Copyright The ORAS 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. name: Feature Request description: File a feature request labels: [enhancement, triage] body: - type: markdown id: preface attributes: value: "Thank you for submitting new features for ORAS." - type: input id: version attributes: label: "What is the version of your ORAS CLI" description: "You can use the command `oras version` to get it" - type: textarea id: description attributes: label: "What would you like to be added?" validations: required: true - type: textarea id: solution attributes: label: "Why is this needed for ORAS?" description: "Please describe your user story or scenario." validations: required: true - type: checkboxes id: idea attributes: label: "Are you willing to submit PRs to contribute to this feature?" description: "This is absolutely not required, but we are happy to guide you in the contribution process especially when you already have a good proposal or understanding of how to implement it. Join us at https://slack.cncf.io/ and choose #oras channel." options: - label: Yes, I am willing to implement it.oras-1.3.3/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000013561522423464100176330ustar00rootroot00000000000000**What this PR does / why we need it**: **Which issue(s) this PR fixes** *(optional, in `fixes #(, fixes #, ...)` format, will close the issue(s) when PR gets merged)*: Fixes # **Please check the following list**: - [ ] Does the affected code have corresponding tests, e.g. unit test, E2E test? - [ ] Does this change require a documentation update? - [ ] Does this introduce breaking changes that would require an announcement or bumping the major version? - [ ] Do all new files have an appropriate license header? oras-1.3.3/.github/dependabot.yml000066400000000000000000000020361522423464100166560ustar00rootroot00000000000000# Copyright The ORAS 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. version: 2 updates: - package-ecosystem: "gomod" directory: "/" schedule: interval: "daily" - package-ecosystem: "gomod" directory: "/test/e2e/" schedule: interval: "daily" - package-ecosystem: "github-actions" # Workflow files stored in the # default location of `.github/workflows` directory: "/" schedule: interval: "weekly" - package-ecosystem: "docker" directory: "/" schedule: interval: "weekly" oras-1.3.3/.github/licenserc.yml000066400000000000000000000031561522423464100165240ustar00rootroot00000000000000# Copyright The ORAS 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. header: license: spdx-id: Apache-2.0 content: | Copyright The ORAS 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. paths-ignore: - '**/*.md' - 'CODEOWNERS' - 'LICENSE' - 'KEYS' - 'go.mod' - 'go.sum' - 'go.work' - '**/testdata/**' - '**/*.svg' comment: on-failure dependency: files: - go.mod licenses: # known issue: https://github.com/apache/skywalking-eyes/pull/107#issuecomment-1129761574 - name: github.com/chzyer/logex version: v1.1.10 license: MIT oras-1.3.3/.github/workflows/000077500000000000000000000000001522423464100160625ustar00rootroot00000000000000oras-1.3.3/.github/workflows/build.yml000066400000000000000000000034631522423464100177120ustar00rootroot00000000000000# Copyright The ORAS 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. name: build on: push: branches: - main - release-* pull_request: branches: - main - release-* jobs: build: runs-on: ubuntu-latest strategy: matrix: go-version: ['1.25'] fail-fast: true steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Setup Go ${{ matrix.go-version }} uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: go-version: ${{ matrix.go-version }} check-latest: true - name: Build CLI run: make build-linux-amd64 - name: Run Unit Tests run: make test - name: Run E2E Tests run: | if [[ $GITHUB_REF_NAME == v* && $GITHUB_REF_TYPE == tag ]]; then make teste2e else make teste2e-covdata fi env: ORAS_PATH: bin/linux/amd64/oras - name: Check Version run: bin/linux/amd64/oras version - name: Upload coverage to codecov.io uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} with: fail_ci_if_error: true oras-1.3.3/.github/workflows/codeql-analysis.yml000066400000000000000000000031261522423464100216770ustar00rootroot00000000000000# Copyright The ORAS 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. name: CodeQL on: push: branches: - main - release-* pull_request: branches: - main - release-* schedule: - cron: '43 12 * * 6' jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: matrix: go-version: ['1.25'] fail-fast: false steps: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Set up Go ${{ matrix.go-version }} environment uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: go-version: ${{ matrix.go-version }} check-latest: true - name: Initialize CodeQL uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 with: languages: go - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 oras-1.3.3/.github/workflows/golangci-lint.yml000066400000000000000000000023511522423464100213350ustar00rootroot00000000000000# Copyright The ORAS 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. name: golangci-lint on: pull_request: paths-ignore: - 'docs/**' permissions: contents: read jobs: golangci: name: lint runs-on: ubuntu-latest strategy: matrix: go-version: ['1.25'] fail-fast: true steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Setup Go ${{ matrix.go-version }} uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: go-version: ${{ matrix.go-version }} check-latest: true - name: golangci-lint uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0 oras-1.3.3/.github/workflows/license-checker.yml000066400000000000000000000024631522423464100216360ustar00rootroot00000000000000# Copyright The ORAS 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. name: License Checker on: push: branches: - main - release-* pull_request: branches: - main - release-* permissions: contents: write pull-requests: write jobs: check-license: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Check license header uses: apache/skywalking-eyes/header@61275cc80d0798a405cb070f7d3a8aaf7cf2c2c1 # v0.8.0 with: mode: check config: .github/licenserc.yml - name: Check dependencies license uses: apache/skywalking-eyes/dependency@61275cc80d0798a405cb070f7d3a8aaf7cf2c2c1 # v0.8.0 with: config: .github/licenserc.yml oras-1.3.3/.github/workflows/release-ghcr.yml000066400000000000000000000031511522423464100211460ustar00rootroot00000000000000# Copyright The ORAS 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. name: release-ghcr on: push: tags: [v*] branches: [main] jobs: build: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: prepare id: prepare run: | TAG=${GITHUB_REF#refs/*/} # get branch or tag as image tag echo "ref=ghcr.io/${{ github.repository }}:${TAG}" >> $GITHUB_OUTPUT - name: docker login uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: docker build run: | docker buildx create --use docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7,linux/s390x,linux/ppc64le -t ${{ steps.prepare.outputs.ref }} --push . - name: clear if: always() run: | rm -f ${HOME}/.docker/config.json oras-1.3.3/.github/workflows/release-github.yml000066400000000000000000000026101522423464100215040ustar00rootroot00000000000000# Copyright The ORAS 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. name: release-github on: push: tags: - v* jobs: build: runs-on: ubuntu-latest permissions: contents: write steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: setup go environment uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: go-version: '1.25.7' - name: Install Syft uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 with: syft-version: v1.32.0 - name: run goreleaser uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 with: version: '~> v2' args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} oras-1.3.3/.github/workflows/release-snap.yml000066400000000000000000000041301522423464100211620ustar00rootroot00000000000000# Copyright The ORAS 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. name: release-snap on: workflow_dispatch: inputs: version: description: 'release version, like v1.2.0-beta.1' required: true isStable: type: boolean description: 'check for stable release' default: false jobs: release-snap: strategy: matrix: arch: - 'amd64' - 'arm64' - 's390x' runs-on: ubuntu-latest permissions: contents: read steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: extract version id: version run: | if [[ "${{ github.event.inputs.isStable }}" == "true" ]]; then echo "release=stable" >> $GITHUB_OUTPUT else echo "release=candidate" >> $GITHUB_OUTPUT fi echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT - name: make snapcraft run: | sed -i 's/{VERSION}/${{ steps.version.outputs.version }}/g' snapcraft.yaml sed -i 's/{ARCH}/${{ matrix.arch }}/g' snapcraft.yaml cat snapcraft.yaml - uses: canonical/action-build@3bdaa03e1ba6bf59a65f84a751d943d549a54e79 # v1.3.0 id: build - uses: canonical/action-publish@214b86e5ca036ead1668c79afb81e550e6c54d40 # v1.2.0 name: publish env: SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} with: snap: ${{ steps.build.outputs.snap }} release: ${{ steps.version.outputs.release }} oras-1.3.3/.github/workflows/stale.yml000066400000000000000000000027721522423464100177250ustar00rootroot00000000000000# Copyright The ORAS 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. name: "Close stale issues and PRs" on: schedule: - cron: "30 1 * * *" jobs: stale: runs-on: ubuntu-latest steps: - uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0 with: stale-issue-message: "This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 30 days." stale-pr-message: "This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 30 days." close-issue-message: "This issue was closed because it has been stalled for 30 days with no activity." close-pr-message: "This PR was closed because it has been stalled for 30 days with no activity." days-before-issue-stale: 60 days-before-pr-stale: 45 days-before-issue-close: 30 days-before-pr-close: 30 exempt-all-milestones: true oras-1.3.3/.github/workflows/vulnerability-check.yml000066400000000000000000000035631522423464100225600ustar00rootroot00000000000000# Copyright The ORAS 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. name: Vulnerability Check on: push: branches: - main - release-* pull_request: branches: - main - release-* jobs: govulncheck: name: Go Vulnerability Check runs-on: ubuntu-latest permissions: contents: read steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Setup Go uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: go-version: '1.25' check-latest: true - name: Install govulncheck run: go install golang.org/x/vuln/cmd/govulncheck@latest - name: Run govulncheck run: | echo "## Vulnerability Check Results" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### govulncheck" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY if govulncheck ./... 2>&1 | tee govulncheck-output.txt; then echo "✅ No vulnerabilities found" >> $GITHUB_STEP_SUMMARY else echo "❌ Vulnerabilities detected:" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY cat govulncheck-output.txt >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY exit 1 fi oras-1.3.3/.gitignore000066400000000000000000000026041522423464100144570ustar00rootroot00000000000000# Copyright The ORAS 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. # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, build with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out coverage.html # VS Code .vscode debug # Jetbrains .idea # vi *.sw* # Custom coverage.txt test/e2e/coverage.txt **/covcounters.* **/covmeta.* bin/ dist/ *.tar.gz vendor/ _dist/ .DS_Store # Distribution storage files for local E2E testing test/e2e/testdata/distribution/mount/docker/ test/e2e/testdata/distribution/mount_fallback/docker/ # OCI Layout Files ZOT storage files for local E2E testing test/e2e/testdata/zot/ !test/e2e/testdata/zot/command/images/**/* !test/e2e/testdata/zot/command/artifacts/**/* !test/e2e/testdata/zot/command/blobs/**/* !test/e2e/testdata/zot/config.json !test/e2e/testdata/zot/passwd_bcrypt test/e2e/go.work.sumoras-1.3.3/.golangci.yml000066400000000000000000000034061522423464100150540ustar00rootroot00000000000000# Copyright The ORAS 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. formatters: enable: - gofmt - goimports exclusions: generated: lax settings: gofmt: simplify: true linters: default: standard enable: - bodyclose - copyloopvar - depguard - dupl - errorlint - goheader - gomodguard - gosec - govet - ineffassign - misspell - nilerr - nilnil - noctx - staticcheck - thelper - unconvert - unparam - unused - usestdlibvars - revive - usetesting - whitespace exclusions: generated: lax presets: - comments - common-false-positives - legacy - std-error-handling rules: - linters: [revive] path: "internal/" text: "var-naming: avoid package names" warn-unused: true settings: depguard: rules: Main: deny: - pkg: github.com/hashicorp/go-multierror desc: "use errors instead" - pkg: github.com/pkg/errors desc: "use errors instead" dupl: threshold: 400 goheader: template-path: copyright.tmpl gosec: excludes: - G306 - G115 - G402 run: timeout: 10m version: "2" oras-1.3.3/.goreleaser.yml000066400000000000000000000035701522423464100154230ustar00rootroot00000000000000# Copyright The ORAS 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. # To release: # GITHUB_TOKEN=*** goreleaser version: 2 builds: - main: ./cmd/oras binary: ./oras env: - CGO_ENABLED=0 flags: - -trimpath goos: - darwin - linux - freebsd - windows goarch: - amd64 - arm64 - arm - s390x - ppc64le - riscv64 - loong64 goarm: - '7' ignore: - goos: freebsd goarch: arm64 - goos: freebsd goarch: arm - goos: freebsd goarch: ppc64le - goos: freebsd goarch: riscv64 - goos: freebsd goarch: s390x - goos: windows goarch: arm64 - goos: windows goarch: arm - goos: darwin goarch: arm ldflags: # one-line ldflags to bypass the goreleaser bugs # the git tree state is guaranteed to be clean by goreleaser - -w -s -buildid= -X oras.land/oras/internal/version.Version={{.Version}} -X oras.land/oras/internal/version.GitCommit={{.FullCommit}} -X oras.land/oras/internal/version.BuildMetadata= -X oras.land/oras/internal/version.GitTreeState=clean mod_timestamp: "{{ .CommitTimestamp }}" archives: - format: tar.gz files: - LICENSE format_overrides: - goos: windows format: zip sboms: - artifacts: archive release: draft: true prerelease: auto #signs: # - artifacts: all # args: ["--output", "${signature}", "--detach-sign", "--armor", "${artifact}"] # signature: "${artifact}.asc" oras-1.3.3/CODEOWNERS000066400000000000000000000001241522423464100140550ustar00rootroot00000000000000# Derived from OWNERS.md * @sabre1041 @sajayantony @shizhMSFT @Wwwsylvia @TerryHowe oras-1.3.3/CODE_OF_CONDUCT.md000066400000000000000000000002311522423464100152600ustar00rootroot00000000000000# Code of Conduct OCI Registry As Storage (ORAS) follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). oras-1.3.3/CONTRIBUTING.md000066400000000000000000000001641522423464100147170ustar00rootroot00000000000000# Contributing Please refer to the [ORAS Contributing guide](https://oras.land/docs/community/contributing_guide). oras-1.3.3/Dockerfile000066400000000000000000000020161522423464100144560ustar00rootroot00000000000000# Copyright The ORAS 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. FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26.5-alpine as builder ARG TARGETPLATFORM RUN apk add git make ENV ORASPKG /oras ADD . ${ORASPKG} WORKDIR ${ORASPKG} RUN make "build-$(echo $TARGETPLATFORM | tr / -)" RUN mv ${ORASPKG}/bin/${TARGETPLATFORM}/oras /go/bin/oras FROM docker.io/library/alpine:3.24.1 RUN apk --update add ca-certificates COPY --from=builder /go/bin/oras /bin/oras RUN mkdir /workspace WORKDIR /workspace ENTRYPOINT ["/bin/oras"] oras-1.3.3/KEYS000066400000000000000000000100041522423464100131560ustar00rootroot00000000000000This file contains the PGP keys of developers who have signed releases of ORAS. For your convenience, commands are provided for those who use pgp and gpg. For users to import keys: pgp < KEYS or gpg --import KEYS Developers to add their keys: pgp -kxa and append it to this file. or (pgpk -ll && pgpk -xa ) >> KEYS or (gpg --list-sigs && gpg --armor --export ) >> KEYS pub rsa4096 2024-12-04 [SC] [expires: 2025-12-04] 73C7F42E8F0B4493115ABED64F723223E9DF0185 uid [ultimate] Shiwei Zhang sig 3 4F723223E9DF0185 2024-12-04 Shiwei Zhang -----BEGIN PGP PUBLIC KEY BLOCK----- mQINBGdP0LsBEACrX5ECJSS1wZpFH0EkA8WK7S0Z8R0PlT2bFRGu0Ozd78cuxxi7 rHt0UGrGkTdr6YLQ45sZvdh+Y/ukq6IgBsHlrL5FYWQ4ZgPFMfZeRZlRJvRlmnXj Pj9JNpqXJNN0fpa3xa6oA31HxNTVMQFL/10UE3BVKJCmmQL3bMu8Q6Kggf6eqJjN HOyF9oYlZ60BE4b/5QrFDqYu87PVbZdQwbPJykmxjUImyFWT0RT+xD9Rhm1i1JUr sj0tx7RsZyLS5u83aMJEhioOSEdmTqa+S9lrXqpgY6sxWHoyt4VdzX2gYcTVl3XJ VoLHr5H63xZYF3wSr8qZ/mcw3tfxPO8kA/kWPCAvn9O7BPccoPGYXmrsutfRY4ce xFL/X7rPx2FCTcXKTaiD+Xp9+sjhBO0hfjXPYJHYjgsgCvDNanypUEXo/aTjh+mn GcJy2/7nw0gSWhlDCKZzZeVQ7B3sIM+EuEE7HsodUfO51QTNan8+fApXPcTnNi5K cPhE26tlgIGoyIKkTSElt+RxIAEVs1s8CQGxVexihVyPZZJW0L3IFsXAuuU2RoYH iyM4zh1HJxUOZoaAV/CHZKyQCaqAdHrMjtVLF/mA0mGgk+YyeHN1XkKxBgnNLZ75 L6rt1ukBhe6Jz61V2um76WO52TRJocybLdlDHkQLQ0n86rakKLmavj3mGwARAQAB tCJTaGl3ZWkgWmhhbmcgPHNoaXpoQG1pY3Jvc29mdC5jb20+iQJUBBMBCgA+FiEE c8f0Lo8LRJMRWr7WT3IyI+nfAYUFAmdP0LsCGwMFCQHhM4AFCwkIBwIGFQoJCAsC BBYCAwECHgECF4AACgkQT3IyI+nfAYUCHg//cOwIs0dSv8bWMxPp+6imi7jZgiW1 i9UePWDVwg28O9lsQAv3Yc7fvC2QoI7cQ5O0k+VqYNYUbnmM+uMYc2B1Q1SbG1sz tcHeTU4Wzq5VqHnWSh1LapPSeHeupQTSEuF3XOSmkYPHs6wxt5wH1qM3E6f8fISZ ZPsvoRaoJfvUYcUbGaldHmPVQy9RJx7i2aWggg1Gs9axj9Am0kWtosQva3Y6jRJ+ DHReHFnokyA3U5q01UcvMu+GIOqhMMF3C5YWxKXfdjn66t4F1FwRldWpmNVLbVzu NQZwKzZW07bfCAx04rPHMkPRlbaWXhSunUzPKxRqw3iUQBURvYb2iAxPLB1meVUr HPEg3nC8EEYQUhii6C+lxOv1YXxKQsSXjyP5+pVQ0MudVlOG/K5MLM/eQ8CoSi/5 IR1nU9bP55OLUSGFxgg7tQT9o4NydlgkzM6oiI84GDWbkWnR/slt/J77EKqULACv CHRPOwmT8gi1uIlbtAPrJ0/Xq2ERpZBZ4EtUZTD2hP3GR3gKShvocDAnRd0mTwNd mhIkUA7BNzIVRzbzwLuxnFawyhu+e2enQRf/ltDRwstnpZ6at/t15DxCRv7GRdG2 R2N/iKu82t4prtB3o4sDU0aYU+NAjGemhkKbLC90yGa8QCXTVWk3t8rOwWEZoTs6 0TaHyX7iwh/uXs8= =2B8p -----END PGP PUBLIC KEY BLOCK----- pub nistp384 2025-08-14 [SC] [expires: 2026-08-14] 3E94D52AFA465877CAEA79F1E8052EA17ECB9A6C uid [ultimate] Lixia (Sylvia) Lei sig 3 E8052EA17ECB9A6C 2025-08-14 [self-signature] -----BEGIN PGP PUBLIC KEY BLOCK----- mG8EaJ1rTxMFK4EEACIDAwSqMYRVpUWwMHv5N6+IW62RlYUI4N75PPOJXxue6Ll1 RUuH6Pr/nPrdPhi4M1euJA8FIPrCaS+lhzDCHatt/TyEomhZo+ClqJFiC4WBvujC 9Pb+Ss6ugAZ1jpDcHMhlotG0KUxpeGlhIChTeWx2aWEpIExlaSA8bGl4bGVpQG1p Y3Jvc29mdC5jb20+iLkEExMJAEEWIQQ+lNUq+kZYd8rqefHoBS6hfsuabAUCaJ1r TwIbAwUJAeEzgAULCQgHAgIiAgYVCgkICwIEFgIDAQIeBwIXgAAKCRDoBS6hfsua bKUBAYCwe/68VUdPLWM1vt+x695wIg3JTn0Cy6sq/j+4WrgkzvOlbVxS2CP2HzNX OyR2zcoBf1p6yubqK+xgnaeEaimBQCGWXiDyM+Kk69F50/ccVr1o2Y25o4Uyu0hi SVi85KgQBA== =Ih64 -----END PGP PUBLIC KEY BLOCK----- pub ed25519 2025-06-06 [SC] 2DA461D13B0C27845EDFA77FE462A3894CBAAA47 uid [ultimate] Terry Howe sig 3 E462A3894CBAAA47 2025-06-06 [self-signature] sub cv25519 2025-06-06 [E] sig E462A3894CBAAA47 2025-06-06 [self-signature] -----BEGIN PGP PUBLIC KEY BLOCK----- mDMEaELDEBYJKwYBBAHaRw8BAQdACwT+K9xzXLTJoPgbbUANEXnCLzej3Er41+YK wuAIX2G0IVRlcnJ5IEhvd2UgPHRlcnJ5bGhvd2VAZ21haWwuY29tPoiTBBMWCgA7 FiEELaRh0TsMJ4Re36d/5GKjiUy6qkcFAmhCwxACGwMFCwkIBwICIgIGFQoJCAsC BBYCAwECHgcCF4AACgkQ5GKjiUy6qkftMgEA4HwMQoHj5PSKbW1Dysoi0z/0w77I STeuKCmqObZvYHIBAI7c4FfJ6pHDghMBcLgg2CzLbUM2C+C9mLIwKI8C8RUKuDgE aELDEBIKKwYBBAGXVQEFAQEHQNqKXMKfLczJCgpjAcNRiHAFJN1n8sYOy6jUCVRF hEdWAwEIB4h4BBgWCgAgFiEELaRh0TsMJ4Re36d/5GKjiUy6qkcFAmhCwxACGwwA CgkQ5GKjiUy6qkfIYAEAnN+lGbgYfzmB2JIj/qb39teFmciJCU8vfxfkNKeE7FoA /jV/gyOIb3rPCBxzkbBOoFi76lGK4GUdEZNU6OJAg1QD =ePpx -----END PGP PUBLIC KEY BLOCK----- oras-1.3.3/LICENSE000066400000000000000000000261171522423464100135010ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2021 ORAS 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. oras-1.3.3/MAINTAINERS.md000066400000000000000000000002211522423464100145540ustar00rootroot00000000000000# Maintainers Maintainers: - Billy Zha (@qweeah) - Sylvia Lei (@Wwwsylvia) - Terry Howe (@TerryHowe) [Owners](OWNERS.md) are also maintainers. oras-1.3.3/Makefile000066400000000000000000000154211522423464100141300ustar00rootroot00000000000000# Copyright The ORAS 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. PROJECT_PKG = oras.land/oras CLI_EXE = oras CLI_PKG = $(PROJECT_PKG)/cmd/oras GIT_COMMIT = $(shell git rev-parse HEAD) GIT_TAG = $(shell git describe --tags --abbrev=0 --exact-match 2>/dev/null) GIT_DIRTY = $(shell test -n "`git status --porcelain`" && echo "dirty" || echo "clean") GO_EXE = go OSNAME = $(shell uname -o) ARCHNAME = $(shell uname -m) PKG = ./... ifeq ($(OSNAME),Darwin) OS = mac else OS = linux endif ifeq ($(ARCHNAME),arm64) ARCH = arm64 else ARCH = amd64 endif TARGET_OBJS ?= checksums.txt darwin_amd64.tar.gz darwin_arm64.tar.gz linux_amd64.tar.gz linux_arm64.tar.gz linux_armv7.tar.gz linux_s390x.tar.gz linux_ppc64le.tar.gz linux_riscv64.tar.gz linux_loong64.tar.gz windows_amd64.zip freebsd_amd64.tar.gz LDFLAGS = -w ifdef VERSION LDFLAGS += -X $(PROJECT_PKG)/internal/version.BuildMetadata=$(VERSION) endif ifneq ($(GIT_TAG),) LDFLAGS += -X $(PROJECT_PKG)/internal/version.BuildMetadata= endif LDFLAGS += -X $(PROJECT_PKG)/internal/version.GitCommit=${GIT_COMMIT} LDFLAGS += -X $(PROJECT_PKG)/internal/version.GitTreeState=${GIT_DIRTY} .PHONY: default default: lint test build-$(OS)-$(ARCH) @echo 'Done ' build-$(OS)-$(ARCH) .PHONY: test test: tidy vendor check-encoding ## tidy and run tests $(GO_EXE) test -race -v -coverprofile=coverage.txt -covermode=atomic -coverpkg=$(PKG) $(PKG) .PHONY: teste2e teste2e: ## run end to end tests ./test/e2e/scripts/e2e.sh $(shell git rev-parse --show-toplevel) --clean .PHONY: covhtml covhtml: ## look at code coverage open .cover/coverage.html .PHONY: clean clean: ## clean up build git status --ignored --short | grep '^!! ' | sed 's/!! //' | xargs rm -rf .PHONY: build build: build-linux build-mac build-windows ## build for all targets .PHONY: build-linux-all build-linux-all: build-linux-amd64 build-linux-arm64 build-linux-arm-v7 build-linux-s390x build-linux-ppc64le build-linux-riscv64 build-linux-loong64 ## build all linux architectures .PHONY: build-linux build-linux: build-linux-amd64 build-linux-arm64 .PHONY: build-linux-amd64 build-linux-amd64: ## build for linux amd64 GOARCH=amd64 CGO_ENABLED=0 GOOS=linux $(GO_EXE) build -v --ldflags="$(LDFLAGS)" \ -o bin/linux/amd64/$(CLI_EXE) $(CLI_PKG) .PHONY: build-linux-arm64 build-linux-arm64: ## build for linux arm64 GOARCH=arm64 CGO_ENABLED=0 GOOS=linux $(GO_EXE) build -v --ldflags="$(LDFLAGS)" \ -o bin/linux/arm64/$(CLI_EXE) $(CLI_PKG) .PHONY: build-linux-arm-v7 build-linux-arm-v7: ## build for linux arm v7 GOARCH=arm CGO_ENABLED=0 GOOS=linux $(GO_EXE) build -v --ldflags="$(LDFLAGS)" \ -o bin/linux/arm/v7/$(CLI_EXE) $(CLI_PKG) .PHONY: build-linux-s390x build-linux-s390x: ## build for linux s390x GOARCH=s390x CGO_ENABLED=0 GOOS=linux $(GO_EXE) build -v --ldflags="$(LDFLAGS)" \ -o bin/linux/s390x/$(CLI_EXE) $(CLI_PKG) .PHONY: build-linux-ppc64le build-linux-ppc64le: ## build for linux ppc64le GOARCH=ppc64le CGO_ENABLED=0 GOOS=linux $(GO_EXE) build -v --ldflags="$(LDFLAGS)" \ -o bin/linux/ppc64le/$(CLI_EXE) $(CLI_PKG) .PHONY: build-linux-riscv64 build-linux-riscv64: ## build for linux riscv64 GOARCH=riscv64 CGO_ENABLED=0 GOOS=linux $(GO_EXE) build -v --ldflags="$(LDFLAGS)" \ -o bin/linux/riscv64/$(CLI_EXE) $(CLI_PKG) .PHONY: build-linux-loong64 build-linux-loong64: ## build for linux loong64 GOARCH=loong64 CGO_ENABLED=0 GOOS=linux $(GO_EXE) build -v --ldflags="$(LDFLAGS)" \ -o bin/linux/loong64/$(CLI_EXE) $(CLI_PKG) .PHONY: build-mac build-mac: build-mac-arm64 build-mac-amd64 ## build all mac architectures .PHONY: build-mac-amd64 build-mac-amd64: ## build for mac amd64 GOARCH=amd64 CGO_ENABLED=0 GOOS=darwin $(GO_EXE) build -v --ldflags="$(LDFLAGS)" \ -o bin/darwin/amd64/$(CLI_EXE) $(CLI_PKG) .PHONY: build-mac-arm64 build-mac-arm64: ## build for mac arm64 GOARCH=arm64 CGO_ENABLED=0 GOOS=darwin $(GO_EXE) build -v --ldflags="$(LDFLAGS)" \ -o bin/darwin/arm64/$(CLI_EXE) $(CLI_PKG) .PHONY: build-windows build-windows: build-windows-amd64 build-windows-arm64 ## build all windows architectures .PHONY: build-windows-amd64 build-windows-amd64: ## build for windows amd64 GOARCH=amd64 CGO_ENABLED=0 GOOS=windows $(GO_EXE) build -v --ldflags="$(LDFLAGS)" \ -o bin/windows/amd64/$(CLI_EXE).exe $(CLI_PKG) .PHONY: build-windows-arm64 build-windows-arm64: ## build for windows arm64 GOARCH=arm64 CGO_ENABLED=0 GOOS=windows $(GO_EXE) build -v --ldflags="$(LDFLAGS)" \ -o bin/windows/arm64/$(CLI_EXE).exe $(CLI_PKG) .PHONY: build-freebsd build-freebsd: build-freebsd-amd64 ## build all freebsd architectures .PHONY: build-freebsd-amd64 build-freebsd-amd64: ## build for freebsd amd64 GOARCH=amd64 CGO_ENABLED=0 GOOS=freebsd $(GO_EXE) build -v --ldflags="$(LDFLAGS)" \ -o bin/freebsd/amd64/$(CLI_EXE) $(CLI_PKG) .PHONY: check-encoding check-encoding: ## check file CR/LF encoding ! find cmd internal -name "*.go" -type f -exec file "{}" ";" | grep CRLF .PHONY: fix-encoding fix-encoding: ## fix file CR/LF encoding find cmd internal -type f -name "*.go" -exec sed -i -e "s/\r//g" {} + .PHONY: lint lint: ## run CI version of lint golangci-lint run $(PKG) .PHONY: govulncheck govulncheck: ## run vulnerability check on dependencies $(GO_EXE) run golang.org/x/vuln/cmd/govulncheck@latest $(PKG) .PHONY: tidy tidy: ## go mod tidy GO111MODULE=on $(GO_EXE) mod tidy .PHONY: vendor vendor: ## go mod vendor GO111MODULE=on $(GO_EXE) mod vendor .PHONY: fetch-dist fetch-dist: ## fetch distribution mkdir -p _dist cd _dist && \ for obj in ${TARGET_OBJS} ; do \ curl -sSL -o oras_${VERSION}_$${obj} https://github.com/oras-project/oras/releases/download/v${VERSION}/oras_${VERSION}_$${obj} ; \ done .PHONY: sign sign: ## sign for f in $$(ls _dist/*.{gz,txt} 2>/dev/null) ; do \ gpg --armor --detach-sign $${f} ; \ done .PHONY: teste2e-covdata teste2e-covdata: ## test e2e coverage export GOCOVERDIR=$(CURDIR)/test/e2e/.cover; \ rm -rf $$GOCOVERDIR; \ mkdir -p $$GOCOVERDIR; \ $(MAKE) teste2e && $(GO_EXE) tool covdata textfmt -i=$$GOCOVERDIR -o "$(CURDIR)/test/e2e/coverage.txt" .PHONY: help help: ## Display this help @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[%\/0-9A-Za-z_-]+:.*?##/ { printf " \033[36m%-45s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) oras-1.3.3/OWNERS.md000066400000000000000000000004551522423464100140300ustar00rootroot00000000000000# Owners Owners: - Andrew Block (@sabre1041) - Sajay Antony (@sajayantony) - Shiwei Zhang (@shizhMSFT) - Terry Howe (@TerryHowe) Emeritus: - Avi Deitcher (@deitch) - Jimmy Zelinskie (@jzelinskie) - Josh Dolitsky (@jdolitsky) - Steve Lasker (@stevelasker) - Vincent Batts (@vbatts) oras-1.3.3/README.md000066400000000000000000000021701522423464100137440ustar00rootroot00000000000000# ORAS CLI [![Build Status](https://github.com/oras-project/oras/actions/workflows/build.yml/badge.svg?event=push)](https://github.com/oras-project/oras/actions/workflows/build.yml?query=workflow%3Abuild+event%3Apush) [![codecov](https://codecov.io/gh/oras-project/oras/branch/main/graph/badge.svg)](https://codecov.io/gh/oras-project/oras) [![Go Report Card](https://goreportcard.com/badge/oras.land/oras)](https://goreportcard.com/report/oras.land/oras)

banner

## Docs Documentation for the ORAS CLI is located on the project website: [oras.land/cli](https://oras.land/docs/category/oras-commands) ## Development Environment Setup Refer to the [development guide](https://oras.land/community/developer_guide) to get started [contributing to ORAS](https://oras.land/community/contributing_guide). ## Code of Conduct This project has adopted the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for further details. oras-1.3.3/SECURITY.md000066400000000000000000000002441522423464100142560ustar00rootroot00000000000000# Security Policy Please follow the [security policy](https://oras.land/docs/community/reporting_security_concerns) to report a security vulnerability or concern. oras-1.3.3/cmd/000077500000000000000000000000001522423464100132305ustar00rootroot00000000000000oras-1.3.3/cmd/oras/000077500000000000000000000000001522423464100141745ustar00rootroot00000000000000oras-1.3.3/cmd/oras/internal/000077500000000000000000000000001522423464100160105ustar00rootroot00000000000000oras-1.3.3/cmd/oras/internal/argument/000077500000000000000000000000001522423464100176325ustar00rootroot00000000000000oras-1.3.3/cmd/oras/internal/argument/checker.go000066400000000000000000000020621522423464100215650ustar00rootroot00000000000000/* Copyright The ORAS 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 argument import "fmt" // Exactly checks if the number of arguments is exactly cnt. func Exactly(cnt int) func(args []string) (bool, string) { return func(args []string) (bool, string) { return len(args) == cnt, fmt.Sprintf("exactly %d argument", cnt) } } // AtLeast checks if the number of arguments is larger or equal to cnt. func AtLeast(cnt int) func(args []string) (bool, string) { return func(args []string) (bool, string) { return len(args) >= cnt, fmt.Sprintf("at least %d argument", cnt) } } oras-1.3.3/cmd/oras/internal/command/000077500000000000000000000000001522423464100174265ustar00rootroot00000000000000oras-1.3.3/cmd/oras/internal/command/logger.go000066400000000000000000000017751522423464100212460ustar00rootroot00000000000000/* Copyright The ORAS 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 command import ( "context" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/internal/trace" ) // GetLogger returns a new FieldLogger and an associated Context derived from command context. func GetLogger(cmd *cobra.Command, opts *option.Common) (context.Context, logrus.FieldLogger) { ctx, logger := trace.NewLogger(cmd.Context(), opts.Debug) cmd.SetContext(ctx) return ctx, logger } oras-1.3.3/cmd/oras/internal/display/000077500000000000000000000000001522423464100174555ustar00rootroot00000000000000oras-1.3.3/cmd/oras/internal/display/content/000077500000000000000000000000001522423464100211275ustar00rootroot00000000000000oras-1.3.3/cmd/oras/internal/display/content/discard.go000066400000000000000000000020461522423464100230710ustar00rootroot00000000000000/* Copyright The ORAS 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 content import ocispec "github.com/opencontainers/image-spec/specs-go/v1" type DiscardHandler struct{} // OnContentFetched implements ManifestFetchHandler. func (DiscardHandler) OnContentFetched(ocispec.Descriptor, []byte) error { return nil } // OnContentCreated implements ManifestIndexCreateHandler. func (DiscardHandler) OnContentCreated([]byte) error { return nil } // NewDiscardHandler returns a new discard handler. func NewDiscardHandler() DiscardHandler { return DiscardHandler{} } oras-1.3.3/cmd/oras/internal/display/content/discard_test.go000066400000000000000000000035111522423464100241260ustar00rootroot00000000000000/* Copyright The ORAS 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 content import ( "testing" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) func TestNewDiscardHandler(t *testing.T) { handler := NewDiscardHandler() if handler != (DiscardHandler{}) { t.Errorf("NewDiscardHandler() returned unexpected handler") } } func TestDiscardHandler_OnContentFetched(t *testing.T) { handler := NewDiscardHandler() desc := ocispec.Descriptor{ MediaType: "application/vnd.oci.image.manifest.v1+json", Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", Size: 0, } content := []byte("test content") err := handler.OnContentFetched(desc, content) if err != nil { t.Errorf("DiscardHandler.OnContentFetched() error = %v, want nil", err) } } func TestDiscardHandler_OnContentCreated(t *testing.T) { handler := NewDiscardHandler() content := []byte("test content") err := handler.OnContentCreated(content) if err != nil { t.Errorf("DiscardHandler.OnContentCreated() error = %v, want nil", err) } } func TestDiscardHandler_ImplementsInterfaces(_ *testing.T) { handler := NewDiscardHandler() // Verify DiscardHandler implements ManifestFetchHandler var _ ManifestFetchHandler = handler // Verify DiscardHandler implements ManifestIndexCreateHandler var _ ManifestIndexCreateHandler = handler } oras-1.3.3/cmd/oras/internal/display/content/interface.go000066400000000000000000000023651522423464100234240ustar00rootroot00000000000000/* Copyright The ORAS 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 content import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) // ManifestFetchHandler handles raw output for manifest fetch events. type ManifestFetchHandler interface { // OnContentFetched is called after the manifest content is fetched. OnContentFetched(desc ocispec.Descriptor, content []byte) error } // ManifestIndexCreateHandler handles raw output for manifest index create events. type ManifestIndexCreateHandler interface { // OnContentCreated is called after the index content is created. OnContentCreated(content []byte) error } // ManifestIndexUpdateHandler handles raw output for manifest index update events. type ManifestIndexUpdateHandler ManifestIndexCreateHandler oras-1.3.3/cmd/oras/internal/display/content/manifest_fetch.go000066400000000000000000000031421522423464100244350ustar00rootroot00000000000000/* Copyright The ORAS 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 content import ( "fmt" "io" "os" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/output" ) // manifestFetch handles raw content output. type manifestFetch struct { pretty bool stdout io.Writer outputPath string } func (h *manifestFetch) OnContentFetched(_ ocispec.Descriptor, manifest []byte) (eventErr error) { out := h.stdout if h.outputPath != "-" && h.outputPath != "" { f, err := os.Create(h.outputPath) if err != nil { return fmt.Errorf("failed to open %q: %w", h.outputPath, err) } defer func() { if err := f.Close(); eventErr == nil { eventErr = err } }() out = f } return output.PrintJSON(out, manifest, h.pretty) } // NewManifestFetchHandler creates a new handler. func NewManifestFetchHandler(out io.Writer, pretty bool, outputPath string) ManifestFetchHandler { // ignore --pretty when output to a file if outputPath != "" && outputPath != "-" { pretty = false } return &manifestFetch{ pretty: pretty, stdout: out, outputPath: outputPath, } } oras-1.3.3/cmd/oras/internal/display/content/manifest_index.go000066400000000000000000000031701522423464100244540ustar00rootroot00000000000000/* Copyright The ORAS 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 content import ( "fmt" "io" "os" "oras.land/oras/cmd/oras/internal/output" ) // manifestIndexCreate handles raw content output. type manifestIndexCreate struct { pretty bool stdout io.Writer outputPath string } // NewManifestIndexCreateHandler creates a new handler. func NewManifestIndexCreateHandler(out io.Writer, pretty bool, outputPath string) ManifestIndexCreateHandler { // ignore --pretty when output to a file if outputPath != "" && outputPath != "-" { pretty = false } return &manifestIndexCreate{ pretty: pretty, stdout: out, outputPath: outputPath, } } // OnContentCreated is called after index content is created. func (h *manifestIndexCreate) OnContentCreated(manifest []byte) (eventErr error) { out := h.stdout if h.outputPath != "" && h.outputPath != "-" { f, err := os.Create(h.outputPath) if err != nil { return fmt.Errorf("failed to open %q: %w", h.outputPath, err) } defer func() { if err := f.Close(); eventErr == nil { eventErr = err } }() out = f } return output.PrintJSON(out, manifest, h.pretty) } oras-1.3.3/cmd/oras/internal/display/content/manifest_index_test.go000066400000000000000000000016271522423464100255200ustar00rootroot00000000000000/* Copyright The ORAS 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 content import ( "os" "testing" ) func Test_manifestIndexCreate_OnContentCreated(t *testing.T) { testHandler := NewManifestIndexCreateHandler(os.Stdout, false, "invalid/path") if err := testHandler.OnContentCreated([]byte("test content")); err == nil { t.Errorf("manifestIndexCreate.OnContentCreated() error = %v, wantErr non-nil error", err) } } oras-1.3.3/cmd/oras/internal/display/handler.go000066400000000000000000000301401522423464100214170ustar00rootroot00000000000000/* Copyright The ORAS 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 display import ( "io" "os" ocispec "github.com/opencontainers/image-spec/specs-go/v1" fetcher "oras.land/oras-go/v2/content" "oras.land/oras/cmd/oras/internal/display/content" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/display/metadata/descriptor" "oras.land/oras/cmd/oras/internal/display/metadata/json" "oras.land/oras/cmd/oras/internal/display/metadata/table" "oras.land/oras/cmd/oras/internal/display/metadata/template" "oras.land/oras/cmd/oras/internal/display/metadata/text" "oras.land/oras/cmd/oras/internal/display/metadata/tree" "oras.land/oras/cmd/oras/internal/display/status" "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/cmd/oras/internal/output" ) // NewPushHandler returns status and metadata handlers for push command. func NewPushHandler(printer *output.Printer, format option.Format, tty *os.File, fetcher fetcher.Fetcher) (status.PushHandler, metadata.PushHandler, error) { var statusHandler status.PushHandler if tty != nil { statusHandler = status.NewTTYPushHandler(tty, fetcher) } else if format.Type == option.FormatTypeText.Name { statusHandler = status.NewTextPushHandler(printer, fetcher) } else { statusHandler = status.NewDiscardHandler() } var metadataHandler metadata.PushHandler switch format.Type { case option.FormatTypeText.Name: metadataHandler = text.NewPushHandler(printer) case option.FormatTypeJSON.Name: metadataHandler = json.NewPushHandler(printer) case option.FormatTypeGoTemplate.Name: metadataHandler = template.NewPushHandler(printer, format.Template) default: return nil, nil, errors.UnsupportedFormatTypeError(format.Type) } return statusHandler, metadataHandler, nil } // NewAttachHandler returns status and metadata handlers for attach command. func NewAttachHandler(printer *output.Printer, format option.Format, tty *os.File, fetcher fetcher.Fetcher) (status.AttachHandler, metadata.AttachHandler, error) { var statusHandler status.AttachHandler if tty != nil { statusHandler = status.NewTTYAttachHandler(tty, fetcher) } else if format.Type == option.FormatTypeText.Name { statusHandler = status.NewTextAttachHandler(printer, fetcher) } else { statusHandler = status.NewDiscardHandler() } var metadataHandler metadata.AttachHandler switch format.Type { case option.FormatTypeText.Name: metadataHandler = text.NewAttachHandler(printer) case option.FormatTypeJSON.Name: metadataHandler = json.NewAttachHandler(printer) case option.FormatTypeGoTemplate.Name: metadataHandler = template.NewAttachHandler(printer, format.Template) default: return nil, nil, errors.UnsupportedFormatTypeError(format.Type) } return statusHandler, metadataHandler, nil } // NewPullHandler returns status and metadata handlers for pull command. func NewPullHandler(printer *output.Printer, format option.Format, path string, tty *os.File) (status.PullHandler, metadata.PullHandler, error) { var statusHandler status.PullHandler if tty != nil { statusHandler = status.NewTTYPullHandler(tty) } else if format.Type == option.FormatTypeText.Name { statusHandler = status.NewTextPullHandler(printer) } else { statusHandler = status.NewDiscardHandler() } var metadataHandler metadata.PullHandler switch format.Type { case option.FormatTypeText.Name: metadataHandler = text.NewPullHandler(printer) case option.FormatTypeJSON.Name: metadataHandler = json.NewPullHandler(printer, path) case option.FormatTypeGoTemplate.Name: metadataHandler = template.NewPullHandler(printer, path, format.Template) default: return nil, nil, errors.UnsupportedFormatTypeError(format.Type) } return statusHandler, metadataHandler, nil } // NewDiscoverHandler returns status and metadata handlers for discover command. func NewDiscoverHandler(out io.Writer, format option.Format, path string, rawReference string, desc ocispec.Descriptor, verbose bool, tty *os.File) (metadata.DiscoverHandler, error) { var handler metadata.DiscoverHandler switch format.Type { case option.FormatTypeTree.Name: handler = tree.NewDiscoverHandler(out, path, desc, verbose, tty) case option.FormatTypeTable.Name: handler = table.NewDiscoverHandler(out, rawReference, desc, verbose) case option.FormatTypeJSON.Name: handler = json.NewDiscoverHandler(out, desc, path) case option.FormatTypeGoTemplate.Name: handler = template.NewDiscoverHandler(out, desc, path, format.Template) default: return nil, errors.UnsupportedFormatTypeError(format.Type) } return handler, nil } // NewManifestFetchHandler returns a manifest fetch handler. func NewManifestFetchHandler(out io.Writer, format option.Format, outputDescriptor, pretty bool, outputPath string) (metadata.ManifestFetchHandler, content.ManifestFetchHandler, error) { var metadataHandler metadata.ManifestFetchHandler var contentHandler content.ManifestFetchHandler switch format.Type { case option.FormatTypeText.Name: // raw if outputDescriptor { metadataHandler = descriptor.NewManifestFetchHandler(out, pretty) } else { metadataHandler = metadata.NewDiscardHandler() } case option.FormatTypeJSON.Name: // json metadataHandler = json.NewManifestFetchHandler(out) if outputPath == "" { contentHandler = content.NewDiscardHandler() } case option.FormatTypeGoTemplate.Name: // go template metadataHandler = template.NewManifestFetchHandler(out, format.Template) if outputPath == "" { contentHandler = content.NewDiscardHandler() } default: return nil, nil, errors.UnsupportedFormatTypeError(format.Type) } if contentHandler == nil { contentHandler = content.NewManifestFetchHandler(out, pretty, outputPath) } return metadataHandler, contentHandler, nil } // NewTagHandler returns a tag handler. func NewTagHandler(printer *output.Printer, target option.Target) metadata.TagHandler { return text.NewTagHandler(printer, target) } // NewManifestPushHandler returns a manifest push handler. func NewManifestPushHandler(printer *output.Printer, outputDescriptor bool, _ bool, desc ocispec.Descriptor, target *option.Target) (status.ManifestPushHandler, metadata.ManifestPushHandler) { if outputDescriptor { return status.NewDiscardHandler(), metadata.NewDiscardHandler() } return status.NewTextManifestPushHandler(printer, desc), text.NewManifestPushHandler(printer, target) } // NewManifestDeleteHandler returns a manifest delete handler. func NewManifestDeleteHandler(printer *output.Printer, target *option.Target) metadata.ManifestDeleteHandler { return text.NewManifestDeleteHandler(printer, target) } // NewManifestIndexCreateHandler returns status, metadata and content handlers for index create command. func NewManifestIndexCreateHandler(outputPath string, printer *output.Printer, pretty bool) (status.ManifestIndexCreateHandler, metadata.ManifestIndexCreateHandler, content.ManifestIndexCreateHandler) { var statusHandler status.ManifestIndexCreateHandler var metadataHandler metadata.ManifestIndexCreateHandler var contentHandler content.ManifestIndexCreateHandler switch outputPath { case "": statusHandler = status.NewTextManifestIndexCreateHandler(printer) metadataHandler = text.NewManifestIndexCreateHandler(printer) contentHandler = content.NewDiscardHandler() case "-": statusHandler = status.NewDiscardHandler() metadataHandler = metadata.NewDiscardHandler() contentHandler = content.NewManifestIndexCreateHandler(printer, pretty, outputPath) default: statusHandler = status.NewTextManifestIndexCreateHandler(printer) metadataHandler = text.NewManifestIndexCreateHandler(printer) contentHandler = content.NewManifestIndexCreateHandler(printer, pretty, outputPath) } return statusHandler, metadataHandler, contentHandler } // NewManifestIndexUpdateHandler returns status, metadata and content handlers for index update command. func NewManifestIndexUpdateHandler(outputPath string, printer *output.Printer, pretty bool) ( status.ManifestIndexUpdateHandler, metadata.ManifestIndexUpdateHandler, content.ManifestIndexUpdateHandler) { statusHandler := status.NewTextManifestIndexUpdateHandler(printer) metadataHandler := text.NewManifestIndexCreateHandler(printer) contentHandler := content.NewManifestIndexCreateHandler(printer, pretty, outputPath) switch outputPath { case "": contentHandler = content.NewDiscardHandler() case "-": statusHandler = status.NewDiscardHandler() metadataHandler = metadata.NewDiscardHandler() } return statusHandler, metadataHandler, contentHandler } // NewCopyHandler returns copy handlers. func NewCopyHandler(printer *output.Printer, tty *os.File, fetcher fetcher.Fetcher) (status.CopyHandler, metadata.CopyHandler) { if tty != nil { return status.NewTTYCopyHandler(tty), text.NewCopyHandler(printer) } return status.NewTextCopyHandler(printer, fetcher), text.NewCopyHandler(printer) } // NewBackupHandler returns backup handlers. func NewBackupHandler(printer *output.Printer, tty *os.File, repo string, fetcher fetcher.Fetcher) (status.BackupHandler, metadata.BackupHandler) { if tty != nil { return status.NewTTYBackupHandler(tty, fetcher), text.NewBackupHandler(repo, printer) } return status.NewTextBackupHandler(printer, fetcher), text.NewBackupHandler(repo, printer) } // NewRestoreHandler returns restore handlers. func NewRestoreHandler(printer *output.Printer, tty *os.File, fetcher fetcher.Fetcher, dryRun bool) (status.RestoreHandler, metadata.RestoreHandler) { if tty != nil { return status.NewTTYRestoreHandler(tty, fetcher), text.NewRestoreHandler(printer, dryRun) } return status.NewTextRestoreHandler(printer, fetcher), text.NewRestoreHandler(printer, dryRun) } // NewBlobPushHandler returns blob push handlers. func NewBlobPushHandler(printer *output.Printer, outputDescriptor bool, _ bool, desc ocispec.Descriptor, tty *os.File) (status.BlobPushHandler, metadata.BlobPushHandler) { if outputDescriptor { return status.NewDiscardHandler(), metadata.NewDiscardHandler() } if tty != nil { return status.NewTTYBlobPushHandler(tty, desc), text.NewBlobPushHandler(printer, desc) } return status.NewTextBlobPushHandler(printer, desc), text.NewBlobPushHandler(printer, desc) } // NewResolveHandler returns a resolve metadata handler. func NewResolveHandler(printer *output.Printer, fullRef bool, path string) metadata.ResolveHandler { return text.NewResolveHandler(printer, fullRef, path) } // NewBlobDeleteHandler returns blob delete handlers. func NewBlobDeleteHandler(printer *output.Printer, target *option.Target) metadata.BlobDeleteHandler { return text.NewBlobDeleteHandler(printer, target) } // NewRepoTagsHandler returns a repo tags handler. func NewRepoTagsHandler(out io.Writer, format option.Format) (metadata.RepoTagsHandler, error) { var handler metadata.RepoTagsHandler switch format.Type { case option.FormatTypeText.Name: handler = text.NewRepoTagsHandler(out) case option.FormatTypeJSON.Name: handler = json.NewRepoTagsHandler(out) case option.FormatTypeGoTemplate.Name: handler = template.NewRepoTagsHandler(out, format.Template) default: return nil, errors.UnsupportedFormatTypeError(format.Type) } return handler, nil } // NewRepoListHandler returns a repo ls handler. func NewRepoListHandler(out io.Writer, format option.Format, registry, namespace string) (metadata.RepoListHandler, error) { var handler metadata.RepoListHandler switch format.Type { case option.FormatTypeText.Name: handler = text.NewRepoListHandler(out, namespace) case option.FormatTypeJSON.Name: handler = json.NewRepoListHandler(out, registry) case option.FormatTypeGoTemplate.Name: handler = template.NewRepoListHandler(out, format.Template, registry) default: return nil, errors.UnsupportedFormatTypeError(format.Type) } return handler, nil } oras-1.3.3/cmd/oras/internal/display/handler_test.go000066400000000000000000000146521522423464100224700ustar00rootroot00000000000000/* Copyright The ORAS 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 display import ( "os" "reflect" "testing" "oras.land/oras/internal/testutils" "oras.land/oras/cmd/oras/internal/display/metadata/text" "oras.land/oras/cmd/oras/internal/display/status" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/cmd/oras/internal/output" ) func TestNewPushHandler(t *testing.T) { mockFetcher := testutils.NewMockFetcher() printer := output.NewPrinter(os.Stdout, os.Stderr) _, _, err := NewPushHandler(printer, option.Format{Type: option.FormatTypeText.Name}, os.Stdout, mockFetcher.Fetcher) if err != nil { t.Errorf("NewPushHandler() error = %v, want nil", err) } } func TestNewAttachHandler(t *testing.T) { mockFetcher := testutils.NewMockFetcher() printer := output.NewPrinter(os.Stdout, os.Stderr) _, _, err := NewAttachHandler(printer, option.Format{Type: option.FormatTypeText.Name}, os.Stdout, mockFetcher.Fetcher) if err != nil { t.Errorf("NewAttachHandler() error = %v, want nil", err) } } func TestNewPullHandler(t *testing.T) { printer := output.NewPrinter(os.Stdout, os.Stderr) _, _, err := NewPullHandler(printer, option.Format{Type: option.FormatTypeText.Name}, "", os.Stdout) if err != nil { t.Errorf("NewPullHandler() error = %v, want nil", err) } } func TestNewCopyHandler(t *testing.T) { printer := output.NewPrinter(os.Stdout, os.Stderr) copyHandler, copyMetadataHandler := NewCopyHandler(printer, os.Stdout, nil) if _, ok := copyHandler.(*status.TTYCopyHandler); !ok { t.Errorf("expected *status.TTYCopyHandler actual %v", reflect.TypeOf(copyHandler)) } if _, ok := copyMetadataHandler.(*text.CopyHandler); !ok { t.Errorf("expected metadata.CopyHandler actual %v", reflect.TypeOf(copyMetadataHandler)) } copyHandler, copyMetadataHandler = NewCopyHandler(printer, nil, nil) if _, ok := copyHandler.(*status.TextCopyHandler); !ok { t.Errorf("expected *status.TextCopyHandler actual %v", reflect.TypeOf(copyHandler)) } if _, ok := copyMetadataHandler.(*text.CopyHandler); !ok { t.Errorf("expected metadata.CopyHandler actual %v", reflect.TypeOf(copyMetadataHandler)) } } func TestNewRepoTagsHandler(t *testing.T) { tests := []struct { name string format option.Format expectError bool }{ {"text format", option.Format{Type: option.FormatTypeText.Name}, false}, {"JSON format", option.Format{Type: option.FormatTypeJSON.Name}, false}, {"Go template", option.Format{Type: option.FormatTypeGoTemplate.Name, Template: "{{.tags}}"}, false}, {"unsupported", option.Format{Type: "unsupported"}, true}, } // Test with stdout for _, tt := range tests { t.Run(tt.name+" with stdout", func(t *testing.T) { handler, err := NewRepoTagsHandler(os.Stdout, tt.format) if tt.expectError && err == nil { t.Error("expected error, got nil") } if !tt.expectError { if err != nil { t.Errorf("error = %v, want nil", err) } if handler == nil { t.Error("returned nil handler") } } }) } } func TestNewRepoListHandler(t *testing.T) { tests := []struct { name string format option.Format expectError bool }{ {"text format", option.Format{Type: option.FormatTypeText.Name}, false}, {"JSON format", option.Format{Type: option.FormatTypeJSON.Name}, false}, {"Go template", option.Format{Type: option.FormatTypeGoTemplate.Name, Template: "{{.repositories}}"}, false}, {"unsupported", option.Format{Type: "unsupported"}, true}, } // Test with stdout for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { registry := "example.com" namespace := "foo/bar" handler, err := NewRepoListHandler(os.Stdout, tt.format, registry, namespace) if tt.expectError && err == nil { t.Error("expected error, got nil") } if !tt.expectError { if err != nil { t.Errorf("error = %v, want nil", err) } if handler == nil { t.Error("returned nil handler") } } }) } } func TestNewBackupHandler(t *testing.T) { printer := output.NewPrinter(os.Stdout, os.Stderr) repo := "test/repo" mockFetcher := testutils.NewMockFetcher() t.Run("with TTY", func(t *testing.T) { statusHandler, metadataHandler := NewBackupHandler(printer, os.Stdout, repo, mockFetcher.Fetcher) if _, ok := statusHandler.(*status.TTYBackupHandler); !ok { t.Errorf("expected *status.TTYBackupHandler actual %v", reflect.TypeOf(statusHandler)) } if _, ok := metadataHandler.(*text.BackupHandler); !ok { t.Errorf("expected *text.BackupHandler actual %v", reflect.TypeOf(metadataHandler)) } }) t.Run("without TTY", func(t *testing.T) { statusHandler, metadataHandler := NewBackupHandler(printer, nil, repo, mockFetcher.Fetcher) if _, ok := statusHandler.(*status.TextBackupHandler); !ok { t.Errorf("expected *status.TextBackupHandler actual %v", reflect.TypeOf(statusHandler)) } if _, ok := metadataHandler.(*text.BackupHandler); !ok { t.Errorf("expected *text.BackupHandler actual %v", reflect.TypeOf(metadataHandler)) } }) } func TestNewRestoreHandler(t *testing.T) { printer := output.NewPrinter(os.Stdout, os.Stderr) mockFetcher := testutils.NewMockFetcher() t.Run("with TTY", func(t *testing.T) { statusHandler, metadataHandler := NewRestoreHandler(printer, os.Stdout, mockFetcher.Fetcher, false) if _, ok := statusHandler.(*status.TTYRestoreHandler); !ok { t.Errorf("expected *status.TTYRestoreHandler actual %v", reflect.TypeOf(statusHandler)) } if _, ok := metadataHandler.(*text.RestoreHandler); !ok { t.Errorf("expected *text.RestoreHandler actual %v", reflect.TypeOf(metadataHandler)) } }) t.Run("without TTY", func(t *testing.T) { statusHandler, metadataHandler := NewRestoreHandler(printer, nil, mockFetcher.Fetcher, false) if _, ok := statusHandler.(*status.TextRestoreHandler); !ok { t.Errorf("expected *status.TextRestoreHandler actual %v", reflect.TypeOf(statusHandler)) } if _, ok := metadataHandler.(*text.RestoreHandler); !ok { t.Errorf("expected *text.RestoreHandler actual %v", reflect.TypeOf(metadataHandler)) } }) } oras-1.3.3/cmd/oras/internal/display/metadata/000077500000000000000000000000001522423464100212355ustar00rootroot00000000000000oras-1.3.3/cmd/oras/internal/display/metadata/descriptor/000077500000000000000000000000001522423464100234135ustar00rootroot00000000000000oras-1.3.3/cmd/oras/internal/display/metadata/descriptor/manifest_fetch.go000066400000000000000000000026101522423464100267200ustar00rootroot00000000000000/* Copyright The ORAS 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 descriptor import ( "encoding/json" "fmt" "io" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/output" ) // manifestFetchHandler handles metadata descriptor output. type manifestFetchHandler struct { pretty bool out io.Writer } // OnFetched implements ManifestFetchHandler. func (h *manifestFetchHandler) OnFetched(_ string, desc ocispec.Descriptor, _ []byte) error { descBytes, err := json.Marshal(desc) if err != nil { return fmt.Errorf("invalid descriptor: %w", err) } return output.PrintJSON(h.out, descBytes, h.pretty) } // NewManifestFetchHandler creates a new handler. func NewManifestFetchHandler(out io.Writer, pretty bool) metadata.ManifestFetchHandler { return &manifestFetchHandler{ pretty: pretty, out: out, } } oras-1.3.3/cmd/oras/internal/display/metadata/discard.go000066400000000000000000000030031522423464100231710ustar00rootroot00000000000000/* Copyright The ORAS 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 metadata import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/option" ) type Discard struct{} // NewDiscardHandler creates a new handler that discards output for all events. func NewDiscardHandler() Discard { return Discard{} } // OnFetched implements ManifestFetchHandler. func (Discard) OnFetched(string, ocispec.Descriptor, []byte) error { return nil } // OnManifestPushed implements ManifestPushHandler. func (Discard) OnManifestPushed(ocispec.Descriptor) error { return nil } // Render implements ManifestPushHandler. func (Discard) Render() error { return nil } // OnTagged implements ManifestIndexCreateHandler. func (Discard) OnTagged(ocispec.Descriptor, string) error { return nil } // OnIndexCreated implements ManifestIndexCreateHandler. func (Discard) OnIndexCreated(ocispec.Descriptor) {} // OnBlobPushed implements BlobPushHandler func (Discard) OnBlobPushed(*option.Target) error { return nil } oras-1.3.3/cmd/oras/internal/display/metadata/discard_test.go000066400000000000000000000021531522423464100242350ustar00rootroot00000000000000/* Copyright The ORAS 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 metadata import ( "testing" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) func TestDiscard_OnTagged(t *testing.T) { testDiscard := NewDiscardHandler() if err := testDiscard.OnTagged(ocispec.Descriptor{}, "test"); err != nil { t.Errorf("testDiscard.OnTagged() error = %v, want nil", err) } } func TestDiscardHandler_OnManifestPushed(t *testing.T) { testDiscard := NewDiscardHandler() if err := testDiscard.OnManifestPushed(ocispec.Descriptor{}); err != nil { t.Errorf("DiscardHandler.OnManifestPushed() error = %v, wantErr nil", err) } } oras-1.3.3/cmd/oras/internal/display/metadata/interface.go000066400000000000000000000114761522423464100235350ustar00rootroot00000000000000/* Copyright The ORAS 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 metadata import ( "time" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/option" ) // Renderer renders metadata information when an operation is complete. type Renderer interface { Render() error } // PushHandler handles metadata output for push events. type PushHandler interface { TaggedHandler Renderer OnCopied(opts *option.Target, root ocispec.Descriptor) error } // AttachHandler handles metadata output for attach events. type AttachHandler interface { Renderer OnAttached(target *option.Target, root ocispec.Descriptor, subject ocispec.Descriptor) } // DiscoverHandler handles metadata output for discover events. type DiscoverHandler interface { Renderer // OnDiscovered is called after a referrer is discovered. OnDiscovered(referrer, subject ocispec.Descriptor) error } // ManifestFetchHandler handles metadata output for manifest fetch events. type ManifestFetchHandler interface { // OnFetched is called after the manifest content is fetched. OnFetched(path string, desc ocispec.Descriptor, content []byte) error } // PullHandler handles metadata output for pull events. type PullHandler interface { Renderer // OnLayerSkipped is called when a layer is skipped. OnLayerSkipped(ocispec.Descriptor) error // OnFilePulled is called after a file is pulled. OnFilePulled(name string, outputDir string, desc ocispec.Descriptor, descPath string) error // OnPulled is called when a pull operation completes. OnPulled(target *option.Target, desc ocispec.Descriptor) } // TaggedHandler handles status output for tag command. type TaggedHandler interface { // OnTagged is called when each tagging operation is done. OnTagged(desc ocispec.Descriptor, tag string) error } // TagHandler handles status output for tag command. type TagHandler interface { // OnTagging is called when tagging starts. OnTagging(desc ocispec.Descriptor, tag string) error TaggedHandler } // ManifestPushHandler handles metadata output for manifest push events. type ManifestPushHandler interface { TaggedHandler Renderer OnManifestPushed(desc ocispec.Descriptor) error } // ManifestIndexCreateHandler handles metadata output for index create events. type ManifestIndexCreateHandler interface { TaggedHandler Renderer OnIndexCreated(desc ocispec.Descriptor) } // ManifestIndexUpdateHandler handles metadata output for index update events. type ManifestIndexUpdateHandler ManifestIndexCreateHandler // CopyHandler handles metadata output for cp events. type CopyHandler interface { TaggedHandler Renderer OnCopied(target *option.BinaryTarget, desc ocispec.Descriptor) error } // BackupHandler handles metadata output for backup events. type BackupHandler interface { Renderer OnTagsFound(tags []string) error OnArtifactPulled(tag string, referrerCount int) error OnTarExporting(path string) error OnTarExported(path string, size int64) error OnBackupCompleted(tagsCount int, path string, duration time.Duration) error } // RestoreHandler handles metadata output for restore events. type RestoreHandler interface { Renderer OnTarLoaded(path string, size int64) error OnTagsFound(tags []string) error OnArtifactPushed(tag string, referrerCount int) error OnRestoreCompleted(tagsCount int, repo string, duration time.Duration) error } // BlobPushHandler handles metadata output for blob push events. type BlobPushHandler interface { Renderer OnBlobPushed(target *option.Target) error } // ResolveHandler handles metadata output for resolve events. type ResolveHandler interface { OnResolved(desc ocispec.Descriptor) error } // ManifestDeleteHandler handles metadata output for manifest delete events. type ManifestDeleteHandler interface { OnManifestMissing() error OnManifestDeleted() error } // BlobDeleteHandler handles metadata output for blob delete events. type BlobDeleteHandler interface { OnBlobMissing() error OnBlobDeleted() error } // RepoTagsHandler handles metadata output for repo tags command. type RepoTagsHandler interface { Renderer // OnTagListed is called for each tag that is listed. OnTagListed(tag string) error } // RepoListHandler handles metadata output for repo ls command. type RepoListHandler interface { Renderer // OnRepositoryListed is called for each repository that is listed. OnRepositoryListed(repo string) error } oras-1.3.3/cmd/oras/internal/display/metadata/json/000077500000000000000000000000001522423464100222065ustar00rootroot00000000000000oras-1.3.3/cmd/oras/internal/display/metadata/json/attach.go000066400000000000000000000027671522423464100240150ustar00rootroot00000000000000/* Copyright The ORAS 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 json import ( "io" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/display/metadata/model" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/cmd/oras/internal/output" ) // AttachHandler handles json metadata output for attach events. type AttachHandler struct { out io.Writer path string root ocispec.Descriptor } // NewAttachHandler creates a new handler for attach events. func NewAttachHandler(out io.Writer) metadata.AttachHandler { return &AttachHandler{ out: out, } } // OnAttached implements AttachHandler. func (ah *AttachHandler) OnAttached(target *option.Target, root ocispec.Descriptor, _ ocispec.Descriptor) { ah.path = target.Path ah.root = root } // Render is called when the attach command is completed. func (ah *AttachHandler) Render() error { return output.PrintPrettyJSON(ah.out, model.NewAttach(ah.root, ah.path)) } oras-1.3.3/cmd/oras/internal/display/metadata/json/discover.go000066400000000000000000000030361522423464100243550ustar00rootroot00000000000000/* Copyright The ORAS 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 json import ( "io" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/display/metadata/model" "oras.land/oras/cmd/oras/internal/output" ) // discoverHandler handles json metadata output for discover events. type discoverHandler struct { out io.Writer path string model model.Discover } // NewDiscoverHandler creates a new handler for discover events. func NewDiscoverHandler(out io.Writer, subject ocispec.Descriptor, path string) metadata.DiscoverHandler { return &discoverHandler{ out: out, path: path, model: model.NewDiscover(path, subject), } } // OnDiscovered implements metadata.DiscoverHandler. func (h *discoverHandler) OnDiscovered(referrer, subject ocispec.Descriptor) error { return h.model.AddReferrer(referrer, subject) } // Render implements metadata.DiscoverHandler. func (h *discoverHandler) Render() error { return output.PrintPrettyJSON(h.out, h.model.Root) } oras-1.3.3/cmd/oras/internal/display/metadata/json/manifest_fetch.go000066400000000000000000000027441522423464100255230ustar00rootroot00000000000000/* Copyright The ORAS 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 json import ( "encoding/json" "io" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/display/metadata/model" "oras.land/oras/cmd/oras/internal/output" ) // manifestFetchHandler handles JSON metadata output for manifest fetch events. type manifestFetchHandler struct { out io.Writer } // NewManifestFetchHandler creates a new handler for manifest fetch events. func NewManifestFetchHandler(out io.Writer) metadata.ManifestFetchHandler { return &manifestFetchHandler{ out: out, } } // OnFetched is called after the manifest fetch is completed. func (h *manifestFetchHandler) OnFetched(path string, desc ocispec.Descriptor, content []byte) error { var manifest map[string]any if err := json.Unmarshal(content, &manifest); err != nil { manifest = nil } return output.PrintPrettyJSON(h.out, model.NewFetched(path, desc, manifest)) } oras-1.3.3/cmd/oras/internal/display/metadata/json/pull.go000066400000000000000000000035441522423464100235170ustar00rootroot00000000000000/* Copyright The ORAS 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 json import ( "io" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/display/metadata/model" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/cmd/oras/internal/output" ) // PullHandler handles JSON metadata output for pull events. type PullHandler struct { path string pulled model.Pulled out io.Writer root ocispec.Descriptor } // NewPullHandler returns a new handler for Pull events. func NewPullHandler(out io.Writer, path string) metadata.PullHandler { return &PullHandler{ out: out, path: path, } } // OnLayerSkipped implements metadata.PullHandler. func (ph *PullHandler) OnLayerSkipped(ocispec.Descriptor) error { return nil } // OnFilePulled implements metadata.PullHandler. func (ph *PullHandler) OnFilePulled(name string, outputDir string, desc ocispec.Descriptor, descPath string) error { return ph.pulled.Add(name, outputDir, desc, descPath) } // OnPulled implements metadata.PullHandler. func (ph *PullHandler) OnPulled(_ *option.Target, desc ocispec.Descriptor) { ph.root = desc } // Render implements metadata.PullHandler. func (ph *PullHandler) Render() error { return output.PrintPrettyJSON(ph.out, model.NewPull(ph.path+"@"+ph.root.Digest.String(), ph.pulled.Files())) } oras-1.3.3/cmd/oras/internal/display/metadata/json/pull_test.go000066400000000000000000000061201522423464100245470ustar00rootroot00000000000000/* Copyright The ORAS 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 json import ( "bytes" "encoding/json" "testing" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/option" ) func TestNewPullHandler(t *testing.T) { buf := &bytes.Buffer{} handler := NewPullHandler(buf, "localhost:5000/test") if handler == nil { t.Fatal("NewPullHandler() returned nil") } } func TestPullHandler_OnLayerSkipped(t *testing.T) { buf := &bytes.Buffer{} handler := NewPullHandler(buf, "localhost:5000/test").(*PullHandler) desc := ocispec.Descriptor{ MediaType: "application/vnd.oci.image.layer.v1.tar+gzip", Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", Size: 1024, } err := handler.OnLayerSkipped(desc) if err != nil { t.Errorf("PullHandler.OnLayerSkipped() error = %v, want nil", err) } } func TestPullHandler_OnFilePulled(t *testing.T) { buf := &bytes.Buffer{} handler := NewPullHandler(buf, "localhost:5000/test").(*PullHandler) desc := ocispec.Descriptor{ MediaType: "application/vnd.oci.image.layer.v1.tar+gzip", Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", Size: 1024, } err := handler.OnFilePulled("test.txt", "/output", desc, "blobs/sha256/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") if err != nil { t.Errorf("PullHandler.OnFilePulled() error = %v, want nil", err) } } func TestPullHandler_OnPulled(t *testing.T) { buf := &bytes.Buffer{} handler := NewPullHandler(buf, "localhost:5000/test").(*PullHandler) desc := ocispec.Descriptor{ MediaType: "application/vnd.oci.image.manifest.v1+json", Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", Size: 100, } handler.OnPulled(&option.Target{}, desc) if handler.root.Digest != desc.Digest { t.Errorf("PullHandler.OnPulled() did not set root descriptor correctly") } } func TestPullHandler_Render(t *testing.T) { buf := &bytes.Buffer{} handler := NewPullHandler(buf, "localhost:5000/test").(*PullHandler) desc := ocispec.Descriptor{ MediaType: "application/vnd.oci.image.manifest.v1+json", Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", Size: 100, } handler.OnPulled(&option.Target{}, desc) err := handler.Render() if err != nil { t.Errorf("PullHandler.Render() error = %v, want nil", err) } // Verify JSON output is valid var result map[string]interface{} if err := json.Unmarshal(buf.Bytes(), &result); err != nil { t.Errorf("PullHandler.Render() produced invalid JSON: %v", err) } } oras-1.3.3/cmd/oras/internal/display/metadata/json/push.go000066400000000000000000000034451522423464100235220ustar00rootroot00000000000000/* Copyright The ORAS 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 json import ( "io" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/display/metadata/model" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/cmd/oras/internal/output" "oras.land/oras/internal/contentutil" ) // PushHandler handles JSON metadata output for push events. type PushHandler struct { path string out io.Writer tagged model.Tagged root ocispec.Descriptor } // NewPushHandler creates a new handler for push events. func NewPushHandler(out io.Writer) metadata.PushHandler { return &PushHandler{ out: out, } } // OnTagged implements metadata.TaggedHandler. func (ph *PushHandler) OnTagged(_ ocispec.Descriptor, tag string) error { ph.tagged.AddTag(tag) return nil } // OnCopied is called after files are copied. func (ph *PushHandler) OnCopied(opts *option.Target, root ocispec.Descriptor) error { if opts.RawReference != "" && !contentutil.IsDigest(opts.Reference) { ph.tagged.AddTag(opts.Reference) } ph.path = opts.Path ph.root = root return nil } // Render implements PushHandler. func (ph *PushHandler) Render() error { return output.PrintPrettyJSON(ph.out, model.NewPush(ph.root, ph.path, ph.tagged.Tags())) } oras-1.3.3/cmd/oras/internal/display/metadata/json/push_test.go000066400000000000000000000066561522423464100245700ustar00rootroot00000000000000/* Copyright The ORAS 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 json import ( "bytes" "encoding/json" "testing" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/option" ) func TestNewPushHandler(t *testing.T) { buf := &bytes.Buffer{} handler := NewPushHandler(buf) if handler == nil { t.Fatal("NewPushHandler() returned nil") } } func TestPushHandler_OnTagged(t *testing.T) { buf := &bytes.Buffer{} handler := NewPushHandler(buf).(*PushHandler) desc := ocispec.Descriptor{ MediaType: "application/vnd.oci.image.manifest.v1+json", Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", Size: 100, } err := handler.OnTagged(desc, "v1.0.0") if err != nil { t.Errorf("PushHandler.OnTagged() error = %v, want nil", err) } } func TestPushHandler_OnCopied(t *testing.T) { buf := &bytes.Buffer{} handler := NewPushHandler(buf).(*PushHandler) desc := ocispec.Descriptor{ MediaType: "application/vnd.oci.image.manifest.v1+json", Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", Size: 100, } opts := &option.Target{ RawReference: "localhost:5000/test:v1.0.0", Path: "localhost:5000/test", Reference: "v1.0.0", } err := handler.OnCopied(opts, desc) if err != nil { t.Errorf("PushHandler.OnCopied() error = %v, want nil", err) } } func TestPushHandler_OnCopied_WithDigest(t *testing.T) { buf := &bytes.Buffer{} handler := NewPushHandler(buf).(*PushHandler) desc := ocispec.Descriptor{ MediaType: "application/vnd.oci.image.manifest.v1+json", Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", Size: 100, } opts := &option.Target{ RawReference: "localhost:5000/test@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", Path: "localhost:5000/test", Reference: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", } err := handler.OnCopied(opts, desc) if err != nil { t.Errorf("PushHandler.OnCopied() error = %v, want nil", err) } } func TestPushHandler_Render(t *testing.T) { buf := &bytes.Buffer{} handler := NewPushHandler(buf).(*PushHandler) desc := ocispec.Descriptor{ MediaType: "application/vnd.oci.image.manifest.v1+json", Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", Size: 100, } opts := &option.Target{ RawReference: "localhost:5000/test:v1.0.0", Path: "localhost:5000/test", Reference: "v1.0.0", } if err := handler.OnCopied(opts, desc); err != nil { t.Fatalf("PushHandler.OnCopied() error = %v", err) } err := handler.Render() if err != nil { t.Errorf("PushHandler.Render() error = %v, want nil", err) } // Verify JSON output is valid var result map[string]interface{} if err := json.Unmarshal(buf.Bytes(), &result); err != nil { t.Errorf("PushHandler.Render() produced invalid JSON: %v", err) } } oras-1.3.3/cmd/oras/internal/display/metadata/json/repo_list.go000066400000000000000000000027131522423464100245400ustar00rootroot00000000000000/* Copyright The ORAS 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 json import ( "io" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/display/metadata/model" "oras.land/oras/cmd/oras/internal/output" ) // repoListHandler handles JSON metadata output for repo ls command. type repoListHandler struct { out io.Writer model *model.Repositories } // NewRepoListHandler creates a new handler for repo ls events. func NewRepoListHandler(out io.Writer, registry string) metadata.RepoListHandler { return &repoListHandler{ out: out, model: model.NewRepositories(registry), } } // OnRepositoryListed implements metadata.RepoListHandler. func (h *repoListHandler) OnRepositoryListed(repo string) error { // For JSON format, show the full repository name h.model.AddRepository(repo) return nil } // Render implements metadata.RepoListHandler. func (h *repoListHandler) Render() error { return output.PrintPrettyJSON(h.out, h.model) } oras-1.3.3/cmd/oras/internal/display/metadata/json/repo_tags.go000066400000000000000000000025241522423464100245230ustar00rootroot00000000000000/* Copyright The ORAS 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 json import ( "io" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/display/metadata/model" "oras.land/oras/cmd/oras/internal/output" ) // repoTagsHandler handles JSON metadata output for repo tags command. type repoTagsHandler struct { out io.Writer model *model.Tags } // NewRepoTagsHandler creates a new handler for repo tags events. func NewRepoTagsHandler(out io.Writer) metadata.RepoTagsHandler { return &repoTagsHandler{ out: out, model: model.NewTags(), } } // OnTagListed implements metadata.TagsHandler. func (h *repoTagsHandler) OnTagListed(tag string) error { h.model.AddTag(tag) return nil } // Render implements metadata.TagsHandler. func (h *repoTagsHandler) Render() error { return output.PrintPrettyJSON(h.out, h.model) } oras-1.3.3/cmd/oras/internal/display/metadata/model/000077500000000000000000000000001522423464100223355ustar00rootroot00000000000000oras-1.3.3/cmd/oras/internal/display/metadata/model/attach.go000066400000000000000000000015721522423464100241350ustar00rootroot00000000000000/* Copyright The ORAS 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 model import ocispec "github.com/opencontainers/image-spec/specs-go/v1" // attach contains metadata formatted by oras attach. type attach struct { Descriptor } // NewAttach returns a metadata getter for attach command. func NewAttach(desc ocispec.Descriptor, path string) any { return attach{FromDescriptor(path, desc)} } oras-1.3.3/cmd/oras/internal/display/metadata/model/descriptor.go000066400000000000000000000031641522423464100250460ustar00rootroot00000000000000/* Copyright The ORAS 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 model import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) // DigestReference is a reference to an artifact with digest. type DigestReference struct { Reference string `json:"reference"` } // NewDigestReference creates a new digest reference. func NewDigestReference(name string, digest string) DigestReference { return DigestReference{ Reference: name + "@" + digest, } } // Descriptor is a descriptor with digest reference. // We cannot use ocispec.Descriptor here since the first letter of the json // annotation key is not uppercase. type Descriptor struct { DigestReference ocispec.Descriptor } // FromDescriptor converts a OCI descriptor to a descriptor with digest reference. func FromDescriptor(name string, desc ocispec.Descriptor) Descriptor { ret := Descriptor{ DigestReference: NewDigestReference(name, desc.Digest.String()), Descriptor: ocispec.Descriptor{ MediaType: desc.MediaType, Size: desc.Size, Digest: desc.Digest, Annotations: desc.Annotations, ArtifactType: desc.ArtifactType, }, } return ret } oras-1.3.3/cmd/oras/internal/display/metadata/model/discover.go000066400000000000000000000033521522423464100245050ustar00rootroot00000000000000/* Copyright The ORAS 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 model import ( "fmt" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) // Discover is a model for discovered referrers. type Discover struct { name string nodes map[digest.Digest]*Node Root *Node } // Node represents a node in the discovered reference tree. type Node struct { Descriptor Referrers []*Node `json:"referrers"` } // AddReferrer adds a node to the discovered referrers tree. func (d *Discover) AddReferrer(referrer, subject ocispec.Descriptor) error { to, ok := d.nodes[subject.Digest] if !ok { return fmt.Errorf("unexpected subject descriptor: %v", subject) } from := NewNode(d.name, referrer) d.nodes[from.Digest] = from to.Referrers = append(to.Referrers, from) return nil } // NewDiscover creates a new discover model. func NewDiscover(path string, root ocispec.Descriptor) Discover { treeRoot := NewNode(path, root) return Discover{ name: path, nodes: map[digest.Digest]*Node{ root.Digest: treeRoot, }, Root: treeRoot, } } // NewNode creates a new node. func NewNode(name string, desc ocispec.Descriptor) *Node { return &Node{ Descriptor: FromDescriptor(name, desc), Referrers: []*Node{}, } } oras-1.3.3/cmd/oras/internal/display/metadata/model/fetched.go000066400000000000000000000016171522423464100242730ustar00rootroot00000000000000/* Copyright The ORAS 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 model import ocispec "github.com/opencontainers/image-spec/specs-go/v1" type fetched struct { Descriptor Content any `json:"content"` } // NewFetched creates a new fetched metadata. func NewFetched(path string, desc ocispec.Descriptor, content any) any { return &fetched{ Descriptor: FromDescriptor(path, desc), Content: content, } } oras-1.3.3/cmd/oras/internal/display/metadata/model/pull.go000066400000000000000000000045221522423464100236430ustar00rootroot00000000000000/* Copyright The ORAS 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 model import ( "fmt" "path/filepath" "slices" "sync" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2/content/file" ) // File records metadata of a pulled file. type File struct { // Path is the absolute path of the pulled file. Path string `json:"path"` Descriptor } // newFile creates a new file metadata. func newFile(name string, outputDir string, desc ocispec.Descriptor, descPath string) (File, error) { path := name if !filepath.IsAbs(name) { var err error path, err = filepath.Abs(filepath.Join(outputDir, name)) // not likely to go wrong since the file has already be written to file store if err != nil { return File{}, fmt.Errorf("failed to get absolute path of pulled file %s: %w", name, err) } } else { path = filepath.Clean(path) } if desc.Annotations[file.AnnotationUnpack] == "true" { path += string(filepath.Separator) } return File{ Path: path, Descriptor: FromDescriptor(descPath, desc), }, nil } type pull struct { DigestReference Files []File `json:"files"` } // NewPull creates a new metadata struct for pull command. func NewPull(digestReference string, files []File) any { return pull{ DigestReference: DigestReference{ Reference: digestReference, }, Files: files, } } // Pulled records all pulled files. type Pulled struct { lock sync.Mutex files []File } // Files returns all pulled files. func (p *Pulled) Files() []File { p.lock.Lock() defer p.lock.Unlock() return slices.Clone(p.files) } // Add adds a pulled file. func (p *Pulled) Add(name string, outputDir string, desc ocispec.Descriptor, descPath string) error { p.lock.Lock() defer p.lock.Unlock() file, err := newFile(name, outputDir, desc, descPath) if err != nil { return err } p.files = append(p.files, file) return nil } oras-1.3.3/cmd/oras/internal/display/metadata/model/push.go000066400000000000000000000021151522423464100236420ustar00rootroot00000000000000/* Copyright The ORAS 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 model import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) // push contains metadata formatted by oras push. type push struct { Descriptor ReferenceAsTags []string `json:"referenceAsTags"` } // NewPush returns a metadata getter for push command. func NewPush(desc ocispec.Descriptor, path string, tags []string) any { var refAsTags []string for _, tag := range tags { refAsTags = append(refAsTags, path+":"+tag) } return push{ Descriptor: FromDescriptor(path, desc), ReferenceAsTags: refAsTags, } } oras-1.3.3/cmd/oras/internal/display/metadata/model/repo_list.go000066400000000000000000000021021522423464100246570ustar00rootroot00000000000000/* Copyright The ORAS 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 model // Repositories contains metadata formatted by oras repo ls. type Repositories struct { Registry string `json:"registry"` Repositories []string `json:"repositories"` } // NewRepositories creates a new Repositories model. func NewRepositories(registry string) *Repositories { return &Repositories{ Registry: registry, Repositories: []string{}, } } // AddRepository adds a repository to the metadata. func (r *Repositories) AddRepository(repo string) { r.Repositories = append(r.Repositories, repo) } oras-1.3.3/cmd/oras/internal/display/metadata/model/repo_tags.go000066400000000000000000000015631522423464100246540ustar00rootroot00000000000000/* Copyright The ORAS 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 model // Tags contains metadata formatted by oras repo tags. type Tags struct { Tags []string `json:"tags"` } // NewTags creates a new Tags model. func NewTags() *Tags { return &Tags{ Tags: []string{}, } } // AddTag adds a tag to the metadata. func (t *Tags) AddTag(tag string) { t.Tags = append(t.Tags, tag) } oras-1.3.3/cmd/oras/internal/display/metadata/model/tag.go000066400000000000000000000017671522423464100234520ustar00rootroot00000000000000/* Copyright The ORAS 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 model import ( "slices" "sync" ) // Tagged contains metadata formatted by oras Tagged. type Tagged struct { tags []string lock sync.RWMutex } // AddTag adds a tag to the metadata. func (tag *Tagged) AddTag(t string) { tag.lock.Lock() defer tag.lock.Unlock() tag.tags = append(tag.tags, t) } // Tags returns the tags. func (tag *Tagged) Tags() []string { tag.lock.RLock() defer tag.lock.RUnlock() slices.Sort(tag.tags) return tag.tags } oras-1.3.3/cmd/oras/internal/display/metadata/table/000077500000000000000000000000001522423464100223245ustar00rootroot00000000000000oras-1.3.3/cmd/oras/internal/display/metadata/table/discover.go000066400000000000000000000057431522423464100245020ustar00rootroot00000000000000/* Copyright The ORAS 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 table import ( "fmt" "io" "strings" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2/content" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/output" ) // discoverHandler handles json metadata output for discover events. type discoverHandler struct { out io.Writer rawReference string root ocispec.Descriptor verbose bool referrers []ocispec.Descriptor } // NewDiscoverHandler creates a new handler for discover events. func NewDiscoverHandler(out io.Writer, rawReference string, root ocispec.Descriptor, verbose bool) metadata.DiscoverHandler { return &discoverHandler{ out: out, rawReference: rawReference, root: root, verbose: verbose, } } // OnDiscovered implements metadata.DiscoverHandler. func (h *discoverHandler) OnDiscovered(referrer, subject ocispec.Descriptor) error { if !content.Equal(subject, h.root) { return fmt.Errorf("unexpected subject descriptor: %v", subject) } h.referrers = append(h.referrers, referrer) return nil } // Render implements metadata.DiscoverHandler. func (h *discoverHandler) Render() (err error) { if n := len(h.referrers); n != 1 { _, err = fmt.Fprintln(h.out, "Discovered", n, "artifacts referencing", h.rawReference) } else { _, err = fmt.Fprintln(h.out, "Discovered", n, "artifact referencing", h.rawReference) } if err != nil { return err } _, err = fmt.Fprintln(h.out, "Digest:", h.root.Digest) if err != nil { return err } if len(h.referrers) == 0 { return nil } _, err = fmt.Fprintln(h.out) if err != nil { return err } return h.printDiscoveredReferrersTable() } func (h *discoverHandler) printDiscoveredReferrersTable() (err error) { typeNameTitle := "Artifact Type" typeNameLength := len(typeNameTitle) for _, ref := range h.referrers { if length := len(ref.ArtifactType); length > typeNameLength { typeNameLength = length } } printKey := func(key string, value any) (err error) { _, err = fmt.Fprintln(h.out, key, strings.Repeat(" ", typeNameLength-len(key)+1), value) return err } err = printKey(typeNameTitle, "Digest") if err != nil { return err } for _, ref := range h.referrers { err = printKey(ref.ArtifactType, ref.Digest) if err != nil { return err } if h.verbose { if err := output.PrintPrettyJSON(h.out, ref); err != nil { return fmt.Errorf("error printing JSON: %w", err) } } } return nil } oras-1.3.3/cmd/oras/internal/display/metadata/table/discover_test.go000066400000000000000000000124221522423464100255310ustar00rootroot00000000000000/* Copyright The ORAS 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 table import ( "bytes" "testing" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/internal/testutils" ) func TestTableDiscoverHandler_OneReferrer(t *testing.T) { buf := new(bytes.Buffer) root := ocispec.Descriptor{Digest: "root"} tdh := NewDiscoverHandler(buf, "rawRef", root, false) one := ocispec.Descriptor{ArtifactType: ocispec.MediaTypeImageLayer, Digest: "one"} if err := tdh.OnDiscovered(one, root); err != nil { t.Errorf("OnDiscovered() unexpected error: %v", err) } if err := tdh.Render(); err != nil { t.Errorf("Render() unexpected error: %v", err) } expected := `Discovered 1 artifact referencing rawRef Digest: root Artifact Type Digest application/vnd.oci.image.layer.v1.tar one ` if buf.String() != expected { t.Errorf("Expected output <%s> actual <%s>", expected, buf.String()) } } func TestTableDiscoverHandler_NoReferrer(t *testing.T) { buf := new(bytes.Buffer) root := ocispec.Descriptor{Digest: "root"} tdh := NewDiscoverHandler(buf, "rawRef", root, false) if err := tdh.Render(); err != nil { t.Errorf("Render() unexpected error: %v", err) } expected := `Discovered 0 artifacts referencing rawRef Digest: root ` if buf.String() != expected { t.Errorf("Expected output <%s> actual <%s>", expected, buf.String()) } } func TestTableDiscoverHandler_MultipleReferrer(t *testing.T) { buf := new(bytes.Buffer) root := ocispec.Descriptor{Digest: "root"} tdh := NewDiscoverHandler(buf, "rawRef", root, false) one := ocispec.Descriptor{ArtifactType: ocispec.MediaTypeImageLayer, Digest: "one"} if err := tdh.OnDiscovered(one, root); err != nil { t.Errorf("OnDiscovered() unexpected error: %v", err) } two := ocispec.Descriptor{ArtifactType: ocispec.MediaTypeImageLayer, Digest: "two"} if err := tdh.OnDiscovered(two, root); err != nil { t.Errorf("OnDiscovered() unexpected error: %v", err) } if err := tdh.Render(); err != nil { t.Errorf("Render() unexpected error: %v", err) } expected := `Discovered 2 artifacts referencing rawRef Digest: root Artifact Type Digest application/vnd.oci.image.layer.v1.tar one application/vnd.oci.image.layer.v1.tar two ` if buf.String() != expected { t.Errorf("Expected output <%s> actual <%s>", expected, buf.String()) } } func TestTableDiscoverHandler_Verbose(t *testing.T) { buf := new(bytes.Buffer) root := ocispec.Descriptor{Digest: "root"} tdh := NewDiscoverHandler(buf, "rawRef", root, true) one := ocispec.Descriptor{ArtifactType: ocispec.MediaTypeImageLayer, Digest: "one"} if err := tdh.OnDiscovered(one, root); err != nil { t.Errorf("OnDiscovered() unexpected error: %v", err) } if err := tdh.Render(); err != nil { t.Errorf("Render() unexpected error: %v", err) } expected := `Discovered 1 artifact referencing rawRef Digest: root Artifact Type Digest application/vnd.oci.image.layer.v1.tar one { "mediaType": "", "digest": "one", "size": 0, "artifactType": "application/vnd.oci.image.layer.v1.tar" } ` if buf.String() != expected { t.Errorf("Expected output <%s> actual <%s>", expected, buf.String()) } } func TestTableDiscoverHandler_Failure(t *testing.T) { buf := new(bytes.Buffer) root := ocispec.Descriptor{Digest: "root"} tdh := NewDiscoverHandler(buf, "rawRef", root, false) one := ocispec.Descriptor{ArtifactType: ocispec.MediaTypeImageLayer, Digest: "one"} notRoot := ocispec.Descriptor{Digest: "notRoot"} err := tdh.OnDiscovered(one, notRoot) if err == nil { t.Fatal("OnDiscovered() Expected error with wrong parameter") } expected := "unexpected subject descriptor: { notRoot 0 [] map[] [] }" if err.Error() != expected { t.Errorf("Expected error <%s> actual error <%s>", expected, err.Error()) } } func Test_discoverHandler_Render(t *testing.T) { root := ocispec.Descriptor{Digest: "root"} one := ocispec.Descriptor{ArtifactType: ocispec.MediaTypeImageLayer, Digest: "one"} two := ocispec.Descriptor{ArtifactType: ocispec.MediaTypeImageLayer, Digest: "two"} tests := []struct { wf *testutils.WriteFailure }{ {wf: testutils.NewWriteFailure(1)}, {wf: testutils.NewWriteFailure(2)}, {wf: testutils.NewWriteFailure(3)}, {wf: testutils.NewWriteFailure(4)}, {wf: testutils.NewWriteFailure(5)}, } for _, tt := range tests { t.Run(tt.wf.Expected(), func(t *testing.T) { tdh := &discoverHandler{ out: tt.wf, rawReference: "rawRef", root: root, verbose: true, referrers: []ocispec.Descriptor{one, two}, } err := tdh.Render() if err == nil { t.Errorf("OnDiscovered() Expected error <%s>", tt.wf.Expected()) } else if err.Error() != tt.wf.Expected() { t.Errorf("Expected error <%s> actual error <%s>", tt.wf.Expected(), err.Error()) } }) } } oras-1.3.3/cmd/oras/internal/display/metadata/template/000077500000000000000000000000001522423464100230505ustar00rootroot00000000000000oras-1.3.3/cmd/oras/internal/display/metadata/template/attach.go000066400000000000000000000031271522423464100246460ustar00rootroot00000000000000/* Copyright The ORAS 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 template import ( "io" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/display/metadata/model" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/cmd/oras/internal/output" ) // AttachHandler handles go-template metadata output for attach events. type AttachHandler struct { template string out io.Writer path string root ocispec.Descriptor } // NewAttachHandler returns a new handler for attach metadata events. func NewAttachHandler(out io.Writer, template string) metadata.AttachHandler { return &AttachHandler{ out: out, template: template, } } // OnAttached implements AttachHandler. func (ah *AttachHandler) OnAttached(target *option.Target, root ocispec.Descriptor, _ ocispec.Descriptor) { ah.path = target.Path ah.root = root } // Render formats the metadata of attach command. func (ah *AttachHandler) Render() error { return output.ParseAndWrite(ah.out, model.NewAttach(ah.root, ah.path), ah.template) } oras-1.3.3/cmd/oras/internal/display/metadata/template/discover.go000066400000000000000000000031601522423464100252150ustar00rootroot00000000000000/* Copyright The ORAS 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 template import ( "io" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/display/metadata/model" "oras.land/oras/cmd/oras/internal/output" ) // discoverHandler handles json metadata output for discover events. type discoverHandler struct { template string path string out io.Writer model model.Discover } // NewDiscoverHandler creates a new handler for discover events. func NewDiscoverHandler(out io.Writer, root ocispec.Descriptor, path string, template string) metadata.DiscoverHandler { return &discoverHandler{ out: out, path: path, template: template, model: model.NewDiscover(path, root), } } // OnDiscovered implements metadata.DiscoverHandler. func (h *discoverHandler) OnDiscovered(referrer, subject ocispec.Descriptor) error { return h.model.AddReferrer(referrer, subject) } // Render implements metadata.DiscoverHandler. func (h *discoverHandler) Render() error { return output.ParseAndWrite(h.out, h.model.Root, h.template) } oras-1.3.3/cmd/oras/internal/display/metadata/template/manifest_fetch.go000066400000000000000000000030641522423464100263610ustar00rootroot00000000000000/* Copyright The ORAS 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 template import ( "encoding/json" "io" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/display/metadata/model" "oras.land/oras/cmd/oras/internal/output" ) // manifestFetchHandler handles JSON metadata output for manifest fetch events. type manifestFetchHandler struct { template string out io.Writer } // NewManifestFetchHandler creates a new handler for manifest fetch events. func NewManifestFetchHandler(out io.Writer, template string) metadata.ManifestFetchHandler { return &manifestFetchHandler{ template: template, out: out, } } // OnFetched is called after the manifest fetch is completed. func (h *manifestFetchHandler) OnFetched(path string, desc ocispec.Descriptor, content []byte) error { var manifest map[string]any if err := json.Unmarshal(content, &manifest); err != nil { manifest = nil } return output.ParseAndWrite(h.out, model.NewFetched(path, desc, manifest), h.template) } oras-1.3.3/cmd/oras/internal/display/metadata/template/pull.go000066400000000000000000000036731522423464100243640ustar00rootroot00000000000000/* Copyright The ORAS 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 template import ( "io" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/display/metadata/model" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/cmd/oras/internal/output" ) // PullHandler handles text metadata output for pull events. type PullHandler struct { template string path string out io.Writer pulled model.Pulled root ocispec.Descriptor } // NewPullHandler returns a new handler for pull events. func NewPullHandler(out io.Writer, path string, template string) metadata.PullHandler { return &PullHandler{ path: path, template: template, out: out, } } // OnPulled implements metadata.PullHandler. func (ph *PullHandler) OnPulled(_ *option.Target, desc ocispec.Descriptor) { ph.root = desc } // Render implements metadata.PullHandler. func (ph *PullHandler) Render() error { return output.ParseAndWrite(ph.out, model.NewPull(ph.path+"@"+ph.root.Digest.String(), ph.pulled.Files()), ph.template) } // OnFilePulled implements metadata.PullHandler. func (ph *PullHandler) OnFilePulled(name string, outputDir string, desc ocispec.Descriptor, descPath string) error { return ph.pulled.Add(name, outputDir, desc, descPath) } // OnLayerSkipped implements metadata.PullHandler. func (ph *PullHandler) OnLayerSkipped(ocispec.Descriptor) error { return nil } oras-1.3.3/cmd/oras/internal/display/metadata/template/push.go000066400000000000000000000036001522423464100243550ustar00rootroot00000000000000/* Copyright The ORAS 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 template import ( "io" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/display/metadata/model" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/cmd/oras/internal/output" "oras.land/oras/internal/contentutil" ) // PushHandler handles go-template metadata output for push events. type PushHandler struct { template string path string tagged model.Tagged out io.Writer root ocispec.Descriptor } // NewPushHandler returns a new handler for push events. func NewPushHandler(out io.Writer, template string) metadata.PushHandler { return &PushHandler{ out: out, template: template, } } // OnTagged implements metadata.TaggedHandler. func (ph *PushHandler) OnTagged(_ ocispec.Descriptor, tag string) error { ph.tagged.AddTag(tag) return nil } // OnCopied is called after files are copied. func (ph *PushHandler) OnCopied(opts *option.Target, root ocispec.Descriptor) error { if opts.RawReference != "" && !contentutil.IsDigest(opts.Reference) { ph.tagged.AddTag(opts.Reference) } ph.path = opts.Path ph.root = root return nil } // Render implements PushHandler. func (ph *PushHandler) Render() error { return output.ParseAndWrite(ph.out, model.NewPush(ph.root, ph.path, ph.tagged.Tags()), ph.template) } oras-1.3.3/cmd/oras/internal/display/metadata/template/repo_list.go000066400000000000000000000030521522423464100253770ustar00rootroot00000000000000/* Copyright The ORAS 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 template import ( "io" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/display/metadata/model" "oras.land/oras/cmd/oras/internal/output" ) // repoListHandler handles template metadata output for repo ls command. type repoListHandler struct { out io.Writer model *model.Repositories template string } // NewRepoListHandler creates a new template handler for repo ls command. func NewRepoListHandler(out io.Writer, tmpl string, registry string) metadata.RepoListHandler { return &repoListHandler{ out: out, model: model.NewRepositories(registry), template: tmpl, } } // OnRepositoryListed implements metadata.RepoListHandler. func (h *repoListHandler) OnRepositoryListed(repo string) error { // For Go-template format, show the full repository name h.model.AddRepository(repo) return nil } // Render implements metadata.RepoListHandler. func (h *repoListHandler) Render() error { return output.ParseAndWrite(h.out, h.model, h.template) } oras-1.3.3/cmd/oras/internal/display/metadata/template/repo_tags.go000066400000000000000000000026541522423464100253710ustar00rootroot00000000000000/* Copyright The ORAS 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 template import ( "io" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/display/metadata/model" "oras.land/oras/cmd/oras/internal/output" ) // repoTagsHandler handles template metadata output for repo tags command. type repoTagsHandler struct { out io.Writer model *model.Tags template string } // NewRepoTagsHandler creates a new template handler for repo tags command. func NewRepoTagsHandler(out io.Writer, tmpl string) metadata.RepoTagsHandler { return &repoTagsHandler{ out: out, model: model.NewTags(), template: tmpl, } } // OnTagListed implements metadata.TagsHandler. func (h *repoTagsHandler) OnTagListed(tag string) error { h.model.AddTag(tag) return nil } // Render implements metadata.TagsHandler. func (h *repoTagsHandler) Render() error { return output.ParseAndWrite(h.out, h.model, h.template) } oras-1.3.3/cmd/oras/internal/display/metadata/text/000077500000000000000000000000001522423464100222215ustar00rootroot00000000000000oras-1.3.3/cmd/oras/internal/display/metadata/text/attach.go000066400000000000000000000036631522423464100240240ustar00rootroot00000000000000/* Copyright The ORAS 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 text import ( "fmt" "strings" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/cmd/oras/internal/output" ) // AttachHandler handles text metadata output for attach events. type AttachHandler struct { printer *output.Printer subjectDisplayReference string root ocispec.Descriptor } // NewAttachHandler returns a new handler for attach events. func NewAttachHandler(printer *output.Printer) metadata.AttachHandler { return &AttachHandler{ printer: printer, } } // OnAttached implements AttachHandler. func (ah *AttachHandler) OnAttached(target *option.Target, root ocispec.Descriptor, subject ocispec.Descriptor) { ah.root = root if strings.HasSuffix(target.RawReference, subject.Digest.String()) { ah.subjectDisplayReference = target.GetDisplayReference() } else { // use subject digest instead of tag newTarget := *target newTarget.RawReference = fmt.Sprintf("%s@%s", target.Path, subject.Digest) ah.subjectDisplayReference = newTarget.GetDisplayReference() } } // Render is called when the attach command is complete. func (ah *AttachHandler) Render() error { err := ah.printer.Println("Attached to", ah.subjectDisplayReference) if err != nil { return err } return ah.printer.Println("Digest:", ah.root.Digest) } oras-1.3.3/cmd/oras/internal/display/metadata/text/attach_test.go000066400000000000000000000156531522423464100250650ustar00rootroot00000000000000/* Copyright The ORAS 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 text import ( "bytes" "os" "testing" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/cmd/oras/internal/output" ) func TestNewAttachHandler(t *testing.T) { printer := output.NewPrinter(&bytes.Buffer{}, os.Stderr) handler := NewAttachHandler(printer) if handler == nil { t.Fatal("NewAttachHandler() returned nil") } attachHandler, ok := handler.(*AttachHandler) if !ok { t.Fatal("NewAttachHandler() did not return an *AttachHandler") } if attachHandler.printer != printer { t.Error("NewAttachHandler() did not set printer correctly") } } func TestAttachHandler_OnAttached(t *testing.T) { content := []byte("test content") subjectDigest := digest.FromBytes(content) rootDigest := digest.FromBytes([]byte("root content")) tests := []struct { name string target *option.Target root ocispec.Descriptor subject ocispec.Descriptor expectedDisplayReference string }{ { name: "reference ends with subject digest", target: &option.Target{ Type: "registry", RawReference: "example.com/repo@" + subjectDigest.String(), Path: "example.com/repo", }, root: ocispec.Descriptor{ MediaType: "application/vnd.oci.image.manifest.v1+json", Digest: rootDigest, Size: 100, }, subject: ocispec.Descriptor{ MediaType: "application/vnd.oci.image.manifest.v1+json", Digest: subjectDigest, Size: 50, }, expectedDisplayReference: "[registry] example.com/repo@" + subjectDigest.String(), }, { name: "reference with tag, not digest", target: &option.Target{ Type: "registry", RawReference: "example.com/repo:latest", Path: "example.com/repo", }, root: ocispec.Descriptor{ MediaType: "application/vnd.oci.image.manifest.v1+json", Digest: rootDigest, Size: 100, }, subject: ocispec.Descriptor{ MediaType: "application/vnd.oci.image.manifest.v1+json", Digest: subjectDigest, Size: 50, }, expectedDisplayReference: "[registry] example.com/repo@" + subjectDigest.String(), }, { name: "reference with partial digest match", target: &option.Target{ Type: "registry", RawReference: "example.com/repo@sha256:partial" + subjectDigest.String()[12:], Path: "example.com/repo", }, root: ocispec.Descriptor{ MediaType: "application/vnd.oci.image.manifest.v1+json", Digest: rootDigest, Size: 100, }, subject: ocispec.Descriptor{ MediaType: "application/vnd.oci.image.manifest.v1+json", Digest: subjectDigest, Size: 50, }, expectedDisplayReference: "[registry] example.com/repo@" + subjectDigest.String(), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { printer := output.NewPrinter(&bytes.Buffer{}, os.Stderr) handler := &AttachHandler{ printer: printer, } handler.OnAttached(tt.target, tt.root, tt.subject) if handler.root.Digest != tt.root.Digest { t.Errorf("OnAttached() root digest = %v, want %v", handler.root.Digest, tt.root.Digest) } if handler.subjectDisplayReference != tt.expectedDisplayReference { t.Errorf("OnAttached() subjectDisplayReference = %v, want %v", handler.subjectDisplayReference, tt.expectedDisplayReference) } }) } } func TestAttachHandler_Render(t *testing.T) { content := []byte("test content") rootDigest := digest.FromBytes(content) tests := []struct { name string out *bytes.Buffer errorOut bool subjectDisplayReference string root ocispec.Descriptor wantErr bool expectedOutput string }{ { name: "successful render", out: &bytes.Buffer{}, subjectDisplayReference: "[registry] example.com/repo:latest", root: ocispec.Descriptor{ MediaType: "application/vnd.oci.image.manifest.v1+json", Digest: rootDigest, Size: int64(len(content)), }, wantErr: false, expectedOutput: "Attached to [registry] example.com/repo:latest\nDigest: " + rootDigest.String() + "\n", }, { name: "error on first print", out: nil, errorOut: true, subjectDisplayReference: "[registry] example.com/repo:latest", root: ocispec.Descriptor{ MediaType: "application/vnd.oci.image.manifest.v1+json", Digest: rootDigest, Size: int64(len(content)), }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var printer *output.Printer if tt.errorOut { printer = output.NewPrinter(&errorWriter{}, os.Stderr) } else { printer = output.NewPrinter(tt.out, os.Stderr) } handler := &AttachHandler{ printer: printer, subjectDisplayReference: tt.subjectDisplayReference, root: tt.root, } err := handler.Render() if (err != nil) != tt.wantErr { t.Errorf("AttachHandler.Render() error = %v, wantErr %v", err, tt.wantErr) return } if !tt.wantErr && tt.out != nil { output := tt.out.String() if output != tt.expectedOutput { t.Errorf("AttachHandler.Render() output = %q, want %q", output, tt.expectedOutput) } } }) } } func TestAttachHandler_Render_SuccessfulCase(t *testing.T) { content := []byte("test content") rootDigest := digest.FromBytes(content) buffer := &bytes.Buffer{} printer := output.NewPrinter(buffer, os.Stderr) handler := &AttachHandler{ printer: printer, subjectDisplayReference: "[registry] example.com/repo:latest", root: ocispec.Descriptor{ MediaType: "application/vnd.oci.image.manifest.v1+json", Digest: rootDigest, Size: int64(len(content)), }, } err := handler.Render() if err != nil { t.Errorf("AttachHandler.Render() error = %v, want nil", err) } expectedOutput := "Attached to [registry] example.com/repo:latest\nDigest: " + rootDigest.String() + "\n" if buffer.String() != expectedOutput { t.Errorf("AttachHandler.Render() output = %q, want %q", buffer.String(), expectedOutput) } } func TestAttachHandler_InterfaceCompliance(_ *testing.T) { var _ metadata.AttachHandler = (*AttachHandler)(nil) } oras-1.3.3/cmd/oras/internal/display/metadata/text/backup.go000066400000000000000000000053711522423464100240230ustar00rootroot00000000000000/* Copyright The ORAS 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 text import ( "strings" "time" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/display/status/progress/humanize" "oras.land/oras/cmd/oras/internal/output" ) // BackupHandler handles text metadata output for backup events. type BackupHandler struct { printer *output.Printer repo string } // NewBackupHandler returns a new handler for backup events. func NewBackupHandler(repo string, printer *output.Printer) metadata.BackupHandler { return &BackupHandler{ repo: repo, printer: printer, } } // OnBackupCompleted implements metadata.BackupHandler. func (bh *BackupHandler) OnBackupCompleted(tagsCount int, path string, duration time.Duration) error { return bh.printer.Printf("Successfully backed up %d tag(s) from %q to %q in %s.\n", tagsCount, bh.repo, path, humanize.FormatDuration(duration)) } // OnTarExported implements metadata.BackupHandler. func (bh *BackupHandler) OnTarExported(path string, size int64) error { return bh.printer.Printf("Exported to %s (%s)\n", path, humanize.ToBytes(size)) } // OnTarExporting implements metadata.BackupHandler. func (bh *BackupHandler) OnTarExporting(path string) error { return bh.printer.Printf("Exporting to %s\n", path) } // OnArtifactPulled implements metadata.BackupHandler. func (bh *BackupHandler) OnArtifactPulled(tag string, referrerCount int) error { // represent duration in a human-readable format return bh.printer.Printf("Pulled tag %s with %d referrer(s)\n", tag, referrerCount) } // OnTagsFound implements metadata.BackupHandler. func (bh *BackupHandler) OnTagsFound(tags []string) error { if len(tags) == 0 { return bh.printer.Printf("No tags found in %s\n", bh.repo) } if len(tags) <= 5 { // print small number of tags in one line return bh.printer.Printf("Found %d tag(s) in %s: %s\n", len(tags), bh.repo, strings.Join(tags, ", ")) } // print large number of tags line by line if err := bh.printer.Printf("Found %d tag(s) in %s:\n", len(tags), bh.repo); err != nil { return err } for _, tag := range tags { if err := bh.printer.Println(tag); err != nil { return err } } return nil } // Render implements metadata.BackupHandler. func (bh *BackupHandler) Render() error { return nil } oras-1.3.3/cmd/oras/internal/display/metadata/text/backup_test.go000066400000000000000000000140121522423464100250520ustar00rootroot00000000000000/* Copyright The ORAS 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 text import ( "bytes" "fmt" "io" "os" "testing" "time" "oras.land/oras/cmd/oras/internal/output" ) func TestBackupHandler_OnTarExported(t *testing.T) { path := "test.tar" tests := []struct { name string out io.Writer size int64 wantErr bool want string }{ { name: "good path with 1KiB size", out: &bytes.Buffer{}, size: int64(1024), wantErr: false, want: fmt.Sprintf("Exported to %s (1 KB)\n", path), }, { name: "good path with 0 size", out: &bytes.Buffer{}, size: 0, wantErr: false, want: fmt.Sprintf("Exported to %s (0 B)\n", path), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { printer := output.NewPrinter(tt.out, os.Stderr) bh := NewBackupHandler("any", printer) if err := bh.OnTarExported(path, tt.size); (err != nil) != tt.wantErr { t.Errorf("OnTarExported() error = %v, wantErr %v", err, tt.wantErr) } if !tt.wantErr { got := tt.out.(*bytes.Buffer).String() if got != tt.want { t.Errorf("OnTarExported() got = %v, want %v", got, tt.want) } } }) } } func TestBackupHandler_OnTarExporting(t *testing.T) { path := "test.tar" tests := []struct { name string out io.Writer wantErr bool want string }{ { name: "good path", out: &bytes.Buffer{}, wantErr: false, want: fmt.Sprintf("Exporting to %s\n", path), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { printer := output.NewPrinter(tt.out, os.Stderr) bh := NewBackupHandler("any", printer) if err := bh.OnTarExporting(path); (err != nil) != tt.wantErr { t.Errorf("OnTarExporting() error = %v, wantErr %v", err, tt.wantErr) } if !tt.wantErr { got := tt.out.(*bytes.Buffer).String() if got != tt.want { t.Errorf("OnTarExporting() got = %v, want %v", got, tt.want) } } }) } } func TestBackupHandler_OnArtifactPulled(t *testing.T) { tag := "latest" referrerCount := 3 tests := []struct { name string out io.Writer wantErr bool want string }{ { name: "good path", out: &bytes.Buffer{}, wantErr: false, want: fmt.Sprintf("Pulled tag %s with %d referrer(s)\n", tag, referrerCount), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { printer := output.NewPrinter(tt.out, os.Stderr) bh := NewBackupHandler("any", printer) if err := bh.OnArtifactPulled(tag, referrerCount); (err != nil) != tt.wantErr { t.Errorf("OnArtifactPulled() error = %v, wantErr %v", err, tt.wantErr) } if !tt.wantErr { got := tt.out.(*bytes.Buffer).String() if got != tt.want { t.Errorf("OnArtifactPulled() got = %v, want %v", got, tt.want) } } }) } } func TestBackupHandler_OnTagsFound(t *testing.T) { repo := "testRepo" tests := []struct { name string tags []string out io.Writer wantErr bool want string }{ { name: "good path with few tags", tags: []string{"t1", "t2"}, out: &bytes.Buffer{}, wantErr: false, want: fmt.Sprintf("Found 2 tag(s) in %s: t1, t2\n", repo), }, { name: "good path with exactly 5 tags", tags: []string{"t1", "t2", "t3", "t4", "t5"}, out: &bytes.Buffer{}, wantErr: false, want: fmt.Sprintf("Found 5 tag(s) in %s: t1, t2, t3, t4, t5\n", repo), }, { name: "good path with more than 5 tags", tags: []string{"t1", "t2", "t3", "t4", "t5", "t6"}, out: &bytes.Buffer{}, wantErr: false, want: fmt.Sprintf("Found 6 tag(s) in %s:\nt1\nt2\nt3\nt4\nt5\nt6\n", repo), }, { name: "good path with no tags", tags: []string{}, out: &bytes.Buffer{}, wantErr: false, want: "No tags found in " + repo + "\n", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { printer := output.NewPrinter(tt.out, os.Stderr) bh := NewBackupHandler(repo, printer) if err := bh.OnTagsFound(tt.tags); (err != nil) != tt.wantErr { t.Errorf("OnTagsFound() error = %v, wantErr %v", err, tt.wantErr) } if !tt.wantErr { got := tt.out.(*bytes.Buffer).String() if got != tt.want { t.Errorf("OnTagsFound() got = %v, want %v", got, tt.want) } } }) } } func TestBackupHandler_Render(t *testing.T) { tests := []struct { name string wantErr bool }{ { name: "good path", wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { bh := &BackupHandler{} if err := bh.Render(); (err != nil) != tt.wantErr { t.Errorf("Render() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestBackupHandler_OnBackupCompleted(t *testing.T) { repo := "testRepo" path := "testPath" tagsCount := 5 duration := time.Second * 30 tests := []struct { name string out io.Writer wantErr bool want string }{ { name: "good path", out: &bytes.Buffer{}, wantErr: false, want: fmt.Sprintf("Successfully backed up %d tag(s) from %q to %q in %s.\n", tagsCount, repo, path, "30s"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { printer := output.NewPrinter(tt.out, os.Stderr) bh := NewBackupHandler(repo, printer) if err := bh.OnBackupCompleted(tagsCount, path, duration); (err != nil) != tt.wantErr { t.Errorf("OnBackupCompleted() error = %v, wantErr %v", err, tt.wantErr) } if !tt.wantErr { got := tt.out.(*bytes.Buffer).String() if got != tt.want { t.Errorf("OnBackupCompleted() got = %v, want %v", got, tt.want) } } }) } } oras-1.3.3/cmd/oras/internal/display/metadata/text/blob_delete.go000066400000000000000000000026601522423464100250140ustar00rootroot00000000000000/* Copyright The ORAS 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 text import ( "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/cmd/oras/internal/output" ) // BlobDeleteHandler handles text metadata output for blob delete events. type BlobDeleteHandler struct { printer *output.Printer target *option.Target } // NewBlobDeleteHandler returns a new handler for blob delete events. func NewBlobDeleteHandler(printer *output.Printer, target *option.Target) metadata.BlobDeleteHandler { return &BlobDeleteHandler{ printer: printer, target: target, } } // OnBlobMissing implements BlobDeleteHandler. func (h *BlobDeleteHandler) OnBlobMissing() error { return h.printer.Println("Missing", h.target.RawReference) } // OnBlobDeleted implements BlobDeleteHandler. func (h *BlobDeleteHandler) OnBlobDeleted() error { return h.printer.Println("Deleted", h.target.GetDisplayReference()) } oras-1.3.3/cmd/oras/internal/display/metadata/text/blob_delete_test.go000066400000000000000000000126131522423464100260520ustar00rootroot00000000000000/* Copyright The ORAS 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 text import ( "bytes" "io" "os" "testing" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/cmd/oras/internal/output" ) func TestNewBlobDeleteHandler(t *testing.T) { printer := output.NewPrinter(&bytes.Buffer{}, os.Stderr) target := &option.Target{ Type: "registry", RawReference: "localhost:5000/test@sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", } handler := NewBlobDeleteHandler(printer, target) if handler == nil { t.Fatal("expected a non-nil handler") } // Type assertion to access internal fields if bdh, ok := handler.(*BlobDeleteHandler); ok { if bdh.printer != printer { t.Errorf("expected handler.printer to be %v, got %v", printer, bdh.printer) } if bdh.target != target { t.Errorf("expected handler.target to be %v, got %v", target, bdh.target) } } else { t.Error("expected handler to be of type *BlobDeleteHandler") } } func TestBlobDeleteHandler_OnBlobMissing(t *testing.T) { tests := []struct { name string out io.Writer target *option.Target wantErr bool wantOutput string }{ { name: "blob missing with digest reference", out: &bytes.Buffer{}, target: &option.Target{ Type: "registry", RawReference: "localhost:5000/test@sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", }, wantErr: false, wantOutput: "Missing localhost:5000/test@sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08\n", }, { name: "blob missing with tag reference", out: &bytes.Buffer{}, target: &option.Target{ Type: "registry", RawReference: "localhost:5000/test:latest", }, wantErr: false, wantOutput: "Missing localhost:5000/test:latest\n", }, { name: "blob missing with oci layout", out: &bytes.Buffer{}, target: &option.Target{ Type: "oci-layout", RawReference: "./layout@sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", }, wantErr: false, wantOutput: "Missing ./layout@sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08\n", }, { name: "error writer", out: &errorWriter{}, target: &option.Target{ Type: "registry", RawReference: "localhost:5000/test@sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { printer := output.NewPrinter(tt.out, os.Stderr) handler := NewBlobDeleteHandler(printer, tt.target) err := handler.OnBlobMissing() if (err != nil) != tt.wantErr { t.Errorf("OnBlobMissing() error = %v, wantErr %v", err, tt.wantErr) } if !tt.wantErr { got := tt.out.(*bytes.Buffer).String() if got != tt.wantOutput { t.Errorf("OnBlobMissing() output = %q, want %q", got, tt.wantOutput) } } }) } } func TestBlobDeleteHandler_OnBlobDeleted(t *testing.T) { tests := []struct { name string out io.Writer target *option.Target wantErr bool wantOutput string }{ { name: "blob deleted with digest reference", out: &bytes.Buffer{}, target: &option.Target{ Type: "registry", RawReference: "localhost:5000/test@sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", }, wantErr: false, wantOutput: "Deleted [registry] localhost:5000/test@sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08\n", }, { name: "blob deleted with tag reference", out: &bytes.Buffer{}, target: &option.Target{ Type: "registry", RawReference: "localhost:5000/test:latest", }, wantErr: false, wantOutput: "Deleted [registry] localhost:5000/test:latest\n", }, { name: "blob deleted with oci layout", out: &bytes.Buffer{}, target: &option.Target{ Type: "oci-layout", RawReference: "./layout@sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", }, wantErr: false, wantOutput: "Deleted [oci-layout] ./layout@sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08\n", }, { name: "error writer", out: &errorWriter{}, target: &option.Target{ Type: "registry", RawReference: "localhost:5000/test@sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { printer := output.NewPrinter(tt.out, os.Stderr) handler := NewBlobDeleteHandler(printer, tt.target) err := handler.OnBlobDeleted() if (err != nil) != tt.wantErr { t.Errorf("OnBlobDeleted() error = %v, wantErr %v", err, tt.wantErr) } if !tt.wantErr { got := tt.out.(*bytes.Buffer).String() if got != tt.wantOutput { t.Errorf("OnBlobDeleted() output = %q, want %q", got, tt.wantOutput) } } }) } } oras-1.3.3/cmd/oras/internal/display/metadata/text/blob_push.go000066400000000000000000000027451522423464100245350ustar00rootroot00000000000000/* Copyright The ORAS 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 text import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/cmd/oras/internal/output" ) // BlobPushHandler handles text metadata output for blob push events. type BlobPushHandler struct { printer *output.Printer desc ocispec.Descriptor } // NewBlobPushHandler returns a new handler for Blob push events. func NewBlobPushHandler(printer *output.Printer, desc ocispec.Descriptor) metadata.BlobPushHandler { return &BlobPushHandler{ printer: printer, desc: desc, } } // OnBlobPushed implements metadata.BlobPushHandler. func (h *BlobPushHandler) OnBlobPushed(target *option.Target) error { return h.printer.Println("Pushed:", target.GetDisplayReference()) } // Render implements metadata.BlobPushHandler. func (h *BlobPushHandler) Render() error { return h.printer.Println("Digest:", h.desc.Digest) } oras-1.3.3/cmd/oras/internal/display/metadata/text/blob_push_test.go000066400000000000000000000106641522423464100255730ustar00rootroot00000000000000/* Copyright The ORAS 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 text import ( "bytes" "fmt" "io" "os" "reflect" "testing" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/cmd/oras/internal/output" ) func TestNewBlobPushHandler(t *testing.T) { printer := output.NewPrinter(&bytes.Buffer{}, os.Stderr) desc := ocispec.Descriptor{ MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: digest.FromString("test"), Size: 123, } handler := NewBlobPushHandler(printer, desc) if handler == nil { t.Fatal("expected a non-nil handler") } // Type assertion to access internal fields if bph, ok := handler.(*BlobPushHandler); ok { if bph.printer != printer { t.Errorf("expected handler.printer to be %v, got %v", printer, bph.printer) } if !reflect.DeepEqual(bph.desc, desc) { t.Errorf("expected handler.desc to be %v, got %v", desc, bph.desc) } } else { t.Error("expected handler to be of type *BlobPushHandler") } } func TestBlobPushHandler_OnBlobPushed(t *testing.T) { tests := []struct { name string out io.Writer target *option.Target wantErr bool wantOutput string }{ { name: "successful push output", out: &bytes.Buffer{}, target: &option.Target{ Type: "registry", RawReference: "localhost:5000/test:latest", }, wantErr: false, wantOutput: "Pushed: [registry] localhost:5000/test:latest\n", }, { name: "oci layout target", out: &bytes.Buffer{}, target: &option.Target{ Type: "oci-layout", RawReference: "./layout:latest", }, wantErr: false, wantOutput: "Pushed: [oci-layout] ./layout:latest\n", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { printer := output.NewPrinter(tt.out, os.Stderr) desc := ocispec.Descriptor{ MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: digest.FromString("test"), Size: 123, } handler := NewBlobPushHandler(printer, desc) err := handler.OnBlobPushed(tt.target) if (err != nil) != tt.wantErr { t.Errorf("OnBlobPushed() error = %v, wantErr %v", err, tt.wantErr) } if !tt.wantErr { got := tt.out.(*bytes.Buffer).String() if got != tt.wantOutput { t.Errorf("OnBlobPushed() output = %q, want %q", got, tt.wantOutput) } } }) } } func TestBlobPushHandler_Render(t *testing.T) { content := []byte("test content") testDigest := digest.FromBytes(content) tests := []struct { name string out io.Writer desc ocispec.Descriptor wantErr bool wantOutput string }{ { name: "render digest output", out: &bytes.Buffer{}, desc: ocispec.Descriptor{ MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: testDigest, Size: int64(len(content)), }, wantErr: false, wantOutput: fmt.Sprintf("Digest: %s\n", testDigest), }, { name: "render with different media type", out: &bytes.Buffer{}, desc: ocispec.Descriptor{ MediaType: "application/vnd.example.file", Digest: testDigest, Size: int64(len(content)), }, wantErr: false, wantOutput: fmt.Sprintf("Digest: %s\n", testDigest), }, { name: "error writer", out: &errorWriter{}, desc: ocispec.Descriptor{ MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: testDigest, Size: int64(len(content)), }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { printer := output.NewPrinter(tt.out, os.Stderr) handler := NewBlobPushHandler(printer, tt.desc) err := handler.Render() if (err != nil) != tt.wantErr { t.Errorf("Render() error = %v, wantErr %v", err, tt.wantErr) } if !tt.wantErr { got := tt.out.(*bytes.Buffer).String() if got != tt.wantOutput { t.Errorf("Render() output = %q, want %q", got, tt.wantOutput) } } }) } } oras-1.3.3/cmd/oras/internal/display/metadata/text/copy.go000066400000000000000000000031671522423464100235310ustar00rootroot00000000000000/* Copyright The ORAS 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 text import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/cmd/oras/internal/output" ) // CopyHandler handles text metadata output for cp events. type CopyHandler struct { printer *output.Printer desc ocispec.Descriptor } // NewCopyHandler returns a new handler for cp events. func NewCopyHandler(printer *output.Printer) metadata.CopyHandler { return &CopyHandler{ printer: printer, } } // OnTagged implements metadata.TaggedHandler. func (h *CopyHandler) OnTagged(_ ocispec.Descriptor, tag string) error { return h.printer.Println("Tagged", tag) } // Render implements metadata.Renderer. func (h *CopyHandler) Render() error { return h.printer.Println("Digest:", h.desc.Digest) } // OnCopied implements metadata.CopyHandler. func (h *CopyHandler) OnCopied(target *option.BinaryTarget, desc ocispec.Descriptor) error { h.desc = desc return h.printer.Println("Copied", target.From.GetDisplayReference(), "=>", target.To.GetDisplayReference()) } oras-1.3.3/cmd/oras/internal/display/metadata/text/copy_test.go000066400000000000000000000236161522423464100245710ustar00rootroot00000000000000/* Copyright The ORAS 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 text import ( "bytes" "fmt" "io" "os" "testing" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/cmd/oras/internal/output" ) func TestNewCopyHandler(t *testing.T) { tests := []struct { name string out io.Writer }{ { name: "creates handler with valid printer", out: &bytes.Buffer{}, }, { name: "creates handler with error writer", out: &errorWriter{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { printer := output.NewPrinter(tt.out, os.Stderr) handler := NewCopyHandler(printer) // Verify it's the correct concrete type if _, ok := handler.(*CopyHandler); !ok { t.Error("NewCopyHandler() does not return a *CopyHandler") } }) } } func TestCopyHandler_OnTagged(t *testing.T) { content := []byte("test content") desc := ocispec.Descriptor{ MediaType: "application/vnd.oci.image.manifest.v1+json", Digest: digest.FromBytes(content), Size: int64(len(content)), } tests := []struct { name string out io.Writer desc ocispec.Descriptor tag string want string wantErr bool }{ { name: "tags with simple tag name", out: &bytes.Buffer{}, desc: desc, tag: "latest", want: "Tagged latest\n", wantErr: false, }, { name: "tags with version tag", out: &bytes.Buffer{}, desc: desc, tag: "v1.0.0", want: "Tagged v1.0.0\n", wantErr: false, }, { name: "tags with complex tag name", out: &bytes.Buffer{}, desc: desc, tag: "feature-branch-123", want: "Tagged feature-branch-123\n", wantErr: false, }, { name: "tags with empty tag", out: &bytes.Buffer{}, desc: desc, tag: "", want: "Tagged \n", wantErr: false, }, { name: "error when writing fails", out: &errorWriter{}, desc: desc, tag: "latest", want: "", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { printer := output.NewPrinter(tt.out, os.Stderr) handler := NewCopyHandler(printer).(*CopyHandler) err := handler.OnTagged(tt.desc, tt.tag) if (err != nil) != tt.wantErr { t.Errorf("CopyHandler.OnTagged() error = %v, wantErr %v", err, tt.wantErr) return } if !tt.wantErr { if buf, ok := tt.out.(*bytes.Buffer); ok { got := buf.String() if got != tt.want { t.Errorf("CopyHandler.OnTagged() output = %q, want %q", got, tt.want) } } } }) } } func TestCopyHandler_Render(t *testing.T) { content := []byte("test content") testDigest := digest.FromBytes(content) tests := []struct { name string out io.Writer desc ocispec.Descriptor want string wantErr bool }{ { name: "renders digest correctly", out: &bytes.Buffer{}, desc: ocispec.Descriptor{ MediaType: "application/vnd.oci.image.manifest.v1+json", Digest: testDigest, Size: int64(len(content)), }, want: fmt.Sprintf("Digest: %s\n", testDigest), wantErr: false, }, { name: "renders empty digest", out: &bytes.Buffer{}, desc: ocispec.Descriptor{ MediaType: "application/vnd.oci.image.manifest.v1+json", Digest: "", Size: 0, }, want: "Digest: \n", wantErr: false, }, { name: "error when writing fails", out: &errorWriter{}, desc: ocispec.Descriptor{ MediaType: "application/vnd.oci.image.manifest.v1+json", Digest: testDigest, Size: int64(len(content)), }, want: "", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { printer := output.NewPrinter(tt.out, os.Stderr) handler := &CopyHandler{ printer: printer, desc: tt.desc, } err := handler.Render() if (err != nil) != tt.wantErr { t.Errorf("CopyHandler.Render() error = %v, wantErr %v", err, tt.wantErr) return } if !tt.wantErr { if buf, ok := tt.out.(*bytes.Buffer); ok { got := buf.String() if got != tt.want { t.Errorf("CopyHandler.Render() output = %q, want %q", got, tt.want) } } } }) } } func TestCopyHandler_OnCopied(t *testing.T) { content := []byte("test content") testDesc := ocispec.Descriptor{ MediaType: "application/vnd.oci.image.manifest.v1+json", Digest: digest.FromBytes(content), Size: int64(len(content)), } tests := []struct { name string out io.Writer target *option.BinaryTarget desc ocispec.Descriptor want string wantErr bool }{ { name: "copies between registries", out: &bytes.Buffer{}, target: &option.BinaryTarget{ From: option.Target{ Type: option.TargetTypeRemote, RawReference: "localhost:5000/source:latest", }, To: option.Target{ Type: option.TargetTypeRemote, RawReference: "localhost:5000/destination:latest", }, }, desc: testDesc, want: "Copied [registry] localhost:5000/source:latest => [registry] localhost:5000/destination:latest\n", wantErr: false, }, { name: "copies from registry to oci-layout", out: &bytes.Buffer{}, target: &option.BinaryTarget{ From: option.Target{ Type: option.TargetTypeRemote, RawReference: "localhost:5000/source:v1.0.0", }, To: option.Target{ Type: option.TargetTypeOCILayout, RawReference: "./oci-layout:latest", }, }, desc: testDesc, want: "Copied [registry] localhost:5000/source:v1.0.0 => [oci-layout] ./oci-layout:latest\n", wantErr: false, }, { name: "copies from oci-layout to registry", out: &bytes.Buffer{}, target: &option.BinaryTarget{ From: option.Target{ Type: option.TargetTypeOCILayout, RawReference: "./source-layout:tag1", }, To: option.Target{ Type: option.TargetTypeRemote, RawReference: "ghcr.io/example/repo:tag2", }, }, desc: testDesc, want: "Copied [oci-layout] ./source-layout:tag1 => [registry] ghcr.io/example/repo:tag2\n", wantErr: false, }, { name: "copies between oci-layouts", out: &bytes.Buffer{}, target: &option.BinaryTarget{ From: option.Target{ Type: option.TargetTypeOCILayout, RawReference: "./source:v1", }, To: option.Target{ Type: option.TargetTypeOCILayout, RawReference: "./dest:v2", }, }, desc: testDesc, want: "Copied [oci-layout] ./source:v1 => [oci-layout] ./dest:v2\n", wantErr: false, }, { name: "error when writing fails", out: &errorWriter{}, target: &option.BinaryTarget{ From: option.Target{ Type: option.TargetTypeRemote, RawReference: "localhost:5000/source:latest", }, To: option.Target{ Type: option.TargetTypeRemote, RawReference: "localhost:5000/destination:latest", }, }, desc: testDesc, want: "", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { printer := output.NewPrinter(tt.out, os.Stderr) handler := NewCopyHandler(printer).(*CopyHandler) err := handler.OnCopied(tt.target, tt.desc) if (err != nil) != tt.wantErr { t.Errorf("CopyHandler.OnCopied() error = %v, wantErr %v", err, tt.wantErr) return } // Verify that the descriptor was stored if !tt.wantErr { if handler.desc.Digest != tt.desc.Digest { t.Errorf("CopyHandler.OnCopied() did not store descriptor correctly, got digest %v, want %v", handler.desc.Digest, tt.desc.Digest) } if handler.desc.MediaType != tt.desc.MediaType { t.Errorf("CopyHandler.OnCopied() did not store descriptor correctly, got MediaType %v, want %v", handler.desc.MediaType, tt.desc.MediaType) } if handler.desc.Size != tt.desc.Size { t.Errorf("CopyHandler.OnCopied() did not store descriptor correctly, got Size %v, want %v", handler.desc.Size, tt.desc.Size) } } if !tt.wantErr { if buf, ok := tt.out.(*bytes.Buffer); ok { got := buf.String() if got != tt.want { t.Errorf("CopyHandler.OnCopied() output = %q, want %q", got, tt.want) } } } }) } } func TestCopyHandler_Integration(t *testing.T) { // Test that shows the flow: OnCopied followed by Render content := []byte("integration test content") testDesc := ocispec.Descriptor{ MediaType: "application/vnd.oci.image.manifest.v1+json", Digest: digest.FromBytes(content), Size: int64(len(content)), } buf := &bytes.Buffer{} printer := output.NewPrinter(buf, os.Stderr) handler := NewCopyHandler(printer).(*CopyHandler) target := &option.BinaryTarget{ From: option.Target{ Type: option.TargetTypeRemote, RawReference: "localhost:5000/src:latest", }, To: option.Target{ Type: option.TargetTypeRemote, RawReference: "localhost:5000/dst:latest", }, } // Call OnCopied first err := handler.OnCopied(target, testDesc) if err != nil { t.Fatalf("OnCopied() failed: %v", err) } // Then call Render err = handler.Render() if err != nil { t.Fatalf("Render() failed: %v", err) } got := buf.String() expectedCopy := "Copied [registry] localhost:5000/src:latest => [registry] localhost:5000/dst:latest\n" expectedDigest := fmt.Sprintf("Digest: %s\n", testDesc.Digest) expected := expectedCopy + expectedDigest if got != expected { t.Errorf("Integration test failed.\nGot:\n%q\nWant:\n%q", got, expected) } } oras-1.3.3/cmd/oras/internal/display/metadata/text/manifest_delete.go000066400000000000000000000027601522423464100257050ustar00rootroot00000000000000/* Copyright The ORAS 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 text import ( "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/cmd/oras/internal/output" ) // ManifestDeleteHandler handles text metadata output for manifest delete events. type ManifestDeleteHandler struct { printer *output.Printer target *option.Target } // NewManifestDeleteHandler returns a new handler for manifest delete events. func NewManifestDeleteHandler(printer *output.Printer, target *option.Target) metadata.ManifestDeleteHandler { return &ManifestDeleteHandler{ printer: printer, target: target, } } // OnManifestMissing implements ManifestDeleteHandler. func (h *ManifestDeleteHandler) OnManifestMissing() error { return h.printer.Println("Missing", h.target.RawReference) } // OnManifestDeleted implements ManifestDeleteHandler. func (h *ManifestDeleteHandler) OnManifestDeleted() error { return h.printer.Println("Deleted", h.target.GetDisplayReference()) } oras-1.3.3/cmd/oras/internal/display/metadata/text/manifest_index.go000066400000000000000000000031501522423464100255440ustar00rootroot00000000000000/* Copyright The ORAS 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 text import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/output" ) // ManifestIndexCreateHandler handles text metadata output for index create events. type ManifestIndexCreateHandler struct { printer *output.Printer root ocispec.Descriptor } // NewManifestIndexCreateHandler returns a new handler for index create events. func NewManifestIndexCreateHandler(printer *output.Printer) metadata.ManifestIndexCreateHandler { return &ManifestIndexCreateHandler{ printer: printer, } } // OnTagged implements TaggedHandler. func (h *ManifestIndexCreateHandler) OnTagged(_ ocispec.Descriptor, tag string) error { return h.printer.Println("Tagged", tag) } // OnIndexCreated implements ManifestIndexCreateHandler. func (h *ManifestIndexCreateHandler) OnIndexCreated(desc ocispec.Descriptor) { h.root = desc } // Render implements ManifestIndexCreateHandler. func (h *ManifestIndexCreateHandler) Render() error { return h.printer.Println("Digest:", h.root.Digest) } oras-1.3.3/cmd/oras/internal/display/metadata/text/manifest_push.go000066400000000000000000000033641522423464100254230ustar00rootroot00000000000000/* Copyright The ORAS 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 text import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/cmd/oras/internal/output" ) // ManifestPushHandler handles text metadata output for manifest push events. type ManifestPushHandler struct { printer *output.Printer target *option.Target desc ocispec.Descriptor } // NewManifestPushHandler returns a new handler for manifest push events. func NewManifestPushHandler(printer *output.Printer, target *option.Target) metadata.ManifestPushHandler { return &ManifestPushHandler{ printer: printer, target: target, } } // OnTagged implements metadata.TaggedHandler. func (h *ManifestPushHandler) OnTagged(_ ocispec.Descriptor, tag string) error { return h.printer.Println("Tagged", tag) } // OnManifestPushed implements metadata.ManifestPushHandler. func (h *ManifestPushHandler) OnManifestPushed(desc ocispec.Descriptor) error { h.desc = desc return h.printer.Println("Pushed:", h.target.GetDisplayReference()) } // Render implements metadata.ManifestPushHandler. func (h *ManifestPushHandler) Render() error { return h.printer.Println("Digest:", h.desc.Digest) } oras-1.3.3/cmd/oras/internal/display/metadata/text/pull.go000066400000000000000000000040601522423464100235240ustar00rootroot00000000000000/* Copyright The ORAS 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 text import ( "sync/atomic" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/cmd/oras/internal/output" ) // PullHandler handles text metadata output for pull events. type PullHandler struct { printer *output.Printer layerSkipped atomic.Bool target *option.Target root ocispec.Descriptor } // NewPullHandler returns a new handler for Pull events. func NewPullHandler(printer *output.Printer) metadata.PullHandler { return &PullHandler{ printer: printer, } } func (ph *PullHandler) OnFilePulled(_ string, _ string, _ ocispec.Descriptor, _ string) error { return nil } // OnLayerSkipped implements metadata.PullHandler. func (ph *PullHandler) OnLayerSkipped(ocispec.Descriptor) error { ph.layerSkipped.Store(true) return nil } // OnPulled implements metadata.PullHandler. func (ph *PullHandler) OnPulled(target *option.Target, desc ocispec.Descriptor) { ph.target = target ph.root = desc } // Render implements metadata.PullHandler. func (ph *PullHandler) Render() error { if ph.layerSkipped.Load() { _ = ph.printer.Printf("Skipped pulling layers without file name in %q\n", ocispec.AnnotationTitle) _ = ph.printer.Printf("Use 'oras copy %s --to-oci-layout ' to pull all layers.\n", ph.target.RawReference) } else { _ = ph.printer.Println("Pulled", ph.target.GetDisplayReference()) _ = ph.printer.Println("Digest:", ph.root.Digest) } return nil } oras-1.3.3/cmd/oras/internal/display/metadata/text/push.go000066400000000000000000000033521522423464100235320ustar00rootroot00000000000000/* Copyright The ORAS 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 text import ( "sync" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/cmd/oras/internal/output" ) // PushHandler handles text metadata output for push events. type PushHandler struct { printer *output.Printer tagLock sync.Mutex root ocispec.Descriptor } // NewPushHandler returns a new handler for push events. func NewPushHandler(printer *output.Printer) metadata.PushHandler { return &PushHandler{ printer: printer, } } // OnTagged implements metadata.TaggedHandler. func (h *PushHandler) OnTagged(_ ocispec.Descriptor, tag string) error { h.tagLock.Lock() defer h.tagLock.Unlock() return h.printer.Println("Tagged", tag) } // OnCopied is called after files are copied. func (h *PushHandler) OnCopied(opts *option.Target, root ocispec.Descriptor) error { h.root = root return h.printer.Println("Pushed", opts.GetDisplayReference()) } // Render implements PushHandler. func (h *PushHandler) Render() error { err := h.printer.Println("ArtifactType:", h.root.ArtifactType) if err != nil { return err } return h.printer.Println("Digest:", h.root.Digest) } oras-1.3.3/cmd/oras/internal/display/metadata/text/push_test.go000066400000000000000000000035011522423464100245650ustar00rootroot00000000000000/* Copyright The ORAS 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 text import ( "bytes" "fmt" "io" "os" "testing" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/output" ) type errorWriter struct{} // Write implements the io.Writer interface and returns an error in Write. func (w *errorWriter) Write(_ []byte) (n int, err error) { return 0, fmt.Errorf("got an error") } func TestPushHandler_Render(t *testing.T) { content := []byte("content") tests := []struct { name string out io.Writer root ocispec.Descriptor wantErr bool }{ { "good path", &bytes.Buffer{}, ocispec.Descriptor{ MediaType: "example", Digest: digest.FromBytes(content), Size: int64(len(content)), }, false, }, { "error path", &errorWriter{}, ocispec.Descriptor{ MediaType: "example", Digest: digest.FromBytes(content), Size: int64(len(content)), }, true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { printer := output.NewPrinter(tt.out, os.Stderr) p := &PushHandler{ printer: printer, root: tt.root, } if err := p.Render(); (err != nil) != tt.wantErr { t.Errorf("PushHandler.Render() error = %v, wantErr %v", err, tt.wantErr) } }) } } oras-1.3.3/cmd/oras/internal/display/metadata/text/repo_list.go000066400000000000000000000027001522423464100245470ustar00rootroot00000000000000/* Copyright The ORAS 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 text import ( "io" "strings" "oras.land/oras/cmd/oras/internal/display/metadata" ) // repoListHandler handles text output for repo ls command. type repoListHandler struct { out io.Writer namespace string } // NewRepoListHandler creates a new text handler for repo ls command. func NewRepoListHandler(out io.Writer, namespace string) metadata.RepoListHandler { return &repoListHandler{ out: out, namespace: namespace, } } // OnRepositoryListed implements metadata.RepoListHandler. func (h *repoListHandler) OnRepositoryListed(repo string) error { // For text format, show only the sub repo (without the namespace prefix) for better readability if h.namespace != "" { repo = strings.TrimPrefix(repo, h.namespace) } _, err := io.WriteString(h.out, repo+"\n") return err } // Render implements metadata.RepoListHandler. func (h *repoListHandler) Render() error { return nil } oras-1.3.3/cmd/oras/internal/display/metadata/text/repo_tags.go000066400000000000000000000022551522423464100245370ustar00rootroot00000000000000/* Copyright The ORAS 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 text import ( "io" "oras.land/oras/cmd/oras/internal/display/metadata" ) // repoTagsHandler handles text output for repo tags command. type repoTagsHandler struct { out io.Writer } // NewRepoTagsHandler creates a new text handler for repo tags command. func NewRepoTagsHandler(out io.Writer) metadata.RepoTagsHandler { return &repoTagsHandler{ out: out, } } // OnTagListed implements metadata.TagsHandler. func (h *repoTagsHandler) OnTagListed(tag string) error { _, err := io.WriteString(h.out, tag+"\n") return err } // Render implements metadata.TagsHandler. func (h *repoTagsHandler) Render() error { return nil } oras-1.3.3/cmd/oras/internal/display/metadata/text/resolve.go000066400000000000000000000025551522423464100242360ustar00rootroot00000000000000/* Copyright The ORAS 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 text import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/output" ) // ResolveHandler handles text metadata output for resolve events. type ResolveHandler struct { printer *output.Printer fullRef bool path string } // NewResolveHandler returns a new handler for resolve events. func NewResolveHandler(printer *output.Printer, fullRef bool, path string) metadata.ResolveHandler { return &ResolveHandler{ printer: printer, fullRef: fullRef, path: path, } } // OnResolved implements metadata.ResolveHandler. func (h *ResolveHandler) OnResolved(desc ocispec.Descriptor) error { if h.fullRef { return h.printer.Printf("%s@%s\n", h.path, desc.Digest) } return h.printer.Println(desc.Digest.String()) } oras-1.3.3/cmd/oras/internal/display/metadata/text/resolve_test.go000066400000000000000000000031731522423464100252720ustar00rootroot00000000000000/* Copyright The ORAS 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 text import ( "bytes" "testing" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/output" ) func TestResolveHandle_onResolved(t *testing.T) { var buf bytes.Buffer printer := output.NewPrinter(&buf, &buf) tests := []struct { name string fullRef bool path string expected string }{ { name: "full reference output", fullRef: true, path: "localhost:5000/test", expected: "localhost:5000/test@sha256:abcd1234\n", }, { name: "digest only output", fullRef: false, path: "localhost:5000/test", expected: "sha256:abcd1234\n", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { buf.Reset() handler := NewResolveHandler(printer, tt.fullRef, tt.path) desc := ocispec.Descriptor{ Digest: "sha256:abcd1234", } err := handler.OnResolved(desc) if err != nil { t.Fatalf("OnResolved() error = %v", err) } got := buf.String() if got != tt.expected { t.Errorf("OnResolved() output = %q, want %q", got, tt.expected) } }) } } oras-1.3.3/cmd/oras/internal/display/metadata/text/restore.go000066400000000000000000000053311522423464100242350ustar00rootroot00000000000000/* Copyright The ORAS 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 text import ( "strings" "time" "oras.land/oras/cmd/oras/internal/display/status/progress/humanize" "oras.land/oras/cmd/oras/internal/output" ) // RestoreHandler handles text metadata output for restore command. type RestoreHandler struct { printer *output.Printer dryRun bool } // NewRestoreHandler creates a new RestoreHandler. func NewRestoreHandler(printer *output.Printer, dryRun bool) *RestoreHandler { return &RestoreHandler{ printer: printer, dryRun: dryRun, } } // OnTarLoaded implements metadata.RestoreHandler. func (rh *RestoreHandler) OnTarLoaded(path string, size int64) error { return rh.printer.Printf("Loaded backup archive: %s (%s)\n", path, humanize.ToBytes(size)) } // OnTagsFound implements metadata.RestoreHandler. func (rh *RestoreHandler) OnTagsFound(tags []string) error { if len(tags) == 0 { return rh.printer.Printf("No tags found in the backup\n") } if len(tags) <= 5 { // print small number of tags in one line return rh.printer.Printf("Found %d tag(s) in the backup: %s\n", len(tags), strings.Join(tags, ", ")) } // print large number of tags line by line if err := rh.printer.Printf("Found %d tag(s) in the backup:\n", len(tags)); err != nil { return err } for _, tag := range tags { if err := rh.printer.Println(tag); err != nil { return err } } return nil } // OnArtifactPushed implements metadata.RestoreHandler. func (rh *RestoreHandler) OnArtifactPushed(tag string, referrerCount int) error { if rh.dryRun { return rh.printer.Printf("Dry run: would push tag %s with %d referrer(s)\n", tag, referrerCount) } return rh.printer.Printf("Pushed tag %s with %d referrer(s)\n", tag, referrerCount) } // OnRestoreCompleted implements metadata.RestoreHandler. func (rh *RestoreHandler) OnRestoreCompleted(tagsCount int, repo string, duration time.Duration) error { if rh.dryRun { return rh.printer.Printf("Dry run complete: %d tag(s) would be restored to %q (no data pushed)\n", tagsCount, repo) } return rh.printer.Printf("Successfully restored %d tag(s) to %q in %s\n", tagsCount, repo, humanize.FormatDuration(duration)) } // Render implements metadata.RestoreHandler. func (rh *RestoreHandler) Render() error { return nil } oras-1.3.3/cmd/oras/internal/display/metadata/text/restore_test.go000066400000000000000000000155111522423464100252750ustar00rootroot00000000000000/* Copyright The ORAS 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 text import ( "bytes" "fmt" "io" "os" "testing" "time" "oras.land/oras/cmd/oras/internal/output" ) // TestNewRestoreHandler tests the constructor for RestoreHandler func TestNewRestoreHandler(t *testing.T) { tests := []struct { name string dryRun bool }{ { name: "with dryRun false", dryRun: false, }, { name: "with dryRun true", dryRun: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { printer := output.NewPrinter(&bytes.Buffer{}, &bytes.Buffer{}) handler := NewRestoreHandler(printer, tt.dryRun) if handler.printer != printer { t.Errorf("expected handler.printer to be %v, got %v", printer, handler.printer) } if handler.dryRun != tt.dryRun { t.Errorf("expected handler.dryRun to be %v, got %v", tt.dryRun, handler.dryRun) } }) } } func TestRestoreHandler_OnTarLoaded(t *testing.T) { path := "backup.tar" tests := []struct { name string out io.Writer size int64 wantErr bool want string }{ { name: "good path with 1MiB size", out: &bytes.Buffer{}, size: int64(1024 * 1024), wantErr: false, want: fmt.Sprintf("Loaded backup archive: %s (1 MB)\n", path), }, { name: "good path with 0 size", out: &bytes.Buffer{}, size: 0, wantErr: false, want: fmt.Sprintf("Loaded backup archive: %s (0 B)\n", path), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { printer := output.NewPrinter(tt.out, os.Stderr) handler := NewRestoreHandler(printer, false) if err := handler.OnTarLoaded(path, tt.size); (err != nil) != tt.wantErr { t.Errorf("OnTarLoaded() error = %v, wantErr %v", err, tt.wantErr) } if !tt.wantErr { got := tt.out.(*bytes.Buffer).String() if got != tt.want { t.Errorf("OnTarLoaded() got = %v, want %v", got, tt.want) } } }) } } func TestRestoreHandler_OnTagsFound(t *testing.T) { tests := []struct { name string tags []string out io.Writer wantErr bool want string }{ { name: "good path with a few tags", tags: []string{"v1.0", "latest", "stable"}, out: &bytes.Buffer{}, wantErr: false, want: "Found 3 tag(s) in the backup: v1.0, latest, stable\n", }, { name: "good path with exactly 5 tags", tags: []string{"v1.0", "v2.0", "latest", "stable", "beta"}, out: &bytes.Buffer{}, wantErr: false, want: "Found 5 tag(s) in the backup: v1.0, v2.0, latest, stable, beta\n", }, { name: "good path with more than 5 tags", tags: []string{"v1.0", "v2.0", "v3.0", "latest", "stable", "beta", "dev"}, out: &bytes.Buffer{}, wantErr: false, want: "Found 7 tag(s) in the backup:\nv1.0\nv2.0\nv3.0\nlatest\nstable\nbeta\ndev\n", }, { name: "good path with one tag", tags: []string{"latest"}, out: &bytes.Buffer{}, wantErr: false, want: "Found 1 tag(s) in the backup: latest\n", }, { name: "good path with no tags", tags: []string{}, out: &bytes.Buffer{}, wantErr: false, want: "No tags found in the backup\n", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { printer := output.NewPrinter(tt.out, os.Stderr) handler := NewRestoreHandler(printer, false) if err := handler.OnTagsFound(tt.tags); (err != nil) != tt.wantErr { t.Errorf("OnTagsFound() error = %v, wantErr %v", err, tt.wantErr) } if !tt.wantErr { got := tt.out.(*bytes.Buffer).String() if got != tt.want { t.Errorf("OnTagsFound() got = %v, want %v", got, tt.want) } } }) } } func TestRestoreHandler_OnArtifactPushed(t *testing.T) { tag := "latest" referrerCount := 2 tests := []struct { name string dryRun bool out io.Writer wantErr bool want string }{ { name: "normal push", dryRun: false, out: &bytes.Buffer{}, wantErr: false, want: fmt.Sprintf("Pushed tag %s with %d referrer(s)\n", tag, referrerCount), }, { name: "dry run", dryRun: true, out: &bytes.Buffer{}, wantErr: false, want: fmt.Sprintf("Dry run: would push tag %s with %d referrer(s)\n", tag, referrerCount), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { printer := output.NewPrinter(tt.out, os.Stderr) handler := NewRestoreHandler(printer, tt.dryRun) if err := handler.OnArtifactPushed(tag, referrerCount); (err != nil) != tt.wantErr { t.Errorf("OnArtifactPushed() error = %v, wantErr %v", err, tt.wantErr) } if !tt.wantErr { got := tt.out.(*bytes.Buffer).String() if got != tt.want { t.Errorf("OnArtifactPushed() got = %v, want %v", got, tt.want) } } }) } } func TestRestoreHandler_OnRestoreCompleted(t *testing.T) { tagsCount := 5 repo := "example.com/myrepo" duration := time.Second * 3 tests := []struct { name string dryRun bool out io.Writer wantErr bool want string }{ { name: "normal completion", dryRun: false, out: &bytes.Buffer{}, wantErr: false, want: fmt.Sprintf("Successfully restored %d tag(s) to %q in %s\n", tagsCount, repo, "3s"), }, { name: "dry run completion", dryRun: true, out: &bytes.Buffer{}, wantErr: false, want: fmt.Sprintf("Dry run complete: %d tag(s) would be restored to %q (no data pushed)\n", tagsCount, repo), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { printer := output.NewPrinter(tt.out, os.Stderr) handler := NewRestoreHandler(printer, tt.dryRun) if err := handler.OnRestoreCompleted(tagsCount, repo, duration); (err != nil) != tt.wantErr { t.Errorf("OnRestoreCompleted() error = %v, wantErr %v", err, tt.wantErr) } if !tt.wantErr { got := tt.out.(*bytes.Buffer).String() if got != tt.want { t.Errorf("OnRestoreCompleted() got = %v, want %v", got, tt.want) } } }) } } func TestRestoreHandler_Render(t *testing.T) { tests := []struct { name string wantErr bool }{ { name: "good path", wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { handler := &RestoreHandler{ printer: output.NewPrinter(&bytes.Buffer{}, &bytes.Buffer{}), dryRun: false, } if err := handler.Render(); (err != nil) != tt.wantErr { t.Errorf("Render() error = %v, wantErr %v", err, tt.wantErr) } }) } } oras-1.3.3/cmd/oras/internal/display/metadata/text/tag.go000066400000000000000000000031561522423464100233300ustar00rootroot00000000000000/* Copyright The ORAS 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 text import ( "fmt" "sync" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/cmd/oras/internal/output" ) // TagHandler handles text metadata output for tag events. type TagHandler struct { printer *output.Printer printOnce sync.Once refPrefix string } // NewTagHandler returns a new handler for tag events. func NewTagHandler(printer *output.Printer, target option.Target) metadata.TagHandler { return &TagHandler{ printer: printer, refPrefix: fmt.Sprintf("[%s] %s", target.Type, target.Path), } } // OnTagging is called when the tagging is complete. func (ah *TagHandler) OnTagging(desc ocispec.Descriptor, _ string) (err error) { ah.printOnce.Do(func() { ref := ah.refPrefix + "@" + desc.Digest.String() err = ah.printer.Println("Tagging", ref) }) return err } // OnTagged is called when the tagging is complete. func (ah *TagHandler) OnTagged(_ ocispec.Descriptor, tag string) error { return ah.printer.Println("Tagged", tag) } oras-1.3.3/cmd/oras/internal/display/metadata/text/tag_test.go000066400000000000000000000101601522423464100243600ustar00rootroot00000000000000/* Copyright The ORAS 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 text import ( "bytes" "fmt" "os" "testing" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/cmd/oras/internal/output" ) func TestNewTagHandler(t *testing.T) { buf := &bytes.Buffer{} printer := output.NewPrinter(buf, os.Stderr) target := option.Target{ Type: "registry", Path: "localhost:5000/test", } handler := NewTagHandler(printer, target) if handler == nil { t.Fatal("NewTagHandler should not return nil") } th, ok := handler.(*TagHandler) if !ok { t.Fatal("NewTagHandler should return *TagHandler") } if th.printer != printer { t.Error("printer not set correctly") } expectedPrefix := fmt.Sprintf("[%s] %s", target.Type, target.Path) if th.refPrefix != expectedPrefix { t.Errorf("refPrefix = %q, want %q", th.refPrefix, expectedPrefix) } } func TestTagHandler_OnTagging(t *testing.T) { testContent := []byte("hello world") testDesc := ocispec.Descriptor{ MediaType: "application/vnd.oci.image.manifest.v1+json", Digest: digest.FromBytes(testContent), Size: int64(len(testContent)), } buf := &bytes.Buffer{} printer := output.NewPrinter(buf, os.Stderr) handler := &TagHandler{ printer: printer, refPrefix: "[registry] localhost:5000/test", } err := handler.OnTagging(testDesc, "v1.0") if err != nil { t.Errorf("OnTagging failed: %v", err) } expected := "Tagging [registry] localhost:5000/test@" + testDesc.Digest.String() + "\n" if buf.String() != expected { t.Errorf("output = %q, want %q", buf.String(), expected) } // Second call shouldn't print anything (printOnce) buf.Reset() err = handler.OnTagging(testDesc, "v2.0") if err != nil { t.Errorf("second OnTagging failed: %v", err) } if buf.Len() > 0 { t.Error("OnTagging should only print once") } } func TestTagHandler_OnTagging_WithError(t *testing.T) { testDesc := ocispec.Descriptor{ MediaType: "application/vnd.oci.image.manifest.v1+json", Digest: digest.FromBytes([]byte("test")), Size: 4, } printer := output.NewPrinter(&errorWriter{}, os.Stderr) handler := &TagHandler{ printer: printer, refPrefix: "[registry] localhost:5000/test", } err := handler.OnTagging(testDesc, "v1.0") if err == nil { t.Error("OnTagging should return error when printer fails") } } func TestTagHandler_OnTagged(t *testing.T) { tests := []struct { name string tag string }{ {"latest tag", "latest"}, {"version tag", "v1.2.3"}, {"custom tag", "production-2024"}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { buf := &bytes.Buffer{} printer := output.NewPrinter(buf, os.Stderr) handler := &TagHandler{ printer: printer, refPrefix: "[registry] localhost:5000/test", } err := handler.OnTagged(ocispec.Descriptor{}, tc.tag) if err != nil { t.Errorf("OnTagged failed: %v", err) } expected := "Tagged " + tc.tag + "\n" if buf.String() != expected { t.Errorf("output = %q, want %q", buf.String(), expected) } }) } } func TestTagHandler_OnTagged_WithError(t *testing.T) { printer := output.NewPrinter(&errorWriter{}, os.Stderr) handler := &TagHandler{ printer: printer, refPrefix: "[registry] localhost:5000/test", } err := handler.OnTagged(ocispec.Descriptor{}, "test") if err == nil { t.Error("OnTagged should return error when printer fails") } } func TestTagHandler_InterfaceImplementation(_ *testing.T) { // Verify that TagHandler implements metadata.TagHandler var _ metadata.TagHandler = (*TagHandler)(nil) } oras-1.3.3/cmd/oras/internal/display/metadata/tree/000077500000000000000000000000001522423464100221745ustar00rootroot00000000000000oras-1.3.3/cmd/oras/internal/display/metadata/tree/discover.go000066400000000000000000000056451522423464100243530ustar00rootroot00000000000000/* Copyright The ORAS 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 tree import ( "fmt" "io" "os" "strings" "github.com/morikuni/aec" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "go.yaml.in/yaml/v4" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/internal/tree" ) var ( artifactTypeColor = aec.LightYellowF digestColor = aec.GreenF annotationsColor = aec.LightBlackF ) // discoverHandler handles json metadata output for discover events. type discoverHandler struct { out io.Writer path string root *tree.Node nodes map[digest.Digest]*tree.Node verbose bool tty *os.File } // NewDiscoverHandler creates a new handler for discover events. func NewDiscoverHandler(out io.Writer, path string, root ocispec.Descriptor, verbose bool, tty *os.File) metadata.DiscoverHandler { rootDigest := fmt.Sprintf("%s@%s", path, root.Digest) if tty != nil { rootDigest = digestColor.Apply(rootDigest) } treeRoot := tree.New(rootDigest) return &discoverHandler{ out: out, path: path, root: treeRoot, nodes: map[digest.Digest]*tree.Node{ root.Digest: treeRoot, }, verbose: verbose, tty: tty, } } // OnDiscovered implements metadata.DiscoverHandler. func (h *discoverHandler) OnDiscovered(referrer, subject ocispec.Descriptor) error { node, ok := h.nodes[subject.Digest] if !ok { return fmt.Errorf("unexpected subject descriptor: %v", subject) } // add artifact type and digest to the referrer artifactType := referrer.ArtifactType if artifactType == "" { artifactType = "" } dgst := referrer.Digest.String() if h.tty != nil { artifactType = artifactTypeColor.Apply(artifactType) dgst = digestColor.Apply(dgst) } referrerNode := node.AddPath(artifactType, dgst) // add annotations to the referrer if h.verbose && len(referrer.Annotations) > 0 { annotationsTitle := "[annotations]" if h.tty != nil { annotationsTitle = annotationsColor.Apply(annotationsTitle) } annotationsNode := referrerNode.Add(annotationsTitle) for k, v := range referrer.Annotations { bytes, err := yaml.Marshal(map[string]string{k: v}) if err != nil { return err } annotationsNode.AddPath(strings.TrimSpace(string(bytes))) } } h.nodes[referrer.Digest] = referrerNode return nil } // Render implements metadata.DiscoverHandler. func (h *discoverHandler) Render() error { return tree.NewPrinter(h.out).Print(h.root) } oras-1.3.3/cmd/oras/internal/display/metadata/tree/discover_test.go000066400000000000000000000077261522423464100254140ustar00rootroot00000000000000/* Copyright The ORAS 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 tree import ( "bytes" "fmt" "os" "strings" "testing" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) func TestDiscoverHandler_OnDiscovered(t *testing.T) { path := "localhost:5000/test" subjectDesc := ocispec.Descriptor{ MediaType: "application/vnd.oci.image.manifest.v1+json", Digest: "sha256:9d16f5505246424aed7116cb21216704ba8c919997d0f1f37e154c11d509e1d2", Size: 529, } referrerDesc := ocispec.Descriptor{ MediaType: "application/vnd.oci.image.manifest.v1+json", Digest: digest.Digest("sha256:e2c6633a79985906f1ed55c592718c73c41e809fb9818de232a635904a74d48d"), Size: 660, Annotations: map[string]string{ "org.opencontainers.image.created": "2023-01-18T08:37:42Z", }, ArtifactType: "test/sbom.file", } coloredRoot := digestColor.Apply(fmt.Sprintf("%s@%s", path, subjectDesc.Digest)) coloredArtifactType := artifactTypeColor.Apply(referrerDesc.ArtifactType) coloredDigest := digestColor.Apply(referrerDesc.Digest.String()) coloredAnnotations := annotationsColor.Apply("[annotations]") t.Run("WithTTY", func(t *testing.T) { var buf bytes.Buffer // create a temp file to mock TTY tmp, err := os.CreateTemp(t.TempDir(), "test-tty") if err != nil { t.Fatalf("Failed to create temporary file: %v", err) } defer func() { _ = tmp.Close() }() h := NewDiscoverHandler(&buf, path, subjectDesc, true, tmp) if err := h.OnDiscovered(referrerDesc, subjectDesc); err != nil { t.Fatalf("OnDiscovered() error = %v", err) } // when rendered to a buffer, the node content should include colors if err := h.Render(); err != nil { t.Fatalf("Failed to render tree: %v", err) } output := buf.String() // verify root if !strings.Contains(output, coloredRoot) { t.Errorf("expected root output contains %v, got: %v", coloredRoot, output) } // verify artifact type if !strings.Contains(output, coloredArtifactType) { t.Errorf("expected artifact type output contains %v, got: %v", coloredArtifactType, output) } // verify digest if !strings.Contains(output, coloredDigest) { t.Errorf("expected digest output contains %v, got: %v", coloredDigest, output) } // verify annotations if !strings.Contains(output, coloredAnnotations) { t.Errorf("expected annotations output contains %v, got: %v", coloredAnnotations, output) } }) t.Run("WithoutTTY", func(t *testing.T) { var buf bytes.Buffer h := NewDiscoverHandler(&buf, path, subjectDesc, true, nil) if err := h.OnDiscovered(referrerDesc, subjectDesc); err != nil { t.Fatalf("OnDiscovered() error = %v", err) } // when rendered to a buffer, the node content should not include colors if err := h.Render(); err != nil { t.Fatalf("Failed to render tree: %v", err) } output := buf.String() // verify root if strings.Contains(output, coloredRoot) { t.Errorf("expected root output not contains %v, got: %v", coloredRoot, output) } // verify artifact type if strings.Contains(output, coloredArtifactType) { t.Errorf("expected artifact type output not contains %v, got: %v", coloredArtifactType, output) } // verify digest if strings.Contains(output, coloredDigest) { t.Errorf("expected digest output not contains %v, got: %v", coloredDigest, output) } // verify annotations if strings.Contains(output, coloredAnnotations) { t.Errorf("expected annotations output not contains %v, got: %v", coloredAnnotations, output) } }) } oras-1.3.3/cmd/oras/internal/display/status/000077500000000000000000000000001522423464100210005ustar00rootroot00000000000000oras-1.3.3/cmd/oras/internal/display/status/console/000077500000000000000000000000001522423464100224425ustar00rootroot00000000000000oras-1.3.3/cmd/oras/internal/display/status/console/console.go000066400000000000000000000054551522423464100244440ustar00rootroot00000000000000/* Copyright The ORAS 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 console import ( "os" containerd "github.com/containerd/console" "github.com/morikuni/aec" ) const ( // MinWidth is the minimal width of supported console. MinWidth = 80 // MinHeight is the minimal height of supported console. MinHeight = 10 // Save cannot use aec.Save since DEC has better compatibility than SCO Save = "\0337" // Restore cannot use aec.Restore since DEC has better compatibility than SCO Restore = "\0338" ) // Console is a wrapper around containerd's Console and ANSI escape codes. type Console interface { containerd.Console GetHeightWidth() (height, width int) Save() NewRow() OutputTo(upCnt uint, str string) Restore() } type console struct { containerd.Console } // NewConsole generates a console from a file. func NewConsole(f *os.File) (Console, error) { c, err := containerd.ConsoleFromFile(f) if err != nil { return nil, err } return &console{c}, nil } // GetHeightWidth returns the width and height of the console. // If the console size cannot be determined, returns a default value of 80x10. func (c *console) GetHeightWidth() (height, width int) { windowSize, err := c.Size() if err != nil { return MinHeight, MinWidth } if windowSize.Height < MinHeight { windowSize.Height = MinHeight } if windowSize.Width < MinWidth { windowSize.Width = MinWidth } return int(windowSize.Height), int(windowSize.Width) } // Save saves the current cursor position. func (c *console) Save() { _, _ = c.Write([]byte(aec.Hide.Apply(Save))) } // NewRow allocates a horizontal space to the output area with scroll if needed. func (c *console) NewRow() { _, _ = c.Write([]byte(Restore)) _, _ = c.Write([]byte("\n")) _, _ = c.Write([]byte(Save)) } // OutputTo outputs a string to a specific line. func (c *console) OutputTo(upCnt uint, str string) { _, _ = c.Write([]byte(Restore)) _, _ = c.Write([]byte(aec.PreviousLine(upCnt).Apply(str))) _, _ = c.Write([]byte("\n")) _, _ = c.Write([]byte(aec.EraseLine(aec.EraseModes.Tail).String())) } // Restore restores the saved cursor position. func (c *console) Restore() { // cannot use aec.Restore since DEC has better compatibility than SCO _, _ = c.Write([]byte(Restore)) _, _ = c.Write([]byte(aec.Column(0). With(aec.EraseLine(aec.EraseModes.All)). With(aec.Show).String())) } oras-1.3.3/cmd/oras/internal/display/status/console/console_test.go000066400000000000000000000063351522423464100255010ustar00rootroot00000000000000//go:build !windows && !darwin /* Copyright The ORAS 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 console import ( "os" "testing" containerd "github.com/containerd/console" "oras.land/oras/internal/testutils" ) func givenConsole(t *testing.T) (c Console, pty containerd.Console) { t.Helper() pty, _, err := containerd.NewPty() if err != nil { t.Fatal(err) } c = &console{ Console: pty, } return c, pty } func givenTestConsole(t *testing.T) (c Console, pty containerd.Console, tty *os.File) { t.Helper() var err error pty, tty, err = testutils.NewPty() if err != nil { t.Fatal(err) } c, err = NewConsole(tty) if err != nil { t.Fatal(err) } return } func validateSize(t *testing.T, gotWidth, gotHeight, wantWidth, wantHeight int) { t.Helper() if gotWidth != wantWidth { t.Errorf("Console.Size() gotWidth = %v, want %v", gotWidth, wantWidth) } if gotHeight != wantHeight { t.Errorf("Console.Size() gotHeight = %v, want %v", gotHeight, wantHeight) } } func TestNewConsole(t *testing.T) { _, err := NewConsole(os.Stdin) if err == nil { t.Error("expected error creating bogus console") } } func TestConsole_GetHeightWidth(t *testing.T) { c, pty := givenConsole(t) // minimal width and height gotHeight, gotWidth := c.GetHeightWidth() validateSize(t, gotWidth, gotHeight, MinWidth, MinHeight) // zero width _ = pty.Resize(containerd.WinSize{Width: 0, Height: MinHeight}) gotHeight, gotWidth = c.GetHeightWidth() validateSize(t, gotWidth, gotHeight, MinWidth, MinHeight) // zero height _ = pty.Resize(containerd.WinSize{Width: MinWidth, Height: 0}) gotHeight, gotWidth = c.GetHeightWidth() validateSize(t, gotWidth, gotHeight, MinWidth, MinHeight) // valid zero and height _ = pty.Resize(containerd.WinSize{Width: 200, Height: 100}) gotHeight, gotWidth = c.GetHeightWidth() validateSize(t, gotWidth, gotHeight, 200, 100) } func TestConsole_NewRow(t *testing.T) { c, pty, tty := givenTestConsole(t) c.NewRow() err := testutils.MatchPty(pty, tty, "\x1b8\r\n\x1b7") if err != nil { t.Fatalf("NewRow output error: %v", err) } } func TestConsole_OutputTo(t *testing.T) { c, pty, tty := givenTestConsole(t) c.OutputTo(1, "test string") err := testutils.MatchPty(pty, tty, "\x1b8\x1b[1Ftest string\x1b[0m\r\n\x1b[0K") if err != nil { t.Fatalf("OutputTo output error: %v", err) } } func TestConsole_Restore(t *testing.T) { c, pty, tty := givenTestConsole(t) c.Restore() err := testutils.MatchPty(pty, tty, "\x1b8\x1b[0G\x1b[2K\x1b[?25h") if err != nil { t.Fatalf("Restore output error: %v", err) } } func TestConsole_Save(t *testing.T) { c, pty, tty := givenTestConsole(t) c.Save() err := testutils.MatchPty(pty, tty, "\x1b[?25l\x1b7\x1b[0m") if err != nil { t.Fatalf("Save output error: %v", err) } } oras-1.3.3/cmd/oras/internal/display/status/discard.go000066400000000000000000000104351522423464100227430ustar00rootroot00000000000000/* Copyright The ORAS 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 status import ( "context" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2" ) func discardStopTrack() error { return nil } // DiscardHandler is a no-op handler that discards all status updates. type DiscardHandler struct{} // NewDiscardHandler returns a new no-op handler. func NewDiscardHandler() DiscardHandler { return DiscardHandler{} } // OnFileLoading is called before a file is being loaded. func (DiscardHandler) OnFileLoading(string) error { return nil } // OnEmptyArtifact is called when no file is loaded for an artifact push. func (DiscardHandler) OnEmptyArtifact() error { return nil } // TrackTarget returns a target with status tracking. func (DiscardHandler) TrackTarget(gt oras.GraphTarget) (oras.GraphTarget, StopTrackTargetFunc, error) { return gt, discardStopTrack, nil } // OnCopySkipped is called when an object already exists. func (DiscardHandler) OnCopySkipped(_ context.Context, _ ocispec.Descriptor) error { return nil } // PreCopy implements PreCopy of CopyHandler. func (DiscardHandler) PreCopy(_ context.Context, _ ocispec.Descriptor) error { return nil } // PostCopy implements PostCopy of CopyHandler. func (DiscardHandler) PostCopy(_ context.Context, _ ocispec.Descriptor) error { return nil } // OnNodeDownloading implements PullHandler. func (DiscardHandler) OnNodeDownloading(_ ocispec.Descriptor) error { return nil } // OnNodeDownloaded implements PullHandler. func (DiscardHandler) OnNodeDownloaded(_ ocispec.Descriptor) error { return nil } // OnNodeRestored implements PullHandler. func (DiscardHandler) OnNodeRestored(_ ocispec.Descriptor) error { return nil } // OnNodeProcessing implements PullHandler. func (DiscardHandler) OnNodeProcessing(_ ocispec.Descriptor) error { return nil } // OnNodeProcessing implements PullHandler. func (DiscardHandler) OnNodeSkipped(_ ocispec.Descriptor) error { return nil } // OnFetching implements referenceFetchHandler. func (DiscardHandler) OnFetching(string) error { return nil } // OnFetched implements referenceFetchHandler. func (DiscardHandler) OnFetched(string, ocispec.Descriptor) error { return nil } // OnManifestPushSkipped implements ManifestPushHandler. func (DiscardHandler) OnManifestPushSkipped() error { return nil } // OnManifestPushing implements ManifestPushHandler. func (DiscardHandler) OnManifestPushing() error { return nil } // OnManifestPushed implements ManifestPushHandler. func (DiscardHandler) OnManifestPushed() error { return nil } // OnManifestRemoved implements ManifestIndexUpdateHandler. func (DiscardHandler) OnManifestRemoved(digest.Digest) error { return nil } // OnManifestAdded implements ManifestIndexUpdateHandler. func (DiscardHandler) OnManifestAdded(string, ocispec.Descriptor) error { return nil } // OnIndexMerged implements ManifestIndexUpdateHandler. func (DiscardHandler) OnIndexMerged(string, ocispec.Descriptor) error { return nil } // OnIndexPacked implements ManifestIndexCreateHandler. func (DiscardHandler) OnIndexPacked(ocispec.Descriptor) error { return nil } // OnIndexPushed implements ManifestIndexCreateHandler. func (DiscardHandler) OnIndexPushed(string) error { return nil } // OnBlobExists implements BlobPushHandler. func (DiscardHandler) OnBlobExists() error { return nil } // OnBlobUploading implements BlobPushHandler. func (DiscardHandler) OnBlobUploading() error { return nil } // OnBlobUploaded implements BlobPushHandler. func (DiscardHandler) OnBlobUploaded() error { return nil } // StartTracking implements BlobPushHandler. func (DiscardHandler) StartTracking(gt oras.GraphTarget) (oras.GraphTarget, error) { return gt, nil } // StopTracking implements BlobPushHandler. func (DiscardHandler) StopTracking() error { return nil } oras-1.3.3/cmd/oras/internal/display/status/discard_test.go000066400000000000000000000031771522423464100240070ustar00rootroot00000000000000/* Copyright The ORAS 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 status import ( "testing" v1 "github.com/opencontainers/image-spec/specs-go/v1" ) func TestDiscardHandler_OnPushSkipped(t *testing.T) { testDiscard := NewDiscardHandler() if err := testDiscard.OnManifestPushSkipped(); err != nil { t.Errorf("DiscardHandler.OnPushSkipped() error = %v, wantErr nil", err) } } func TestDiscardHandler_OnManifestRemoved(t *testing.T) { testDiscard := NewDiscardHandler() if err := testDiscard.OnManifestRemoved("sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"); err != nil { t.Errorf("DiscardHandler.OnManifestRemoved() error = %v, wantErr nil", err) } } func TestDiscardHandler_OnIndexMerged(t *testing.T) { testDiscard := NewDiscardHandler() if err := testDiscard.OnIndexMerged("test", v1.Descriptor{}); err != nil { t.Errorf("DiscardHandler.OnIndexMerged() error = %v, wantErr nil", err) } } func TestDiscardHandler_OnIndexPushed(t *testing.T) { testDiscard := NewDiscardHandler() if err := testDiscard.OnIndexPushed("test"); err != nil { t.Errorf("DiscardHandler.OnIndexPushed() error = %v, wantErr nil", err) } } oras-1.3.3/cmd/oras/internal/display/status/interface.go000066400000000000000000000104741522423464100232750ustar00rootroot00000000000000/* Copyright The ORAS 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 status import ( "context" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2" ) // StopTrackTargetFunc is the function type to stop tracking a target. type StopTrackTargetFunc func() error // PushHandler handles status output for push command. type PushHandler interface { OnFileLoading(name string) error OnEmptyArtifact() error TrackTarget(gt oras.GraphTarget) (oras.GraphTarget, StopTrackTargetFunc, error) OnCopySkipped(ctx context.Context, desc ocispec.Descriptor) error PreCopy(ctx context.Context, desc ocispec.Descriptor) error PostCopy(ctx context.Context, desc ocispec.Descriptor) error } // AttachHandler handles text status output for attach command. type AttachHandler PushHandler // PullHandler handles status output for pull command. type PullHandler interface { // TrackTarget returns a tracked target. // If no TTY is available, it returns the original target. TrackTarget(gt oras.GraphTarget) (oras.GraphTarget, StopTrackTargetFunc, error) // OnNodeProcessing is called when processing a manifest. OnNodeProcessing(desc ocispec.Descriptor) error // OnNodeDownloading is called before downloading a node. OnNodeDownloading(desc ocispec.Descriptor) error // OnNodeDownloaded is called after a node is downloaded. OnNodeDownloaded(desc ocispec.Descriptor) error // OnNodeRestored is called after a deduplicated node is restored. OnNodeRestored(desc ocispec.Descriptor) error // OnNodeSkipped is called when a node is skipped. OnNodeSkipped(desc ocispec.Descriptor) error } // CopyHandler handles status output for cp command. type CopyHandler interface { OnCopySkipped(ctx context.Context, desc ocispec.Descriptor) error PreCopy(ctx context.Context, desc ocispec.Descriptor) error PostCopy(ctx context.Context, desc ocispec.Descriptor) error OnMounted(ctx context.Context, desc ocispec.Descriptor) error StartTracking(gt oras.GraphTarget) (oras.GraphTarget, error) StopTracking() error } // BackupHandler handles status output for backup command. type BackupHandler interface { StartTracking(gt oras.GraphTarget) (oras.GraphTarget, error) StopTracking() error OnCopySkipped(ctx context.Context, desc ocispec.Descriptor) error PreCopy(ctx context.Context, desc ocispec.Descriptor) error PostCopy(ctx context.Context, desc ocispec.Descriptor) error } // RestoreHandler handles status output for restore command. type RestoreHandler interface { StartTracking(gt oras.GraphTarget) (oras.GraphTarget, error) StopTracking() error OnCopySkipped(ctx context.Context, desc ocispec.Descriptor) error PreCopy(ctx context.Context, desc ocispec.Descriptor) error PostCopy(ctx context.Context, desc ocispec.Descriptor) error } // ManifestPushHandler handles status output for manifest push command. type ManifestPushHandler interface { OnManifestPushSkipped() error OnManifestPushing() error OnManifestPushed() error } // ManifestIndexCreateHandler handles status output for manifest index create command. type ManifestIndexCreateHandler interface { OnFetching(manifestRef string) error OnFetched(manifestRef string, desc ocispec.Descriptor) error OnIndexPacked(desc ocispec.Descriptor) error OnIndexPushed(path string) error } // ManifestIndexUpdateHandler handles status output for manifest index update command. type ManifestIndexUpdateHandler interface { ManifestIndexCreateHandler OnManifestRemoved(digest digest.Digest) error OnManifestAdded(manifestRef string, desc ocispec.Descriptor) error OnIndexMerged(indexRef string, desc ocispec.Descriptor) error } // BlobPushHandler handles status output for blob push command. type BlobPushHandler interface { OnBlobExists() error OnBlobUploading() error OnBlobUploaded() error StartTracking(gt oras.GraphTarget) (oras.GraphTarget, error) StopTracking() error } oras-1.3.3/cmd/oras/internal/display/status/progress/000077500000000000000000000000001522423464100226445ustar00rootroot00000000000000oras-1.3.3/cmd/oras/internal/display/status/progress/humanize/000077500000000000000000000000001522423464100244645ustar00rootroot00000000000000oras-1.3.3/cmd/oras/internal/display/status/progress/humanize/bytes.go000066400000000000000000000027101522423464100261410ustar00rootroot00000000000000/* Copyright The ORAS 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 humanize import ( "fmt" "math" ) const base = 1024.0 var units = []string{"B", "KB", "MB", "GB", "TB"} type Bytes struct { Size float64 Unit string } // ToBytes converts size in bytes to human readable format. func ToBytes(sizeInBytes int64) Bytes { f := float64(sizeInBytes) if f < base { return Bytes{f, units[0]} } e := int(math.Floor(math.Log(f) / math.Log(base))) if e >= len(units) { // only support up to TB e = len(units) - 1 } p := f / math.Pow(base, float64(e)) return Bytes{RoundTo(p), units[e]} } // String returns the string representation of Bytes. func (b Bytes) String() string { return fmt.Sprintf("%g %2s", b.Size, b.Unit) } // RoundTo makes length of the size string to less than or equal to 4. func RoundTo(size float64) float64 { if size < 10 { return math.Round(size*100) / 100 } else if size < 100 { return math.Round(size*10) / 10 } return math.Round(size) } oras-1.3.3/cmd/oras/internal/display/status/progress/humanize/bytes_test.go000066400000000000000000000036021522423464100272010ustar00rootroot00000000000000/* Copyright The ORAS 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 humanize import ( "reflect" "testing" ) func TestRoundTo(t *testing.T) { type args struct { quantity float64 } tests := []struct { name string args args want float64 }{ {"round to 2 digit", args{1.223}, 1.22}, {"round to 1 digit", args{12.23}, 12.2}, {"round to no digit", args{122.6}, 123}, {"round to no digit", args{1223.123}, 1223}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := RoundTo(tt.args.quantity); got != tt.want { t.Errorf("RoundTo() = %v, want %v", got, tt.want) } }) } } func TestToBytes(t *testing.T) { type args struct { sizeInBytes int64 } tests := []struct { name string args args want Bytes }{ {"0 bytes", args{0}, Bytes{0, "B"}}, {"1023 bytes", args{1023}, Bytes{1023, "B"}}, {"1 KB", args{1024}, Bytes{1, "KB"}}, {"1.5 KB", args{1024 + 512}, Bytes{1.5, "KB"}}, {"12.5 KB", args{1024 * 12.5}, Bytes{12.5, "KB"}}, {"512.5 KB", args{1024 * 512.5}, Bytes{513, "KB"}}, {"1 MB", args{1024 * 1024}, Bytes{1, "MB"}}, {"1 GB", args{1024 * 1024 * 1024}, Bytes{1, "GB"}}, {"1 TB", args{1024 * 1024 * 1024 * 1024}, Bytes{1, "TB"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := ToBytes(tt.args.sizeInBytes); !reflect.DeepEqual(got, tt.want) { t.Errorf("ToBytes() = %v, want %v", got, tt.want) } }) } } oras-1.3.3/cmd/oras/internal/display/status/progress/humanize/duration.go000066400000000000000000000017371522423464100266500ustar00rootroot00000000000000/* Copyright The ORAS 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 humanize import "time" // FormatDuration formats a duration into a human-readable string. // It rounds the duration to the nearest second, millisecond, or microsecond // depending on its value. func FormatDuration(d time.Duration) string { switch { case d > time.Second: d = d.Round(time.Second) case d > time.Millisecond: d = d.Round(time.Millisecond) default: d = d.Round(time.Microsecond) } return d.String() } oras-1.3.3/cmd/oras/internal/display/status/progress/humanize/duration_test.go000066400000000000000000000047741522423464100277130ustar00rootroot00000000000000/* Copyright The ORAS 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 humanize import ( "testing" "time" ) func TestFormatDuration(t *testing.T) { tests := []struct { name string d time.Duration want string }{ { name: "zero duration", d: 0, want: "0s", }, { name: "round to microsecond", d: 12345 * time.Nanosecond, want: "12µs", }, { name: "round to millisecond", d: 12345 * time.Microsecond, want: "12ms", }, { name: "round to second", d: 12345 * time.Millisecond, want: "12s", }, { name: "round up to second", d: time.Second + 500*time.Millisecond, want: "2s", }, { name: "round down to second", d: time.Second + 499*time.Millisecond, want: "1s", }, { name: "round up to millisecond", d: time.Millisecond + 500*time.Microsecond, want: "2ms", }, { name: "round down to millisecond", d: time.Millisecond + 499*time.Microsecond, want: "1ms", }, { name: "round up to microsecond", d: time.Microsecond + 500*time.Nanosecond, want: "2µs", }, { name: "round down to microsecond", d: time.Microsecond + 499*time.Nanosecond, want: "1µs", }, { name: "exact second", d: 3 * time.Second, want: "3s", }, { name: "exact millisecond", d: 5 * time.Millisecond, want: "5ms", }, { name: "exact microsecond", d: 7 * time.Microsecond, want: "7µs", }, { name: "less than a microsecond", d: 100 * time.Nanosecond, want: "0s", }, { name: "large duration", d: 2*time.Hour + 3*time.Minute + 4*time.Second + 501*time.Millisecond, want: "2h3m5s", }, { name: "negative duration", d: -5 * time.Second, want: "-5s", }, { name: "negative small duration", d: -500 * time.Microsecond, want: "-500µs", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := FormatDuration(tt.d); got != tt.want { t.Errorf("FormatDuration() = %v, want %v", got, tt.want) } }) } } oras-1.3.3/cmd/oras/internal/display/status/progress/manager.go000066400000000000000000000073221522423464100246110ustar00rootroot00000000000000/* Copyright The ORAS 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 progress import ( "errors" "os" "sync" "time" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/status/console" "oras.land/oras/internal/progress" ) const ( // bufferSize is the size of the status channel buffer. bufferSize = 1 framePerSecond = 5 bufFlushDuration = time.Second / framePerSecond ) var errManagerStopped = errors.New("progress output manager has already been stopped") type manager struct { status []*status lock sync.RWMutex // locks status and console, write lock is used for adding new status so that it has a higher priority console console.Console updating sync.WaitGroup renderDone chan struct{} renderClosed chan struct{} prompts map[progress.State]string } // NewManager initialized a new progress manager. func NewManager(tty *os.File, prompts map[progress.State]string) (progress.Manager, error) { c, err := console.NewConsole(tty) if err != nil { return nil, err } return newManager(c, prompts), nil } func newManager(c console.Console, prompts map[progress.State]string) progress.Manager { m := &manager{ console: c, renderDone: make(chan struct{}), renderClosed: make(chan struct{}), prompts: prompts, } m.start() return m } func (m *manager) start() { m.console.Save() renderTicker := time.NewTicker(bufFlushDuration) go func() { defer m.console.Restore() defer renderTicker.Stop() for { select { case <-m.renderDone: m.render() close(m.renderClosed) return case <-renderTicker.C: m.render() } } }() } func (m *manager) render() { m.lock.RLock() defer m.lock.RUnlock() // render with culling: only the latter statuses are rendered. models := m.status height, width := m.console.GetHeightWidth() if n := len(m.status) - height/2; n > 0 { models = models[n:] if height%2 == 1 { view := m.status[n-1].Render(width) m.console.OutputTo(uint(len(models)*2+1), view[1]) } } viewHeight := len(models) * 2 for i, model := range models { view := model.Render(width) m.console.OutputTo(uint(viewHeight-i*2), view[0]) m.console.OutputTo(uint(viewHeight-i*2-1), view[1]) } } // Track appends a new status with 2-line space for rendering. func (m *manager) Track(desc ocispec.Descriptor) (progress.Tracker, error) { if m.closed() { return nil, errManagerStopped } m.render() s := newStatus(desc) m.lock.Lock() m.status = append(m.status, s) m.console.NewRow() m.console.NewRow() m.lock.Unlock() return m.newTracker(s), nil } func (m *manager) newTracker(s *status) progress.Tracker { ch := make(chan statusUpdate, bufferSize) m.updating.Go(func() { for update := range ch { update(s) } }) return &messenger{ update: ch, prompts: m.prompts, } } // Close stops all status and waits for updating and rendering. func (m *manager) Close() error { if m.closed() { return errManagerStopped } // 1. wait for update to stop m.updating.Wait() // 2. stop periodic rendering close(m.renderDone) // 3. wait for the render stop <-m.renderClosed return nil } func (m *manager) closed() bool { select { case <-m.renderClosed: return true default: return false } } oras-1.3.3/cmd/oras/internal/display/status/progress/manager_test.go000066400000000000000000000054351522423464100256530ustar00rootroot00000000000000/* Copyright The ORAS 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 progress import ( "regexp" "testing" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/status/console" "oras.land/oras/internal/progress" ) type mockConsole struct { console.Console view []string height int width int } func newMockConsole(width, height int) *mockConsole { return &mockConsole{ height: height, width: width, } } func (c *mockConsole) GetHeightWidth() (int, int) { return c.height, c.width } func (c *mockConsole) NewRow() { c.view = append(c.view, "") } func (c *mockConsole) OutputTo(upCnt uint, str string) { c.view[len(c.view)-int(upCnt)] = str } func (c *mockConsole) Restore() {} func (c *mockConsole) Save() {} func Test_manager(t *testing.T) { desc := ocispec.Descriptor{ MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", Size: 1234567890, Digest: "sha256:c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646", Annotations: map[string]string{ "org.opencontainers.image.title": "hello.bin", }, } // simulate a console run c := newMockConsole(80, 24) m := newManager(c, map[progress.State]string{ progress.StateExists: "Exists", }) tracker, err := m.Track(desc) if err != nil { t.Fatalf("manager.Track() error = %v, wantErr nil", err) } if err = tracker.Update(progress.Status{ State: progress.StateExists, Offset: -1, }); err != nil { t.Errorf("tracker.Update() error = %v, wantErr nil", err) } if err := tracker.Close(); err != nil { t.Errorf("tracker.Close() error = %v, wantErr nil", err) } if err := m.Close(); err != nil { t.Errorf("manager.Close() error = %v, wantErr nil", err) } // verify the console output want := []string{ "✓ Exists hello.bin 1.15/1.15 GB 100.00% 0s", " └─ sha256:c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646 ", } if len(c.view) != len(want) { t.Errorf("console view length = %d, want %d", len(c.view), len(want)) } escRegexp := regexp.MustCompile("\x1b\\[[0-9]+m") equal := func(got, want string) bool { return escRegexp.ReplaceAllString(got, "") == want } for i, v := range want { if !equal(c.view[i], v) { t.Errorf("console view[%d] = %q, want %q", i, c.view[i], v) } } } oras-1.3.3/cmd/oras/internal/display/status/progress/messenger.go000066400000000000000000000031311522423464100251610ustar00rootroot00000000000000/* Copyright The ORAS 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 progress import "oras.land/oras/internal/progress" // messenger is progress message channel. type messenger struct { update chan statusUpdate closed bool prompts map[progress.State]string } // Update sends the status to the message channel. func (m *messenger) Update(status progress.Status) error { switch status.State { case progress.StateInitialized: m.update <- updateStatusStartTime() case progress.StateTransmitting: select { case m.update <- updateStatusMessage(m.prompts[progress.StateTransmitting], status.Offset): default: // drop message if channel is full } default: m.update <- updateStatusMessage(m.prompts[status.State], status.Offset) } return nil } // Fail sends the error to the message channel. func (m *messenger) Fail(err error) error { m.update <- updateStatusError(err) return nil } // Close marks the progress as completed and closes the message channel. func (m *messenger) Close() error { if m.closed { return nil } m.update <- updateStatusEndTime() close(m.update) m.closed = true return nil } oras-1.3.3/cmd/oras/internal/display/status/progress/messenger_test.go000066400000000000000000000073571522423464100262360ustar00rootroot00000000000000/* Copyright The ORAS 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 progress import ( "errors" "testing" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/internal/progress" ) func Test_messenger_Update(t *testing.T) { m := &messenger{ update: make(chan statusUpdate, 1), prompts: map[progress.State]string{ progress.StateInitialized: "initialized", progress.StateTransmitting: "testing", progress.StateTransmitted: "tested", }, } defer func() { _ = m.Close() }() desc := ocispec.Descriptor{ MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", Size: 1234567890, Digest: "sha256:c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646", } s := newStatus(desc) // test StateInitialized if err := m.Update(progress.Status{ State: progress.StateInitialized, Offset: -1, }); err != nil { t.Fatalf("messenger.Update() error = %v, wantErr nil", err) } update := <-m.update update(s) if s.startTime.IsZero() { t.Errorf("messenger.Update(progress.StateInitialized) startTime = zero, want non-zero") } select { case <-m.update: t.Errorf("messenger channel is not empty") default: } // test StateTransmitting if err := m.Update(progress.Status{ State: progress.StateTransmitting, Offset: 42, }); err != nil { t.Fatalf("messenger.Update() error = %v, wantErr nil", err) } // messages are dropped if channel is full if err := m.Update(progress.Status{ State: progress.StateTransmitting, Offset: 2048, }); err != nil { t.Fatalf("messenger.Update() error = %v, wantErr nil", err) } update = <-m.update update(s) if s.text != "testing" { t.Errorf("messenger.Update(progress.StateTransmitting) text = %q, want %q", s.text, "testing") } if s.offset != 42 { t.Errorf("messenger.Update(progress.StateTransmitting) offset = %d, want %d", s.offset, 42) } select { case <-m.update: t.Errorf("messenger channel is not empty") default: } // test StateTransmitted if err := m.Update(progress.Status{ State: progress.StateTransmitted, Offset: desc.Size, }); err != nil { t.Fatalf("messenger.Update() error = %v, wantErr nil", err) } update = <-m.update update(s) if s.text != "tested" { t.Errorf("messenger.Update(progress.StateTransmitted) text = %q, want %q", s.text, "tested") } if s.offset != desc.Size { t.Errorf("messenger.Update(progress.StateTransmitted) offset = %d, want %d", s.offset, desc.Size) } select { case <-m.update: t.Errorf("messenger channel is not empty") default: } } func Test_messenger_Fail(t *testing.T) { m := &messenger{ update: make(chan statusUpdate, 1), } defer func() { _ = m.Close() }() s := new(status) errTest := errors.New("test error") if err := m.Fail(errTest); err != nil { t.Fatalf("messenger.Fail() error = %v, wantErr nil", err) } update := <-m.update update(s) if !errors.Is(errTest, s.err) { t.Errorf("messenger.Fail() = %v, want %v", s.err, errTest) } } func Test_messenger_Close(t *testing.T) { m := &messenger{ update: make(chan statusUpdate, 1), } if err := m.Close(); err != nil { t.Fatalf("messenger.Close() error = %v, wantErr nil", err) } // double close should not panic or return an error if err := m.Close(); err != nil { t.Fatalf("messenger.Close() error = %v, wantErr nil", err) } } oras-1.3.3/cmd/oras/internal/display/status/progress/speed.go000066400000000000000000000030221522423464100242700ustar00rootroot00000000000000/* Copyright The ORAS 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 progress import "time" type speedPoint struct { time time.Time offset int64 } type speedWindow struct { point []speedPoint next int size int } // newSpeedWindow creates a new speed window with a given capacity. func newSpeedWindow(capacity int) *speedWindow { return &speedWindow{ point: make([]speedPoint, capacity), } } // Add adds a done workload to the window. func (w *speedWindow) Add(time time.Time, offset int64) { if w.size != len(w.point) { w.size++ } w.point[w.next] = speedPoint{ time: time, offset: offset, } w.next = (w.next + 1) % len(w.point) } // Mean returns the mean speed of the window with unit of byte per second. func (w *speedWindow) Mean() float64 { if w.size < 2 { // no speed displayed for first read return 0 } begin := (w.next - w.size + len(w.point)) % len(w.point) end := (begin - 1 + w.size) % w.size return float64(w.point[end].offset-w.point[begin].offset) / w.point[end].time.Sub(w.point[begin].time).Seconds() } oras-1.3.3/cmd/oras/internal/display/status/progress/speed_test.go000066400000000000000000000022141522423464100253310ustar00rootroot00000000000000/* Copyright The ORAS 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 progress import ( "testing" "time" ) func Test_speedWindow(t *testing.T) { w := newSpeedWindow(3) if s := w.Mean(); s != 0 { t.Errorf("expected 0, got %f", s) } now := time.Now() w.Add(now, 100) if s := w.Mean(); s != 0 { t.Errorf("expected 0, got %f", s) } w.Add(now.Add(1*time.Second), 200) if s := w.Mean(); s != 100 { t.Errorf("expected 100, got %f", s) } w.Add(now.Add(4*time.Second), 900) if s := w.Mean(); s != 200 { t.Errorf("expected 200, got %f", s) } w.Add(now.Add(5*time.Second), 1400) if s := w.Mean(); s != 300 { t.Errorf("expected 300, got %f", s) } } oras-1.3.3/cmd/oras/internal/display/status/progress/spinner.go000066400000000000000000000015211522423464100246500ustar00rootroot00000000000000/* Copyright The ORAS 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 progress var spinnerSymbols = []rune("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏") type spinner int // symbol returns the rune of status mark and shift to the next. func (s *spinner) symbol() rune { last := int(*s) *s = spinner((last + 1) % len(spinnerSymbols)) return spinnerSymbols[last] } oras-1.3.3/cmd/oras/internal/display/status/progress/spinner_test.go000066400000000000000000000016171522423464100257150ustar00rootroot00000000000000/* Copyright The ORAS 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 progress import "testing" func Test_spinner_symbol(t *testing.T) { var s spinner for i := range len(spinnerSymbols) { if s.symbol() != spinnerSymbols[i] { t.Errorf("symbol() = %v, want %v", s.symbol(), spinnerSymbols[i]) } } if s.symbol() != spinnerSymbols[0] { t.Errorf("symbol() = %v, want %v", s.symbol(), spinnerSymbols[0]) } } oras-1.3.3/cmd/oras/internal/display/status/progress/status.go000066400000000000000000000137641522423464100245310ustar00rootroot00000000000000/* Copyright The ORAS 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 progress import ( "fmt" "strings" "sync" "time" "unicode/utf8" "github.com/morikuni/aec" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/status/progress/humanize" ) const ( barLength = 20 speedLength = 7 // speed_size(4) + space(1) + speed_unit(2) zeroDuration = "0s" // default zero value of time.Duration.String() ) var ( spinnerColor = aec.LightYellowF doneMarkColor = aec.LightGreenF progressColor = aec.LightBlueB failureColor = aec.LightRedF ) // status is the model to present the progress of an operation. type status struct { lock sync.RWMutex done bool // true if the operation is succeeded err error // non-nil if the operation fails mark spinner text string startTime time.Time endTime time.Time descriptor ocispec.Descriptor offset int64 total humanize.Bytes speed *speedWindow } // newStatus generates a base empty status. func newStatus(desc ocispec.Descriptor) *status { return &status{ descriptor: desc, offset: -1, total: humanize.ToBytes(desc.Size), speed: newSpeedWindow(framePerSecond), } } // Render returns human-readable TTY strings of the status. // Format: // // [left--------------------------------------------][margin][right---------------------------------] // mark(1) bar(22) speed(8) action(<=11) name(<=126) size_per_size(<=13) percent(8) time(>=6) // └─ digest(72) func (s *status) Render(width int) [2]string { s.lock.RLock() defer s.lock.RUnlock() // obtain object name name := s.descriptor.Annotations[ocispec.AnnotationTitle] if name == "" { name = s.descriptor.MediaType } // calculate the progress percentage var offset string var percent float64 if s.done { // 100%, show exact size offset = fmt.Sprint(s.total.Size) percent = 1 } else if s.offset < 0 { // not started, show 0% offset = "-" } else if s.descriptor.Size == 0 { // 0 byte, show 100% offset = "0" percent = 1 } else { // 0% ~ 99%, show 2-digit precision percent = float64(s.offset) / float64(s.descriptor.Size) offset = fmt.Sprintf("%.2f", humanize.RoundTo(s.total.Size*percent)) } // render the left side of the primary line var left string lenLeft := 0 // manually calculate the string length due to the color escape sequence if s.done { left = fmt.Sprintf("%s %s %s", doneMarkColor.Apply("✓"), s.text, name) } else { var mark string if s.err == nil { mark = spinnerColor.Apply(string(s.mark.symbol())) } else { mark = failureColor.Apply("✗") } lenBar := int(percent * barLength) speed := s.calculateSpeed() left = fmt.Sprintf("%s [%s%s](%*s/s) %s %s", mark, progressColor.Apply(strings.Repeat(" ", lenBar)), strings.Repeat(".", barLength-lenBar), speedLength, speed, s.text, name) // bar + wrapper(2) + space(1) + speed + "/s"(2) + wrapper(2) = len(bar) + len(speed) + 7 lenLeft = barLength + speedLength + 7 } // mark(1) + space(1) + prompt + space(1) + name = len(prompt) + len(name) + 3 lenLeft += utf8.RuneCountInString(s.text) + utf8.RuneCountInString(name) + 3 // render the right side of the primary line right := fmt.Sprintf(" %s/%s %6.2f%% %6s", offset, s.total, percent*100, s.durationString()) lenRight := utf8.RuneCountInString(right) // render view lenMargin := width - lenLeft - lenRight if lenMargin < 0 { // hide partial name with one space left left = left[:len(left)+lenMargin-1] + "." lenMargin = 0 } var padding string if paddingLen := width - len(s.descriptor.Digest) - 5; paddingLen > 0 { padding = strings.Repeat(" ", paddingLen) } return [2]string{ fmt.Sprintf("%s%s%s", left, strings.Repeat(" ", lenMargin), right), fmt.Sprintf(" └─ %s%s", s.descriptor.Digest, padding), } } // calculateSpeed calculates the speed of the progress and update last status. // caller must hold the lock. func (s *status) calculateSpeed() humanize.Bytes { if s.offset < 0 { // not started return humanize.ToBytes(0) } s.speed.Add(time.Now(), s.offset) return humanize.ToBytes(int64(s.speed.Mean())) } // durationString returns a viewable TTY string of the status with duration. func (s *status) durationString() string { if s.startTime.IsZero() { return zeroDuration } var d time.Duration if s.endTime.IsZero() { d = time.Since(s.startTime) } else { d = s.endTime.Sub(s.startTime) } return humanize.FormatDuration(d) } // statusUpdate is a function to update the status. type statusUpdate func(*status) // updateStatusMessage returns a statusUpdate to update the status message. // Optionally, it can update the offset of the status. func updateStatusMessage(text string, offset int64) statusUpdate { return func(s *status) { s.lock.Lock() defer s.lock.Unlock() s.text = text if offset >= 0 { s.offset = offset } } } // updateStatusStartTime returns a statusUpdate to update the status start time. func updateStatusStartTime() statusUpdate { return func(s *status) { s.lock.Lock() defer s.lock.Unlock() s.startTime = time.Now() s.speed.Add(s.startTime, 0) } } // updateStatusEndTime returns a statusUpdate to update the status end time. func updateStatusEndTime() statusUpdate { return func(s *status) { s.lock.Lock() defer s.lock.Unlock() s.endTime = time.Now() if s.err == nil { s.done = true } } } // updateStatusError returns a statusUpdate to update the status error. func updateStatusError(err error) statusUpdate { return func(s *status) { s.lock.Lock() defer s.lock.Unlock() s.err = err } } oras-1.3.3/cmd/oras/internal/display/status/progress/status_test.go000066400000000000000000000272311522423464100255620ustar00rootroot00000000000000/* Copyright The ORAS 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 progress import ( "errors" "reflect" "regexp" "testing" "time" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/status/progress/humanize" ) func Test_status_Render(t *testing.T) { escRegexp := regexp.MustCompile("\x1b\\[[0-9]+m") equal := func(got, want [2]string) bool { noColor := [2]string{ escRegexp.ReplaceAllString(got[0], ""), got[1], } return noColor == want } desc := ocispec.Descriptor{ MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", Size: 1234567890, Digest: "sha256:c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646", Annotations: map[string]string{ "org.opencontainers.image.title": "hello.bin", }, } tests := []struct { name string status func() *status // constructor required to work the time.Now() around width int want [2]string }{ { name: "default status", status: func() *status { return newStatus(desc) }, width: 80, want: [2]string{ "⠋ [....................]( 0 B/s) hello.bin -/1.15 GB 0.00% 0s", " └─ sha256:c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646 ", }, }, { name: "operation in progress", status: func() *status { return &status{ text: "Test", startTime: time.Now().Add(-time.Second * 100), descriptor: desc, offset: 123456789, total: humanize.ToBytes(desc.Size), speed: newSpeedWindow(10), } }, width: 80, want: [2]string{ "⠋ [ ..................]( 0 B/s) Test hello.bin 0.12/1.15 GB 10.00% 1m40s", " └─ sha256:c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646 ", }, }, { name: "operation succeeded", status: func() *status { return &status{ done: true, text: "Tested", startTime: time.Now().Add(-time.Second * 100), endTime: time.Now(), descriptor: desc, offset: 1234567890, total: humanize.ToBytes(desc.Size), speed: newSpeedWindow(10), } }, width: 80, want: [2]string{ "✓ Tested hello.bin 1.15/1.15 GB 100.00% 1m40s", " └─ sha256:c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646 ", }, }, { name: "operation failed", status: func() *status { return &status{ text: "Test", err: errors.New("test error"), startTime: time.Now().Add(-time.Second * 100), descriptor: desc, offset: 123456789, total: humanize.ToBytes(desc.Size), speed: newSpeedWindow(10), } }, width: 80, want: [2]string{ "✗ [ ..................]( 0 B/s) Test hello.bin 0.12/1.15 GB 10.00% 1m40s", " └─ sha256:c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646 ", }, }, { name: "long status text in progress", status: func() *status { return &status{ text: "Longer test", startTime: time.Now().Add(-time.Second * 100), descriptor: desc, offset: 123456789, total: humanize.ToBytes(desc.Size), speed: newSpeedWindow(10), } }, width: 80, want: [2]string{ "⠋ [ ..................]( 0 B/s) Longer test hel. 0.12/1.15 GB 10.00% 1m40s", " └─ sha256:c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646 ", }, }, { name: "object with no name in progress", status: func() *status { plainDesc := desc plainDesc.Annotations = nil return &status{ text: "Test", startTime: time.Now().Add(-time.Second * 100), descriptor: plainDesc, offset: 123456789, total: humanize.ToBytes(plainDesc.Size), speed: newSpeedWindow(10), } }, width: 80, want: [2]string{ "⠋ [ ..................]( 0 B/s) Test applicatio. 0.12/1.15 GB 10.00% 1m40s", " └─ sha256:c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646 ", }, }, { name: "object with zero size in progress", status: func() *status { zeroDesc := ocispec.Descriptor{ MediaType: "text/plain", Size: 0, Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", } return &status{ text: "Test", startTime: time.Now().Add(-time.Second * 100), descriptor: zeroDesc, total: humanize.ToBytes(zeroDesc.Size), speed: newSpeedWindow(10), } }, width: 80, want: [2]string{ "⠋ [ ]( 0 B/s) Test text/plain 0/0 B 100.00% 1m40s", " └─ sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ", }, }, { name: "long status text succeeded", status: func() *status { return &status{ done: true, text: "Long Long Long Long Long Long Long Long test", startTime: time.Now().Add(-time.Second * 100), endTime: time.Now(), descriptor: desc, offset: 1234567890, total: humanize.ToBytes(desc.Size), speed: newSpeedWindow(10), } }, width: 80, want: [2]string{ "✓ Long Long Long Long Long Long Long Long test hell. 1.15/1.15 GB 100.00% 1m40s", " └─ sha256:c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646 ", }, }, { name: "object with no name succeeded", status: func() *status { plainDesc := desc plainDesc.Annotations = nil return &status{ done: true, text: "Tested", startTime: time.Now().Add(-time.Second * 100), endTime: time.Now(), descriptor: plainDesc, offset: 1234567890, total: humanize.ToBytes(plainDesc.Size), speed: newSpeedWindow(10), } }, width: 80, want: [2]string{ "✓ Tested application/vnd.docker.image.rootfs.diff.t. 1.15/1.15 GB 100.00% 1m40s", " └─ sha256:c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646 ", }, }, { name: "object with zero size succeeded", status: func() *status { zeroDesc := ocispec.Descriptor{ MediaType: "text/plain", Size: 0, Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", } return &status{ done: true, text: "Tested", startTime: time.Now().Add(-time.Second * 100), endTime: time.Now(), descriptor: zeroDesc, total: humanize.ToBytes(zeroDesc.Size), speed: newSpeedWindow(10), } }, width: 80, want: [2]string{ "✓ Tested text/plain 0/0 B 100.00% 1m40s", " └─ sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := tt.status() if got := s.Render(tt.width); !equal(got, tt.want) { t.Errorf("status.Render() = %v, want %v", got, tt.want) } }) } } func Test_status_durationString(t *testing.T) { tests := []struct { name string s *status want string }{ { name: "duration in hours", s: &status{ startTime: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), endTime: time.Date(2021, 1, 1, 2, 1, 10, 0, time.UTC), }, want: "2h1m10s", }, { name: "duration in minutes", s: &status{ startTime: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), endTime: time.Date(2021, 1, 1, 0, 1, 10, 0, time.UTC), }, want: "1m10s", }, { name: "duration in seconds", s: &status{ startTime: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), endTime: time.Date(2021, 1, 1, 0, 0, 10, 0, time.UTC), }, want: "10s", }, { name: "duration in milliseconds", s: &status{ startTime: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), endTime: time.Date(2021, 1, 1, 0, 0, 0, 10000000, time.UTC), }, want: "10ms", }, { name: "duration in microseconds", s: &status{ startTime: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), endTime: time.Date(2021, 1, 1, 0, 0, 0, 10000, time.UTC), }, want: "10µs", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := tt.s.durationString(); got != tt.want { t.Errorf("status.durationString() = %v, want %v", got, tt.want) } }) } } func Test_updateStatus(t *testing.T) { desc := ocispec.Descriptor{ MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", Size: 1234567890, Digest: "sha256:c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646", Annotations: map[string]string{ "org.opencontainers.image.title": "hello.bin", }, } equal := func(s, t *status) bool { // speed is not compared return s.done == t.done && errors.Is(s.err, t.err) && s.text == t.text && s.startTime.Equal(t.startTime) && s.endTime.Equal(t.endTime) && reflect.DeepEqual(s.descriptor, t.descriptor) && s.offset == t.offset && reflect.DeepEqual(s.total, t.total) } errTest := errors.New("test error") tests := []struct { name string setup statusUpdate update statusUpdate want *status check func(*testing.T, *status) // custom check instead of want }{ { name: "updateStatusMessage", update: updateStatusMessage("Test", 42), want: &status{ text: "Test", descriptor: desc, offset: 42, total: humanize.ToBytes(desc.Size), }, }, { name: "updateStatusMessage (text only)", setup: updateStatusMessage("foo", 42), update: updateStatusMessage("bar", -1), want: &status{ text: "bar", descriptor: desc, offset: 42, total: humanize.ToBytes(desc.Size), }, }, { name: "updateStatusStartTime", update: updateStatusStartTime(), check: func(t *testing.T, s *status) { t.Helper() if s.startTime.IsZero() { t.Errorf("updateStatusStartTime() = %v, want non-zero", s.startTime) } if s.speed.next != 1 { t.Errorf("updateStatusStartTime() did not add a speed sample") } }, }, { name: "updateStatusEndTime", update: updateStatusEndTime(), check: func(t *testing.T, s *status) { t.Helper() if s.endTime.IsZero() { t.Errorf("updateStatusEndTime() = %v, want non-zero", s.endTime) } if !s.done { t.Errorf("updateStatusEndTime() did not set done") } }, }, { name: "updateStatusEndTime with error", setup: updateStatusError(errTest), update: updateStatusEndTime(), check: func(t *testing.T, s *status) { t.Helper() if s.endTime.IsZero() { t.Errorf("updateStatusEndTime() = %v, want non-zero", s.endTime) } if s.done { t.Errorf("updateStatusEndTime() set done") } }, }, { name: "updateStatusError", update: updateStatusError(errTest), want: &status{ err: errTest, descriptor: desc, offset: -1, total: humanize.ToBytes(desc.Size), }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := newStatus(desc) if tt.setup != nil { tt.setup(got) } if tt.update(got); tt.check != nil { tt.check(t, got) } else if !equal(got, tt.want) { t.Errorf("updateStatus() = %v, want %v", got, tt.want) } }) } } oras-1.3.3/cmd/oras/internal/display/status/text.go000066400000000000000000000367121522423464100223240ustar00rootroot00000000000000/* Copyright The ORAS 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 status import ( "context" "sync" "oras.land/oras/internal/contentutil" "oras.land/oras/internal/descriptor" "oras.land/oras/internal/graph" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2" "oras.land/oras-go/v2/content" "oras.land/oras/cmd/oras/internal/output" ) // TextPushHandler handles text status output for push events. type TextPushHandler struct { printer *output.Printer committed *sync.Map fetcher content.Fetcher } // NewTextPushHandler returns a new handler for push command. func NewTextPushHandler(printer *output.Printer, fetcher content.Fetcher) PushHandler { tch := TextPushHandler{ printer: printer, fetcher: fetcher, committed: &sync.Map{}, } return &tch } // OnFileLoading is called when a file is being prepared for upload. func (ph *TextPushHandler) OnFileLoading(name string) error { return ph.printer.PrintVerbose("Preparing", name) } // OnEmptyArtifact is called when an empty artifact is being uploaded. func (ph *TextPushHandler) OnEmptyArtifact() error { return ph.printer.Println("Uploading empty artifact") } // TrackTarget returns a tracked target. func (ph *TextPushHandler) TrackTarget(gt oras.GraphTarget) (oras.GraphTarget, StopTrackTargetFunc, error) { return gt, discardStopTrack, nil } // OnCopySkipped is called when an object already exists. func (ph *TextPushHandler) OnCopySkipped(_ context.Context, desc ocispec.Descriptor) error { ph.committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) return ph.printer.PrintStatus(desc, PushPromptExists) } // PreCopy implements PreCopy of CopyHandler. func (ph *TextPushHandler) PreCopy(_ context.Context, desc ocispec.Descriptor) error { return ph.printer.PrintStatus(desc, PushPromptUploading) } // PostCopy implements PostCopy of CopyHandler. func (ph *TextPushHandler) PostCopy(ctx context.Context, desc ocispec.Descriptor) error { ph.committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) successors, err := graph.FilteredSuccessors(ctx, desc, ph.fetcher, DeduplicatedFilter(ph.committed)) if err != nil { return err } for _, successor := range successors { if err = ph.printer.PrintStatus(successor, PushPromptExists); err != nil { return err } } return ph.printer.PrintStatus(desc, PushPromptUploaded) } // NewTextAttachHandler returns a new handler for attach command. func NewTextAttachHandler(printer *output.Printer, fetcher content.Fetcher) AttachHandler { return NewTextPushHandler(printer, fetcher) } // TextPullHandler handles text status output for pull events. type TextPullHandler struct { printer *output.Printer } // TrackTarget implements PullHandler. func (ph *TextPullHandler) TrackTarget(gt oras.GraphTarget) (oras.GraphTarget, StopTrackTargetFunc, error) { return gt, discardStopTrack, nil } // OnNodeDownloading implements PullHandler. func (ph *TextPullHandler) OnNodeDownloading(desc ocispec.Descriptor) error { return ph.printer.PrintStatus(desc, PullPromptDownloading) } // OnNodeDownloaded implements PullHandler. func (ph *TextPullHandler) OnNodeDownloaded(desc ocispec.Descriptor) error { return ph.printer.PrintStatus(desc, PullPromptDownloaded) } // OnNodeRestored implements PullHandler. func (ph *TextPullHandler) OnNodeRestored(desc ocispec.Descriptor) error { return ph.printer.PrintStatus(desc, PullPromptRestored) } // OnNodeProcessing implements PullHandler. func (ph *TextPullHandler) OnNodeProcessing(desc ocispec.Descriptor) error { return ph.printer.PrintStatus(desc, PullPromptProcessing) } // OnNodeSkipped implements PullHandler. func (ph *TextPullHandler) OnNodeSkipped(desc ocispec.Descriptor) error { return ph.printer.PrintStatus(desc, PullPromptSkipped) } // NewTextPullHandler returns a new handler for pull command. func NewTextPullHandler(printer *output.Printer) PullHandler { return &TextPullHandler{ printer: printer, } } // TextCopyHandler handles text status output for push events. type TextCopyHandler struct { printer *output.Printer committed *sync.Map fetcher content.Fetcher } // NewTextCopyHandler returns a new handler for push command. func NewTextCopyHandler(printer *output.Printer, fetcher content.Fetcher) CopyHandler { return &TextCopyHandler{ printer: printer, fetcher: fetcher, committed: &sync.Map{}, } } // StartTracking starts a tracked target from a graph target. func (ch *TextCopyHandler) StartTracking(gt oras.GraphTarget) (oras.GraphTarget, error) { return gt, nil } // StopTracking ends the copy tracking for the target. func (ch *TextCopyHandler) StopTracking() error { return nil } // OnCopySkipped is called when an object already exists. func (ch *TextCopyHandler) OnCopySkipped(_ context.Context, desc ocispec.Descriptor) error { ch.committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) return ch.printer.PrintStatus(desc, copyPromptExists) } // PreCopy implements PreCopy of CopyHandler. func (ch *TextCopyHandler) PreCopy(_ context.Context, desc ocispec.Descriptor) error { return ch.printer.PrintStatus(desc, copyPromptCopying) } // PostCopy implements PostCopy of CopyHandler. func (ch *TextCopyHandler) PostCopy(ctx context.Context, desc ocispec.Descriptor) error { ch.committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) deduplicated, err := graph.FilteredSuccessors(ctx, desc, ch.fetcher, DeduplicatedFilter(ch.committed)) if err != nil { return err } for _, successor := range deduplicated { if err = ch.printer.PrintStatus(successor, copyPromptSkipped); err != nil { return err } } return ch.printer.PrintStatus(desc, copyPromptCopied) } // OnMounted implements OnMounted of CopyHandler. func (ch *TextCopyHandler) OnMounted(_ context.Context, desc ocispec.Descriptor) error { ch.committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) return ch.printer.PrintStatus(desc, copyPromptMounted) } // TextBackupHandler handles text status output for backup events. type TextBackupHandler struct { printer *output.Printer committed *sync.Map fetcher content.Fetcher } // NewTextBackupHandler returns a new text handler for backup command. func NewTextBackupHandler(printer *output.Printer, fetcher content.Fetcher) BackupHandler { return &TextBackupHandler{ printer: printer, fetcher: fetcher, committed: &sync.Map{}, } } // OnCopySkipped implements OnCopySkipped of BackupHandler. func (tbh *TextBackupHandler) OnCopySkipped(_ context.Context, desc ocispec.Descriptor) error { tbh.committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) return tbh.printer.PrintStatus(desc, backupPromptExists) } // PreCopy implements PreCopy of BackupHandler. func (tbh *TextBackupHandler) PreCopy(_ context.Context, desc ocispec.Descriptor) error { return tbh.printer.PrintStatus(desc, backupPromptPulling) } // PostCopy implements PostCopy of BackupHandler. func (tbh *TextBackupHandler) PostCopy(ctx context.Context, desc ocispec.Descriptor) error { tbh.committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) deduplicated, err := graph.FilteredSuccessors(ctx, desc, tbh.fetcher, DeduplicatedFilter(tbh.committed)) if err != nil { return err } for _, successor := range deduplicated { if err = tbh.printer.PrintStatus(successor, backupPromptSkipped); err != nil { return err } } return tbh.printer.PrintStatus(desc, backupPromptPulled) } // StartTracking implements StartTracking of BackupHandler. func (tbh *TextBackupHandler) StartTracking(gt oras.GraphTarget) (oras.GraphTarget, error) { return gt, nil } // StopTracking implements StopTracking of BackupHandler. func (tbh *TextBackupHandler) StopTracking() error { return nil } // TextRestoreHandler handles text status output for restore events. type TextRestoreHandler struct { printer *output.Printer committed *sync.Map fetcher content.Fetcher } // NewTextRestoreHandler returns a new text handler for restore command. func NewTextRestoreHandler(printer *output.Printer, fetcher content.Fetcher) RestoreHandler { return &TextRestoreHandler{ printer: printer, fetcher: fetcher, committed: &sync.Map{}, } } // OnCopySkipped implements OnCopySkipped of RestoreHandler. func (trh *TextRestoreHandler) OnCopySkipped(_ context.Context, desc ocispec.Descriptor) error { trh.committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) return trh.printer.PrintStatus(desc, restorePromptExists) } // PreCopy implements PreCopy of RestoreHandler. func (trh *TextRestoreHandler) PreCopy(_ context.Context, desc ocispec.Descriptor) error { return trh.printer.PrintStatus(desc, restorePromptPushing) } // PostCopy implements PostCopy of RestoreHandler. func (trh *TextRestoreHandler) PostCopy(ctx context.Context, desc ocispec.Descriptor) error { trh.committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) deduplicated, err := graph.FilteredSuccessors(ctx, desc, trh.fetcher, DeduplicatedFilter(trh.committed)) if err != nil { return err } for _, successor := range deduplicated { if err = trh.printer.PrintStatus(successor, restorePromptSkipped); err != nil { return err } } return trh.printer.PrintStatus(desc, restorePromptPushed) } // StartTracking implements StartTracking of RestoreHandler. func (trh *TextRestoreHandler) StartTracking(gt oras.GraphTarget) (oras.GraphTarget, error) { return gt, nil } // StopTracking implements StopTracking of RestoreHandler. func (trh *TextRestoreHandler) StopTracking() error { return nil } // TextManifestPushHandler handles text status output for manifest push events. type TextManifestPushHandler struct { desc ocispec.Descriptor printer *output.Printer } // NewTextManifestPushHandler returns a new handler for manifest push command. func NewTextManifestPushHandler(printer *output.Printer, desc ocispec.Descriptor) ManifestPushHandler { return &TextManifestPushHandler{ desc: desc, printer: printer, } } func (mph *TextManifestPushHandler) OnManifestPushSkipped() error { return mph.printer.PrintStatus(mph.desc, PushPromptExists) } func (mph *TextManifestPushHandler) OnManifestPushing() error { return mph.printer.PrintStatus(mph.desc, PushPromptUploading) } func (mph *TextManifestPushHandler) OnManifestPushed() error { return mph.printer.PrintStatus(mph.desc, PushPromptUploaded) } // TextManifestIndexCreateHandler handles text status output for manifest index create events. type TextManifestIndexCreateHandler struct { printer *output.Printer } // NewTextManifestIndexCreateHandler returns a new handler for manifest index create command. func NewTextManifestIndexCreateHandler(printer *output.Printer) ManifestIndexCreateHandler { tmich := TextManifestIndexCreateHandler{ printer: printer, } return &tmich } // OnFetching implements ManifestIndexCreateHandler. func (mich *TextManifestIndexCreateHandler) OnFetching(source string) error { return mich.printer.Println(IndexPromptFetching, source) } // OnFetched implements ManifestIndexCreateHandler. func (mich *TextManifestIndexCreateHandler) OnFetched(ref string, desc ocispec.Descriptor) error { if contentutil.IsDigest(ref) { return mich.printer.Println(IndexPromptFetched, ref) } return mich.printer.Println(IndexPromptFetched, desc.Digest, ref) } // OnIndexPacked implements ManifestIndexCreateHandler. func (mich *TextManifestIndexCreateHandler) OnIndexPacked(desc ocispec.Descriptor) error { return mich.printer.Println(IndexPromptPacked, descriptor.ShortDigest(desc), ocispec.MediaTypeImageIndex) } // OnIndexPushed implements ManifestIndexCreateHandler. func (mich *TextManifestIndexCreateHandler) OnIndexPushed(path string) error { return mich.printer.Println(IndexPromptPushed, path) } // TextManifestIndexUpdateHandler handles text status output for manifest index update events. type TextManifestIndexUpdateHandler struct { printer *output.Printer } // NewTextManifestIndexUpdateHandler returns a new handler for manifest index create command. func NewTextManifestIndexUpdateHandler(printer *output.Printer) ManifestIndexUpdateHandler { miuh := TextManifestIndexUpdateHandler{ printer: printer, } return &miuh } // OnFetching implements ManifestIndexUpdateHandler. func (miuh *TextManifestIndexUpdateHandler) OnFetching(ref string) error { return miuh.printer.Println(IndexPromptFetching, ref) } // OnFetched implements ManifestIndexUpdateHandler. func (miuh *TextManifestIndexUpdateHandler) OnFetched(ref string, desc ocispec.Descriptor) error { if contentutil.IsDigest(ref) { return miuh.printer.Println(IndexPromptFetched, ref) } return miuh.printer.Println(IndexPromptFetched, desc.Digest, ref) } // OnManifestRemoved implements ManifestIndexUpdateHandler. func (miuh *TextManifestIndexUpdateHandler) OnManifestRemoved(digest digest.Digest) error { return miuh.printer.Println(IndexPromptRemoved, digest) } // OnManifestAdded implements ManifestIndexUpdateHandler. func (miuh *TextManifestIndexUpdateHandler) OnManifestAdded(ref string, desc ocispec.Descriptor) error { if contentutil.IsDigest(ref) { return miuh.printer.Println(IndexPromptAdded, ref) } return miuh.printer.Println(IndexPromptAdded, desc.Digest, ref) } // OnIndexMerged implements ManifestIndexUpdateHandler. func (miuh *TextManifestIndexUpdateHandler) OnIndexMerged(ref string, desc ocispec.Descriptor) error { if contentutil.IsDigest(ref) { return miuh.printer.Println(IndexPromptMerged, ref) } return miuh.printer.Println(IndexPromptMerged, desc.Digest, ref) } // OnIndexPacked implements ManifestIndexUpdateHandler. func (miuh *TextManifestIndexUpdateHandler) OnIndexPacked(desc ocispec.Descriptor) error { return miuh.printer.Println(IndexPromptUpdated, desc.Digest) } // OnIndexPushed implements ManifestIndexUpdateHandler. func (miuh *TextManifestIndexUpdateHandler) OnIndexPushed(indexRef string) error { return miuh.printer.Println(IndexPromptPushed, indexRef) } // TextBlobPushHandler handles text status output for blob push events. type TextBlobPushHandler struct { desc ocispec.Descriptor printer *output.Printer } // NewTextBlobPushHandler returns a new handler for blob push command. func NewTextBlobPushHandler(printer *output.Printer, desc ocispec.Descriptor) BlobPushHandler { return &TextBlobPushHandler{ desc: desc, printer: printer, } } // OnBlobExists implements BlobPushHandler. func (bph *TextBlobPushHandler) OnBlobExists() error { return bph.printer.PrintStatus(bph.desc, PushPromptExists) } // OnBlobUploading implements BlobPushHandler. func (bph *TextBlobPushHandler) OnBlobUploading() error { return bph.printer.PrintStatus(bph.desc, PushPromptUploading) } // OnBlobUploaded implements BlobPushHandler. func (bph *TextBlobPushHandler) OnBlobUploaded() error { return bph.printer.PrintStatus(bph.desc, PushPromptUploaded) } // StartTracking implements BlobPushHandler. func (bph *TextBlobPushHandler) StartTracking(gt oras.GraphTarget) (oras.GraphTarget, error) { return gt, nil } // StopTracking implements BlobPushHandler. func (bph *TextBlobPushHandler) StopTracking() error { return nil } oras-1.3.3/cmd/oras/internal/display/status/text_test.go000066400000000000000000000305131522423464100233540ustar00rootroot00000000000000/* Copyright The ORAS 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 status import ( "context" "os" "strings" "sync" "testing" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2/content/memory" "oras.land/oras/cmd/oras/internal/output" "oras.land/oras/internal/testutils" ) var ( ctx context.Context builder *strings.Builder printer *output.Printer bogus ocispec.Descriptor mockFetcher testutils.MockFetcher ) func TestMain(m *testing.M) { mockFetcher = testutils.NewMockFetcher() ctx = context.Background() builder = &strings.Builder{} printer = output.NewPrinter(builder, os.Stderr) bogus = ocispec.Descriptor{MediaType: ocispec.MediaTypeImageManifest} os.Exit(m.Run()) } func validatePrinted(t *testing.T, expected string) { t.Helper() actual := strings.TrimSpace(builder.String()) if expected != actual { t.Error("Output does not match expected <" + expected + "> actual <" + actual + ">") } } func TestTextCopyHandler_OnMounted(t *testing.T) { builder.Reset() ch := NewTextCopyHandler(printer, mockFetcher.Fetcher) if ch.OnMounted(ctx, mockFetcher.OciImage) != nil { t.Error("OnMounted() should not return an error") } validatePrinted(t, "Mounted 0b442c23c1dd oci-image") } func TestTextCopyHandler_OnCopySkipped(t *testing.T) { builder.Reset() ch := NewTextCopyHandler(printer, mockFetcher.Fetcher) if ch.OnCopySkipped(ctx, mockFetcher.OciImage) != nil { t.Error("OnCopySkipped() should not return an error") } validatePrinted(t, "Exists 0b442c23c1dd oci-image") } func TestTextCopyHandler_PostCopy_titled(t *testing.T) { builder.Reset() ch := NewTextCopyHandler(printer, mockFetcher.Fetcher) if ch.PostCopy(ctx, mockFetcher.OciImage) != nil { t.Error("PostCopy() should not return an error") } if ch.PostCopy(ctx, bogus) == nil { t.Error("PostCopy() should return an error") } validatePrinted(t, "Copied 0b442c23c1dd oci-image") } func TestTextCopyHandler_PostCopy_skipped(t *testing.T) { builder.Reset() ch := &TextCopyHandler{ printer: printer, fetcher: mockFetcher.Fetcher, committed: &sync.Map{}, } ch.committed.Store(mockFetcher.ImageLayer.Digest.String(), mockFetcher.ImageLayer.Annotations[ocispec.AnnotationTitle]+"bogus") if err := ch.PostCopy(ctx, mockFetcher.OciImage); err != nil { t.Error("PostCopy() returns unexpected err:", err) } validatePrinted(t, "Skipped f6b87e8e0fe1 layer\nCopied 0b442c23c1dd oci-image") } func TestTextCopyHandler_PreCopy(t *testing.T) { builder.Reset() ch := NewTextCopyHandler(printer, mockFetcher.Fetcher) if ch.PreCopy(ctx, mockFetcher.OciImage) != nil { t.Error("PreCopy() should not return an error") } validatePrinted(t, "Copying 0b442c23c1dd oci-image") } func TestTextPullHandler_OnNodeDownloaded(t *testing.T) { builder.Reset() ph := NewTextPullHandler(printer) if ph.OnNodeDownloaded(mockFetcher.OciImage) != nil { t.Error("OnNodeDownloaded() should not return an error") } validatePrinted(t, "Downloaded 0b442c23c1dd oci-image") } func TestTextPullHandler_OnNodeDownloading(t *testing.T) { builder.Reset() ph := NewTextPullHandler(printer) if ph.OnNodeDownloading(mockFetcher.OciImage) != nil { t.Error("OnNodeDownloading() should not return an error") } validatePrinted(t, "Downloading 0b442c23c1dd oci-image") } func TestTextPullHandler_OnNodeProcessing(t *testing.T) { builder.Reset() ph := NewTextPullHandler(printer) if ph.OnNodeProcessing(mockFetcher.OciImage) != nil { t.Error("OnNodeProcessing() should not return an error") } validatePrinted(t, "Processing 0b442c23c1dd oci-image") } func TestTextPullHandler_OnNodeRestored(t *testing.T) { builder.Reset() ph := NewTextPullHandler(printer) if ph.OnNodeRestored(mockFetcher.OciImage) != nil { t.Error("OnNodeRestored() should not return an error") } validatePrinted(t, "Restored 0b442c23c1dd oci-image") } func TestTextPullHandler_OnNodeSkipped(t *testing.T) { builder.Reset() ph := NewTextPullHandler(printer) if ph.OnNodeSkipped(mockFetcher.OciImage) != nil { t.Error("OnNodeSkipped() should not return an error") } validatePrinted(t, "Skipped 0b442c23c1dd oci-image") } func TestTextPushHandler_OnCopySkipped(t *testing.T) { builder.Reset() ph := NewTextPushHandler(printer, mockFetcher.Fetcher) if ph.OnCopySkipped(ctx, mockFetcher.OciImage) != nil { t.Error("OnCopySkipped() should not return an error") } validatePrinted(t, "Exists 0b442c23c1dd oci-image") } func TestTextPushHandler_OnEmptyArtifact(t *testing.T) { builder.Reset() ph := NewTextPushHandler(printer, mockFetcher.Fetcher) if ph.OnEmptyArtifact() != nil { t.Error("OnEmptyArtifact() should not return an error") } validatePrinted(t, "Uploading empty artifact") } func TestTextPushHandler_OnFileLoading(t *testing.T) { builder.Reset() ph := NewTextPushHandler(printer, mockFetcher.Fetcher) if ph.OnFileLoading("name") != nil { t.Error("OnFileLoading() should not return an error") } validatePrinted(t, "") } func TestTextPushHandler_PostCopy(t *testing.T) { builder.Reset() ph := NewTextPushHandler(printer, mockFetcher.Fetcher) if ph.PostCopy(ctx, mockFetcher.OciImage) != nil { t.Error("PostCopy() should not return an error") } validatePrinted(t, "Uploaded 0b442c23c1dd oci-image") } func TestTextPushHandler_PreCopy(t *testing.T) { builder.Reset() ph := NewTextPushHandler(printer, mockFetcher.Fetcher) if ph.PreCopy(ctx, mockFetcher.OciImage) != nil { t.Error("PreCopy() should not return an error") } validatePrinted(t, "Uploading 0b442c23c1dd oci-image") } func TestTextManifestPushHandler_OnPushSkipped(t *testing.T) { mph := NewTextManifestPushHandler(printer, ocispec.Descriptor{}) if mph.OnManifestPushSkipped() != nil { t.Error("OnManifestExists() should not return an error") } } func TestTextManifestIndexUpdateHandler_OnManifestAdded(t *testing.T) { tests := []struct { name string printer *output.Printer ref string desc ocispec.Descriptor wantErr bool }{ { name: "ref is a digest", printer: output.NewPrinter(os.Stdout, os.Stderr), ref: "sha256:fd6ed2f36b5465244d5dc86cb4e7df0ab8a9d24adc57825099f522fe009a22bb", desc: ocispec.Descriptor{MediaType: "test", Digest: "sha256:fd6ed2f36b5465244d5dc86cb4e7df0ab8a9d24adc57825099f522fe009a22bb", Size: 25}, wantErr: false, }, { name: "ref is not a digest", printer: output.NewPrinter(os.Stdout, os.Stderr), ref: "v1", desc: ocispec.Descriptor{MediaType: "test", Digest: "sha256:fd6ed2f36b5465244d5dc86cb4e7df0ab8a9d24adc57825099f522fe009a22bb", Size: 25}, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { miuh := &TextManifestIndexUpdateHandler{ printer: tt.printer, } if err := miuh.OnManifestAdded(tt.ref, tt.desc); (err != nil) != tt.wantErr { t.Errorf("TextManifestIndexUpdateHandler.OnManifestAdded() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestTextManifestIndexUpdateHandler_OnIndexMerged(t *testing.T) { tests := []struct { name string printer *output.Printer ref string desc ocispec.Descriptor wantErr bool }{ { name: "ref is a digest", printer: output.NewPrinter(os.Stdout, os.Stderr), ref: "sha256:fd6ed2f36b5465244d5dc86cb4e7df0ab8a9d24adc57825099f522fe009a22bb", desc: ocispec.Descriptor{MediaType: "test", Digest: "sha256:fd6ed2f36b5465244d5dc86cb4e7df0ab8a9d24adc57825099f522fe009a22bb", Size: 25}, wantErr: false, }, { name: "ref is not a digest", printer: output.NewPrinter(os.Stdout, os.Stderr), ref: "v1", desc: ocispec.Descriptor{MediaType: "test", Digest: "sha256:fd6ed2f36b5465244d5dc86cb4e7df0ab8a9d24adc57825099f522fe009a22bb", Size: 25}, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { miuh := &TextManifestIndexUpdateHandler{ printer: tt.printer, } if err := miuh.OnIndexMerged(tt.ref, tt.desc); (err != nil) != tt.wantErr { t.Errorf("TextManifestIndexUpdateHandler.OnIndexMerged() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestTextBackupHandler(t *testing.T) { t.Run("OnCopySkipped", func(t *testing.T) { builder.Reset() bh := NewTextBackupHandler(printer, mockFetcher.Fetcher) if bh.OnCopySkipped(ctx, mockFetcher.OciImage) != nil { t.Error("OnCopySkipped() should not return an error") } validatePrinted(t, "Exists 0b442c23c1dd oci-image") }) t.Run("PreCopy", func(t *testing.T) { builder.Reset() bh := NewTextBackupHandler(printer, mockFetcher.Fetcher) if bh.PreCopy(ctx, mockFetcher.OciImage) != nil { t.Error("PreCopy() should not return an error") } validatePrinted(t, "Pulling 0b442c23c1dd oci-image") }) t.Run("PostCopy", func(t *testing.T) { builder.Reset() bh := NewTextBackupHandler(printer, mockFetcher.Fetcher) if bh.PostCopy(ctx, mockFetcher.OciImage) != nil { t.Error("PostCopy() should not return an error") } validatePrinted(t, "Pulled 0b442c23c1dd oci-image") }) t.Run("PostCopy_Skipped", func(t *testing.T) { builder.Reset() bh := &TextBackupHandler{ printer: printer, fetcher: mockFetcher.Fetcher, committed: &sync.Map{}, } bh.committed.Store(mockFetcher.ImageLayer.Digest.String(), mockFetcher.ImageLayer.Annotations[ocispec.AnnotationTitle]+"bogus") if err := bh.PostCopy(ctx, mockFetcher.OciImage); err != nil { t.Error("PostCopy() returns unexpected err:", err) } validatePrinted(t, "Skipped f6b87e8e0fe1 layer\nPulled 0b442c23c1dd oci-image") }) t.Run("StartTracking", func(t *testing.T) { bh := NewTextBackupHandler(printer, mockFetcher.Fetcher) gt := memory.New() result, err := bh.StartTracking(gt) if err != nil { t.Error("StartTracking() should not return an error") } if result != gt { t.Error("StartTracking() should return the same GraphTarget") } }) t.Run("StopTracking", func(t *testing.T) { bh := NewTextBackupHandler(printer, mockFetcher.Fetcher) if bh.StopTracking() != nil { t.Error("StopTracking() should not return an error") } }) } func TestTextRestoreHandler(t *testing.T) { t.Run("OnCopySkipped", func(t *testing.T) { builder.Reset() rh := NewTextRestoreHandler(printer, mockFetcher.Fetcher) if rh.OnCopySkipped(ctx, mockFetcher.OciImage) != nil { t.Error("OnCopySkipped() should not return an error") } validatePrinted(t, "Exists 0b442c23c1dd oci-image") }) t.Run("PreCopy", func(t *testing.T) { builder.Reset() rh := NewTextRestoreHandler(printer, mockFetcher.Fetcher) if rh.PreCopy(ctx, mockFetcher.OciImage) != nil { t.Error("PreCopy() should not return an error") } validatePrinted(t, "Pushing 0b442c23c1dd oci-image") }) t.Run("PostCopy", func(t *testing.T) { builder.Reset() rh := NewTextRestoreHandler(printer, mockFetcher.Fetcher) if rh.PostCopy(ctx, mockFetcher.OciImage) != nil { t.Error("PostCopy() should not return an error") } validatePrinted(t, "Pushed 0b442c23c1dd oci-image") }) t.Run("PostCopy_Skipped", func(t *testing.T) { builder.Reset() rh := &TextRestoreHandler{ printer: printer, fetcher: mockFetcher.Fetcher, committed: &sync.Map{}, } rh.committed.Store(mockFetcher.ImageLayer.Digest.String(), mockFetcher.ImageLayer.Annotations[ocispec.AnnotationTitle]+"bogus") if err := rh.PostCopy(ctx, mockFetcher.OciImage); err != nil { t.Error("PostCopy() returns unexpected err:", err) } validatePrinted(t, "Skipped f6b87e8e0fe1 layer\nPushed 0b442c23c1dd oci-image") }) t.Run("StartTracking", func(t *testing.T) { rh := NewTextRestoreHandler(printer, mockFetcher.Fetcher) gt := memory.New() result, err := rh.StartTracking(gt) if err != nil { t.Error("StartTracking() should not return an error") } if result != gt { t.Error("StartTracking() should return the same GraphTarget") } }) t.Run("StopTracking", func(t *testing.T) { rh := NewTextRestoreHandler(printer, mockFetcher.Fetcher) if rh.StopTracking() != nil { t.Error("StopTracking() should not return an error") } }) } oras-1.3.3/cmd/oras/internal/display/status/track/000077500000000000000000000000001522423464100221045ustar00rootroot00000000000000oras-1.3.3/cmd/oras/internal/display/status/track/reader.go000066400000000000000000000037601522423464100237030ustar00rootroot00000000000000/* Copyright The ORAS 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 track import ( "io" "os" ocispec "github.com/opencontainers/image-spec/specs-go/v1" sprogress "oras.land/oras/cmd/oras/internal/display/status/progress" "oras.land/oras/internal/progress" ) // Reader is a tracked io.Reader. type Reader struct { io.Reader tracker progress.Tracker manager progress.Manager } // NewReader returns a new reader with tracked progress. func NewReader(r io.Reader, descriptor ocispec.Descriptor, actionPrompt string, donePrompt string, tty *os.File) (*Reader, error) { prompt := map[progress.State]string{ progress.StateInitialized: actionPrompt, progress.StateTransmitting: actionPrompt, progress.StateTransmitted: donePrompt, } manager, err := sprogress.NewManager(tty, prompt) if err != nil { return nil, err } return newReader(r, descriptor, manager) } func newReader(r io.Reader, descriptor ocispec.Descriptor, manager progress.Manager) (*Reader, error) { tracker, err := manager.Track(descriptor) if err != nil { return nil, err } return &Reader{ Reader: progress.TrackReader(tracker, r), tracker: tracker, manager: manager, }, nil } // Tracker returns the progress tracker. func (r *Reader) Tracker() progress.Tracker { return r.tracker } // StopTracker stops the messenger channel. func (r *Reader) StopTracker() { _ = r.tracker.Close() } // StopManager stops the messenger channel and related manager. func (r *Reader) StopManager() { _ = r.tracker.Close() _ = r.manager.Close() } oras-1.3.3/cmd/oras/internal/display/status/track/target.go000066400000000000000000000070761522423464100237330ustar00rootroot00000000000000/* Copyright The ORAS 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 track import ( "context" "errors" "io" "os" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2" "oras.land/oras-go/v2/errdef" "oras.land/oras-go/v2/registry" sprogress "oras.land/oras/cmd/oras/internal/display/status/progress" "oras.land/oras/internal/progress" ) // GraphTarget is a tracked oras.GraphTarget. type GraphTarget interface { oras.GraphTarget io.Closer Report(desc ocispec.Descriptor, state progress.State) error } type graphTarget struct { oras.GraphTarget manager progress.Manager } type referenceGraphTarget struct { *graphTarget } // NewTarget creates a new tracked Target. func NewTarget(t oras.GraphTarget, prompts map[progress.State]string, tty *os.File) (GraphTarget, error) { manager, err := sprogress.NewManager(tty, prompts) if err != nil { return nil, err } gt := &graphTarget{ GraphTarget: t, manager: manager, } if _, ok := t.(registry.ReferencePusher); ok { return &referenceGraphTarget{ graphTarget: gt, }, nil } return gt, nil } // Mount mounts a blob from a specified repository. This method is invoked only // by the `*remote.Repository` target. func (t *graphTarget) Mount(ctx context.Context, desc ocispec.Descriptor, fromRepo string, getContent func() (io.ReadCloser, error)) error { mounter := t.GraphTarget.(registry.Mounter) return mounter.Mount(ctx, desc, fromRepo, getContent) } // Push pushes the content to the base oras.GraphTarget with tracking. func (t *graphTarget) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error { r, err := newReader(content, expected, t.manager) if err != nil { return err } defer r.StopTracker() if err := progress.Start(r.Tracker()); err != nil { return err } if err := t.GraphTarget.Push(ctx, expected, r); err != nil { if errors.Is(err, errdef.ErrAlreadyExists) { // allowed error types in oras-go oci and memory store if err := progress.Done(r.Tracker()); err != nil { return err } } return err } return progress.Done(r.Tracker()) } // PushReference pushes the content to the base oras.GraphTarget with tracking. func (rgt *referenceGraphTarget) PushReference(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error { r, err := newReader(content, expected, rgt.manager) if err != nil { return err } defer r.StopTracker() if err := progress.Start(r.Tracker()); err != nil { return err } err = rgt.GraphTarget.(registry.ReferencePusher).PushReference(ctx, expected, r, reference) if err != nil { return err } return progress.Done(r.Tracker()) } // Close closes the tracking manager. func (t *graphTarget) Close() error { return t.manager.Close() } // Report prompts the user with the provided state and descriptor. func (t *graphTarget) Report(desc ocispec.Descriptor, state progress.State) error { tracker, err := t.manager.Track(desc) if err != nil { return err } if err = tracker.Update(progress.Status{ State: state, Offset: desc.Size, }); err != nil { return err } return tracker.Close() } oras-1.3.3/cmd/oras/internal/display/status/track/target_test.go000066400000000000000000000072661522423464100247730ustar00rootroot00000000000000//go:build !windows && !darwin /* Copyright The ORAS 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 track import ( "bytes" "context" "errors" "io" "testing" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2" "oras.land/oras-go/v2/content/memory" "oras.land/oras-go/v2/errdef" "oras.land/oras-go/v2/registry/remote" "oras.land/oras/internal/progress" "oras.land/oras/internal/testutils" ) type testReferenceGraphTarget struct { oras.GraphTarget } func (t *testReferenceGraphTarget) PushReference(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error { err := t.Push(ctx, expected, content) if err != nil { return err } return t.Tag(ctx, expected, reference) } func Test_referenceGraphTarget_PushReference(t *testing.T) { // prepare pty, device, err := testutils.NewPty() if err != nil { t.Fatal(err) } defer func() { _ = device.Close() }() src := memory.New() content := []byte("test") r := bytes.NewReader(content) desc := ocispec.Descriptor{ MediaType: "application/octet-stream", Digest: digest.FromBytes(content), Size: int64(len(content)), } // test tag := "tagged" donePrompt := "done" prompt := map[progress.State]string{ progress.StateTransmitted: donePrompt, } target, err := NewTarget(&testReferenceGraphTarget{src}, prompt, device) if err != nil { t.Fatal(err) } if rgt, ok := target.(*referenceGraphTarget); ok { if err := rgt.PushReference(context.Background(), desc, r, tag); err != nil { t.Fatal(err) } if err := rgt.manager.Close(); err != nil { t.Fatal(err) } } else { t.Fatal("not testing based on a referenceGraphTarget") } // validate if err = testutils.MatchPty(pty, device, donePrompt, desc.MediaType, "100.00%", desc.Digest.String()); err != nil { t.Fatal(err) } } func Test_referenceGraphTarget_Mount(_ *testing.T) { target := graphTarget{GraphTarget: &remote.Repository{}} _ = target.Mount(context.Background(), ocispec.Descriptor{}, "", nil) } func Test_graphTarget_Push_alreadyExists(t *testing.T) { // prepare pty, device, err := testutils.NewPty() if err != nil { t.Fatal(err) } defer func() { _ = device.Close() }() src := memory.New() content := []byte("test") r := bytes.NewReader(content) desc := ocispec.Descriptor{ MediaType: "application/octet-stream", Digest: digest.FromBytes(content), Size: int64(len(content)), } if err := src.Push(context.Background(), desc, r); err != nil { t.Fatal("Failed to prepare test environment:", err) } // test donePrompt := "done" prompt := map[progress.State]string{ progress.StateTransmitted: donePrompt, } target, err := NewTarget(src, prompt, device) if err != nil { t.Fatal(err) } if gt, ok := target.(*graphTarget); ok { if err := gt.Push(context.Background(), desc, r); !errors.Is(err, errdef.ErrAlreadyExists) { t.Fatal("Expected ErrAlreadyExists, got:", err) } if err := gt.manager.Close(); err != nil { t.Fatal(err) } } else { t.Fatal("not testing based on a referenceGraphTarget") } // validate if err = testutils.MatchPty(pty, device, donePrompt, desc.MediaType, "100.00%", desc.Digest.String()); err != nil { t.Fatal(err) } } oras-1.3.3/cmd/oras/internal/display/status/tty.go000066400000000000000000000312441522423464100221530ustar00rootroot00000000000000/* Copyright The ORAS 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 status import ( "context" "os" "sync" "oras.land/oras/internal/graph" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2" "oras.land/oras-go/v2/content" "oras.land/oras/cmd/oras/internal/display/status/track" "oras.land/oras/internal/progress" ) // TTYPushHandler handles TTY status output for push command. type TTYPushHandler struct { tty *os.File tracked track.GraphTarget committed *sync.Map fetcher content.Fetcher } // NewTTYPushHandler returns a new handler for push status events. func NewTTYPushHandler(tty *os.File, fetcher content.Fetcher) PushHandler { return &TTYPushHandler{ tty: tty, fetcher: fetcher, committed: &sync.Map{}, } } // OnFileLoading is called before loading a file. func (ph *TTYPushHandler) OnFileLoading(_ string) error { return nil } // OnEmptyArtifact is called when no file is loaded for an artifact push. func (ph *TTYPushHandler) OnEmptyArtifact() error { return nil } // TrackTarget returns a tracked target. func (ph *TTYPushHandler) TrackTarget(gt oras.GraphTarget) (oras.GraphTarget, StopTrackTargetFunc, error) { prompt := map[progress.State]string{ progress.StateInitialized: PushPromptUploading, progress.StateTransmitting: PushPromptUploading, progress.StateTransmitted: PushPromptUploaded, progress.StateExists: PushPromptExists, progress.StateSkipped: PushPromptSkipped, } tracked, err := track.NewTarget(gt, prompt, ph.tty) if err != nil { return nil, nil, err } ph.tracked = tracked return tracked, tracked.Close, nil } // OnCopySkipped is called when an object already exists. func (ph *TTYPushHandler) OnCopySkipped(_ context.Context, desc ocispec.Descriptor) error { ph.committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) return ph.tracked.Report(desc, progress.StateExists) } // PreCopy implements PreCopy of CopyHandler. func (ph *TTYPushHandler) PreCopy(_ context.Context, _ ocispec.Descriptor) error { return nil } // PostCopy implements PostCopy of CopyHandler. func (ph *TTYPushHandler) PostCopy(ctx context.Context, desc ocispec.Descriptor) error { ph.committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) successors, err := graph.FilteredSuccessors(ctx, desc, ph.fetcher, DeduplicatedFilter(ph.committed)) if err != nil { return err } for _, successor := range successors { if err = ph.tracked.Report(successor, progress.StateSkipped); err != nil { return err } } return nil } // NewTTYAttachHandler returns a new handler for attach status events. func NewTTYAttachHandler(tty *os.File, fetcher content.Fetcher) AttachHandler { return NewTTYPushHandler(tty, fetcher) } // TTYPullHandler handles TTY status output for pull events. type TTYPullHandler struct { tty *os.File tracked track.GraphTarget } // NewTTYPullHandler returns a new handler for Pull status events. func NewTTYPullHandler(tty *os.File) PullHandler { return &TTYPullHandler{ tty: tty, } } // OnNodeDownloading implements PullHandler. func (ph *TTYPullHandler) OnNodeDownloading(_ ocispec.Descriptor) error { return nil } // OnNodeDownloaded implements PullHandler. func (ph *TTYPullHandler) OnNodeDownloaded(_ ocispec.Descriptor) error { return nil } // OnNodeProcessing implements PullHandler. func (ph *TTYPullHandler) OnNodeProcessing(_ ocispec.Descriptor) error { return nil } // OnNodeRestored implements PullHandler. func (ph *TTYPullHandler) OnNodeRestored(desc ocispec.Descriptor) error { return ph.tracked.Report(desc, progress.StateRestored) } // OnNodeSkipped implements PullHandler. func (ph *TTYPullHandler) OnNodeSkipped(desc ocispec.Descriptor) error { return ph.tracked.Report(desc, progress.StateSkipped) } // TrackTarget returns a tracked target. func (ph *TTYPullHandler) TrackTarget(gt oras.GraphTarget) (oras.GraphTarget, StopTrackTargetFunc, error) { prompt := map[progress.State]string{ progress.StateInitialized: PullPromptDownloading, progress.StateTransmitting: PullPromptDownloading, progress.StateTransmitted: PullPromptPulled, progress.StateSkipped: PullPromptSkipped, progress.StateRestored: PullPromptRestored, } tracked, err := track.NewTarget(gt, prompt, ph.tty) if err != nil { return nil, nil, err } ph.tracked = tracked return tracked, tracked.Close, nil } // TTYCopyHandler handles tty status output for copy events. type TTYCopyHandler struct { tty *os.File committed sync.Map tracked track.GraphTarget } // NewTTYCopyHandler returns a new handler for copy command. func NewTTYCopyHandler(tty *os.File) CopyHandler { return &TTYCopyHandler{ tty: tty, } } // StartTracking returns a tracked target from a graph target. func (ch *TTYCopyHandler) StartTracking(gt oras.GraphTarget) (oras.GraphTarget, error) { prompt := map[progress.State]string{ progress.StateInitialized: copyPromptCopying, progress.StateTransmitting: copyPromptCopying, progress.StateTransmitted: copyPromptCopied, progress.StateExists: copyPromptExists, progress.StateSkipped: copyPromptSkipped, progress.StateMounted: copyPromptMounted, } var err error ch.tracked, err = track.NewTarget(gt, prompt, ch.tty) if err != nil { return nil, err } return ch.tracked, err } // StopTracking ends the copy tracking for the target. func (ch *TTYCopyHandler) StopTracking() error { return ch.tracked.Close() } // OnCopySkipped is called when an object already exists. func (ch *TTYCopyHandler) OnCopySkipped(_ context.Context, desc ocispec.Descriptor) error { ch.committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) return ch.tracked.Report(desc, progress.StateExists) } // PreCopy implements PreCopy of CopyHandler. func (ch *TTYCopyHandler) PreCopy(context.Context, ocispec.Descriptor) error { return nil } // PostCopy implements PostCopy of CopyHandler. func (ch *TTYCopyHandler) PostCopy(ctx context.Context, desc ocispec.Descriptor) error { ch.committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) successors, err := graph.FilteredSuccessors(ctx, desc, ch.tracked, DeduplicatedFilter(&ch.committed)) if err != nil { return err } for _, successor := range successors { if err = ch.tracked.Report(successor, progress.StateSkipped); err != nil { return err } } return nil } // OnMounted implements OnMounted of CopyHandler. func (ch *TTYCopyHandler) OnMounted(_ context.Context, desc ocispec.Descriptor) error { ch.committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) return ch.tracked.Report(desc, progress.StateMounted) } // TTYBackupHandler handles tty status output for backup events. type TTYBackupHandler struct { tty *os.File committed *sync.Map tracked track.GraphTarget fetcher content.Fetcher } // NewTTYBackupHandler returns a new handler for backup command. func NewTTYBackupHandler(tty *os.File, fetcher content.Fetcher) BackupHandler { return &TTYBackupHandler{ tty: tty, committed: &sync.Map{}, fetcher: fetcher, } } // StartTracking returns a tracked target from a graph target. func (bh *TTYBackupHandler) StartTracking(gt oras.GraphTarget) (oras.GraphTarget, error) { prompts := map[progress.State]string{ progress.StateInitialized: backupPromptPulling, progress.StateTransmitting: backupPromptPulling, progress.StateTransmitted: backupPromptPulled, progress.StateExists: backupPromptExists, progress.StateSkipped: backupPromptSkipped, } var err error bh.tracked, err = track.NewTarget(gt, prompts, bh.tty) if err != nil { return nil, err } return bh.tracked, err } // StopTracking ends the backup tracking for the target. func (bh *TTYBackupHandler) StopTracking() error { return bh.tracked.Close() } // OnCopySkipped implements OnCopySkipped of BackupHandler. func (bh *TTYBackupHandler) OnCopySkipped(_ context.Context, desc ocispec.Descriptor) error { bh.committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) return bh.tracked.Report(desc, progress.StateExists) } // PreCopy implements PreCopy of BackupHandler. func (bh *TTYBackupHandler) PreCopy(_ context.Context, _ ocispec.Descriptor) error { return nil } // PostCopy implements PostCopy of BackupHandler. func (bh *TTYBackupHandler) PostCopy(ctx context.Context, desc ocispec.Descriptor) error { bh.committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) successors, err := graph.FilteredSuccessors(ctx, desc, bh.fetcher, DeduplicatedFilter(bh.committed)) if err != nil { return err } for _, successor := range successors { if err = bh.tracked.Report(successor, progress.StateSkipped); err != nil { return err } } return nil } // TTYRestoreHandler handles tty status output for restore events. type TTYRestoreHandler struct { tty *os.File committed *sync.Map tracked track.GraphTarget fetcher content.Fetcher } // NewTTYRestoreHandler returns a new handler for restore command. func NewTTYRestoreHandler(tty *os.File, fetcher content.Fetcher) RestoreHandler { return &TTYRestoreHandler{ tty: tty, committed: &sync.Map{}, fetcher: fetcher, } } // StartTracking returns a tracked target from a graph target. func (rh *TTYRestoreHandler) StartTracking(gt oras.GraphTarget) (oras.GraphTarget, error) { prompts := map[progress.State]string{ progress.StateInitialized: restorePromptPushing, progress.StateTransmitting: restorePromptPushing, progress.StateTransmitted: restorePromptPushed, progress.StateExists: restorePromptExists, progress.StateSkipped: restorePromptSkipped, } var err error rh.tracked, err = track.NewTarget(gt, prompts, rh.tty) if err != nil { return nil, err } return rh.tracked, err } // StopTracking ends the restore tracking for the target. func (rh *TTYRestoreHandler) StopTracking() error { return rh.tracked.Close() } // OnCopySkipped implements OnCopySkipped of RestoreHandler. func (rh *TTYRestoreHandler) OnCopySkipped(_ context.Context, desc ocispec.Descriptor) error { rh.committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) return rh.tracked.Report(desc, progress.StateExists) } // PreCopy implements PreCopy of RestoreHandler. func (rh *TTYRestoreHandler) PreCopy(_ context.Context, _ ocispec.Descriptor) error { return nil } // PostCopy implements PostCopy of RestoreHandler. func (rh *TTYRestoreHandler) PostCopy(ctx context.Context, desc ocispec.Descriptor) error { rh.committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) successors, err := graph.FilteredSuccessors(ctx, desc, rh.fetcher, DeduplicatedFilter(rh.committed)) if err != nil { return err } for _, successor := range successors { if err = rh.tracked.Report(successor, progress.StateSkipped); err != nil { return err } } return nil } // TTYBlobPushHandler handles tty status output for blob push events. type TTYBlobPushHandler struct { desc ocispec.Descriptor tty *os.File tracked track.GraphTarget } // NewTTYBlobPushHandler returns a new handler for blob push command. func NewTTYBlobPushHandler(tty *os.File, desc ocispec.Descriptor) BlobPushHandler { return &TTYBlobPushHandler{ tty: tty, desc: desc, } } // StartTracking returns a tracked target from a graph target. func (bph *TTYBlobPushHandler) StartTracking(gt oras.GraphTarget) (oras.GraphTarget, error) { prompt := map[progress.State]string{ progress.StateInitialized: PushPromptUploading, progress.StateTransmitting: PushPromptUploading, progress.StateTransmitted: PushPromptUploaded, progress.StateExists: PushPromptExists, } tracked, err := track.NewTarget(gt, prompt, bph.tty) if err != nil { return nil, err } bph.tracked = tracked return bph.tracked, nil } // StopTracking ends the blob push tracking for the target. func (bph *TTYBlobPushHandler) StopTracking() error { return bph.tracked.Close() } // OnBlobExists implements BlobPushHandler. func (bph *TTYBlobPushHandler) OnBlobExists() error { return bph.tracked.Report(bph.desc, progress.StateExists) } // OnBlobUploading implements BlobPushHandler. func (bph *TTYBlobPushHandler) OnBlobUploading() error { return nil } // OnBlobUploaded implements BlobPushHandler. func (bph *TTYBlobPushHandler) OnBlobUploaded() error { return nil } oras-1.3.3/cmd/oras/internal/display/status/tty_console_test.go000066400000000000000000000111301522423464100247240ustar00rootroot00000000000000//go:build !windows && !darwin /* Copyright The ORAS 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 status import ( "strconv" "testing" "oras.land/oras-go/v2" "oras.land/oras-go/v2/content/memory" "oras.land/oras/internal/testutils" ) type testGraphTarget struct { oras.GraphTarget } func TestTTYPushHandler_TrackTarget(t *testing.T) { // prepare pty _, child, err := testutils.NewPty() if err != nil { t.Fatal(err) } defer func() { _ = child.Close() }() ph := NewTTYPushHandler(child, mockFetcher.Fetcher) store := memory.New() // test _, fn, err := ph.TrackTarget(store) if err != nil { t.Fatal("TrackTarget() should not return an error") } defer func() { if err := fn(); err != nil { t.Fatal(err) } }() if ttyPushHandler, ok := ph.(*TTYPushHandler); !ok { t.Fatalf("TrackTarget() should return a *TTYPushHandler, got %T", ttyPushHandler) } } func Test_TTYPullHandler_TrackTarget(t *testing.T) { src := memory.New() t.Run("has TTY", func(t *testing.T) { _, device, err := testutils.NewPty() if err != nil { t.Fatal(err) } defer func() { _ = device.Close() }() ph := NewTTYPullHandler(device) got, fn, err := ph.TrackTarget(src) if err != nil { t.Fatal(err) } defer func() { if err := fn(); err != nil { t.Fatal(err) } }() if got == src { t.Fatal("GraphTarget not be modified on TTY") } }) t.Run("invalid TTY", func(t *testing.T) { ph := NewTTYPullHandler(nil) if _, _, err := ph.TrackTarget(src); err == nil { t.Fatal("expected error for no tty but got nil") } }) } func TestTTYCopyHandler_OnMounted(t *testing.T) { pty, child, err := testutils.NewPty() if err != nil { t.Fatal(err) } defer func() { _ = child.Close() }() ch := NewTTYCopyHandler(child) _, err = ch.StartTracking(&testGraphTarget{memory.New()}) if err != nil { t.Fatal(err) } if err = ch.OnMounted(ctx, mockFetcher.OciImage); err != nil { t.Fatalf("OnMounted() should not return an error: %v", err) } if err = ch.StopTracking(); err != nil { t.Fatalf("StopTracking() should not return an error: %v", err) } if err = testutils.MatchPty(pty, child, "✓", "Mounted", strconv.FormatInt(mockFetcher.OciImage.Size, 10), "100.00%", mockFetcher.OciImage.Digest.String()); err != nil { t.Fatal(err) } } func TestTTYCopyHandler_OnCopySkipped(t *testing.T) { pty, child, err := testutils.NewPty() if err != nil { t.Fatal(err) } defer func() { _ = child.Close() }() ch := NewTTYCopyHandler(child) _, err = ch.StartTracking(&testGraphTarget{memory.New()}) if err != nil { t.Fatal(err) } if err = ch.OnCopySkipped(ctx, mockFetcher.OciImage); err != nil { t.Errorf("OnCopySkipped() should not return an error: %v", err) } if err = ch.StopTracking(); err != nil { t.Errorf("StopTracking() should not return an error: %v", err) } if err = testutils.MatchPty(pty, child, "Exists", "oci-image", strconv.FormatInt(mockFetcher.OciImage.Size, 10), "100.00%"); err != nil { t.Fatal(err) } } func TestTTYCopyHandler_PostCopy(t *testing.T) { pty, child, err := testutils.NewPty() if err != nil { t.Fatal(err) } defer func() { _ = child.Close() }() ch := NewTTYCopyHandler(child) _, err = ch.StartTracking(&testGraphTarget{memory.New()}) if err != nil { t.Fatal(err) } if ch.PostCopy(ctx, bogus) == nil { t.Error("PostCopy() should return an error") } if err = ch.StopTracking(); err != nil { t.Errorf("StopTracking() should not return an error: %v", err) } if err = testutils.MatchPty(pty, child, "\x1b[?25l\x1b7\x1b[0m"); err != nil { t.Fatal(err) } } func TestTTYCopyHandler_PreCopy(t *testing.T) { pty, child, err := testutils.NewPty() if err != nil { t.Fatal(err) } defer func() { _ = child.Close() }() ch := NewTTYCopyHandler(child) _, err = ch.StartTracking(&testGraphTarget{memory.New()}) if err != nil { t.Fatal(err) } if err = ch.PreCopy(ctx, mockFetcher.OciImage); err != nil { t.Errorf("PreCopy() should not return an error: %v", err) } if err = ch.StopTracking(); err != nil { t.Errorf("StopTracking() should not return an error: %v", err) } if err = testutils.MatchPty(pty, child, "\x1b[?25l\x1b7\x1b[0m"); err != nil { t.Fatal(err) } } oras-1.3.3/cmd/oras/internal/display/status/tty_test.go000066400000000000000000000216711522423464100232150ustar00rootroot00000000000000/* Copyright The ORAS 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 status import ( "errors" "os" "sync" "testing" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2/content/memory" "oras.land/oras/internal/testutils" ) func TestTTYPushHandler_OnFileLoading(t *testing.T) { ph := NewTTYPushHandler(os.Stdout, mockFetcher.Fetcher) if ph.OnFileLoading("test") != nil { t.Error("OnFileLoading() should not return an error") } } func TestTTYPushHandler_OnEmptyArtifact(t *testing.T) { ph := NewTTYAttachHandler(os.Stdout, mockFetcher.Fetcher) if ph.OnEmptyArtifact() != nil { t.Error("OnEmptyArtifact() should not return an error") } } func TestTTYPushHandler_TrackTarget_invalidTTY(t *testing.T) { ph := NewTTYPushHandler(os.Stdin, mockFetcher.Fetcher) if _, _, err := ph.TrackTarget(nil); err == nil { t.Error("TrackTarget() should return an error for non-tty file") } } func TestTTYPullHandler_OnNodeDownloading(t *testing.T) { ph := NewTTYPullHandler(nil) if err := ph.OnNodeDownloading(ocispec.Descriptor{}); err != nil { t.Error("OnNodeDownloading() should not return an error") } } func TestTTYPullHandler_OnNodeDownloaded(t *testing.T) { ph := NewTTYPullHandler(nil) if err := ph.OnNodeDownloaded(ocispec.Descriptor{}); err != nil { t.Error("OnNodeDownloaded() should not return an error") } } func TestTTYPullHandler_OnNodeProcessing(t *testing.T) { ph := NewTTYPullHandler(nil) if err := ph.OnNodeProcessing(ocispec.Descriptor{}); err != nil { t.Error("OnNodeProcessing() should not return an error") } } func TestTTYPushHandler_PostCopy(t *testing.T) { fetcher := testutils.NewMockFetcher() committed := &sync.Map{} committed.Store(fetcher.ImageLayer.Digest.String(), fetcher.ImageLayer.Annotations[ocispec.AnnotationTitle]) ph := &TTYPushHandler{ tracked: &testutils.PromptDiscarder{}, committed: committed, fetcher: fetcher.Fetcher, } if err := ph.PostCopy(ctx, fetcher.OciImage); err != nil { t.Errorf("unexpected error from PostCopy(): %v", err) } } func TestTTYPushHandler_PostCopy_errGetSuccessor(t *testing.T) { errorFetcher := testutils.NewErrorFetcher() ph := NewTTYPushHandler(nil, errorFetcher) err := ph.PostCopy(ctx, ocispec.Descriptor{ MediaType: ocispec.MediaTypeImageManifest, }) if err.Error() != errorFetcher.ExpectedError.Error() { t.Errorf("PostCopy() should return expected error got %v", err.Error()) } } func TestTTYPushHandler_PostCopy_errPrompt(t *testing.T) { fetcher := testutils.NewMockFetcher() committed := &sync.Map{} committed.Store(fetcher.ImageLayer.Digest.String(), fetcher.ImageLayer.Annotations[ocispec.AnnotationTitle]+"1") wantedError := errors.New("wanted error") ph := &TTYPushHandler{ tracked: testutils.NewErrorPrompt(wantedError), committed: committed, fetcher: fetcher.Fetcher, } if err := ph.PostCopy(ctx, fetcher.OciImage); !errors.Is(err, wantedError) { t.Errorf("PostCopy() should return expected error got %v", err) } } func TestNewTTYBackupHandler(t *testing.T) { handler := NewTTYBackupHandler(os.Stdout, nil) if handler == nil { t.Error("NewTTYBackupHandler() should not return nil") } } func TestTTYBackupHandler_StartTracking_invalidTTY(t *testing.T) { bh := NewTTYBackupHandler(os.Stdin, nil) gt := memory.New() if _, err := bh.StartTracking(gt); err == nil { t.Error("StartTracking() should return an error for non-tty file") } } func TestTTYBackupHandler_OnCopySkipped(t *testing.T) { fetcher := testutils.NewMockFetcher() bh := &TTYBackupHandler{ tracked: &testutils.PromptDiscarder{}, // Keep PromptDiscarder here for Report method committed: &sync.Map{}, fetcher: fetcher.Fetcher, } if err := bh.OnCopySkipped(ctx, fetcher.ImageLayer); err != nil { t.Errorf("OnCopySkipped() should not return an error: %v", err) } // Verify that the descriptor is stored in the committed map if _, ok := bh.committed.Load(fetcher.ImageLayer.Digest.String()); !ok { t.Error("OnCopySkipped() should store the descriptor in the committed map") } } func TestTTYBackupHandler_PreCopy(t *testing.T) { fetcher := testutils.NewMockFetcher() bh := &TTYBackupHandler{} if err := bh.PreCopy(ctx, fetcher.ImageLayer); err != nil { t.Errorf("PreCopy() should not return an error: %v", err) } } func TestTTYBackupHandler_PostCopy(t *testing.T) { fetcher := testutils.NewMockFetcher() committed := &sync.Map{} committed.Store(fetcher.ImageLayer.Digest.String(), fetcher.ImageLayer.Annotations[ocispec.AnnotationTitle]) bh := &TTYBackupHandler{ tracked: &testutils.PromptDiscarder{}, committed: committed, fetcher: fetcher.Fetcher, } if err := bh.PostCopy(ctx, fetcher.OciImage); err != nil { t.Errorf("unexpected error from PostCopy(): %v", err) } } func TestTTYBackupHandler_PostCopy_errGetSuccessor(t *testing.T) { errorFetcher := testutils.NewErrorFetcher() prompt := &testutils.PromptDiscarder{} bh := &TTYBackupHandler{ tracked: prompt, committed: &sync.Map{}, fetcher: errorFetcher, } err := bh.PostCopy(ctx, ocispec.Descriptor{ MediaType: ocispec.MediaTypeImageManifest, }) if err == nil || err.Error() != errorFetcher.ExpectedError.Error() { t.Errorf("PostCopy() should return expected error got %v", err.Error()) } } func TestTTYBackupHandler_PostCopy_errPrompt(t *testing.T) { fetcher := testutils.NewMockFetcher() committed := &sync.Map{} committed.Store(fetcher.ImageLayer.Digest.String(), fetcher.ImageLayer.Annotations[ocispec.AnnotationTitle]+"1") wantedError := errors.New("wanted error") bh := &TTYBackupHandler{ tracked: testutils.NewErrorPrompt(wantedError), committed: committed, fetcher: fetcher.Fetcher, } if err := bh.PostCopy(ctx, fetcher.OciImage); !errors.Is(err, wantedError) { t.Errorf("PostCopy() should return expected error got %v", err) } } func TestNewTTYRestoreHandler(t *testing.T) { handler := NewTTYRestoreHandler(os.Stdout, nil) if handler == nil { t.Error("NewTTYRestoreHandler() should not return nil") } } func TestTTYRestoreHandler_StartTracking_invalidTTY(t *testing.T) { rh := NewTTYRestoreHandler(os.Stdin, nil) gt := memory.New() if _, err := rh.StartTracking(gt); err == nil { t.Error("StartTracking() should return an error for non-tty file") } } func TestTTYRestoreHandler_OnCopySkipped(t *testing.T) { fetcher := testutils.NewMockFetcher() rh := &TTYRestoreHandler{ tracked: &testutils.PromptDiscarder{}, committed: &sync.Map{}, fetcher: fetcher.Fetcher, } if err := rh.OnCopySkipped(ctx, fetcher.ImageLayer); err != nil { t.Errorf("OnCopySkipped() should not return an error: %v", err) } // Verify that the descriptor is stored in the committed map if _, ok := rh.committed.Load(fetcher.ImageLayer.Digest.String()); !ok { t.Error("OnCopySkipped() should store the descriptor in the committed map") } } func TestTTYRestoreHandler_PreCopy(t *testing.T) { fetcher := testutils.NewMockFetcher() rh := &TTYRestoreHandler{} if err := rh.PreCopy(ctx, fetcher.ImageLayer); err != nil { t.Errorf("PreCopy() should not return an error: %v", err) } } func TestTTYRestoreHandler_PostCopy(t *testing.T) { fetcher := testutils.NewMockFetcher() committed := &sync.Map{} committed.Store(fetcher.ImageLayer.Digest.String(), fetcher.ImageLayer.Annotations[ocispec.AnnotationTitle]) rh := &TTYRestoreHandler{ tracked: &testutils.PromptDiscarder{}, committed: committed, fetcher: fetcher.Fetcher, } if err := rh.PostCopy(ctx, fetcher.OciImage); err != nil { t.Errorf("unexpected error from PostCopy(): %v", err) } } func TestTTYRestoreHandler_PostCopy_errGetSuccessor(t *testing.T) { errorFetcher := testutils.NewErrorFetcher() prompt := &testutils.PromptDiscarder{} rh := &TTYRestoreHandler{ tracked: prompt, committed: &sync.Map{}, fetcher: errorFetcher, } err := rh.PostCopy(ctx, ocispec.Descriptor{ MediaType: ocispec.MediaTypeImageManifest, }) if err == nil || err.Error() != errorFetcher.ExpectedError.Error() { t.Errorf("PostCopy() should return expected error got %v", err) } } func TestTTYRestoreHandler_PostCopy_errPrompt(t *testing.T) { fetcher := testutils.NewMockFetcher() committed := &sync.Map{} committed.Store(fetcher.ImageLayer.Digest.String(), fetcher.ImageLayer.Annotations[ocispec.AnnotationTitle]+"1") wantedError := errors.New("wanted error") rh := &TTYRestoreHandler{ tracked: testutils.NewErrorPrompt(wantedError), committed: committed, fetcher: fetcher.Fetcher, } if err := rh.PostCopy(ctx, fetcher.OciImage); !errors.Is(err, wantedError) { t.Errorf("PostCopy() should return expected error got %v", err) } } oras-1.3.3/cmd/oras/internal/display/status/utils.go000066400000000000000000000044231522423464100224720ustar00rootroot00000000000000/* Copyright The ORAS 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 status import ( "sync" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) // Prompts for pull events. const ( PullPromptDownloading = "Downloading" PullPromptPulled = "Pulled " PullPromptProcessing = "Processing " PullPromptSkipped = "Skipped " PullPromptRestored = "Restored " PullPromptDownloaded = "Downloaded " ) // Prompts for push/attach events. const ( PushPromptUploaded = "Uploaded " PushPromptUploading = "Uploading" PushPromptSkipped = "Skipped " PushPromptExists = "Exists " ) // Prompts for cp events. const ( copyPromptExists = "Exists " copyPromptCopying = "Copying" copyPromptCopied = "Copied " copyPromptSkipped = "Skipped" copyPromptMounted = "Mounted" ) // Prompts for backup events. const ( backupPromptPulling = "Pulling " backupPromptPulled = "Pulled " backupPromptExists = "Exists " backupPromptSkipped = "Skipped " ) const ( restorePromptPushing = "Pushing " restorePromptPushed = "Pushed " restorePromptExists = "Exists " restorePromptSkipped = "Skipped " ) // Prompts for index events. const ( IndexPromptFetching = "Fetching " IndexPromptFetched = "Fetched " IndexPromptAdded = "Added " IndexPromptMerged = "Merged " IndexPromptRemoved = "Removed " IndexPromptPacked = "Packed " IndexPromptPushed = "Pushed " IndexPromptUpdated = "Updated " ) // DeduplicatedFilter filters out deduplicated descriptors. func DeduplicatedFilter(committed *sync.Map) func(desc ocispec.Descriptor) bool { return func(desc ocispec.Descriptor) bool { name := desc.Annotations[ocispec.AnnotationTitle] v, ok := committed.Load(desc.Digest.String()) // committed but not printed == deduplicated return ok && v != name } } oras-1.3.3/cmd/oras/internal/errors/000077500000000000000000000000001522423464100173245ustar00rootroot00000000000000oras-1.3.3/cmd/oras/internal/errors/errors.go000066400000000000000000000162171522423464100211760ustar00rootroot00000000000000/* Copyright The ORAS 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 ( "errors" "fmt" "strings" "github.com/spf13/cobra" "github.com/spf13/pflag" "oras.land/oras-go/v2" "oras.land/oras-go/v2/registry/remote/auth" "oras.land/oras-go/v2/registry/remote/errcode" ) // OperationType stands for certain type of operations. type OperationType int const ( // OperationTypeParseArtifactReference represents parsing artifact // reference operation. OperationTypeParseArtifactReference OperationType = iota + 1 ) // RegistryErrorPrefix is the commandline prefix for errors from registry. const RegistryErrorPrefix = "Error response from registry:" // UnsupportedFormatTypeError generates the error message for an invalid type. type UnsupportedFormatTypeError string // Error implements the error interface. func (e UnsupportedFormatTypeError) Error() string { return "unsupported format type: " + string(e) } // Error is the error type for CLI error messaging. type Error struct { OperationType OperationType Err error Usage string Recommendation string } // Unwrap implements the errors.Wrapper interface. func (o *Error) Unwrap() error { return o.Err } // Error implements the error interface. func (o *Error) Error() string { ret := o.Err.Error() if o.Usage != "" { ret += fmt.Sprintf("\nUsage: %s", o.Usage) } if o.Recommendation != "" { ret += fmt.Sprintf("\n%s", o.Recommendation) } return ret } // CheckArgs checks the args with the checker function. func CheckArgs(checker func(args []string) (bool, string), Usage string) cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { if ok, text := checker(args); !ok { return &Error{ Err: fmt.Errorf(`%q requires %s but got %d`, cmd.CommandPath(), text, len(args)), Usage: fmt.Sprintf("%s %s", cmd.Parent().CommandPath(), cmd.Use), Recommendation: fmt.Sprintf(`Please specify %s as %s. Run "%s -h" for more options and examples`, text, Usage, cmd.CommandPath()), } } return nil } } // Modifier modifies the error during cmd execution. type Modifier interface { ModifyError(cmd *cobra.Command, err error) (modified bool, modifiedErr error) } // Command returns an error-handled cobra command. func Command(cmd *cobra.Command, handler Modifier) *cobra.Command { runE := cmd.RunE cmd.RunE = func(cmd *cobra.Command, args []string) error { err := runE(cmd, args) if err != nil { _, err = handler.ModifyError(cmd, err) return err } return nil } return cmd } // ReportErrResp returns the inner error message from errResp.Errors. // If errResp.Errors is empty, it returns the original errResp. func ReportErrResp(errResp *errcode.ErrorResponse) error { if len(errResp.Errors) == 0 { // Example error string: // GET "registry.example.com/v2/_catalog": response status code 401: 401 return errResp } // Example error string: // unauthorized: authentication required return errResp.Errors } // UnwrapCopyError extracts the underlying error from an oras.CopyError. // If err is of type *oras.CopyError, it returns the inner error (copyErr.Err). // Otherwise, it returns the original error unchanged. func UnwrapCopyError(err error) error { var copyErr *oras.CopyError if errors.As(err, ©Err) { return copyErr.Err } return err } // TrimErrBasicCredentialNotFound trims the credentials from err. // Caller should make sure the err is auth.ErrBasicCredentialNotFound. func TrimErrBasicCredentialNotFound(err error) error { toTrim := err inner := err for { var unwrapper interface{ Unwrap() error } var multiUnwrapper interface{ Unwrap() []error } if errors.As(inner, &unwrapper) { toTrim = inner inner = unwrapper.Unwrap() continue } else if errors.As(inner, &multiUnwrapper) { for _, errItem := range multiUnwrapper.Unwrap() { if errors.Is(errItem, auth.ErrBasicCredentialNotFound) { toTrim = errItem inner = errItem break } } continue } break } return reWrap(err, toTrim, auth.ErrBasicCredentialNotFound) } // reWrap re-wraps outer to inner by trimming out mid, returns inner if extraction fails. // +---------- outer ----------+ +------ outer ------+ // | +---- mid ----+ | | | // | | inner | | => | inner | // | +-------------+ | | | // +---------------------------+ +-------------------+ func reWrap(outer, mid, inner error) error { msgOuter := outer.Error() msgMid := mid.Error() if idx := strings.Index(msgOuter, msgMid); idx > 0 { return fmt.Errorf("%s%w", msgOuter[:idx], inner) } return inner } // NewErrEmptyTagOrDigest creates a new error based on the reference string. func NewErrEmptyTagOrDigest(ref string, cmd *cobra.Command, needsTag bool) error { form := `"@"` errMsg := `no digest specified` if needsTag { form = fmt.Sprintf(`":" or %s`, form) errMsg = "no tag or digest specified" } return &Error{ OperationType: OperationTypeParseArtifactReference, Err: fmt.Errorf(`"%s": %s`, ref, errMsg), Usage: fmt.Sprintf("%s %s", cmd.Parent().CommandPath(), cmd.Use), Recommendation: fmt.Sprintf(`Please specify a reference in the form of %s. Run "%s -h" for more options and examples`, form, cmd.CommandPath()), } } // CheckMutuallyExclusiveFlags checks if any mutually exclusive flags are used // at the same time, returns an error when detecting used exclusive flags. func CheckMutuallyExclusiveFlags(fs *pflag.FlagSet, exclusiveFlagSet ...string) error { changedFlags, _ := checkChangedFlags(fs, exclusiveFlagSet...) if len(changedFlags) >= 2 { flags := strings.Join(changedFlags, ", ") return fmt.Errorf("%s cannot be used at the same time", flags) } return nil } // CheckRequiredTogetherFlags checks if any flags required together are all used, // returns an error when detecting any flags not used while other flags have been used. func CheckRequiredTogetherFlags(fs *pflag.FlagSet, requiredTogetherFlags ...string) error { changed, unchanged := checkChangedFlags(fs, requiredTogetherFlags...) unchangedCount := len(unchanged) if unchangedCount != 0 && unchangedCount != len(requiredTogetherFlags) { changed := strings.Join(changed, ", ") unchanged := strings.Join(unchanged, ", ") return fmt.Errorf("%s must be used in conjunction with %s", changed, unchanged) } return nil } func checkChangedFlags(fs *pflag.FlagSet, flagSet ...string) (changedFlags []string, unchangedFlags []string) { for _, flagName := range flagSet { if fs.Changed(flagName) { changedFlags = append(changedFlags, fmt.Sprintf("--%s", flagName)) } else { unchangedFlags = append(unchangedFlags, fmt.Sprintf("--%s", flagName)) } } return } oras-1.3.3/cmd/oras/internal/errors/errors_test.go000066400000000000000000000111151522423464100222250ustar00rootroot00000000000000/* Copyright The ORAS 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 ( "errors" "fmt" "net/url" "testing" "github.com/spf13/pflag" "oras.land/oras-go/v2" "oras.land/oras-go/v2/registry/remote/errcode" ) func TestCheckMutuallyExclusiveFlags(t *testing.T) { fs := &pflag.FlagSet{} var foo, bar, hello bool fs.BoolVar(&foo, "foo", false, "foo test") fs.BoolVar(&bar, "bar", false, "bar test") fs.BoolVar(&hello, "hello", false, "hello test") fs.Lookup("foo").Changed = true fs.Lookup("bar").Changed = true tests := []struct { name string exclusiveFlagSet []string wantErr bool }{ { "--foo and --bar should not be used at the same time", []string{"foo", "bar"}, true, }, { "--foo and --hello are not used at the same time", []string{"foo", "hello"}, false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if err := CheckMutuallyExclusiveFlags(fs, tt.exclusiveFlagSet...); (err != nil) != tt.wantErr { t.Errorf("CheckMutuallyExclusiveFlags() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestCheckRequiredTogetherFlags(t *testing.T) { fs := &pflag.FlagSet{} var foo, bar, hello, world bool fs.BoolVar(&foo, "foo", false, "foo test") fs.BoolVar(&bar, "bar", false, "bar test") fs.BoolVar(&hello, "hello", false, "hello test") fs.BoolVar(&world, "world", false, "world test") fs.Lookup("foo").Changed = true fs.Lookup("bar").Changed = true tests := []struct { name string requiredTogetherFlags []string wantErr bool }{ { "--foo and --bar are both used, no error is returned", []string{"foo", "bar"}, false, }, { "--foo and --hello are not both used, an error is returned", []string{"foo", "hello"}, true, }, { "none of --hello and --world is used, no error is returned", []string{"hello", "world"}, false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if err := CheckRequiredTogetherFlags(fs, tt.requiredTogetherFlags...); (err != nil) != tt.wantErr { t.Errorf("CheckRequiredTogetherFlags() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestReportErrResp(t *testing.T) { // Test case with empty errors emptyErrorsResp := &errcode.ErrorResponse{ Errors: []errcode.Error{}, StatusCode: 401, URL: &url.URL{Host: "localhost:5000"}, Method: "GET", } // Test case with non-empty errors nonEmptyErrorsResp := &errcode.ErrorResponse{ Errors: []errcode.Error{ { Code: "UNAUTHORIZED", Message: "authentication required", }, { Code: "INVALID_CREDENTIALS", Message: "invalid credentials provided", Detail: "please check your username and password", }, }, StatusCode: 401, URL: &url.URL{Host: "localhost:5000"}, Method: "GET", } tests := []struct { name string errResp *errcode.ErrorResponse wantErr error }{ { name: "empty errors", errResp: emptyErrorsResp, wantErr: emptyErrorsResp, }, { name: "non-empty errors", errResp: nonEmptyErrorsResp, wantErr: nonEmptyErrorsResp.Errors, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := ReportErrResp(tt.errResp) if got.Error() != tt.wantErr.Error() { t.Errorf("ReportErrResp() = %v, want %v", got, tt.wantErr) } }) } } func TestUnwrapCopyError(t *testing.T) { // Create a regular error regularErr := fmt.Errorf("regular error") // Create an oras.CopyError with an inner error innerErr := fmt.Errorf("inner error") copyErr := &oras.CopyError{Err: innerErr} tests := []struct { name string inputErr error wantErr error }{ { name: "nil error", inputErr: nil, wantErr: nil, }, { name: "regular error", inputErr: regularErr, wantErr: regularErr, }, { name: "copy error", inputErr: copyErr, wantErr: innerErr, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotErr := UnwrapCopyError(tt.inputErr) if !errors.Is(gotErr, tt.wantErr) { t.Errorf("UnwrapCopyError() = %v, want %v", gotErr, tt.wantErr) } }) } } oras-1.3.3/cmd/oras/internal/fileref/000077500000000000000000000000001522423464100174245ustar00rootroot00000000000000oras-1.3.3/cmd/oras/internal/fileref/unix.go000066400000000000000000000020361522423464100207370ustar00rootroot00000000000000//go:build !windows /* Copyright The ORAS 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 fileref import ( "fmt" "strings" ) // Parse parses file reference on unix. func Parse(reference string, defaultMetadata string) (filePath, metadata string, err error) { i := strings.LastIndex(reference, ":") if i < 0 { filePath, metadata = reference, defaultMetadata } else { filePath, metadata = reference[:i], reference[i+1:] } if filePath == "" { return "", "", fmt.Errorf("found empty file path in %q", reference) } return filePath, metadata, nil } oras-1.3.3/cmd/oras/internal/fileref/unix_test.go000066400000000000000000000055541522423464100220060ustar00rootroot00000000000000//go:build !windows /* Copyright The ORAS 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 fileref import "testing" func Test_ParseFileReference(t *testing.T) { type args struct { reference string mediaType string } tests := []struct { name string args args wantFilePath string wantMediatype string }{ {"file name and media type", args{"az:b", ""}, "az", "b"}, {"file name and empty media type", args{"az:", ""}, "az", ""}, {"file name and default media type", args{"az", "c"}, "az", "c"}, {"file name and media type, default type ignored", args{"az:b", "c"}, "az", "b"}, {"file name and empty media type, default type ignored", args{"az:", "c"}, "az", ""}, {"colon file name and media type", args{"az:b:c", "d"}, "az:b", "c"}, {"colon file name and empty media type", args{"az:b:", "c"}, "az:b", ""}, {"colon-prefix file name and media type", args{":az:b:c", "d"}, ":az:b", "c"}, {"pure colon file name and media type", args{"::a", "b"}, ":", "a"}, {"pure colon file name and empty media type", args{"::", "a"}, ":", ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotFilePath, gotMediatype, _ := Parse(tt.args.reference, tt.args.mediaType) if gotFilePath != tt.wantFilePath { t.Errorf("Parse() gotFilePath = %v, want %v", gotFilePath, tt.wantFilePath) } if gotMediatype != tt.wantMediatype { t.Errorf("Parse() gotMediatype = %v, want %v", gotMediatype, tt.wantMediatype) } }) } } func TestParse(t *testing.T) { type args struct { reference string mediaType string } tests := []struct { name string args args wantFilePath string wantMediatype string wantErr bool }{ {"no input", args{"", ""}, "", "", true}, {"empty file name and media type", args{":", ""}, "", "", true}, {"empty file name with media type", args{":a", "b"}, "", "", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotFilePath, gotMediatype, err := Parse(tt.args.reference, tt.args.mediaType) if (err != nil) != tt.wantErr { t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) return } if gotFilePath != tt.wantFilePath { t.Errorf("Parse() gotFilePath = %v, want %v", gotFilePath, tt.wantFilePath) } if gotMediatype != tt.wantMediatype { t.Errorf("Parse() gotMediatype = %v, want %v", gotMediatype, tt.wantMediatype) } }) } } oras-1.3.3/cmd/oras/internal/fileref/windows.go000066400000000000000000000030751522423464100214520ustar00rootroot00000000000000//go:build windows /* Copyright The ORAS 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 fileref import ( "fmt" "strings" "unicode" ) // Parse parses file reference into filePath and metadata. func Parse(reference string, defaultMetadata string) (filePath, metadata string, err error) { filePath, metadata = doParse(reference, defaultMetadata) if filePath == "" { return "", "", fmt.Errorf("found empty file path in %q", reference) } if strings.ContainsAny(filePath, `<>"|?*`) { // Reference: https://learn.microsoft.com/windows/win32/fileio/naming-a-file#naming-conventions return "", "", fmt.Errorf("reserved characters found in the file path: %s", filePath) } return filePath, metadata, nil } func doParse(reference string, defaultMetadata string) (filePath, metadata string) { i := strings.LastIndex(reference, ":") if i < 0 || (i == 1 && len(reference) > 2 && unicode.IsLetter(rune(reference[0])) && reference[2] == '\\') { // Relative file path with disk prefix is NOT supported, e.g. `c:file1` return reference, defaultMetadata } return reference[:i], reference[i+1:] } oras-1.3.3/cmd/oras/internal/fileref/windows_test.go000066400000000000000000000112721522423464100225070ustar00rootroot00000000000000//go:build windows /* Copyright The ORAS 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 fileref import ( "testing" ) func Test_doParse(t *testing.T) { type args struct { reference string mediaType string } tests := []struct { name string args args wantFilePath string wantMediatype string }{ {"file name and media type", args{"az:b", ""}, "az", "b"}, {"file name and empty media type", args{"az:", ""}, "az", ""}, {"file name and default media type", args{"az", "c"}, "az", "c"}, {"file name and media type, default type ignored", args{"az:b", "c"}, "az", "b"}, {"file name and empty media type, default type ignored", args{"az:", "c"}, "az", ""}, {"empty file name and media type", args{":a", "b"}, "", "a"}, {"empty file name and empty media type", args{":", "a"}, "", ""}, {"empty name and default media type", args{"", "a"}, "", "a"}, {"colon file name and media type", args{"az:b:c", "d"}, "az:b", "c"}, {"colon file name and empty media type", args{"az:b:", "c"}, "az:b", ""}, {"colon-prefix file name and media type", args{":az:b:c", "d"}, ":az:b", "c"}, {"pure colon file name and media type", args{"::a", "b"}, ":", "a"}, {"pure colon file name and empty media type", args{"::", "a"}, ":", ""}, {"windows file name1 and default type", args{`a:\b`, "c"}, `a:\b`, "c"}, {"windows file name2 and default type", args{`z:b`, "c"}, `z`, "b"}, {"windows file name and media type", args{`a:\b:c`, "d"}, `a:\b`, "c"}, {"windows file name and empty media type", args{`a:\b:`, "c"}, `a:\b`, ""}, {"numeric file name and media type", args{`1:\a`, "b"}, `1`, `\a`}, {"non-windows file name and media type", args{`ab:\c`, ""}, `ab`, `\c`}, {"non-windows file name and media type, default type ignored", args{`1:\a`, "b"}, `1`, `\a`}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotFilePath, gotMediatype := doParse(tt.args.reference, tt.args.mediaType) if gotFilePath != tt.wantFilePath { t.Errorf("doParse() gotFilePath = %v, want %v", gotFilePath, tt.wantFilePath) } if gotMediatype != tt.wantMediatype { t.Errorf("doParse() gotMediatype = %v, want %v", gotMediatype, tt.wantMediatype) } }) } } func TestParse(t *testing.T) { type args struct { reference string mediaType string } tests := []struct { name string args args wantFilePath string wantMediatype string wantErr bool }{ {"valid file name", args{`c:\some-folder\test`, ""}, `c:\some-folder\test`, "", false}, {"valid file name and media type", args{`c:\some-folder\test`, "type"}, `c:\some-folder\test`, "type", false}, {"no input", args{"", ""}, "", "", true}, {"empty file name", args{":", ""}, "", "", true}, {"reserved character1 in file name", args{"<", "a"}, "", "", true}, {"reserved character2 in file name", args{">", "a"}, "", "", true}, {"reserved character3 in file name", args{"*", "a"}, "", "", true}, {"reserved character4 in file name", args{`"`, "a"}, "", "", true}, {"reserved character5 in file name", args{"|", "a"}, "", "", true}, {"reserved character6 in file name", args{"?", "a"}, "", "", true}, {"empty file name, with media type", args{":", "a"}, "", "", true}, {"reserved character1 in file name, with media type", args{"<:", "a"}, "", "", true}, {"reserved character2 in file name, with media type", args{">:", "a"}, "", "", true}, {"reserved character3 in file name, with media type", args{"*:", "a"}, "", "", true}, {"reserved character4 in file name, with media type", args{`":`, "a"}, "", "", true}, {"reserved character5 in file name, with media type", args{"|:", "a"}, "", "", true}, {"reserved character6 in file name, with media type", args{"?:", "a"}, "", "", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotFilePath, gotMediatype, err := Parse(tt.args.reference, tt.args.mediaType) if (err != nil) != tt.wantErr { t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) return } if gotFilePath != tt.wantFilePath { t.Errorf("Parse() gotFilePath = %v, want %v", gotFilePath, tt.wantFilePath) } if gotMediatype != tt.wantMediatype { t.Errorf("Parse() gotMediatype = %v, want %v", gotMediatype, tt.wantMediatype) } }) } } oras-1.3.3/cmd/oras/internal/manifest/000077500000000000000000000000001522423464100176165ustar00rootroot00000000000000oras-1.3.3/cmd/oras/internal/manifest/manifest.go000066400000000000000000000023721522423464100217570ustar00rootroot00000000000000/* Copyright The ORAS 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 manifest import ( "encoding/json" "errors" ) var ( // ErrMediaTypeNotFound is returned when the media type is not specified. ErrMediaTypeNotFound = errors.New(`media type is not specified`) // ErrInvalidJSON is returned when the json file is malformed. ErrInvalidJSON = errors.New("not a valid json file") ) // ExtractMediaType parses the media type field of bytes content in json format. func ExtractMediaType(content []byte) (string, error) { var manifest struct { MediaType string `json:"mediaType"` } if err := json.Unmarshal(content, &manifest); err != nil { return "", ErrInvalidJSON } if manifest.MediaType == "" { return "", ErrMediaTypeNotFound } return manifest.MediaType, nil } oras-1.3.3/cmd/oras/internal/manifest/manifest_test.go000066400000000000000000000042231522423464100230130ustar00rootroot00000000000000/* Copyright The ORAS 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 manifest import ( "errors" "reflect" "testing" ) const ( manifest = `{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.unknown.config.v1+json","digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar","digest":"sha256:5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03","size":6,"annotations":{"org.opencontainers.image.title":"hello.txt"}}]}` manifestMediaType = "application/vnd.oci.image.manifest.v1+json" ) func Test_ExtractMediaType(t *testing.T) { // generate test content content := []byte(manifest) // test ExtractMediaType want := manifestMediaType got, err := ExtractMediaType(content) if err != nil { t.Fatal("ExtractMediaType() error=", err) } if !reflect.DeepEqual(got, want) { t.Errorf("ExtractMediaType() = %v, want %v", got, want) } } func Test_ExtractMediaType_invalidContent_notAJson(t *testing.T) { // generate test content content := []byte("manifest") // test ExtractMediaType _, err := ExtractMediaType(content) expected := "not a valid json file" if err.Error() != expected { t.Fatalf("ExtractMediaType() error = %v, wantErr %v", err, expected) } } func Test_ExtractMediaType_invalidContent_missingMediaType(t *testing.T) { // generate test content content := []byte(`{"schemaVersion":2}`) // test ExtractMediaType _, err := ExtractMediaType(content) if !errors.Is(err, ErrMediaTypeNotFound) { t.Fatalf("ExtractMediaType() error = %v, wantErr %v", err, ErrMediaTypeNotFound) } } oras-1.3.3/cmd/oras/internal/option/000077500000000000000000000000001522423464100173205ustar00rootroot00000000000000oras-1.3.3/cmd/oras/internal/option/annotation.go000066400000000000000000000040021522423464100220150ustar00rootroot00000000000000/* Copyright The ORAS 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 option import ( "errors" "fmt" "strings" "github.com/spf13/cobra" "github.com/spf13/pflag" oerrors "oras.land/oras/cmd/oras/internal/errors" ) var ( errAnnotationFormat = errors.New("annotation value doesn't match the required format") errAnnotationDuplication = errors.New("duplicate annotation key") ) // Annotation option struct. type Annotation struct { // ManifestAnnotations contains raw input of manifest annotation "key=value" pairs ManifestAnnotations []string // Annotations contains parsed manifest and config annotations Annotations map[string]map[string]string } // ApplyFlags applies flags to a command flag set. func (opts *Annotation) ApplyFlags(fs *pflag.FlagSet) { fs.StringArrayVarP(&opts.ManifestAnnotations, "annotation", "a", nil, "manifest annotations") } // Parse parses the input annotation flags. func (opts *Annotation) Parse(*cobra.Command) error { manifestAnnotations := make(map[string]string) for _, anno := range opts.ManifestAnnotations { key, val, success := strings.Cut(anno, "=") if !success { return &oerrors.Error{ Err: errAnnotationFormat, Recommendation: `Please use the correct format in the flag: --annotation "key=value"`, } } if _, ok := manifestAnnotations[key]; ok { return fmt.Errorf("%w: %v, ", errAnnotationDuplication, key) } manifestAnnotations[key] = val } opts.Annotations = map[string]map[string]string{ AnnotationManifest: manifestAnnotations, } return nil } oras-1.3.3/cmd/oras/internal/option/applier.go000066400000000000000000000020501522423464100213000ustar00rootroot00000000000000/* Copyright The ORAS 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 option import ( "github.com/spf13/pflag" ) // FlagApplier applies flags to a command flag set. type FlagApplier interface { ApplyFlags(*pflag.FlagSet) } // ApplyFlags applies applicable fields of the passed-in option pointer to the // target flag set. // NOTE: The option argument need to be a pointer to the options, so its value // becomes addressable. func ApplyFlags(optsPtr any, target *pflag.FlagSet) { for applier := range fields[FlagApplier](optsPtr) { applier.ApplyFlags(target) } } oras-1.3.3/cmd/oras/internal/option/applier_test.go000066400000000000000000000022031522423464100223370ustar00rootroot00000000000000/* Copyright The ORAS 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 option_test import ( "testing" "github.com/spf13/pflag" "oras.land/oras/cmd/oras/internal/option" ) func (t *Test) ApplyFlags(_ *pflag.FlagSet) { *t.CntPtr++ } func TestApplyFlags(t *testing.T) { cnt := 0 type args struct { Test } tests := []struct { name string args args wantErr bool }{ {"flags should be applied once", args{Test{CntPtr: &cnt}}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { option.ApplyFlags(&tt.args, nil) if cnt != 1 { t.Errorf("Expect ApplyFlags() to be called once but got %v", cnt) } }) } } oras-1.3.3/cmd/oras/internal/option/binary_target.go000066400000000000000000000066521522423464100225120ustar00rootroot00000000000000/* Copyright The ORAS 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 option import ( "errors" "fmt" "sync" "github.com/spf13/cobra" "github.com/spf13/pflag" "oras.land/oras-go/v2" oerrors "oras.land/oras/cmd/oras/internal/errors" ) // BinaryTarget struct contains flags and arguments specifying two registries or // image layouts. // BinaryTarget implements errors.Handler interface. type BinaryTarget struct { From Target To Target resolveFlag []string } // EnsureSourceTargetReferenceNotEmpty ensures that from target reference is not empty. func (target *BinaryTarget) EnsureSourceTargetReferenceNotEmpty(cmd *cobra.Command) error { if target.From.Reference == "" { return oerrors.NewErrEmptyTagOrDigest(target.From.RawReference, cmd, true) } return nil } // EnableDistributionSpecFlag set distribution specification flag as applicable. func (target *BinaryTarget) EnableDistributionSpecFlag() { target.From.EnableDistributionSpecFlag() target.To.EnableDistributionSpecFlag() } // ApplyFlags applies flags to a command flag set fs. func (target *BinaryTarget) ApplyFlags(fs *pflag.FlagSet) { target.From.setFlagDetails("from", "source") target.From.ApplyFlags(fs) target.To.setFlagDetails("to", "destination") target.To.ApplyFlags(fs) fs.StringArrayVarP(&target.resolveFlag, "resolve", "", nil, "base DNS rules formatted in `host:port:address[:address_port]` for --from-resolve and --to-resolve") } // Parse parses user-provided flags and arguments into option struct. func (target *BinaryTarget) Parse(cmd *cobra.Command) error { target.From.warned = make(map[string]*sync.Map) target.To.warned = target.From.warned // resolve are parsed in array order, latter will overwrite former target.From.resolveFlag = append(target.resolveFlag, target.From.resolveFlag...) target.To.resolveFlag = append(target.resolveFlag, target.To.resolveFlag...) return Parse(cmd, target) } // ModifyError handles error during cmd execution. func (target *BinaryTarget) ModifyError(cmd *cobra.Command, err error) (bool, error) { var copyErr *oras.CopyError if !errors.As(err, ©Err) { return target.modifyError(cmd, err) } err = copyErr.Err // extract the inner error var errTarget Target switch copyErr.Origin { case oras.CopyErrorOriginSource: errTarget = target.From case oras.CopyErrorOriginDestination: errTarget = target.To default: _, err := target.modifyError(cmd, err) return true, err } _, err = target.modifyError(cmd, err) // Example: Error from source registry for "localhost:5000/test:v1": // Example: Error from destination oci-layout for "oci-dir:v1": cmd.SetErrPrefix(fmt.Sprintf("Error from %s %s for %q:", copyErr.Origin, errTarget.Type, errTarget.RawReference)) return true, err } func (target *BinaryTarget) modifyError(cmd *cobra.Command, err error) (bool, error) { if modified, modifiedErr := target.From.ModifyError(cmd, err); modified { return modified, modifiedErr } return target.To.ModifyError(cmd, err) } oras-1.3.3/cmd/oras/internal/option/binary_target_test.go000066400000000000000000000056521522423464100235500ustar00rootroot00000000000000/* Copyright The ORAS 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 option import ( "errors" "testing" "github.com/spf13/cobra" "oras.land/oras-go/v2" ) func TestBinaryTarget_ModifyError(t *testing.T) { testCases := []struct { name string target *BinaryTarget err error wantModified bool wantPrefix string wantErr error }{ { name: "CopyError with Source origin sets prefix", target: &BinaryTarget{ From: Target{ Type: "registry", RawReference: "localhost:5000/test:v1", }, To: Target{ Type: "oci-layout", RawReference: "oci-dir:v1", }, }, err: &oras.CopyError{Origin: oras.CopyErrorOriginSource, Err: errors.New("source error")}, wantModified: true, wantPrefix: `Error from source registry for "localhost:5000/test:v1":`, wantErr: errors.New("source error"), }, { name: "CopyError with Destination origin sets prefix", target: &BinaryTarget{ From: Target{ Type: "registry", RawReference: "localhost:5000/test:v1", }, To: Target{ Type: "oci-layout", RawReference: "oci-dir:v1", }, }, err: &oras.CopyError{Origin: oras.CopyErrorOriginDestination, Err: errors.New("destination error")}, wantModified: true, wantPrefix: `Error from destination oci-layout for "oci-dir:v1":`, wantErr: errors.New("destination error"), }, { name: "CopyError with unknown origin", target: &BinaryTarget{ From: Target{ Type: "registry", RawReference: "localhost:5000/test:v1", }, To: Target{ Type: "oci-layout", RawReference: "oci-dir:v1", }, }, err: &oras.CopyError{Origin: oras.CopyErrorOrigin(-1), Err: errors.New("unknown error")}, wantPrefix: "Error:", wantModified: true, wantErr: errors.New("unknown error"), }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { cmd := &cobra.Command{} modified, err := tc.target.ModifyError(cmd, tc.err) if modified != tc.wantModified { t.Errorf("ModifyError() modified = %v, want %v", modified, tc.wantModified) } if modified && cmd.ErrPrefix() != tc.wantPrefix { t.Errorf("ModifyError() cmd.ErrPrefix() = %q, want %q", cmd.ErrPrefix(), tc.wantPrefix) } if err.Error() != tc.wantErr.Error() { t.Errorf("ModifyError() error = %q, want %q", err.Error(), tc.wantErr.Error()) } }) } } oras-1.3.3/cmd/oras/internal/option/cache.go000066400000000000000000000020771522423464100207200ustar00rootroot00000000000000/* Copyright The ORAS 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 option import ( "os" "oras.land/oras-go/v2" "oras.land/oras-go/v2/content/oci" "oras.land/oras/internal/cache" ) type Cache struct { Root string } // CachedTarget gets the target storage with caching if cache root is specified. func (opts *Cache) CachedTarget(src oras.ReadOnlyTarget) (oras.ReadOnlyTarget, error) { opts.Root = os.Getenv("ORAS_CACHE") if opts.Root != "" { ociStore, err := oci.New(opts.Root) if err != nil { return nil, err } return cache.New(src, ociStore), nil } return src, nil } oras-1.3.3/cmd/oras/internal/option/cache_test.go000066400000000000000000000030711522423464100217520ustar00rootroot00000000000000/* Copyright The ORAS 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 option import ( "reflect" "testing" "oras.land/oras-go/v2" "oras.land/oras-go/v2/content/memory" "oras.land/oras-go/v2/content/oci" "oras.land/oras/internal/cache" ) var mockTarget oras.ReadOnlyTarget = memory.New() func TestCache_CachedTarget(t *testing.T) { tempDir := t.TempDir() t.Setenv("ORAS_CACHE", tempDir) opts := Cache{} ociStore, err := oci.New(tempDir) if err != nil { t.Fatal("error calling oci.New(), error =", err) } want := cache.New(mockTarget, ociStore) got, err := opts.CachedTarget(mockTarget) if err != nil { t.Fatal("Cache.CachedTarget() error=", err) } if !reflect.DeepEqual(got, want) { t.Fatalf("Cache.CachedTarget() got %v, want %v", got, want) } } func TestCache_CachedTarget_emptyRoot(t *testing.T) { t.Setenv("ORAS_CACHE", "") opts := Cache{} got, err := opts.CachedTarget(mockTarget) if err != nil { t.Fatal("Cache.CachedTarget() error=", err) } if !reflect.DeepEqual(got, mockTarget) { t.Fatalf("Cache.CachedTarget() got %v, want %v", got, mockTarget) } } oras-1.3.3/cmd/oras/internal/option/common.go000066400000000000000000000021711522423464100211400ustar00rootroot00000000000000/* Copyright The ORAS 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 option import ( "github.com/spf13/cobra" "github.com/spf13/pflag" "oras.land/oras/cmd/oras/internal/output" ) // Common option struct. type Common struct { Printer *output.Printer Debug bool } // ApplyFlags applies flags to a command flag set. func (opts *Common) ApplyFlags(fs *pflag.FlagSet) { fs.BoolVarP(&opts.Debug, "debug", "d", false, "output debug logs (implies --no-tty)") } // Parse gets target options from user input. func (opts *Common) Parse(cmd *cobra.Command) error { opts.Printer = output.NewPrinter(cmd.OutOrStdout(), cmd.OutOrStderr()) return nil } oras-1.3.3/cmd/oras/internal/option/confirmation.go000066400000000000000000000027701522423464100223450ustar00rootroot00000000000000/* Copyright The ORAS 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 option import ( "bufio" "fmt" "io" "strings" "github.com/spf13/pflag" ) // Confirmation option struct. type Confirmation struct { Force bool } // ApplyFlags applies flags to a command flag set. func (opts *Confirmation) ApplyFlags(fs *pflag.FlagSet) { fs.BoolVarP(&opts.Force, "force", "f", false, "ignore nonexistent references, never prompt") } // AskForConfirmation prints a propmt to ask for confirmation before doing an // action and takes user input as response. func (opts *Confirmation) AskForConfirmation(r io.Reader, prompt string) (bool, error) { if opts.Force { return true, nil } fmt.Print(prompt, " [y/N] ") var response string scanner := bufio.NewScanner(r) if ok := scanner.Scan(); ok { response = scanner.Text() } if err := scanner.Err(); err != nil { return false, err } switch strings.ToLower(response) { case "y", "yes": return true, nil default: fmt.Println("Operation cancelled.") return false, nil } } oras-1.3.3/cmd/oras/internal/option/confirmation_test.go000066400000000000000000000036241522423464100234030ustar00rootroot00000000000000/* Copyright The ORAS 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 option import ( "reflect" "strings" "testing" "github.com/spf13/pflag" ) func TestConfirmation_ApplyFlags(t *testing.T) { var test struct{ Confirmation } ApplyFlags(&test, pflag.NewFlagSet("oras-test", pflag.ExitOnError)) if test.Force != false { t.Fatalf("expecting Confirmed to be false but got: %v", test.Force) } } func TestConfirmation_AskForConfirmation_forciblyConfirmed(t *testing.T) { opts := Confirmation{ Force: true, } r := strings.NewReader("") got, err := opts.AskForConfirmation(r, "") if err != nil { t.Fatal("Confirmation.AskForConfirmation() error =", err) } if !reflect.DeepEqual(got, true) { t.Fatalf("Confirmation.AskForConfirmation() got %v, want %v", got, true) } } func TestConfirmation_AskForConfirmation_manuallyConfirmed(t *testing.T) { opts := Confirmation{ Force: false, } r := strings.NewReader("yes") got, err := opts.AskForConfirmation(r, "") if err != nil { t.Fatal("Confirmation.AskForConfirmation() error =", err) } if !reflect.DeepEqual(got, true) { t.Fatalf("Confirmation.AskForConfirmation() got %v, want %v", got, true) } r = strings.NewReader("no") got, err = opts.AskForConfirmation(r, "") if err != nil { t.Fatal("Confirmation.AskForConfirmation() error =", err) } if !reflect.DeepEqual(got, false) { t.Fatalf("Confirmation.AskForConfirmation() got %v, want %v", got, false) } } oras-1.3.3/cmd/oras/internal/option/descriptor.go000066400000000000000000000023171522423464100220300ustar00rootroot00000000000000/* Copyright The ORAS 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 option import ( "encoding/json" "fmt" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/spf13/pflag" ) // Descriptor option struct. type Descriptor struct { OutputDescriptor bool } // ApplyFlags applies flags to a command flag set. func (opts *Descriptor) ApplyFlags(fs *pflag.FlagSet) { fs.BoolVarP(&opts.OutputDescriptor, "descriptor", "", false, "output the descriptor") } // Marshal returns the JSON encoding of descriptor. func (opts *Descriptor) Marshal(desc ocispec.Descriptor) ([]byte, error) { b, err := json.Marshal(desc) if err != nil { return nil, fmt.Errorf("failed to marshal descriptor: %w", err) } return b, nil } oras-1.3.3/cmd/oras/internal/option/descriptor_test.go000066400000000000000000000031121522423464100230610ustar00rootroot00000000000000/* Copyright The ORAS 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 option import ( "encoding/json" "reflect" "testing" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/spf13/pflag" ) func TestDescriptor_ApplyFlags(t *testing.T) { var test struct{ Descriptor } ApplyFlags(&test, pflag.NewFlagSet("oras-test", pflag.ExitOnError)) if test.OutputDescriptor != false { t.Fatalf("expecting OutputDescriptor to be false but got: %v", test.OutputDescriptor) } } func TestDescriptor_Marshal(t *testing.T) { // generate test content blob := []byte("hello world") desc := ocispec.Descriptor{ MediaType: "test", Digest: digest.FromBytes(blob), Size: int64(len(blob)), } want, err := json.Marshal(desc) if err != nil { t.Fatal("error calling json.Marshal(), error =", err) } opts := Descriptor{ OutputDescriptor: true, } got, err := opts.Marshal(desc) if err != nil { t.Fatal("Descriptor.Marshal() error =", err) } if !reflect.DeepEqual(got, want) { t.Fatalf("Descriptor.Marshal() got %v, want %v", got, want) } } oras-1.3.3/cmd/oras/internal/option/format.go000066400000000000000000000104151522423464100211400ustar00rootroot00000000000000/* Copyright The ORAS 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 option import ( "bytes" "fmt" "strings" "text/tabwriter" "github.com/spf13/cobra" "github.com/spf13/pflag" oerrors "oras.land/oras/cmd/oras/internal/errors" ) // FormatType represents a format type. type FormatType struct { // Name is the format type name. Name string // Usage is the usage string in help doc. Usage string // HasParams indicates whether the format type has parameters. HasParams bool } // WithUsage returns a new format type with provided usage string. func (ft *FormatType) WithUsage(usage string) *FormatType { return &FormatType{ Name: ft.Name, HasParams: ft.HasParams, Usage: usage, } } // format types var ( FormatTypeJSON = &FormatType{ Name: "json", Usage: "Print in JSON format", } FormatTypeGoTemplate = &FormatType{ Name: "go-template", Usage: "Print output using the given Go template", HasParams: true, } // the table format is deprecated FormatTypeTable = &FormatType{ Name: "table", Usage: "[Deprecated] Get referrers and output in table format", } FormatTypeTree = &FormatType{ Name: "tree", Usage: "Get referrers and print in tree format", } FormatTypeText = &FormatType{ Name: "text", Usage: "Print in text format", } ) // Format contains input and parsed options for formatted output flags. type Format struct { FormatFlag string Type string Template string allowedTypes []*FormatType } // SetTypes sets the default format type and allowed format types. func (f *Format) SetTypes(defaultType *FormatType, otherTypes ...*FormatType) { f.FormatFlag = defaultType.Name f.allowedTypes = append(otherTypes, defaultType) } // ApplyFlags implements FlagProvider.ApplyFlag. func (f *Format) ApplyFlags(fs *pflag.FlagSet) { buf := bytes.NewBufferString("[Experimental] format output using a custom template:") w := tabwriter.NewWriter(buf, 0, 0, 2, ' ', 0) for _, t := range f.allowedTypes { _, _ = fmt.Fprintf(w, "\n'%s':\t%s", t.Name, t.Usage) } _ = w.Flush() // apply flags fs.StringVar(&f.FormatFlag, "format", f.FormatFlag, buf.String()) fs.StringVar(&f.Template, "template", "", "[Experimental] template string used to format output") } // Parse parses the input format flag. func (f *Format) Parse(cmd *cobra.Command) error { // print deprecation message for table format if f.FormatFlag == FormatTypeTable.Name { _, _ = fmt.Fprint(cmd.ErrOrStderr(), "Format \"table\" is deprecated and will be removed in a future release.\n") } if err := f.parseFlag(); err != nil { return err } if f.Type == FormatTypeText.Name { // flag not specified return nil } if f.Type == FormatTypeGoTemplate.Name && f.Template == "" { return &oerrors.Error{ Err: fmt.Errorf("%q format specified but no template given", f.Type), Recommendation: fmt.Sprintf("use `--format %s=TEMPLATE` to specify the template", f.Type), } } var optionalTypes []string for _, t := range f.allowedTypes { if f.Type == t.Name { // type validation passed return nil } optionalTypes = append(optionalTypes, t.Name) } return &oerrors.Error{ Err: fmt.Errorf("invalid format type: %q", f.Type), Recommendation: fmt.Sprintf("supported types: %s", strings.Join(optionalTypes, ", ")), } } func (f *Format) parseFlag() error { f.Type = f.FormatFlag if f.Template != "" { // template explicitly set if f.Type != FormatTypeGoTemplate.Name { return fmt.Errorf("--template must be used with --format %s", FormatTypeGoTemplate.Name) } return nil } for _, t := range f.allowedTypes { if !t.HasParams { continue } prefix := t.Name + "=" if strings.HasPrefix(f.FormatFlag, prefix) { // parse type and add parameter to template f.Type = t.Name f.Template = f.FormatFlag[len(prefix):] } } return nil } oras-1.3.3/cmd/oras/internal/option/interface.go000066400000000000000000000017401522423464100216110ustar00rootroot00000000000000/* Copyright The ORAS 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 option import ( "oras.land/oras-go/v2" "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/registry" ) // ResolvableDeleter a target that resolve and delete type ResolvableDeleter interface { content.Resolver content.Deleter } // ReadOnlyGraphTagFinderTarget represents a read-only graph target with tag // finder capability. type ReadOnlyGraphTagFinderTarget interface { oras.ReadOnlyGraphTarget registry.TagLister } oras-1.3.3/cmd/oras/internal/option/packer.go000066400000000000000000000074741522423464100211300ustar00rootroot00000000000000/* Copyright The ORAS 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 option import ( "context" "encoding/json" "errors" "fmt" "os" "path/filepath" "strings" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/spf13/cobra" "github.com/spf13/pflag" "oras.land/oras-go/v2/content" oerrors "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/cmd/oras/internal/fileref" ) // Pre-defined annotation keys for annotation file const ( AnnotationManifest = "$manifest" AnnotationConfig = "$config" ) var ( errAnnotationConflict = errors.New("`--annotation` and `--annotation-file` cannot be both specified") errPathValidation = errors.New("absolute file path detected. If it's intentional, use --disable-path-validation flag to skip this check") ) // Packer option struct. type Packer struct { Annotation ManifestExportPath string PathValidationDisabled bool AnnotationFilePath string FileRefs []string } // ApplyFlags applies flags to a command flag set. func (opts *Packer) ApplyFlags(fs *pflag.FlagSet) { opts.Annotation.ApplyFlags(fs) fs.StringVarP(&opts.ManifestExportPath, "export-manifest", "", "", "`path` of the pushed manifest") fs.StringVarP(&opts.AnnotationFilePath, "annotation-file", "", "", "path of the annotation file") fs.BoolVarP(&opts.PathValidationDisabled, "disable-path-validation", "", false, "skip path validation") } // ExportManifest saves the pushed manifest to a local file. func (opts *Packer) ExportManifest(ctx context.Context, fetcher content.Fetcher, desc ocispec.Descriptor) error { if opts.ManifestExportPath == "" { return nil } manifestBytes, err := content.FetchAll(ctx, fetcher, desc) if err != nil { return err } return os.WriteFile(opts.ManifestExportPath, manifestBytes, 0666) } func (opts *Packer) Parse(cmd *cobra.Command) error { if !opts.PathValidationDisabled { var failedPaths []string for _, path := range opts.FileRefs { // Remove the type if specified in the path [:] format path, _, err := fileref.Parse(path, "") if err != nil { return err } if filepath.IsAbs(path) { failedPaths = append(failedPaths, path) } } if len(failedPaths) > 0 { return fmt.Errorf("%w: %v", errPathValidation, strings.Join(failedPaths, ", ")) } } return opts.parseAnnotations(cmd) } // parseAnnotations loads the manifest annotation map. func (opts *Packer) parseAnnotations(cmd *cobra.Command) error { if opts.AnnotationFilePath != "" && len(opts.ManifestAnnotations) != 0 { return errAnnotationConflict } if opts.AnnotationFilePath != "" { if err := decodeJSON(opts.AnnotationFilePath, &opts.Annotations); err != nil { return &oerrors.Error{ Err: fmt.Errorf(`invalid annotation json file: failed to load annotations from %s`, opts.AnnotationFilePath), Recommendation: `Annotation file doesn't match the required format. Please refer to the document at https://oras.land/docs/how_to_guides/manifest_annotations`, } } } if len(opts.ManifestAnnotations) != 0 { return opts.Annotation.Parse(cmd) } return nil } // decodeJSON decodes file contents into json. func decodeJSON(filename string, v any) (err error) { file, err := os.Open(filename) if err != nil { return err } defer func() { closeErr := file.Close() if err == nil { err = closeErr } }() return json.NewDecoder(file).Decode(v) } oras-1.3.3/cmd/oras/internal/option/packer_test.go000066400000000000000000000115671522423464100221650ustar00rootroot00000000000000/* Copyright The ORAS 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 option import ( "errors" "io/fs" "os" "path/filepath" "reflect" "testing" "github.com/spf13/pflag" ) const testContent = `{"$config":{"hello":"world"},"$manifest":{"foo":"bar"},"cake.txt":{"fun":"more cream"}}` var expectedResult = map[string]map[string]string{"$config": {"hello": "world"}, "$manifest": {"foo": "bar"}, "cake.txt": {"fun": "more cream"}} func TestPacker_FlagInit(_ *testing.T) { var test struct { Packer } ApplyFlags(&test, pflag.NewFlagSet("oras-test", pflag.ExitOnError)) } func TestPacker_parseAnnotations_err(t *testing.T) { opts := Packer{ Annotation: Annotation{ ManifestAnnotations: []string{"Key=Val"}, }, AnnotationFilePath: "this is not a file", // testFile, } if err := opts.parseAnnotations(nil); !errors.Is(err, errAnnotationConflict) { t.Fatalf("unexpected error: %v", err) } opts = Packer{ AnnotationFilePath: "this is not a file", // testFile, } if err := opts.parseAnnotations(nil); err == nil { t.Fatalf("unexpected error: %v", err) } opts = Packer{ Annotation: Annotation{ ManifestAnnotations: []string{"KeyVal"}, }, } if err := opts.parseAnnotations(nil); !errors.Is(err, errAnnotationFormat) { t.Fatalf("unexpected error: %v", err) } opts = Packer{ Annotation: Annotation{ ManifestAnnotations: []string{"Key=Val1", "Key=Val2"}, }, } if err := opts.parseAnnotations(nil); !errors.Is(err, errAnnotationDuplication) { t.Fatalf("unexpected error: %v", err) } } func TestPacker_parseAnnotations_annotationFile(t *testing.T) { testFile := filepath.Join(t.TempDir(), "testAnnotationFile") err := os.WriteFile(testFile, []byte(testContent), fs.ModePerm) if err != nil { t.Fatalf("Error writing %s: %v", testFile, err) } opts := Packer{ AnnotationFilePath: testFile, } err = opts.parseAnnotations(nil) if err != nil { t.Fatalf("unexpected error: %v", err) } if !reflect.DeepEqual(opts.Annotations, expectedResult) { t.Fatalf("unexpected error: %v", opts.Annotations) } } func TestPacker_parseAnnotations_annotationFlag(t *testing.T) { // Item do not contains '=' invalidFlag0 := []string{ "Key", } opts := Packer{ Annotation: Annotation{ ManifestAnnotations: invalidFlag0, }, } err := opts.parseAnnotations(nil) if !errors.Is(err, errAnnotationFormat) { t.Fatalf("unexpected error: %v", err) } // Duplication Key invalidFlag1 := []string{ "Key=0", "Key=1", } opts = Packer{ Annotation: Annotation{ ManifestAnnotations: invalidFlag1, }, } err = opts.parseAnnotations(nil) if !errors.Is(err, errAnnotationDuplication) { t.Fatalf("unexpected error: %v", err) } // Valid Annotations validFlag := []string{ "Key0=", // 1. Item not contains 'val' "Key1=Val", // 2. Normal Item "Key2=${env:USERNAME}", // 3. Item contains variable eg. "${env:USERNAME}" } opts = Packer{ Annotation: Annotation{ ManifestAnnotations: validFlag, }, } err = opts.parseAnnotations(nil) if err != nil { t.Fatalf("unexpected error: %v", err) } if _, ok := opts.Annotations["$manifest"]; !ok { t.Fatalf("unexpected error: failed when looking for '$manifest' in annotations") } if !reflect.DeepEqual(opts.Annotations, map[string]map[string]string{ "$manifest": { "Key0": "", "Key1": "Val", "Key2": "${env:USERNAME}", }, }) { t.Fatalf("unexpected error: %v", errors.New("content not match")) } } func givenTestFile(t *testing.T, data string) (path string) { t.Helper() tempDir := t.TempDir() fileName := "test.txt" path = filepath.Join(tempDir, fileName) content := []byte(data) if err := os.WriteFile(path, content, 0444); err != nil { t.Fatal("error calling WriteFile(), error =", err) } return path } func TestPacker_decodeJSON(t *testing.T) { var annotation Annotation path := "nonexistent-file.json" err := decodeJSON(path, &annotation.Annotations) if !errors.Is(err, fs.ErrNotExist) { t.Fatalf("unexpected error: %v", err) } path = givenTestFile(t, "bogus data") err = decodeJSON(path, &annotation.Annotations) if err == nil || err.Error() != "invalid character 'b' looking for beginning of value" { t.Fatalf("unexpected error: %v", err) } path = givenTestFile(t, "{\"annotations\":{\"org.opencontainers.image.ref.name\":\"ghcr.io/stefanprodan/podinfo:6.8.0\"}}") err = decodeJSON(path, &annotation.Annotations) if err != nil { t.Fatalf("unexpected error: %v", err) } } oras-1.3.3/cmd/oras/internal/option/parser.go000066400000000000000000000026231522423464100211460ustar00rootroot00000000000000/* Copyright The ORAS 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 option import ( "iter" "reflect" "github.com/spf13/cobra" ) // FlagParser parses flags in an option. type FlagParser interface { Parse(cmd *cobra.Command) error } // Parse parses applicable fields of the passed-in option pointer and returns // error during parsing. func Parse(cmd *cobra.Command, optsPtr any) error { for parser := range fields[FlagParser](optsPtr) { if err := parser.Parse(cmd); err != nil { return err } } return nil } // fields returns an iterator that yields all fields of the given struct that // implement the given interface. func fields[T any](ptr any) iter.Seq[T] { return func(yield func(T) bool) { v := reflect.ValueOf(ptr).Elem() for i := range v.NumField() { f := v.Field(i) if f.CanSet() { if opts, ok := f.Addr().Interface().(T); ok { if !yield(opts) { return } } } } } } oras-1.3.3/cmd/oras/internal/option/parser_test.go000066400000000000000000000037651522423464100222150ustar00rootroot00000000000000/* Copyright The ORAS 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 option_test import ( "errors" "testing" "github.com/spf13/cobra" "oras.land/oras/cmd/oras/internal/option" ) type Test struct { CntPtr *int } func (t *Test) Parse(_ *cobra.Command) error { *t.CntPtr++ if *t.CntPtr == 2 { return errors.New("should not be tried twice") } return nil } func TestParse_once(t *testing.T) { cnt := 0 type args struct { Test } tests := []struct { name string args args wantErr bool }{ {"parse should be called once", args{Test{CntPtr: &cnt}}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if err := option.Parse(nil, &tt.args); (err != nil) != tt.wantErr { t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) } if cnt != 1 { t.Errorf("Expect Parse() to be called once but got %v", cnt) } }) } } func TestParse_err(t *testing.T) { cnt := 0 type args struct { Test1 Test Test2 Test Test3 Test Test4 Test } tests := []struct { name string args args wantErr bool }{ {"parse should be called twice and aborted with error", args{Test{CntPtr: &cnt}, Test{CntPtr: &cnt}, Test{CntPtr: &cnt}, Test{CntPtr: &cnt}}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if err := option.Parse(nil, &tt.args); (err != nil) != tt.wantErr { t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) } if cnt != 2 { t.Errorf("Expect Parse() to be called twice but got %v", cnt) } }) } } oras-1.3.3/cmd/oras/internal/option/platform.go000066400000000000000000000046431522423464100215020ustar00rootroot00000000000000/* Copyright The ORAS 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 option import ( "fmt" "runtime" "strings" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/spf13/cobra" "github.com/spf13/pflag" ) // Platform option struct. type Platform struct { platform string Platform *ocispec.Platform FlagDescription string } // ApplyFlags applies flags to a command flag set. func (opts *Platform) ApplyFlags(fs *pflag.FlagSet) { if opts.FlagDescription == "" { opts.FlagDescription = "request platform" } fs.StringVarP(&opts.platform, "platform", "", "", opts.FlagDescription+" in the form of `os[/arch][/variant][:os_version]`") } // Parse parses the input platform flag to an oci platform type. func (opts *Platform) Parse(*cobra.Command) error { if opts.platform == "" { return nil } // OS[/Arch[/Variant]][:OSVersion] // If Arch is not provided, will use GOARCH instead var platformStr string var p ocispec.Platform platformStr, p.OSVersion, _ = strings.Cut(opts.platform, ":") parts := strings.Split(platformStr, "/") switch len(parts) { case 3: p.Variant = parts[2] fallthrough case 2: p.Architecture = parts[1] case 1: p.Architecture = runtime.GOARCH default: return fmt.Errorf("failed to parse platform %q: expected format os[/arch[/variant]]", opts.platform) } p.OS = parts[0] if p.OS == "" { return fmt.Errorf("invalid platform: OS cannot be empty") } if p.Architecture == "" { return fmt.Errorf("invalid platform: Architecture cannot be empty") } opts.Platform = &p return nil } // ArtifactPlatform option struct. type ArtifactPlatform struct { Platform } // ApplyFlags applies flags to a command flag set. func (opts *ArtifactPlatform) ApplyFlags(fs *pflag.FlagSet) { opts.FlagDescription = "set artifact platform" fs.StringVarP(&opts.platform, "artifact-platform", "", "", "[Experimental] "+opts.FlagDescription+" in the form of `os[/arch][/variant][:os_version]`") } oras-1.3.3/cmd/oras/internal/option/platform_test.go000066400000000000000000000055611522423464100225410ustar00rootroot00000000000000/* Copyright The ORAS 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 option import ( "reflect" "runtime" "testing" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/spf13/pflag" ) func TestPlatform_ApplyFlags(t *testing.T) { var test struct{ Platform } ApplyFlags(&test, pflag.NewFlagSet("oras-test", pflag.ExitOnError)) if test.platform != "" { t.Fatalf("expecting platform to be empty but got: %v", test.platform) } } func TestPlatform_Parse_err(t *testing.T) { tests := []struct { name string opts *Platform }{ {name: "empty arch 1", opts: &Platform{"os/", nil, ""}}, {name: "empty arch 2", opts: &Platform{"os//variant", nil, ""}}, {name: "empty os", opts: &Platform{"/arch", nil, ""}}, {name: "empty os with variant", opts: &Platform{"/arch/variant", nil, ""}}, {name: "trailing slash", opts: &Platform{"os/arch/variant/llama", nil, ""}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.opts.Parse(nil) if err == nil { t.Errorf("Platform.Parse() error = %v, wantErr %v", err, true) return } }) } } func TestPlatform_Parse(t *testing.T) { tests := []struct { name string opts *Platform want *ocispec.Platform }{ {name: "empty", opts: &Platform{platform: ""}, want: nil}, {name: "default arch", opts: &Platform{platform: "os"}, want: &ocispec.Platform{OS: "os", Architecture: runtime.GOARCH}}, {name: "os&arch", opts: &Platform{platform: "os/aRcH"}, want: &ocispec.Platform{OS: "os", Architecture: "aRcH"}}, {name: "empty variant", opts: &Platform{platform: "os/aRcH/"}, want: &ocispec.Platform{OS: "os", Architecture: "aRcH", Variant: ""}}, {name: "os&arch&variant", opts: &Platform{platform: "os/aRcH/vAriAnt"}, want: &ocispec.Platform{OS: "os", Architecture: "aRcH", Variant: "vAriAnt"}}, {name: "os version", opts: &Platform{platform: "os/aRcH/vAriAnt:osversion"}, want: &ocispec.Platform{OS: "os", Architecture: "aRcH", Variant: "vAriAnt", OSVersion: "osversion"}}, {name: "long os version", opts: &Platform{platform: "os/aRcH"}, want: &ocispec.Platform{OS: "os", Architecture: "aRcH"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if err := tt.opts.Parse(nil); err != nil { t.Errorf("Platform.Parse() error = %v", err) } got := tt.opts.Platform if !reflect.DeepEqual(got, tt.want) { t.Errorf("Platform.Parse() = %v, want %v", got, tt.want) } }) } } oras-1.3.3/cmd/oras/internal/option/pretty.go000066400000000000000000000021721522423464100212000ustar00rootroot00000000000000/* Copyright The ORAS 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 option import ( "io" "github.com/spf13/pflag" "oras.land/oras/cmd/oras/internal/output" ) // Pretty option struct. type Pretty struct { Pretty bool } // ApplyFlags applies flags to a command flag set. func (opts *Pretty) ApplyFlags(fs *pflag.FlagSet) { fs.BoolVarP(&opts.Pretty, "pretty", "", false, "prettify JSON objects printed to stdout") } // Output outputs the prettified content if `--pretty` flag is used. Otherwise // outputs the original content. func (opts *Pretty) Output(w io.Writer, content []byte) error { return output.PrintJSON(w, content, opts.Pretty) } oras-1.3.3/cmd/oras/internal/option/pretty_test.go000066400000000000000000000052261522423464100222420ustar00rootroot00000000000000/* Copyright The ORAS 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 option import ( "io" "os" "path/filepath" "reflect" "testing" "github.com/spf13/pflag" ) func TestPretty_ApplyFlags(t *testing.T) { var test struct{ Pretty } ApplyFlags(&test, pflag.NewFlagSet("oras-test", pflag.ExitOnError)) if test.Pretty.Pretty != false { t.Fatalf("expecting pretty to be false but got: %v", test.Pretty.Pretty) } } func TestPretty_Output(t *testing.T) { // generate test content raw := []byte("{\"mediaType\":\"test\",\"digest\":\"sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9\",\"size\":11}") prettified := []byte("{\n \"mediaType\": \"test\",\n \"digest\": \"sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9\",\n \"size\": 11\n}\n") tempDir := t.TempDir() fileName := "test.txt" path := filepath.Join(tempDir, fileName) fp, err := os.Create(path) if err != nil { t.Fatal("error calling os.Create(), error =", err) } defer func() { _ = fp.Close() }() // test unprettified content opts := Pretty{ Pretty: false, } err = opts.Output(fp, raw) if err != nil { t.Fatal("Pretty.Output() error =", err) } if _, err = fp.Seek(0, io.SeekStart); err != nil { t.Fatal("error calling File.Seek(), error =", err) } got, err := io.ReadAll(fp) if err != nil { t.Fatal("error calling io.ReadAll(), error =", err) } if !reflect.DeepEqual(got, raw) { t.Fatalf("Pretty.Output() got %v, want %v", got, raw) } // remove all content in the file if err := os.Truncate(path, 0); err != nil { t.Fatal("error calling os.Truncate(), error =", err) } if _, err = fp.Seek(0, io.SeekStart); err != nil { t.Fatal("error calling File.Seek(), error =", err) } // test prettified content opts = Pretty{ Pretty: true, } err = opts.Output(fp, raw) if err != nil { t.Fatal("Pretty.Output() error =", err) } if _, err = fp.Seek(0, io.SeekStart); err != nil { t.Fatal("error calling File.Seek(), error =", err) } got, err = io.ReadAll(fp) if err != nil { t.Fatal("error calling io.ReadAll(), error =", err) } if !reflect.DeepEqual(got, prettified) { t.Fatalf("Pretty.Output() failed to prettified the content: %v", got) } } oras-1.3.3/cmd/oras/internal/option/remote.go000066400000000000000000000347211522423464100211510ustar00rootroot00000000000000/* Copyright The ORAS 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 option import ( "context" "crypto/tls" "errors" "fmt" "io" "net" "net/http" "os" "strconv" "strings" "sync" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" "oras.land/oras-go/v2/errdef" "oras.land/oras-go/v2/registry/remote" "oras.land/oras-go/v2/registry/remote/auth" "oras.land/oras-go/v2/registry/remote/credentials" "oras.land/oras-go/v2/registry/remote/errcode" "oras.land/oras-go/v2/registry/remote/retry" oerrors "oras.land/oras/cmd/oras/internal/errors" "oras.land/oras/internal/credential" "oras.land/oras/internal/crypto" onet "oras.land/oras/internal/net" "oras.land/oras/internal/trace" "oras.land/oras/internal/version" ) const ( caFileFlag = "ca-file" certFileFlag = "cert-file" keyFileFlag = "key-file" usernameFlag = "username" passwordFlag = "password" passwordFromStdinFlag = "password-stdin" identityTokenFlag = "identity-token" identityTokenFromStdinFlag = "identity-token-stdin" ) // Remote options struct contains flags and arguments specifying one registry. // Remote implements oerrors.Handler and interface. type Remote struct { DistributionSpec CACertFilePath string CertFilePath string KeyFilePath string Insecure bool Configs []string Username string secretFromStdin bool Secret string //nolint:gosec // G117: not a hardcoded secret, this is a CLI option field flagPrefix string resolveFlag []string applyDistributionSpec bool headerFlags []string headers http.Header warned map[string]*sync.Map plainHTTP func() (plainHTTP bool, enforced bool) store credentials.Store } // EnableDistributionSpecFlag set distribution specification flag as applicable. func (remo *Remote) EnableDistributionSpecFlag() { remo.applyDistributionSpec = true } // ApplyFlags applies flags to a command flag set. func (remo *Remote) ApplyFlags(fs *pflag.FlagSet) { remo.ApplyFlagsWithPrefix(fs, "", "") remo.applyStdinFlags(fs) } func (remo *Remote) applyStdinFlags(fs *pflag.FlagSet) { fs.BoolVar(&remo.secretFromStdin, passwordFromStdinFlag, false, "read password from stdin") fs.BoolVar(&remo.secretFromStdin, identityTokenFromStdinFlag, false, "read identity token from stdin") } // ApplyFlagsWithPrefix applies flags to a command flag set with a prefix string. // Commonly used for non-unary remote targets. func (remo *Remote) ApplyFlagsWithPrefix(fs *pflag.FlagSet, prefix, description string) { var ( shortUser string shortPassword string shortHeader string ) if prefix == "" { shortUser, shortPassword = "u", "p" shortHeader = "H" } remo.flagPrefix = prefix if remo.applyDistributionSpec { remo.DistributionSpec.ApplyFlagsWithPrefix(fs, prefix, description) } fs.StringVarP(&remo.Username, remo.flagPrefix+usernameFlag, shortUser, "", description+"registry username") fs.StringVarP(&remo.Secret, remo.flagPrefix+passwordFlag, shortPassword, "", description+"registry password or identity token") fs.StringVar(&remo.Secret, remo.flagPrefix+identityTokenFlag, "", description+"registry identity token") fs.BoolVar(&remo.Insecure, remo.flagPrefix+"insecure", false, "allow connections to "+description+"SSL registry without certs") plainHTTPFlagName := remo.flagPrefix + "plain-http" plainHTTP := fs.Bool(plainHTTPFlagName, false, "allow insecure connections to "+description+"registry without SSL check") remo.plainHTTP = func() (bool, bool) { return *plainHTTP, fs.Changed(plainHTTPFlagName) } fs.StringVar(&remo.CACertFilePath, remo.flagPrefix+caFileFlag, "", "server certificate authority file for the remote "+description+"registry") fs.StringVarP(&remo.CertFilePath, remo.flagPrefix+certFileFlag, "", "", "client certificate file for the remote "+description+"registry") fs.StringVarP(&remo.KeyFilePath, remo.flagPrefix+keyFileFlag, "", "", "client private key file for the remote "+description+"registry") fs.StringArrayVar(&remo.resolveFlag, remo.flagPrefix+"resolve", nil, "customized DNS for "+description+"registry, formatted in `host:port:address[:address_port]`") fs.StringArrayVar(&remo.Configs, remo.flagPrefix+"registry-config", nil, "`path` of the authentication file for "+description+"registry") fs.StringArrayVarP(&remo.headerFlags, remo.flagPrefix+"header", shortHeader, nil, "add custom headers to "+description+"requests") } // CheckStdinConflict checks if PasswordFromStdin or IdentityTokenFromStdin of a // *pflag.FlagSet conflicts with read file from input. func CheckStdinConflict(flags *pflag.FlagSet) error { switch { case flags.Changed(passwordFromStdinFlag): return fmt.Errorf("`-` read file from input and `--%s` read password from input cannot be both used", passwordFromStdinFlag) case flags.Changed(identityTokenFromStdinFlag): return fmt.Errorf("`-` read file from input and `--%s` read identity token from input cannot be both used", identityTokenFromStdinFlag) } return nil } // Parse tries to read password with optional cmd prompt. func (remo *Remote) Parse(cmd *cobra.Command) error { usernameAndIDTokenFlags := []string{remo.flagPrefix + usernameFlag, remo.flagPrefix + identityTokenFlag} passwordAndIDTokenFlags := []string{remo.flagPrefix + passwordFlag, remo.flagPrefix + identityTokenFlag} certFileAndKeyFileFlags := []string{remo.flagPrefix + certFileFlag, remo.flagPrefix + keyFileFlag} if cmd.Flags().Lookup(identityTokenFromStdinFlag) != nil { usernameAndIDTokenFlags = append(usernameAndIDTokenFlags, identityTokenFromStdinFlag) passwordAndIDTokenFlags = append(passwordAndIDTokenFlags, identityTokenFromStdinFlag) } if cmd.Flags().Lookup(passwordFromStdinFlag) != nil { passwordAndIDTokenFlags = append(passwordAndIDTokenFlags, passwordFromStdinFlag) } if err := oerrors.CheckMutuallyExclusiveFlags(cmd.Flags(), usernameAndIDTokenFlags...); err != nil { return err } if err := oerrors.CheckMutuallyExclusiveFlags(cmd.Flags(), passwordAndIDTokenFlags...); err != nil { return err } if err := remo.parseCustomHeaders(); err != nil { return err } if err := oerrors.CheckRequiredTogetherFlags(cmd.Flags(), certFileAndKeyFileFlags...); err != nil { return err } return remo.readSecret(cmd) } // readSecret tries to read password or identity token with // optional cmd prompt. func (remo *Remote) readSecret(cmd *cobra.Command) (err error) { if cmd.Flags().Changed(identityTokenFlag) { _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "WARNING! Using --identity-token via the CLI is insecure. Use --identity-token-stdin.") } else if cmd.Flags().Changed(passwordFlag) { _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "WARNING! Using --password via the CLI is insecure. Use --password-stdin.") } else if remo.secretFromStdin { // Prompt for credential secret, err := io.ReadAll(os.Stdin) if err != nil { return err } remo.Secret = strings.TrimSuffix(string(secret), "\n") remo.Secret = strings.TrimSuffix(remo.Secret, "\r") } return nil } // parseResolve parses resolve flag. func (remo *Remote) parseResolve(baseDial onet.DialFunc) (onet.DialFunc, error) { if len(remo.resolveFlag) == 0 { return baseDial, nil } formatError := func(param, message string) error { return fmt.Errorf("failed to parse resolve flag %q: %s", param, message) } var dialer onet.Dialer for _, r := range remo.resolveFlag { parts := strings.SplitN(r, ":", 4) length := len(parts) if length < 3 { return nil, formatError(r, "expecting host:port:address[:address_port]") } host := parts[0] hostPort, err := strconv.Atoi(parts[1]) if err != nil { return nil, formatError(r, "expecting uint64 host port") } // ipv6 zone is not parsed address := net.ParseIP(parts[2]) if address == nil { return nil, formatError(r, "invalid IP address") } addressPort := hostPort if length > 3 { addressPort, err = strconv.Atoi(parts[3]) if err != nil { return nil, formatError(r, "expecting uint64 address port") } } dialer.Add(host, hostPort, address, addressPort) } dialer.BaseDialContext = baseDial return dialer.DialContext, nil } // tlsConfig assembles the tls config. func (remo *Remote) tlsConfig() (*tls.Config, error) { config := &tls.Config{ InsecureSkipVerify: remo.Insecure, } if remo.CACertFilePath != "" { var err error config.RootCAs, err = crypto.LoadCertPool(remo.CACertFilePath) if err != nil { return nil, err } } if remo.CertFilePath != "" && remo.KeyFilePath != "" { cert, err := tls.LoadX509KeyPair(remo.CertFilePath, remo.KeyFilePath) if err != nil { return nil, err } config.Certificates = []tls.Certificate{cert} } return config, nil } // authClient assembles a oras auth client. func (remo *Remote) authClient(_ string, debug bool) (client *auth.Client, err error) { config, err := remo.tlsConfig() if err != nil { return nil, err } baseTransport := http.DefaultTransport.(*http.Transport).Clone() baseTransport.TLSClientConfig = config dialContext, err := remo.parseResolve(baseTransport.DialContext) if err != nil { return nil, err } baseTransport.DialContext = dialContext client = &auth.Client{ Client: &http.Client{ // http.RoundTripper with a retry using the DefaultPolicy // see: https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/retry#Policy Transport: retry.NewTransport(baseTransport), }, Cache: auth.NewCache(), Header: remo.headers, } client.SetUserAgent("oras/" + version.GetVersion()) if debug { client.Client.Transport = trace.NewTransport(client.Client.Transport) } cred := remo.Credential() if cred != auth.EmptyCredential { client.Credential = func(_ context.Context, _ string) (auth.Credential, error) { return cred, nil } } else { var err error remo.store, err = credential.NewStore(remo.Configs...) if err != nil { return nil, err } client.Credential = credentials.Credential(remo.store) } return } // ConfigPath returns the config path of the credential store. func (remo *Remote) ConfigPath() (string, error) { if remo.store == nil { return "", errors.New("no credential store initialized") } if ds, ok := remo.store.(*credentials.DynamicStore); ok { return ds.ConfigPath(), nil } return "", errors.New("store doesn't support getting config path") } func (remo *Remote) parseCustomHeaders() error { if len(remo.headerFlags) != 0 { headers := map[string][]string{} for _, h := range remo.headerFlags { name, value, found := strings.Cut(h, ":") if !found || strings.TrimSpace(name) == "" { // In conformance to the RFC 2616 specification // Reference: https://www.rfc-editor.org/rfc/rfc2616#section-4.2 return fmt.Errorf("invalid header: %q", h) } headers[name] = append(headers[name], value) } remo.headers = headers } return nil } // Credential returns a credential based on the remote options. func (remo *Remote) Credential() auth.Credential { return credential.Credential(remo.Username, remo.Secret) } func (remo *Remote) handleWarning(registry string, logger logrus.FieldLogger) func(warning remote.Warning) { if remo.warned == nil { remo.warned = make(map[string]*sync.Map) } warned := remo.warned[registry] if warned == nil { warned = &sync.Map{} remo.warned[registry] = warned } logger = logger.WithField("registry", registry) return func(warning remote.Warning) { if _, loaded := warned.LoadOrStore(warning.WarningValue, struct{}{}); !loaded { logger.Warn(warning.Text) } } } // NewRegistry assembles a oras remote registry. func (remo *Remote) NewRegistry(registry string, common Common, logger logrus.FieldLogger) (reg *remote.Registry, err error) { reg, err = remote.NewRegistry(registry) if err != nil { return nil, err } registry = reg.Reference.Registry reg.PlainHTTP = remo.isPlainHTTP(registry) reg.HandleWarning = remo.handleWarning(registry, logger) if reg.Client, err = remo.authClient(registry, common.Debug); err != nil { return nil, err } return } // NewRepository assembles a oras remote repository. func (remo *Remote) NewRepository(reference string, common Common, logger logrus.FieldLogger) (repo *remote.Repository, err error) { repo, err = remote.NewRepository(reference) if err != nil { if errors.Is(err, errdef.ErrInvalidReference) { return nil, fmt.Errorf("%q: %w", reference, err) } return nil, err } registry := repo.Reference.Registry repo.PlainHTTP = remo.isPlainHTTP(registry) repo.HandleWarning = remo.handleWarning(registry, logger) if repo.Client, err = remo.authClient(registry, common.Debug); err != nil { return nil, err } repo.SkipReferrersGC = true if remo.ReferrersAPI != ReferrersStateUnknown { if err := repo.SetReferrersCapability(remo.ReferrersAPI == ReferrersStateSupported); err != nil { return nil, err } } return } // isPlainHTTP returns the plain http flag for a given registry. func (remo *Remote) isPlainHTTP(registry string) bool { plainHTTP, enforced := remo.plainHTTP() if enforced { return plainHTTP } host, _, _ := net.SplitHostPort(registry) if host == "localhost" || registry == "localhost" { // not specified, defaults to plain http for localhost return true } return plainHTTP } // ModifyError modifies error during cmd execution. func (remo *Remote) ModifyError(cmd *cobra.Command, err error) (bool, error) { if errors.Is(err, auth.ErrBasicCredentialNotFound) { return true, remo.DecorateCredentialError(err) } var errResp *errcode.ErrorResponse if errors.As(err, &errResp) { cmd.SetErrPrefix(oerrors.RegistryErrorPrefix) return true, &oerrors.Error{ Err: oerrors.ReportErrResp(errResp), } } return false, err } // DecorateCredentialError decorate error with recommendation. func (remo *Remote) DecorateCredentialError(err error) *oerrors.Error { configPath := " " if path, pathErr := remo.ConfigPath(); pathErr == nil { configPath += fmt.Sprintf("at %q ", path) } return &oerrors.Error{ Err: oerrors.TrimErrBasicCredentialNotFound(err), Recommendation: fmt.Sprintf(`Please check whether the registry credential stored in the authentication file%sis correct`, configPath), } } oras-1.3.3/cmd/oras/internal/option/remote_test.go000066400000000000000000000402211522423464100222000ustar00rootroot00000000000000/* Copyright The ORAS 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 option import ( "bytes" "context" "crypto/rand" "crypto/tls" "crypto/x509" _ "embed" "encoding/base64" "encoding/json" "fmt" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "reflect" "testing" "github.com/sirupsen/logrus" "github.com/spf13/pflag" "oras.land/oras-go/v2/registry/remote/auth" ) var ts *httptest.Server var testRepo = "test-repo" var testTagList = struct { Tags []string `json:"tags"` }{ Tags: []string{"tag"}, } // localhostServerCert is a PEM-encoded TLS cert with SAN IPs // "127.0.0.1" and "[::1]", expiring at Jan 29 16:00:00 2084 GMT. // adapted from golang crypto/tls: // go run generate_cert.go --rsa-bits 4096 --host 127.0.0.1,::1,oras.land --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h // //go:embed testdata/localhostServer.crt var localhostServerCert []byte // localhostServerKey is the private key for localhostServerCert. // //go:embed testdata/localhostServer.key var localhostServerKey []byte // localhostClientCert is a PEM-encoded TLS cert with SAN IPs // "127.0.0.1" and "[::1]", expiring at Jan 29 16:00:00 2084 GMT. // adapted from golang crypto/tls (added Client Auth usage): // go run generate_cert.go --rsa-bits 4096 --host 127.0.0.1,::1,oras.land --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h // //go:embed testdata/localhostClient.crt var localhostClientCert []byte // localhostClientKey is the private key for localhostClientCert. // //go:embed testdata/localhostClient.key var localhostClientKey []byte func testingKey(s []byte) []byte { return bytes.ReplaceAll(s, []byte("TESTING KEY"), []byte("PRIVATE KEY")) } func loadTestingTLSConfig() *tls.Config { clientCertPool := x509.NewCertPool() clientCertPool.AppendCertsFromPEM(localhostClientCert) tlsConfig := &tls.Config{ Certificates: []tls.Certificate{loadTestingCert(localhostServerCert, testingKey(localhostServerKey))}, ClientAuth: tls.VerifyClientCertIfGiven, ClientCAs: clientCertPool, } return tlsConfig } func loadTestingCert(certificate, key []byte) tls.Certificate { cert, err := tls.X509KeyPair(certificate, key) if err != nil { panic(fmt.Sprintf("Unable to load testing certificate: %v", err)) } return cert } func TestMain(m *testing.M) { // Test server ts = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { p := r.URL.Path m := r.Method switch { case p == "/v2/" && m == "GET": w.WriteHeader(http.StatusOK) case p == fmt.Sprintf("/v2/%s/tags/list", testRepo) && m == "GET": if err := json.NewEncoder(w).Encode(testTagList); err != nil { http.Error(w, "error encoding", http.StatusBadRequest) } } })) ts.TLS = loadTestingTLSConfig() ts.StartTLS() defer ts.Close() os.Exit(m.Run()) } func TestRemote_FlagsInit(_ *testing.T) { var test struct { Remote } ApplyFlags(&test, pflag.NewFlagSet("oras-test", pflag.ExitOnError)) } func TestRemote_authClient_RawCredential(t *testing.T) { password := make([]byte, 12) if _, err := rand.Read(password); err != nil { t.Fatalf("unexpected error: %v", err) } want := auth.Credential{ Username: "mocked^^??oras-@@!#", Password: base64.StdEncoding.EncodeToString(password), } opts := Remote{ Username: want.Username, Secret: want.Password, } client, err := opts.authClient("hostname", false) if err != nil { t.Fatalf("unexpected error: %v", err) } got, err := client.Credential(nil, "") if err != nil { t.Fatalf("unexpected error: %v", err) } if got.Username != want.Username || got.Password != want.Password { t.Fatalf("expect: %v, got: %v", want, got) } } func TestRemote_authClient_skipTlsVerify(t *testing.T) { opts := Remote{ Insecure: true, } client, err := opts.authClient("hostname", false) if err != nil { t.Fatalf("unexpected error: %v", err) } req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, ts.URL, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } resp, err := client.Do(req) if err != nil { t.Fatalf("unexpected error: %v", err) } resp.Body.Close() } func TestRemote_authClient_CARoots(t *testing.T) { caPath := filepath.Join(t.TempDir(), "oras-test.pem") if err := os.WriteFile(caPath, localhostServerCert, 0644); err != nil { t.Fatalf("unexpected error: %v", err) } opts := Remote{ CACertFilePath: caPath, } client, err := opts.authClient("hostname", false) if err != nil { t.Fatalf("unexpected error: %v", err) } req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, ts.URL, nil) if err != nil { t.Fatalf("unexpected error: %v", err) } resp, err := client.Do(req) if err != nil { t.Fatalf("unexpected error: %v", err) } resp.Body.Close() } func TestRemote_authClient_resolve(t *testing.T) { URL, err := url.Parse(ts.URL) if err != nil { t.Fatalf("invalid url in test server: %s", ts.URL) } testHost := "test.unit.oras" opts := Remote{ resolveFlag: []string{fmt.Sprintf("%s:%s:%s", testHost, URL.Port(), URL.Hostname())}, Insecure: true, } client, err := opts.authClient(testHost, false) if err != nil { t.Fatalf("unexpected error when creating auth client: %v", err) } req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, fmt.Sprintf("https://%s:%s", testHost, URL.Port()), nil) if err != nil { t.Fatalf("unexpected error when generating request: %v", err) } resp, err := client.Do(req) if err != nil { t.Fatalf("unexpected error when sending request: %v", err) } resp.Body.Close() } func plainHTTPEnabled() (plainHTTP bool, fromFlag bool) { return true, true } func HTTPSEnabled() (plainHTTP bool, fromFlag bool) { return false, true } func plainHTTPNotSpecified() (plainHTTP bool, fromFlag bool) { return false, false } func TestRemote_NewRegistry(t *testing.T) { caPath := filepath.Join(t.TempDir(), "oras-test.pem") if err := os.WriteFile(caPath, localhostServerCert, 0644); err != nil { t.Fatalf("unexpected error: %v", err) } opts := struct { Remote Common }{ Remote{ CACertFilePath: caPath, plainHTTP: plainHTTPNotSpecified, }, Common{}, } uri, err := url.ParseRequestURI(ts.URL) if err != nil { t.Fatalf("unexpected error: %v", err) } reg, err := opts.NewRegistry(uri.Host, opts.Common, logrus.New()) if err != nil { t.Fatalf("unexpected error: %v", err) } if err = reg.Ping(context.Background()); err != nil { t.Fatalf("unexpected error: %v", err) } } func TestRemote_NewRepository(t *testing.T) { caPath := filepath.Join(t.TempDir(), "oras-test.pem") if err := os.WriteFile(caPath, localhostServerCert, 0644); err != nil { t.Fatalf("unexpected error: %v", err) } opts := struct { Remote Common }{ Remote{ CACertFilePath: caPath, plainHTTP: plainHTTPNotSpecified, }, Common{}, } uri, err := url.ParseRequestURI(ts.URL) if err != nil { t.Fatalf("unexpected error: %v", err) } repo, err := opts.NewRepository(uri.Host+"/"+testRepo, opts.Common, logrus.New()) if err != nil { t.Fatalf("unexpected error: %v", err) } if err = repo.Tags(context.Background(), "", func(got []string) error { want := []string{"tag"} if len(got) != len(testTagList.Tags) || !reflect.DeepEqual(got, want) { return fmt.Errorf("expect: %v, got: %v", testTagList.Tags, got) } return nil }); err != nil { t.Fatalf("unexpected error: %v", err) } } func TestRemote_NewRepositoryMTLS(t *testing.T) { caPath := filepath.Join(t.TempDir(), "oras-test.pem") if err := os.WriteFile(caPath, localhostServerCert, 0644); err != nil { t.Fatalf("unexpected error: %v", err) } clientCertPath := filepath.Join(t.TempDir(), "oras-test-client.pem") if err := os.WriteFile(clientCertPath, localhostClientCert, 0644); err != nil { t.Fatalf("unexpected error: %v", err) } clientKeyPath := filepath.Join(t.TempDir(), "oras-test-client.key") if err := os.WriteFile(clientKeyPath, testingKey(localhostClientKey), 0644); err != nil { t.Fatalf("unexpected error: %v", err) } opts := struct { Remote Common }{ Remote{ CACertFilePath: caPath, CertFilePath: clientCertPath, KeyFilePath: clientKeyPath, plainHTTP: plainHTTPNotSpecified, }, Common{}, } uri, err := url.ParseRequestURI(ts.URL) if err != nil { t.Fatalf("unexpected error: %v", err) } repo, err := opts.NewRepository(uri.Host+"/"+testRepo, opts.Common, logrus.New()) if err != nil { t.Fatalf("unexpected error: %v", err) } if err = repo.Tags(context.Background(), "", func(got []string) error { want := []string{"tag"} if len(got) != len(testTagList.Tags) || !reflect.DeepEqual(got, want) { return fmt.Errorf("expect: %v, got: %v", testTagList.Tags, got) } return nil }); err != nil { t.Fatalf("unexpected error: %v", err) } } func TestRemote_NewRepository_Retry(t *testing.T) { caPath := filepath.Join(t.TempDir(), "oras-test.pem") if err := os.WriteFile(caPath, localhostServerCert, 0644); err != nil { t.Fatalf("unexpected error: %v", err) } retries, count := 3, 0 ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { count++ if count < retries { http.Error(w, "error", http.StatusTooManyRequests) return } err := json.NewEncoder(w).Encode(testTagList) if err != nil { http.Error(w, "error encoding", http.StatusBadRequest) } })) ts.TLS = loadTestingTLSConfig() ts.StartTLS() defer ts.Close() opts := struct { Remote Common }{ Remote{ CACertFilePath: caPath, plainHTTP: plainHTTPNotSpecified, }, Common{}, } uri, err := url.ParseRequestURI(ts.URL) if err != nil { t.Fatalf("unexpected error: %v", err) } repo, err := opts.NewRepository(uri.Host+"/"+testRepo, opts.Common, logrus.New()) if err != nil { t.Fatalf("unexpected error: %v", err) } if err = repo.Tags(context.Background(), "", func(got []string) error { want := []string{"tag"} if len(got) != len(testTagList.Tags) || !reflect.DeepEqual(got, want) { return fmt.Errorf("expect: %v, got: %v", testTagList.Tags, got) } return nil }); err != nil { t.Fatalf("unexpected error: %v", err) } if count != retries { t.Errorf("expected %d retries, got %d", retries, count) } } func TestRemote_default_localhost(t *testing.T) { opts := Remote{plainHTTP: plainHTTPNotSpecified} got := opts.isPlainHTTP("localhost") if got != true { t.Fatalf("tls should be disabled when domain is localhost") } got = opts.isPlainHTTP("localhost:9090") if got != true { t.Fatalf("tls should be disabled when domain is localhost") } } func TestRemote_isPlainHTTP_localhost(t *testing.T) { opts := Remote{plainHTTP: plainHTTPEnabled} isplainHTTP := opts.isPlainHTTP("localhost") if isplainHTTP != true { t.Fatalf("tls should be disabled when domain is localhost and --plain-http is used") } isplainHTTP = opts.isPlainHTTP("localhost:9090") if isplainHTTP != true { t.Fatalf("tls should be disabled when domain is localhost and --plain-http is used") } } func TestRemote_isHTTPS_localhost(t *testing.T) { opts := Remote{plainHTTP: HTTPSEnabled} got := opts.isPlainHTTP("localhost") if got != false { t.Fatalf("tls should be enabled when domain is localhost and --plain-http=false is used") } got = opts.isPlainHTTP("localhost:9090") if got != false { t.Fatalf("tls should be enabled when domain is localhost and --plain-http=false is used") } } func TestRemote_parseResolve_err(t *testing.T) { tests := []struct { name string opts *Remote }{ { name: "invalid flag", opts: &Remote{resolveFlag: []string{"this-shouldn't_work"}}, }, { name: "no host", opts: &Remote{resolveFlag: []string{":port:address"}}, }, { name: "no address", opts: &Remote{resolveFlag: []string{"host:port:"}}, }, { name: "invalid address", opts: &Remote{resolveFlag: []string{"host:port:invalid-ip"}}, }, { name: "no port", opts: &Remote{resolveFlag: []string{"host::address"}}, }, { name: "invalid source port", opts: &Remote{resolveFlag: []string{"host:port:address"}}, }, { name: "invalid destination port", opts: &Remote{resolveFlag: []string{"host:443:address:port"}}, }, { name: "no source port", opts: &Remote{resolveFlag: []string{"host::address"}}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if _, err := tt.opts.parseResolve(nil); err == nil { t.Errorf("Expecting error in Remote.parseResolve()") } }) } } func TestRemote_parseResolve(t *testing.T) { tests := []struct { name string opts *Remote }{ { name: "fromHost:fromPort:toIp", opts: &Remote{resolveFlag: []string{"host:443:0.0.0.0"}}, }, { name: "fromHost:fromPort:toIp:toPort", opts: &Remote{resolveFlag: []string{"host:443:0.0.0.0:5000"}}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if _, err := tt.opts.parseResolve(nil); err != nil { t.Errorf("Remote.parseResolve() error = %v", err) } }) } } func TestRemote_parseCustomHeaders(t *testing.T) { tests := []struct { name string headerFlags []string want http.Header wantErr bool }{ { name: "no custom header is provided", headerFlags: []string{}, want: nil, wantErr: false, }, { name: "one name-value pair", headerFlags: []string{"key:value"}, want: map[string][]string{"key": {"value"}}, wantErr: false, }, { name: "multiple name-value pairs", headerFlags: []string{"key:value", "k:v"}, want: map[string][]string{"key": {"value"}, "k": {"v"}}, wantErr: false, }, { name: "multiple name-value pairs with commas", headerFlags: []string{"key:value,value2,value3", "k:v,v2,v3"}, want: map[string][]string{"key": {"value,value2,value3"}, "k": {"v,v2,v3"}}, wantErr: false, }, { name: "empty string is a valid value", headerFlags: []string{"k:", "key:value,value2,value3"}, want: map[string][]string{"k": {""}, "key": {"value,value2,value3"}}, wantErr: false, }, { name: "multiple colons are allowed", headerFlags: []string{"k::::v,v2,v3", "key:value,value2,value3"}, want: map[string][]string{"k": {":::v,v2,v3"}, "key": {"value,value2,value3"}}, wantErr: false, }, { name: "name with spaces", headerFlags: []string{"bar :b"}, want: map[string][]string{"bar ": {"b"}}, wantErr: false, }, { name: "value with spaces", headerFlags: []string{"foo: a"}, want: map[string][]string{"foo": {" a"}}, wantErr: false, }, { name: "repeated pairs", headerFlags: []string{"key:value", "key:value"}, want: map[string][]string{"key": {"value", "value"}}, wantErr: false, }, { name: "repeated name with different values", headerFlags: []string{"key:value", "key:value2"}, want: map[string][]string{"key": {"value", "value2"}}, wantErr: false, }, { name: "one valid header and one invalid header(no pair)", headerFlags: []string{"key:value,value2,value3", "vk"}, want: nil, wantErr: true, }, { name: "one valid header and one invalid header(empty name)", headerFlags: []string{":v", "key:value,value2,value3"}, want: nil, wantErr: true, }, { name: "pure-space name is invalid", headerFlags: []string{" : foo "}, want: nil, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { opts := &Remote{ headerFlags: tt.headerFlags, } if err := opts.parseCustomHeaders(); (err != nil) != tt.wantErr { t.Errorf("Remote.parseCustomHeaders() error = %v, wantErr %v", err, tt.wantErr) } if !reflect.DeepEqual(tt.want, opts.headers) { t.Errorf("Remote.parseCustomHeaders() = %v, want %v", opts.headers, tt.want) } }) } } oras-1.3.3/cmd/oras/internal/option/spec.go000066400000000000000000000112311522423464100205770ustar00rootroot00000000000000/* Copyright The ORAS 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 option import ( "fmt" "strings" "github.com/spf13/pflag" "oras.land/oras-go/v2" oerrors "oras.land/oras/cmd/oras/internal/errors" ) const ( ImageSpecV1_1 = "v1.1" ImageSpecV1_0 = "v1.0" ) const ( DistributionSpecReferrersUnknown = "unknown" DistributionSpecReferrersTagV1_1 = "v1.1-referrers-tag" DistributionSpecReferrersAPIV1_1 = "v1.1-referrers-api" ) // ImageSpec option struct which implements pflag.Value interface. type ImageSpec struct { Flag string PackVersion oras.PackManifestVersion } // Set validates and sets the flag value from a string argument. func (is *ImageSpec) Set(value string) error { is.Flag = value switch value { case ImageSpecV1_1: is.PackVersion = oras.PackManifestVersion1_1 case ImageSpecV1_0: is.PackVersion = oras.PackManifestVersion1_0 default: return &oerrors.Error{ Err: fmt.Errorf("unknown image specification flag: %s", value), Recommendation: fmt.Sprintf("Available options: %s", is.Options()), } } return nil } // Type returns the string value of the inner flag. func (is *ImageSpec) Type() string { return "string" } // Options returns the string of usable options for the flag. func (is *ImageSpec) Options() string { return strings.Join([]string{ ImageSpecV1_1, ImageSpecV1_0, }, ", ") } // String returns the string representation of the flag. func (is *ImageSpec) String() string { // to avoid printing default value in usage doc return "" } // ApplyFlags applies flags to a command flag set. func (is *ImageSpec) ApplyFlags(fs *pflag.FlagSet) { // default to v1.1, unless --config is used and --artifact-type is not used is.PackVersion = oras.PackManifestVersion1_1 is.Flag = ImageSpecV1_1 fs.Var(is, "image-spec", `[Preview] specify manifest type for building artifact. Options: v1.1, v1.0 (default v1.1, overridden to v1.0 if --config is used without --artifact-type)`) } // referrersState represents the state of Referrers API. type referrersState = int32 const ( // ReferrersStateUnknown represents an unknown state of Referrers API. ReferrersStateUnknown referrersState = iota // ReferrersStateSupported represents that the repository is known to // support Referrers API. ReferrersStateSupported // ReferrersStateUnsupported represents that the repository is known to // not support Referrers API. ReferrersStateUnsupported ) // DistributionSpec option struct which implements pflag.Value interface. type DistributionSpec struct { // ReferrersAPI indicates the preference of the implementation of the Referrers API. // Set to true for referrers API, false for referrers tag scheme, and nil for auto fallback. ReferrersAPI referrersState // specFlag should be provided in form of`--