pax_global_header00006660000000000000000000000064146253043270014520gustar00rootroot0000000000000052 comment=dcef719e208a9b226b15bc6512ad729a7dd93270 oras-1.2.0/000077500000000000000000000000001462530432700124645ustar00rootroot00000000000000oras-1.2.0/.github/000077500000000000000000000000001462530432700140245ustar00rootroot00000000000000oras-1.2.0/.github/.codecov.yml000066400000000000000000000013441462530432700162510ustar00rootroot00000000000000# 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: errororas-1.2.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001462530432700162075ustar00rootroot00000000000000oras-1.2.0/.github/ISSUE_TEMPLATE/bug-report.yaml000066400000000000000000000041021462530432700211560ustar00rootroot00000000000000# 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.2.0/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000014301462530432700201750ustar00rootroot00000000000000# 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.2.0/.github/ISSUE_TEMPLATE/feature-request.yaml000066400000000000000000000033301462530432700222130ustar00rootroot00000000000000# 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.2.0/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000013561462530432700176320ustar00rootroot00000000000000**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.2.0/.github/dependabot.yml000066400000000000000000000017021462530432700166540ustar00rootroot00000000000000# 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" oras-1.2.0/.github/licenserc.yml000066400000000000000000000031351462530432700165200ustar00rootroot00000000000000# 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/**' 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.2.0/.github/workflows/000077500000000000000000000000001462530432700160615ustar00rootroot00000000000000oras-1.2.0/.github/workflows/build.yml000066400000000000000000000032461462530432700177100ustar00rootroot00000000000000# 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.22'] fail-fast: true steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Go ${{ matrix.go-version }} uses: actions/setup-go@v5 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@v4 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} with: fail_ci_if_error: true oras-1.2.0/.github/workflows/codeql-analysis.yml000066400000000000000000000026301462530432700216750ustar00rootroot00000000000000# 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.22'] fail-fast: false steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up Go ${{ matrix.go-version }} environment uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} check-latest: true - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: go - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 oras-1.2.0/.github/workflows/golangci-lint.yml000066400000000000000000000021341462530432700213330ustar00rootroot00000000000000# 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.22'] fail-fast: true steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Go ${{ matrix.go-version }} uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} check-latest: true - name: golangci-lint uses: golangci/golangci-lint-action@v6 oras-1.2.0/.github/workflows/license-checker.yml000066400000000000000000000022561462530432700216350ustar00rootroot00000000000000# 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@v4 - name: Check license header uses: apache/skywalking-eyes/header@v0.6.0 with: mode: check config: .github/licenserc.yml - name: Check dependencies license uses: apache/skywalking-eyes/dependency@v0.6.0 with: config: .github/licenserc.yml oras-1.2.0/.github/workflows/release-ghcr.yml000066400000000000000000000030131462530432700211420ustar00rootroot00000000000000# 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@v4 - 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@v3 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.2.0/.github/workflows/release-github.yml000066400000000000000000000021201462530432700214770ustar00rootroot00000000000000# 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@v4 - name: setup go environment uses: actions/setup-go@v5 with: go-version: '1.22.3' - name: run goreleaser uses: goreleaser/goreleaser-action@v5 with: version: latest args: release --rm-dist env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} oras-1.2.0/.github/workflows/release-snap.yml000066400000000000000000000037111462530432700211650ustar00rootroot00000000000000# 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@v4 - 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: snapcore/action-build@v1 id: build - uses: snapcore/action-publish@v1 name: publish env: SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} with: snap: ${{ steps.build.outputs.snap }} release: ${{ steps.version.outputs.release }} oras-1.2.0/.github/workflows/stale.yml000066400000000000000000000027121462530432700177160ustar00rootroot00000000000000# 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@v9 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.2.0/.gitignore000066400000000000000000000025401462530432700144550ustar00rootroot00000000000000# 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 # 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_bcryptoras-1.2.0/.goreleaser.yml000066400000000000000000000035021462530432700154150ustar00rootroot00000000000000# 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 builds: - main: ./cmd/oras binary: ./oras env: - CGO_ENABLED=0 flags: - -trimpath goos: - darwin - linux - freebsd - windows goarch: - amd64 - arm64 - arm - s390x - ppc64le - riscv64 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 release: draft: true prerelease: auto #signs: # - artifacts: all # args: ["--output", "${signature}", "--detach-sign", "--armor", "${artifact}"] # signature: "${artifact}.asc" oras-1.2.0/CODEOWNERS000066400000000000000000000001231462530432700140530ustar00rootroot00000000000000# Derived from OWNERS.md * @sajayantony @shizhMSFT @stevelasker @qweeah @TerryHowe oras-1.2.0/CODE_OF_CONDUCT.md000066400000000000000000000002311462530432700152570ustar00rootroot00000000000000# 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.2.0/CONTRIBUTING.md000066400000000000000000000001641462530432700147160ustar00rootroot00000000000000# Contributing Please refer to the [ORAS Contributing guide](https://oras.land/docs/community/contributing_guide). oras-1.2.0/Dockerfile000066400000000000000000000020161462530432700144550ustar00rootroot00000000000000# 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.22.3-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.17.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.2.0/KEYS000066400000000000000000000074571462530432700131770ustar00rootroot00000000000000This 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-01-31 [SC] [expires: 2025-01-30] 46D3369B393F6F8271FD1CE8F86EC70D2B0C404F uid [ultimate] Billy Zha sig 3 F86EC70D2B0C404F 2024-01-31 Billy Zha -----BEGIN PGP PUBLIC KEY BLOCK----- mQINBGW5+fIBEADk9gPL8CPQzYcYwa/vwWmuFMo7Fo9ceLPBik67M1DTlfuRPl5P CCYAWEG+ou+qAj5rVApE2focHrO30z3RkAyUxE4AkC/JZF9cxIOVK+zAbNOrbKDP ZJNED79uuM1LM70T6683alB/RC5fD8QvxjoJOQs9J+YBrKfmNBMKONyVdhH1FFN9 PqwXNg8VeB82JFWSHeCLVOcZibhHDHo4hV7uGyU5k1z/TplMhr7/VQps7ej9X8dI xmWKlKPCFXfRr944aQUyCWCVf7KPLz5AhBTFZrb3mioTGX1F2Jvu60gr2THGMICK 4wIMsKle2Pu+xN1cINvwGLm7p55TOcRR1pHmoW9hXHkExBWvx6NnQdp772VAcoRS /0UABSurLYG2I0w1dzzQhAX8TY0UUC4mW2uTQCr1ihKzH7LTjXEy9+kghLr/NYmo VlcETTm7ACDHHaPqB0db90Rv1gUXzabU7acjYhUV4z1t/0suR5woYg9zEB7Y55vg FGfSHLbGJV6mdifJyZ0SNi8yK2FsH2bJRJUlaMvZ2ob+ORx+AATEsAKzyQZk260B hN+06httAzknKc3QfzhlddDgjCPzuatTCnBA1/Sz0MVNNAaYCtbszGyLz1NNYUXp ksb76GwvmHslAVBnEMqgJaJPKHfrdeLTPbGeRwCQBKiR38CL7gzZS06fFQARAQAB tCFCaWxseSBaaGEgPGppbnpoYTFAbWljcm9zb2Z0LmNvbT6JAlQEEwEKAD4WIQRG 0zabOT9vgnH9HOj4bscNKwxATwUCZbn58gIbAwUJAeEzgAULCQgHAgYVCgkICwIE FgIDAQIeAQIXgAAKCRD4bscNKwxAT3TiEACuaSzmzkBXJNUUh1VQvOui5uoiX8ln MqXqEDhrVLjliuMTwG5tVTEwKZ07fsTnLKfvDUVJUrcESdk1yNm66KXkx9kUXKi8 9xfCCRjBD+p9ejCXQa2ovyiesFJzHJnlHJ2s6NiDsOq622h5WlmQMG6q7hm2eGmv kKYh9iTtrfU4zqihoMa4QZb0yvN0cSOdzI5MFAY1fKb9IXQcUKQgpaZViMqTmBJg kqheNcpl1VMKYopCVthDrB0Pmdvgf5dzwSq0vZKwA+Jelm9hfj7xkDwqQL8Yacn2 oXKqim+5hSKFuXYbMQwtvgjFvE3CuNXWuAi1ZLenOgfUxpHyWL9FuhWUi15gcr+t KJ1LSuQdMy2cLhZQ2QH4lSe7sUdOPXHln9KVyuwq2JxpRB/WkZvXzJqD9nga99tv Dhy6hqGTzLqGXIzxkKjJptHdMiUYdvhHgxofQTrVVZNKLyVIKwwbPwB3iTtcleWB MQUk8zqg+wuJP7NS+zgxB84Y4tkXvIloAZScT/wCrBxJA/ssiG1RqANtPcFbeIua q8p0JW3EDVXI8XnYjdES4FnLpOdNpZIQ5Rje2kSJE+H44HSX2uUH4F7drHOTlxj3 IhDdgxLInXr2JW5qRpVw2RKIaMtlm8ik5GKaj8bPQYbEb2p/TDDGiGPHFoOeRJdo 1v1vh+eidc0D77kCDQRlufnyARAAyxNbIccAfShakgiU6iKEKDDwgdGP8dElu7Zb +2xq16KUHFrD+zpv1xmH8DsVKdFlLrbQH8IT3NxLnXwY8Va92+Qe5kva1hjVMTXi v3M+ndsSmNtqw0wlHIuKlQHIs10V0yi4GaQ2gsxlsc4S4lTwWpleGBX6Zso9PFE6 77nd0hIx00En1YB+OJLLuZY+KgiG3nfvRaHoD0i3prc6paK1fSGkcedg15WXyvf4 yoOHe6+wUd4oy1nQkLVs6l0rwBTJDG1F8pwYbYPX0KRfMXmJk0DRQ5TGS3s+aMQj kXU39KkNs8UtLVkKLc7ksB4foukc0pFBAuubgb/YZ55Akld/Q95HLS/nFcpUBhTf 49zxBMssg6KPu+1mcCqoeocfCnRXSlnbaKVGkWFnTSe52iPBSL6X4XNyF3JOKqvo llz4zm/EklrusOUvI1eITNj4HbCM1DWm94KG41mBMIe8V2SVHLlyVn/5Jy7aDVk4 9OMJc/QYchj0U/xE6xS30/cMH2mY+JgnlfQ9uu6rFhZzclZzO+eBDj5pEZXOueLq 0x3oJqdOQREOdSDwxSgU1w6W/C1y7dzk2johOQ0rTTgwDz2QNcCEmHZ3gati7i3d xtQ/0JR69I4/GlJ3nyViuMqUQkkf0RfOcNpBjJwcg40koStkzPEJFfm3HqDZ1lwH s7BjlEkAEQEAAYkCPAQYAQoAJhYhBEbTNps5P2+Ccf0c6Phuxw0rDEBPBQJlufny AhsMBQkB4TOAAAoJEPhuxw0rDEBPUj8P+QE92A0Phb1/QAGnaU/XfU9iooUTrYF6 u1tQQLrX4SqTETtWQjEoW+7i+BzTtP2hjPcroC/EeJC7PP9IJFPJSiLK42f8OVBc eD3VKl8Ae2yv5irlQMYC0q4yOkT/ZdT4DAPzRfwpZON5FsX+e+3tCj41Z6zxgICz gdw0oIWHi7SpyqQF3cnHQPK24NFCkp5QQgLTySBZ1luAaXzdiXeImuN6IHU+60BW zdhCdjI/s1ewXhVk23HfeOSQ06tgXSOr9fDa8nicPSuFZ6FmVgrDtA1qnMeDHvFm wXbphWl+rBvBP1ktjIL3eaaL91NepdXN+H6A3Yk8lH0zL7GBEPo8oMz/zrCxpkcl dyx/Lt459UY1jMat9UX0GlkqZ7xRjKq8YDUMtLAdp5h7L+MsotKZ9YrbEyda+Ife fHjCxV4jXpO/MGLXRKM8tRHqgzUPesq6SLDMYsdyQ6SdFTcDrZtQmkuPXgUHuqLW j1xL5tFC6oz5Y2pWYv6JmEMz7wXhhS/PXo+5A2GicpAFOgoaTK+OM7VvN4f4ExCo j9ugKadS2ggYIU1Vi4iEWuLykQKQtEa4qAjC+cD7oQ1mgSZVmg7wQS7keoshbsZM H9l5uBixf+MDUpSPe/YNMz9iZSeWwkW8fmA1EOOFapryw+DEA6glT7AjERGz/pFx blbo6H6w/7d+ =sd98 -----END PGP PUBLIC KEY BLOCK----- oras-1.2.0/LICENSE000066400000000000000000000261171462530432700135000ustar00rootroot00000000000000 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.2.0/MAINTAINERS.md000066400000000000000000000001671462530432700145640ustar00rootroot00000000000000# Maintainers Maintainers: - Billy Zha (@qweeah) - Terry Howe (@TerryHowe) [Owners](OWNERS.md) are also maintainers. oras-1.2.0/Makefile000066400000000000000000000140231462530432700141240ustar00rootroot00000000000000# 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 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 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: test test: tidy vendor check-encoding ## tidy and run tests $(GO_EXE) test -race -v -coverprofile=coverage.txt -covermode=atomic -coverpkg=./... ./... .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 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-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: 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.2.0/OWNERS.md000066400000000000000000000003631462530432700140250ustar00rootroot00000000000000# Owners Owners: - Sajay Antony (@sajayantony) - Shiwei Zhang (@shizhMSFT) - Steve Lasker (@stevelasker) Emeritus: - Avi Deitcher (@deitch) - Jimmy Zelinskie (@jzelinskie) - Josh Dolitsky (@jdolitsky) - Vincent Batts (@vbatts) oras-1.2.0/README.md000066400000000000000000000020171462530432700137430ustar00rootroot00000000000000# 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)

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/docs/community/developer_guide) to get started [contributing to ORAS](https://oras.land/docs/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.2.0/SECURITY.md000066400000000000000000000002441462530432700142550ustar00rootroot00000000000000# Security Policy Please follow the [security policy](https://oras.land/docs/community/reporting_security_concerns) to report a security vulnerability or concern. oras-1.2.0/cmd/000077500000000000000000000000001462530432700132275ustar00rootroot00000000000000oras-1.2.0/cmd/oras/000077500000000000000000000000001462530432700141735ustar00rootroot00000000000000oras-1.2.0/cmd/oras/internal/000077500000000000000000000000001462530432700160075ustar00rootroot00000000000000oras-1.2.0/cmd/oras/internal/argument/000077500000000000000000000000001462530432700176315ustar00rootroot00000000000000oras-1.2.0/cmd/oras/internal/argument/checker.go000066400000000000000000000020621462530432700215640ustar00rootroot00000000000000/* 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.2.0/cmd/oras/internal/command/000077500000000000000000000000001462530432700174255ustar00rootroot00000000000000oras-1.2.0/cmd/oras/internal/command/logger.go000066400000000000000000000020131462530432700212270ustar00rootroot00000000000000/* 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, opts.Verbose) cmd.SetContext(ctx) return ctx, logger } oras-1.2.0/cmd/oras/internal/display/000077500000000000000000000000001462530432700174545ustar00rootroot00000000000000oras-1.2.0/cmd/oras/internal/display/content/000077500000000000000000000000001462530432700211265ustar00rootroot00000000000000oras-1.2.0/cmd/oras/internal/display/content/discard.go000066400000000000000000000016531462530432700230730ustar00rootroot00000000000000/* 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 } // NewDiscardHandler returns a new discard handler. func NewDiscardHandler() ManifestFetchHandler { return discardHandler{} } oras-1.2.0/cmd/oras/internal/display/content/interface.go000066400000000000000000000015711462530432700234210ustar00rootroot00000000000000/* 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 } oras-1.2.0/cmd/oras/internal/display/content/manifest_fetch.go000066400000000000000000000026571462530432700244460ustar00rootroot00000000000000/* 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/display/utils" ) // manifestFetch handles raw content output. type manifestFetch struct { pretty bool stdout io.Writer outputPath string } func (h *manifestFetch) OnContentFetched(desc ocispec.Descriptor, manifest []byte) 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 f.Close() out = f } return utils.PrintJSON(out, manifest, h.pretty) } // NewManifestFetchHandler creates a new handler. func NewManifestFetchHandler(out io.Writer, pretty bool, outputPath string) ManifestFetchHandler { return &manifestFetch{ pretty: pretty, stdout: out, outputPath: outputPath, } } oras-1.2.0/cmd/oras/internal/display/handler.go000066400000000000000000000135111462530432700214210ustar00rootroot00000000000000/* 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" "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" ) // NewPushHandler returns status and metadata handlers for push command. func NewPushHandler(out io.Writer, format option.Format, tty *os.File, verbose bool) (status.PushHandler, metadata.PushHandler, error) { var statusHandler status.PushHandler if tty != nil { statusHandler = status.NewTTYPushHandler(tty) } else if format.Type == "" { statusHandler = status.NewTextPushHandler(out, verbose) } else { statusHandler = status.NewDiscardHandler() } var metadataHandler metadata.PushHandler switch format.Type { case "": metadataHandler = text.NewPushHandler(out) case option.FormatTypeJSON.Name: metadataHandler = json.NewPushHandler(out) case option.FormatTypeGoTemplate.Name: metadataHandler = template.NewPushHandler(out, 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(out io.Writer, format option.Format, tty *os.File, verbose bool) (status.AttachHandler, metadata.AttachHandler, error) { var statusHandler status.AttachHandler if tty != nil { statusHandler = status.NewTTYAttachHandler(tty) } else if format.Type == "" { statusHandler = status.NewTextAttachHandler(out, verbose) } else { statusHandler = status.NewDiscardHandler() } var metadataHandler metadata.AttachHandler switch format.Type { case "": metadataHandler = text.NewAttachHandler(out) case option.FormatTypeJSON.Name: metadataHandler = json.NewAttachHandler(out) case option.FormatTypeGoTemplate.Name: metadataHandler = template.NewAttachHandler(out, 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(out io.Writer, format option.Format, path string, tty *os.File, verbose bool) (status.PullHandler, metadata.PullHandler, error) { var statusHandler status.PullHandler if tty != nil { statusHandler = status.NewTTYPullHandler(tty) } else if format.Type == "" { statusHandler = status.NewTextPullHandler(out, verbose) } else { statusHandler = status.NewDiscardHandler() } var metadataHandler metadata.PullHandler switch format.Type { case "": metadataHandler = text.NewPullHandler(out) case option.FormatTypeJSON.Name: metadataHandler = json.NewPullHandler(out, path) case option.FormatTypeGoTemplate.Name: metadataHandler = template.NewPullHandler(out, 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) (metadata.DiscoverHandler, error) { var handler metadata.DiscoverHandler switch format.Type { case option.FormatTypeTree.Name, "": handler = tree.NewDiscoverHandler(out, path, desc, verbose) 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 "": // 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 } oras-1.2.0/cmd/oras/internal/display/metadata/000077500000000000000000000000001462530432700212345ustar00rootroot00000000000000oras-1.2.0/cmd/oras/internal/display/metadata/descriptor/000077500000000000000000000000001462530432700234125ustar00rootroot00000000000000oras-1.2.0/cmd/oras/internal/display/metadata/descriptor/manifest_fetch.go000066400000000000000000000026161462530432700267250ustar00rootroot00000000000000/* 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/display/utils" ) // 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 utils.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.2.0/cmd/oras/internal/display/metadata/discard.go000066400000000000000000000016401462530432700231750ustar00rootroot00000000000000/* 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" 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 } oras-1.2.0/cmd/oras/internal/display/metadata/interface.go000066400000000000000000000044731462530432700235330ustar00rootroot00000000000000/* 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" ) // PushHandler handles metadata output for push events. type PushHandler interface { TagHandler OnCopied(opts *option.Target) error OnCompleted(root ocispec.Descriptor) error } // AttachHandler handles metadata output for attach events. type AttachHandler interface { OnCompleted(opts *option.Target, root, subject ocispec.Descriptor) error } // DiscoverHandler handles metadata output for discover events. type DiscoverHandler interface { // MultiLevelSupported returns true if the handler supports multi-level // discovery. MultiLevelSupported() bool // OnDiscovered is called after a referrer is discovered. OnDiscovered(referrer, subject ocispec.Descriptor) error // OnCompleted is called when referrer discovery is completed. OnCompleted() 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 { // 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 // OnCompleted is called when the pull cmd execution is completed. OnCompleted(opts *option.Target, desc ocispec.Descriptor) error } // TagHandler handles status output for tag command. type TagHandler interface { // OnTagged is called when each tagging operation is done. OnTagged(desc ocispec.Descriptor, tag string) error } oras-1.2.0/cmd/oras/internal/display/metadata/json/000077500000000000000000000000001462530432700222055ustar00rootroot00000000000000oras-1.2.0/cmd/oras/internal/display/metadata/json/attach.go000066400000000000000000000025261462530432700240050ustar00rootroot00000000000000/* 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/display/utils" "oras.land/oras/cmd/oras/internal/option" ) // AttachHandler handles json metadata output for attach events. type AttachHandler struct { out io.Writer } // NewAttachHandler creates a new handler for attach events. func NewAttachHandler(out io.Writer) metadata.AttachHandler { return &AttachHandler{ out: out, } } // OnCompleted is called when the attach command is completed. func (ah *AttachHandler) OnCompleted(opts *option.Target, root, subject ocispec.Descriptor) error { return utils.PrintPrettyJSON(ah.out, model.NewAttach(root, opts.Path)) } oras-1.2.0/cmd/oras/internal/display/metadata/json/discover.go000066400000000000000000000035661462530432700243640ustar00rootroot00000000000000/* 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 ( "fmt" "io" 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/display/metadata/model" "oras.land/oras/cmd/oras/internal/display/utils" ) // discoverHandler handles json metadata output for discover events. type discoverHandler struct { out io.Writer root ocispec.Descriptor path string referrers []ocispec.Descriptor } // NewDiscoverHandler creates a new handler for discover events. func NewDiscoverHandler(out io.Writer, root ocispec.Descriptor, path string) metadata.DiscoverHandler { return &discoverHandler{ out: out, root: root, path: path, } } // MultiLevelSupported implements metadata.DiscoverHandler. func (h *discoverHandler) MultiLevelSupported() bool { return false } // 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 } // OnCompleted implements metadata.DiscoverHandler. func (h *discoverHandler) OnCompleted() error { return utils.PrintPrettyJSON(h.out, model.NewDiscover(h.path, h.referrers)) } oras-1.2.0/cmd/oras/internal/display/metadata/json/manifest_fetch.go000066400000000000000000000027521462530432700255210ustar00rootroot00000000000000/* 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/display/utils" ) // 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 utils.PrintPrettyJSON(h.out, model.NewFetched(path, desc, manifest)) } oras-1.2.0/cmd/oras/internal/display/metadata/json/pull.go000066400000000000000000000033651462530432700235170ustar00rootroot00000000000000/* 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/display/utils" "oras.land/oras/cmd/oras/internal/option" ) // PullHandler handles JSON metadata output for pull events. type PullHandler struct { path string pulled model.Pulled out io.Writer } // OnLayerSkipped implements metadata.PullHandler. func (ph *PullHandler) OnLayerSkipped(ocispec.Descriptor) error { return nil } // NewPullHandler returns a new handler for Pull events. func NewPullHandler(out io.Writer, path string) metadata.PullHandler { return &PullHandler{ out: out, path: path, } } // 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) } // OnCompleted implements metadata.PullHandler. func (ph *PullHandler) OnCompleted(opts *option.Target, desc ocispec.Descriptor) error { return utils.PrintPrettyJSON(ph.out, model.NewPull(ph.path+"@"+desc.Digest.String(), ph.pulled.Files())) } oras-1.2.0/cmd/oras/internal/display/metadata/json/push.go000066400000000000000000000034241462530432700235160ustar00rootroot00000000000000/* 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/display/utils" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/internal/contentutil" ) // PushHandler handles JSON metadata output for push events. type PushHandler struct { path string out io.Writer tagged model.Tagged } // NewPushHandler creates a new handler for push events. func NewPushHandler(out io.Writer) metadata.PushHandler { return &PushHandler{ out: out, } } // OnTagged implements metadata.TagHandler. func (ph *PushHandler) OnTagged(desc 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) error { if opts.RawReference != "" && !contentutil.IsDigest(opts.Reference) { ph.tagged.AddTag(opts.Reference) } ph.path = opts.Path return nil } // OnCompleted is called after the push is completed. func (ph *PushHandler) OnCompleted(root ocispec.Descriptor) error { return utils.PrintPrettyJSON(ph.out, model.NewPush(root, ph.path, ph.tagged.Tags())) } oras-1.2.0/cmd/oras/internal/display/metadata/model/000077500000000000000000000000001462530432700223345ustar00rootroot00000000000000oras-1.2.0/cmd/oras/internal/display/metadata/model/attach.go000066400000000000000000000015721462530432700241340ustar00rootroot00000000000000/* 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.2.0/cmd/oras/internal/display/metadata/model/descriptor.go000066400000000000000000000031641462530432700250450ustar00rootroot00000000000000/* 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.2.0/cmd/oras/internal/display/metadata/model/discover.go000066400000000000000000000020001462530432700244710ustar00rootroot00000000000000/* 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 discover struct { Manifests []Descriptor `json:"manifests"` } // NewDiscover creates a new discover model. func NewDiscover(name string, descs []ocispec.Descriptor) discover { discover := discover{ Manifests: make([]Descriptor, 0, len(descs)), } for _, desc := range descs { discover.Manifests = append(discover.Manifests, FromDescriptor(name, desc)) } return discover } oras-1.2.0/cmd/oras/internal/display/metadata/model/fetched.go000066400000000000000000000016171462530432700242720ustar00rootroot00000000000000/* 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.2.0/cmd/oras/internal/display/metadata/model/pull.go000066400000000000000000000045221462530432700236420ustar00rootroot00000000000000/* 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.2.0/cmd/oras/internal/display/metadata/model/push.go000066400000000000000000000021151462530432700236410ustar00rootroot00000000000000/* 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.2.0/cmd/oras/internal/display/metadata/model/tag.go000066400000000000000000000017671462530432700234510ustar00rootroot00000000000000/* 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.2.0/cmd/oras/internal/display/metadata/table/000077500000000000000000000000001462530432700223235ustar00rootroot00000000000000oras-1.2.0/cmd/oras/internal/display/metadata/table/discover.go000066400000000000000000000055571462530432700245040ustar00rootroot00000000000000/* 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/display/utils" ) // 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, } } // MultiLevelSupported implements metadata.DiscoverHandler. func (h *discoverHandler) MultiLevelSupported() bool { return false } // 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 } // OnCompleted implements metadata.DiscoverHandler. func (h *discoverHandler) OnCompleted() error { if n := len(h.referrers); n > 1 { fmt.Fprintln(h.out, "Discovered", n, "artifacts referencing", h.rawReference) } else { fmt.Fprintln(h.out, "Discovered", n, "artifact referencing", h.rawReference) } fmt.Fprintln(h.out, "Digest:", h.root.Digest) if len(h.referrers) == 0 { return nil } fmt.Fprintln(h.out) return h.printDiscoveredReferrersTable() } func (h *discoverHandler) printDiscoveredReferrersTable() error { typeNameTitle := "Artifact Type" typeNameLength := len(typeNameTitle) for _, ref := range h.referrers { if length := len(ref.ArtifactType); length > typeNameLength { typeNameLength = length } } print := func(key string, value interface{}) { fmt.Fprintln(h.out, key, strings.Repeat(" ", typeNameLength-len(key)+1), value) } print(typeNameTitle, "Digest") for _, ref := range h.referrers { print(ref.ArtifactType, ref.Digest) if h.verbose { if err := utils.PrintPrettyJSON(h.out, ref); err != nil { return fmt.Errorf("error printing JSON: %w", err) } } } return nil } oras-1.2.0/cmd/oras/internal/display/metadata/template/000077500000000000000000000000001462530432700230475ustar00rootroot00000000000000oras-1.2.0/cmd/oras/internal/display/metadata/template/attach.go000066400000000000000000000025671462530432700246540ustar00rootroot00000000000000/* 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" ) // AttachHandler handles go-template metadata output for attach events. type AttachHandler struct { template string out io.Writer } // NewAttachHandler returns a new handler for attach metadata events. func NewAttachHandler(out io.Writer, template string) metadata.AttachHandler { return &AttachHandler{ out: out, template: template, } } // OnCompleted formats the metadata of attach command. func (ah *AttachHandler) OnCompleted(opts *option.Target, root, subject ocispec.Descriptor) error { return parseAndWrite(ah.out, model.NewAttach(root, opts.Path), ah.template) } oras-1.2.0/cmd/oras/internal/display/metadata/template/discover.go000066400000000000000000000036211462530432700252160ustar00rootroot00000000000000/* 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 ( "fmt" "io" 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/display/metadata/model" ) // discoverHandler handles json metadata output for discover events. type discoverHandler struct { referrers []ocispec.Descriptor template string path string root ocispec.Descriptor out io.Writer } // 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, root: root, path: path, template: template, } } // MultiLevelSupported implements metadata.DiscoverHandler. func (h *discoverHandler) MultiLevelSupported() bool { return false } // 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 } // OnCompleted implements metadata.DiscoverHandler. func (h *discoverHandler) OnCompleted() error { return parseAndWrite(h.out, model.NewDiscover(h.path, h.referrers), h.template) } oras-1.2.0/cmd/oras/internal/display/metadata/template/manifest_fetch.go000066400000000000000000000030021462530432700263500ustar00rootroot00000000000000/* 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" ) // 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 parseAndWrite(h.out, model.NewFetched(path, desc, manifest), h.template) } oras-1.2.0/cmd/oras/internal/display/metadata/template/pull.go000066400000000000000000000034221462530432700243530ustar00rootroot00000000000000/* 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" ) // PullHandler handles text metadata output for pull events. type PullHandler struct { template string path string out io.Writer pulled model.Pulled } // OnCompleted implements metadata.PullHandler. func (ph *PullHandler) OnCompleted(opts *option.Target, desc ocispec.Descriptor) error { return parseAndWrite(ph.out, model.NewPull(ph.path+"@"+desc.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 } // 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, } } oras-1.2.0/cmd/oras/internal/display/metadata/template/push.go000066400000000000000000000034661462530432700243660ustar00rootroot00000000000000/* 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/internal/contentutil" ) // PushHandler handles go-template metadata output for push events. type PushHandler struct { template string path string tagged model.Tagged out io.Writer } // 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.TagHandler. func (ph *PushHandler) OnTagged(desc ocispec.Descriptor, tag string) error { ph.tagged.AddTag(tag) return nil } // OnStarted is called after files are copied. func (ph *PushHandler) OnCopied(opts *option.Target) error { if opts.RawReference != "" && !contentutil.IsDigest(opts.Reference) { ph.tagged.AddTag(opts.Reference) } ph.path = opts.Path return nil } // OnCompleted is called after the push is completed. func (ph *PushHandler) OnCompleted(root ocispec.Descriptor) error { return parseAndWrite(ph.out, model.NewPush(root, ph.path, ph.tagged.Tags()), ph.template) } oras-1.2.0/cmd/oras/internal/display/metadata/template/template.go000066400000000000000000000020301462530432700252040ustar00rootroot00000000000000/* 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" "text/template" "github.com/Masterminds/sprig/v3" "oras.land/oras/cmd/oras/internal/display/utils" ) func parseAndWrite(out io.Writer, object any, templateStr string) error { // parse template t, err := template.New("format output").Funcs(sprig.FuncMap()).Parse(templateStr) if err != nil { return err } // convert object to map[string]any converted, err := utils.ToMap(object) if err != nil { return err } return t.Execute(out, converted) } oras-1.2.0/cmd/oras/internal/display/metadata/template/template_test.go000066400000000000000000000013561462530432700262550ustar00rootroot00000000000000/* 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 ( "os" "testing" ) func Test_parseAndWrite_err(t *testing.T) { if err := parseAndWrite(os.Stdout, func() {}, ""); err == nil { t.Errorf("should return error") } } oras-1.2.0/cmd/oras/internal/display/metadata/text/000077500000000000000000000000001462530432700222205ustar00rootroot00000000000000oras-1.2.0/cmd/oras/internal/display/metadata/text/attach.go000066400000000000000000000030011462530432700240050ustar00rootroot00000000000000/* 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" "io" "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" ) // AttachHandler handles text metadata output for attach events. type AttachHandler struct { out io.Writer } // NewAttachHandler returns a new handler for attach events. func NewAttachHandler(out io.Writer) metadata.AttachHandler { return &AttachHandler{ out: out, } } // OnCompleted is called when the attach command is completed. func (ah *AttachHandler) OnCompleted(opts *option.Target, root, subject ocispec.Descriptor) error { digest := subject.Digest.String() if !strings.HasSuffix(opts.RawReference, digest) { opts.RawReference = fmt.Sprintf("%s@%s", opts.Path, subject.Digest) } _, err := fmt.Fprintln(ah.out, "Attached to", opts.AnnotatedReference()) if err != nil { return err } _, err = fmt.Fprintln(ah.out, "Digest:", root.Digest) return err } oras-1.2.0/cmd/oras/internal/display/metadata/text/pull.go000066400000000000000000000035401462530432700235250ustar00rootroot00000000000000/* 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" "io" "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" ) // PullHandler handles text metadata output for pull events. type PullHandler struct { out io.Writer layerSkipped atomic.Bool } // OnCompleted implements metadata.PullHandler. func (p *PullHandler) OnCompleted(opts *option.Target, desc ocispec.Descriptor) error { if p.layerSkipped.Load() { _, _ = fmt.Fprintf(p.out, "Skipped pulling layers without file name in %q\n", ocispec.AnnotationTitle) _, _ = fmt.Fprintf(p.out, "Use 'oras copy %s --to-oci-layout ' to pull all layers.\n", opts.RawReference) } else { _, _ = fmt.Fprintln(p.out, "Pulled", opts.AnnotatedReference()) _, _ = fmt.Fprintln(p.out, "Digest:", desc.Digest) } return nil } func (p *PullHandler) OnFilePulled(name string, outputDir string, desc ocispec.Descriptor, descPath string) error { return nil } // OnLayerSkipped implements metadata.PullHandler. func (ph *PullHandler) OnLayerSkipped(ocispec.Descriptor) error { ph.layerSkipped.Store(true) return nil } // NewPullHandler returns a new handler for Pull events. func NewPullHandler(out io.Writer) metadata.PullHandler { return &PullHandler{ out: out, } } oras-1.2.0/cmd/oras/internal/display/metadata/text/push.go000066400000000000000000000033231462530432700235270ustar00rootroot00000000000000/* 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" "io" "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" ) // PushHandler handles text metadata output for push events. type PushHandler struct { out io.Writer tagLock sync.Mutex } // NewPushHandler returns a new handler for push events. func NewPushHandler(out io.Writer) metadata.PushHandler { return &PushHandler{ out: out, } } // OnTagged implements metadata.TextTagHandler. func (h *PushHandler) OnTagged(_ ocispec.Descriptor, tag string) error { h.tagLock.Lock() defer h.tagLock.Unlock() _, err := fmt.Fprintln(h.out, "Tagged", tag) return err } // OnCopied is called after files are copied. func (h *PushHandler) OnCopied(opts *option.Target) error { _, err := fmt.Fprintln(h.out, "Pushed", opts.AnnotatedReference()) return err } // OnCompleted is called after the push is completed. func (h *PushHandler) OnCompleted(root ocispec.Descriptor) error { _, err := fmt.Fprintln(h.out, "ArtifactType:", root.ArtifactType) if err != nil { return err } _, err = fmt.Fprintln(h.out, "Digest:", root.Digest) return err } oras-1.2.0/cmd/oras/internal/display/metadata/text/push_test.go000066400000000000000000000033301462530432700245640ustar00rootroot00000000000000/* 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" "testing" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) type errorWriter struct{} // Write implements the io.Writer interface and returns an error in Write. func (w *errorWriter) Write(p []byte) (n int, err error) { return 0, fmt.Errorf("got an error") } func TestPushHandler_OnCompleted(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) { p := &PushHandler{ out: tt.out, } if err := p.OnCompleted(tt.root); (err != nil) != tt.wantErr { t.Errorf("PushHandler.OnCompleted() error = %v, wantErr %v", err, tt.wantErr) } }) } } oras-1.2.0/cmd/oras/internal/display/metadata/tree/000077500000000000000000000000001462530432700221735ustar00rootroot00000000000000oras-1.2.0/cmd/oras/internal/display/metadata/tree/discover.go000066400000000000000000000045171462530432700243470ustar00rootroot00000000000000/* 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" "strings" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "gopkg.in/yaml.v3" "oras.land/oras/cmd/oras/internal/display/metadata" "oras.land/oras/internal/tree" ) // 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 } // NewDiscoverHandler creates a new handler for discover events. func NewDiscoverHandler(out io.Writer, path string, root ocispec.Descriptor, verbose bool) metadata.DiscoverHandler { treeRoot := tree.New(fmt.Sprintf("%s@%s", path, root.Digest)) return &discoverHandler{ out: out, path: path, root: treeRoot, nodes: map[digest.Digest]*tree.Node{ root.Digest: treeRoot, }, verbose: verbose, } } // MultiLevelSupported implements metadata.DiscoverHandler. func (h *discoverHandler) MultiLevelSupported() bool { return true } // 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) } if referrer.ArtifactType == "" { referrer.ArtifactType = "" } referrerNode := node.AddPath(referrer.ArtifactType, referrer.Digest) if h.verbose { for k, v := range referrer.Annotations { bytes, err := yaml.Marshal(map[string]string{k: v}) if err != nil { return err } referrerNode.AddPath(strings.TrimSpace(string(bytes))) } } h.nodes[referrer.Digest] = referrerNode return nil } // OnCompleted implements metadata.DiscoverHandler. func (h *discoverHandler) OnCompleted() error { return tree.NewPrinter(h.out).Print(h.root) } oras-1.2.0/cmd/oras/internal/display/status/000077500000000000000000000000001462530432700207775ustar00rootroot00000000000000oras-1.2.0/cmd/oras/internal/display/status/console/000077500000000000000000000000001462530432700224415ustar00rootroot00000000000000oras-1.2.0/cmd/oras/internal/display/status/console/console.go000066400000000000000000000050631462530432700244360ustar00rootroot00000000000000/* 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" "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 // cannot use aec.Save since DEC has better compatilibity than SCO Save = "\0337" // cannot use aec.Restore since DEC has better compatilibity than SCO Restore = "\0338" ) // Console is a wrapper around containerd's console.Console and ANSI escape // codes. type Console struct { console.Console } // Size returns the width and height of the console. // If the console size cannot be determined, returns a default value of 80x10. func (c *Console) Size() (width, height int) { width = MinWidth height = MinHeight size, err := c.Console.Size() if err == nil { if size.Height > MinHeight { height = int(size.Height) } if size.Width > MinWidth { width = int(size.Width) } } return } // New generates a Console from a file. func New(f *os.File) (*Console, error) { c, err := console.ConsoleFromFile(f) if err != nil { return nil, err } return &Console{c}, nil } // 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 compatilibity than SCO _, _ = c.Write([]byte(Restore)) _, _ = c.Write([]byte(aec.Column(0). With(aec.EraseLine(aec.EraseModes.All)). With(aec.Show).String())) } oras-1.2.0/cmd/oras/internal/display/status/console/console_test.go000066400000000000000000000033501462530432700254720ustar00rootroot00000000000000//go:build freebsd || linux || netbsd || openbsd || solaris /* 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 ( "testing" "github.com/containerd/console" ) 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 TestConsole_Size(t *testing.T) { pty, _, err := console.NewPty() if err != nil { t.Fatal(err) } c := &Console{ Console: pty, } // minimal width and height gotWidth, gotHeight := c.Size() validateSize(t, gotWidth, gotHeight, MinWidth, MinHeight) // zero width _ = pty.Resize(console.WinSize{Width: 0, Height: MinHeight}) gotWidth, gotHeight = c.Size() validateSize(t, gotWidth, gotHeight, MinWidth, MinHeight) // zero height _ = pty.Resize(console.WinSize{Width: MinWidth, Height: 0}) gotWidth, gotHeight = c.Size() validateSize(t, gotWidth, gotHeight, MinWidth, MinHeight) // valid zero and height _ = pty.Resize(console.WinSize{Width: 200, Height: 100}) gotWidth, gotHeight = c.Size() validateSize(t, gotWidth, gotHeight, 200, 100) } oras-1.2.0/cmd/oras/internal/display/status/console/testutils/000077500000000000000000000000001462530432700245015ustar00rootroot00000000000000oras-1.2.0/cmd/oras/internal/display/status/console/testutils/testutils.go000066400000000000000000000033651462530432700270770ustar00rootroot00000000000000//go:build freebsd || linux || netbsd || openbsd || solaris /* 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 testutils import ( "bytes" "fmt" "io" "os" "strings" "sync" "github.com/containerd/console" ) // NewPty creates a new pty pair for testing, caller is responsible for closing // the returned device file if err is not nil. func NewPty() (console.Console, *os.File, error) { pty, devicePath, err := console.NewPty() if err != nil { return nil, nil, err } device, err := os.OpenFile(devicePath, os.O_RDWR, 0) if err != nil { return nil, nil, err } return pty, device, nil } // MatchPty checks that the output matches the expected strings in specified // order. func MatchPty(pty console.Console, device *os.File, expected ...string) error { var wg sync.WaitGroup wg.Add(1) var buffer bytes.Buffer go func() { defer wg.Done() _, _ = io.Copy(&buffer, pty) }() device.Close() wg.Wait() return OrderedMatch(buffer.String(), expected...) } // OrderedMatch matches the got with the expected strings in order. func OrderedMatch(got string, want ...string) error { for _, e := range want { i := strings.Index(got, e) if i < 0 { return fmt.Errorf("failed to find %q in %q", e, got) } got = got[i+len(e):] } return nil } oras-1.2.0/cmd/oras/internal/display/status/deprecated.go000066400000000000000000000036101462530432700234260ustar00rootroot00000000000000/* 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 ( "os" "sync" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2" "oras.land/oras/internal/listener" ) // Types and functions in this file are deprecated and should be removed when // no-longer referenced. // NewTagStatusHintPrinter creates a wrapper type for printing // tag status and hint. func NewTagStatusHintPrinter(target oras.Target, refPrefix string) oras.Target { var printHint sync.Once var printHintErr error onTagging := func(desc ocispec.Descriptor, tag string) error { printHint.Do(func() { ref := refPrefix + "@" + desc.Digest.String() printHintErr = Print("Tagging", ref) }) return printHintErr } onTagged := func(desc ocispec.Descriptor, tag string) error { return Print("Tagged", tag) } return listener.NewTagListener(target, onTagging, onTagged) } // NewTagStatusPrinter creates a wrapper type for printing tag status. func NewTagStatusPrinter(target oras.Target) oras.Target { return listener.NewTagListener(target, nil, func(desc ocispec.Descriptor, tag string) error { return Print("Tagged", tag) }) } // printer is used by the code being deprecated. Related functions should be // removed when no-longer referenced. var printer = NewPrinter(os.Stdout) // Print objects to display concurrent-safely. func Print(a ...any) error { return printer.Println(a...) } oras-1.2.0/cmd/oras/internal/display/status/discard.go000066400000000000000000000043021462530432700227360ustar00rootroot00000000000000/* 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 ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2" "oras.land/oras-go/v2/content" ) 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(name 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 } // UpdateCopyOptions updates the copy options for the artifact push. func (DiscardHandler) UpdateCopyOptions(opts *oras.CopyGraphOptions, fetcher content.Fetcher) {} // OnNodeDownloading implements PullHandler. func (DiscardHandler) OnNodeDownloading(desc ocispec.Descriptor) error { return nil } // OnNodeDownloaded implements PullHandler. func (DiscardHandler) OnNodeDownloaded(desc ocispec.Descriptor) error { return nil } // OnNodeRestored implements PullHandler. func (DiscardHandler) OnNodeRestored(_ ocispec.Descriptor) error { return nil } // OnNodeProcessing implements PullHandler. func (DiscardHandler) OnNodeProcessing(desc ocispec.Descriptor) error { return nil } // OnNodeProcessing implements PullHandler. func (DiscardHandler) OnNodeSkipped(desc ocispec.Descriptor) error { return nil } oras-1.2.0/cmd/oras/internal/display/status/interface.go000066400000000000000000000037341462530432700232750ustar00rootroot00000000000000/* 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 ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2" "oras.land/oras-go/v2/content" ) // 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) UpdateCopyOptions(opts *oras.CopyGraphOptions, fetcher content.Fetcher) } // 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 } oras-1.2.0/cmd/oras/internal/display/status/print.go000066400000000000000000000050141462530432700224620ustar00rootroot00000000000000/* 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" "fmt" "io" "oras.land/oras/internal/descriptor" "os" "sync" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2/content" ) // PrintFunc is the function type returned by StatusPrinter. type PrintFunc func(ocispec.Descriptor) error // Printer prints for status handlers. type Printer struct { out io.Writer lock sync.Mutex } // NewPrinter creates a new Printer. func NewPrinter(out io.Writer) *Printer { return &Printer{out: out} } // Println prints objects concurrent-safely with newline. func (p *Printer) Println(a ...any) error { p.lock.Lock() defer p.lock.Unlock() _, err := fmt.Fprintln(p.out, a...) if err != nil { err = fmt.Errorf("display output error: %w", err) _, _ = fmt.Fprint(os.Stderr, err) } // Errors are handled above, so return nil return nil } // PrintStatus prints transfer status. func (p *Printer) PrintStatus(desc ocispec.Descriptor, status string, verbose bool) error { name, ok := desc.Annotations[ocispec.AnnotationTitle] if !ok { // no status for unnamed content if !verbose { return nil } name = desc.MediaType } return p.Println(status, descriptor.ShortDigest(desc), name) } // StatusPrinter returns a tracking function for transfer status. func (p *Printer) StatusPrinter(status string, verbose bool) PrintFunc { return func(desc ocispec.Descriptor) error { return p.PrintStatus(desc, status, verbose) } } // PrintSuccessorStatus prints transfer status of successors. func PrintSuccessorStatus(ctx context.Context, desc ocispec.Descriptor, fetcher content.Fetcher, committed *sync.Map, print PrintFunc) error { successors, err := content.Successors(ctx, fetcher, desc) if err != nil { return err } for _, s := range successors { name := s.Annotations[ocispec.AnnotationTitle] if v, ok := committed.Load(s.Digest.String()); ok && v != name { // Reprint status for deduplicated content if err := print(s); err != nil { return err } } } return nil } oras-1.2.0/cmd/oras/internal/display/status/print_test.go000066400000000000000000000031731462530432700235250ustar00rootroot00000000000000/* 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 ( "fmt" "strconv" "strings" "testing" ) type mockWriter struct { errorCount int written string } func (mw *mockWriter) Write(p []byte) (n int, err error) { mw.written += string(p) if strings.TrimSpace(string(p)) != "boom" { return len(string(p)), nil } mw.errorCount++ return 0, fmt.Errorf("Boom: " + string(p)) } func (mw *mockWriter) String() string { return mw.written } func TestPrint_Error(t *testing.T) { mockWriter := &mockWriter{} printer := NewPrinter(mockWriter) printer.Println("boom") if mockWriter.errorCount != 1 { t.Error("Expected one errors actual <" + strconv.Itoa(mockWriter.errorCount) + ">") } } func TestPrint_NoError(t *testing.T) { mockWriter := &mockWriter{} printer := NewPrinter(mockWriter) expected := "blah blah" printer.Println(expected) actual := strings.TrimSpace(mockWriter.String()) if expected != actual { t.Error("Expected <" + expected + "> not equal to actual <" + actual + ">") } if mockWriter.errorCount != 0 { t.Error("Expected no errors actual <" + strconv.Itoa(mockWriter.errorCount) + ">") } } oras-1.2.0/cmd/oras/internal/display/status/progress/000077500000000000000000000000001462530432700226435ustar00rootroot00000000000000oras-1.2.0/cmd/oras/internal/display/status/progress/humanize/000077500000000000000000000000001462530432700244635ustar00rootroot00000000000000oras-1.2.0/cmd/oras/internal/display/status/progress/humanize/bytes.go000066400000000000000000000027101462530432700261400ustar00rootroot00000000000000/* 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("%v %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.2.0/cmd/oras/internal/display/status/progress/humanize/bytes_test.go000066400000000000000000000036021462530432700272000ustar00rootroot00000000000000/* 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.2.0/cmd/oras/internal/display/status/progress/manager.go000066400000000000000000000064411462530432700246110ustar00rootroot00000000000000/* 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" "oras.land/oras/cmd/oras/internal/display/status/console" ) 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") // Status is print message channel type Status chan *status // Manager is progress view master type Manager interface { Add() (Status, error) Close() error } type manager struct { status []*status statusLock sync.RWMutex console *console.Console updating sync.WaitGroup renderDone chan struct{} renderClosed chan struct{} } // NewManager initialized a new progress manager. func NewManager(f *os.File) (Manager, error) { c, err := console.New(f) if err != nil { return nil, err } m := &manager{ console: c, renderDone: make(chan struct{}), renderClosed: make(chan struct{}), } m.start() return m, nil } 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.statusLock.RLock() defer m.statusLock.RUnlock() // todo: update size in another routine width, height := m.console.Size() len := len(m.status) * 2 offset := 0 if len > height { // skip statuses that cannot be rendered offset = len - height } for ; offset < len; offset += 2 { status, progress := m.status[offset/2].String(width) m.console.OutputTo(uint(len-offset), status) m.console.OutputTo(uint(len-offset-1), progress) } } // Add appends a new status with 2-line space for rendering. func (m *manager) Add() (Status, error) { if m.closed() { return nil, errManagerStopped } s := newStatus() m.statusLock.Lock() m.status = append(m.status, s) m.statusLock.Unlock() defer m.console.NewRow() defer m.console.NewRow() return m.statusChan(s), nil } func (m *manager) statusChan(s *status) Status { ch := make(chan *status, BufferSize) m.updating.Add(1) go func() { defer m.updating.Done() for newStatus := range ch { s.Update(newStatus) } }() return ch } // 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.2.0/cmd/oras/internal/display/status/progress/speed.go000066400000000000000000000030211462530432700242660ustar00rootroot00000000000000/* 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 diplayed 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.2.0/cmd/oras/internal/display/status/progress/speed_test.go000066400000000000000000000022141462530432700253300ustar00rootroot00000000000000/* 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.2.0/cmd/oras/internal/display/status/progress/spinner.go000066400000000000000000000015211462530432700246470ustar00rootroot00000000000000/* 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.2.0/cmd/oras/internal/display/status/progress/spinner_test.go000066400000000000000000000016251462530432700257130ustar00rootroot00000000000000/* 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 := 0; i < len(spinnerSymbols); i++ { 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.2.0/cmd/oras/internal/display/status/progress/status.go000066400000000000000000000132761462530432700245260ustar00rootroot00000000000000/* 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() zeroStatus = "loading status..." zeroDigest = " └─ loading digest..." ) var ( spinnerColor = aec.LightYellowF doneMarkColor = aec.LightGreenF progressColor = aec.LightBlueB ) // status is used as message to update progress view. type status struct { done bool // done is true when the end time is set prompt string descriptor ocispec.Descriptor offset int64 total humanize.Bytes speedWindow *speedWindow startTime time.Time endTime time.Time mark spinner lock sync.Mutex } // newStatus generates a base empty status. func newStatus() *status { return &status{ offset: -1, total: humanize.ToBytes(0), speedWindow: newSpeedWindow(framePerSecond), } } // NewStatusMessage generates a status for messaging. func NewStatusMessage(prompt string, descriptor ocispec.Descriptor, offset int64) *status { return &status{ prompt: prompt, descriptor: descriptor, offset: offset, } } // StartTiming starts timing. func StartTiming() *status { return &status{ offset: -1, startTime: time.Now(), } } // EndTiming ends timing and set status to done. func EndTiming() *status { return &status{ offset: -1, endTime: time.Now(), } } func (s *status) isZero() bool { return s.offset < 0 && s.startTime.IsZero() && s.endTime.IsZero() } // String returns human-readable TTY strings of the status. func (s *status) String(width int) (string, string) { s.lock.Lock() defer s.lock.Unlock() if s.isZero() { return zeroStatus, zeroDigest } // todo: doesn't support multiline prompt total := uint64(s.descriptor.Size) var percent float64 name := s.descriptor.Annotations["org.opencontainers.image.title"] if name == "" { name = s.descriptor.MediaType } // format: [left--------------------------------------------][margin][right---------------------------------] // mark(1) bar(22) speed(8) action(<=11) name(<=126) size_per_size(<=13) percent(8) time(>=6) // └─ digest(72) var offset string switch s.done { case true: // 100%, show exact size offset = fmt.Sprint(s.total.Size) percent = 1 default: // 0% ~ 99%, show 2-digit precision if total != 0 && s.offset >= 0 { // percentage calculatable percent = float64(s.offset) / float64(total) } offset = fmt.Sprintf("%.2f", humanize.RoundTo(s.total.Size*percent)) } right := fmt.Sprintf(" %s/%s %6.2f%% %6s", offset, s.total, percent*100, s.durationString()) lenRight := utf8.RuneCountInString(right) var left string lenLeft := 0 if !s.done { lenBar := int(percent * barLength) bar := fmt.Sprintf("[%s%s]", progressColor.Apply(strings.Repeat(" ", lenBar)), strings.Repeat(".", barLength-lenBar)) speed := s.calculateSpeed() left = fmt.Sprintf("%s %s(%*s/s) %s %s", spinnerColor.Apply(string(s.mark.symbol())), bar, speedLength, speed, s.prompt, name) // bar + wrapper(2) + space(1) + speed + "/s"(2) + wrapper(2) = len(bar) + len(speed) + 7 lenLeft = barLength + speedLength + 7 } else { left = fmt.Sprintf("%s %s %s", doneMarkColor.Apply("✓"), s.prompt, name) } // mark(1) + space(1) + prompt + space(1) + name = len(prompt) + len(name) + 3 lenLeft += utf8.RuneCountInString(s.prompt) + utf8.RuneCountInString(name) + 3 lenMargin := width - lenLeft - lenRight if lenMargin < 0 { // hide partial name with one space left left = left[:len(left)+lenMargin-1] + "." lenMargin = 0 } return fmt.Sprintf("%s%s%s", left, strings.Repeat(" ", lenMargin), right), fmt.Sprintf(" └─ %s", s.descriptor.Digest.String()) } // 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.speedWindow.Add(time.Now(), s.offset) return humanize.ToBytes(int64(s.speedWindow.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) } 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() } // Update updates a status. func (s *status) Update(n *status) { s.lock.Lock() defer s.lock.Unlock() if n.offset >= 0 { s.offset = n.offset if n.descriptor.Size != s.descriptor.Size { s.total = humanize.ToBytes(n.descriptor.Size) } s.descriptor = n.descriptor } if n.prompt != "" { s.prompt = n.prompt } if !n.startTime.IsZero() { s.startTime = n.startTime s.speedWindow.Add(s.startTime, 0) } if !n.endTime.IsZero() { s.endTime = n.endTime s.done = true } } oras-1.2.0/cmd/oras/internal/display/status/progress/status_test.go000066400000000000000000000115101462530432700255520ustar00rootroot00000000000000//go:build freebsd || linux || netbsd || openbsd || solaris /* 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" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras/cmd/oras/internal/display/status/console" "oras.land/oras/cmd/oras/internal/display/status/console/testutils" "oras.land/oras/cmd/oras/internal/display/status/progress/humanize" ) func Test_status_String(t *testing.T) { // zero status and progress s := newStatus() if status, digest := s.String(console.MinWidth); status != zeroStatus || digest != zeroDigest { t.Errorf("status.String() = %v, %v, want %v, %v", status, digest, zeroStatus, zeroDigest) } // not done s.Update(&status{ prompt: "test", descriptor: ocispec.Descriptor{ MediaType: "application/vnd.oci.empty.oras.test.v1+json", Size: 2, Digest: "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", }, startTime: time.Now().Add(-time.Minute), offset: 0, total: humanize.ToBytes(2), }) // full name statusStr, digestStr := s.String(120) if err := testutils.OrderedMatch(statusStr+digestStr, "\x1b[0m....................]", s.prompt, s.descriptor.MediaType, "0.00/2 B", "0.00%", s.descriptor.Digest.String()); err != nil { t.Error(err) } // partial name statusStr, digestStr = s.String(console.MinWidth) if err := testutils.OrderedMatch(statusStr+digestStr, "\x1b[0m....................]", s.prompt, "application/v.", "0.00/2 B", "0.00%", s.descriptor.Digest.String()); err != nil { t.Error(err) } // done s.Update(&status{ endTime: time.Now(), offset: s.descriptor.Size, descriptor: s.descriptor, }) statusStr, digestStr = s.String(120) if err := testutils.OrderedMatch(statusStr+digestStr, "✓", s.prompt, s.descriptor.MediaType, "2/2 B", "100.00%", s.descriptor.Digest.String()); err != nil { t.Error(err) } } func Test_status_String_zeroWitdth(t *testing.T) { // zero status and progress s := newStatus() if status, digest := s.String(console.MinWidth); status != zeroStatus || digest != zeroDigest { t.Errorf("status.String() = %v, %v, want %v, %v", status, digest, zeroStatus, zeroDigest) } // not done s.Update(&status{ prompt: "test", descriptor: ocispec.Descriptor{ MediaType: "application/vnd.oci.empty.oras.test.v1+json", Size: 0, Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", }, startTime: time.Now().Add(-time.Minute), offset: 0, total: humanize.ToBytes(0), }) // not done statusStr, digestStr := s.String(120) if err := testutils.OrderedMatch(statusStr+digestStr, "\x1b[0m....................]", s.prompt, s.descriptor.MediaType, "0.00/0 B", "0.00%", s.descriptor.Digest.String()); err != nil { t.Error(err) } // done s.Update(&status{ endTime: time.Now(), offset: s.descriptor.Size, descriptor: s.descriptor, }) statusStr, digestStr = s.String(120) if err := testutils.OrderedMatch(statusStr+digestStr, "✓", s.prompt, s.descriptor.MediaType, "0/0 B", "100.00%", s.descriptor.Digest.String()); err != nil { t.Error(err) } } func Test_status_durationString(t *testing.T) { // zero duration s := newStatus() if d := s.durationString(); d != zeroDuration { t.Errorf("status.durationString() = %v, want %v", d, zeroDuration) } // not ended s.startTime = time.Now().Add(-time.Second) if d := s.durationString(); d == zeroDuration { t.Errorf("status.durationString() = %v, want not %v", d, zeroDuration) } // ended: 61 seconds s.startTime = time.Now() s.endTime = s.startTime.Add(61 * time.Second) want := "1m1s" if d := s.durationString(); d != want { t.Errorf("status.durationString() = %v, want %v", d, want) } // ended: 1001 Microsecond s.startTime = time.Now() s.endTime = s.startTime.Add(1001 * time.Microsecond) want = "1ms" if d := s.durationString(); d != want { t.Errorf("status.durationString() = %v, want %v", d, want) } // ended: 1001 Nanosecond s.startTime = time.Now() s.endTime = s.startTime.Add(1001 * time.Nanosecond) want = "1µs" if d := s.durationString(); d != want { t.Errorf("status.durationString() = %v, want %v", d, want) } } func Test_status_calculateSpeed_negative(t *testing.T) { s := &status{ offset: -1, } if s.calculateSpeed() != humanize.ToBytes(0) { t.Errorf("status.calculateSpeed() = %v, want 0", s.calculateSpeed()) } } oras-1.2.0/cmd/oras/internal/display/status/text.go000066400000000000000000000104541462530432700223160ustar00rootroot00000000000000/* 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" "io" "sync" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2" "oras.land/oras-go/v2/content" ) // TextPushHandler handles text status output for push events. type TextPushHandler struct { verbose bool printer *Printer } // NewTextPushHandler returns a new handler for push command. func NewTextPushHandler(out io.Writer, verbose bool) PushHandler { return &TextPushHandler{ verbose: verbose, printer: NewPrinter(out), } } // OnFileLoading is called when a file is being prepared for upload. func (ph *TextPushHandler) OnFileLoading(name string) error { if !ph.verbose { return nil } return ph.printer.Println("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 } // UpdateCopyOptions adds status update to the copy options. func (ph *TextPushHandler) UpdateCopyOptions(opts *oras.CopyGraphOptions, fetcher content.Fetcher) { const ( promptSkipped = "Skipped " promptUploaded = "Uploaded " promptExists = "Exists " promptUploading = "Uploading" ) committed := &sync.Map{} opts.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) return ph.printer.PrintStatus(desc, promptExists, ph.verbose) } opts.PreCopy = func(ctx context.Context, desc ocispec.Descriptor) error { return ph.printer.PrintStatus(desc, promptUploading, ph.verbose) } opts.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) if err := PrintSuccessorStatus(ctx, desc, fetcher, committed, ph.printer.StatusPrinter(promptSkipped, ph.verbose)); err != nil { return err } return ph.printer.PrintStatus(desc, promptUploaded, ph.verbose) } } // NewTextAttachHandler returns a new handler for attach command. func NewTextAttachHandler(out io.Writer, verbose bool) AttachHandler { return NewTextPushHandler(out, verbose) } // TextPullHandler handles text status output for pull events. type TextPullHandler struct { verbose bool printer *Printer } // TrackTarget implements PullHander. 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, ph.verbose) } // OnNodeDownloaded implements PullHandler. func (ph *TextPullHandler) OnNodeDownloaded(desc ocispec.Descriptor) error { return ph.printer.PrintStatus(desc, PullPromptDownloaded, ph.verbose) } // OnNodeRestored implements PullHandler. func (ph *TextPullHandler) OnNodeRestored(desc ocispec.Descriptor) error { return ph.printer.PrintStatus(desc, PullPromptRestored, ph.verbose) } // OnNodeProcessing implements PullHandler. func (ph *TextPullHandler) OnNodeProcessing(desc ocispec.Descriptor) error { return ph.printer.PrintStatus(desc, PullPromptProcessing, ph.verbose) } // OnNodeProcessing implements PullHandler. func (ph *TextPullHandler) OnNodeSkipped(desc ocispec.Descriptor) error { return ph.printer.PrintStatus(desc, PullPromptSkipped, ph.verbose) } // NewTextPullHandler returns a new handler for pull command. func NewTextPullHandler(out io.Writer, verbose bool) PullHandler { return &TextPullHandler{ verbose: verbose, printer: NewPrinter(out), } } oras-1.2.0/cmd/oras/internal/display/status/track/000077500000000000000000000000001462530432700221035ustar00rootroot00000000000000oras-1.2.0/cmd/oras/internal/display/status/track/reader.go000066400000000000000000000052441462530432700237010ustar00rootroot00000000000000/* 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" "oras.land/oras/cmd/oras/internal/display/status/progress" ) type reader struct { base io.Reader offset int64 actionPrompt string donePrompt string descriptor ocispec.Descriptor manager progress.Manager status progress.Status } // 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) { manager, err := progress.NewManager(tty) if err != nil { return nil, err } return managedReader(r, descriptor, manager, actionPrompt, donePrompt) } func managedReader(r io.Reader, descriptor ocispec.Descriptor, manager progress.Manager, actionPrompt string, donePrompt string) (*reader, error) { ch, err := manager.Add() if err != nil { return nil, err } return &reader{ base: r, descriptor: descriptor, actionPrompt: actionPrompt, donePrompt: donePrompt, manager: manager, status: ch, }, nil } // StopManager stops the status channel and related manager. func (r *reader) StopManager() { r.Close() _ = r.manager.Close() } // Done sends message to mark the tracked progress as complete. func (r *reader) Done() { r.status <- progress.NewStatusMessage(r.donePrompt, r.descriptor, r.descriptor.Size) r.status <- progress.EndTiming() } // Close closes the update channel. func (r *reader) Close() { close(r.status) } // Start sends the start timing to the status channel. func (r *reader) Start() { r.status <- progress.StartTiming() } // Read reads from the underlying reader and updates the progress. func (r *reader) Read(p []byte) (int, error) { n, err := r.base.Read(p) if err != nil && err != io.EOF { return n, err } r.offset = r.offset + int64(n) if err == io.EOF { if r.offset != r.descriptor.Size { return n, io.ErrUnexpectedEOF } } for { select { case r.status <- progress.NewStatusMessage(r.actionPrompt, r.descriptor, r.offset): // purge the channel until successfully pushed return n, err case <-r.status: } } } oras-1.2.0/cmd/oras/internal/display/status/track/target.go000066400000000000000000000064361462530432700237310ustar00rootroot00000000000000/* 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" "io" "os" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2" "oras.land/oras-go/v2/registry" "oras.land/oras/cmd/oras/internal/display/status/progress" ) // GraphTarget is a tracked oras.GraphTarget. type GraphTarget interface { oras.GraphTarget io.Closer Prompt(desc ocispec.Descriptor, prompt string) error } type graphTarget struct { oras.GraphTarget manager progress.Manager actionPrompt string donePrompt string } type referenceGraphTarget struct { *graphTarget } // NewTarget creates a new tracked Target. func NewTarget(t oras.GraphTarget, actionPrompt, donePrompt string, tty *os.File) (GraphTarget, error) { manager, err := progress.NewManager(tty) if err != nil { return nil, err } gt := &graphTarget{ GraphTarget: t, manager: manager, actionPrompt: actionPrompt, donePrompt: donePrompt, } 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 := managedReader(content, expected, t.manager, t.actionPrompt, t.donePrompt) if err != nil { return err } defer r.Close() r.Start() if err := t.GraphTarget.Push(ctx, expected, r); err != nil { return err } r.Done() return nil } // 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 := managedReader(content, expected, rgt.manager, rgt.actionPrompt, rgt.donePrompt) if err != nil { return err } defer r.Close() r.Start() err = rgt.GraphTarget.(registry.ReferencePusher).PushReference(ctx, expected, r, reference) if err != nil { return err } r.Done() return nil } // Close closes the tracking manager. func (t *graphTarget) Close() error { return t.manager.Close() } // Prompt prompts the user with the provided prompt and descriptor. func (t *graphTarget) Prompt(desc ocispec.Descriptor, prompt string) error { status, err := t.manager.Add() if err != nil { return err } defer close(status) status <- progress.NewStatusMessage(prompt, desc, desc.Size) status <- progress.EndTiming() return nil } oras-1.2.0/cmd/oras/internal/display/status/track/target_test.go000066400000000000000000000047231462530432700247650ustar00rootroot00000000000000//go:build freebsd || linux || netbsd || openbsd || solaris /* 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" "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/registry/remote" "oras.land/oras/cmd/oras/internal/display/status/console/testutils" ) type testReferenceGraphTarget struct { oras.GraphTarget } func (t *testReferenceGraphTarget) PushReference(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error { err := t.GraphTarget.Push(ctx, expected, content) if err != nil { return err } return t.GraphTarget.Tag(ctx, expected, reference) } func Test_referenceGraphTarget_PushReference(t *testing.T) { // prepare pty, device, err := testutils.NewPty() if err != nil { t.Fatal(err) } defer 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" actionPrompt := "action" donePrompt := "done" target, err := NewTarget(&testReferenceGraphTarget{src}, actionPrompt, donePrompt, 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(t *testing.T) { target := graphTarget{GraphTarget: &remote.Repository{}} _ = target.Mount(context.Background(), ocispec.Descriptor{}, "", nil) } oras-1.2.0/cmd/oras/internal/display/status/tty.go000066400000000000000000000077731462530432700221640ustar00rootroot00000000000000/* 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" 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" ) // TTYPushHandler handles TTY status output for push command. type TTYPushHandler struct { tty *os.File tracked track.GraphTarget } // NewTTYPushHandler returns a new handler for push status events. func NewTTYPushHandler(tty *os.File) PushHandler { return &TTYPushHandler{ tty: tty, } } // OnFileLoading is called before loading a file. func (ph *TTYPushHandler) OnFileLoading(name 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) { const ( promptUploaded = "Uploaded " promptUploading = "Uploading" ) tracked, err := track.NewTarget(gt, promptUploading, promptUploaded, ph.tty) if err != nil { return nil, nil, err } ph.tracked = tracked return tracked, tracked.Close, nil } // UpdateCopyOptions adds TTY status output to the copy options. func (ph *TTYPushHandler) UpdateCopyOptions(opts *oras.CopyGraphOptions, fetcher content.Fetcher) { const ( promptSkipped = "Skipped " promptExists = "Exists " ) committed := &sync.Map{} opts.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) return ph.tracked.Prompt(desc, promptExists) } opts.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) return PrintSuccessorStatus(ctx, desc, fetcher, committed, func(d ocispec.Descriptor) error { return ph.tracked.Prompt(d, promptSkipped) }) } } // NewTTYAttachHandler returns a new handler for attach status events. func NewTTYAttachHandler(tty *os.File) AttachHandler { return NewTTYPushHandler(tty) } // 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(desc ocispec.Descriptor) error { return nil } // OnNodeDownloaded implements PullHandler. func (ph *TTYPullHandler) OnNodeDownloaded(desc ocispec.Descriptor) error { return nil } // OnNodeProcessing implements PullHandler. func (ph *TTYPullHandler) OnNodeProcessing(desc ocispec.Descriptor) error { return nil } // OnNodeRestored implements PullHandler. func (ph *TTYPullHandler) OnNodeRestored(desc ocispec.Descriptor) error { return ph.tracked.Prompt(desc, PullPromptRestored) } // OnNodeProcessing implements PullHandler. func (ph *TTYPullHandler) OnNodeSkipped(desc ocispec.Descriptor) error { return ph.tracked.Prompt(desc, PullPromptSkipped) } // TrackTarget returns a tracked target. func (ph *TTYPullHandler) TrackTarget(gt oras.GraphTarget) (oras.GraphTarget, StopTrackTargetFunc, error) { tracked, err := track.NewTarget(gt, PullPromptDownloading, PullPromptPulled, ph.tty) if err != nil { return nil, nil, err } ph.tracked = tracked return tracked, tracked.Close, nil } oras-1.2.0/cmd/oras/internal/display/status/tty_test.go000066400000000000000000000114541462530432700232120ustar00rootroot00000000000000//go:build freebsd || linux || netbsd || openbsd || solaris /* 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 ( "bytes" "context" "fmt" "os" "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/cmd/oras/internal/display/status/console/testutils" "oras.land/oras/cmd/oras/internal/display/status/track" ) var ( memStore *memory.Store memDesc ocispec.Descriptor ) func TestMain(m *testing.M) { // memory store for testing memStore = memory.New() content := []byte("test") r := bytes.NewReader(content) memDesc = ocispec.Descriptor{ MediaType: "application/octet-stream", Digest: digest.FromBytes(content), Size: int64(len(content)), } if err := memStore.Push(context.Background(), memDesc, r); err != nil { fmt.Println("Setup failed:", err) os.Exit(1) } if err := memStore.Tag(context.Background(), memDesc, memDesc.Digest.String()); err != nil { fmt.Println("Setup failed:", err) os.Exit(1) } m.Run() } func TestTTYPushHandler_OnFileLoading(t *testing.T) { ph := NewTTYPushHandler(os.Stdout) if ph.OnFileLoading("test") != nil { t.Error("OnFileLoading() should not return an error") } } func TestTTYPushHandler_OnEmptyArtifact(t *testing.T) { ph := NewTTYAttachHandler(os.Stdout) if ph.OnEmptyArtifact() != nil { t.Error("OnEmptyArtifact() should not return an error") } } func TestTTYPushHandler_TrackTarget(t *testing.T) { // prepare pty _, slave, err := testutils.NewPty() if err != nil { t.Fatal(err) } defer slave.Close() ph := NewTTYPushHandler(slave) store := memory.New() // test _, fn, err := ph.TrackTarget(store) if err != nil { t.Error("TrackTarget() should not return an error") } defer func() { if err := fn(); err != nil { t.Fatal(err) } }() if ttyPushHandler, ok := ph.(*TTYPushHandler); !ok { t.Errorf("TrackTarget() should return a *TTYPushHandler, got %T", ttyPushHandler) } } func TestTTYPushHandler_UpdateCopyOptions(t *testing.T) { // prepare pty pty, slave, err := testutils.NewPty() if err != nil { t.Fatal(err) } defer slave.Close() ph := NewTTYPushHandler(slave) gt, _, err := ph.TrackTarget(memory.New()) if err != nil { t.Errorf("TrackTarget() should not return an error: %v", err) } // test opts := oras.CopyGraphOptions{} ph.UpdateCopyOptions(&opts, memStore) if err := oras.CopyGraph(context.Background(), memStore, gt, memDesc, opts); err != nil { t.Errorf("CopyGraph() should not return an error: %v", err) } if err := oras.CopyGraph(context.Background(), memStore, gt, memDesc, opts); err != nil { t.Errorf("CopyGraph() should not return an error: %v", err) } if tracked, ok := gt.(track.GraphTarget); !ok { t.Errorf("TrackTarget() should return a *track.GraphTarget, got %T", tracked) } else { tracked.Close() } // validate if err = testutils.MatchPty(pty, slave, "Exists", memDesc.MediaType, "100.00%", memDesc.Digest.String()); err != nil { t.Fatal(err) } } 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 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 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") } } oras-1.2.0/cmd/oras/internal/display/status/utils.go000066400000000000000000000022121462530432700224630ustar00rootroot00000000000000/* 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 ocispec "github.com/opencontainers/image-spec/specs-go/v1" // GenerateContentKey generates a unique key for each content descriptor, using // its digest and name if applicable. func GenerateContentKey(desc ocispec.Descriptor) string { return desc.Digest.String() + desc.Annotations[ocispec.AnnotationTitle] } // Prompts for pull events. const ( PullPromptDownloading = "Downloading" PullPromptPulled = "Pulled " PullPromptProcessing = "Processing " PullPromptSkipped = "Skipped " PullPromptRestored = "Restored " PullPromptDownloaded = "Downloaded " ) oras-1.2.0/cmd/oras/internal/display/utils/000077500000000000000000000000001462530432700206145ustar00rootroot00000000000000oras-1.2.0/cmd/oras/internal/display/utils/const.go000066400000000000000000000015141462530432700222720ustar00rootroot00000000000000/* 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 utils // Prompt constants for pull. const ( PullPromptDownloading = "Downloading" PullPromptPulled = "Pulled " PullPromptProcessing = "Processing " PullPromptSkipped = "Skipped " PullPromptRestored = "Restored " PullPromptDownloaded = "Downloaded " ) oras-1.2.0/cmd/oras/internal/display/utils/json.go000066400000000000000000000030221462530432700221110ustar00rootroot00000000000000/* 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 utils import ( "bytes" "encoding/json" "fmt" "io" ) // PrintPrettyJSON prints the object to the writer in JSON format. func PrintPrettyJSON(out io.Writer, object any) error { encoder := json.NewEncoder(out) encoder.SetIndent("", " ") return encoder.Encode(object) } // PrintJSON writes the data to the output stream, optionally prettifying it. func PrintJSON(out io.Writer, data []byte, pretty bool) error { if pretty { buf := bytes.NewBuffer(nil) if err := json.Indent(buf, data, "", " "); err != nil { return fmt.Errorf("failed to prettify: %w", err) } buf.WriteByte('\n') data = buf.Bytes() } _, err := out.Write(data) return err } // ToMap converts the data to a map[string]any with json tag as key. func ToMap(data any) (map[string]any, error) { // slow but easy content, err := json.Marshal(data) if err != nil { return nil, err } var ret map[string]any if err = json.Unmarshal(content, &ret); err != nil { return nil, err } return ret, nil } oras-1.2.0/cmd/oras/internal/errors/000077500000000000000000000000001462530432700173235ustar00rootroot00000000000000oras-1.2.0/cmd/oras/internal/errors/errors.go000066400000000000000000000144641462530432700211770ustar00rootroot00000000000000/* 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/registry/remote/auth" "oras.land/oras-go/v2/registry/remote/errcode" ) // 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 { 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 { Modify(cmd *cobra.Command, err error) (modifiedErr error, modified bool) } // 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.Modify(cmd, err) return err } return nil } return cmd } // TrimErrResp tries to trim toTrim from err. func TrimErrResp(err error, toTrim error) error { var inner error if errResp, ok := toTrim.(*errcode.ErrorResponse); ok { if len(errResp.Errors) == 0 { return fmt.Errorf("recognizable error message not found: %w", toTrim) } inner = errResp.Errors } else { return err } return reWrap(err, toTrim, inner) } // 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 { switch x := inner.(type) { case interface{ Unwrap() error }: toTrim = inner inner = x.Unwrap() continue case interface{ Unwrap() []error }: for _, errItem := range x.Unwrap() { if errors.Is(errItem, auth.ErrBasicCredentialNotFound) { toTrim = errItem inner = errItem break } } continue } break } return reWrap(err, toTrim, auth.ErrBasicCredentialNotFound) } // reWrap re-wraps errA to errC and trims out errB, returns errC if scrub fails. // +---------- errA ----------+ // | +---- errB ----+ | +---- errA ----+ // | | errC | | => | errC | // | +--------------+ | +--------------+ // +--------------------------+ func reWrap(errA, errB, errC error) error { // TODO: trim dedicated error type when // https://github.com/oras-project/oras-go/issues/677 is done contentA := errA.Error() contentB := errB.Error() if idx := strings.Index(contentA, contentB); idx > 0 { return fmt.Errorf("%s%w", contentA[:idx], errC) } return errC } // 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{ 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.2.0/cmd/oras/internal/errors/errors_test.go000066400000000000000000000050341462530432700222270ustar00rootroot00000000000000/* 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 ( "testing" "github.com/spf13/pflag" ) 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) } }) } } oras-1.2.0/cmd/oras/internal/fileref/000077500000000000000000000000001462530432700174235ustar00rootroot00000000000000oras-1.2.0/cmd/oras/internal/fileref/unix.go000066400000000000000000000020361462530432700207360ustar00rootroot00000000000000//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.2.0/cmd/oras/internal/fileref/unix_test.go000066400000000000000000000055541462530432700220050ustar00rootroot00000000000000//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.2.0/cmd/oras/internal/fileref/windows.go000066400000000000000000000030751462530432700214510ustar00rootroot00000000000000//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.2.0/cmd/oras/internal/fileref/windows_test.go000066400000000000000000000112721462530432700225060ustar00rootroot00000000000000//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.2.0/cmd/oras/internal/manifest/000077500000000000000000000000001462530432700176155ustar00rootroot00000000000000oras-1.2.0/cmd/oras/internal/manifest/manifest.go000066400000000000000000000023721462530432700217560ustar00rootroot00000000000000/* 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.2.0/cmd/oras/internal/manifest/manifest_test.go000066400000000000000000000042231462530432700230120ustar00rootroot00000000000000/* 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.2.0/cmd/oras/internal/option/000077500000000000000000000000001462530432700173175ustar00rootroot00000000000000oras-1.2.0/cmd/oras/internal/option/applier.go000066400000000000000000000020731462530432700213040ustar00rootroot00000000000000/* 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 interface{}, target *pflag.FlagSet) { _ = rangeFields(optsPtr, func(fa FlagApplier) error { fa.ApplyFlags(target) return nil }) } oras-1.2.0/cmd/oras/internal/option/applier_test.go000066400000000000000000000022071462530432700223420ustar00rootroot00000000000000/* 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(fs *pflag.FlagSet) { *t.CntPtr += 1 } 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.2.0/cmd/oras/internal/option/cache.go000066400000000000000000000020771462530432700207170ustar00rootroot00000000000000/* 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.2.0/cmd/oras/internal/option/cache_test.go000066400000000000000000000031421462530432700217500ustar00rootroot00000000000000/* 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" "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() os.Setenv("ORAS_CACHE", tempDir) defer os.Unsetenv("ORAS_CACHE") 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) { os.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.2.0/cmd/oras/internal/option/common.go000066400000000000000000000035111462530432700211360ustar00rootroot00000000000000/* 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" "github.com/spf13/cobra" "github.com/spf13/pflag" "golang.org/x/term" ) const NoTTYFlag = "no-tty" // Common option struct. type Common struct { Debug bool Verbose bool TTY *os.File noTTY 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)") fs.BoolVarP(&opts.Verbose, "verbose", "v", false, "verbose output") fs.BoolVarP(&opts.noTTY, NoTTYFlag, "", false, "[Preview] do not show progress output") } // Parse gets target options from user input. func (opts *Common) Parse(*cobra.Command) error { // use STDERR as TTY output since STDOUT is reserved for pipeable output return opts.parseTTY(os.Stderr) } // parseTTY gets target options from user input. func (opts *Common) parseTTY(f *os.File) error { if !opts.noTTY { if opts.Debug { opts.noTTY = true } else if term.IsTerminal(int(f.Fd())) { opts.TTY = f } } return nil } // UpdateTTY updates the TTY value, given the status of --no-tty flag and output // path value. func (opts *Common) UpdateTTY(flagPresent bool, toSTDOUT bool) { ttyEnforced := flagPresent && !opts.noTTY if opts.noTTY || (toSTDOUT && !ttyEnforced) { opts.TTY = nil } } oras-1.2.0/cmd/oras/internal/option/common_test.go000066400000000000000000000034401462530432700221760ustar00rootroot00000000000000/* 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" "reflect" "testing" "github.com/spf13/pflag" ) func TestCommon_FlagsInit(t *testing.T) { var test struct { Common } ApplyFlags(&test, pflag.NewFlagSet("oras-test", pflag.ExitOnError)) } func TestCommon_UpdateTTY(t *testing.T) { testTTY := &os.File{} tests := []struct { name string flagPresent bool toSTDOUT bool noTTY bool expectedTTY *os.File }{ { "output to STDOUT, --no-tty flag not used, reset TTY", false, true, false, nil, }, { "output to STDOUT, --no-tty set to true, reset TTY", true, true, true, nil, }, { "output to STDOUT, --no-tty set to false", true, true, false, testTTY, }, { "not output to STDOUT, --no-tty flag not used", false, false, false, testTTY, }, { "not output to STDOUT, --no-tty set to true, reset TTY", true, false, true, nil, }, { "not output to STDOUT, --no-tty set to false", true, false, false, testTTY, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { opts := &Common{ noTTY: tt.noTTY, TTY: testTTY, } opts.UpdateTTY(tt.flagPresent, tt.toSTDOUT) if !reflect.DeepEqual(opts.TTY, tt.expectedTTY) { t.Fatalf("tt.TTY got %v, want %v", opts.TTY, tt.expectedTTY) } }) } } oras-1.2.0/cmd/oras/internal/option/common_unix_test.go000066400000000000000000000022761462530432700232470ustar00rootroot00000000000000//go:build freebsd || linux || netbsd || openbsd || solaris /* 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 ( "testing" "oras.land/oras/cmd/oras/internal/display/status/console/testutils" ) func TestCommon_parseTTY(t *testing.T) { _, device, err := testutils.NewPty() if err != nil { t.Fatal(err) } defer device.Close() var opts Common // TTY output if err := opts.parseTTY(device); err != nil { t.Errorf("unexpected error with TTY output: %v", err) } // --debug opts.Debug = true if err := opts.parseTTY(device); err != nil { t.Errorf("unexpected error with --debug: %v", err) } if !opts.noTTY { t.Errorf("expected --no-tty to be true with --debug") } } oras-1.2.0/cmd/oras/internal/option/confirmation.go000066400000000000000000000027701462530432700223440ustar00rootroot00000000000000/* 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.2.0/cmd/oras/internal/option/confirmation_test.go000066400000000000000000000036561462530432700234070ustar00rootroot00000000000000/* 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.Confirmation.Force != false { t.Fatalf("expecting Confirmed to be false but got: %v", test.Confirmation.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.2.0/cmd/oras/internal/option/descriptor.go000066400000000000000000000023171462530432700220270ustar00rootroot00000000000000/* 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.2.0/cmd/oras/internal/option/descriptor_test.go000066400000000000000000000031401462530432700230610ustar00rootroot00000000000000/* 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.Descriptor.OutputDescriptor != false { t.Fatalf("expecting OutputDescriptor to be false but got: %v", test.Descriptor.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.2.0/cmd/oras/internal/option/format.go000066400000000000000000000074251462530432700211460ustar00rootroot00000000000000/* 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, } FormatTypeTable = &FormatType{ Name: "table", Usage: "Get direct referrers and output in table format", } FormatTypeTree = &FormatType{ Name: "tree", Usage: "Get referrers recursively and print in tree format", } ) // Format contains input and parsed options for formatted output flags. type Format struct { FormatFlag string Type string Template string AllowedTypes []*FormatType } // ApplyFlag implements FlagProvider.ApplyFlag. func (opts *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 opts.AllowedTypes { _, _ = fmt.Fprintf(w, "\n'%s':\t%s", t.Name, t.Usage) } w.Flush() // apply flags fs.StringVar(&opts.FormatFlag, "format", opts.FormatFlag, buf.String()) fs.StringVar(&opts.Template, "template", "", "[Experimental] Template string used to format output") } // Parse parses the input format flag. func (opts *Format) Parse(_ *cobra.Command) error { if err := opts.parseFlag(); err != nil { return err } if opts.Type == "" { // flag not specified return nil } if opts.Type == FormatTypeGoTemplate.Name && opts.Template == "" { return &oerrors.Error{ Err: fmt.Errorf("%q format specified but no template given", opts.Type), Recommendation: fmt.Sprintf("use `--format %s=TEMPLATE` to specify the template", opts.Type), } } var optionalTypes []string for _, t := range opts.AllowedTypes { if opts.Type == t.Name { // type validation passed return nil } optionalTypes = append(optionalTypes, t.Name) } return &oerrors.Error{ Err: fmt.Errorf("invalid format type: %q", opts.Type), Recommendation: fmt.Sprintf("supported types: %s", strings.Join(optionalTypes, ", ")), } } func (opts *Format) parseFlag() error { opts.Type = opts.FormatFlag if opts.Template != "" { // template explicitly set if opts.Type != FormatTypeGoTemplate.Name { return fmt.Errorf("--template must be used with --format %s", FormatTypeGoTemplate.Name) } return nil } for _, t := range opts.AllowedTypes { if !t.HasParams { continue } prefix := t.Name + "=" if strings.HasPrefix(opts.FormatFlag, prefix) { // parse type and add parameter to template opts.Type = t.Name opts.Template = opts.FormatFlag[len(prefix):] } } return nil } oras-1.2.0/cmd/oras/internal/option/packer.go000066400000000000000000000114451462530432700211200ustar00rootroot00000000000000/* 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") errAnnotationFormat = errors.New("annotation value doesn't match the required format") errAnnotationDuplication = errors.New("duplicate annotation key") 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 { ManifestExportPath string PathValidationDisabled bool AnnotationFilePath string ManifestAnnotations []string FileRefs []string } // ApplyFlags applies flags to a command flag set. func (opts *Packer) ApplyFlags(fs *pflag.FlagSet) { fs.StringVarP(&opts.ManifestExportPath, "export-manifest", "", "", "`path` of the pushed manifest") fs.StringArrayVarP(&opts.ManifestAnnotations, "annotation", "a", nil, "manifest annotations") 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(*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 nil } // LoadManifestAnnotations loads the manifest annotation map. func (opts *Packer) LoadManifestAnnotations() (annotations map[string]map[string]string, err error) { if opts.AnnotationFilePath != "" && len(opts.ManifestAnnotations) != 0 { return nil, errAnnotationConflict } if opts.AnnotationFilePath != "" { if err = decodeJSON(opts.AnnotationFilePath, &annotations); err != nil { return nil, &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 { annotations = make(map[string]map[string]string) if err = parseAnnotationFlags(opts.ManifestAnnotations, annotations); err != nil { return nil, err } } return } // decodeJSON decodes a json file v to filename. func decodeJSON(filename string, v interface{}) error { file, err := os.Open(filename) if err != nil { return err } defer file.Close() return json.NewDecoder(file).Decode(v) } // parseAnnotationFlags parses annotation flags into a map. func parseAnnotationFlags(flags []string, annotations map[string]map[string]string) error { manifestAnnotations := make(map[string]string) for _, anno := range flags { 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 } annotations[AnnotationManifest] = manifestAnnotations return nil } oras-1.2.0/cmd/oras/internal/option/packer_test.go000066400000000000000000000074211462530432700221560ustar00rootroot00000000000000/* 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(t *testing.T) { var test struct { Packer } ApplyFlags(&test, pflag.NewFlagSet("oras-test", pflag.ExitOnError)) } func TestPacker_LoadManifestAnnotations_err(t *testing.T) { opts := Packer{ AnnotationFilePath: "this is not a file", // testFile, ManifestAnnotations: []string{"Key=Val"}, } if _, err := opts.LoadManifestAnnotations(); !errors.Is(err, errAnnotationConflict) { t.Fatalf("unexpected error: %v", err) } opts = Packer{ AnnotationFilePath: "this is not a file", // testFile, } if _, err := opts.LoadManifestAnnotations(); err == nil { t.Fatalf("unexpected error: %v", err) } opts = Packer{ ManifestAnnotations: []string{"KeyVal"}, } if _, err := opts.LoadManifestAnnotations(); !errors.Is(err, errAnnotationFormat) { t.Fatalf("unexpected error: %v", err) } opts = Packer{ ManifestAnnotations: []string{"Key=Val1", "Key=Val2"}, } if _, err := opts.LoadManifestAnnotations(); !errors.Is(err, errAnnotationDuplication) { t.Fatalf("unexpected error: %v", err) } } func TestPacker_LoadManifestAnnotations_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} anno, err := opts.LoadManifestAnnotations() if err != nil { t.Fatalf("unexpected error: %v", err) } if !reflect.DeepEqual(anno, expectedResult) { t.Fatalf("unexpected error: %v", anno) } } func TestPacker_LoadManifestAnnotations_annotationFlag(t *testing.T) { // Item do not contains '=' invalidFlag0 := []string{ "Key", } var annotations map[string]map[string]string opts := Packer{ManifestAnnotations: invalidFlag0} _, err := opts.LoadManifestAnnotations() if !errors.Is(err, errAnnotationFormat) { t.Fatalf("unexpected error: %v", err) } // Duplication Key invalidFlag1 := []string{ "Key=0", "Key=1", } opts = Packer{ManifestAnnotations: invalidFlag1} _, err = opts.LoadManifestAnnotations() 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{ManifestAnnotations: validFlag} annotations, err = opts.LoadManifestAnnotations() if err != nil { t.Fatalf("unexpected error: %v", err) } if _, ok := annotations["$manifest"]; !ok { t.Fatalf("unexpected error: failed when looking for '$manifest' in annotations") } if !reflect.DeepEqual(annotations, map[string]map[string]string{ "$manifest": { "Key0": "", "Key1": "Val", "Key2": "${env:USERNAME}", }, }) { t.Fatalf("unexpected error: %v", errors.New("content not match")) } } oras-1.2.0/cmd/oras/internal/option/parser.go000066400000000000000000000025641462530432700211510ustar00rootroot00000000000000/* 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" "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 interface{}) error { return rangeFields(optsPtr, func(fp FlagParser) error { return fp.Parse(cmd) }) } // rangeFields goes through all fields of ptr, optionally run fn if a field is // public AND typed T. func rangeFields[T any](ptr any, fn func(T) error) error { v := reflect.ValueOf(ptr).Elem() for i := 0; i < v.NumField(); i++ { f := v.Field(i) if f.CanSet() { iface := f.Addr().Interface() if opts, ok := iface.(T); ok { if err := fn(opts); err != nil { return err } } } } return nil } oras-1.2.0/cmd/oras/internal/option/parser_test.go000066400000000000000000000037721462530432700222120ustar00rootroot00000000000000/* 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(cmd *cobra.Command) error { *t.CntPtr += 1 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.2.0/cmd/oras/internal/option/platform.go000066400000000000000000000040271462530432700214750ustar00rootroot00000000000000/* 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 } oras-1.2.0/cmd/oras/internal/option/platform_test.go000066400000000000000000000056011462530432700225330ustar00rootroot00000000000000/* 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.platform != "" { t.Fatalf("expecting platform to be empty but got: %v", test.Platform.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.2.0/cmd/oras/internal/option/pretty.go000066400000000000000000000022001462530432700211670ustar00rootroot00000000000000/* 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/display/utils" ) // 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 utils.PrintJSON(w, content, opts.Pretty) } oras-1.2.0/cmd/oras/internal/option/pretty_test.go000066400000000000000000000052051462530432700222360ustar00rootroot00000000000000/* 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 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.2.0/cmd/oras/internal/option/remote.go000066400000000000000000000347011462530432700211460ustar00rootroot00000000000000/* 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 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 (opts *Remote) EnableDistributionSpecFlag() { opts.applyDistributionSpec = true } // ApplyFlags applies flags to a command flag set. func (opts *Remote) ApplyFlags(fs *pflag.FlagSet) { opts.ApplyFlagsWithPrefix(fs, "", "") fs.BoolVar(&opts.secretFromStdin, passwordFromStdinFlag, false, "read password from stdin") fs.BoolVar(&opts.secretFromStdin, identityTokenFromStdinFlag, false, "read identity token from stdin") } func applyPrefix(prefix, description string) (flagPrefix, notePrefix string) { if prefix == "" { return "", "" } return prefix + "-", description + " " } // ApplyFlagsWithPrefix applies flags to a command flag set with a prefix string. // Commonly used for non-unary remote targets. func (opts *Remote) ApplyFlagsWithPrefix(fs *pflag.FlagSet, prefix, description string) { var ( shortUser string shortPassword string shortHeader string notePrefix string ) if prefix == "" { shortUser, shortPassword = "u", "p" shortHeader = "H" } opts.flagPrefix, notePrefix = applyPrefix(prefix, description) if opts.applyDistributionSpec { opts.DistributionSpec.ApplyFlagsWithPrefix(fs, prefix, description) } fs.StringVarP(&opts.Username, opts.flagPrefix+usernameFlag, shortUser, "", notePrefix+"registry username") fs.StringVarP(&opts.Secret, opts.flagPrefix+passwordFlag, shortPassword, "", notePrefix+"registry password or identity token") fs.StringVar(&opts.Secret, opts.flagPrefix+identityTokenFlag, "", notePrefix+"registry identity token") fs.BoolVar(&opts.Insecure, opts.flagPrefix+"insecure", false, "allow connections to "+notePrefix+"SSL registry without certs") plainHTTPFlagName := opts.flagPrefix + "plain-http" plainHTTP := fs.Bool(plainHTTPFlagName, false, "allow insecure connections to "+notePrefix+"registry without SSL check") opts.plainHTTP = func() (bool, bool) { return *plainHTTP, fs.Changed(plainHTTPFlagName) } fs.StringVar(&opts.CACertFilePath, opts.flagPrefix+caFileFlag, "", "server certificate authority file for the remote "+notePrefix+"registry") fs.StringVarP(&opts.CertFilePath, opts.flagPrefix+certFileFlag, "", "", "client certificate file for the remote "+notePrefix+"registry") fs.StringVarP(&opts.KeyFilePath, opts.flagPrefix+keyFileFlag, "", "", "client private key file for the remote "+notePrefix+"registry") fs.StringArrayVar(&opts.resolveFlag, opts.flagPrefix+"resolve", nil, "customized DNS for "+notePrefix+"registry, formatted in `host:port:address[:address_port]`") fs.StringArrayVar(&opts.Configs, opts.flagPrefix+"registry-config", nil, "`path` of the authentication file for "+notePrefix+"registry") fs.StringArrayVarP(&opts.headerFlags, opts.flagPrefix+"header", shortHeader, nil, "add custom headers to "+notePrefix+"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 (opts *Remote) Parse(cmd *cobra.Command) error { usernameAndIdTokenFlags := []string{opts.flagPrefix + usernameFlag, opts.flagPrefix + identityTokenFlag} passwordAndIdTokenFlags := []string{opts.flagPrefix + passwordFlag, opts.flagPrefix + identityTokenFlag} certFileAndKeyFileFlags := []string{opts.flagPrefix + certFileFlag, opts.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 := opts.parseCustomHeaders(); err != nil { return err } if err := oerrors.CheckRequiredTogetherFlags(cmd.Flags(), certFileAndKeyFileFlags...); err != nil { return err } return opts.readSecret(cmd) } // readSecret tries to read password or identity token with // optional cmd prompt. func (opts *Remote) readSecret(cmd *cobra.Command) (err error) { if cmd.Flags().Changed(identityTokenFlag) { fmt.Fprintln(os.Stderr, "WARNING! Using --identity-token via the CLI is insecure. Use --identity-token-stdin.") } else if cmd.Flags().Changed(passwordFlag) { fmt.Fprintln(os.Stderr, "WARNING! Using --password via the CLI is insecure. Use --password-stdin.") } else if opts.secretFromStdin { // Prompt for credential secret, err := io.ReadAll(os.Stdin) if err != nil { return err } opts.Secret = strings.TrimSuffix(string(secret), "\n") opts.Secret = strings.TrimSuffix(opts.Secret, "\r") } return nil } // parseResolve parses resolve flag. func (opts *Remote) parseResolve(baseDial onet.DialFunc) (onet.DialFunc, error) { if len(opts.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 opts.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 (opts *Remote) tlsConfig() (*tls.Config, error) { config := &tls.Config{ InsecureSkipVerify: opts.Insecure, } if opts.CACertFilePath != "" { var err error config.RootCAs, err = crypto.LoadCertPool(opts.CACertFilePath) if err != nil { return nil, err } } if opts.CertFilePath != "" && opts.KeyFilePath != "" { cert, err := tls.LoadX509KeyPair(opts.CertFilePath, opts.KeyFilePath) if err != nil { return nil, err } config.Certificates = []tls.Certificate{cert} } return config, nil } // authClient assembles a oras auth client. func (opts *Remote) authClient(registry string, debug bool) (client *auth.Client, err error) { config, err := opts.tlsConfig() if err != nil { return nil, err } baseTransport := http.DefaultTransport.(*http.Transport).Clone() baseTransport.TLSClientConfig = config dialContext, err := opts.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: opts.headers, } client.SetUserAgent("oras/" + version.GetVersion()) if debug { client.Client.Transport = trace.NewTransport(client.Client.Transport) } cred := opts.Credential() if cred != auth.EmptyCredential { client.Credential = func(ctx context.Context, s string) (auth.Credential, error) { return cred, nil } } else { var err error opts.store, err = credential.NewStore(opts.Configs...) if err != nil { return nil, err } client.Credential = credentials.Credential(opts.store) } return } // ConfigPath returns the config path of the credential store. func (opts *Remote) ConfigPath() (string, error) { if opts.store == nil { return "", errors.New("no credential store initialized") } if ds, ok := opts.store.(*credentials.DynamicStore); ok { return ds.ConfigPath(), nil } return "", errors.New("store doesn't support getting config path") } func (opts *Remote) parseCustomHeaders() error { if len(opts.headerFlags) != 0 { headers := map[string][]string{} for _, h := range opts.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) } opts.headers = headers } return nil } // Credential returns a credential based on the remote options. func (opts *Remote) Credential() auth.Credential { return credential.Credential(opts.Username, opts.Secret) } func (opts *Remote) handleWarning(registry string, logger logrus.FieldLogger) func(warning remote.Warning) { if opts.warned == nil { opts.warned = make(map[string]*sync.Map) } warned := opts.warned[registry] if warned == nil { warned = &sync.Map{} opts.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 (opts *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 = opts.isPlainHttp(registry) reg.HandleWarning = opts.handleWarning(registry, logger) if reg.Client, err = opts.authClient(registry, common.Debug); err != nil { return nil, err } return } // NewRepository assembles a oras remote repository. func (opts *Remote) NewRepository(reference string, common Common, logger logrus.FieldLogger) (repo *remote.Repository, err error) { repo, err = remote.NewRepository(reference) if err != nil { if errors.Unwrap(err) == errdef.ErrInvalidReference { return nil, fmt.Errorf("%q: %v", reference, err) } return nil, err } registry := repo.Reference.Registry repo.PlainHTTP = opts.isPlainHttp(registry) repo.HandleWarning = opts.handleWarning(registry, logger) if repo.Client, err = opts.authClient(registry, common.Debug); err != nil { return nil, err } repo.SkipReferrersGC = true if opts.ReferrersAPI != nil { if err := repo.SetReferrersCapability(*opts.ReferrersAPI); err != nil { return nil, err } } return } // isPlainHttp returns the plain http flag for a given registry. func (opts *Remote) isPlainHttp(registry string) bool { plainHTTP, enforced := opts.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 } // Modify modifies error during cmd execution. func (opts *Remote) Modify(cmd *cobra.Command, err error) (error, bool) { var errResp *errcode.ErrorResponse if errors.Is(err, auth.ErrBasicCredentialNotFound) { return opts.DecorateCredentialError(err), true } if errors.As(err, &errResp) { cmd.SetErrPrefix(oerrors.RegistryErrorPrefix) return &oerrors.Error{ Err: oerrors.TrimErrResp(err, errResp), }, true } return err, false } // DecorateCredentialError decorate error with recommendation. func (opts *Remote) DecorateCredentialError(err error) *oerrors.Error { configPath := " " if path, pathErr := opts.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.2.0/cmd/oras/internal/option/remote_test.go000066400000000000000000000401131462530432700221770ustar00rootroot00000000000000/* 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() m.Run() } func TestRemote_FlagsInit(t *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) } _, err = client.Do(req) if err != nil { t.Fatalf("unexpected error: %v", err) } } 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) } _, err = client.Do(req) if err != nil { t.Fatalf("unexpected error: %v", err) } } 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) } _, err = client.Do(req) if err != nil { t.Fatalf("unexpected error when sending request: %v", err) } } 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, r *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.2.0/cmd/oras/internal/option/spec.go000066400000000000000000000101651462530432700206030ustar00rootroot00000000000000/* 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 ( 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)`) } // 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 *bool // specFlag should be provided in form of`--