pax_global_header 0000666 0000000 0000000 00000000064 14576745303 0014530 g ustar 00root root 0000000 0000000 52 comment=9b6f32158776a699b23edb3db86d053623619b60
oras-go-2.5.0/ 0000775 0000000 0000000 00000000000 14576745303 0013103 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/.github/ 0000775 0000000 0000000 00000000000 14576745303 0014443 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/.github/.codecov.yml 0000664 0000000 0000000 00000001233 14576745303 0016665 0 ustar 00root root 0000000 0000000 # Copyright The ORAS Authors.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT 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
oras-go-2.5.0/.github/dependabot.yml 0000664 0000000 0000000 00000001641 14576745303 0017275 0 ustar 00root root 0000000 0000000 # Copyright The ORAS Authors.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT 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: "github-actions"
directory: "/"
schedule:
interval: "daily"
# update go dependencies for v1 branch
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"
target-branch: "v1"
oras-go-2.5.0/.github/licenserc.yml 0000664 0000000 0000000 00000002610 14576745303 0017134 0 ustar 00root root 0000000 0000000 # Copyright The ORAS Authors.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT 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'
- 'go.mod'
- 'go.sum'
- '**/testdata/**'
comment: on-failure
dependency:
files:
- go.mod
oras-go-2.5.0/.github/workflows/ 0000775 0000000 0000000 00000000000 14576745303 0016500 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/.github/workflows/build.yml 0000664 0000000 0000000 00000002306 14576745303 0020323 0 ustar 00root root 0000000 0000000 # Copyright The ORAS Authors.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT 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.21', '1.22']
fail-fast: true
steps:
- name: Checkout
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: Run unit tests
run: make test
- name: Upload coverage to codecov.io
uses: codecov/codecov-action@v3
oras-go-2.5.0/.github/workflows/codeql-analysis.yml 0000664 0000000 0000000 00000002640 14576745303 0022315 0 ustar 00root root 0000000 0000000 # Copyright The ORAS Authors.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT 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: '34 13 * * 3'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
matrix:
go-version: ['1.21', '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-go-2.5.0/.github/workflows/license-checker.yml 0000664 0000000 0000000 00000002256 14576745303 0022254 0 ustar 00root root 0000000 0000000 # Copyright The ORAS Authors.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT 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.5.0
with:
mode: check
config: .github/licenserc.yml
- name: Check dependencies license
uses: apache/skywalking-eyes/dependency@v0.5.0
with:
config: .github/licenserc.yml
oras-go-2.5.0/.gitignore 0000664 0000000 0000000 00000001550 14576745303 0015074 0 ustar 00root root 0000000 0000000 # Copyright The ORAS Authors.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT 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
# Custom
coverage.txt
bin/
dist/
*.tar.gz
vendor/
_dist/
.cover
oras-go-2.5.0/CODEOWNERS 0000664 0000000 0000000 00000000113 14576745303 0014471 0 ustar 00root root 0000000 0000000 # Derived from OWNERS.md
* @sajayantony @shizhMSFT @stevelasker @Wwwsylvia
oras-go-2.5.0/CODE_OF_CONDUCT.md 0000664 0000000 0000000 00000000231 14576745303 0015676 0 ustar 00root root 0000000 0000000 # 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-go-2.5.0/LICENSE 0000664 0000000 0000000 00000026117 14576745303 0014117 0 ustar 00root root 0000000 0000000 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-go-2.5.0/MIGRATION_GUIDE.md 0000664 0000000 0000000 00000003324 14576745303 0015675 0 ustar 00root root 0000000 0000000 # Migration Guide
In version `v2`, ORAS Go library has been completely refreshed with:
- More unified interfaces
- Notably fewer dependencies
- Higher test coverage
- Better documentation
**Besides, ORAS Go `v2` is now a registry client.**
## Major Changes in `v2`
- Moves `content.FileStore` to [file.Store](https://pkg.go.dev/oras.land/oras-go/v2/content/file#Store)
- Moves `content.OCIStore` to [oci.Store](https://pkg.go.dev/oras.land/oras-go/v2/content/oci#Store)
- Moves `content.MemoryStore` to [memory.Store](https://pkg.go.dev/oras.land/oras-go/v2/content/memory#Store)
- Provides [SDK](https://pkg.go.dev/oras.land/oras-go/v2/registry/remote) to interact with OCI-compliant and Docker-compliant registries
- Supports [Copy](https://pkg.go.dev/oras.land/oras-go/v2#Copy) with more flexible options
- Supports [Extended Copy](https://pkg.go.dev/oras.land/oras-go/v2#ExtendedCopy) with options *(experimental)*
- No longer supports `docker.Login` and `docker.Logout` (removes the dependency on `docker`); instead, provides authentication through [auth.Client](https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/auth#Client)
Documentation and examples are available at [pkg.go.dev](https://pkg.go.dev/oras.land/oras-go/v2).
## Migrating from `v1` to `v2`
1. Get the `v2` package
```sh
go get oras.land/oras-go/v2
```
2. Import and use the `v2` package
```go
import "oras.land/oras-go/v2"
```
3. Run
```sh
go mod tidy
```
Since breaking changes are introduced in `v2`, code refactoring is required for migrating from `v1` to `v2`.
The migration can be done in an iterative fashion, as `v1` and `v2` can be imported and used at the same time.
oras-go-2.5.0/Makefile 0000664 0000000 0000000 00000002344 14576745303 0014546 0 ustar 00root root 0000000 0000000 # Copyright The ORAS Authors.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
.PHONY: test
test: vendor check-encoding
go test -race -v -coverprofile=coverage.txt -covermode=atomic ./...
.PHONY: covhtml
covhtml:
open .cover/coverage.html
.PHONY: clean
clean:
git status --ignored --short | grep '^!! ' | sed 's/!! //' | xargs rm -rf
.PHONY: check-encoding
check-encoding:
! find . -not -path "./vendor/*" -name "*.go" -type f -exec file "{}" ";" | grep CRLF
! find scripts -name "*.sh" -type f -exec file "{}" ";" | grep CRLF
.PHONY: fix-encoding
fix-encoding:
find . -not -path "./vendor/*" -name "*.go" -type f -exec sed -i -e "s/\r//g" {} +
find scripts -name "*.sh" -type f -exec sed -i -e "s/\r//g" {} +
.PHONY: vendor
vendor:
go mod vendor
oras-go-2.5.0/OWNERS.md 0000664 0000000 0000000 00000000321 14576745303 0014436 0 ustar 00root root 0000000 0000000 # Owners
Owners:
- Sajay Antony (@sajayantony)
- Shiwei Zhang (@shizhMSFT)
- Steve Lasker (@stevelasker)
- Sylvia Lei (@Wwwsylvia)
Emeritus:
- Avi Deitcher (@deitch)
- Josh Dolitsky (@jdolitsky)
oras-go-2.5.0/README.md 0000664 0000000 0000000 00000006730 14576745303 0014370 0 ustar 00root root 0000000 0000000 # ORAS Go library
## Project status
### Versioning
The ORAS Go library follows [Semantic Versioning](https://semver.org/), where breaking changes are reserved for MAJOR releases, and MINOR and PATCH releases must be 100% backwards compatible.
### v2: stable
[](https://github.com/oras-project/oras-go/actions/workflows/build.yml?query=workflow%3Abuild+event%3Apush+branch%3Amain)
[](https://codecov.io/gh/oras-project/oras-go)
[](https://goreportcard.com/report/oras.land/oras-go/v2)
[](https://pkg.go.dev/oras.land/oras-go/v2)
The version `2` is actively developed in the [`main`](https://github.com/oras-project/oras-go/tree/main) branch with all new features.
> [!Note]
> The `main` branch follows [Go's Security Policy](https://github.com/golang/go/security/policy) and supports the two latest versions of Go (currently `1.21` and `1.22`).
Examples for common use cases can be found below:
- [Copy examples](https://pkg.go.dev/oras.land/oras-go/v2#pkg-examples)
- [Registry interaction examples](https://pkg.go.dev/oras.land/oras-go/v2/registry#pkg-examples)
- [Repository interaction examples](https://pkg.go.dev/oras.land/oras-go/v2/registry/remote#pkg-examples)
- [Authentication examples](https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/auth#pkg-examples)
If you are seeking latest changes, you should use the [`main`](https://github.com/oras-project/oras-go/tree/main) branch (or a specific commit hash) over a tagged version when including the ORAS Go library in your project's `go.mod`.
The Go Reference for the `main` branch is available [here](https://pkg.go.dev/oras.land/oras-go/v2@main).
To migrate from `v1` to `v2`, see [MIGRATION_GUIDE.md](./MIGRATION_GUIDE.md).
### v1: stable
[](https://github.com/oras-project/oras-go/actions/workflows/build.yml?query=workflow%3Abuild+event%3Apush+branch%3Av1)
[](https://goreportcard.com/report/oras.land/oras-go)
[](https://pkg.go.dev/oras.land/oras-go)
As there are various stable projects depending on the ORAS Go library `v1`, the
[`v1`](https://github.com/oras-project/oras-go/tree/v1) branch
is maintained for API stability, dependency updates, and security patches.
All `v1.*` releases are based upon this branch.
Since `v1` is in a maintenance state, you are highly encouraged
to use releases with major version `2` for new features.
## Docs
- [oras.land/client_libraries/go](https://oras.land/docs/Client_Libraries/go): Documentation for the ORAS Go library
- [Reviewing guide](https://github.com/oras-project/community/blob/main/REVIEWING.md): All reviewers must read the reviewing guide and agree to follow the project review guidelines.
## 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-go-2.5.0/SECURITY.md 0000664 0000000 0000000 00000000244 14576745303 0014674 0 ustar 00root root 0000000 0000000 # Security Policy
Please follow the [security policy](https://oras.land/docs/community/reporting_security_concerns) to report a security vulnerability or concern.
oras-go-2.5.0/content.go 0000664 0000000 0000000 00000033014 14576745303 0015105 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 oras
import (
"bytes"
"context"
"errors"
"fmt"
"io"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/internal/cas"
"oras.land/oras-go/v2/internal/docker"
"oras.land/oras-go/v2/internal/interfaces"
"oras.land/oras-go/v2/internal/platform"
"oras.land/oras-go/v2/internal/syncutil"
"oras.land/oras-go/v2/registry"
"oras.land/oras-go/v2/registry/remote/auth"
)
const (
// defaultTagConcurrency is the default concurrency of tagging.
defaultTagConcurrency int = 5 // This value is consistent with dockerd
// defaultTagNMaxMetadataBytes is the default value of
// TagNOptions.MaxMetadataBytes.
defaultTagNMaxMetadataBytes int64 = 4 * 1024 * 1024 // 4 MiB
// defaultResolveMaxMetadataBytes is the default value of
// ResolveOptions.MaxMetadataBytes.
defaultResolveMaxMetadataBytes int64 = 4 * 1024 * 1024 // 4 MiB
// defaultMaxBytes is the default value of FetchBytesOptions.MaxBytes.
defaultMaxBytes int64 = 4 * 1024 * 1024 // 4 MiB
)
// DefaultTagNOptions provides the default TagNOptions.
var DefaultTagNOptions TagNOptions
// TagNOptions contains parameters for [oras.TagN].
type TagNOptions struct {
// Concurrency limits the maximum number of concurrent tag tasks.
// If less than or equal to 0, a default (currently 5) is used.
Concurrency int
// MaxMetadataBytes limits the maximum size of metadata that can be cached
// in the memory.
// If less than or equal to 0, a default (currently 4 MiB) is used.
MaxMetadataBytes int64
}
// TagN tags the descriptor identified by srcReference with dstReferences.
func TagN(ctx context.Context, target Target, srcReference string, dstReferences []string, opts TagNOptions) (ocispec.Descriptor, error) {
switch len(dstReferences) {
case 0:
return ocispec.Descriptor{}, fmt.Errorf("dstReferences cannot be empty: %w", errdef.ErrMissingReference)
case 1:
return Tag(ctx, target, srcReference, dstReferences[0])
}
if opts.Concurrency <= 0 {
opts.Concurrency = defaultTagConcurrency
}
if opts.MaxMetadataBytes <= 0 {
opts.MaxMetadataBytes = defaultTagNMaxMetadataBytes
}
_, isRefFetcher := target.(registry.ReferenceFetcher)
_, isRefPusher := target.(registry.ReferencePusher)
if isRefFetcher && isRefPusher {
if repo, ok := target.(interfaces.ReferenceParser); ok {
// add scope hints to minimize the number of auth requests
ref, err := repo.ParseReference(srcReference)
if err != nil {
return ocispec.Descriptor{}, err
}
ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull, auth.ActionPush)
}
desc, contentBytes, err := FetchBytes(ctx, target, srcReference, FetchBytesOptions{
MaxBytes: opts.MaxMetadataBytes,
})
if err != nil {
if errors.Is(err, errdef.ErrSizeExceedsLimit) {
err = fmt.Errorf(
"content size %v exceeds MaxMetadataBytes %v: %w",
desc.Size,
opts.MaxMetadataBytes,
errdef.ErrSizeExceedsLimit)
}
return ocispec.Descriptor{}, err
}
if err := tagBytesN(ctx, target, desc, contentBytes, dstReferences, TagBytesNOptions{
Concurrency: opts.Concurrency,
}); err != nil {
return ocispec.Descriptor{}, err
}
return desc, nil
}
desc, err := target.Resolve(ctx, srcReference)
if err != nil {
return ocispec.Descriptor{}, err
}
eg, egCtx := syncutil.LimitGroup(ctx, opts.Concurrency)
for _, dstRef := range dstReferences {
eg.Go(func(dst string) func() error {
return func() error {
if err := target.Tag(egCtx, desc, dst); err != nil {
return fmt.Errorf("failed to tag %s as %s: %w", srcReference, dst, err)
}
return nil
}
}(dstRef))
}
if err := eg.Wait(); err != nil {
return ocispec.Descriptor{}, err
}
return desc, nil
}
// Tag tags the descriptor identified by src with dst.
func Tag(ctx context.Context, target Target, src, dst string) (ocispec.Descriptor, error) {
refFetcher, okFetch := target.(registry.ReferenceFetcher)
refPusher, okPush := target.(registry.ReferencePusher)
if okFetch && okPush {
if repo, ok := target.(interfaces.ReferenceParser); ok {
// add scope hints to minimize the number of auth requests
ref, err := repo.ParseReference(src)
if err != nil {
return ocispec.Descriptor{}, err
}
ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull, auth.ActionPush)
}
desc, rc, err := refFetcher.FetchReference(ctx, src)
if err != nil {
return ocispec.Descriptor{}, err
}
defer rc.Close()
if err := refPusher.PushReference(ctx, desc, rc, dst); err != nil {
return ocispec.Descriptor{}, err
}
return desc, nil
}
desc, err := target.Resolve(ctx, src)
if err != nil {
return ocispec.Descriptor{}, err
}
if err := target.Tag(ctx, desc, dst); err != nil {
return ocispec.Descriptor{}, err
}
return desc, nil
}
// DefaultResolveOptions provides the default ResolveOptions.
var DefaultResolveOptions ResolveOptions
// ResolveOptions contains parameters for [oras.Resolve].
type ResolveOptions struct {
// TargetPlatform ensures the resolved content matches the target platform
// if the node is a manifest, or selects the first resolved content that
// matches the target platform if the node is a manifest list.
TargetPlatform *ocispec.Platform
// MaxMetadataBytes limits the maximum size of metadata that can be cached
// in the memory.
// If less than or equal to 0, a default (currently 4 MiB) is used.
MaxMetadataBytes int64
}
// Resolve resolves a descriptor with provided reference from the target.
func Resolve(ctx context.Context, target ReadOnlyTarget, reference string, opts ResolveOptions) (ocispec.Descriptor, error) {
if opts.TargetPlatform == nil {
return target.Resolve(ctx, reference)
}
return resolve(ctx, target, nil, reference, opts)
}
// resolve resolves a descriptor with provided reference from the target, with
// specified caching.
func resolve(ctx context.Context, target ReadOnlyTarget, proxy *cas.Proxy, reference string, opts ResolveOptions) (ocispec.Descriptor, error) {
if opts.MaxMetadataBytes <= 0 {
opts.MaxMetadataBytes = defaultResolveMaxMetadataBytes
}
if refFetcher, ok := target.(registry.ReferenceFetcher); ok {
// optimize performance for ReferenceFetcher targets
desc, rc, err := refFetcher.FetchReference(ctx, reference)
if err != nil {
return ocispec.Descriptor{}, err
}
defer rc.Close()
switch desc.MediaType {
case docker.MediaTypeManifestList, ocispec.MediaTypeImageIndex,
docker.MediaTypeManifest, ocispec.MediaTypeImageManifest:
// cache the fetched content
if desc.Size > opts.MaxMetadataBytes {
return ocispec.Descriptor{}, fmt.Errorf(
"content size %v exceeds MaxMetadataBytes %v: %w",
desc.Size,
opts.MaxMetadataBytes,
errdef.ErrSizeExceedsLimit)
}
if proxy == nil {
proxy = cas.NewProxyWithLimit(target, cas.NewMemory(), opts.MaxMetadataBytes)
}
if err := proxy.Cache.Push(ctx, desc, rc); err != nil {
return ocispec.Descriptor{}, err
}
// stop caching as SelectManifest may fetch a config blob
proxy.StopCaching = true
return platform.SelectManifest(ctx, proxy, desc, opts.TargetPlatform)
default:
return ocispec.Descriptor{}, fmt.Errorf("%s: %s: %w", desc.Digest, desc.MediaType, errdef.ErrUnsupported)
}
}
desc, err := target.Resolve(ctx, reference)
if err != nil {
return ocispec.Descriptor{}, err
}
return platform.SelectManifest(ctx, target, desc, opts.TargetPlatform)
}
// DefaultFetchOptions provides the default FetchOptions.
var DefaultFetchOptions FetchOptions
// FetchOptions contains parameters for [oras.Fetch].
type FetchOptions struct {
// ResolveOptions contains parameters for resolving reference.
ResolveOptions
}
// Fetch fetches the content identified by the reference.
func Fetch(ctx context.Context, target ReadOnlyTarget, reference string, opts FetchOptions) (ocispec.Descriptor, io.ReadCloser, error) {
if opts.TargetPlatform == nil {
if refFetcher, ok := target.(registry.ReferenceFetcher); ok {
return refFetcher.FetchReference(ctx, reference)
}
desc, err := target.Resolve(ctx, reference)
if err != nil {
return ocispec.Descriptor{}, nil, err
}
rc, err := target.Fetch(ctx, desc)
if err != nil {
return ocispec.Descriptor{}, nil, err
}
return desc, rc, nil
}
if opts.MaxMetadataBytes <= 0 {
opts.MaxMetadataBytes = defaultResolveMaxMetadataBytes
}
proxy := cas.NewProxyWithLimit(target, cas.NewMemory(), opts.MaxMetadataBytes)
desc, err := resolve(ctx, target, proxy, reference, opts.ResolveOptions)
if err != nil {
return ocispec.Descriptor{}, nil, err
}
// if the content exists in cache, fetch it from cache
// otherwise fetch without caching
proxy.StopCaching = true
rc, err := proxy.Fetch(ctx, desc)
if err != nil {
return ocispec.Descriptor{}, nil, err
}
return desc, rc, nil
}
// DefaultFetchBytesOptions provides the default FetchBytesOptions.
var DefaultFetchBytesOptions FetchBytesOptions
// FetchBytesOptions contains parameters for [oras.FetchBytes].
type FetchBytesOptions struct {
// FetchOptions contains parameters for fetching content.
FetchOptions
// MaxBytes limits the maximum size of the fetched content bytes.
// If less than or equal to 0, a default (currently 4 MiB) is used.
MaxBytes int64
}
// FetchBytes fetches the content bytes identified by the reference.
func FetchBytes(ctx context.Context, target ReadOnlyTarget, reference string, opts FetchBytesOptions) (ocispec.Descriptor, []byte, error) {
if opts.MaxBytes <= 0 {
opts.MaxBytes = defaultMaxBytes
}
desc, rc, err := Fetch(ctx, target, reference, opts.FetchOptions)
if err != nil {
return ocispec.Descriptor{}, nil, err
}
defer rc.Close()
if desc.Size > opts.MaxBytes {
return ocispec.Descriptor{}, nil, fmt.Errorf(
"content size %v exceeds MaxBytes %v: %w",
desc.Size,
opts.MaxBytes,
errdef.ErrSizeExceedsLimit)
}
bytes, err := content.ReadAll(rc, desc)
if err != nil {
return ocispec.Descriptor{}, nil, err
}
return desc, bytes, nil
}
// PushBytes describes the contentBytes using the given mediaType and pushes it.
// If mediaType is not specified, "application/octet-stream" is used.
func PushBytes(ctx context.Context, pusher content.Pusher, mediaType string, contentBytes []byte) (ocispec.Descriptor, error) {
desc := content.NewDescriptorFromBytes(mediaType, contentBytes)
r := bytes.NewReader(contentBytes)
if err := pusher.Push(ctx, desc, r); err != nil {
return ocispec.Descriptor{}, err
}
return desc, nil
}
// DefaultTagBytesNOptions provides the default TagBytesNOptions.
var DefaultTagBytesNOptions TagBytesNOptions
// TagBytesNOptions contains parameters for [oras.TagBytesN].
type TagBytesNOptions struct {
// Concurrency limits the maximum number of concurrent tag tasks.
// If less than or equal to 0, a default (currently 5) is used.
Concurrency int
}
// TagBytesN describes the contentBytes using the given mediaType, pushes it,
// and tag it with the given references.
// If mediaType is not specified, "application/octet-stream" is used.
func TagBytesN(ctx context.Context, target Target, mediaType string, contentBytes []byte, references []string, opts TagBytesNOptions) (ocispec.Descriptor, error) {
if len(references) == 0 {
return PushBytes(ctx, target, mediaType, contentBytes)
}
desc := content.NewDescriptorFromBytes(mediaType, contentBytes)
if opts.Concurrency <= 0 {
opts.Concurrency = defaultTagConcurrency
}
if err := tagBytesN(ctx, target, desc, contentBytes, references, opts); err != nil {
return ocispec.Descriptor{}, err
}
return desc, nil
}
// tagBytesN pushes the contentBytes using the given desc, and tag it with the
// given references.
func tagBytesN(ctx context.Context, target Target, desc ocispec.Descriptor, contentBytes []byte, references []string, opts TagBytesNOptions) error {
eg, egCtx := syncutil.LimitGroup(ctx, opts.Concurrency)
if refPusher, ok := target.(registry.ReferencePusher); ok {
for _, reference := range references {
eg.Go(func(ref string) func() error {
return func() error {
r := bytes.NewReader(contentBytes)
if err := refPusher.PushReference(egCtx, desc, r, ref); err != nil && !errors.Is(err, errdef.ErrAlreadyExists) {
return fmt.Errorf("failed to tag %s: %w", ref, err)
}
return nil
}
}(reference))
}
} else {
r := bytes.NewReader(contentBytes)
if err := target.Push(ctx, desc, r); err != nil && !errors.Is(err, errdef.ErrAlreadyExists) {
return fmt.Errorf("failed to push content: %w", err)
}
for _, reference := range references {
eg.Go(func(ref string) func() error {
return func() error {
if err := target.Tag(egCtx, desc, ref); err != nil {
return fmt.Errorf("failed to tag %s: %w", ref, err)
}
return nil
}
}(reference))
}
}
return eg.Wait()
}
// TagBytes describes the contentBytes using the given mediaType, pushes it,
// and tag it with the given reference.
// If mediaType is not specified, "application/octet-stream" is used.
func TagBytes(ctx context.Context, target Target, mediaType string, contentBytes []byte, reference string) (ocispec.Descriptor, error) {
return TagBytesN(ctx, target, mediaType, contentBytes, []string{reference}, DefaultTagBytesNOptions)
}
oras-go-2.5.0/content/ 0000775 0000000 0000000 00000000000 14576745303 0014555 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/content/descriptor.go 0000664 0000000 0000000 00000002516 14576745303 0017266 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 (
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/internal/descriptor"
)
// NewDescriptorFromBytes returns a descriptor, given the content and media type.
// If no media type is specified, "application/octet-stream" will be used.
func NewDescriptorFromBytes(mediaType string, content []byte) ocispec.Descriptor {
if mediaType == "" {
mediaType = descriptor.DefaultMediaType
}
return ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
}
// Equal returns true if two descriptors point to the same content.
func Equal(a, b ocispec.Descriptor) bool {
return a.Size == b.Size && a.Digest == b.Digest && a.MediaType == b.MediaType
}
oras-go-2.5.0/content/descriptor_test.go 0000664 0000000 0000000 00000010535 14576745303 0020325 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 (
"reflect"
"testing"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/internal/descriptor"
)
func TestGenerateDescriptor(t *testing.T) {
contentFoo := []byte("foo")
contentBar := []byte("bar")
type args struct {
content []byte
mediaType string
}
tests := []struct {
name string
args args
want ocispec.Descriptor
}{
{
name: "foo descriptor",
args: args{contentFoo, "example media type"},
want: ocispec.Descriptor{
MediaType: "example media type",
Digest: digest.FromBytes(contentFoo),
Size: int64(len(contentFoo))},
},
{
name: "empty content",
args: args{[]byte(""), "example media type"},
want: ocispec.Descriptor{
MediaType: "example media type",
Digest: digest.FromBytes([]byte("")),
Size: int64(len([]byte("")))},
},
{
name: "missing media type",
args: args{contentBar, ""},
want: ocispec.Descriptor{
MediaType: descriptor.DefaultMediaType,
Digest: digest.FromBytes(contentBar),
Size: int64(len(contentBar))},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := NewDescriptorFromBytes(tt.args.mediaType, tt.args.content)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("GenerateDescriptor() = %v, want %v", got, tt.want)
}
})
}
}
func TestEqual(t *testing.T) {
contentFoo := []byte("foo")
contentBar := []byte("bar")
type args struct {
a ocispec.Descriptor
b ocispec.Descriptor
}
tests := []struct {
name string
args args
want bool
}{
{
name: "same media type, digest and size",
args: args{
ocispec.Descriptor{
MediaType: "example media type",
Digest: digest.FromBytes(contentFoo),
Size: int64(len(contentFoo))},
ocispec.Descriptor{
MediaType: "example media type",
Digest: digest.FromBytes(contentFoo),
Size: int64(len(contentFoo))}},
want: true,
},
{
name: "different media type, same digest and size",
args: args{
ocispec.Descriptor{
MediaType: "example media type",
Digest: digest.FromBytes(contentFoo),
Size: int64(len(contentFoo))},
ocispec.Descriptor{
MediaType: "another media type",
Digest: digest.FromBytes(contentFoo),
Size: int64(len(contentFoo))}},
want: false,
},
{
name: "different digest, same media type and size",
args: args{
ocispec.Descriptor{
MediaType: "example media type",
Digest: digest.FromBytes(contentFoo),
Size: int64(len(contentFoo))},
ocispec.Descriptor{
MediaType: "example media type",
Digest: digest.FromBytes(contentBar),
Size: int64(len(contentBar))}},
want: false,
},
{
name: "only same media type",
args: args{
ocispec.Descriptor{
MediaType: "example media type",
Digest: digest.FromBytes([]byte("fooooo")),
Size: int64(len([]byte("foooo")))},
ocispec.Descriptor{
MediaType: "example media type",
Digest: digest.FromBytes(contentBar),
Size: int64(len(contentBar))}},
want: false,
},
{
name: "different size, same media type and digest",
args: args{
ocispec.Descriptor{
MediaType: "example media type",
Digest: digest.FromBytes(contentFoo),
Size: int64(len(contentFoo))},
ocispec.Descriptor{
MediaType: "example media type",
Digest: digest.FromBytes(contentFoo),
Size: int64(len(contentFoo)) + 1}},
want: false,
},
{
name: "two empty descriptors",
args: args{
ocispec.Descriptor{},
ocispec.Descriptor{}},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Equal(tt.args.a, tt.args.b); got != tt.want {
t.Errorf("Equal() = %v, want %v", got, tt.want)
}
})
}
}
oras-go-2.5.0/content/example_test.go 0000664 0000000 0000000 00000002357 14576745303 0017605 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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_test
import (
"bytes"
"fmt"
"io"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content"
)
// ExampleVerifyReader gives an example of creating and using VerifyReader.
func ExampleVerifyReader() {
blob := []byte("hello world")
desc := content.NewDescriptorFromBytes(ocispec.MediaTypeImageLayer, blob)
r := bytes.NewReader(blob)
vr := content.NewVerifyReader(r, desc)
buf := bytes.NewBuffer(nil)
if _, err := io.Copy(buf, vr); err != nil {
panic(err)
}
// note: users should not trust the the read content until
// Verify() returns nil.
if err := vr.Verify(); err != nil {
panic(err)
}
fmt.Println(buf)
// Output:
// hello world
}
oras-go-2.5.0/content/file/ 0000775 0000000 0000000 00000000000 14576745303 0015474 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/content/file/errors.go 0000664 0000000 0000000 00000001715 14576745303 0017343 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 file
import "errors"
var (
ErrMissingName = errors.New("missing name")
ErrDuplicateName = errors.New("duplicate name")
ErrPathTraversalDisallowed = errors.New("path traversal disallowed")
ErrOverwriteDisallowed = errors.New("overwrite disallowed")
ErrStoreClosed = errors.New("store already closed")
)
var errSkipUnnamed = errors.New("unnamed descriptor skipped")
oras-go-2.5.0/content/file/example_test.go 0000664 0000000 0000000 00000005762 14576745303 0020527 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 file_test includes all the testable examples for the file package.
package file_test
import (
"context"
"fmt"
"os"
"path/filepath"
"testing"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2"
"oras.land/oras-go/v2/content/file"
)
var workingDir string // the working directory for the examples
func TestMain(m *testing.M) {
// prepare test directory
var err error
workingDir, err = os.MkdirTemp("", "oras_file_example_*")
if err != nil {
panic(err)
}
tearDown := func() {
if err := os.RemoveAll(workingDir); err != nil {
panic(err)
}
}
// prepare test file 1
content := []byte("foo")
filename := "foo.txt"
path := filepath.Join(workingDir, filename)
if err := os.WriteFile(path, content, 0444); err != nil {
panic(err)
}
// prepare test file 2
content = []byte("bar")
filename = "bar.txt"
path = filepath.Join(workingDir, filename)
if err := os.WriteFile(path, content, 0444); err != nil {
panic(err)
}
// run tests
exitCode := m.Run()
// tear down and exit
tearDown()
os.Exit(exitCode)
}
// Example_packFiles gives an example of adding files and generating a manifest
// referencing the files.
func Example_packFiles() {
store, err := file.New(workingDir)
if err != nil {
panic(err)
}
defer store.Close()
ctx := context.Background()
// 1. Add files into the file store
mediaType := "example/file"
fileNames := []string{"foo.txt", "bar.txt"}
fileDescriptors := make([]ocispec.Descriptor, 0, len(fileNames))
for _, name := range fileNames {
fileDescriptor, err := store.Add(ctx, name, mediaType, "")
if err != nil {
panic(err)
}
fileDescriptors = append(fileDescriptors, fileDescriptor)
fmt.Printf("file descriptor for %s: %v\n", name, fileDescriptor)
}
// 2. Generate a manifest referencing the files
artifactType := "example/test"
manifestDescriptor, err := oras.Pack(ctx, store, artifactType, fileDescriptors, oras.PackOptions{})
if err != nil {
panic(err)
}
fmt.Println("manifest media type:", manifestDescriptor.MediaType)
// Output:
// file descriptor for foo.txt: {example/file sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae 3 [] map[org.opencontainers.image.title:foo.txt] [] }
// file descriptor for bar.txt: {example/file sha256:fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9 3 [] map[org.opencontainers.image.title:bar.txt] [] }
// manifest media type: application/vnd.oci.artifact.manifest.v1+json
}
oras-go-2.5.0/content/file/file.go 0000664 0000000 0000000 00000047516 14576745303 0016757 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 file provides implementation of a content store based on file system.
package file
import (
"compress/gzip"
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/internal/cas"
"oras.land/oras-go/v2/internal/graph"
"oras.land/oras-go/v2/internal/ioutil"
"oras.land/oras-go/v2/internal/resolver"
)
// bufPool is a pool of byte buffers that can be reused for copying content
// between files.
var bufPool = sync.Pool{
New: func() interface{} {
// the buffer size should be larger than or equal to 128 KiB
// for performance considerations.
// we choose 1 MiB here so there will be less disk I/O.
buffer := make([]byte, 1<<20) // buffer size = 1 MiB
return &buffer
},
}
const (
// AnnotationDigest is the annotation key for the digest of the uncompressed content.
AnnotationDigest = "io.deis.oras.content.digest"
// AnnotationUnpack is the annotation key for indication of unpacking.
AnnotationUnpack = "io.deis.oras.content.unpack"
// defaultBlobMediaType specifies the default blob media type.
defaultBlobMediaType = ocispec.MediaTypeImageLayer
// defaultBlobDirMediaType specifies the default blob directory media type.
defaultBlobDirMediaType = ocispec.MediaTypeImageLayerGzip
// defaultFallbackPushSizeLimit specifies the default size limit for pushing no-name contents.
defaultFallbackPushSizeLimit = 1 << 22 // 4 MiB
)
// Store represents a file system based store, which implements `oras.Target`.
//
// In the file store, the contents described by names are location-addressed
// by file paths. Meanwhile, the file paths are mapped to a virtual CAS
// where all metadata are stored in the memory.
//
// The contents that are not described by names are stored in a fallback storage,
// which is a limited memory CAS by default.
// As all the metadata are stored in the memory, the file store
// cannot be restored from the file system.
//
// After use, the file store needs to be closed by calling the [Store.Close] function.
// The file store cannot be used after being closed.
type Store struct {
// TarReproducible controls if the tarballs generated
// for the added directories are reproducible.
// When specified, some metadata such as change time
// will be removed from the files in the tarballs. Default value: false.
TarReproducible bool
// AllowPathTraversalOnWrite controls if path traversal is allowed
// when writing files. When specified, writing files
// outside the working directory will be allowed. Default value: false.
AllowPathTraversalOnWrite bool
// DisableOverwrite controls if push operations can overwrite existing files.
// When specified, saving files to existing paths will be disabled.
// Default value: false.
DisableOverwrite bool
// ForceCAS controls if files with same content but different names are
// deduped after push operations. When a DAG is copied between CAS
// targets, nodes are deduped by content. By default, file store restores
// deduped successor files after a node is copied. This may result in two
// files with identical content. If this is not the desired behavior,
// ForceCAS can be specified to enforce CAS style dedup.
// Default value: false.
ForceCAS bool
// IgnoreNoName controls if push operations should ignore descriptors
// without a name. When specified, corresponding content will be discarded.
// Otherwise, content will be saved to a fallback storage.
// A typical scenario is pulling an arbitrary artifact masqueraded as OCI
// image to file store. This option can be specified to discard unnamed
// manifest and config file, while leaving only named layer files.
// Default value: false.
IgnoreNoName bool
// SkipUnpack controls if push operations should skip unpacking files. This
// value overrides the [AnnotationUnpack].
// Default value: false.
SkipUnpack bool
workingDir string // the working directory of the file store
closed int32 // if the store is closed - 0: false, 1: true.
digestToPath sync.Map // map[digest.Digest]string
nameToStatus sync.Map // map[string]*nameStatus
tmpFiles sync.Map // map[string]bool
fallbackStorage content.Storage
resolver content.TagResolver
graph *graph.Memory
}
// nameStatus contains a flag indicating if a name exists,
// and a RWMutex protecting it.
type nameStatus struct {
sync.RWMutex
exists bool
}
// New creates a file store, using a default limited memory CAS
// as the fallback storage for contents without names.
// When pushing content without names, the size of content being pushed
// cannot exceed the default size limit: 4 MiB.
func New(workingDir string) (*Store, error) {
return NewWithFallbackLimit(workingDir, defaultFallbackPushSizeLimit)
}
// NewWithFallbackLimit creates a file store, using a default
// limited memory CAS as the fallback storage for contents without names.
// When pushing content without names, the size of content being pushed
// cannot exceed the size limit specified by the `limit` parameter.
func NewWithFallbackLimit(workingDir string, limit int64) (*Store, error) {
m := cas.NewMemory()
ls := content.LimitStorage(m, limit)
return NewWithFallbackStorage(workingDir, ls)
}
// NewWithFallbackStorage creates a file store,
// using the provided fallback storage for contents without names.
func NewWithFallbackStorage(workingDir string, fallbackStorage content.Storage) (*Store, error) {
workingDirAbs, err := filepath.Abs(workingDir)
if err != nil {
return nil, fmt.Errorf("failed to resolve absolute path for %s: %w", workingDir, err)
}
return &Store{
workingDir: workingDirAbs,
fallbackStorage: fallbackStorage,
resolver: resolver.NewMemory(),
graph: graph.NewMemory(),
}, nil
}
// Close closes the file store and cleans up all the temporary files used by it.
// The store cannot be used after being closed.
// This function is not go-routine safe.
func (s *Store) Close() error {
if s.isClosedSet() {
return nil
}
s.setClosed()
var errs []string
s.tmpFiles.Range(func(name, _ interface{}) bool {
if err := os.Remove(name.(string)); err != nil {
errs = append(errs, err.Error())
}
return true
})
if len(errs) > 0 {
return errors.New(strings.Join(errs, "; "))
}
return nil
}
// Fetch fetches the content identified by the descriptor.
func (s *Store) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
if s.isClosedSet() {
return nil, ErrStoreClosed
}
// if the target has name, check if the name exists.
name := target.Annotations[ocispec.AnnotationTitle]
if name != "" && !s.nameExists(name) {
return nil, fmt.Errorf("%s: %s: %w", name, target.MediaType, errdef.ErrNotFound)
}
// check if the content exists in the store
val, exists := s.digestToPath.Load(target.Digest)
if exists {
path := val.(string)
fp, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf("%s: %s: %w", target.Digest, target.MediaType, errdef.ErrNotFound)
}
return nil, err
}
return fp, nil
}
// if the content does not exist in the store,
// then fall back to the fallback storage.
return s.fallbackStorage.Fetch(ctx, target)
}
// Push pushes the content, matching the expected descriptor.
// If name is not specified in the descriptor, the content will be pushed to
// the fallback storage by default, or will be discarded when
// Store.IgnoreNoName is true.
func (s *Store) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error {
if s.isClosedSet() {
return ErrStoreClosed
}
if err := s.push(ctx, expected, content); err != nil {
if errors.Is(err, errSkipUnnamed) {
return nil
}
return err
}
if !s.ForceCAS {
if err := s.restoreDuplicates(ctx, expected); err != nil {
return fmt.Errorf("failed to restore duplicated file: %w", err)
}
}
return s.graph.Index(ctx, s, expected)
}
// push pushes the content, matching the expected descriptor.
// If name is not specified in the descriptor, the content will be pushed to
// the fallback storage by default, or will be discarded when
// Store.IgnoreNoName is true.
func (s *Store) push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error {
name := expected.Annotations[ocispec.AnnotationTitle]
if name == "" {
if s.IgnoreNoName {
return errSkipUnnamed
}
return s.fallbackStorage.Push(ctx, expected, content)
}
// check the status of the name
status := s.status(name)
status.Lock()
defer status.Unlock()
if status.exists {
return fmt.Errorf("%s: %w", name, ErrDuplicateName)
}
target, err := s.resolveWritePath(name)
if err != nil {
return fmt.Errorf("failed to resolve path for writing: %w", err)
}
if needUnpack := expected.Annotations[AnnotationUnpack]; needUnpack == "true" && !s.SkipUnpack {
err = s.pushDir(name, target, expected, content)
} else {
err = s.pushFile(target, expected, content)
}
if err != nil {
return err
}
// update the name status as existed
status.exists = true
return nil
}
// restoreDuplicates restores successor files with same content but different
// names.
// See Store.ForceCAS for more info.
func (s *Store) restoreDuplicates(ctx context.Context, desc ocispec.Descriptor) error {
successors, err := content.Successors(ctx, s, desc)
if err != nil {
return err
}
for _, successor := range successors {
name := successor.Annotations[ocispec.AnnotationTitle]
if name == "" || s.nameExists(name) {
continue
}
if err := func() error {
desc := ocispec.Descriptor{
MediaType: successor.MediaType,
Digest: successor.Digest,
Size: successor.Size,
}
rc, err := s.Fetch(ctx, desc)
if err != nil {
return fmt.Errorf("%q: %s: %w", name, desc.MediaType, err)
}
defer rc.Close()
if err := s.push(ctx, successor, rc); err != nil {
return fmt.Errorf("%q: %s: %w", name, desc.MediaType, err)
}
return nil
}(); err != nil {
switch {
case errors.Is(err, errdef.ErrNotFound):
// allow pushing manifests before blobs
case errors.Is(err, ErrDuplicateName):
// in case multiple goroutines are pushing or restoring the same
// named content, the error is ignored
default:
return err
}
}
}
return nil
}
// Exists returns true if the described content exists.
func (s *Store) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) {
if s.isClosedSet() {
return false, ErrStoreClosed
}
// if the target has name, check if the name exists.
name := target.Annotations[ocispec.AnnotationTitle]
if name != "" && !s.nameExists(name) {
return false, nil
}
// check if the content exists in the store
_, exists := s.digestToPath.Load(target.Digest)
if exists {
return true, nil
}
// if the content does not exist in the store,
// then fall back to the fallback storage.
return s.fallbackStorage.Exists(ctx, target)
}
// Resolve resolves a reference to a descriptor.
func (s *Store) Resolve(ctx context.Context, ref string) (ocispec.Descriptor, error) {
if s.isClosedSet() {
return ocispec.Descriptor{}, ErrStoreClosed
}
if ref == "" {
return ocispec.Descriptor{}, errdef.ErrMissingReference
}
return s.resolver.Resolve(ctx, ref)
}
// Tag tags a descriptor with a reference string.
func (s *Store) Tag(ctx context.Context, desc ocispec.Descriptor, ref string) error {
if s.isClosedSet() {
return ErrStoreClosed
}
if ref == "" {
return errdef.ErrMissingReference
}
exists, err := s.Exists(ctx, desc)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("%s: %s: %w", desc.Digest, desc.MediaType, errdef.ErrNotFound)
}
return s.resolver.Tag(ctx, desc, ref)
}
// Predecessors returns the nodes directly pointing to the current node.
// Predecessors returns nil without error if the node does not exists in the
// store.
func (s *Store) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) {
if s.isClosedSet() {
return nil, ErrStoreClosed
}
return s.graph.Predecessors(ctx, node)
}
// Add adds a file into the file store.
func (s *Store) Add(_ context.Context, name, mediaType, path string) (ocispec.Descriptor, error) {
if s.isClosedSet() {
return ocispec.Descriptor{}, ErrStoreClosed
}
if name == "" {
return ocispec.Descriptor{}, ErrMissingName
}
// check the status of the name
status := s.status(name)
status.Lock()
defer status.Unlock()
if status.exists {
return ocispec.Descriptor{}, fmt.Errorf("%s: %w", name, ErrDuplicateName)
}
if path == "" {
path = name
}
path = s.absPath(path)
fi, err := os.Stat(path)
if err != nil {
return ocispec.Descriptor{}, fmt.Errorf("failed to stat %s: %w", path, err)
}
// generate descriptor
var desc ocispec.Descriptor
if fi.IsDir() {
desc, err = s.descriptorFromDir(name, mediaType, path)
} else {
desc, err = s.descriptorFromFile(fi, mediaType, path)
}
if err != nil {
return ocispec.Descriptor{}, fmt.Errorf("failed to generate descriptor from %s: %w", path, err)
}
if desc.Annotations == nil {
desc.Annotations = make(map[string]string)
}
desc.Annotations[ocispec.AnnotationTitle] = name
// update the name status as existed
status.exists = true
return desc, nil
}
// saveFile saves content matching the descriptor to the given file.
func (s *Store) saveFile(fp *os.File, expected ocispec.Descriptor, content io.Reader) (err error) {
defer func() {
closeErr := fp.Close()
if err == nil {
err = closeErr
}
}()
path := fp.Name()
buf := bufPool.Get().(*[]byte)
defer bufPool.Put(buf)
if err := ioutil.CopyBuffer(fp, content, *buf, expected); err != nil {
return fmt.Errorf("failed to copy content to %s: %w", path, err)
}
s.digestToPath.Store(expected.Digest, path)
return nil
}
// pushFile saves content matching the descriptor to the target path.
func (s *Store) pushFile(target string, expected ocispec.Descriptor, content io.Reader) error {
if err := ensureDir(filepath.Dir(target)); err != nil {
return fmt.Errorf("failed to ensure directories of the target path: %w", err)
}
fp, err := os.Create(target)
if err != nil {
return fmt.Errorf("failed to create file %s: %w", target, err)
}
return s.saveFile(fp, expected, content)
}
// pushDir saves content matching the descriptor to the target directory.
func (s *Store) pushDir(name, target string, expected ocispec.Descriptor, content io.Reader) (err error) {
if err := ensureDir(target); err != nil {
return fmt.Errorf("failed to ensure directories of the target path: %w", err)
}
gz, err := s.tempFile()
if err != nil {
return err
}
gzPath := gz.Name()
// the digest of the gz is verified while saving
if err := s.saveFile(gz, expected, content); err != nil {
return fmt.Errorf("failed to save gzip to %s: %w", gzPath, err)
}
checksum := expected.Annotations[AnnotationDigest]
buf := bufPool.Get().(*[]byte)
defer bufPool.Put(buf)
if err := extractTarGzip(target, name, gzPath, checksum, *buf); err != nil {
return fmt.Errorf("failed to extract tar to %s: %w", target, err)
}
return nil
}
// descriptorFromDir generates descriptor from the given directory.
func (s *Store) descriptorFromDir(name, mediaType, dir string) (desc ocispec.Descriptor, err error) {
// make a temp file to store the gzip
gz, err := s.tempFile()
if err != nil {
return ocispec.Descriptor{}, err
}
defer func() {
closeErr := gz.Close()
if err == nil {
err = closeErr
}
}()
// compress the directory
gzDigester := digest.Canonical.Digester()
gzw := gzip.NewWriter(io.MultiWriter(gz, gzDigester.Hash()))
defer func() {
closeErr := gzw.Close()
if err == nil {
err = closeErr
}
}()
tarDigester := digest.Canonical.Digester()
tw := io.MultiWriter(gzw, tarDigester.Hash())
buf := bufPool.Get().(*[]byte)
defer bufPool.Put(buf)
if err := tarDirectory(dir, name, tw, s.TarReproducible, *buf); err != nil {
return ocispec.Descriptor{}, fmt.Errorf("failed to tar %s: %w", dir, err)
}
// flush all
if err := gzw.Close(); err != nil {
return ocispec.Descriptor{}, err
}
if err := gz.Sync(); err != nil {
return ocispec.Descriptor{}, err
}
fi, err := gz.Stat()
if err != nil {
return ocispec.Descriptor{}, err
}
// map gzip digest to gzip path
gzDigest := gzDigester.Digest()
s.digestToPath.Store(gzDigest, gz.Name())
// generate descriptor
if mediaType == "" {
mediaType = defaultBlobDirMediaType
}
return ocispec.Descriptor{
MediaType: mediaType,
Digest: gzDigest, // digest for the compressed content
Size: fi.Size(),
Annotations: map[string]string{
AnnotationDigest: tarDigester.Digest().String(), // digest fot the uncompressed content
AnnotationUnpack: "true", // the content needs to be unpacked
},
}, nil
}
// descriptorFromFile generates descriptor from the given file.
func (s *Store) descriptorFromFile(fi os.FileInfo, mediaType, path string) (desc ocispec.Descriptor, err error) {
fp, err := os.Open(path)
if err != nil {
return ocispec.Descriptor{}, err
}
defer func() {
closeErr := fp.Close()
if err == nil {
err = closeErr
}
}()
dgst, err := digest.FromReader(fp)
if err != nil {
return ocispec.Descriptor{}, err
}
// map digest to file path
s.digestToPath.Store(dgst, path)
// generate descriptor
if mediaType == "" {
mediaType = defaultBlobMediaType
}
return ocispec.Descriptor{
MediaType: mediaType,
Digest: dgst,
Size: fi.Size(),
}, nil
}
// resolveWritePath resolves the path to write for the given name.
func (s *Store) resolveWritePath(name string) (string, error) {
path := s.absPath(name)
if !s.AllowPathTraversalOnWrite {
base, err := filepath.Abs(s.workingDir)
if err != nil {
return "", err
}
target, err := filepath.Abs(path)
if err != nil {
return "", err
}
rel, err := filepath.Rel(base, target)
if err != nil {
return "", ErrPathTraversalDisallowed
}
rel = filepath.ToSlash(rel)
if strings.HasPrefix(rel, "../") || rel == ".." {
return "", ErrPathTraversalDisallowed
}
}
if s.DisableOverwrite {
if _, err := os.Stat(path); err == nil {
return "", ErrOverwriteDisallowed
} else if !os.IsNotExist(err) {
return "", err
}
}
return path, nil
}
// status returns the nameStatus for the given name.
func (s *Store) status(name string) *nameStatus {
v, _ := s.nameToStatus.LoadOrStore(name, &nameStatus{sync.RWMutex{}, false})
status := v.(*nameStatus)
return status
}
// nameExists returns if the given name exists in the file store.
func (s *Store) nameExists(name string) bool {
status := s.status(name)
status.RLock()
defer status.RUnlock()
return status.exists
}
// tempFile creates a temp file with the file name format "oras_file_randomString",
// and returns the pointer to the temp file.
func (s *Store) tempFile() (*os.File, error) {
tmp, err := os.CreateTemp("", "oras_file_*")
if err != nil {
return nil, err
}
s.tmpFiles.Store(tmp.Name(), true)
return tmp, nil
}
// absPath returns the absolute path of the path.
func (s *Store) absPath(path string) string {
if filepath.IsAbs(path) {
return path
}
return filepath.Join(s.workingDir, path)
}
// isClosedSet returns true if the `closed` flag is set, otherwise returns false.
func (s *Store) isClosedSet() bool {
return atomic.LoadInt32(&s.closed) == 1
}
// setClosed sets the `closed` flag.
func (s *Store) setClosed() {
atomic.StoreInt32(&s.closed, 1)
}
// ensureDir ensures the directories of the path exists.
func ensureDir(path string) error {
return os.MkdirAll(path, 0777)
}
oras-go-2.5.0/content/file/file_test.go 0000664 0000000 0000000 00000243264 14576745303 0020014 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 file
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"strings"
"sync/atomic"
"testing"
_ "crypto/sha256"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"golang.org/x/sync/errgroup"
"oras.land/oras-go/v2"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/content/memory"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/internal/cas"
"oras.land/oras-go/v2/internal/descriptor"
"oras.land/oras-go/v2/internal/spec"
)
// storageTracker tracks storage API counts.
type storageTracker struct {
content.Storage
fetch int64
push int64
exists int64
}
func (t *storageTracker) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
atomic.AddInt64(&t.fetch, 1)
return t.Storage.Fetch(ctx, target)
}
func (t *storageTracker) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error {
atomic.AddInt64(&t.push, 1)
return t.Storage.Push(ctx, expected, content)
}
func (t *storageTracker) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) {
atomic.AddInt64(&t.exists, 1)
return t.Storage.Exists(ctx, target)
}
func TestStoreInterface(t *testing.T) {
var store interface{} = &Store{}
if _, ok := store.(oras.Target); !ok {
t.Error("&Store{} does not conform oras.Target")
}
if _, ok := store.(content.PredecessorFinder); !ok {
t.Error("&Store{} does not conform content.PredecessorFinder")
}
}
func TestStore_Success(t *testing.T) {
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
ctx := context.Background()
blob := []byte("hello world")
name := "test.txt"
mediaType := "test"
desc := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
Annotations: map[string]string{
ocispec.AnnotationTitle: name,
},
}
path := filepath.Join(tempDir, name)
if err := os.WriteFile(path, blob, 0444); err != nil {
t.Fatal("error calling WriteFile(), error =", err)
}
// test blob add
gotDesc, err := s.Add(ctx, name, mediaType, path)
if err != nil {
t.Fatal("Store.Add() error =", err)
}
if descriptor.FromOCI(gotDesc) != descriptor.FromOCI(desc) {
t.Fatal("got descriptor mismatch")
}
// test blob exists
exists, err := s.Exists(ctx, gotDesc)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
// test blob fetch
rc, err := s.Fetch(ctx, gotDesc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, blob) {
t.Errorf("Store.Fetch() = %v, want %v", got, blob)
}
// test push config
config := []byte("{}")
configDesc := ocispec.Descriptor{
MediaType: "config",
Digest: digest.FromBytes(config),
Size: int64(len(config)),
Annotations: map[string]string{
ocispec.AnnotationTitle: "config.blob",
},
}
if err := s.Push(ctx, configDesc, bytes.NewReader(config)); err != nil {
t.Fatal("Store.Push() error =", err)
}
// test push manifest
manifest := ocispec.Manifest{
MediaType: ocispec.MediaTypeImageManifest,
Config: configDesc,
Layers: []ocispec.Descriptor{
gotDesc,
},
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal("json.Marshal() error =", err)
}
manifestDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(manifestJSON),
Size: int64(len(manifestJSON)),
}
if err := s.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON)); err != nil {
t.Fatal("Store.Push() error =", err)
}
// test tag
ref := "foobar"
if err := s.Tag(ctx, manifestDesc, ref); err != nil {
t.Fatal("Store.Tag() error =", err)
}
// test resolve
gotManifestDesc, err := s.Resolve(ctx, ref)
if err != nil {
t.Fatal("Store.Resolve() error =", err)
}
if !reflect.DeepEqual(gotManifestDesc, manifestDesc) {
t.Errorf("Store.Resolve() = %v, want %v", gotManifestDesc, manifestDesc)
}
// test fetch
exists, err = s.Exists(ctx, gotManifestDesc)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
mrc, err := s.Fetch(ctx, gotManifestDesc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got, err = io.ReadAll(mrc)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = mrc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, manifestJSON) {
t.Errorf("Store.Fetch() = %v, want %v", got, manifestJSON)
}
}
func TestStore_RelativeRoot_Success(t *testing.T) {
tempDir, err := filepath.EvalSymlinks(t.TempDir())
if err != nil {
t.Fatal("error calling filepath.EvalSymlinks(), error =", err)
}
currDir, err := os.Getwd()
if err != nil {
t.Fatal("error calling Getwd(), error=", err)
}
if err := os.Chdir(tempDir); err != nil {
t.Fatal("error calling Chdir(), error=", err)
}
s, err := New(".")
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
if want := tempDir; s.workingDir != want {
t.Errorf("Store.workingDir = %s, want %s", s.workingDir, want)
}
// cd back to allow the temp directory to be removed
if err := os.Chdir(currDir); err != nil {
t.Fatal("error calling Chdir(), error=", err)
}
ctx := context.Background()
blob := []byte("hello world")
name := "test.txt"
mediaType := "test"
desc := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
Annotations: map[string]string{
ocispec.AnnotationTitle: name,
},
}
path := filepath.Join(tempDir, name)
if err := os.WriteFile(path, blob, 0444); err != nil {
t.Fatal("error calling WriteFile(), error =", err)
}
// test blob add
gotDesc, err := s.Add(ctx, name, mediaType, path)
if err != nil {
t.Fatal("Store.Add() error =", err)
}
if descriptor.FromOCI(gotDesc) != descriptor.FromOCI(desc) {
t.Fatal("got descriptor mismatch")
}
// test blob exists
exists, err := s.Exists(ctx, gotDesc)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
// test blob fetch
rc, err := s.Fetch(ctx, gotDesc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, blob) {
t.Errorf("Store.Fetch() = %v, want %v", got, blob)
}
// test push config
config := []byte("{}")
configDesc := ocispec.Descriptor{
MediaType: "config",
Digest: digest.FromBytes(config),
Size: int64(len(config)),
Annotations: map[string]string{
ocispec.AnnotationTitle: "config.blob",
},
}
if err := s.Push(ctx, configDesc, bytes.NewReader(config)); err != nil {
t.Fatal("Store.Push() error =", err)
}
// test push manifest
manifest := ocispec.Manifest{
MediaType: ocispec.MediaTypeImageManifest,
Config: configDesc,
Layers: []ocispec.Descriptor{
gotDesc,
},
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal("json.Marshal() error =", err)
}
manifestDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(manifestJSON),
Size: int64(len(manifestJSON)),
}
if err := s.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON)); err != nil {
t.Fatal("Store.Push() error =", err)
}
// test tag
ref := "foobar"
if err := s.Tag(ctx, manifestDesc, ref); err != nil {
t.Fatal("Store.Tag() error =", err)
}
// test resolve
gotManifestDesc, err := s.Resolve(ctx, ref)
if err != nil {
t.Fatal("Store.Resolve() error =", err)
}
if !reflect.DeepEqual(gotManifestDesc, manifestDesc) {
t.Errorf("Store.Resolve() = %v, want %v", gotManifestDesc, manifestDesc)
}
// test fetch
exists, err = s.Exists(ctx, gotManifestDesc)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
mrc, err := s.Fetch(ctx, gotManifestDesc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got, err = io.ReadAll(mrc)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = mrc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, manifestJSON) {
t.Errorf("Store.Fetch() = %v, want %v", got, manifestJSON)
}
}
func TestStore_Close(t *testing.T) {
content := []byte("hello world")
name := "test.txt"
mediaType := "test"
desc := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
Annotations: map[string]string{
ocispec.AnnotationTitle: name,
},
}
ref := "foobar"
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
ctx := context.Background()
// test push
err = s.Push(ctx, desc, bytes.NewReader(content))
if err != nil {
t.Fatal("Store.Push() error =", err)
}
// test exists
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
// test fetch
rc, err := s.Fetch(ctx, desc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Store.Fetch() = %v, want %v", got, content)
}
// test close
if err := s.Close(); err != nil {
t.Error("Store.Close() error =", err)
}
// test close twice
if err := s.Close(); err != nil {
t.Error("Store.Close() error =", err)
}
// test add after closed
if _, err := s.Add(ctx, name, mediaType, ""); !errors.Is(err, ErrStoreClosed) {
t.Errorf("Store.Add() = %v, want %v", err, ErrStoreClosed)
}
// test push after closed
if err = s.Push(ctx, desc, bytes.NewReader(content)); !errors.Is(err, ErrStoreClosed) {
t.Errorf("Store.Push() = %v, want %v", err, ErrStoreClosed)
}
// test exists after closed
if _, err := s.Exists(ctx, desc); !errors.Is(err, ErrStoreClosed) {
t.Errorf("Store.Exists() = %v, want %v", err, ErrStoreClosed)
}
// test tag after closed
if err := s.Tag(ctx, desc, ref); !errors.Is(err, ErrStoreClosed) {
t.Errorf("Store.Tag() = %v, want %v", err, ErrStoreClosed)
}
// test resolve after closed
if _, err := s.Resolve(ctx, ref); !errors.Is(err, ErrStoreClosed) {
t.Errorf("Store.Resolve() = %v, want %v", err, ErrStoreClosed)
}
// test fetch after closed
if _, err := s.Fetch(ctx, desc); !errors.Is(err, ErrStoreClosed) {
t.Errorf("Store.Fetch() = %v, want %v", err, ErrStoreClosed)
}
// test Predecessors after closed
if _, err := s.Predecessors(ctx, desc); !errors.Is(err, ErrStoreClosed) {
t.Errorf("Store.Predecessors() = %v, want %v", err, ErrStoreClosed)
}
}
func TestStore_File_Push(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
Annotations: map[string]string{
ocispec.AnnotationTitle: "test.txt",
},
}
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
ctx := context.Background()
// test push
err = s.Push(ctx, desc, bytes.NewReader(content))
if err != nil {
t.Fatal("Store.Push() error =", err)
}
// test exists
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
// test fetch
rc, err := s.Fetch(ctx, desc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Store.Fetch() = %v, want %v", got, content)
}
}
func TestStore_Dir_Push(t *testing.T) {
tempDir := t.TempDir()
dirName := "testdir"
dirPath := filepath.Join(tempDir, dirName)
if err := os.MkdirAll(dirPath, 0777); err != nil {
t.Fatal("error calling Mkdir(), error =", err)
}
content := []byte("hello world")
fileName := "test.txt"
if err := os.WriteFile(filepath.Join(dirPath, fileName), content, 0444); err != nil {
t.Fatal("error calling WriteFile(), error =", err)
}
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
ctx := context.Background()
// test add
desc, err := s.Add(ctx, dirName, "", dirPath)
if err != nil {
t.Fatal("Store.Add() error=", err)
}
val, ok := s.digestToPath.Load(desc.Digest)
if !ok {
t.Fatal("failed to find internal gz")
}
tmpPath := val.(string)
zrc, err := os.Open(tmpPath)
if err != nil {
t.Fatal("failed to open internal gz")
}
gz, err := io.ReadAll(zrc)
if err != nil {
t.Fatal("failed to read internal gz")
}
if err := zrc.Close(); err != nil {
t.Fatal("failed to close internal gz reader")
}
anotherTempDir := t.TempDir()
// test with another file store instance to mock push gz
anotherS, err := New(anotherTempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer anotherS.Close()
// test push
if err := anotherS.Push(ctx, desc, bytes.NewReader(gz)); err != nil {
t.Fatal("Store.Push() error =", err)
}
// test exists
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
// test fetch
rc, err := s.Fetch(ctx, desc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, gz) {
t.Errorf("Store.Fetch() = %v, want %v", got, gz)
}
// test file content
path := filepath.Join(s.workingDir, dirName, fileName)
fp, err := os.Open(path)
if err != nil {
t.Fatalf("failed to open file %s:%v", path, err)
}
fc, err := io.ReadAll(fp)
if err != nil {
t.Fatalf("failed to read file %s:%v", path, err)
}
if err := fp.Close(); err != nil {
t.Fatalf("failed to close file %s:%v", path, err)
}
anotherFilePath := filepath.Join(anotherS.workingDir, dirName, fileName)
anotherFp, err := os.Open(anotherFilePath)
if err != nil {
t.Fatalf("failed to open file %s:%v", anotherFilePath, err)
}
anotherFc, err := io.ReadAll(anotherFp)
if err != nil {
t.Fatalf("failed to read file %s:%v", anotherFilePath, err)
}
if err := anotherFp.Close(); err != nil {
t.Fatalf("failed to close file %s:%v", anotherFilePath, err)
}
if !bytes.Equal(fc, anotherFc) {
t.Errorf("file content mismatch")
}
}
func TestStore_Dir_Push_SkipUnpack(t *testing.T) {
// add a file to file store, and obtain its directory as gz
tempDir := t.TempDir()
dirName := "testdir"
dirPath := filepath.Join(tempDir, dirName)
if err := os.MkdirAll(dirPath, 0777); err != nil {
t.Fatal("error calling Mkdir(), error =", err)
}
content := []byte("hello world")
fileName := "test.txt"
if err := os.WriteFile(filepath.Join(dirPath, fileName), content, 0444); err != nil {
t.Fatal("error calling WriteFile(), error =", err)
}
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
ctx := context.Background()
desc, err := s.Add(ctx, dirName, "", dirPath)
if err != nil {
t.Fatal("Store.Add() error=", err)
}
val, ok := s.digestToPath.Load(desc.Digest)
if !ok {
t.Fatal("failed to find internal gz")
}
tmpPath := val.(string)
zrc, err := os.Open(tmpPath)
if err != nil {
t.Fatal("failed to open internal gz")
}
gz, err := io.ReadAll(zrc)
if err != nil {
t.Fatal("failed to read internal gz")
}
if err := zrc.Close(); err != nil {
t.Fatal("failed to close internal gz reader")
}
// push the gz to another store
anotherTempDir := t.TempDir()
anotherS, err := New(anotherTempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer anotherS.Close()
anotherS.SkipUnpack = true
gzPath := filepath.Join(anotherTempDir, dirName)
// push the gz to the store
if err := anotherS.Push(ctx, desc, bytes.NewReader(gz)); err != nil {
t.Fatal("Store.Push() error =", err)
}
pushedFile, err := os.Open(gzPath)
if err != nil {
t.Fatal("failed to open internal gz")
}
pushedContent, err := io.ReadAll(pushedFile)
if err != nil {
t.Fatal(err)
}
// check that the pushedContent is equal to the original gz, i.e. it is
// not unpacked
if !bytes.Equal(gz, pushedContent) {
t.Errorf("file content mismatch")
}
}
func TestStore_Push_NoName(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
ctx := context.Background()
// test push
err = s.Push(ctx, desc, bytes.NewReader(content))
if err != nil {
t.Fatal("Store.Push() error =", err)
}
// test exists
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
// test fetch
rc, err := s.Fetch(ctx, desc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Store.Fetch() = %v, want %v", got, content)
}
}
func TestStore_Push_NoName_ExceedLimit(t *testing.T) {
blob := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
tempDir := t.TempDir()
s, err := NewWithFallbackLimit(tempDir, 1)
if err != nil {
t.Fatal("Store.NewWithFallbackLimit() error =", err)
}
defer s.Close()
ctx := context.Background()
// test push
err = s.Push(ctx, desc, bytes.NewReader(blob))
if !errors.Is(err, errdef.ErrSizeExceedsLimit) {
t.Errorf("Store.Push() error = %v, want %v", err, errdef.ErrSizeExceedsLimit)
}
}
func TestStore_Push_NoName_SizeNotMatch(t *testing.T) {
blob := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(blob),
Size: 1,
}
tempDir := t.TempDir()
s, err := NewWithFallbackLimit(tempDir, 1)
if err != nil {
t.Fatal("Store.NewWithFallbackLimit() error =", err)
}
defer s.Close()
ctx := context.Background()
// test push
err = s.Push(ctx, desc, bytes.NewReader(blob))
if err == nil {
t.Errorf("Store.Push() error = nil, want: error")
}
}
func TestStore_File_NotFound(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
Annotations: map[string]string{
ocispec.AnnotationTitle: "test.txt",
},
}
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
ctx := context.Background()
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Error("Store.Exists() error =", err)
}
if exists {
t.Errorf("Store.Exists() = %v, want %v", exists, false)
}
_, err = s.Fetch(ctx, desc)
if !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("Store.Fetch() error = %v, want %v", err, errdef.ErrNotFound)
}
}
func TestStore_File_ContentBadPush(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
Annotations: map[string]string{
ocispec.AnnotationTitle: "test.txt",
},
}
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
ctx := context.Background()
err = s.Push(ctx, desc, strings.NewReader("foobar"))
if err == nil {
t.Errorf("Store.Push() error = %v, wantErr %v", err, true)
}
}
func TestStore_File_Add(t *testing.T) {
content := []byte("hello world")
name := "test.txt"
mediaType := "test"
desc := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
Annotations: map[string]string{
ocispec.AnnotationTitle: name,
},
}
tempDir := t.TempDir()
path := filepath.Join(tempDir, name)
if err := os.WriteFile(path, content, 0444); err != nil {
t.Fatal("error calling WriteFile(), error =", err)
}
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
ctx := context.Background()
// test add
gotDesc, err := s.Add(ctx, name, mediaType, path)
if err != nil {
t.Fatal("Store.Add() error =", err)
}
if descriptor.FromOCI(gotDesc) != descriptor.FromOCI(desc) {
t.Fatal("got descriptor mismatch")
}
// test exists
exists, err := s.Exists(ctx, gotDesc)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
// test fetch
rc, err := s.Fetch(ctx, gotDesc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Store.Fetch() = %v, want %v", got, content)
}
}
func TestStore_Dir_Add(t *testing.T) {
tempDir := t.TempDir()
dirName := "testdir"
dirPath := filepath.Join(tempDir, dirName)
if err := os.MkdirAll(dirPath, 0777); err != nil {
t.Fatal("error calling Mkdir(), error =", err)
}
content := []byte("hello world")
if err := os.WriteFile(filepath.Join(dirPath, "test.txt"), content, 0444); err != nil {
t.Fatal("error calling WriteFile(), error =", err)
}
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
ctx := context.Background()
// test add
gotDesc, err := s.Add(ctx, dirName, "", dirPath)
if err != nil {
t.Fatal("Store.Add() error=", err)
}
// test exists
exists, err := s.Exists(ctx, gotDesc)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
val, ok := s.digestToPath.Load(gotDesc.Digest)
if !ok {
t.Fatal("failed to find internal gz")
}
tmpPath := val.(string)
zrc, err := os.Open(tmpPath)
if err != nil {
t.Fatal("failed to open internal gz")
}
gotgz, err := io.ReadAll(zrc)
if err != nil {
t.Fatal("failed to read internal gz")
}
// test fetch
rc, err := s.Fetch(ctx, gotDesc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, gotgz) {
t.Errorf("Store.Fetch() = %v, want %v", got, gotgz)
}
}
func TestStore_File_SameContent_DuplicateName(t *testing.T) {
content := []byte("hello world")
name := "test.txt"
mediaType := "test"
desc := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
Annotations: map[string]string{
ocispec.AnnotationTitle: name,
},
}
tempDir := t.TempDir()
path := filepath.Join(tempDir, name)
if err := os.WriteFile(path, content, 0444); err != nil {
t.Fatal("error calling WriteFile(), error =", err)
}
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
ctx := context.Background()
// test add
gotDesc, err := s.Add(ctx, name, mediaType, path)
if err != nil {
t.Fatal("Store.Add() error =", err)
}
if descriptor.FromOCI(gotDesc) != descriptor.FromOCI(desc) {
t.Fatal("got descriptor mismatch")
}
// test exists
exists, err := s.Exists(ctx, gotDesc)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
// test fetch
rc, err := s.Fetch(ctx, gotDesc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Store.Fetch() = %v, want %v", got, content)
}
// test duplicate name
if _, err := s.Add(ctx, name, mediaType, path); !errors.Is(err, ErrDuplicateName) {
t.Errorf("Store.Add() = %v, want %v", err, ErrDuplicateName)
}
}
func TestStore_File_DifferentContent_DuplicateName(t *testing.T) {
content_1 := []byte("hello world")
content_2 := []byte("goodbye world")
name_1 := "test_1.txt"
name_2 := "test_2.txt"
mediaType_1 := "test"
mediaType_2 := "test_2"
desc_1 := ocispec.Descriptor{
MediaType: mediaType_1,
Digest: digest.FromBytes(content_1),
Size: int64(len(content_1)),
Annotations: map[string]string{
ocispec.AnnotationTitle: name_1,
},
}
tempDir := t.TempDir()
path_1 := filepath.Join(tempDir, name_1)
if err := os.WriteFile(path_1, content_1, 0444); err != nil {
t.Fatal("error calling WriteFile(), error =", err)
}
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
ctx := context.Background()
// test add
gotDesc, err := s.Add(ctx, name_1, mediaType_1, path_1)
if err != nil {
t.Fatal("Store.Add() error =", err)
}
if descriptor.FromOCI(gotDesc) != descriptor.FromOCI(desc_1) {
t.Fatal("got descriptor mismatch")
}
// test exists
exists, err := s.Exists(ctx, gotDesc)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
// test fetch
rc, err := s.Fetch(ctx, gotDesc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content_1) {
t.Errorf("Store.Fetch() = %v, want %v", got, content_1)
}
// test add duplicate name
path_2 := filepath.Join(tempDir, name_2)
if err := os.WriteFile(path_2, content_2, 0444); err != nil {
t.Fatal("error calling WriteFile(), error =", err)
}
if _, err := s.Add(ctx, name_1, mediaType_2, path_2); !errors.Is(err, ErrDuplicateName) {
t.Errorf("Store.Add() = %v, want %v", err, ErrDuplicateName)
}
}
func TestStore_File_Add_MissingName(t *testing.T) {
content := []byte("hello world")
name := "test.txt"
mediaType := "test"
tempDir := t.TempDir()
path := filepath.Join(tempDir, name)
if err := os.WriteFile(path, content, 0444); err != nil {
t.Fatal("error calling WriteFile(), error =", err)
}
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
ctx := context.Background()
// test add with empty name
_, err = s.Add(ctx, "", mediaType, path)
if !errors.Is(err, ErrMissingName) {
t.Errorf("Store.Add() error = %v, want %v", err, ErrMissingName)
}
}
func TestStore_File_Add_SameContent(t *testing.T) {
mediaType := "test"
content := []byte("hello world")
name_1 := "test_1.txt"
desc_1 := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
Annotations: map[string]string{
ocispec.AnnotationTitle: name_1,
},
}
name_2 := "test_2.txt"
desc_2 := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
Annotations: map[string]string{
ocispec.AnnotationTitle: name_2,
},
}
tempDir := t.TempDir()
path_1 := filepath.Join(tempDir, name_1)
if err := os.WriteFile(path_1, content, 0444); err != nil {
t.Fatal("error calling WriteFile(), error =", err)
}
path_2 := filepath.Join(tempDir, name_2)
if err := os.WriteFile(path_2, content, 0444); err != nil {
t.Fatal("error calling WriteFile(), error =", err)
}
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
ctx := context.Background()
// test add
gotDesc_1, err := s.Add(ctx, name_1, mediaType, path_1)
if err != nil {
t.Fatal("Store.Add() error =", err)
}
if descriptor.FromOCI(gotDesc_1) != descriptor.FromOCI(desc_1) {
t.Fatal("got descriptor mismatch")
}
gotDesc_2, err := s.Add(ctx, name_2, mediaType, path_2)
if err != nil {
t.Fatal("Store.Add() error =", err)
}
if descriptor.FromOCI(gotDesc_2) != descriptor.FromOCI(desc_2) {
t.Fatal("got descriptor mismatch")
}
// test exists
exists, err := s.Exists(ctx, gotDesc_1)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
exists, err = s.Exists(ctx, gotDesc_2)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
// test fetch
rc_1, err := s.Fetch(ctx, gotDesc_1)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got_1, err := io.ReadAll(rc_1)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc_1.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got_1, content) {
t.Errorf("Store.Fetch() = %v, want %v", got_1, content)
}
rc_2, err := s.Fetch(ctx, gotDesc_2)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got_2, err := io.ReadAll(rc_2)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc_2.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got_2, content) {
t.Errorf("Store.Fetch() = %v, want %v", got_2, content)
}
}
func TestStore_File_Push_SameContent(t *testing.T) {
mediaType := "test"
content := []byte("hello world")
name_1 := "test_1.txt"
desc_1 := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
Annotations: map[string]string{
ocispec.AnnotationTitle: name_1,
},
}
name_2 := "test_2.txt"
desc_2 := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
Annotations: map[string]string{
ocispec.AnnotationTitle: name_2,
},
}
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
ctx := context.Background()
// test push
if err := s.Push(ctx, desc_1, bytes.NewReader(content)); err != nil {
t.Fatal("Store.Push() error =", err)
}
if err := s.Push(ctx, desc_2, bytes.NewReader(content)); err != nil {
t.Fatal("Store.Push() error =", err)
}
// test exists
exists, err := s.Exists(ctx, desc_1)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
exists, err = s.Exists(ctx, desc_2)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
// test fetch
rc_1, err := s.Fetch(ctx, desc_1)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got_1, err := io.ReadAll(rc_1)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc_1.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got_1, content) {
t.Errorf("Store.Fetch() = %v, want %v", got_1, content)
}
rc_2, err := s.Fetch(ctx, desc_2)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got_2, err := io.ReadAll(rc_2)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc_2.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got_2, content) {
t.Errorf("Store.Fetch() = %v, want %v", got_2, content)
}
}
func TestStore_File_Push_DuplicateName(t *testing.T) {
mediaType := "test"
name := "test.txt"
content_1 := []byte("hello world")
content_2 := []byte("goodbye world")
desc_1 := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(content_1),
Size: int64(len(content_1)),
Annotations: map[string]string{
ocispec.AnnotationTitle: name,
},
}
desc_2 := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(content_2),
Size: int64(len(content_2)),
Annotations: map[string]string{
ocispec.AnnotationTitle: name,
},
}
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
ctx := context.Background()
// test push
err = s.Push(ctx, desc_1, bytes.NewReader(content_1))
if err != nil {
t.Fatal("Store.Push() error =", err)
}
// test exists
exists, err := s.Exists(ctx, desc_1)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
// test fetch
rc, err := s.Fetch(ctx, desc_1)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content_1) {
t.Errorf("Store.Fetch() = %v, want %v", got, content_1)
}
// test push with duplicate name
err = s.Push(ctx, desc_2, bytes.NewBuffer(content_2))
if !errors.Is(err, ErrDuplicateName) {
t.Errorf("Store.Push() error = %v, want %v", err, ErrDuplicateName)
}
}
func TestStore_File_Push_ForceCAS(t *testing.T) {
mediaType := "test"
content := []byte("hello world")
desc1 := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
Annotations: map[string]string{
ocispec.AnnotationTitle: "blob1",
},
}
desc2 := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
Annotations: map[string]string{
ocispec.AnnotationTitle: "blob2",
},
}
config := []byte("{}")
configDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageConfig,
Digest: digest.FromBytes(config),
Size: int64(len(config)),
}
manifest := ocispec.Manifest{
MediaType: ocispec.MediaTypeImageManifest,
Config: configDesc,
Layers: []ocispec.Descriptor{desc1, desc2},
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal("json.Marshal() error =", err)
}
manifestDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(manifestJSON),
Size: int64(len(manifestJSON)),
}
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
s.ForceCAS = true
defer s.Close()
ctx := context.Background()
// push blob1
if err := s.Push(ctx, desc1, bytes.NewReader(content)); err != nil {
t.Fatal("Store.Push() error =", err)
}
// push manifest
if err := s.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON)); err != nil {
t.Fatal("Store.Push() error =", err)
}
// verify blob2 not exists
exists, err := s.Exists(ctx, desc2)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if exists {
t.Error("Blob2 is restored")
}
}
func TestStore_File_Push_RestoreDuplicates(t *testing.T) {
mediaType := "test"
content := []byte("hello world")
desc1 := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
Annotations: map[string]string{
ocispec.AnnotationTitle: "blob1",
},
}
desc2 := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
Annotations: map[string]string{
ocispec.AnnotationTitle: "blob2",
},
}
config := []byte("{}")
configDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageConfig,
Digest: digest.FromBytes(config),
Size: int64(len(config)),
}
manifest := ocispec.Manifest{
MediaType: ocispec.MediaTypeImageManifest,
Config: configDesc,
Layers: []ocispec.Descriptor{desc1, desc2},
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal("json.Marshal() error =", err)
}
manifestDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(manifestJSON),
Size: int64(len(manifestJSON)),
}
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
ctx := context.Background()
// push blob1
if err := s.Push(ctx, desc1, bytes.NewReader(content)); err != nil {
t.Fatal("Store.Push() error =", err)
}
// push manifest
if err := s.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON)); err != nil {
t.Fatal("Store.Push() error =", err)
}
// verify blob2 is restored
exists, err := s.Exists(ctx, desc2)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Error("Blob2 is not restored")
}
rc, err := s.Fetch(ctx, desc2)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Store.Fetch() = %v, want %v", got, content)
}
}
func TestStore_File_Push_RestoreDuplicates_NotFound(t *testing.T) {
mediaType := "test"
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
Annotations: map[string]string{
ocispec.AnnotationTitle: "blob1",
},
}
config := []byte("{}")
configDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageConfig,
Digest: digest.FromBytes(config),
Size: int64(len(config)),
}
manifest := ocispec.Manifest{
MediaType: ocispec.MediaTypeImageManifest,
Config: configDesc,
Layers: []ocispec.Descriptor{desc},
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal("json.Marshal() error =", err)
}
manifestDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(manifestJSON),
Size: int64(len(manifestJSON)),
}
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
ctx := context.Background()
// push manifest before blob is fine
if err := s.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON)); err != nil {
t.Error("Store.Push(): error = ", err)
}
}
type storageMock struct {
content.Storage
OnFetch func(ctx context.Context, desc ocispec.Descriptor) error
}
func (m *storageMock) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error) {
if m.OnFetch != nil {
if err := m.OnFetch(ctx, desc); err != nil {
return nil, err
}
}
return m.Storage.Fetch(ctx, desc)
}
func TestStore_File_Push_RestoreDuplicates_DuplicateName(t *testing.T) {
mediaType := "test"
content := []byte("hello world")
blobDesc := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
Annotations: map[string]string{
ocispec.AnnotationTitle: "blob",
},
}
config := []byte("{}")
configDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageConfig,
Digest: digest.FromBytes(config),
Size: int64(len(config)),
}
manifest := ocispec.Manifest{
MediaType: ocispec.MediaTypeImageManifest,
Config: configDesc,
Layers: []ocispec.Descriptor{blobDesc},
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal("json.Marshal() error =", err)
}
manifestDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(manifestJSON),
Size: int64(len(manifestJSON)),
}
tempDir := t.TempDir()
fallbackMock := &storageMock{
Storage: cas.NewMemory(),
}
s, err := NewWithFallbackStorage(tempDir, fallbackMock)
if err != nil {
t.Fatal("NewWithFallbackStorage() error =", err)
}
defer s.Close()
ctx := context.Background()
// push blob as unnamed
if err := fallbackMock.Push(ctx, blobDesc, bytes.NewReader(content)); err != nil {
t.Fatal("Store.Push() error =", err)
}
// push manifest
fallbackMock.OnFetch = func(ctx context.Context, desc ocispec.Descriptor) error {
if desc.Digest == blobDesc.Digest {
// push blob before being restored by manifest put to simulate
// concurrent pushing for race condition
if err := s.Push(ctx, blobDesc, bytes.NewReader(content)); err != nil {
t.Fatal("Store.Push() error =", err)
}
}
return nil
}
if err := s.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON)); err != nil {
t.Fatal("Store.Push() error =", err)
}
// verify blob is restored
got, err := os.ReadFile(filepath.Join(tempDir, "blob"))
if err != nil {
t.Fatal("os.ReadFile() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("os.ReadFile() = %v, want %v", got, content)
}
}
func TestStore_File_Push_RestoreDuplicates_Failure(t *testing.T) {
mediaType := "test"
content := []byte("hello world")
blobDesc := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
Annotations: map[string]string{
ocispec.AnnotationTitle: "blob",
},
}
config := []byte("{}")
configDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageConfig,
Digest: digest.FromBytes(config),
Size: int64(len(config)),
}
manifest := ocispec.Manifest{
MediaType: ocispec.MediaTypeImageManifest,
Config: configDesc,
Layers: []ocispec.Descriptor{blobDesc},
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal("json.Marshal() error =", err)
}
manifestDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(manifestJSON),
Size: int64(len(manifestJSON)),
}
tempDir := t.TempDir()
fallbackMock := &storageMock{
Storage: cas.NewMemory(),
}
s, err := NewWithFallbackStorage(tempDir, fallbackMock)
if err != nil {
t.Fatal("NewWithFallbackStorage() error =", err)
}
defer s.Close()
ctx := context.Background()
// push manifest
wantErr := errors.New("restoreDuplicates: fetch error")
fallbackMock.OnFetch = func(ctx context.Context, desc ocispec.Descriptor) error {
if desc.Digest == blobDesc.Digest {
return wantErr
}
return nil
}
if err := s.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON)); !errors.Is(err, wantErr) {
t.Fatalf("Store.Push() error = %v, wantErr %v", err, wantErr)
}
}
func TestStore_File_Fetch_SameDigest_NoName(t *testing.T) {
mediaType := "test"
content := []byte("hello world")
name_1 := "test_1.txt"
desc_1 := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
Annotations: map[string]string{
ocispec.AnnotationTitle: name_1,
},
}
desc_2 := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
ctx := context.Background()
// test push
if err := s.Push(ctx, desc_1, bytes.NewReader(content)); err != nil {
t.Fatal("Store.Push() error =", err)
}
if err := s.Push(ctx, desc_2, bytes.NewReader(content)); err != nil {
t.Fatal("Store.Push() error =", err)
}
// test exists
exists, err := s.Exists(ctx, desc_1)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
exists, err = s.Exists(ctx, desc_2)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
// test fetch
rc_1, err := s.Fetch(ctx, desc_1)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got_1, err := io.ReadAll(rc_1)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc_1.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got_1, content) {
t.Errorf("Store.Fetch() = %v, want %v", got_1, content)
}
rc_2, err := s.Fetch(ctx, desc_2)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got_2, err := io.ReadAll(rc_2)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc_2.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got_2, content) {
t.Errorf("Store.Fetch() = %v, want %v", got_2, content)
}
}
func TestStore_File_Fetch_SameDigest_DifferentName(t *testing.T) {
mediaType := "test"
content := []byte("hello world")
name_1 := "test_1.txt"
desc_1 := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
Annotations: map[string]string{
ocispec.AnnotationTitle: name_1,
},
}
name_2 := "test_2.txt"
desc_2 := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
Annotations: map[string]string{
ocispec.AnnotationTitle: name_2,
},
}
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
ctx := context.Background()
// test desc_1
if err := s.Push(ctx, desc_1, bytes.NewReader(content)); err != nil {
t.Fatal("Store.Push() error =", err)
}
exists, err := s.Exists(ctx, desc_1)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
rc_1, err := s.Fetch(ctx, desc_1)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got_1, err := io.ReadAll(rc_1)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc_1.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got_1, content) {
t.Errorf("Store.Fetch() = %v, want %v", got_1, content)
}
// test desc_2
exists, err = s.Exists(ctx, desc_2)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if exists {
t.Errorf("Store.Exists() = %v, want %v", exists, false)
}
_, err = s.Fetch(ctx, desc_2)
if !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("Store.Fetch() error = %v, want %v", err, errdef.ErrNotFound)
}
}
func TestStore_File_Push_Overwrite(t *testing.T) {
mediaType := "test"
name := "test.txt"
old_content := []byte("hello world")
new_content := []byte("goodbye world")
desc := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(new_content),
Size: int64(len(new_content)),
Annotations: map[string]string{
ocispec.AnnotationTitle: name,
},
}
tempDir := t.TempDir()
path := filepath.Join(tempDir, name)
if err := os.WriteFile(path, old_content, 0666); err != nil {
t.Fatal("error calling WriteFile(), error =", err)
}
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
ctx := context.Background()
// test push
err = s.Push(ctx, desc, bytes.NewReader(new_content))
if err != nil {
t.Fatal("Store.Push() error =", err)
}
// test exists
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
// test fetch
rc, err := s.Fetch(ctx, desc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, new_content) {
t.Errorf("Store.Fetch() = %v, want %v", got, new_content)
}
}
func TestStore_File_Push_DisableOverwrite(t *testing.T) {
content := []byte("hello world")
name := "test.txt"
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
Annotations: map[string]string{
ocispec.AnnotationTitle: name,
},
}
tempDir := t.TempDir()
path := filepath.Join(tempDir, name)
if err := os.WriteFile(path, content, 0444); err != nil {
t.Fatal("error calling WriteFile(), error =", err)
}
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
s.DisableOverwrite = true
ctx := context.Background()
err = s.Push(ctx, desc, bytes.NewReader(content))
if !errors.Is(err, ErrOverwriteDisallowed) {
t.Errorf("Store.Push() error = %v, want %v", err, ErrOverwriteDisallowed)
}
}
func TestStore_File_Push_IgnoreNoName(t *testing.T) {
config := []byte("{}")
configDesc := ocispec.Descriptor{
MediaType: "config",
Digest: digest.FromBytes(config),
Size: int64(len(config)),
}
manifest := ocispec.Manifest{
MediaType: ocispec.MediaTypeImageManifest,
Config: configDesc,
Layers: []ocispec.Descriptor{},
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal("json.Marshal() error =", err)
}
manifestDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(manifestJSON),
Size: int64(len(manifestJSON)),
}
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
s.IgnoreNoName = true
// push an OCI manifest
ctx := context.Background()
err = s.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON))
if err != nil {
t.Fatal("Store.Push() error = ", err)
}
// verify the manifest is not saved
exists, err := s.Exists(ctx, manifestDesc)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if exists {
t.Errorf("Unnamed manifest is saved in file store")
}
// verify the manifest is not indexed
predecessors, err := s.Predecessors(ctx, configDesc)
if err != nil {
t.Fatal("Store.Predecessors() error = ", err)
}
if len(predecessors) != 0 {
t.Errorf("Unnamed manifest is indexed in file store")
}
}
func TestStore_File_Push_DisallowPathTraversal(t *testing.T) {
content := []byte("hello world")
name := "../test.txt"
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
Annotations: map[string]string{
ocispec.AnnotationTitle: name,
},
}
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
ctx := context.Background()
err = s.Push(ctx, desc, bytes.NewReader(content))
if !errors.Is(err, ErrPathTraversalDisallowed) {
t.Errorf("Store.Push() error = %v, want %v", err, ErrPathTraversalDisallowed)
}
}
func TestStore_Dir_Push_DisallowPathTraversal(t *testing.T) {
tempDir := t.TempDir()
dirName := "../testdir"
dirPath := filepath.Join(tempDir, dirName)
if err := os.MkdirAll(dirPath, 0777); err != nil {
t.Fatal("error calling Mkdir(), error =", err)
}
content := []byte("hello world")
fileName := "test.txt"
if err := os.WriteFile(filepath.Join(dirPath, fileName), content, 0444); err != nil {
t.Fatal("error calling WriteFile(), error =", err)
}
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
ctx := context.Background()
// test add
desc, err := s.Add(ctx, dirName, "", dirPath)
if err != nil {
t.Fatal("Store.Add() error=", err)
}
val, ok := s.digestToPath.Load(desc.Digest)
if !ok {
t.Fatal("failed to find internal gz")
}
tmpPath := val.(string)
zrc, err := os.Open(tmpPath)
if err != nil {
t.Fatal("failed to open internal gz")
}
gz, err := io.ReadAll(zrc)
if err != nil {
t.Fatal("failed to read internal gz")
}
if err := zrc.Close(); err != nil {
t.Fatal("failed to close internal gz reader")
}
anotherTempDir := t.TempDir()
// test with another file store instance to mock push gz
anotherS, err := New(anotherTempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer anotherS.Close()
// test push
err = anotherS.Push(ctx, desc, bytes.NewReader(gz))
if !errors.Is(err, ErrPathTraversalDisallowed) {
t.Errorf("Store.Push() error = %v, want %v", err, ErrPathTraversalDisallowed)
}
}
func TestStore_File_Push_PathTraversal(t *testing.T) {
content := []byte("hello world")
name := "../test.txt"
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
Annotations: map[string]string{
ocispec.AnnotationTitle: name,
},
}
tempDir := t.TempDir()
subTempDir, err := os.MkdirTemp(tempDir, "oras_filestore_*")
if err != nil {
t.Fatal("error creating temp dir, error =", err)
}
s, err := New(subTempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
s.AllowPathTraversalOnWrite = true
ctx := context.Background()
// test push
err = s.Push(ctx, desc, bytes.NewReader(content))
if err != nil {
t.Fatal("Store.Push() error =", err)
}
// test exists
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
// test fetch
rc, err := s.Fetch(ctx, desc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Store.Fetch() = %v, want %v", got, content)
}
}
func TestStore_File_Push_Concurrent(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
Annotations: map[string]string{
ocispec.AnnotationTitle: "test.txt",
},
}
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
ctx := context.Background()
concurrency := 64
eg, egCtx := errgroup.WithContext(ctx)
for i := 0; i < concurrency; i++ {
eg.Go(func(i int) func() error {
return func() error {
if err := s.Push(egCtx, desc, bytes.NewReader(content)); err != nil {
if errors.Is(err, ErrDuplicateName) {
return nil
}
return fmt.Errorf("failed to push content: %v", err)
}
return nil
}
}(i))
}
if err := eg.Wait(); err != nil {
t.Fatal(err)
}
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
rc, err := s.Fetch(ctx, desc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Store.Fetch() = %v, want %v", got, content)
}
}
func TestStore_File_Fetch_Concurrent(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
Annotations: map[string]string{
ocispec.AnnotationTitle: "test.txt",
},
}
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
ctx := context.Background()
if err := s.Push(ctx, desc, bytes.NewReader(content)); err != nil {
t.Fatal("Store.Push() error =", err)
}
concurrency := 64
eg, egCtx := errgroup.WithContext(ctx)
for i := 0; i < concurrency; i++ {
eg.Go(func(i int) func() error {
return func() error {
rc, err := s.Fetch(egCtx, desc)
if err != nil {
return fmt.Errorf("failed to fetch content: %v", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Store.Fetch() = %v, want %v", got, content)
}
return nil
}
}(i))
}
if err := eg.Wait(); err != nil {
t.Fatal(err)
}
}
func TestStore_TagNotFound(t *testing.T) {
ref := "foobar"
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
ctx := context.Background()
_, err = s.Resolve(ctx, ref)
if !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("Store.Resolve() error = %v, want %v", err, errdef.ErrNotFound)
}
}
func TestStore_TagUnknownContent(t *testing.T) {
content := []byte(`{"layers":[]}`)
desc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
ref := "foobar"
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
ctx := context.Background()
err = s.Tag(ctx, desc, ref)
if !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("Store.Resolve() error = %v, want %v", err, errdef.ErrNotFound)
}
}
func TestStore_RepeatTag(t *testing.T) {
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
ctx := context.Background()
generate := func(content []byte) ocispec.Descriptor {
dgst := digest.FromBytes(content)
desc := ocispec.Descriptor{
MediaType: "test",
Digest: dgst,
Size: int64(len(content)),
Annotations: map[string]string{
ocispec.AnnotationTitle: dgst.Encoded() + ".blob",
},
}
return desc
}
ref := "foobar"
// initial tag
content := []byte("hello world")
desc := generate(content)
err = s.Push(ctx, desc, bytes.NewReader(content))
if err != nil {
t.Fatal("Store.Push() error =", err)
}
err = s.Tag(ctx, desc, ref)
if err != nil {
t.Fatal("Store.Tag() error =", err)
}
gotDesc, err := s.Resolve(ctx, ref)
if err != nil {
t.Fatal("Store.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, desc) {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, desc)
}
// repeat tag
content = []byte("foo")
desc = generate(content)
err = s.Push(ctx, desc, bytes.NewReader(content))
if err != nil {
t.Fatal("Store.Push() error =", err)
}
err = s.Tag(ctx, desc, ref)
if err != nil {
t.Fatal("Store.Tag() error =", err)
}
gotDesc, err = s.Resolve(ctx, ref)
if err != nil {
t.Fatal("Store.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, desc) {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, desc)
}
// repeat tag
content = []byte("bar")
desc = generate(content)
err = s.Push(ctx, desc, bytes.NewReader(content))
if err != nil {
t.Fatal("Store.Push() error =", err)
}
err = s.Tag(ctx, desc, ref)
if err != nil {
t.Fatal("Store.Tag() error =", err)
}
gotDesc, err = s.Resolve(ctx, ref)
if err != nil {
t.Fatal("Store.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, desc) {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, desc)
}
}
func TestStore_Predecessors(t *testing.T) {
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer s.Close()
ctx := context.Background()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
dgst := digest.FromBytes(blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: dgst,
Size: int64(len(blob)),
Annotations: map[string]string{
ocispec.AnnotationTitle: dgst.Encoded() + ".blob",
},
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
generateArtifactManifest := func(subject ocispec.Descriptor, blobs ...ocispec.Descriptor) {
var manifest spec.Artifact
manifest.Subject = &subject
manifest.Blobs = append(manifest.Blobs, blobs...)
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(spec.MediaTypeArtifactManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 3
generateManifest(descs[0], descs[1:3]...) // Blob 4
generateManifest(descs[0], descs[3]) // Blob 5
generateManifest(descs[0], descs[1:4]...) // Blob 6
generateIndex(descs[4:6]...) // Blob 7
generateIndex(descs[6]) // Blob 8
generateIndex() // Blob 9
generateIndex(descs[7:10]...) // Blob 10
appendBlob(ocispec.MediaTypeImageLayer, []byte("sig_1")) // Blob 11
generateArtifactManifest(descs[6], descs[11]) // Blob 12
appendBlob(ocispec.MediaTypeImageLayer, []byte("sig_2")) // Blob 13
generateArtifactManifest(descs[10], descs[13]) // Blob 14
eg, egCtx := errgroup.WithContext(ctx)
for i := range blobs {
eg.Go(func(i int) func() error {
return func() error {
err := s.Push(egCtx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
return fmt.Errorf("failed to push test content to src: %d: %v", i, err)
}
return nil
}
}(i))
}
if err := eg.Wait(); err != nil {
t.Fatal(err)
}
// verify predecessors
wants := [][]ocispec.Descriptor{
descs[4:7], // Blob 0
{descs[4], descs[6]}, // Blob 1
{descs[4], descs[6]}, // Blob 2
{descs[5], descs[6]}, // Blob 3
{descs[7]}, // Blob 4
{descs[7]}, // Blob 5
{descs[8], descs[12]}, // Blob 6
{descs[10]}, // Blob 7
{descs[10]}, // Blob 8
{descs[10]}, // Blob 9
{descs[14]}, // Blob 10
{descs[12]}, // Blob 11
nil, // Blob 12
{descs[14]}, // Blob 13
nil, // Blob 14
}
for i, want := range wants {
predecessors, err := s.Predecessors(ctx, descs[i])
if err != nil {
t.Errorf("Store.Predecessors(%d) error = %v", i, err)
}
if !equalDescriptorSet(predecessors, want) {
t.Errorf("Store.Predecessors(%d) = %v, want %v", i, predecessors, want)
}
}
}
func TestCopy_File_MemoryToFile_FullCopy(t *testing.T) {
src := memory.New()
tempDir := t.TempDir()
dst, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer dst.Close()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
dgst := digest.FromBytes(blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: dgst,
Size: int64(len(blob)),
Annotations: map[string]string{
ocispec.AnnotationTitle: dgst.Encoded() + ".blob",
},
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
generateManifest(descs[0], descs[1:3]...) // Blob 3
ctx := context.Background()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
root := descs[3]
ref := "foobar"
err = src.Tag(ctx, root, ref)
if err != nil {
t.Fatal("fail to tag root node", err)
}
// test copy
gotDesc, err := oras.Copy(ctx, src, ref, dst, "", oras.CopyOptions{})
if err != nil {
t.Fatalf("Copy() error = %v, wantErr %v", err, false)
}
if !reflect.DeepEqual(gotDesc, root) {
t.Errorf("Copy() = %v, want %v", gotDesc, root)
}
// verify contents
for i, desc := range descs {
exists, err := dst.Exists(ctx, desc)
if err != nil {
t.Fatalf("dst.Exists(%d) error = %v", i, err)
}
if !exists {
t.Errorf("dst.Exists(%d) = %v, want %v", i, exists, true)
}
}
// verify tag
gotDesc, err = dst.Resolve(ctx, ref)
if err != nil {
t.Fatal("dst.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, root) {
t.Errorf("dst.Resolve() = %v, want %v", gotDesc, root)
}
}
func TestCopyGraph_MemoryToFile_FullCopy(t *testing.T) {
src := memory.New()
tempDir := t.TempDir()
dst, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer dst.Close()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
dgst := digest.FromBytes(blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: dgst,
Size: int64(len(blob)),
Annotations: map[string]string{
ocispec.AnnotationTitle: dgst.Encoded() + ".blob",
},
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 3
generateManifest(descs[0], descs[1:3]...) // Blob 4
generateManifest(descs[0], descs[3]) // Blob 5
generateManifest(descs[0], descs[1:4]...) // Blob 6
generateIndex(descs[4:6]...) // Blob 7
generateIndex(descs[6]) // Blob 8
generateIndex() // Blob 9
generateIndex(descs[7:10]...) // Blob 10
ctx := context.Background()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// test copy
srcTracker := &storageTracker{Storage: src}
dstTracker := &storageTracker{Storage: dst}
root := descs[len(descs)-1]
if err := oras.CopyGraph(ctx, srcTracker, dstTracker, root, oras.CopyGraphOptions{}); err != nil {
t.Fatalf("CopyGraph() error = %v, wantErr %v", err, false)
}
// verify contents
for i := range blobs {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, false)
continue
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Errorf("content[%d] = %v, want %v", i, got, want)
}
}
// verify API counts
if got, want := srcTracker.fetch, int64(len(blobs)); got != want {
t.Errorf("count(src.Fetch()) = %v, want %v", got, want)
}
if got, want := srcTracker.push, int64(0); got != want {
t.Errorf("count(src.Push()) = %v, want %v", got, want)
}
if got, want := srcTracker.exists, int64(0); got != want {
t.Errorf("count(src.Exists()) = %v, want %v", got, want)
}
if got, want := dstTracker.fetch, int64(0); got != want {
t.Errorf("count(dst.Fetch()) = %v, want %v", got, want)
}
if got, want := dstTracker.push, int64(len(blobs)); got != want {
t.Errorf("count(dst.Push()) = %v, want %v", got, want)
}
if got, want := dstTracker.exists, int64(len(blobs)); got != want {
t.Errorf("count(dst.Exists()) = %v, want %v", got, want)
}
}
func TestCopyGraph_MemoryToFile_PartialCopy(t *testing.T) {
src := memory.New()
tempDir := t.TempDir()
dst, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer dst.Close()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
dgst := digest.FromBytes(blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: dgst,
Size: int64(len(blob)),
Annotations: map[string]string{
ocispec.AnnotationTitle: dgst.Encoded() + ".blob",
},
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
generateManifest(descs[0], descs[1:3]...) // Blob 3
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 4
generateManifest(descs[0], descs[4]) // Blob 5
generateIndex(descs[3], descs[5]) // Blob 6
ctx := context.Background()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// initial copy
root := descs[3]
if err := oras.CopyGraph(ctx, src, dst, root, oras.CopyGraphOptions{}); err != nil {
t.Fatalf("CopyGraph() error = %v, wantErr %v", err, false)
}
// verify contents
for i := range blobs[:4] {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Fatalf("content[%d] error = %v, wantErr %v", i, err, false)
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Fatalf("content[%d] = %v, want %v", i, got, want)
}
}
// test copy
srcTracker := &storageTracker{Storage: src}
dstTracker := &storageTracker{Storage: dst}
root = descs[len(descs)-1]
if err := oras.CopyGraph(ctx, srcTracker, dstTracker, root, oras.CopyGraphOptions{}); err != nil {
t.Fatalf("CopyGraph() error = %v, wantErr %v", err, false)
}
// verify contents
for i := range blobs {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, false)
continue
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Errorf("content[%d] = %v, want %v", i, got, want)
}
}
// verify API counts
if got, want := srcTracker.fetch, int64(3); got != want {
t.Errorf("count(src.Fetch()) = %v, want %v", got, want)
}
if got, want := srcTracker.push, int64(0); got != want {
t.Errorf("count(src.Push()) = %v, want %v", got, want)
}
if got, want := srcTracker.exists, int64(0); got != want {
t.Errorf("count(src.Exists()) = %v, want %v", got, want)
}
if got, want := dstTracker.fetch, int64(0); got != want {
t.Errorf("count(dst.Fetch()) = %v, want %v", got, want)
}
if got, want := dstTracker.push, int64(3); got != want {
t.Errorf("count(dst.Push()) = %v, want %v", got, want)
}
if got, want := dstTracker.exists, int64(5); got != want {
t.Errorf("count(dst.Exists()) = %v, want %v", got, want)
}
}
func TestCopy_File_FileToMemory_FullCopy(t *testing.T) {
tempDir := t.TempDir()
src, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer src.Close()
dst := memory.New()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
dgst := digest.FromBytes(blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: dgst,
Size: int64(len(blob)),
Annotations: map[string]string{
ocispec.AnnotationTitle: dgst.Encoded() + ".blob",
},
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
generateManifest(descs[0], descs[1:3]...) // Blob 3
ctx := context.Background()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
root := descs[3]
ref := "foobar"
err = src.Tag(ctx, root, ref)
if err != nil {
t.Fatal("fail to tag root node", err)
}
// test copy
gotDesc, err := oras.Copy(ctx, src, ref, dst, "", oras.CopyOptions{})
if err != nil {
t.Fatalf("Copy() error = %v, wantErr %v", err, false)
}
if !reflect.DeepEqual(gotDesc, root) {
t.Errorf("Copy() = %v, want %v", gotDesc, root)
}
// verify contents
for i, desc := range descs {
exists, err := dst.Exists(ctx, desc)
if err != nil {
t.Fatalf("dst.Exists(%d) error = %v", i, err)
}
if !exists {
t.Errorf("dst.Exists(%d) = %v, want %v", i, exists, true)
}
}
// verify tag
gotDesc, err = dst.Resolve(ctx, ref)
if err != nil {
t.Fatal("dst.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, root) {
t.Errorf("dst.Resolve() = %v, want %v", gotDesc, root)
}
}
func TestCopyGraph_FileToMemory_FullCopy(t *testing.T) {
tempDir := t.TempDir()
src, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer src.Close()
dst := memory.New()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
dgst := digest.FromBytes(blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: dgst,
Size: int64(len(blob)),
Annotations: map[string]string{
ocispec.AnnotationTitle: dgst.Encoded() + ".blob",
},
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 3
generateManifest(descs[0], descs[1:3]...) // Blob 4
generateManifest(descs[0], descs[3]) // Blob 5
generateManifest(descs[0], descs[1:4]...) // Blob 6
generateIndex(descs[4:6]...) // Blob 7
generateIndex(descs[6]) // Blob 8
generateIndex() // Blob 9
generateIndex(descs[7:10]...) // Blob 10
ctx := context.Background()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// test copy
srcTracker := &storageTracker{Storage: src}
dstTracker := &storageTracker{Storage: dst}
root := descs[len(descs)-1]
if err := oras.CopyGraph(ctx, srcTracker, dstTracker, root, oras.CopyGraphOptions{}); err != nil {
t.Fatalf("CopyGraph() error = %v, wantErr %v", err, false)
}
// verify contents
for i := range blobs {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, false)
continue
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Errorf("content[%d] = %v, want %v", i, got, want)
}
}
// verify API counts
if got, want := srcTracker.fetch, int64(len(blobs)); got != want {
t.Errorf("count(src.Fetch()) = %v, want %v", got, want)
}
if got, want := srcTracker.push, int64(0); got != want {
t.Errorf("count(src.Push()) = %v, want %v", got, want)
}
if got, want := srcTracker.exists, int64(0); got != want {
t.Errorf("count(src.Exists()) = %v, want %v", got, want)
}
if got, want := dstTracker.fetch, int64(0); got != want {
t.Errorf("count(dst.Fetch()) = %v, want %v", got, want)
}
if got, want := dstTracker.push, int64(len(blobs)); got != want {
t.Errorf("count(dst.Push()) = %v, want %v", got, want)
}
if got, want := dstTracker.exists, int64(len(blobs)); got != want {
t.Errorf("count(dst.Exists()) = %v, want %v", got, want)
}
}
func TestCopyGraph_FileToMemory_PartialCopy(t *testing.T) {
tempDir := t.TempDir()
src, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer src.Close()
dst := memory.New()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
dgst := digest.FromBytes(blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: dgst,
Size: int64(len(blob)),
Annotations: map[string]string{
ocispec.AnnotationTitle: dgst.Encoded() + ".blob",
},
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
generateManifest(descs[0], descs[1:3]...) // Blob 3
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 4
generateManifest(descs[0], descs[4]) // Blob 5
generateIndex(descs[3], descs[5]) // Blob 6
ctx := context.Background()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// initial copy
root := descs[3]
if err := oras.CopyGraph(ctx, src, dst, root, oras.CopyGraphOptions{}); err != nil {
t.Fatalf("CopyGraph() error = %v, wantErr %v", err, false)
}
// verify contents
for i := range blobs[:4] {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Fatalf("content[%d] error = %v, wantErr %v", i, err, false)
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Fatalf("content[%d] = %v, want %v", i, got, want)
}
}
// test copy
srcTracker := &storageTracker{Storage: src}
dstTracker := &storageTracker{Storage: dst}
root = descs[len(descs)-1]
if err := oras.CopyGraph(ctx, srcTracker, dstTracker, root, oras.CopyGraphOptions{}); err != nil {
t.Fatalf("CopyGraph() error = %v, wantErr %v", err, false)
}
// verify contents
for i := range blobs {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, false)
continue
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Errorf("content[%d] = %v, want %v", i, got, want)
}
}
// verify API counts
if got, want := srcTracker.fetch, int64(3); got != want {
t.Errorf("count(src.Fetch()) = %v, want %v", got, want)
}
if got, want := srcTracker.push, int64(0); got != want {
t.Errorf("count(src.Push()) = %v, want %v", got, want)
}
if got, want := srcTracker.exists, int64(0); got != want {
t.Errorf("count(src.Exists()) = %v, want %v", got, want)
}
if got, want := dstTracker.fetch, int64(0); got != want {
t.Errorf("count(dst.Fetch()) = %v, want %v", got, want)
}
if got, want := dstTracker.push, int64(3); got != want {
t.Errorf("count(dst.Push()) = %v, want %v", got, want)
}
if got, want := dstTracker.exists, int64(5); got != want {
t.Errorf("count(dst.Exists()) = %v, want %v", got, want)
}
}
func equalDescriptorSet(actual []ocispec.Descriptor, expected []ocispec.Descriptor) bool {
if len(actual) != len(expected) {
return false
}
contains := func(node ocispec.Descriptor) bool {
for _, candidate := range actual {
if reflect.DeepEqual(candidate, node) {
return true
}
}
return false
}
for _, node := range expected {
if !contains(node) {
return false
}
}
return true
}
oras-go-2.5.0/content/file/file_unix_test.go 0000664 0000000 0000000 00000016560 14576745303 0021054 0 ustar 00root root 0000000 0000000 //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 file
import (
"bytes"
"context"
"os"
"path/filepath"
"testing"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2"
)
// Related issue: https://github.com/oras-project/oras-go/issues/402
func TestStore_Dir_ExtractSymlinkRel(t *testing.T) {
// prepare test content
tempDir := t.TempDir()
dirName := "testdir"
dirPath := filepath.Join(tempDir, dirName)
if err := os.MkdirAll(dirPath, 0777); err != nil {
t.Fatal("error calling Mkdir(), error =", err)
}
content := []byte("hello world")
fileName := "test.txt"
filePath := filepath.Join(dirPath, fileName)
if err := os.WriteFile(filePath, content, 0444); err != nil {
t.Fatal("error calling WriteFile(), error =", err)
}
// create symlink to a relative path
symlinkName := "test_symlink"
symlinkPath := filepath.Join(dirPath, symlinkName)
if err := os.Symlink(fileName, symlinkPath); err != nil {
t.Fatal("error calling Symlink(), error =", err)
}
src, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer src.Close()
ctx := context.Background()
// add dir
desc, err := src.Add(ctx, dirName, "", dirPath)
if err != nil {
t.Fatal("Store.Add() error =", err)
}
// pack a manifest
manifestDesc, err := oras.Pack(ctx, src, "dir", []ocispec.Descriptor{desc}, oras.PackOptions{})
if err != nil {
t.Fatal("oras.Pack() error =", err)
}
// copy to another file store created from an absolute root, to trigger extracting directory
tempDir = t.TempDir()
dstAbs, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer dstAbs.Close()
if err := oras.CopyGraph(ctx, src, dstAbs, manifestDesc, oras.DefaultCopyGraphOptions); err != nil {
t.Fatal("oras.CopyGraph() error =", err)
}
// verify extracted symlink
extractedSymlink := filepath.Join(tempDir, dirName, symlinkName)
symlinkDst, err := os.Readlink(extractedSymlink)
if err != nil {
t.Fatal("failed to get symlink destination, error =", err)
}
if want := fileName; symlinkDst != want {
t.Errorf("symlink destination = %v, want %v", symlinkDst, want)
}
got, err := os.ReadFile(extractedSymlink)
if err != nil {
t.Fatal("failed to read symlink file, error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("symlink content = %v, want %v", got, content)
}
// copy to another file store created from a relative root, to trigger extracting directory
tempDir = t.TempDir()
if err := os.Chdir(tempDir); err != nil {
t.Fatal("error calling Chdir(), error=", err)
}
dstRel, err := New(".")
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer dstRel.Close()
if err := oras.CopyGraph(ctx, src, dstRel, manifestDesc, oras.DefaultCopyGraphOptions); err != nil {
t.Fatal("oras.CopyGraph() error =", err)
}
// verify extracted symlink
extractedSymlink = filepath.Join(tempDir, dirName, symlinkName)
symlinkDst, err = os.Readlink(extractedSymlink)
if err != nil {
t.Fatal("failed to get symlink destination, error =", err)
}
if want := fileName; symlinkDst != want {
t.Errorf("symlink destination = %v, want %v", symlinkDst, want)
}
got, err = os.ReadFile(extractedSymlink)
if err != nil {
t.Fatal("failed to read symlink file, error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("symlink content = %v, want %v", got, content)
}
}
// Related issue: https://github.com/oras-project/oras-go/issues/402
func TestStore_Dir_ExtractSymlinkAbs(t *testing.T) {
// prepare test content
tempDir, err := filepath.EvalSymlinks(t.TempDir())
if err != nil {
t.Fatal("error calling filepath.EvalSymlinks(), error =", err)
}
dirName := "testdir"
dirPath := filepath.Join(tempDir, dirName)
if err := os.MkdirAll(dirPath, 0777); err != nil {
t.Fatal("error calling Mkdir(), error =", err)
}
content := []byte("hello world")
fileName := "test.txt"
filePath := filepath.Join(dirPath, fileName)
if err := os.WriteFile(filePath, content, 0444); err != nil {
t.Fatal("error calling WriteFile(), error =", err)
}
// create symlink to an absolute path
symlink := filepath.Join(dirPath, "test_symlink")
if err := os.Symlink(filePath, symlink); err != nil {
t.Fatal("error calling Symlink(), error =", err)
}
src, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer src.Close()
ctx := context.Background()
// add dir
desc, err := src.Add(ctx, dirName, "", dirPath)
if err != nil {
t.Fatal("Store.Add() error =", err)
}
// pack a manifest
manifestDesc, err := oras.Pack(ctx, src, "dir", []ocispec.Descriptor{desc}, oras.PackOptions{})
if err != nil {
t.Fatal("oras.Pack() error =", err)
}
// remove the original testing directory and create a new store using an absolute root
if err := os.RemoveAll(dirPath); err != nil {
t.Fatal("error calling RemoveAll(), error =", err)
}
dstAbs, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer dstAbs.Close()
if err := oras.CopyGraph(ctx, src, dstAbs, manifestDesc, oras.DefaultCopyGraphOptions); err != nil {
t.Fatal("oras.CopyGraph() error =", err)
}
// verify extracted symlink
symlinkDst, err := os.Readlink(symlink)
if err != nil {
t.Fatal("failed to get symlink destination, error =", err)
}
if want := filePath; symlinkDst != want {
t.Errorf("symlink destination = %v, want %v", symlinkDst, want)
}
got, err := os.ReadFile(symlink)
if err != nil {
t.Fatal("failed to read symlink file, error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("symlink content = %v, want %v", got, content)
}
// remove the original testing directory and create a new store using a relative path
if err := os.RemoveAll(dirPath); err != nil {
t.Fatal("error calling RemoveAll(), error =", err)
}
if err := os.Chdir(tempDir); err != nil {
t.Fatal("error calling Chdir(), error=", err)
}
dstRel, err := New(".")
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer dstRel.Close()
if err := oras.CopyGraph(ctx, src, dstRel, manifestDesc, oras.DefaultCopyGraphOptions); err != nil {
t.Fatal("oras.CopyGraph() error =", err)
}
// verify extracted symlink
symlinkDst, err = os.Readlink(symlink)
if err != nil {
t.Fatal("failed to get symlink destination, error =", err)
}
if want := filePath; symlinkDst != want {
t.Errorf("symlink destination = %v, want %v", symlinkDst, want)
}
got, err = os.ReadFile(symlink)
if err != nil {
t.Fatal("failed to read symlink file, error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("symlink content = %v, want %v", got, content)
}
// copy to another file store created from an outside root, to trigger extracting directory
tempDir = t.TempDir()
dstOutside, err := New(tempDir)
if err != nil {
t.Fatal("Store.New() error =", err)
}
defer dstOutside.Close()
if err := oras.CopyGraph(ctx, src, dstOutside, manifestDesc, oras.DefaultCopyGraphOptions); err == nil {
t.Error("oras.CopyGraph() error = nil, wantErr ", true)
}
}
oras-go-2.5.0/content/file/utils.go 0000664 0000000 0000000 00000014415 14576745303 0017170 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 file
import (
"archive/tar"
"compress/gzip"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"github.com/opencontainers/go-digest"
)
// tarDirectory walks the directory specified by path, and tar those files with a new
// path prefix.
func tarDirectory(root, prefix string, w io.Writer, removeTimes bool, buf []byte) (err error) {
tw := tar.NewWriter(w)
defer func() {
closeErr := tw.Close()
if err == nil {
err = closeErr
}
}()
return filepath.Walk(root, func(path string, info os.FileInfo, err error) (returnErr error) {
if err != nil {
return err
}
// Rename path
name, err := filepath.Rel(root, path)
if err != nil {
return err
}
name = filepath.Join(prefix, name)
name = filepath.ToSlash(name)
// Generate header
var link string
mode := info.Mode()
if mode&os.ModeSymlink != 0 {
if link, err = os.Readlink(path); err != nil {
return err
}
}
header, err := tar.FileInfoHeader(info, link)
if err != nil {
return fmt.Errorf("%s: %w", path, err)
}
header.Name = name
header.Uid = 0
header.Gid = 0
header.Uname = ""
header.Gname = ""
if removeTimes {
header.ModTime = time.Time{}
header.AccessTime = time.Time{}
header.ChangeTime = time.Time{}
}
// Write file
if err := tw.WriteHeader(header); err != nil {
return fmt.Errorf("tar: %w", err)
}
if mode.IsRegular() {
fp, err := os.Open(path)
if err != nil {
return err
}
defer func() {
closeErr := fp.Close()
if returnErr == nil {
returnErr = closeErr
}
}()
if _, err := io.CopyBuffer(tw, fp, buf); err != nil {
return fmt.Errorf("failed to copy to %s: %w", path, err)
}
}
return nil
})
}
// extractTarGzip decompresses the gzip
// and extracts tar file to a directory specified by the `dir` parameter.
func extractTarGzip(dir, prefix, filename, checksum string, buf []byte) (err error) {
fp, err := os.Open(filename)
if err != nil {
return err
}
defer func() {
closeErr := fp.Close()
if err == nil {
err = closeErr
}
}()
gzr, err := gzip.NewReader(fp)
if err != nil {
return err
}
defer func() {
closeErr := gzr.Close()
if err == nil {
err = closeErr
}
}()
var r io.Reader = gzr
var verifier digest.Verifier
if checksum != "" {
if digest, err := digest.Parse(checksum); err == nil {
verifier = digest.Verifier()
r = io.TeeReader(r, verifier)
}
}
if err := extractTarDirectory(dir, prefix, r, buf); err != nil {
return err
}
if verifier != nil && !verifier.Verified() {
return errors.New("content digest mismatch")
}
return nil
}
// extractTarDirectory extracts tar file to a directory specified by the `dir`
// parameter. The file name prefix is ensured to be the string specified by the
// `prefix` parameter and is trimmed.
func extractTarDirectory(dir, prefix string, r io.Reader, buf []byte) error {
tr := tar.NewReader(r)
for {
header, err := tr.Next()
if err != nil {
if err == io.EOF {
return nil
}
return err
}
// Name check
name := header.Name
path, err := ensureBasePath(dir, prefix, name)
if err != nil {
return err
}
path = filepath.Join(dir, path)
// Create content
switch header.Typeflag {
case tar.TypeReg:
err = writeFile(path, tr, header.FileInfo().Mode(), buf)
case tar.TypeDir:
err = os.MkdirAll(path, header.FileInfo().Mode())
case tar.TypeLink:
var target string
if target, err = ensureLinkPath(dir, prefix, path, header.Linkname); err == nil {
err = os.Link(target, path)
}
case tar.TypeSymlink:
var target string
if target, err = ensureLinkPath(dir, prefix, path, header.Linkname); err == nil {
err = os.Symlink(target, path)
}
default:
continue // Non-regular files are skipped
}
if err != nil {
return err
}
// Change access time and modification time if possible (error ignored)
os.Chtimes(path, header.AccessTime, header.ModTime)
}
}
// ensureBasePath ensures the target path is in the base path,
// returning its relative path to the base path.
// target can be either an absolute path or a relative path.
func ensureBasePath(baseAbs, baseRel, target string) (string, error) {
base := baseRel
if filepath.IsAbs(target) {
// ensure base and target are consistent
base = baseAbs
}
path, err := filepath.Rel(base, target)
if err != nil {
return "", err
}
cleanPath := filepath.ToSlash(filepath.Clean(path))
if cleanPath == ".." || strings.HasPrefix(cleanPath, "../") {
return "", fmt.Errorf("%q is outside of %q", target, baseRel)
}
// No symbolic link allowed in the relative path
dir := filepath.Dir(path)
for dir != "." {
if info, err := os.Lstat(filepath.Join(baseAbs, dir)); err != nil {
if !os.IsNotExist(err) {
return "", err
}
} else if info.Mode()&os.ModeSymlink != 0 {
return "", fmt.Errorf("no symbolic link allowed between %q and %q", baseRel, target)
}
dir = filepath.Dir(dir)
}
return path, nil
}
// ensureLinkPath ensures the target path pointed by the link is in the base
// path. It returns target path if validated.
func ensureLinkPath(baseAbs, baseRel, link, target string) (string, error) {
// resolve link
path := target
if !filepath.IsAbs(target) {
path = filepath.Join(filepath.Dir(link), target)
}
// ensure path is under baseAbs or baseRel
if _, err := ensureBasePath(baseAbs, baseRel, path); err != nil {
return "", err
}
return target, nil
}
// writeFile writes content to the file specified by the `path` parameter.
func writeFile(path string, r io.Reader, perm os.FileMode, buf []byte) (err error) {
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
if err != nil {
return err
}
defer func() {
closeErr := file.Close()
if err == nil {
err = closeErr
}
}()
_, err = io.CopyBuffer(file, r, buf)
return err
}
oras-go-2.5.0/content/file/utils_test.go 0000664 0000000 0000000 00000006704 14576745303 0020231 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 file
import (
"os"
"path/filepath"
"testing"
)
func Test_ensureBasePath(t *testing.T) {
root := t.TempDir()
if err := os.MkdirAll(filepath.Join(root, "hello world", "foo", "bar"), 0700); err != nil {
t.Fatal("failed to create temp folders:", err)
}
base := "hello world/foo"
tests := []struct {
name string
target string
want string
wantErr bool
}{
{
name: "valid case (depth 0)",
target: "hello world/foo",
want: ".",
},
{
name: "valid case (depth 1)",
target: "hello world/foo/bar",
want: "bar",
},
{
name: "valid case (depth 2)",
target: "hello world/foo/bar/fun",
want: filepath.Join("bar", "fun"),
},
{
name: "invalid prefix",
target: "hello world/fun",
wantErr: true,
},
{
name: "invalid prefix",
target: "hello/foo",
wantErr: true,
},
{
name: "bad traversal",
target: "hello world/foo/..",
wantErr: true,
},
{
name: "valid traversal",
target: "hello world/foo/../foo/bar/../bar",
want: "bar",
},
{
name: "complex traversal",
target: "hello world/foo/../foo/bar/../..",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ensureBasePath(root, base, tt.target)
if (err != nil) != tt.wantErr {
t.Errorf("ensureBasePath() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("ensureBasePath() = %v, want %v", got, tt.want)
}
})
}
}
func Test_ensureLinkPath(t *testing.T) {
root := t.TempDir()
if err := os.MkdirAll(filepath.Join(root, "hello world", "foo", "bar"), 0700); err != nil {
t.Fatal("failed to create temp folders:", err)
}
base := "hello world/foo"
tests := []struct {
name string
link string
target string
want string
wantErr bool
}{
{
name: "valid case (depth 1)",
link: "hello world/foo/bar",
target: "fun",
want: "fun",
},
{
name: "valid case (depth 2)",
link: "hello world/foo/bar/fun",
target: "../fun",
want: "../fun",
},
{
name: "invalid prefix",
link: "hello world/foo",
target: "fun",
wantErr: true,
},
{
name: "bad traversal",
link: "hello world/foo/bar",
target: "../fun",
wantErr: true,
},
{
name: "valid traversal",
link: "hello world/foo/../foo/bar/../bar", // hello world/foo/bar
target: "../foo/../foo/fun",
want: "../foo/../foo/fun",
},
{
name: "complex traversal",
link: "hello world/foo/bar",
target: "../foo/../../fun",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ensureLinkPath(root, base, tt.link, tt.target)
if (err != nil) != tt.wantErr {
t.Errorf("ensureLinkPath() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("ensureLinkPath() = %v, want %v", got, tt.want)
}
})
}
}
oras-go-2.5.0/content/graph.go 0000664 0000000 0000000 00000007161 14576745303 0016212 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 (
"context"
"encoding/json"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/internal/docker"
"oras.land/oras-go/v2/internal/spec"
)
// PredecessorFinder finds out the nodes directly pointing to a given node of a
// directed acyclic graph.
// In other words, returns the "parents" of the current descriptor.
// PredecessorFinder is an extension of Storage.
type PredecessorFinder interface {
// Predecessors returns the nodes directly pointing to the current node.
Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error)
}
// GraphStorage represents a CAS that supports direct predecessor node finding.
type GraphStorage interface {
Storage
PredecessorFinder
}
// ReadOnlyGraphStorage represents a read-only GraphStorage.
type ReadOnlyGraphStorage interface {
ReadOnlyStorage
PredecessorFinder
}
// Successors returns the nodes directly pointed by the current node.
// In other words, returns the "children" of the current descriptor.
func Successors(ctx context.Context, fetcher Fetcher, node ocispec.Descriptor) ([]ocispec.Descriptor, error) {
switch node.MediaType {
case docker.MediaTypeManifest:
content, err := FetchAll(ctx, fetcher, node)
if err != nil {
return nil, err
}
// OCI manifest schema can be used to marshal docker manifest
var manifest ocispec.Manifest
if err := json.Unmarshal(content, &manifest); err != nil {
return nil, err
}
return append([]ocispec.Descriptor{manifest.Config}, manifest.Layers...), nil
case ocispec.MediaTypeImageManifest:
content, err := FetchAll(ctx, fetcher, node)
if err != nil {
return nil, err
}
var manifest ocispec.Manifest
if err := json.Unmarshal(content, &manifest); err != nil {
return nil, err
}
var nodes []ocispec.Descriptor
if manifest.Subject != nil {
nodes = append(nodes, *manifest.Subject)
}
nodes = append(nodes, manifest.Config)
return append(nodes, manifest.Layers...), nil
case docker.MediaTypeManifestList:
content, err := FetchAll(ctx, fetcher, node)
if err != nil {
return nil, err
}
// OCI manifest index schema can be used to marshal docker manifest list
var index ocispec.Index
if err := json.Unmarshal(content, &index); err != nil {
return nil, err
}
return index.Manifests, nil
case ocispec.MediaTypeImageIndex:
content, err := FetchAll(ctx, fetcher, node)
if err != nil {
return nil, err
}
var index ocispec.Index
if err := json.Unmarshal(content, &index); err != nil {
return nil, err
}
var nodes []ocispec.Descriptor
if index.Subject != nil {
nodes = append(nodes, *index.Subject)
}
return append(nodes, index.Manifests...), nil
case spec.MediaTypeArtifactManifest:
content, err := FetchAll(ctx, fetcher, node)
if err != nil {
return nil, err
}
var manifest spec.Artifact
if err := json.Unmarshal(content, &manifest); err != nil {
return nil, err
}
var nodes []ocispec.Descriptor
if manifest.Subject != nil {
nodes = append(nodes, *manifest.Subject)
}
return append(nodes, manifest.Blobs...), nil
}
return nil, nil
}
oras-go-2.5.0/content/graph_test.go 0000664 0000000 0000000 00000027640 14576745303 0017255 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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_test
import (
"bytes"
"context"
"encoding/json"
"reflect"
"testing"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/internal/cas"
"oras.land/oras-go/v2/internal/docker"
"oras.land/oras-go/v2/internal/spec"
)
func TestSuccessors_dockerManifest(t *testing.T) {
storage := cas.NewMemory()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(docker.MediaTypeManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 3
generateManifest(descs[0], descs[1:4]...) // Blob 4
ctx := context.Background()
for i := range blobs {
err := storage.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// test Successors
manifestDesc := descs[4]
got, err := content.Successors(ctx, storage, manifestDesc)
if err != nil {
t.Fatal("Successors() error =", err)
}
if want := descs[0:4]; !reflect.DeepEqual(got, want) {
t.Errorf("Successors() = %v, want %v", got, want)
}
}
func TestSuccessors_imageManifest(t *testing.T) {
storage := cas.NewMemory()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(subject *ocispec.Descriptor, config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Subject: subject,
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 3
generateManifest(nil, descs[0], descs[1:4]...) // Blob 4
appendBlob(ocispec.MediaTypeImageConfig, []byte("{}")) // Blob 5
appendBlob("test/sig", []byte("sig")) // Blob 6
generateManifest(&descs[4], descs[5], descs[6]) // Blob 7
ctx := context.Background()
for i := range blobs {
err := storage.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// test Successors: image manifest without a subject
manifestDesc := descs[4]
got, err := content.Successors(ctx, storage, manifestDesc)
if err != nil {
t.Fatal("Successors() error =", err)
}
if want := descs[0:4]; !reflect.DeepEqual(got, want) {
t.Errorf("Successors() = %v, want %v", got, want)
}
// test Successors: image manifest with a subject
manifestDesc = descs[7]
got, err = content.Successors(ctx, storage, manifestDesc)
if err != nil {
t.Fatal("Successors() error =", err)
}
if want := descs[4:7]; !reflect.DeepEqual(got, want) {
t.Errorf("Successors() = %v, want %v", got, want)
}
}
func TestSuccessors_dockerManifestList(t *testing.T) {
storage := cas.NewMemory()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(docker.MediaTypeManifest, manifestJSON)
}
generateIndex := func(manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(docker.MediaTypeManifestList, indexJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 3
generateManifest(descs[0], descs[1:3]...) // Blob 4
generateManifest(descs[0], descs[3]) // Blob 5
generateIndex(descs[4:6]...) // Blob 6
ctx := context.Background()
for i := range blobs {
err := storage.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// test Successors
manifestDesc := descs[6]
got, err := content.Successors(ctx, storage, manifestDesc)
if err != nil {
t.Fatal("Successors() error =", err)
}
if want := descs[4:6]; !reflect.DeepEqual(got, want) {
t.Errorf("Successors() = %v, want %v", got, want)
}
}
func TestSuccessors_imageIndex(t *testing.T) {
storage := cas.NewMemory()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(subject *ocispec.Descriptor, config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Subject: subject,
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(subject *ocispec.Descriptor, manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Subject: subject,
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 3
generateManifest(nil, descs[0], descs[1:3]...) // Blob 4
generateManifest(nil, descs[0], descs[3]) // Blob 5
appendBlob(ocispec.MediaTypeImageConfig, []byte("{}")) // Blob 6
appendBlob("test/sig", []byte("sig")) // Blob 7
generateManifest(&descs[4], descs[5], descs[6]) // Blob 8
generateIndex(&descs[8], descs[4:6]...) // Blob 9
ctx := context.Background()
for i := range blobs {
err := storage.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// test Successors
manifestDesc := descs[9]
got, err := content.Successors(ctx, storage, manifestDesc)
if err != nil {
t.Fatal("Successors() error =", err)
}
if want := append([]ocispec.Descriptor{descs[8]}, descs[4:6]...); !reflect.DeepEqual(got, want) {
t.Errorf("Successors() = %v, want %v", got, want)
}
}
func TestSuccessors_artifactManifest(t *testing.T) {
storage := cas.NewMemory()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateArtifactManifest := func(subject *ocispec.Descriptor, blobs ...ocispec.Descriptor) {
manifest := spec.Artifact{
Subject: subject,
Blobs: blobs,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(spec.MediaTypeArtifactManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 2
generateArtifactManifest(nil, descs[0:3]...) // Blob 3
appendBlob("test/sig", []byte("sig")) // Blob 4
generateArtifactManifest(&descs[3], descs[4]) // Blob 5
ctx := context.Background()
for i := range blobs {
err := storage.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// test Successors: image manifest without a subject
manifestDesc := descs[3]
got, err := content.Successors(ctx, storage, manifestDesc)
if err != nil {
t.Fatal("Successors() error =", err)
}
if want := descs[0:3]; !reflect.DeepEqual(got, want) {
t.Errorf("Successors() = %v, want %v", got, want)
}
// test Successors: image manifest with a subject
manifestDesc = descs[5]
got, err = content.Successors(ctx, storage, manifestDesc)
if err != nil {
t.Fatal("Successors() error =", err)
}
if want := descs[3:5]; !reflect.DeepEqual(got, want) {
t.Errorf("Successors() = %v, want %v", got, want)
}
}
func TestSuccessors_otherMediaType(t *testing.T) {
storage := cas.NewMemory()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(mediaType string, config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(mediaType, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 3
generateManifest("whatever", descs[0], descs[1:4]...) // Blob 4
ctx := context.Background()
for i := range blobs {
err := storage.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// test Successors: other media type
manifestDesc := descs[4]
got, err := content.Successors(ctx, storage, manifestDesc)
if err != nil {
t.Fatal("Successors() error =", err)
}
if got != nil {
t.Errorf("Successors() = %v, want nil", got)
}
}
oras-go-2.5.0/content/limitedstorage.go 0000664 0000000 0000000 00000002743 14576745303 0020126 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 (
"context"
"fmt"
"io"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/errdef"
)
// LimitedStorage represents a CAS with a push size limit.
type LimitedStorage struct {
Storage // underlying storage
PushLimit int64 // max size for push
}
// Push pushes the content, matching the expected descriptor.
// The size of the content cannot exceed the push size limit.
func (ls *LimitedStorage) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error {
if expected.Size > ls.PushLimit {
return fmt.Errorf(
"content size %v exceeds push size limit %v: %w",
expected.Size,
ls.PushLimit,
errdef.ErrSizeExceedsLimit)
}
return ls.Storage.Push(ctx, expected, io.LimitReader(content, expected.Size))
}
// LimitStorage returns a storage with a push size limit.
func LimitStorage(s Storage, n int64) *LimitedStorage {
return &LimitedStorage{s, n}
}
oras-go-2.5.0/content/memory/ 0000775 0000000 0000000 00000000000 14576745303 0016065 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/content/memory/memory.go 0000664 0000000 0000000 00000006142 14576745303 0017727 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 memory provides implementation of a memory backed content store.
package memory
import (
"context"
"fmt"
"io"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/internal/cas"
"oras.land/oras-go/v2/internal/graph"
"oras.land/oras-go/v2/internal/resolver"
)
// Store represents a memory based store, which implements `oras.Target`.
type Store struct {
storage content.Storage
resolver content.TagResolver
graph *graph.Memory
}
// New creates a new memory based store.
func New() *Store {
return &Store{
storage: cas.NewMemory(),
resolver: resolver.NewMemory(),
graph: graph.NewMemory(),
}
}
// Fetch fetches the content identified by the descriptor.
func (s *Store) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
return s.storage.Fetch(ctx, target)
}
// Push pushes the content, matching the expected descriptor.
func (s *Store) Push(ctx context.Context, expected ocispec.Descriptor, reader io.Reader) error {
if err := s.storage.Push(ctx, expected, reader); err != nil {
return err
}
// index predecessors.
// there is no data consistency issue as long as deletion is not implemented
// for the memory store.
return s.graph.Index(ctx, s.storage, expected)
}
// Exists returns true if the described content exists.
func (s *Store) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) {
return s.storage.Exists(ctx, target)
}
// Resolve resolves a reference to a descriptor.
func (s *Store) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) {
return s.resolver.Resolve(ctx, reference)
}
// Tag tags a descriptor with a reference string.
// Returns ErrNotFound if the tagged content does not exist.
func (s *Store) Tag(ctx context.Context, desc ocispec.Descriptor, reference string) error {
exists, err := s.storage.Exists(ctx, desc)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("%s: %s: %w", desc.Digest, desc.MediaType, errdef.ErrNotFound)
}
return s.resolver.Tag(ctx, desc, reference)
}
// Predecessors returns the nodes directly pointing to the current node.
// Predecessors returns nil without error if the node does not exists in the
// store.
// Like other operations, calling Predecessors() is go-routine safe. However,
// it does not necessarily correspond to any consistent snapshot of the stored
// contents.
func (s *Store) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) {
return s.graph.Predecessors(ctx, node)
}
oras-go-2.5.0/content/memory/memory_test.go 0000664 0000000 0000000 00000025570 14576745303 0020774 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 memory
import (
"bytes"
"context"
_ "crypto/sha256"
"encoding/json"
"errors"
"fmt"
"io"
"reflect"
"strings"
"testing"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"golang.org/x/sync/errgroup"
"oras.land/oras-go/v2"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/internal/cas"
"oras.land/oras-go/v2/internal/resolver"
"oras.land/oras-go/v2/internal/spec"
)
func TestStoreInterface(t *testing.T) {
var store interface{} = &Store{}
if _, ok := store.(oras.Target); !ok {
t.Error("&Store{} does not conform oras.Target")
}
if _, ok := store.(content.PredecessorFinder); !ok {
t.Error("&Store{} does not conform content.PredecessorFinder")
}
}
func TestStoreSuccess(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
ref := "foobar"
s := New()
ctx := context.Background()
err := s.Push(ctx, desc, bytes.NewReader(content))
if err != nil {
t.Fatal("Store.Push() error =", err)
}
err = s.Tag(ctx, desc, ref)
if err != nil {
t.Fatal("Store.Tag() error =", err)
}
gotDesc, err := s.Resolve(ctx, ref)
if err != nil {
t.Fatal("Store.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, desc) {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, desc)
}
internalResolver := s.resolver.(*resolver.Memory)
if got := len(internalResolver.Map()); got != 1 {
t.Errorf("resolver.Map() = %v, want %v", got, 1)
}
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
rc, err := s.Fetch(ctx, desc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Store.Fetch() = %v, want %v", got, content)
}
internalStorage := s.storage.(*cas.Memory)
if got := len(internalStorage.Map()); got != 1 {
t.Errorf("storage.Map() = %v, want %v", got, 1)
}
}
func TestStoreContentNotFound(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
s := New()
ctx := context.Background()
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Error("Store.Exists() error =", err)
}
if exists {
t.Errorf("Store.Exists() = %v, want %v", exists, false)
}
_, err = s.Fetch(ctx, desc)
if !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("Store.Fetch() error = %v, want %v", err, errdef.ErrNotFound)
}
}
func TestStoreContentAlreadyExists(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
s := New()
ctx := context.Background()
err := s.Push(ctx, desc, bytes.NewReader(content))
if err != nil {
t.Fatal("Store.Push() error =", err)
}
err = s.Push(ctx, desc, bytes.NewReader(content))
if !errors.Is(err, errdef.ErrAlreadyExists) {
t.Errorf("Store.Push() error = %v, want %v", err, errdef.ErrAlreadyExists)
}
}
func TestStoreContentBadPush(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
s := New()
ctx := context.Background()
err := s.Push(ctx, desc, strings.NewReader("foobar"))
if err == nil {
t.Errorf("Store.Push() error = %v, wantErr %v", err, true)
}
}
func TestStoreTagNotFound(t *testing.T) {
ref := "foobar"
s := New()
ctx := context.Background()
_, err := s.Resolve(ctx, ref)
if !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("Store.Resolve() error = %v, want %v", err, errdef.ErrNotFound)
}
}
func TestStoreTagUnknownContent(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
ref := "foobar"
s := New()
ctx := context.Background()
err := s.Tag(ctx, desc, ref)
if !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("Store.Resolve() error = %v, want %v", err, errdef.ErrNotFound)
}
}
func TestStoreRepeatTag(t *testing.T) {
generate := func(content []byte) ocispec.Descriptor {
return ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
}
ref := "foobar"
s := New()
ctx := context.Background()
// get internal resolver
internalResolver := s.resolver.(*resolver.Memory)
// initial tag
content := []byte("hello world")
desc := generate(content)
err := s.Push(ctx, desc, bytes.NewReader(content))
if err != nil {
t.Fatal("Store.Push() error =", err)
}
err = s.Tag(ctx, desc, ref)
if err != nil {
t.Fatal("Store.Tag() error =", err)
}
gotDesc, err := s.Resolve(ctx, ref)
if err != nil {
t.Fatal("Store.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, desc) {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, desc)
}
if got := len(internalResolver.Map()); got != 1 {
t.Errorf("resolver.Map() = %v, want %v", got, 1)
}
// repeat tag
content = []byte("foo")
desc = generate(content)
err = s.Push(ctx, desc, bytes.NewReader(content))
if err != nil {
t.Fatal("Store.Push() error =", err)
}
err = s.Tag(ctx, desc, ref)
if err != nil {
t.Fatal("Store.Tag() error =", err)
}
gotDesc, err = s.Resolve(ctx, ref)
if err != nil {
t.Fatal("Store.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, desc) {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, desc)
}
if got := len(internalResolver.Map()); got != 1 {
t.Errorf("resolver.Map() = %v, want %v", got, 1)
}
// repeat tag
content = []byte("bar")
desc = generate(content)
err = s.Push(ctx, desc, bytes.NewReader(content))
if err != nil {
t.Fatal("Store.Push() error =", err)
}
err = s.Tag(ctx, desc, ref)
if err != nil {
t.Fatal("Store.Tag() error =", err)
}
gotDesc, err = s.Resolve(ctx, ref)
if err != nil {
t.Fatal("Store.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, desc) {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, desc)
}
if got := len(internalResolver.Map()); got != 1 {
t.Errorf("resolver.Map() = %v, want %v", got, 1)
}
}
func TestStorePredecessors(t *testing.T) {
s := New()
ctx := context.Background()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
generateArtifactManifest := func(subject ocispec.Descriptor, blobs ...ocispec.Descriptor) {
var manifest spec.Artifact
manifest.Subject = &subject
manifest.Blobs = append(manifest.Blobs, blobs...)
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(spec.MediaTypeArtifactManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 3
generateManifest(descs[0], descs[1:3]...) // Blob 4
generateManifest(descs[0], descs[3]) // Blob 5
generateManifest(descs[0], descs[1:4]...) // Blob 6
generateIndex(descs[4:6]...) // Blob 7
generateIndex(descs[6]) // Blob 8
generateIndex() // Blob 9
generateIndex(descs[7:10]...) // Blob 10
appendBlob(ocispec.MediaTypeImageLayer, []byte("sig_1")) // Blob 11
generateArtifactManifest(descs[6], descs[11]) // Blob 12
appendBlob(ocispec.MediaTypeImageLayer, []byte("sig_2")) // Blob 13
generateArtifactManifest(descs[10], descs[13]) // Blob 14
eg, egCtx := errgroup.WithContext(ctx)
for i := range blobs {
eg.Go(func(i int) func() error {
return func() error {
err := s.Push(egCtx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
return fmt.Errorf("failed to push test content to src: %d: %v", i, err)
}
return nil
}
}(i))
}
if err := eg.Wait(); err != nil {
t.Fatal(err)
}
// verify predecessors
wants := [][]ocispec.Descriptor{
descs[4:7], // Blob 0
{descs[4], descs[6]}, // Blob 1
{descs[4], descs[6]}, // Blob 2
{descs[5], descs[6]}, // Blob 3
{descs[7]}, // Blob 4
{descs[7]}, // Blob 5
{descs[8], descs[12]}, // Blob 6
{descs[10]}, // Blob 7
{descs[10]}, // Blob 8
{descs[10]}, // Blob 9
{descs[14]}, // Blob 10
{descs[12]}, // Blob 11
nil, // Blob 12
{descs[14]}, // Blob 13
nil, // Blob 14
}
for i, want := range wants {
predecessors, err := s.Predecessors(ctx, descs[i])
if err != nil {
t.Errorf("Store.Predecessors(%d) error = %v", i, err)
}
if !equalDescriptorSet(predecessors, want) {
t.Errorf("Store.Predecessors(%d) = %v, want %v", i, predecessors, want)
}
}
}
func equalDescriptorSet(actual []ocispec.Descriptor, expected []ocispec.Descriptor) bool {
if len(actual) != len(expected) {
return false
}
contains := func(node ocispec.Descriptor) bool {
for _, candidate := range actual {
if reflect.DeepEqual(candidate, node) {
return true
}
}
return false
}
for _, node := range expected {
if !contains(node) {
return false
}
}
return true
}
oras-go-2.5.0/content/oci/ 0000775 0000000 0000000 00000000000 14576745303 0015327 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/content/oci/oci.go 0000664 0000000 0000000 00000043375 14576745303 0016444 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 oci provides access to an OCI content store.
// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/image-layout.md
package oci
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"maps"
"os"
"path"
"path/filepath"
"sync"
"github.com/opencontainers/go-digest"
specs "github.com/opencontainers/image-spec/specs-go"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/internal/container/set"
"oras.land/oras-go/v2/internal/descriptor"
"oras.land/oras-go/v2/internal/graph"
"oras.land/oras-go/v2/internal/manifestutil"
"oras.land/oras-go/v2/internal/resolver"
"oras.land/oras-go/v2/registry"
)
// Store implements `oras.Target`, and represents a content store
// based on file system with the OCI-Image layout.
// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/image-layout.md
type Store struct {
// AutoSaveIndex controls if the OCI store will automatically save the index
// file when needed.
// - If AutoSaveIndex is set to true, the OCI store will automatically save
// the changes to `index.json` when
// 1. pushing a manifest
// 2. calling Tag() or Delete()
// - If AutoSaveIndex is set to false, it's the caller's responsibility
// to manually call SaveIndex() when needed.
// - Default value: true.
AutoSaveIndex bool
// AutoGC controls if the OCI store will automatically clean dangling
// (unreferenced) blobs created by the Delete() operation. This includes the
// referrers and the unreferenced successor blobs of the deleted content.
// Tagged manifests will not be deleted.
// - Default value: true.
AutoGC bool
root string
indexPath string
index *ocispec.Index
storage *Storage
tagResolver *resolver.Memory
graph *graph.Memory
// sync ensures that most operations can be done concurrently, while Delete
// has the exclusive access to Store if a delete operation is underway.
// Operations such as Fetch, Push use sync.RLock(), while Delete uses
// sync.Lock().
sync sync.RWMutex
// indexLock ensures that only one go-routine is writing to the index.
indexLock sync.Mutex
}
// New creates a new OCI store with context.Background().
func New(root string) (*Store, error) {
return NewWithContext(context.Background(), root)
}
// NewWithContext creates a new OCI store.
func NewWithContext(ctx context.Context, root string) (*Store, error) {
rootAbs, err := filepath.Abs(root)
if err != nil {
return nil, fmt.Errorf("failed to resolve absolute path for %s: %w", root, err)
}
storage, err := NewStorage(rootAbs)
if err != nil {
return nil, fmt.Errorf("failed to create storage: %w", err)
}
store := &Store{
AutoSaveIndex: true,
AutoGC: true,
root: rootAbs,
indexPath: filepath.Join(rootAbs, ocispec.ImageIndexFile),
storage: storage,
tagResolver: resolver.NewMemory(),
graph: graph.NewMemory(),
}
if err := ensureDir(filepath.Join(rootAbs, ocispec.ImageBlobsDir)); err != nil {
return nil, err
}
if err := store.ensureOCILayoutFile(); err != nil {
return nil, fmt.Errorf("invalid OCI Image Layout: %w", err)
}
if err := store.loadIndexFile(ctx); err != nil {
return nil, fmt.Errorf("invalid OCI Image Index: %w", err)
}
return store, nil
}
// Fetch fetches the content identified by the descriptor. It returns an io.ReadCloser.
// It's recommended to close the io.ReadCloser before a Delete operation, otherwise
// Delete may fail (for example on NTFS file systems).
func (s *Store) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
s.sync.RLock()
defer s.sync.RUnlock()
return s.storage.Fetch(ctx, target)
}
// Push pushes the content, matching the expected descriptor.
func (s *Store) Push(ctx context.Context, expected ocispec.Descriptor, reader io.Reader) error {
s.sync.RLock()
defer s.sync.RUnlock()
if err := s.storage.Push(ctx, expected, reader); err != nil {
return err
}
if err := s.graph.Index(ctx, s.storage, expected); err != nil {
return err
}
if descriptor.IsManifest(expected) {
// tag by digest
return s.tag(ctx, expected, expected.Digest.String())
}
return nil
}
// Exists returns true if the described content exists.
func (s *Store) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) {
s.sync.RLock()
defer s.sync.RUnlock()
return s.storage.Exists(ctx, target)
}
// Delete deletes the content matching the descriptor from the store. Delete may
// fail on certain systems (i.e. NTFS), if there is a process (i.e. an unclosed
// Reader) using target. If s.AutoGC is set to true, Delete will recursively
// remove the dangling blobs caused by the current delete. If s.AutoDeleteReferrers
// is set to true, Delete will recursively remove the referrers of the manifests
// being deleted.
func (s *Store) Delete(ctx context.Context, target ocispec.Descriptor) error {
s.sync.Lock()
defer s.sync.Unlock()
deleteQueue := []ocispec.Descriptor{target}
for len(deleteQueue) > 0 {
head := deleteQueue[0]
deleteQueue = deleteQueue[1:]
// get referrers if applicable
if s.AutoGC && descriptor.IsManifest(head) {
referrers, err := registry.Referrers(ctx, &unsafeStore{s}, head, "")
if err != nil {
return err
}
deleteQueue = append(deleteQueue, referrers...)
}
// delete the head of queue
danglings, err := s.delete(ctx, head)
if err != nil {
return err
}
if s.AutoGC {
for _, d := range danglings {
// do not delete existing tagged manifests
if !s.isTagged(d) {
deleteQueue = append(deleteQueue, d)
}
}
}
}
return nil
}
// delete deletes one node and returns the dangling nodes caused by the delete.
func (s *Store) delete(ctx context.Context, target ocispec.Descriptor) ([]ocispec.Descriptor, error) {
resolvers := s.tagResolver.Map()
untagged := false
for reference, desc := range resolvers {
if content.Equal(desc, target) {
s.tagResolver.Untag(reference)
untagged = true
}
}
danglings := s.graph.Remove(target)
if untagged && s.AutoSaveIndex {
err := s.saveIndex()
if err != nil {
return nil, err
}
}
if err := s.storage.Delete(ctx, target); err != nil {
return nil, err
}
return danglings, nil
}
// Tag tags a descriptor with a reference string.
// reference should be a valid tag (e.g. "latest").
// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/image-layout.md#indexjson-file
func (s *Store) Tag(ctx context.Context, desc ocispec.Descriptor, reference string) error {
s.sync.RLock()
defer s.sync.RUnlock()
if err := validateReference(reference); err != nil {
return err
}
exists, err := s.storage.Exists(ctx, desc)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("%s: %s: %w", desc.Digest, desc.MediaType, errdef.ErrNotFound)
}
return s.tag(ctx, desc, reference)
}
// tag tags a descriptor with a reference string.
func (s *Store) tag(ctx context.Context, desc ocispec.Descriptor, reference string) error {
dgst := desc.Digest.String()
if reference != dgst {
// also tag desc by its digest
if err := s.tagResolver.Tag(ctx, desc, dgst); err != nil {
return err
}
}
if err := s.tagResolver.Tag(ctx, desc, reference); err != nil {
return err
}
if s.AutoSaveIndex {
return s.saveIndex()
}
return nil
}
// Resolve resolves a reference to a descriptor. If the reference to be resolved
// is a tag, the returned descriptor will be a full descriptor declared by
// github.com/opencontainers/image-spec/specs-go/v1. If the reference is a
// digest the returned descriptor will be a plain descriptor (containing only
// the digest, media type and size).
func (s *Store) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) {
s.sync.RLock()
defer s.sync.RUnlock()
if reference == "" {
return ocispec.Descriptor{}, errdef.ErrMissingReference
}
// attempt resolving manifest
desc, err := s.tagResolver.Resolve(ctx, reference)
if err != nil {
if errors.Is(err, errdef.ErrNotFound) {
// attempt resolving blob
return resolveBlob(os.DirFS(s.root), reference)
}
return ocispec.Descriptor{}, err
}
if reference == desc.Digest.String() {
return descriptor.Plain(desc), nil
}
return desc, nil
}
func (s *Store) Untag(ctx context.Context, reference string) error {
if reference == "" {
return errdef.ErrMissingReference
}
s.sync.RLock()
defer s.sync.RUnlock()
desc, err := s.tagResolver.Resolve(ctx, reference)
if err != nil {
return fmt.Errorf("resolving reference %q: %w", reference, err)
}
if reference == desc.Digest.String() {
return fmt.Errorf("reference %q is a digest and not a tag: %w", reference, errdef.ErrInvalidReference)
}
s.tagResolver.Untag(reference)
if s.AutoSaveIndex {
return s.saveIndex()
}
return nil
}
// Predecessors returns the nodes directly pointing to the current node.
// Predecessors returns nil without error if the node does not exists in the
// store.
func (s *Store) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) {
s.sync.RLock()
defer s.sync.RUnlock()
return s.graph.Predecessors(ctx, node)
}
// Tags lists the tags presented in the `index.json` file of the OCI layout,
// returned in ascending order.
// If `last` is NOT empty, the entries in the response start after the tag
// specified by `last`. Otherwise, the response starts from the top of the tags
// list.
//
// See also `Tags()` in the package `registry`.
func (s *Store) Tags(ctx context.Context, last string, fn func(tags []string) error) error {
s.sync.RLock()
defer s.sync.RUnlock()
return listTags(s.tagResolver, last, fn)
}
// ensureOCILayoutFile ensures the `oci-layout` file.
func (s *Store) ensureOCILayoutFile() error {
layoutFilePath := filepath.Join(s.root, ocispec.ImageLayoutFile)
layoutFile, err := os.Open(layoutFilePath)
if err != nil {
if !os.IsNotExist(err) {
return fmt.Errorf("failed to open OCI layout file: %w", err)
}
layout := ocispec.ImageLayout{
Version: ocispec.ImageLayoutVersion,
}
layoutJSON, err := json.Marshal(layout)
if err != nil {
return fmt.Errorf("failed to marshal OCI layout file: %w", err)
}
return os.WriteFile(layoutFilePath, layoutJSON, 0666)
}
defer layoutFile.Close()
var layout ocispec.ImageLayout
err = json.NewDecoder(layoutFile).Decode(&layout)
if err != nil {
return fmt.Errorf("failed to decode OCI layout file: %w", err)
}
return validateOCILayout(&layout)
}
// loadIndexFile reads index.json from the file system.
// Create index.json if it does not exist.
func (s *Store) loadIndexFile(ctx context.Context) error {
indexFile, err := os.Open(s.indexPath)
if err != nil {
if !os.IsNotExist(err) {
return fmt.Errorf("failed to open index file: %w", err)
}
// write index.json if it does not exist
s.index = &ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value
},
Manifests: []ocispec.Descriptor{},
}
return s.writeIndexFile()
}
defer indexFile.Close()
var index ocispec.Index
if err := json.NewDecoder(indexFile).Decode(&index); err != nil {
return fmt.Errorf("failed to decode index file: %w", err)
}
s.index = &index
return loadIndex(ctx, s.index, s.storage, s.tagResolver, s.graph)
}
// SaveIndex writes the `index.json` file to the file system.
// - If AutoSaveIndex is set to true (default value),
// the OCI store will automatically save the changes to `index.json`
// on Tag() and Delete() calls, and when pushing a manifest.
// - If AutoSaveIndex is set to false, it's the caller's responsibility
// to manually call this method when needed.
func (s *Store) SaveIndex() error {
s.sync.RLock()
defer s.sync.RUnlock()
return s.saveIndex()
}
func (s *Store) saveIndex() error {
s.indexLock.Lock()
defer s.indexLock.Unlock()
var manifests []ocispec.Descriptor
tagged := set.New[digest.Digest]()
refMap := s.tagResolver.Map()
// 1. Add descriptors that are associated with tags
// Note: One descriptor can be associated with multiple tags.
for ref, desc := range refMap {
if ref != desc.Digest.String() {
annotations := make(map[string]string, len(desc.Annotations)+1)
maps.Copy(annotations, desc.Annotations)
annotations[ocispec.AnnotationRefName] = ref
desc.Annotations = annotations
manifests = append(manifests, desc)
// mark the digest as tagged for deduplication in step 2
tagged.Add(desc.Digest)
}
}
// 2. Add descriptors that are not associated with any tag
for ref, desc := range refMap {
if ref == desc.Digest.String() && !tagged.Contains(desc.Digest) {
// skip tagged ones since they have been added in step 1
manifests = append(manifests, deleteAnnotationRefName(desc))
}
}
s.index.Manifests = manifests
return s.writeIndexFile()
}
// writeIndexFile writes the `index.json` file.
func (s *Store) writeIndexFile() error {
indexJSON, err := json.Marshal(s.index)
if err != nil {
return fmt.Errorf("failed to marshal index file: %w", err)
}
return os.WriteFile(s.indexPath, indexJSON, 0666)
}
// GC removes garbage from Store. Unsaved index will be lost. To prevent unexpected
// loss, call SaveIndex() before GC or set AutoSaveIndex to true.
// The garbage to be cleaned are:
// - unreferenced (dangling) blobs in Store which have no predecessors
// - garbage blobs in the storage whose metadata is not stored in Store
func (s *Store) GC(ctx context.Context) error {
s.sync.Lock()
defer s.sync.Unlock()
// get reachable nodes by reloading the index
err := s.gcIndex(ctx)
if err != nil {
return fmt.Errorf("unable to reload index: %w", err)
}
reachableNodes := s.graph.DigestSet()
// clean up garbage blobs in the storage
rootpath := filepath.Join(s.root, ocispec.ImageBlobsDir)
algDirs, err := os.ReadDir(rootpath)
if err != nil {
return err
}
for _, algDir := range algDirs {
if !algDir.IsDir() {
continue
}
alg := algDir.Name()
// skip unsupported directories
if !isKnownAlgorithm(alg) {
continue
}
algPath := path.Join(rootpath, alg)
digestEntries, err := os.ReadDir(algPath)
if err != nil {
return err
}
for _, digestEntry := range digestEntries {
if err := isContextDone(ctx); err != nil {
return err
}
dgst := digestEntry.Name()
blobDigest := digest.NewDigestFromEncoded(digest.Algorithm(alg), dgst)
if err := blobDigest.Validate(); err != nil {
// skip irrelevant content
continue
}
if !reachableNodes.Contains(blobDigest) {
// remove the blob from storage if it does not exist in Store
err = os.Remove(path.Join(algPath, dgst))
if err != nil {
return err
}
}
}
}
return nil
}
// gcIndex reloads the index and updates metadata. Information of untagged blobs
// are cleaned and only tagged blobs remain.
func (s *Store) gcIndex(ctx context.Context) error {
tagResolver := resolver.NewMemory()
graph := graph.NewMemory()
tagged := set.New[digest.Digest]()
// index tagged manifests
refMap := s.tagResolver.Map()
for ref, desc := range refMap {
if ref == desc.Digest.String() {
continue
}
if err := tagResolver.Tag(ctx, deleteAnnotationRefName(desc), desc.Digest.String()); err != nil {
return err
}
if err := tagResolver.Tag(ctx, desc, ref); err != nil {
return err
}
plain := descriptor.Plain(desc)
if err := graph.IndexAll(ctx, s.storage, plain); err != nil {
return err
}
tagged.Add(desc.Digest)
}
// index referrer manifests
for ref, desc := range refMap {
if ref != desc.Digest.String() || tagged.Contains(desc.Digest) {
continue
}
// check if the referrers manifest can traverse to the existing graph
subject := &desc
for {
subject, err := manifestutil.Subject(ctx, s.storage, *subject)
if err != nil {
return err
}
if subject == nil {
break
}
if graph.Exists(*subject) {
if err := tagResolver.Tag(ctx, deleteAnnotationRefName(desc), desc.Digest.String()); err != nil {
return err
}
plain := descriptor.Plain(desc)
if err := graph.IndexAll(ctx, s.storage, plain); err != nil {
return err
}
break
}
}
}
s.tagResolver = tagResolver
s.graph = graph
return nil
}
// isTagged checks if the blob given by the descriptor is tagged.
func (s *Store) isTagged(desc ocispec.Descriptor) bool {
tagSet := s.tagResolver.TagSet(desc)
if tagSet.Contains(string(desc.Digest)) {
return len(tagSet) > 1
}
return len(tagSet) > 0
}
// unsafeStore is used to bypass lock restrictions in Delete.
type unsafeStore struct {
*Store
}
func (s *unsafeStore) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
return s.storage.Fetch(ctx, target)
}
func (s *unsafeStore) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) {
return s.graph.Predecessors(ctx, node)
}
// isContextDone returns an error if the context is done.
// Reference: https://pkg.go.dev/context#Context
func isContextDone(ctx context.Context) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
return nil
}
}
// validateReference validates ref.
func validateReference(ref string) error {
if ref == "" {
return errdef.ErrMissingReference
}
// TODO: may enforce more strict validation if needed.
return nil
}
// isKnownAlgorithm checks is a string is a supported hash algorithm
func isKnownAlgorithm(alg string) bool {
switch digest.Algorithm(alg) {
case digest.SHA256, digest.SHA512, digest.SHA384:
return true
default:
return false
}
}
oras-go-2.5.0/content/oci/oci_test.go 0000664 0000000 0000000 00000243343 14576745303 0017500 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 oci
import (
"bytes"
"context"
_ "crypto/sha256"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"reflect"
"strconv"
"strings"
"sync/atomic"
"testing"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"golang.org/x/sync/errgroup"
"oras.land/oras-go/v2"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/content/memory"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/internal/cas"
"oras.land/oras-go/v2/internal/descriptor"
"oras.land/oras-go/v2/internal/spec"
"oras.land/oras-go/v2/registry"
)
// storageTracker tracks storage API counts.
type storageTracker struct {
content.Storage
fetch int64
push int64
exists int64
}
func (t *storageTracker) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
atomic.AddInt64(&t.fetch, 1)
return t.Storage.Fetch(ctx, target)
}
func (t *storageTracker) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error {
atomic.AddInt64(&t.push, 1)
return t.Storage.Push(ctx, expected, content)
}
func (t *storageTracker) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) {
atomic.AddInt64(&t.exists, 1)
return t.Storage.Exists(ctx, target)
}
func TestStoreInterface(t *testing.T) {
var store interface{} = &Store{}
if _, ok := store.(oras.GraphTarget); !ok {
t.Error("&Store{} does not conform oras.Target")
}
if _, ok := store.(registry.TagLister); !ok {
t.Error("&Store{} does not conform registry.TagLister")
}
}
func TestStore_Success(t *testing.T) {
blob := []byte("test")
blobDesc := content.NewDescriptorFromBytes("test", blob)
manifest := []byte(`{"layers":[]}`)
manifestDesc := content.NewDescriptorFromBytes(ocispec.MediaTypeImageManifest, manifest)
ref := "foobar"
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
ctx := context.Background()
// validate layout
layoutFilePath := filepath.Join(tempDir, ocispec.ImageLayoutFile)
layoutFile, err := os.Open(layoutFilePath)
if err != nil {
t.Errorf("error opening layout file, error = %v", err)
}
defer layoutFile.Close()
var layout *ocispec.ImageLayout
err = json.NewDecoder(layoutFile).Decode(&layout)
if err != nil {
t.Fatal("error decoding layout, error =", err)
}
if want := ocispec.ImageLayoutVersion; layout.Version != want {
t.Errorf("layout.Version = %s, want %s", layout.Version, want)
}
// validate index.json
indexFilePath := filepath.Join(tempDir, "index.json")
indexFile, err := os.Open(indexFilePath)
if err != nil {
t.Errorf("error opening layout file, error = %v", err)
}
defer indexFile.Close()
var index *ocispec.Index
err = json.NewDecoder(indexFile).Decode(&index)
if err != nil {
t.Fatal("error decoding index.json, error =", err)
}
if want := 2; index.SchemaVersion != want {
t.Errorf("index.SchemaVersion = %v, want %v", index.SchemaVersion, want)
}
// test push blob
err = s.Push(ctx, blobDesc, bytes.NewReader(blob))
if err != nil {
t.Fatal("Store.Push() error =", err)
}
internalResolver := s.tagResolver
if got, want := len(internalResolver.Map()), 0; got != want {
t.Errorf("resolver.Map() = %v, want %v", got, want)
}
// test push manifest
err = s.Push(ctx, manifestDesc, bytes.NewReader(manifest))
if err != nil {
t.Fatal("Store.Push() error =", err)
}
if got, want := len(internalResolver.Map()), 1; got != want {
t.Errorf("resolver.Map() = %v, want %v", got, want)
}
// test resolving blob by digest
gotDesc, err := s.Resolve(ctx, blobDesc.Digest.String())
if err != nil {
t.Fatal("Store.Resolve() error =", err)
}
if want := blobDesc; gotDesc.Size != want.Size || gotDesc.Digest != want.Digest {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, blobDesc)
}
// test resolving manifest by digest
gotDesc, err = s.Resolve(ctx, manifestDesc.Digest.String())
if err != nil {
t.Fatal("Store.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, manifestDesc)
}
// test tag
err = s.Tag(ctx, manifestDesc, ref)
if err != nil {
t.Fatal("Store.Tag() error =", err)
}
if got, want := len(internalResolver.Map()), 2; got != want {
t.Errorf("resolver.Map() = %v, want %v", got, want)
}
// test resolving manifest by tag
gotDesc, err = s.Resolve(ctx, ref)
if err != nil {
t.Fatal("Store.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, manifestDesc)
}
// test fetch
exists, err := s.Exists(ctx, manifestDesc)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
rc, err := s.Fetch(ctx, manifestDesc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, manifest) {
t.Errorf("Store.Fetch() = %v, want %v", got, manifest)
}
}
func TestStore_RelativeRoot_Success(t *testing.T) {
blob := []byte("test")
blobDesc := content.NewDescriptorFromBytes("test", blob)
manifest := []byte(`{"layers":[]}`)
manifestDesc := content.NewDescriptorFromBytes(ocispec.MediaTypeImageManifest, manifest)
ref := "foobar"
tempDir, err := filepath.EvalSymlinks(t.TempDir())
if err != nil {
t.Fatal("error calling filepath.EvalSymlinks(), error =", err)
}
currDir, err := os.Getwd()
if err != nil {
t.Fatal("error calling Getwd(), error=", err)
}
if err := os.Chdir(tempDir); err != nil {
t.Fatal("error calling Chdir(), error=", err)
}
s, err := New(".")
if err != nil {
t.Fatal("New() error =", err)
}
if want := tempDir; s.root != want {
t.Errorf("Store.root = %s, want %s", s.root, want)
}
// cd back to allow the temp directory to be removed
if err := os.Chdir(currDir); err != nil {
t.Fatal("error calling Chdir(), error=", err)
}
ctx := context.Background()
// validate layout
layoutFilePath := filepath.Join(tempDir, ocispec.ImageLayoutFile)
layoutFile, err := os.Open(layoutFilePath)
if err != nil {
t.Errorf("error opening layout file, error = %v", err)
}
defer layoutFile.Close()
var layout *ocispec.ImageLayout
err = json.NewDecoder(layoutFile).Decode(&layout)
if err != nil {
t.Fatal("error decoding layout, error =", err)
}
if want := ocispec.ImageLayoutVersion; layout.Version != want {
t.Errorf("layout.Version = %s, want %s", layout.Version, want)
}
// test push blob
err = s.Push(ctx, blobDesc, bytes.NewReader(blob))
if err != nil {
t.Fatal("Store.Push() error =", err)
}
internalResolver := s.tagResolver
if got, want := len(internalResolver.Map()), 0; got != want {
t.Errorf("resolver.Map() = %v, want %v", got, want)
}
// test push manifest
err = s.Push(ctx, manifestDesc, bytes.NewReader(manifest))
if err != nil {
t.Fatal("Store.Push() error =", err)
}
if got, want := len(internalResolver.Map()), 1; got != want {
t.Errorf("resolver.Map() = %v, want %v", got, want)
}
// test resolving blob by digest
gotDesc, err := s.Resolve(ctx, blobDesc.Digest.String())
if err != nil {
t.Fatal("Store.Resolve() error =", err)
}
if want := blobDesc; gotDesc.Size != want.Size || gotDesc.Digest != want.Digest {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, blobDesc)
}
// test resolving manifest by digest
gotDesc, err = s.Resolve(ctx, manifestDesc.Digest.String())
if err != nil {
t.Fatal("Store.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, manifestDesc)
}
// test tag
err = s.Tag(ctx, manifestDesc, ref)
if err != nil {
t.Fatal("Store.Tag() error =", err)
}
if got, want := len(internalResolver.Map()), 2; got != want {
t.Errorf("resolver.Map() = %v, want %v", got, want)
}
// test resolving manifest by tag
gotDesc, err = s.Resolve(ctx, ref)
if err != nil {
t.Fatal("Store.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, manifestDesc)
}
// test fetch
exists, err := s.Exists(ctx, manifestDesc)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
rc, err := s.Fetch(ctx, manifestDesc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, manifest) {
t.Errorf("Store.Fetch() = %v, want %v", got, manifest)
}
}
func TestStore_NotExistingRoot(t *testing.T) {
tempDir := t.TempDir()
root := filepath.Join(tempDir, "rootDir")
_, err := New(root)
if err != nil {
t.Fatal("New() error =", err)
}
// validate layout
layoutFilePath := filepath.Join(root, ocispec.ImageLayoutFile)
layoutFile, err := os.Open(layoutFilePath)
if err != nil {
t.Errorf("error opening layout file, error = %v", err)
}
defer layoutFile.Close()
var layout *ocispec.ImageLayout
err = json.NewDecoder(layoutFile).Decode(&layout)
if err != nil {
t.Fatal("error decoding layout, error =", err)
}
if want := ocispec.ImageLayoutVersion; layout.Version != want {
t.Errorf("layout.Version = %s, want %s", layout.Version, want)
}
// validate index.json
indexFilePath := filepath.Join(root, "index.json")
indexFile, err := os.Open(indexFilePath)
if err != nil {
t.Errorf("error opening layout file, error = %v", err)
}
defer indexFile.Close()
var index *ocispec.Index
err = json.NewDecoder(indexFile).Decode(&index)
if err != nil {
t.Fatal("error decoding index.json, error =", err)
}
if want := 2; index.SchemaVersion != want {
t.Errorf("index.SchemaVersion = %v, want %v", index.SchemaVersion, want)
}
}
func TestStore_ContentNotFound(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
ctx := context.Background()
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Error("Store.Exists() error =", err)
}
if exists {
t.Errorf("Store.Exists() = %v, want %v", exists, false)
}
_, err = s.Fetch(ctx, desc)
if !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("Store.Fetch() error = %v, want %v", err, errdef.ErrNotFound)
}
}
func TestStore_ContentAlreadyExists(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
ctx := context.Background()
err = s.Push(ctx, desc, bytes.NewReader(content))
if err != nil {
t.Fatal("Store.Push() error =", err)
}
err = s.Push(ctx, desc, bytes.NewReader(content))
if !errors.Is(err, errdef.ErrAlreadyExists) {
t.Errorf("Store.Push() error = %v, want %v", err, errdef.ErrAlreadyExists)
}
}
func TestStore_ContentBadPush(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
ctx := context.Background()
err = s.Push(ctx, desc, strings.NewReader("foobar"))
if err == nil {
t.Errorf("Store.Push() error = %v, wantErr %v", err, true)
}
}
func TestStore_ResolveByTagReturnsFullDescriptor(t *testing.T) {
content := []byte("hello world")
ref := "hello-world:0.0.1"
annotations := map[string]string{"name": "Hello"}
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
Annotations: annotations,
}
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
ctx := context.Background()
err = s.Push(ctx, desc, bytes.NewReader(content))
if err != nil {
t.Errorf("Store.Push() error = %v, wantErr %v", err, false)
}
err = s.Tag(ctx, desc, ref)
if err != nil {
t.Errorf("error tagging descriptor error = %v, wantErr %v", err, false)
}
resolvedDescr, err := s.Resolve(ctx, ref)
if err != nil {
t.Errorf("error resolving descriptor error = %v, wantErr %v", err, false)
}
if !reflect.DeepEqual(resolvedDescr, desc) {
t.Errorf("Store.Resolve() = %v, want %v", resolvedDescr, desc)
}
}
func TestStore_ResolveByDigestReturnsPlainDescriptor(t *testing.T) {
content := []byte("hello world")
ref := "hello-world:0.0.1"
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
Annotations: map[string]string{"name": "Hello"},
}
plainDescriptor := descriptor.Plain(desc)
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
ctx := context.Background()
err = s.Push(ctx, desc, bytes.NewReader(content))
if err != nil {
t.Errorf("Store.Push() error = %v, wantErr %v", err, false)
}
err = s.Tag(ctx, desc, ref)
if err != nil {
t.Errorf("error tagging descriptor error = %v, wantErr %v", err, false)
}
resolvedDescr, err := s.Resolve(ctx, string(desc.Digest))
if err != nil {
t.Errorf("error resolving descriptor error = %v, wantErr %v", err, false)
}
if !reflect.DeepEqual(resolvedDescr, plainDescriptor) {
t.Errorf("Store.Resolve() = %v, want %v", resolvedDescr, plainDescriptor)
}
}
func TestStore_TagNotFound(t *testing.T) {
ref := "foobar"
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
ctx := context.Background()
_, err = s.Resolve(ctx, ref)
if !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("Store.Resolve() error = %v, want %v", err, errdef.ErrNotFound)
}
}
func TestStore_TagUnknownContent(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
ref := "foobar"
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
ctx := context.Background()
err = s.Tag(ctx, desc, ref)
if !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("Store.Resolve() error = %v, want %v", err, errdef.ErrNotFound)
}
}
func TestStore_DisableAutoSaveIndex(t *testing.T) {
content := []byte(`{"layers":[]}`)
desc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
ref0 := "foobar"
ref1 := "barfoo"
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
// disable auto save
s.AutoSaveIndex = false
ctx := context.Background()
// validate layout
layoutFilePath := filepath.Join(tempDir, ocispec.ImageLayoutFile)
layoutFile, err := os.Open(layoutFilePath)
if err != nil {
t.Errorf("error opening layout file, error = %v", err)
}
defer layoutFile.Close()
var layout *ocispec.ImageLayout
err = json.NewDecoder(layoutFile).Decode(&layout)
if err != nil {
t.Fatal("error decoding layout, error =", err)
}
if want := ocispec.ImageLayoutVersion; layout.Version != want {
t.Errorf("layout.Version = %s, want %s", layout.Version, want)
}
// test push
err = s.Push(ctx, desc, bytes.NewReader(content))
if err != nil {
t.Fatal("Store.Push() error =", err)
}
internalResolver := s.tagResolver
if got, want := len(internalResolver.Map()), 1; got != want {
t.Errorf("resolver.Map() = %v, want %v", got, want)
}
// test resolving by digest
gotDesc, err := s.Resolve(ctx, desc.Digest.String())
if err != nil {
t.Fatal("Store.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, desc) {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, desc)
}
// test tag
err = s.Tag(ctx, desc, ref0)
if err != nil {
t.Fatal("Store.Tag() error =", err)
}
err = s.Tag(ctx, desc, ref1)
if err != nil {
t.Fatal("Store.Tag() error =", err)
}
if got, want := len(internalResolver.Map()), 3; got != want {
t.Errorf("resolver.Map() = %v, want %v", got, want)
}
// test resolving by digest
gotDesc, err = s.Resolve(ctx, ref0)
if err != nil {
t.Fatal("Store.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, desc) {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, desc)
}
// test index file
if got, want := len(s.index.Manifests), 0; got != want {
t.Errorf("len(index.Manifests) = %v, want %v", got, want)
}
if err := s.saveIndex(); err != nil {
t.Fatal("Store.SaveIndex() error =", err)
}
// test index file again
if got, want := len(s.index.Manifests), 2; got != want {
t.Errorf("len(index.Manifests) = %v, want %v", got, want)
}
if _, err := os.Stat(s.indexPath); err != nil {
t.Errorf("error: %s does not exist", s.indexPath)
}
// test untag
err = s.Untag(ctx, ref0)
if err != nil {
t.Fatal("Store.Untag() error =", err)
}
if got, want := len(internalResolver.Map()), 2; got != want {
t.Errorf("resolver.Map() = %v, want %v", got, want)
}
if got, want := len(s.index.Manifests), 2; got != want {
t.Errorf("len(index.Manifests) = %v, want %v", got, want)
}
if err := s.saveIndex(); err != nil {
t.Fatal("Store.SaveIndex() error =", err)
}
// test index file again
if got, want := len(s.index.Manifests), 1; got != want {
t.Errorf("len(index.Manifests) = %v, want %v", got, want)
}
}
func TestStore_RepeatTag(t *testing.T) {
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
ctx := context.Background()
ref := "foobar"
// get internal resolver
internalResolver := s.tagResolver
// first tag a manifest
manifest := []byte(`{"layers":[]}`)
desc := content.NewDescriptorFromBytes(ocispec.MediaTypeImageManifest, manifest)
err = s.Push(ctx, desc, bytes.NewReader(manifest))
if err != nil {
t.Fatal("Store.Push() error =", err)
}
if got, want := len(internalResolver.Map()), 1; got != want {
t.Errorf("len(resolver.Map()) = %v, want %v", got, want)
}
if got, want := len(s.index.Manifests), 1; got != want {
t.Errorf("len(index.Manifests) = %v, want %v", got, want)
}
err = s.Tag(ctx, desc, ref)
if err != nil {
t.Fatal("Store.Tag() error =", err)
}
if got, want := len(internalResolver.Map()), 2; got != want {
t.Errorf("resolver.Map() = %v, want %v", got, want)
}
if got, want := len(s.index.Manifests), 1; got != want {
t.Errorf("len(index.Manifests) = %v, want %v", got, want)
}
gotDesc, err := s.Resolve(ctx, desc.Digest.String())
if err != nil {
t.Fatal("Store.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, desc) {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, desc)
}
gotDesc, err = s.Resolve(ctx, ref)
if err != nil {
t.Fatal("Store.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, desc) {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, desc)
}
// tag another manifest
manifest = []byte(`{"layers":[], "annotations":{}}`)
desc = content.NewDescriptorFromBytes(ocispec.MediaTypeImageManifest, manifest)
err = s.Push(ctx, desc, bytes.NewReader(manifest))
if err != nil {
t.Fatal("Store.Push() error =", err)
}
if got, want := len(internalResolver.Map()), 3; got != want {
t.Errorf("resolver.Map() = %v, want %v", got, want)
}
if got, want := len(s.index.Manifests), 2; got != want {
t.Errorf("len(index.Manifests) = %v, want %v", got, want)
}
err = s.Tag(ctx, desc, ref)
if err != nil {
t.Fatal("Store.Tag() error =", err)
}
if got, want := len(internalResolver.Map()), 3; got != want {
t.Errorf("resolver.Map() = %v, want %v", got, want)
}
if got, want := len(s.index.Manifests), 2; got != want {
t.Errorf("len(index.Manifests) = %v, want %v", got, want)
}
gotDesc, err = s.Resolve(ctx, desc.Digest.String())
if err != nil {
t.Fatal("Store.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, desc) {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, desc)
}
gotDesc, err = s.Resolve(ctx, ref)
if err != nil {
t.Fatal("Store.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, desc) {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, desc)
}
// tag a blob
blob := []byte("foobar")
desc = content.NewDescriptorFromBytes("test", blob)
err = s.Push(ctx, desc, bytes.NewReader(blob))
if err != nil {
t.Fatal("Store.Push() error =", err)
}
if got, want := len(internalResolver.Map()), 3; got != want {
t.Errorf("resolver.Map() = %v, want %v", got, want)
}
if got, want := len(s.index.Manifests), 2; got != want {
t.Errorf("len(index.Manifests) = %v, want %v", got, want)
}
err = s.Tag(ctx, desc, ref)
if err != nil {
t.Fatal("Store.Tag() error =", err)
}
if got, want := len(internalResolver.Map()), 4; got != want {
t.Errorf("resolver.Map() = %v, want %v", got, want)
}
if got, want := len(s.index.Manifests), 3; got != want {
t.Errorf("len(index.Manifests) = %v, want %v", got, want)
}
gotDesc, err = s.Resolve(ctx, desc.Digest.String())
if err != nil {
t.Fatal("Store.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, desc) {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, desc)
}
gotDesc, err = s.Resolve(ctx, ref)
if err != nil {
t.Fatal("Store.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, desc) {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, desc)
}
// tag another blob
blob = []byte("barfoo")
desc = content.NewDescriptorFromBytes("test", blob)
err = s.Push(ctx, desc, bytes.NewReader(blob))
if err != nil {
t.Fatal("Store.Push() error =", err)
}
if got, want := len(internalResolver.Map()), 4; got != want {
t.Errorf("resolver.Map() = %v, want %v", got, want)
}
if got, want := len(s.index.Manifests), 3; got != want {
t.Errorf("len(index.Manifests) = %v, want %v", got, want)
}
err = s.Tag(ctx, desc, ref)
if err != nil {
t.Fatal("Store.Tag() error =", err)
}
if got, want := len(internalResolver.Map()), 5; got != want {
t.Errorf("resolver.Map() = %v, want %v", got, want)
}
if got, want := len(s.index.Manifests), 4; got != want {
t.Errorf("len(index.Manifests) = %v, want %v", got, want)
}
gotDesc, err = s.Resolve(ctx, desc.Digest.String())
if err != nil {
t.Fatal("Store.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, desc) {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, desc)
}
gotDesc, err = s.Resolve(ctx, ref)
if err != nil {
t.Fatal("Store.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, desc) {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, desc)
}
}
// Related bug: https://github.com/oras-project/oras-go/issues/461
func TestStore_TagByDigest(t *testing.T) {
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
ctx := context.Background()
// get internal resolver
internalResolver := s.tagResolver
manifest := []byte(`{"layers":[]}`)
manifestDesc := content.NewDescriptorFromBytes(ocispec.MediaTypeImageManifest, manifest)
// push a manifest
err = s.Push(ctx, manifestDesc, bytes.NewReader(manifest))
if err != nil {
t.Fatal("Store.Push() error =", err)
}
if got, want := len(internalResolver.Map()), 1; got != want {
t.Errorf("len(resolver.Map()) = %v, want %v", got, want)
}
if got, want := len(s.index.Manifests), 1; got != want {
t.Errorf("len(index.Manifests) = %v, want %v", got, want)
}
gotDesc, err := s.Resolve(ctx, manifestDesc.Digest.String())
if err != nil {
t.Fatal("Store.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, manifestDesc)
}
// tag manifest by digest
err = s.Tag(ctx, manifestDesc, manifestDesc.Digest.String())
if err != nil {
t.Fatal("Store.Tag() error =", err)
}
if got, want := len(internalResolver.Map()), 1; got != want {
t.Errorf("len(resolver.Map()) = %v, want %v", got, want)
}
if got, want := len(s.index.Manifests), 1; got != want {
t.Errorf("len(index.Manifests) = %v, want %v", got, want)
}
gotDesc, err = s.Resolve(ctx, manifestDesc.Digest.String())
if err != nil {
t.Fatal("Store.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, manifestDesc)
}
// push a blob
blob := []byte("foobar")
blobDesc := content.NewDescriptorFromBytes("test", blob)
err = s.Push(ctx, blobDesc, bytes.NewReader(blob))
if err != nil {
t.Fatal("Store.Push() error =", err)
}
if got, want := len(internalResolver.Map()), 1; got != want {
t.Errorf("resolver.Map() = %v, want %v", got, want)
}
if got, want := len(s.index.Manifests), 1; got != want {
t.Errorf("len(index.Manifests) = %v, want %v", got, want)
}
gotDesc, err = s.Resolve(ctx, blobDesc.Digest.String())
if err != nil {
t.Fatal("Store.Resolve() error =", err)
}
if gotDesc.Digest != blobDesc.Digest || gotDesc.Size != blobDesc.Size {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, blobDesc)
}
// tag blob by digest
err = s.Tag(ctx, blobDesc, blobDesc.Digest.String())
if err != nil {
t.Fatal("Store.Tag() error =", err)
}
if got, want := len(internalResolver.Map()), 2; got != want {
t.Errorf("resolver.Map() = %v, want %v", got, want)
}
if got, want := len(s.index.Manifests), 2; got != want {
t.Errorf("len(index.Manifests) = %v, want %v", got, want)
}
gotDesc, err = s.Resolve(ctx, blobDesc.Digest.String())
if err != nil {
t.Fatal("Store.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, blobDesc) {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, blobDesc)
}
}
func TestStore_BadIndex(t *testing.T) {
tempDir := t.TempDir()
content := []byte("whatever")
path := filepath.Join(tempDir, "index.json")
os.WriteFile(path, content, 0666)
_, err := New(tempDir)
if err == nil {
t.Errorf("New() error = nil, want: error")
}
}
func TestStore_BadLayout(t *testing.T) {
tempDir := t.TempDir()
content := []byte("whatever")
path := filepath.Join(tempDir, ocispec.ImageLayoutFile)
os.WriteFile(path, content, 0666)
_, err := New(tempDir)
if err == nil {
t.Errorf("New() error = nil, want: error")
}
}
func TestStore_Predecessors(t *testing.T) {
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
ctx := context.Background()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
generateArtifactManifest := func(subject ocispec.Descriptor, blobs ...ocispec.Descriptor) {
var manifest spec.Artifact
manifest.Subject = &subject
manifest.Blobs = append(manifest.Blobs, blobs...)
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(spec.MediaTypeArtifactManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 3
generateManifest(descs[0], descs[1:3]...) // Blob 4
generateManifest(descs[0], descs[3]) // Blob 5
generateManifest(descs[0], descs[1:4]...) // Blob 6
generateIndex(descs[4:6]...) // Blob 7
generateIndex(descs[6]) // Blob 8
generateIndex() // Blob 9
generateIndex(descs[7:10]...) // Blob 10
appendBlob(ocispec.MediaTypeImageLayer, []byte("sig_1")) // Blob 11
generateArtifactManifest(descs[6], descs[11]) // Blob 12
appendBlob(ocispec.MediaTypeImageLayer, []byte("sig_2")) // Blob 13
generateArtifactManifest(descs[10], descs[13]) // Blob 14
eg, egCtx := errgroup.WithContext(ctx)
for i := range blobs {
eg.Go(func(i int) func() error {
return func() error {
err := s.Push(egCtx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
return fmt.Errorf("failed to push test content to src: %d: %v", i, err)
}
return nil
}
}(i))
}
if err := eg.Wait(); err != nil {
t.Fatal(err)
}
// verify predecessors
wants := [][]ocispec.Descriptor{
descs[4:7], // Blob 0
{descs[4], descs[6]}, // Blob 1
{descs[4], descs[6]}, // Blob 2
{descs[5], descs[6]}, // Blob 3
{descs[7]}, // Blob 4
{descs[7]}, // Blob 5
{descs[8], descs[12]}, // Blob 6
{descs[10]}, // Blob 7
{descs[10]}, // Blob 8
{descs[10]}, // Blob 9
{descs[14]}, // Blob 10
{descs[12]}, // Blob 11
nil, // Blob 12
{descs[14]}, // Blob 13
nil, // Blob 14
}
for i, want := range wants {
predecessors, err := s.Predecessors(ctx, descs[i])
if err != nil {
t.Errorf("Store.Predecessors(%d) error = %v", i, err)
}
if !equalDescriptorSet(predecessors, want) {
t.Errorf("Store.Predecessors(%d) = %v, want %v", i, predecessors, want)
}
}
}
func TestStore_ExistingStore(t *testing.T) {
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
generateArtifactManifest := func(subject ocispec.Descriptor, blobs ...ocispec.Descriptor) {
var manifest spec.Artifact
manifest.Subject = &subject
manifest.Blobs = append(manifest.Blobs, blobs...)
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(spec.MediaTypeArtifactManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 3
generateManifest(descs[0], descs[1:3]...) // Blob 4
generateManifest(descs[0], descs[3]) // Blob 5
generateManifest(descs[0], descs[1:4]...) // Blob 6
generateIndex(descs[4:6]...) // Blob 7
generateIndex(descs[6]) // Blob 8
generateIndex() // Blob 9
generateIndex(descs[7:10]...) // Blob 10
appendBlob(ocispec.MediaTypeImageLayer, []byte("sig_1")) // Blob 11
generateArtifactManifest(descs[6], descs[11]) // Blob 12
appendBlob(ocispec.MediaTypeImageLayer, []byte("sig_2")) // Blob 13
generateArtifactManifest(descs[10], descs[13]) // Blob 14
ctx := context.Background()
eg, egCtx := errgroup.WithContext(ctx)
for i := range blobs {
eg.Go(func(i int) func() error {
return func() error {
err := s.Push(egCtx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
return fmt.Errorf("failed to push test content to src: %d: %v", i, err)
}
return nil
}
}(i))
}
if err := eg.Wait(); err != nil {
t.Fatal(err)
}
// tag index root
indexRoot := descs[10]
tag := "latest"
if err := s.Tag(ctx, indexRoot, tag); err != nil {
t.Fatal("Tag() error =", err)
}
// tag index root by digest
// related bug: https://github.com/oras-project/oras-go/issues/461
if err := s.Tag(ctx, indexRoot, indexRoot.Digest.String()); err != nil {
t.Fatal("Tag() error =", err)
}
// test with another OCI store instance to mock loading from an existing store
anotherS, err := New(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
// test resolving index root by tag
gotDesc, err := anotherS.Resolve(ctx, tag)
if err != nil {
t.Fatal("Store: Resolve() error =", err)
}
if !content.Equal(gotDesc, indexRoot) {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, indexRoot)
}
// test resolving index root by digest
gotDesc, err = anotherS.Resolve(ctx, indexRoot.Digest.String())
if err != nil {
t.Fatal("Store: Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, indexRoot) {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, indexRoot)
}
// test resolving artifact manifest by digest
artifactRootDesc := descs[12]
gotDesc, err = anotherS.Resolve(ctx, artifactRootDesc.Digest.String())
if err != nil {
t.Fatal("Store: Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, artifactRootDesc) {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, artifactRootDesc)
}
// test resolving blob by digest
gotDesc, err = anotherS.Resolve(ctx, descs[0].Digest.String())
if err != nil {
t.Fatal("Store.Resolve() error =", err)
}
if want := descs[0]; gotDesc.Size != want.Size || gotDesc.Digest != want.Digest {
t.Errorf("Store.Resolve() = %v, want %v", gotDesc, want)
}
// test fetching OCI root index
exists, err := anotherS.Exists(ctx, indexRoot)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
// test fetching blobs
for i := range blobs {
eg.Go(func(i int) func() error {
return func() error {
rc, err := s.Fetch(egCtx, descs[i])
if err != nil {
return fmt.Errorf("Store.Fetch(%d) error = %v", i, err)
}
got, err := io.ReadAll(rc)
if err != nil {
return fmt.Errorf("Store.Fetch(%d).Read() error = %v", i, err)
}
err = rc.Close()
if err != nil {
return fmt.Errorf("Store.Fetch(%d).Close() error = %v", i, err)
}
if !bytes.Equal(got, blobs[i]) {
return fmt.Errorf("Store.Fetch(%d) = %v, want %v", i, got, blobs[i])
}
return nil
}
}(i))
}
if err := eg.Wait(); err != nil {
t.Fatal(err)
}
// verify predecessors
wants := [][]ocispec.Descriptor{
descs[4:7], // Blob 0
{descs[4], descs[6]}, // Blob 1
{descs[4], descs[6]}, // Blob 2
{descs[5], descs[6]}, // Blob 3
{descs[7]}, // Blob 4
{descs[7]}, // Blob 5
{descs[8], descs[12]}, // Blob 6
{descs[10]}, // Blob 7
{descs[10]}, // Blob 8
{descs[10]}, // Blob 9
{descs[14]}, // Blob 10
{descs[12]}, // Blob 11
nil, // Blob 12, no predecessors
{descs[14]}, // Blob 13
nil, // Blob 14, no predecessors
}
for i, want := range wants {
predecessors, err := anotherS.Predecessors(ctx, descs[i])
if err != nil {
t.Errorf("Store.Predecessors(%d) error = %v", i, err)
}
if !equalDescriptorSet(predecessors, want) {
t.Errorf("Store.Predecessors(%d) = %v, want %v", i, predecessors, want)
}
}
}
func Test_ExistingStore_Retag(t *testing.T) {
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
ctx := context.Background()
manifest_1 := []byte(`{"layers":[]}`)
manifestDesc_1 := content.NewDescriptorFromBytes(ocispec.MediaTypeImageManifest, manifest_1)
manifestDesc_1.Annotations = map[string]string{"key1": "val1"}
// push a manifest
err = s.Push(ctx, manifestDesc_1, bytes.NewReader(manifest_1))
if err != nil {
t.Fatal("Store.Push() error =", err)
}
// tag manifest by digest
err = s.Tag(ctx, manifestDesc_1, manifestDesc_1.Digest.String())
if err != nil {
t.Fatal("Store.Tag() error =", err)
}
// tag manifest by tag
ref := "foobar"
err = s.Tag(ctx, manifestDesc_1, ref)
if err != nil {
t.Fatal("Store.Tag() error =", err)
}
// verify index
want := []ocispec.Descriptor{
{
MediaType: manifestDesc_1.MediaType,
Digest: manifestDesc_1.Digest,
Size: manifestDesc_1.Size,
Annotations: map[string]string{
"key1": "val1",
ocispec.AnnotationRefName: ref,
},
},
}
if got := s.index.Manifests; !equalDescriptorSet(got, want) {
t.Errorf("index.Manifests = %v, want %v", got, want)
}
// test with another OCI store instance to mock loading from an existing store
anotherS, err := New(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
manifest_2 := []byte(`{"layers":[], "annotations":{}}`)
manifestDesc_2 := content.NewDescriptorFromBytes(ocispec.MediaTypeImageManifest, manifest_2)
manifestDesc_2.Annotations = map[string]string{"key2": "val2"}
err = anotherS.Push(ctx, manifestDesc_2, bytes.NewReader(manifest_2))
if err != nil {
t.Fatal("Store.Push() error =", err)
}
err = anotherS.Tag(ctx, manifestDesc_2, ref)
if err != nil {
t.Fatal("Store.Tag() error =", err)
}
// verify index
want = []ocispec.Descriptor{
{
MediaType: manifestDesc_1.MediaType,
Digest: manifestDesc_1.Digest,
Size: manifestDesc_1.Size,
Annotations: map[string]string{
"key1": "val1",
},
},
{
MediaType: manifestDesc_2.MediaType,
Digest: manifestDesc_2.Digest,
Size: manifestDesc_2.Size,
Annotations: map[string]string{
"key2": "val2",
ocispec.AnnotationRefName: ref,
},
},
}
if got := anotherS.index.Manifests; !equalDescriptorSet(got, want) {
t.Errorf("index.Manifests = %v, want %v", got, want)
}
}
func TestCopy_MemoryToOCI_FullCopy(t *testing.T) {
src := memory.New()
tempDir := t.TempDir()
dst, err := New(tempDir)
if err != nil {
t.Fatal("OCI.New() error =", err)
}
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
generateManifest(descs[0], descs[1:3]...) // Blob 3
ctx := context.Background()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
root := descs[3]
ref := "foobar"
err = src.Tag(ctx, root, ref)
if err != nil {
t.Fatal("fail to tag root node", err)
}
// test copy
gotDesc, err := oras.Copy(ctx, src, ref, dst, "", oras.CopyOptions{})
if err != nil {
t.Fatalf("Copy() error = %v, wantErr %v", err, false)
}
if !reflect.DeepEqual(gotDesc, root) {
t.Errorf("Copy() = %v, want %v", gotDesc, root)
}
// verify contents
for i, desc := range descs {
exists, err := dst.Exists(ctx, desc)
if err != nil {
t.Fatalf("dst.Exists(%d) error = %v", i, err)
}
if !exists {
t.Errorf("dst.Exists(%d) = %v, want %v", i, exists, true)
}
}
// verify tag
gotDesc, err = dst.Resolve(ctx, ref)
if err != nil {
t.Fatal("dst.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, root) {
t.Errorf("dst.Resolve() = %v, want %v", gotDesc, root)
}
}
func TestCopyGraph_MemoryToOCI_FullCopy(t *testing.T) {
src := cas.NewMemory()
tempDir := t.TempDir()
dst, err := New(tempDir)
if err != nil {
t.Fatal("OCI.New() error =", err)
}
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 3
generateManifest(descs[0], descs[1:3]...) // Blob 4
generateManifest(descs[0], descs[3]) // Blob 5
generateManifest(descs[0], descs[1:4]...) // Blob 6
generateIndex(descs[4:6]...) // Blob 7
generateIndex(descs[6]) // Blob 8
generateIndex() // Blob 9
generateIndex(descs[7:10]...) // Blob 10
ctx := context.Background()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// test copy
srcTracker := &storageTracker{Storage: src}
dstTracker := &storageTracker{Storage: dst}
root := descs[len(descs)-1]
if err := oras.CopyGraph(ctx, srcTracker, dstTracker, root, oras.CopyGraphOptions{}); err != nil {
t.Fatalf("CopyGraph() error = %v, wantErr %v", err, false)
}
// verify contents
for i := range blobs {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, false)
continue
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Errorf("content[%d] = %v, want %v", i, got, want)
}
}
// verify API counts
if got, want := srcTracker.fetch, int64(len(blobs)); got != want {
t.Errorf("count(src.Fetch()) = %v, want %v", got, want)
}
if got, want := srcTracker.push, int64(0); got != want {
t.Errorf("count(src.Push()) = %v, want %v", got, want)
}
if got, want := srcTracker.exists, int64(0); got != want {
t.Errorf("count(src.Exists()) = %v, want %v", got, want)
}
if got, want := dstTracker.fetch, int64(0); got != want {
t.Errorf("count(dst.Fetch()) = %v, want %v", got, want)
}
if got, want := dstTracker.push, int64(len(blobs)); got != want {
t.Errorf("count(dst.Push()) = %v, want %v", got, want)
}
if got, want := dstTracker.exists, int64(len(blobs)); got != want {
t.Errorf("count(dst.Exists()) = %v, want %v", got, want)
}
}
func TestCopyGraph_MemoryToOCI_PartialCopy(t *testing.T) {
src := cas.NewMemory()
tempDir := t.TempDir()
dst, err := New(tempDir)
if err != nil {
t.Fatal("OCI.New() error =", err)
}
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
generateManifest(descs[0], descs[1:3]...) // Blob 3
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 4
generateManifest(descs[0], descs[4]) // Blob 5
generateIndex(descs[3], descs[5]) // Blob 6
ctx := context.Background()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// initial copy
root := descs[3]
if err := oras.CopyGraph(ctx, src, dst, root, oras.CopyGraphOptions{}); err != nil {
t.Fatalf("CopyGraph() error = %v, wantErr %v", err, false)
}
// verify contents
for i := range blobs[:4] {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Fatalf("content[%d] error = %v, wantErr %v", i, err, false)
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Fatalf("content[%d] = %v, want %v", i, got, want)
}
}
// test copy
srcTracker := &storageTracker{Storage: src}
dstTracker := &storageTracker{Storage: dst}
root = descs[len(descs)-1]
if err := oras.CopyGraph(ctx, srcTracker, dstTracker, root, oras.CopyGraphOptions{}); err != nil {
t.Fatalf("CopyGraph() error = %v, wantErr %v", err, false)
}
// verify contents
for i := range blobs {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, false)
continue
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Errorf("content[%d] = %v, want %v", i, got, want)
}
}
// verify API counts
if got, want := srcTracker.fetch, int64(3); got != want {
t.Errorf("count(src.Fetch()) = %v, want %v", got, want)
}
if got, want := srcTracker.push, int64(0); got != want {
t.Errorf("count(src.Push()) = %v, want %v", got, want)
}
if got, want := srcTracker.exists, int64(0); got != want {
t.Errorf("count(src.Exists()) = %v, want %v", got, want)
}
if got, want := dstTracker.fetch, int64(0); got != want {
t.Errorf("count(dst.Fetch()) = %v, want %v", got, want)
}
if got, want := dstTracker.push, int64(3); got != want {
t.Errorf("count(dst.Push()) = %v, want %v", got, want)
}
if got, want := dstTracker.exists, int64(5); got != want {
t.Errorf("count(dst.Exists()) = %v, want %v", got, want)
}
}
func TestCopyGraph_OCIToMemory_FullCopy(t *testing.T) {
tempDir := t.TempDir()
src, err := New(tempDir)
if err != nil {
t.Fatal("OCI.New() error =", err)
}
dst := cas.NewMemory()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 3
generateManifest(descs[0], descs[1:3]...) // Blob 4
generateManifest(descs[0], descs[3]) // Blob 5
generateManifest(descs[0], descs[1:4]...) // Blob 6
generateIndex(descs[4:6]...) // Blob 7
generateIndex(descs[6]) // Blob 8
generateIndex() // Blob 9
generateIndex(descs[7:10]...) // Blob 10
ctx := context.Background()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// test copy
srcTracker := &storageTracker{Storage: src}
dstTracker := &storageTracker{Storage: dst}
root := descs[len(descs)-1]
if err := oras.CopyGraph(ctx, srcTracker, dstTracker, root, oras.CopyGraphOptions{}); err != nil {
t.Fatalf("CopyGraph() error = %v, wantErr %v", err, false)
}
// verify contents
for i := range blobs {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, false)
continue
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Errorf("content[%d] = %v, want %v", i, got, want)
}
}
// verify API counts
if got, want := srcTracker.fetch, int64(len(blobs)); got != want {
t.Errorf("count(src.Fetch()) = %v, want %v", got, want)
}
if got, want := srcTracker.push, int64(0); got != want {
t.Errorf("count(src.Push()) = %v, want %v", got, want)
}
if got, want := srcTracker.exists, int64(0); got != want {
t.Errorf("count(src.Exists()) = %v, want %v", got, want)
}
if got, want := dstTracker.fetch, int64(0); got != want {
t.Errorf("count(dst.Fetch()) = %v, want %v", got, want)
}
if got, want := dstTracker.push, int64(len(blobs)); got != want {
t.Errorf("count(dst.Push()) = %v, want %v", got, want)
}
if got, want := dstTracker.exists, int64(len(blobs)); got != want {
t.Errorf("count(dst.Exists()) = %v, want %v", got, want)
}
}
func TestCopyGraph_OCIToMemory_PartialCopy(t *testing.T) {
tempDir := t.TempDir()
src, err := New(tempDir)
if err != nil {
t.Fatal("OCI.New() error =", err)
}
dst := cas.NewMemory()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
generateManifest(descs[0], descs[1:3]...) // Blob 3
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 4
generateManifest(descs[0], descs[4]) // Blob 5
generateIndex(descs[3], descs[5]) // Blob 6
ctx := context.Background()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// initial copy
root := descs[3]
if err := oras.CopyGraph(ctx, src, dst, root, oras.CopyGraphOptions{}); err != nil {
t.Fatalf("CopyGraph() error = %v, wantErr %v", err, false)
}
// verify contents
for i := range blobs[:4] {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Fatalf("content[%d] error = %v, wantErr %v", i, err, false)
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Fatalf("content[%d] = %v, want %v", i, got, want)
}
}
// test copy
srcTracker := &storageTracker{Storage: src}
dstTracker := &storageTracker{Storage: dst}
root = descs[len(descs)-1]
if err := oras.CopyGraph(ctx, srcTracker, dstTracker, root, oras.CopyGraphOptions{}); err != nil {
t.Fatalf("CopyGraph() error = %v, wantErr %v", err, false)
}
// verify contents
for i := range blobs {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, false)
continue
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Errorf("content[%d] = %v, want %v", i, got, want)
}
}
// verify API counts
if got, want := srcTracker.fetch, int64(3); got != want {
t.Errorf("count(src.Fetch()) = %v, want %v", got, want)
}
if got, want := srcTracker.push, int64(0); got != want {
t.Errorf("count(src.Push()) = %v, want %v", got, want)
}
if got, want := srcTracker.exists, int64(0); got != want {
t.Errorf("count(src.Exists()) = %v, want %v", got, want)
}
if got, want := dstTracker.fetch, int64(0); got != want {
t.Errorf("count(dst.Fetch()) = %v, want %v", got, want)
}
if got, want := dstTracker.push, int64(3); got != want {
t.Errorf("count(dst.Push()) = %v, want %v", got, want)
}
if got, want := dstTracker.exists, int64(5); got != want {
t.Errorf("count(dst.Exists()) = %v, want %v", got, want)
}
}
func TestStore_Tags(t *testing.T) {
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
ctx := context.Background()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
// add annotation to make each manifest unique
manifest.Annotations = map[string]string{
"blob_index": strconv.Itoa(len(blobs)),
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
tagManifest := func(desc ocispec.Descriptor, ref string) {
if err := s.Tag(ctx, desc, ref); err != nil {
t.Fatal("Store.Tag() error =", err)
}
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foobar")) // Blob 1
generateManifest(descs[0], descs[1]) // Blob 2
generateManifest(descs[0], descs[1]) // Blob 3
generateManifest(descs[0], descs[1]) // Blob 4
generateManifest(descs[0], descs[1]) // Blob 5
generateManifest(descs[0], descs[1]) // Blob 6
for i := range blobs {
err := s.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content: %d: %v", i, err)
}
}
tagManifest(descs[3], "v2")
tagManifest(descs[4], "v3")
tagManifest(descs[5], "v1")
tagManifest(descs[6], "v4")
// test tags
tests := []struct {
name string
last string
want []string
}{
{
name: "list all tags",
want: []string{"v1", "v2", "v3", "v4"},
},
{
name: "list from middle",
last: "v2",
want: []string{"v3", "v4"},
},
{
name: "list from end",
last: "v4",
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := s.Tags(ctx, tt.last, func(got []string) error {
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Store.Tags() = %v, want %v", got, tt.want)
}
return nil
}); err != nil {
t.Errorf("Store.Tags() error = %v", err)
}
})
}
wantErr := errors.New("expected error")
if err := s.Tags(ctx, "", func(got []string) error {
return wantErr
}); err != wantErr {
t.Errorf("Store.Tags() error = %v, wantErr %v", err, wantErr)
}
}
func TestStore_BasicDelete(t *testing.T) {
content := []byte("test delete")
desc := ocispec.Descriptor{
MediaType: "test-delete",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
ref := "latest"
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("NewDeletableStore() error =", err)
}
ctx := context.Background()
err = s.Push(ctx, desc, bytes.NewReader(content))
if err != nil {
t.Errorf("Store.Push() error = %v, wantErr %v", err, false)
}
err = s.Tag(ctx, desc, ref)
if err != nil {
t.Errorf("error tagging descriptor error = %v, wantErr %v", err, false)
}
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
resolvedDescr, err := s.Resolve(ctx, ref)
if err != nil {
t.Errorf("error resolving descriptor error = %v, wantErr %v", err, false)
}
if !reflect.DeepEqual(resolvedDescr, desc) {
t.Errorf("Store.Resolve() = %v, want %v", resolvedDescr, desc)
}
err = s.Delete(ctx, desc)
if err != nil {
t.Errorf("Store.Delete() = %v, wantErr %v", err, nil)
}
exists, err = s.Exists(ctx, desc)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if exists {
t.Errorf("Store.Exists() = %v, want %v", exists, false)
}
}
func TestStore_FetchAndDelete(t *testing.T) {
// create a store
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("error =", err)
}
// push a content
content := []byte("test delete")
desc := ocispec.Descriptor{
MediaType: "test-delete",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
err = s.Push(context.Background(), desc, bytes.NewReader(content))
if err != nil {
t.Fatal("error =", err)
}
// fetch a content
rc, err := s.Fetch(context.Background(), desc)
if err != nil {
t.Fatal("error =", err)
}
// read and verify the content
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Store.Fetch() = %v, want %v", string(got), string(content))
}
rc.Close()
// delete. If rc is not closed, Delete would fail on some systems.
err = s.Delete(context.Background(), desc)
if err != nil {
t.Fatal("error =", err)
}
}
func TestStore_PredecessorsAndDelete(t *testing.T) {
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
s.AutoGC = false
ctx := context.Background()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 3
generateManifest(descs[0], descs[1:3]...) // Blob 4
generateManifest(descs[0], descs[3]) // Blob 5
generateManifest(descs[0], descs[1:4]...) // Blob 6
generateIndex(descs[4:6]...) // Blob 7
generateIndex(descs[6]) // Blob 8
eg, egCtx := errgroup.WithContext(ctx)
for i := range blobs {
eg.Go(func(i int) func() error {
return func() error {
err := s.Push(egCtx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
return fmt.Errorf("failed to push test content to src: %d: %v", i, err)
}
return nil
}
}(i))
}
if err := eg.Wait(); err != nil {
t.Fatal(err)
}
// verify predecessors
wants := [][]ocispec.Descriptor{
descs[4:7], // Blob 0
{descs[4], descs[6]}, // Blob 1
{descs[4], descs[6]}, // Blob 2
{descs[5], descs[6]}, // Blob 3
{descs[7]}, // Blob 4
{descs[7]}, // Blob 5
{descs[8]}, // Blob 6
nil, // Blob 7
nil, // Blob 8
}
for i, want := range wants {
predecessors, err := s.Predecessors(ctx, descs[i])
if err != nil {
t.Errorf("Store.Predecessors(%d) error = %v", i, err)
}
if !equalDescriptorSet(predecessors, want) {
t.Errorf("Store.Predecessors(%d) = %v, want %v", i, predecessors, want)
}
}
// delete a node and verify the result
s.Delete(egCtx, descs[6])
// verify predecessors
wants = [][]ocispec.Descriptor{
descs[4:6], // Blob 0
{descs[4]}, // Blob 1
{descs[4]}, // Blob 2
{descs[5]}, // Blob 3
{descs[7]}, // Blob 4
{descs[7]}, // Blob 5
{descs[8]}, // Blob 6
nil, // Blob 7
nil, // Blob 8
}
for i, want := range wants {
predecessors, err := s.Predecessors(ctx, descs[i])
if err != nil {
t.Errorf("Store.Predecessors(%d) error = %v", i, err)
}
if !equalDescriptorSet(predecessors, want) {
t.Errorf("Store.Predecessors(%d) = %v, want %v", i, predecessors, want)
}
}
// delete a node and verify the result
s.Delete(egCtx, descs[8])
// verify predecessors
wants = [][]ocispec.Descriptor{
descs[4:6], // Blob 0
{descs[4]}, // Blob 1
{descs[4]}, // Blob 2
{descs[5]}, // Blob 3
{descs[7]}, // Blob 4
{descs[7]}, // Blob 5
nil, // Blob 6
nil, // Blob 7
nil, // Blob 8
}
for i, want := range wants {
predecessors, err := s.Predecessors(ctx, descs[i])
if err != nil {
t.Errorf("Store.Predecessors(%d) error = %v", i, err)
}
if !equalDescriptorSet(predecessors, want) {
t.Errorf("Store.Predecessors(%d) = %v, want %v", i, predecessors, want)
}
}
// delete a node and verify the result
s.Delete(egCtx, descs[5])
// verify predecessors
wants = [][]ocispec.Descriptor{
{descs[4]}, // Blob 0
{descs[4]}, // Blob 1
{descs[4]}, // Blob 2
nil, // Blob 3
{descs[7]}, // Blob 4
{descs[7]}, // Blob 5
nil, // Blob 6
nil, // Blob 7
nil, // Blob 8
}
for i, want := range wants {
predecessors, err := s.Predecessors(ctx, descs[i])
if err != nil {
t.Errorf("Store.Predecessors(%d) error = %v", i, err)
}
if !equalDescriptorSet(predecessors, want) {
t.Errorf("Store.Predecessors(%d) = %v, want %v", i, predecessors, want)
}
}
}
func TestStore_DeleteWithAutoGC(t *testing.T) {
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
ctx := context.Background()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, subject *ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Subject: subject,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 3
generateManifest(descs[0], nil, descs[1]) // Blob 4
generateManifest(descs[0], nil, descs[2]) // Blob 5
generateManifest(descs[0], nil, descs[3]) // Blob 6
generateIndex(descs[4:6]...) // Blob 7
generateIndex(descs[6]) // Blob 8
appendBlob(ocispec.MediaTypeImageLayer, []byte("world")) // Blob 9
generateManifest(descs[0], &descs[6], descs[9]) // Blob 10
generateManifest(descs[0], &descs[10], descs[2]) // Blob 11
eg, egCtx := errgroup.WithContext(ctx)
for i := range blobs {
eg.Go(func(i int) func() error {
return func() error {
err := s.Push(egCtx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
return fmt.Errorf("failed to push test content to src: %d: %v", i, err)
}
return nil
}
}(i))
}
if err := eg.Wait(); err != nil {
t.Fatal(err)
}
// delete blob 4 and verify the result
if err := s.Delete(egCtx, descs[4]); err != nil {
t.Fatal(err)
}
// blob 1 and 4 are now deleted, and other blobs are still present
notPresent := []ocispec.Descriptor{descs[1], descs[4]}
for _, node := range notPresent {
if exists, _ := s.Exists(egCtx, node); exists {
t.Errorf("%v should not exist in store", node)
}
}
stillPresent := []ocispec.Descriptor{descs[0], descs[2], descs[3], descs[5], descs[6], descs[7], descs[8], descs[9], descs[10], descs[11]}
for _, node := range stillPresent {
if exists, _ := s.Exists(egCtx, node); !exists {
t.Errorf("%v should exist in store", node)
}
}
// delete blob 8 and verify the result
if err := s.Delete(egCtx, descs[8]); err != nil {
t.Fatal(err)
}
// blob 1, 4 and 8 are now deleted, and other blobs are still present
notPresent = []ocispec.Descriptor{descs[1], descs[4], descs[8]}
for _, node := range notPresent {
if exists, _ := s.Exists(egCtx, node); exists {
t.Errorf("%v should not exist in store", node)
}
}
stillPresent = []ocispec.Descriptor{descs[0], descs[2], descs[3], descs[5], descs[6], descs[7], descs[9], descs[10], descs[11]}
for _, node := range stillPresent {
if exists, _ := s.Exists(egCtx, node); !exists {
t.Errorf("%v should exist in store", node)
}
}
// delete blob 6 and verify the result
if err := s.Delete(egCtx, descs[6]); err != nil {
t.Fatal(err)
}
// blob 1, 3, 4, 6, 8, 9, 10, 11 are now deleted, and other blobs are still present
notPresent = []ocispec.Descriptor{descs[1], descs[3], descs[4], descs[6], descs[8], descs[9], descs[10], descs[11]}
for _, node := range notPresent {
if exists, _ := s.Exists(egCtx, node); exists {
t.Errorf("%v should not exist in store", node)
}
}
stillPresent = []ocispec.Descriptor{descs[0], descs[2], descs[5], descs[7]}
for _, node := range stillPresent {
if exists, _ := s.Exists(egCtx, node); !exists {
t.Errorf("%v should exist in store", node)
}
}
// verify predecessors information
wants := [][]ocispec.Descriptor{
{descs[5]}, // Blob 0
nil, // Blob 1
{descs[5]}, // Blob 2
nil, // Blob 3
{descs[7]}, // Blob 4's predecessor is descs[7], even though blob 4 no longer exist
{descs[7]}, // Blob 5
nil, // Blob 6
nil, // Blob 7
nil, // Blob 8
nil, // Blob 9
nil, // Blob 10
nil, // Blob 11
}
for i, want := range wants {
predecessors, err := s.Predecessors(ctx, descs[i])
if err != nil {
t.Errorf("Store.Predecessors(%d) error = %v", i, err)
}
if !equalDescriptorSet(predecessors, want) {
t.Errorf("Store.Predecessors(%d) = %v, want %v", i, predecessors, want)
}
}
}
func TestStore_Untag(t *testing.T) {
content := []byte("test delete")
desc := ocispec.Descriptor{
MediaType: "test-delete",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
ref := "latest"
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("NewDeletableStore() error =", err)
}
ctx := context.Background()
err = s.Push(ctx, desc, bytes.NewReader(content))
if err != nil {
t.Errorf("Store.Push() error = %v, wantErr %v", err, false)
}
err = s.Tag(ctx, desc, ref)
if err != nil {
t.Errorf("error tagging descriptor error = %v, wantErr %v", err, false)
}
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
resolvedDescr, err := s.Resolve(ctx, ref)
if err != nil {
t.Errorf("error resolving descriptor error = %v, wantErr %v", err, false)
}
if !reflect.DeepEqual(resolvedDescr, desc) {
t.Errorf("Store.Resolve() = %v, want %v", resolvedDescr, desc)
}
err = s.Untag(ctx, ref)
if err != nil {
t.Errorf("Store.Untag() = %v, wantErr %v", err, nil)
}
_, err = s.Resolve(ctx, ref)
if !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("error resolving descriptor error = %v, wantErr %v", err, errdef.ErrNotFound)
}
exists, err = s.Exists(ctx, desc)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
}
func TestStore_UntagErrorPath(t *testing.T) {
content := []byte("test delete")
desc := ocispec.Descriptor{
MediaType: "test-delete",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
ref := "latest"
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("NewDeletableStore() error =", err)
}
ctx := context.Background()
err = s.Untag(ctx, "")
if !errors.Is(err, errdef.ErrMissingReference) {
t.Errorf("Store.Untag() error = %v, wantErr %v", err, errdef.ErrMissingReference)
}
err = s.Untag(ctx, "foobar")
if !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("Store.Untag() error = %v, wantErr %v", err, errdef.ErrNotFound)
}
err = s.Push(ctx, desc, bytes.NewReader(content))
if err != nil {
t.Errorf("Store.Push() error = %v, wantErr %v", err, false)
}
err = s.Tag(ctx, desc, ref)
if err != nil {
t.Errorf("error tagging descriptor error = %v, wantErr %v", err, false)
}
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Fatal("Store.Exists() error =", err)
}
if !exists {
t.Errorf("Store.Exists() = %v, want %v", exists, true)
}
resolvedDescr, err := s.Resolve(ctx, ref)
if err != nil {
t.Errorf("error resolving descriptor error = %v, wantErr %v", err, false)
}
err = s.Untag(ctx, resolvedDescr.Digest.String())
if !errors.Is(err, errdef.ErrInvalidReference) {
t.Errorf("Store.Untag() error = %v, wantErr %v", err, errdef.ErrInvalidReference)
}
}
func TestStore_GC(t *testing.T) {
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
ctx := context.Background()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, subject *ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Subject: subject,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateImageIndex := func(manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
generateArtifactManifest := func(blobs ...ocispec.Descriptor) {
var manifest spec.Artifact
manifest.Blobs = append(manifest.Blobs, blobs...)
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(spec.MediaTypeArtifactManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("blob")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("dangling layer")) // Blob 2, dangling layer
generateManifest(descs[0], nil, descs[1]) // Blob 3, valid manifest
generateManifest(descs[0], &descs[3], descs[1]) // Blob 4, referrer of a valid manifest
appendBlob(ocispec.MediaTypeImageLayer, []byte("dangling layer 2")) // Blob 5, dangling layer
generateArtifactManifest(descs[4]) // blob 6, dangling artifact
generateManifest(descs[0], &descs[5], descs[1]) // Blob 7, referrer of a dangling manifest
appendBlob(ocispec.MediaTypeImageLayer, []byte("dangling layer 3")) // Blob 8, dangling layer
generateArtifactManifest(descs[6]) // blob 9, dangling artifact
generateImageIndex(descs[7], descs[5]) // blob 10, dangling image index
appendBlob(ocispec.MediaTypeImageLayer, []byte("garbage layer 1")) // Blob 11, garbage layer 1
generateManifest(descs[0], nil, descs[4]) // Blob 12, garbage manifest 1
appendBlob(ocispec.MediaTypeImageConfig, []byte("garbage config")) // Blob 13, garbage config
appendBlob(ocispec.MediaTypeImageLayer, []byte("garbage layer 2")) // Blob 14, garbage layer 2
generateManifest(descs[6], nil, descs[7]) // Blob 15, garbage manifest 2
generateManifest(descs[0], &descs[13], descs[1]) // Blob 16, referrer of a garbage manifest
appendBlob(ocispec.MediaTypeImageLayer, []byte("another layer")) // Blob 17, untagged manifest
generateManifest(descs[0], nil, descs[17]) // Blob 18, valid untagged manifest
// push blobs 0 - blobs 10 into s
for i := 0; i <= 10; i++ {
err := s.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Errorf("failed to push test content to src: %d: %v", i, err)
}
}
// push blobs 17 - blobs 18 into s
for i := 17; i <= 18; i++ {
err := s.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Errorf("failed to push test content to src: %d: %v", i, err)
}
}
// remove blobs 5 - blobs 10 from index.json
for i := 5; i <= 10; i++ {
s.tagResolver.Untag(string(descs[i].Digest))
}
s.SaveIndex()
// push blobs 11 - blobs 16 into s.storage, making them garbage as their metadata
// doesn't exist in s
for i := 11; i < 17; i++ {
err := s.storage.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Errorf("failed to push test content to src: %d: %v", i, err)
}
}
// confirm that all the blobs are in the storage
for i := 0; i < len(blobs); i++ {
exists, err := s.Exists(ctx, descs[i])
if err != nil {
t.Fatal(err)
}
if !exists {
t.Fatalf("descs[%d] should exist", i)
}
}
// tag manifest blob 3
s.Tag(ctx, descs[3], "latest")
// perform double GC
if err = s.GC(ctx); err != nil {
t.Fatal(err)
}
if err = s.GC(ctx); err != nil {
t.Fatal(err)
}
// verify existence
wantExistence := []bool{true, true, false, true, true, false, false, false,
false, false, false, false, false, false, false, false, false, false, false}
for i, wantValue := range wantExistence {
exists, err := s.Exists(ctx, descs[i])
if err != nil {
t.Fatal(err)
}
if exists != wantValue {
t.Fatalf("want existence %d to be %v, got %v", i, wantValue, exists)
}
}
}
func TestStore_GCAndDeleteOnIndex(t *testing.T) {
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
ctx := context.Background()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, subject *ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Subject: subject,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateImageIndex := func(manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("blob1")) // Blob 1
generateManifest(descs[0], nil, descs[1]) // Blob 2, manifest
appendBlob(ocispec.MediaTypeImageLayer, []byte("blob2")) // Blob 3
generateManifest(descs[0], nil, descs[3]) // Blob 4, manifest
appendBlob(ocispec.MediaTypeImageLayer, []byte("blob3")) // Blob 5
generateManifest(descs[0], nil, descs[5]) // Blob 6, manifest
appendBlob(ocispec.MediaTypeImageLayer, []byte("blob4")) // Blob 7
generateManifest(descs[0], nil, descs[7]) // Blob 8, manifest
generateImageIndex(descs[2], descs[4], descs[6], descs[8]) // blob 9, image index
// push all blobs into the store
for i := 0; i < len(blobs); i++ {
err := s.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Errorf("failed to push test content to src: %d: %v", i, err)
}
}
// confirm that all the blobs are in the storage
for i := 0; i < len(blobs); i++ {
exists, err := s.Exists(ctx, descs[i])
if err != nil {
t.Fatal(err)
}
if !exists {
t.Fatalf("descs[%d] should exist", i)
}
}
// tag manifest blob 8
s.Tag(ctx, descs[8], "latest")
// delete the image index
if err := s.Delete(ctx, descs[9]); err != nil {
t.Fatal(err)
}
// verify existence
wantExistence := []bool{true, false, false, false, false, false, false, true, true, false}
for i, wantValue := range wantExistence {
exists, err := s.Exists(ctx, descs[i])
if err != nil {
t.Fatal(err)
}
if exists != wantValue {
t.Fatalf("want existence %d to be %v, got %v", i, wantValue, exists)
}
}
}
func TestStore_GCErrorPath(t *testing.T) {
tempDir := t.TempDir()
s, err := New(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
ctx := context.Background()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
appendBlob(ocispec.MediaTypeImageLayer, []byte("valid blob")) // Blob 0
// push the valid blob
err = s.Push(ctx, descs[0], bytes.NewReader(blobs[0]))
if err != nil {
t.Error("failed to push test content to src")
}
// write random contents
algPath := path.Join(tempDir, "blobs")
dgstPath := path.Join(algPath, "sha256")
if err := os.WriteFile(path.Join(algPath, "other"), []byte("random"), 0444); err != nil {
t.Fatal("error calling WriteFile(), error =", err)
}
if err := os.WriteFile(path.Join(dgstPath, "other2"), []byte("random2"), 0444); err != nil {
t.Fatal("error calling WriteFile(), error =", err)
}
// perform GC
if err = s.GC(ctx); err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageLayer, []byte("valid blob 2")) // Blob 1
// push the valid blob
err = s.Push(ctx, descs[1], bytes.NewReader(blobs[1]))
if err != nil {
t.Error("failed to push test content to src")
}
// unknown algorithm
if err := os.Mkdir(path.Join(algPath, "sha666"), 0777); err != nil {
t.Fatal(err)
}
if err = s.GC(ctx); err != nil {
t.Fatal("this error should be silently ignored")
}
// os.Remove() error
badDigest := digest.FromBytes([]byte("bad digest")).Encoded()
badPath := path.Join(algPath, "sha256", badDigest)
if err := os.Mkdir(badPath, 0777); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path.Join(badPath, "whatever"), []byte("extra content"), 0444); err != nil {
t.Fatal("error calling WriteFile(), error =", err)
}
if err = s.GC(ctx); err == nil {
t.Fatal("expect an error when os.Remove()")
}
}
func equalDescriptorSet(actual []ocispec.Descriptor, expected []ocispec.Descriptor) bool {
if len(actual) != len(expected) {
return false
}
contains := func(node ocispec.Descriptor) bool {
for _, candidate := range actual {
if reflect.DeepEqual(candidate, node) {
return true
}
}
return false
}
for _, node := range expected {
if !contains(node) {
return false
}
}
return true
}
func Test_isContextDone(t *testing.T) {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
if err := isContextDone(ctx); err != nil {
t.Errorf("expect error = %v, got %v", nil, err)
}
cancel()
if err := isContextDone(ctx); err != context.Canceled {
t.Errorf("expect error = %v, got %v", context.Canceled, err)
}
}
oras-go-2.5.0/content/oci/readonlyoci.go 0000664 0000000 0000000 00000017270 14576745303 0020175 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 oci
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"slices"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/internal/descriptor"
"oras.land/oras-go/v2/internal/fs/tarfs"
"oras.land/oras-go/v2/internal/graph"
"oras.land/oras-go/v2/internal/resolver"
)
// ReadOnlyStore implements `oras.ReadonlyTarget`, and represents a read-only
// content store based on file system with the OCI-Image layout.
// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/image-layout.md
type ReadOnlyStore struct {
fsys fs.FS
storage content.ReadOnlyStorage
tagResolver *resolver.Memory
graph *graph.Memory
}
// NewFromFS creates a new read-only OCI store from fsys.
func NewFromFS(ctx context.Context, fsys fs.FS) (*ReadOnlyStore, error) {
store := &ReadOnlyStore{
fsys: fsys,
storage: NewStorageFromFS(fsys),
tagResolver: resolver.NewMemory(),
graph: graph.NewMemory(),
}
if err := store.validateOCILayoutFile(); err != nil {
return nil, fmt.Errorf("invalid OCI Image Layout: %w", err)
}
if err := store.loadIndexFile(ctx); err != nil {
return nil, fmt.Errorf("invalid OCI Image Index: %w", err)
}
return store, nil
}
// NewFromTar creates a new read-only OCI store from a tar archive located at
// path.
func NewFromTar(ctx context.Context, path string) (*ReadOnlyStore, error) {
tfs, err := tarfs.New(path)
if err != nil {
return nil, err
}
return NewFromFS(ctx, tfs)
}
// Fetch fetches the content identified by the descriptor.
func (s *ReadOnlyStore) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
return s.storage.Fetch(ctx, target)
}
// Exists returns true if the described content exists.
func (s *ReadOnlyStore) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) {
return s.storage.Exists(ctx, target)
}
// Resolve resolves a reference to a descriptor. If the reference to be resolved
// is a tag, the returned descriptor will be a full descriptor declared by
// github.com/opencontainers/image-spec/specs-go/v1. If the reference is a
// digest the returned descriptor will be a plain descriptor (containing only
// the digest, media type and size).
func (s *ReadOnlyStore) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) {
if reference == "" {
return ocispec.Descriptor{}, errdef.ErrMissingReference
}
// attempt resolving manifest
desc, err := s.tagResolver.Resolve(ctx, reference)
if err != nil {
if errors.Is(err, errdef.ErrNotFound) {
// attempt resolving blob
return resolveBlob(s.fsys, reference)
}
return ocispec.Descriptor{}, err
}
if reference == desc.Digest.String() {
return descriptor.Plain(desc), nil
}
return desc, nil
}
// Predecessors returns the nodes directly pointing to the current node.
// Predecessors returns nil without error if the node does not exists in the
// store.
func (s *ReadOnlyStore) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) {
return s.graph.Predecessors(ctx, node)
}
// Tags lists the tags presented in the `index.json` file of the OCI layout,
// returned in ascending order.
// If `last` is NOT empty, the entries in the response start after the tag
// specified by `last`. Otherwise, the response starts from the top of the tags
// list.
//
// See also `Tags()` in the package `registry`.
func (s *ReadOnlyStore) Tags(ctx context.Context, last string, fn func(tags []string) error) error {
return listTags(s.tagResolver, last, fn)
}
// validateOCILayoutFile validates the `oci-layout` file.
func (s *ReadOnlyStore) validateOCILayoutFile() error {
layoutFile, err := s.fsys.Open(ocispec.ImageLayoutFile)
if err != nil {
return fmt.Errorf("failed to open OCI layout file: %w", err)
}
defer layoutFile.Close()
var layout ocispec.ImageLayout
err = json.NewDecoder(layoutFile).Decode(&layout)
if err != nil {
return fmt.Errorf("failed to decode OCI layout file: %w", err)
}
return validateOCILayout(&layout)
}
// validateOCILayout validates layout.
func validateOCILayout(layout *ocispec.ImageLayout) error {
if layout.Version != ocispec.ImageLayoutVersion {
return errdef.ErrUnsupportedVersion
}
return nil
}
// loadIndexFile reads index.json from s.fsys.
func (s *ReadOnlyStore) loadIndexFile(ctx context.Context) error {
indexFile, err := s.fsys.Open(ocispec.ImageIndexFile)
if err != nil {
return fmt.Errorf("failed to open index file: %w", err)
}
defer indexFile.Close()
var index ocispec.Index
if err := json.NewDecoder(indexFile).Decode(&index); err != nil {
return fmt.Errorf("failed to decode index file: %w", err)
}
return loadIndex(ctx, &index, s.storage, s.tagResolver, s.graph)
}
// loadIndex loads index into memory.
func loadIndex(ctx context.Context, index *ocispec.Index, fetcher content.Fetcher, tagger content.Tagger, graph *graph.Memory) error {
for _, desc := range index.Manifests {
if err := tagger.Tag(ctx, deleteAnnotationRefName(desc), desc.Digest.String()); err != nil {
return err
}
if ref := desc.Annotations[ocispec.AnnotationRefName]; ref != "" {
if err := tagger.Tag(ctx, desc, ref); err != nil {
return err
}
}
plain := descriptor.Plain(desc)
if err := graph.IndexAll(ctx, fetcher, plain); err != nil {
return err
}
}
return nil
}
// resolveBlob returns a descriptor describing the blob identified by dgst.
func resolveBlob(fsys fs.FS, dgst string) (ocispec.Descriptor, error) {
path, err := blobPath(digest.Digest(dgst))
if err != nil {
if errors.Is(err, errdef.ErrInvalidDigest) {
return ocispec.Descriptor{}, errdef.ErrNotFound
}
return ocispec.Descriptor{}, err
}
fi, err := fs.Stat(fsys, path)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return ocispec.Descriptor{}, errdef.ErrNotFound
}
return ocispec.Descriptor{}, err
}
return ocispec.Descriptor{
MediaType: descriptor.DefaultMediaType,
Size: fi.Size(),
Digest: digest.Digest(dgst),
}, nil
}
// listTags returns the tags in ascending order.
// If `last` is NOT empty, the entries in the response start after the tag
// specified by `last`. Otherwise, the response starts from the top of the tags
// list.
//
// See also `Tags()` in the package `registry`.
func listTags(tagResolver *resolver.Memory, last string, fn func(tags []string) error) error {
var tags []string
tagMap := tagResolver.Map()
for tag, desc := range tagMap {
if tag == desc.Digest.String() {
continue
}
if last != "" && tag <= last {
continue
}
tags = append(tags, tag)
}
slices.Sort(tags)
return fn(tags)
}
// deleteAnnotationRefName deletes the AnnotationRefName from the annotation map
// of desc.
func deleteAnnotationRefName(desc ocispec.Descriptor) ocispec.Descriptor {
if _, ok := desc.Annotations[ocispec.AnnotationRefName]; !ok {
// no ops
return desc
}
size := len(desc.Annotations) - 1
if size == 0 {
desc.Annotations = nil
return desc
}
annotations := make(map[string]string, size)
for k, v := range desc.Annotations {
if k != ocispec.AnnotationRefName {
annotations[k] = v
}
}
desc.Annotations = annotations
return desc
}
oras-go-2.5.0/content/oci/readonlyoci_test.go 0000664 0000000 0000000 00000060167 14576745303 0021237 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 oci
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"reflect"
"strconv"
"strings"
"testing"
"testing/fstest"
"github.com/opencontainers/go-digest"
specs "github.com/opencontainers/image-spec/specs-go"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"golang.org/x/sync/errgroup"
"oras.land/oras-go/v2"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/content/memory"
"oras.land/oras-go/v2/internal/docker"
"oras.land/oras-go/v2/internal/spec"
"oras.land/oras-go/v2/registry"
)
func TestReadonlyStoreInterface(t *testing.T) {
var store interface{} = &ReadOnlyStore{}
if _, ok := store.(oras.ReadOnlyGraphTarget); !ok {
t.Error("&ReadOnlyStore{} does not conform oras.ReadOnlyGraphTarget")
}
if _, ok := store.(registry.TagLister); !ok {
t.Error("&ReadOnlyStore{} does not conform registry.TagLister")
}
}
func TestReadOnlyStore(t *testing.T) {
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
MediaType: ocispec.MediaTypeImageManifest,
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(manifest.MediaType, manifestJSON)
}
generateArtifactManifest := func(subject ocispec.Descriptor, blobs ...ocispec.Descriptor) {
manifest := spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
Subject: &subject,
Blobs: blobs,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(manifest.MediaType, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foobar")) // Blob 1
generateManifest(descs[0], descs[1]) // Blob 2
generateArtifactManifest(descs[2]) // Blob 3
subjectTag := "subject"
layout := ocispec.ImageLayout{
Version: ocispec.ImageLayoutVersion,
}
layoutJSON, err := json.Marshal(layout)
if err != nil {
t.Fatalf("failed to marshal OCI layout: %v", err)
}
index := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value
},
Manifests: []ocispec.Descriptor{
{
MediaType: descs[2].MediaType,
Size: descs[2].Size,
Digest: descs[2].Digest,
Annotations: map[string]string{ocispec.AnnotationRefName: subjectTag},
},
descs[3],
},
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatalf("failed to marshal index: %v", err)
}
// build fs
fsys := fstest.MapFS{}
for i, desc := range descs {
path := strings.Join([]string{"blobs", desc.Digest.Algorithm().String(), desc.Digest.Encoded()}, "/")
fsys[path] = &fstest.MapFile{Data: blobs[i]}
}
fsys[ocispec.ImageLayoutFile] = &fstest.MapFile{Data: layoutJSON}
fsys["index.json"] = &fstest.MapFile{Data: indexJSON}
// test read-only store
ctx := context.Background()
s, err := NewFromFS(ctx, fsys)
if err != nil {
t.Fatal("NewFromFS() error =", err)
}
// test resolving subject by digest
gotDesc, err := s.Resolve(ctx, descs[2].Digest.String())
if err != nil {
t.Error("ReadOnlyStore.Resolve() error =", err)
}
if want := descs[2]; !reflect.DeepEqual(gotDesc, want) {
t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, want)
}
// test resolving subject by tag
gotDesc, err = s.Resolve(ctx, subjectTag)
if err != nil {
t.Error("ReadOnlyStore.Resolve() error =", err)
}
if want := descs[2]; !content.Equal(gotDesc, want) {
t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, want)
}
// descriptor resolved by tag should have annotations
if gotDesc.Annotations[ocispec.AnnotationRefName] != subjectTag {
t.Errorf("ReadOnlyStore.Resolve() returned descriptor without annotations %v, want %v",
gotDesc.Annotations,
map[string]string{ocispec.AnnotationRefName: subjectTag})
}
// test resolving artifact by digest
gotDesc, err = s.Resolve(ctx, descs[3].Digest.String())
if err != nil {
t.Error("ReadOnlyStore.Resolve() error =", err)
}
if want := descs[3]; !reflect.DeepEqual(gotDesc, want) {
t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, want)
}
// test resolving blob by digest
gotDesc, err = s.Resolve(ctx, descs[0].Digest.String())
if err != nil {
t.Error("ReadOnlyStore.Resolve() error =", err)
}
if want := descs[0]; gotDesc.Size != want.Size || gotDesc.Digest != want.Digest {
t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, want)
}
// test fetching blobs
eg, egCtx := errgroup.WithContext(ctx)
for i := range blobs {
eg.Go(func(i int) func() error {
return func() error {
rc, err := s.Fetch(egCtx, descs[i])
if err != nil {
return fmt.Errorf("ReadOnlyStore.Fetch(%d) error = %v", i, err)
}
got, err := io.ReadAll(rc)
if err != nil {
return fmt.Errorf("ReadOnlyStore.Fetch(%d).Read() error = %v", i, err)
}
err = rc.Close()
if err != nil {
return fmt.Errorf("ReadOnlyStore.Fetch(%d).Close() error = %v", i, err)
}
if !bytes.Equal(got, blobs[i]) {
return fmt.Errorf("ReadOnlyStore.Fetch(%d) = %v, want %v", i, got, blobs[i])
}
return nil
}
}(i))
}
if err := eg.Wait(); err != nil {
t.Fatal(err)
}
// test predecessors
wants := [][]ocispec.Descriptor{
{descs[2]}, // blob 0
{descs[2]}, // blob 1
{descs[3]}, // blob 2,
{}, // blob 3
}
for i, want := range wants {
predecessors, err := s.Predecessors(ctx, descs[i])
if err != nil {
t.Errorf("ReadOnlyStore.Predecessors(%d) error = %v", i, err)
}
if !equalDescriptorSet(predecessors, want) {
t.Errorf("ReadOnlyStore.Predecessors(%d) = %v, want %v", i, predecessors, want)
}
}
}
func TestReadOnlyStore_DirFS(t *testing.T) {
tempDir := t.TempDir()
// build an OCI layout on disk
s, err := New(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
generateArtifactManifest := func(subject ocispec.Descriptor, blobs ...ocispec.Descriptor) {
var manifest spec.Artifact
manifest.Subject = &subject
manifest.Blobs = append(manifest.Blobs, blobs...)
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(spec.MediaTypeArtifactManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 3
generateManifest(descs[0], descs[1:3]...) // Blob 4
generateManifest(descs[0], descs[3]) // Blob 5
generateManifest(descs[0], descs[1:4]...) // Blob 6
generateIndex(descs[4:6]...) // Blob 7
generateIndex(descs[6]) // Blob 8
generateIndex() // Blob 9
generateIndex(descs[7:10]...) // Blob 10
appendBlob(ocispec.MediaTypeImageLayer, []byte("sig_1")) // Blob 11
generateArtifactManifest(descs[6], descs[11]) // Blob 12
appendBlob(ocispec.MediaTypeImageLayer, []byte("sig_2")) // Blob 13
generateArtifactManifest(descs[10], descs[13]) // Blob 14
ctx := context.Background()
eg, egCtx := errgroup.WithContext(ctx)
for i := range blobs {
eg.Go(func(i int) func() error {
return func() error {
err := s.Push(egCtx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
return fmt.Errorf("failed to push test content to src: %d: %v", i, err)
}
return nil
}
}(i))
}
if err := eg.Wait(); err != nil {
t.Fatal(err)
}
// tag index root
indexRoot := descs[10]
tag := "latest"
if err := s.Tag(ctx, indexRoot, tag); err != nil {
t.Fatal("Tag() error =", err)
}
// test read-only store
readonlyS, err := NewFromFS(ctx, os.DirFS(tempDir))
if err != nil {
t.Fatal("New() error =", err)
}
// test resolving index root by tag
gotDesc, err := readonlyS.Resolve(ctx, tag)
if err != nil {
t.Fatal("ReadOnlyStore: Resolve() error =", err)
}
if !content.Equal(gotDesc, indexRoot) {
t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, indexRoot)
}
// test resolving index root by digest
gotDesc, err = readonlyS.Resolve(ctx, indexRoot.Digest.String())
if err != nil {
t.Fatal("ReadOnlyStore: Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, indexRoot) {
t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, indexRoot)
}
// test resolving artifact manifest by digest
artifactRootDesc := descs[12]
gotDesc, err = readonlyS.Resolve(ctx, artifactRootDesc.Digest.String())
if err != nil {
t.Fatal("ReadOnlyStore: Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, artifactRootDesc) {
t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, artifactRootDesc)
}
// test resolving blob by digest
gotDesc, err = readonlyS.Resolve(ctx, descs[0].Digest.String())
if err != nil {
t.Fatal("ReadOnlyStore: Resolve() error =", err)
}
if want := descs[0]; gotDesc.Size != want.Size || gotDesc.Digest != want.Digest {
t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, want)
}
// test fetching blobs
for i := range blobs {
eg.Go(func(i int) func() error {
return func() error {
rc, err := s.Fetch(egCtx, descs[i])
if err != nil {
return fmt.Errorf("ReadOnlyStore.Fetch(%d) error = %v", i, err)
}
got, err := io.ReadAll(rc)
if err != nil {
return fmt.Errorf("ReadOnlyStore.Fetch(%d).Read() error = %v", i, err)
}
err = rc.Close()
if err != nil {
return fmt.Errorf("ReadOnlyStore.Fetch(%d).Close() error = %v", i, err)
}
if !bytes.Equal(got, blobs[i]) {
return fmt.Errorf("ReadOnlyStore.Fetch(%d) = %v, want %v", i, got, blobs[i])
}
return nil
}
}(i))
}
if err := eg.Wait(); err != nil {
t.Fatal(err)
}
// verify predecessors
wants := [][]ocispec.Descriptor{
descs[4:7], // Blob 0
{descs[4], descs[6]}, // Blob 1
{descs[4], descs[6]}, // Blob 2
{descs[5], descs[6]}, // Blob 3
{descs[7]}, // Blob 4
{descs[7]}, // Blob 5
{descs[8], descs[12]}, // Blob 6
{descs[10]}, // Blob 7
{descs[10]}, // Blob 8
{descs[10]}, // Blob 9
{descs[14]}, // Blob 10
{descs[12]}, // Blob 11
nil, // Blob 12, no predecessors
{descs[14]}, // Blob 13
nil, // Blob 14, no predecessors
}
for i, want := range wants {
predecessors, err := readonlyS.Predecessors(ctx, descs[i])
if err != nil {
t.Errorf("ReadOnlyStore.Predecessors(%d) error = %v", i, err)
}
if !equalDescriptorSet(predecessors, want) {
t.Errorf("ReadOnlyStore.Predecessors(%d) = %v, want %v", i, predecessors, want)
}
}
}
/*
testdata/hello-world.tar contains:
blobs/
sha256/
2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54 // image layer
f54a58bc1aac5ea1a25d796ae155dc228b3f0e11d046ae276b39c4bf2f13d8c4 // image manifest
faa03e786c97f07ef34423fccceeec2398ec8a5759259f94d99078f264e9d7af // manifest list
feb5d9fea6a5e9606aa995e879d862b825965ba48de054caab5ef356dc6b3412 // config
index.json
manifest.json
oci-layout
*/
func TestReadOnlyStore_TarFS(t *testing.T) {
ctx := context.Background()
s, err := NewFromTar(ctx, "testdata/hello-world.tar")
if err != nil {
t.Fatal("New() error =", err)
}
// test data in testdata/hello-world.tar
descs := []ocispec.Descriptor{
// desc 0: config
{
MediaType: "application/vnd.docker.container.image.v1+json",
Size: 1469,
Digest: "sha256:feb5d9fea6a5e9606aa995e879d862b825965ba48de054caab5ef356dc6b3412",
},
// desc 1: layer
{
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
Size: 2479,
Digest: "sha256:2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54",
},
// desc 2: image manifest
{
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
Digest: "sha256:f54a58bc1aac5ea1a25d796ae155dc228b3f0e11d046ae276b39c4bf2f13d8c4",
Size: 525,
Platform: &ocispec.Platform{
Architecture: "amd64",
OS: "linux",
},
},
// desc 3: manifest list
{
MediaType: docker.MediaTypeManifestList,
Size: 2561,
Digest: "sha256:faa03e786c97f07ef34423fccceeec2398ec8a5759259f94d99078f264e9d7af",
},
}
// test resolving by tag
for _, desc := range descs {
gotDesc, err := s.Resolve(ctx, desc.Digest.String())
if err != nil {
t.Fatal("ReadOnlyStore: Resolve() error =", err)
}
if want := desc; gotDesc.Size != want.Size || gotDesc.Digest != want.Digest {
t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, want)
}
}
// test resolving by tag
gotDesc, err := s.Resolve(ctx, "latest")
if err != nil {
t.Fatal("ReadOnlyStore: Resolve() error =", err)
}
if want := descs[3]; gotDesc.Size != want.Size || gotDesc.Digest != want.Digest {
t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, want)
}
// test Predecessors
wantPredecessors := [][]ocispec.Descriptor{
{descs[2]}, // desc 0
{descs[2]}, // desc 1
{descs[3]}, // desc 2
{}, // desc 3
}
for i, want := range wantPredecessors {
predecessors, err := s.Predecessors(ctx, descs[i])
if err != nil {
t.Errorf("ReadOnlyStore.Predecessors(%d) error = %v", i, err)
}
if !equalDescriptorSet(predecessors, want) {
t.Errorf("ReadOnlyStore.Predecessors(%d) = %v, want %v", i, predecessors, want)
}
}
}
func TestReadOnlyStore_BadIndex(t *testing.T) {
content := []byte("whatever")
fsys := fstest.MapFS{
"index.json": &fstest.MapFile{Data: content},
}
ctx := context.Background()
_, err := NewFromFS(ctx, fsys)
if err == nil {
t.Errorf("NewFromFS() error = %v, wantErr %v", err, true)
}
}
func TestReadOnlyStore_BadLayout(t *testing.T) {
content := []byte("whatever")
fsys := fstest.MapFS{
ocispec.ImageLayoutFile: &fstest.MapFile{Data: content},
}
ctx := context.Background()
_, err := NewFromFS(ctx, fsys)
if err == nil {
t.Errorf("NewFromFS() error = %v, wantErr %v", err, true)
}
}
func TestReadOnlyStore_Copy_OCIToMemory(t *testing.T) {
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
MediaType: ocispec.MediaTypeImageManifest,
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(manifest.MediaType, manifestJSON)
}
generateArtifactManifest := func(subject ocispec.Descriptor, blobs ...ocispec.Descriptor) {
manifest := spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
Subject: &subject,
Blobs: blobs,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(manifest.MediaType, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foobar")) // Blob 1
generateManifest(descs[0], descs[1]) // Blob 2
generateArtifactManifest(descs[2]) // Blob 3
tag := "foobar"
root := descs[3]
layout := ocispec.ImageLayout{
Version: ocispec.ImageLayoutVersion,
}
layoutJSON, err := json.Marshal(layout)
if err != nil {
t.Fatalf("failed to marshal OCI layout: %v", err)
}
index := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value
},
Manifests: []ocispec.Descriptor{
{
MediaType: descs[3].MediaType,
Digest: descs[3].Digest,
Size: descs[3].Size,
Annotations: map[string]string{
ocispec.AnnotationRefName: tag,
},
},
},
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatalf("failed to marshal index: %v", err)
}
// build fs
fsys := fstest.MapFS{}
for i, desc := range descs {
path := strings.Join([]string{"blobs", desc.Digest.Algorithm().String(), desc.Digest.Encoded()}, "/")
fsys[path] = &fstest.MapFile{Data: blobs[i]}
}
fsys[ocispec.ImageLayoutFile] = &fstest.MapFile{Data: layoutJSON}
fsys["index.json"] = &fstest.MapFile{Data: indexJSON}
// test read-only store
ctx := context.Background()
src, err := NewFromFS(ctx, fsys)
if err != nil {
t.Fatal("NewFromFS() error =", err)
}
// test copy
dst := memory.New()
gotDesc, err := oras.Copy(ctx, src, tag, dst, "", oras.DefaultCopyOptions)
if err != nil {
t.Fatalf("Copy() error = %v, wantErr %v", err, false)
}
if !content.Equal(gotDesc, root) {
t.Errorf("Copy() = %v, want %v", gotDesc, root)
}
// verify contents
for i, desc := range descs {
exists, err := dst.Exists(ctx, desc)
if err != nil {
t.Fatalf("dst.Exists(%d) error = %v", i, err)
}
if !exists {
t.Errorf("dst.Exists(%d) = %v, want %v", i, exists, true)
}
}
// verify tag
gotDesc, err = dst.Resolve(ctx, tag)
if err != nil {
t.Fatal("dst.Resolve() error =", err)
}
if !content.Equal(gotDesc, root) {
t.Errorf("dst.Resolve() = %v, want %v", gotDesc, root)
}
}
func TestReadOnlyStore_Tags(t *testing.T) {
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
MediaType: ocispec.MediaTypeImageManifest,
Config: config,
Layers: layers,
}
// add annotation to make each manifest unique
manifest.Annotations = map[string]string{
"blob_index": strconv.Itoa(len(blobs)),
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(manifest.MediaType, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foobar")) // Blob 1
generateManifest(descs[0], descs[1]) // Blob 2
generateManifest(descs[0], descs[1]) // Blob 3
generateManifest(descs[0], descs[1]) // Blob 4
generateManifest(descs[0], descs[1]) // Blob 5
generateManifest(descs[0], descs[1]) // Blob 6
layout := ocispec.ImageLayout{
Version: ocispec.ImageLayoutVersion,
}
layoutJSON, err := json.Marshal(layout)
if err != nil {
t.Fatalf("failed to marshal OCI layout: %v", err)
}
index := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value
},
}
for _, desc := range descs[2:] {
index.Manifests = append(index.Manifests, ocispec.Descriptor{
MediaType: desc.MediaType,
Size: desc.Size,
Digest: desc.Digest,
})
}
index.Manifests[1].Annotations = map[string]string{ocispec.AnnotationRefName: "v2"}
index.Manifests[2].Annotations = map[string]string{ocispec.AnnotationRefName: "v3"}
index.Manifests[3].Annotations = map[string]string{ocispec.AnnotationRefName: "v1"}
index.Manifests[4].Annotations = map[string]string{ocispec.AnnotationRefName: "v4"}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatalf("failed to marshal index: %v", err)
}
// build fs
fsys := fstest.MapFS{}
for i, desc := range descs {
path := strings.Join([]string{"blobs", desc.Digest.Algorithm().String(), desc.Digest.Encoded()}, "/")
fsys[path] = &fstest.MapFile{Data: blobs[i]}
}
fsys[ocispec.ImageLayoutFile] = &fstest.MapFile{Data: layoutJSON}
fsys["index.json"] = &fstest.MapFile{Data: indexJSON}
// test read-only store
ctx := context.Background()
s, err := NewFromFS(ctx, fsys)
if err != nil {
t.Fatal("NewFromFS() error =", err)
}
// test tags
tests := []struct {
name string
last string
want []string
}{
{
name: "list all tags",
want: []string{"v1", "v2", "v3", "v4"},
},
{
name: "list from middle",
last: "v2",
want: []string{"v3", "v4"},
},
{
name: "list from end",
last: "v4",
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := s.Tags(ctx, tt.last, func(got []string) error {
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ReadOnlyStore.Tags() = %v, want %v", got, tt.want)
}
return nil
}); err != nil {
t.Errorf("ReadOnlyStore.Tags() error = %v", err)
}
})
}
wantErr := errors.New("expected error")
if err := s.Tags(ctx, "", func(got []string) error {
return wantErr
}); err != wantErr {
t.Errorf("ReadOnlyStore.Tags() error = %v, wantErr %v", err, wantErr)
}
}
func Test_deleteAnnotationRefName(t *testing.T) {
tests := []struct {
name string
desc ocispec.Descriptor
want ocispec.Descriptor
}{
{
name: "No annotation",
desc: ocispec.Descriptor{},
want: ocispec.Descriptor{},
},
{
name: "Nil annotation",
desc: ocispec.Descriptor{Annotations: nil},
want: ocispec.Descriptor{},
},
{
name: "Empty annotation",
desc: ocispec.Descriptor{Annotations: map[string]string{}},
want: ocispec.Descriptor{Annotations: map[string]string{}},
},
{
name: "No RefName",
desc: ocispec.Descriptor{Annotations: map[string]string{"foo": "bar"}},
want: ocispec.Descriptor{Annotations: map[string]string{"foo": "bar"}},
},
{
name: "Empty RefName",
desc: ocispec.Descriptor{Annotations: map[string]string{
"foo": "bar",
ocispec.AnnotationRefName: "",
}},
want: ocispec.Descriptor{Annotations: map[string]string{"foo": "bar"}},
},
{
name: "RefName only",
desc: ocispec.Descriptor{Annotations: map[string]string{ocispec.AnnotationRefName: "foobar"}},
want: ocispec.Descriptor{},
},
{
name: "Multiple annotations with RefName",
desc: ocispec.Descriptor{Annotations: map[string]string{
"foo": "bar",
ocispec.AnnotationRefName: "foobar",
}},
want: ocispec.Descriptor{Annotations: map[string]string{"foo": "bar"}},
},
{
name: "Multiple annotations with empty RefName",
desc: ocispec.Descriptor{Annotations: map[string]string{
"foo": "bar",
ocispec.AnnotationRefName: "",
}},
want: ocispec.Descriptor{Annotations: map[string]string{"foo": "bar"}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := deleteAnnotationRefName(tt.desc); !reflect.DeepEqual(got, tt.want) {
t.Errorf("deleteAnnotationRefName() = %v, want %v", got, tt.want)
}
})
}
}
oras-go-2.5.0/content/oci/readonlystorage.go 0000664 0000000 0000000 00000005407 14576745303 0021066 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 oci
import (
"context"
"errors"
"fmt"
"io"
"io/fs"
"path"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/internal/fs/tarfs"
)
// ReadOnlyStorage is a read-only CAS based on file system with the OCI-Image
// layout.
// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/image-layout.md
type ReadOnlyStorage struct {
fsys fs.FS
}
// NewStorageFromFS creates a new read-only CAS from fsys.
func NewStorageFromFS(fsys fs.FS) *ReadOnlyStorage {
return &ReadOnlyStorage{
fsys: fsys,
}
}
// NewStorageFromTar creates a new read-only CAS from a tar archive located at
// path.
func NewStorageFromTar(path string) (*ReadOnlyStorage, error) {
tfs, err := tarfs.New(path)
if err != nil {
return nil, err
}
return NewStorageFromFS(tfs), nil
}
// Fetch fetches the content identified by the descriptor.
func (s *ReadOnlyStorage) Fetch(_ context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
path, err := blobPath(target.Digest)
if err != nil {
return nil, fmt.Errorf("%s: %s: %w", target.Digest, target.MediaType, errdef.ErrInvalidDigest)
}
fp, err := s.fsys.Open(path)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil, fmt.Errorf("%s: %s: %w", target.Digest, target.MediaType, errdef.ErrNotFound)
}
return nil, err
}
return fp, nil
}
// Exists returns true if the described content Exists.
func (s *ReadOnlyStorage) Exists(_ context.Context, target ocispec.Descriptor) (bool, error) {
path, err := blobPath(target.Digest)
if err != nil {
return false, fmt.Errorf("%s: %s: %w", target.Digest, target.MediaType, errdef.ErrInvalidDigest)
}
_, err = fs.Stat(s.fsys, path)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return false, nil
}
return false, err
}
return true, nil
}
// blobPath calculates blob path from the given digest.
func blobPath(dgst digest.Digest) (string, error) {
if err := dgst.Validate(); err != nil {
return "", fmt.Errorf("cannot calculate blob path from invalid digest %s: %w: %v",
dgst.String(), errdef.ErrInvalidDigest, err)
}
return path.Join(ocispec.ImageBlobsDir, dgst.Algorithm().String(), dgst.Encoded()), nil
}
oras-go-2.5.0/content/oci/readonlystorage_test.go 0000664 0000000 0000000 00000016300 14576745303 0022117 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 oci
import (
"bytes"
"context"
"errors"
"io"
"os"
"path/filepath"
"strings"
"testing"
"testing/fstest"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/internal/docker"
)
func TestReadOnlyStorage_Exists(t *testing.T) {
blob := []byte("test")
dgst := digest.FromBytes(blob)
desc := content.NewDescriptorFromBytes("", blob)
fsys := fstest.MapFS{
strings.Join([]string{"blobs", dgst.Algorithm().String(), dgst.Encoded()}, "/"): {},
}
s := NewStorageFromFS(fsys)
ctx := context.Background()
// test sha256
got, err := s.Exists(ctx, desc)
if err != nil {
t.Fatal("ReadOnlyStorage.Exists() error =", err)
}
if want := true; got != want {
t.Errorf("ReadOnlyStorage.Exists() = %v, want %v", got, want)
}
// test not found
blob = []byte("whaterver")
desc = content.NewDescriptorFromBytes("", blob)
got, err = s.Exists(ctx, desc)
if err != nil {
t.Fatal("ReadOnlyStorage.Exists() error =", err)
}
if want := false; got != want {
t.Errorf("ReadOnlyStorage.Exists() = %v, want %v", got, want)
}
// test invalid digest
desc = ocispec.Descriptor{Digest: "not a digest"}
_, err = s.Exists(ctx, desc)
if err == nil {
t.Fatalf("ReadOnlyStorage.Exists() error = %v, wantErr %v", err, true)
}
}
func TestReadOnlyStorage_Fetch(t *testing.T) {
blob := []byte("test")
dgst := digest.FromBytes(blob)
desc := content.NewDescriptorFromBytes("", blob)
fsys := fstest.MapFS{
strings.Join([]string{"blobs", dgst.Algorithm().String(), dgst.Encoded()}, "/"): {
Data: blob,
},
}
s := NewStorageFromFS(fsys)
ctx := context.Background()
// test fetch
rc, err := s.Fetch(ctx, desc)
if err != nil {
t.Fatal("ReadOnlyStorage.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("ReadOnlyStorage.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("ReadOnlyStorage.Fetch().Close() error =", err)
}
if !bytes.Equal(got, blob) {
t.Errorf("ReadOnlyStorage.Fetch() = %v, want %v", got, blob)
}
// test not found
anotherBlob := []byte("whatever")
desc = content.NewDescriptorFromBytes("", anotherBlob)
_, err = s.Fetch(ctx, desc)
if !errors.Is(err, errdef.ErrNotFound) {
t.Fatalf("ReadOnlyStorage.Fetch() error = %v, wantErr %v", err, errdef.ErrNotFound)
}
// test invalid digest
desc = ocispec.Descriptor{Digest: "not a digest"}
_, err = s.Fetch(ctx, desc)
if err == nil {
t.Fatalf("ReadOnlyStorage.Fetch() error = %v, wantErr %v", err, true)
}
}
func TestReadOnlyStorage_DirFS(t *testing.T) {
tempDir := t.TempDir()
blob := []byte("test")
dgst := digest.FromBytes(blob)
desc := content.NewDescriptorFromBytes("test", blob)
// write blob to disk
path := filepath.Join(tempDir, "blobs", dgst.Algorithm().String(), dgst.Encoded())
if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil {
t.Fatal("error calling Mkdir(), error =", err)
}
if err := os.WriteFile(path, blob, 0444); err != nil {
t.Fatal("error calling WriteFile(), error =", err)
}
s := NewStorageFromFS(os.DirFS(tempDir))
ctx := context.Background()
// test Exists
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Fatal("ReadOnlyStorage.Exists() error =", err)
}
if !exists {
t.Errorf("ReadOnlyStorage.Exists() = %v, want %v", exists, true)
}
// test Fetch
rc, err := s.Fetch(ctx, desc)
if err != nil {
t.Fatal("ReadOnlyStorage.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("ReadOnlyStorage.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("ReadOnlyStorage.Fetch().Close() error =", err)
}
if !bytes.Equal(got, blob) {
t.Errorf("ReadOnlyStorage.Fetch() = %v, want %v", got, blob)
}
}
func TestReadOnlyStorage_TarFS(t *testing.T) {
s, err := NewStorageFromTar("testdata/hello-world.tar")
if err != nil {
t.Fatal("NewStorageFromTar() error =", err)
}
ctx := context.Background()
// test data in testdata/hello-world.tar
blob := []byte(`{"architecture":"amd64","config":{"Hostname":"","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/hello"],"Image":"sha256:b9935d4e8431fb1a7f0989304ec86b3329a99a25f5efdc7f09f3f8c41434ca6d","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"container":"8746661ca3c2f215da94e6d3f7dfdcafaff5ec0b21c9aff6af3dc379a82fbc72","container_config":{"Hostname":"8746661ca3c2","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\"/hello\"]"],"Image":"sha256:b9935d4e8431fb1a7f0989304ec86b3329a99a25f5efdc7f09f3f8c41434ca6d","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{}},"created":"2021-09-23T23:47:57.442225064Z","docker_version":"20.10.7","history":[{"created":"2021-09-23T23:47:57.098990892Z","created_by":"/bin/sh -c #(nop) COPY file:50563a97010fd7ce1ceebd1fa4f4891ac3decdf428333fb2683696f4358af6c2 in / "},{"created":"2021-09-23T23:47:57.442225064Z","created_by":"/bin/sh -c #(nop) CMD [\"/hello\"]","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:e07ee1baac5fae6a26f30cabfe54a36d3402f96afda318fe0a96cec4ca393359"]}}`)
desc := content.NewDescriptorFromBytes(docker.MediaTypeManifest, blob)
// test Exists
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Fatal("ReadOnlyStorage.Exists() error =", err)
}
if want := true; exists != want {
t.Errorf("ReadOnlyStorage.Exists() = %v, want %v", exists, want)
}
// test Fetch
rc, err := s.Fetch(ctx, desc)
if err != nil {
t.Fatal("ReadOnlyStorage.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("ReadOnlyStorage.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("ReadOnlyStorage.Fetch().Close() error =", err)
}
if !bytes.Equal(got, blob) {
t.Errorf("ReadOnlyStorage.Fetch() = %v, want %v", got, blob)
}
// test Exists against a non-existing content
blob = []byte("whatever")
desc = content.NewDescriptorFromBytes("", blob)
exists, err = s.Exists(ctx, desc)
if err != nil {
t.Fatal("ReadOnlyStorage.Exists() error =", err)
}
if want := false; exists != want {
t.Errorf("ReadOnlyStorage.Exists() = %v, want %v", exists, want)
}
// test Fetch against a non-existing content
_, err = s.Fetch(ctx, desc)
if want := errdef.ErrNotFound; !errors.Is(err, want) {
t.Errorf("ReadOnlyStorage.Fetch() error = %v, wantErr %v", err, want)
}
}
oras-go-2.5.0/content/oci/storage.go 0000664 0000000 0000000 00000012004 14576745303 0017317 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 oci
import (
"context"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"sync"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/internal/ioutil"
)
// bufPool is a pool of byte buffers that can be reused for copying content
// between files.
var bufPool = sync.Pool{
New: func() interface{} {
// the buffer size should be larger than or equal to 128 KiB
// for performance considerations.
// we choose 1 MiB here so there will be less disk I/O.
buffer := make([]byte, 1<<20) // buffer size = 1 MiB
return &buffer
},
}
// Storage is a CAS based on file system with the OCI-Image layout.
// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/image-layout.md
type Storage struct {
*ReadOnlyStorage
// root is the root directory of the OCI layout.
root string
// ingestRoot is the root directory of the temporary ingest files.
ingestRoot string
}
// NewStorage creates a new CAS based on file system with the OCI-Image layout.
func NewStorage(root string) (*Storage, error) {
rootAbs, err := filepath.Abs(root)
if err != nil {
return nil, fmt.Errorf("failed to resolve absolute path for %s: %w", root, err)
}
return &Storage{
ReadOnlyStorage: NewStorageFromFS(os.DirFS(rootAbs)),
root: rootAbs,
ingestRoot: filepath.Join(rootAbs, "ingest"),
}, nil
}
// Push pushes the content, matching the expected descriptor.
func (s *Storage) Push(_ context.Context, expected ocispec.Descriptor, content io.Reader) error {
path, err := blobPath(expected.Digest)
if err != nil {
return fmt.Errorf("%s: %s: %w", expected.Digest, expected.MediaType, errdef.ErrInvalidDigest)
}
target := filepath.Join(s.root, path)
// check if the target content already exists in the blob directory.
if _, err := os.Stat(target); err == nil {
return fmt.Errorf("%s: %s: %w", expected.Digest, expected.MediaType, errdef.ErrAlreadyExists)
} else if !os.IsNotExist(err) {
return err
}
if err := ensureDir(filepath.Dir(target)); err != nil {
return err
}
// write the content to a temporary ingest file.
ingest, err := s.ingest(expected, content)
if err != nil {
return err
}
// move the content from the temporary ingest file to the target path.
// since blobs are read-only once stored, if the target blob already exists,
// Rename() will fail for permission denied when trying to overwrite it.
if err := os.Rename(ingest, target); err != nil {
// remove the ingest file in case of error
os.Remove(ingest)
if errors.Is(err, os.ErrPermission) {
return fmt.Errorf("%s: %s: %w", expected.Digest, expected.MediaType, errdef.ErrAlreadyExists)
}
return err
}
return nil
}
// Delete removes the target from the system.
func (s *Storage) Delete(ctx context.Context, target ocispec.Descriptor) error {
path, err := blobPath(target.Digest)
if err != nil {
return fmt.Errorf("%s: %s: %w", target.Digest, target.MediaType, errdef.ErrInvalidDigest)
}
targetPath := filepath.Join(s.root, path)
err = os.Remove(targetPath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("%s: %s: %w", target.Digest, target.MediaType, errdef.ErrNotFound)
}
return err
}
return nil
}
// ingest write the content into a temporary ingest file.
func (s *Storage) ingest(expected ocispec.Descriptor, content io.Reader) (path string, ingestErr error) {
if err := ensureDir(s.ingestRoot); err != nil {
return "", fmt.Errorf("failed to ensure ingest dir: %w", err)
}
// create a temp file with the file name format "blobDigest_randomString"
// in the ingest directory.
// Go ensures that multiple programs or goroutines calling CreateTemp
// simultaneously will not choose the same file.
fp, err := os.CreateTemp(s.ingestRoot, expected.Digest.Encoded()+"_*")
if err != nil {
return "", fmt.Errorf("failed to create ingest file: %w", err)
}
path = fp.Name()
defer func() {
// remove the temp file in case of error.
// this executes after the file is closed.
if ingestErr != nil {
os.Remove(path)
}
}()
defer fp.Close()
buf := bufPool.Get().(*[]byte)
defer bufPool.Put(buf)
if err := ioutil.CopyBuffer(fp, content, *buf, expected); err != nil {
return "", fmt.Errorf("failed to ingest: %w", err)
}
// change to readonly
if err := os.Chmod(path, 0444); err != nil {
return "", fmt.Errorf("failed to make readonly: %w", err)
}
return
}
// ensureDir ensures the directories of the path exists.
func ensureDir(path string) error {
return os.MkdirAll(path, 0777)
}
oras-go-2.5.0/content/oci/storage_test.go 0000664 0000000 0000000 00000023410 14576745303 0020361 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 oci
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"testing"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"golang.org/x/sync/errgroup"
"oras.land/oras-go/v2/errdef"
)
func TestStorage_Success(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
tempDir := t.TempDir()
s, err := NewStorage(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
ctx := context.Background()
// test push
err = s.Push(ctx, desc, bytes.NewReader(content))
if err != nil {
t.Fatal("Storage.Push() error =", err)
}
// test fetch
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Fatal("Storage.Exists() error =", err)
}
if !exists {
t.Errorf("Storage.Exists() = %v, want %v", exists, true)
}
rc, err := s.Fetch(ctx, desc)
if err != nil {
t.Fatal("Storage.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Storage.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Storage.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Storage.Fetch() = %v, want %v", got, content)
}
}
func TestStorage_RelativeRoot_Success(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
tempDir, err := filepath.EvalSymlinks(t.TempDir())
if err != nil {
t.Fatal("error calling filepath.EvalSymlinks(), error =", err)
}
currDir, err := os.Getwd()
if err != nil {
t.Fatal("error calling Getwd(), error=", err)
}
if err := os.Chdir(tempDir); err != nil {
t.Fatal("error calling Chdir(), error=", err)
}
s, err := NewStorage(".")
if err != nil {
t.Fatal("New() error =", err)
}
if want := tempDir; s.root != want {
t.Errorf("Storage.root = %s, want %s", s.root, want)
}
// cd back to allow the temp directory to be removed
if err := os.Chdir(currDir); err != nil {
t.Fatal("error calling Chdir(), error=", err)
}
ctx := context.Background()
// test push
err = s.Push(ctx, desc, bytes.NewReader(content))
if err != nil {
t.Fatal("Storage.Push() error =", err)
}
// test fetch
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Fatal("Storage.Exists() error =", err)
}
if !exists {
t.Errorf("Storage.Exists() = %v, want %v", exists, true)
}
rc, err := s.Fetch(ctx, desc)
if err != nil {
t.Fatal("Storage.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Storage.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Storage.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Storage.Fetch() = %v, want %v", got, content)
}
}
func TestStorage_NotFound(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
tempDir := t.TempDir()
s, err := NewStorage(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
ctx := context.Background()
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Error("Storage.Exists() error =", err)
}
if exists {
t.Errorf("Storage.Exists() = %v, want %v", exists, false)
}
_, err = s.Fetch(ctx, desc)
if !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("Storage.Fetch() error = %v, want %v", err, errdef.ErrNotFound)
}
}
func TestStorage_AlreadyExists(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
tempDir := t.TempDir()
s, err := NewStorage(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
ctx := context.Background()
err = s.Push(ctx, desc, bytes.NewReader(content))
if err != nil {
t.Fatal("Storage.Push() error =", err)
}
err = s.Push(ctx, desc, bytes.NewReader(content))
if !errors.Is(err, errdef.ErrAlreadyExists) {
t.Errorf("Storage.Push() error = %v, want %v", err, errdef.ErrAlreadyExists)
}
}
func TestStorage_BadPush(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
tempDir := t.TempDir()
s, err := NewStorage(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
ctx := context.Background()
err = s.Push(ctx, desc, strings.NewReader("foobar"))
if err == nil {
t.Errorf("Storage.Push() error = %v, wantErr %v", err, true)
}
}
func TestStorage_Push_Concurrent(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
tempDir := t.TempDir()
s, err := NewStorage(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
ctx := context.Background()
concurrency := 64
eg, egCtx := errgroup.WithContext(ctx)
for i := 0; i < concurrency; i++ {
eg.Go(func(i int) func() error {
return func() error {
if err := s.Push(egCtx, desc, bytes.NewReader(content)); err != nil {
if errors.Is(err, errdef.ErrAlreadyExists) {
return nil
}
return fmt.Errorf("failed to push content: %v", err)
}
return nil
}
}(i))
}
if err := eg.Wait(); err != nil {
t.Fatal(err)
}
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Fatal("Storage.Exists() error =", err)
}
if !exists {
t.Errorf("Storage.Exists() = %v, want %v", exists, true)
}
rc, err := s.Fetch(ctx, desc)
if err != nil {
t.Fatal("Storage.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Storage.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Storage.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Storage.Fetch() = %v, want %v", got, content)
}
}
func TestStorage_Fetch_ExistingBlobs(t *testing.T) {
content := []byte("hello world")
dgst := digest.FromBytes(content)
desc := ocispec.Descriptor{
MediaType: "test",
Digest: dgst,
Size: int64(len(content)),
}
tempDir := t.TempDir()
path := filepath.Join(tempDir, "blobs", dgst.Algorithm().String(), dgst.Encoded())
if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil {
t.Fatal("error calling Mkdir(), error =", err)
}
if err := os.WriteFile(path, content, 0444); err != nil {
t.Fatal("error calling WriteFile(), error =", err)
}
s, err := NewStorage(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
ctx := context.Background()
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Fatal("Storage.Exists() error =", err)
}
if !exists {
t.Errorf("Storage.Exists() = %v, want %v", exists, true)
}
rc, err := s.Fetch(ctx, desc)
if err != nil {
t.Fatal("Storage.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Storage.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Storage.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Storage.Fetch() = %v, want %v", got, content)
}
}
func TestStorage_Fetch_Concurrent(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
tempDir := t.TempDir()
s, err := NewStorage(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
ctx := context.Background()
if err := s.Push(ctx, desc, bytes.NewReader(content)); err != nil {
t.Fatal("Storage.Push() error =", err)
}
concurrency := 64
eg, egCtx := errgroup.WithContext(ctx)
for i := 0; i < concurrency; i++ {
eg.Go(func(i int) func() error {
return func() error {
rc, err := s.Fetch(egCtx, desc)
if err != nil {
return fmt.Errorf("failed to fetch content: %v", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Storage.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Storage.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Storage.Fetch() = %v, want %v", got, content)
}
return nil
}
}(i))
}
if err := eg.Wait(); err != nil {
t.Fatal(err)
}
}
func TestStorage_Delete(t *testing.T) {
content := []byte("test delete")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
tempDir := t.TempDir()
s, err := NewStorage(tempDir)
if err != nil {
t.Fatal("New() error =", err)
}
ctx := context.Background()
if err := s.Push(ctx, desc, bytes.NewReader(content)); err != nil {
t.Fatal("Storage.Push() error =", err)
}
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Fatal("Storage.Exists() error =", err)
}
if !exists {
t.Errorf("Storage.Exists() = %v, want %v", exists, true)
}
err = s.Delete(ctx, desc)
if err != nil {
t.Fatal("Storage.Delete() error =", err)
}
exists, err = s.Exists(ctx, desc)
if err != nil {
t.Fatal("Storage.Exists() error =", err)
}
if exists {
t.Errorf("Storage.Exists() = %v, want %v", exists, false)
}
err = s.Delete(ctx, desc)
if !errors.Is(err, errdef.ErrNotFound) {
t.Fatalf("got error = %v, want %v", err, errdef.ErrNotFound)
}
}
oras-go-2.5.0/content/oci/testdata/ 0000775 0000000 0000000 00000000000 14576745303 0017140 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/content/oci/testdata/hello-world.tar 0000664 0000000 0000000 00000036000 14576745303 0022077 0 ustar 00root root 0000000 0000000 blobs/ 0000755 0000000 0000000 00000000000 00000000000 010306 5 ustar 00 0000000 0000000 blobs/sha256/ 0000755 0000000 0000000 00000000000 00000000000 011316 5 ustar 00 0000000 0000000 blobs/sha256/2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54 0000444 0000000 0000000 00000004657 00000000000 021624 0 ustar 00 0000000 0000000 [l0?⨤ĺCKCʏbz:@
"lwV%S_OؿN !&4tP[Z
lXi}u]RN͢#ѱiFnm
uPWsCg(0wnBˆIE_;Vmq%qxs{\Elpyh3Ga-Ky/Gʮ+Æ\UVx<Ëlڋj]=BtƥE2uu.D5rv_ %_̫XW
.\p
.\|U),q}NX^z؋PӋP(_j"d#F}:s=}#m̕l&Ċ'>0H:m?
"9f̓f(md,7>p
em߸K<YU倷5>\xvam7zUJZ55+ĿeY^7Bc"a ='+/C1_
P()09
%S@z!`\;h}0'W;!`SJP'\Tʽ>O^UN{ɞ=,;/ԃ5='+K1 lJk,bAY%*g#pBxn X33N-1+/m
!QfM<s#=W2o6r`s*qӫ0MGX
-˂b=`0x0
B?z@O_*XE/"zK?H?ħcV?䞛⁞fN}_Z`Y~ÔDz>T˾>|ϳ1ˇ=浏"vDa3D4*w311<@
3fg0wDiT#|ptU7=M$RQgWfTb(;M$zf"(Esi#X`{8m@!y=?[o"y;6E;rjq{YQƯ+v1?c?Ё;p(H)|]`r!$ko0y&6[nSԴ<
ƋF`ci=Z;ٷAHhZ<W
hho '[&gzn9`v:'?Ϯ6H@mrl OXٶ]HB_pk-F!s^S1kOٟwO%
QʙmY
c]Μ;`wV(rۜv;@'qw̓ӾέMs;33pTcʭp$2ch]EVB^Wm 5>q'8ȷ# blfBѥ\CoY}+@:M9Fn:;,k!nK#OqWW]+/%p>}N^^_5%
f:?BGa+$\wj]p
.9VdpT?-)
gdMXK*&I1EWq*)HJbixXT
xTQNe⸢r>_YYIr4LiL2Rie5"k[}8}IBSrฒ%R [T䌒
p"3t_c?~TQӉ58aڵW9}0V)x!p'qUXʣNZVI~4'gYe۱|HDLqXUz\ Xɰn7K㺪Yê,%RكS>Qe)c)TW洰ڙ"vFDqVj&ukJF&I,ELkvq)JRYW5b4E[}X ':O;AIK|IV)K:Q2l4VF\6fTvAVN2m
` +:JN˚+%R'V-?(T"'>
&`VOfUz~y=7G<B|lY.EVs܅.\p
w7ػ%\!ВQ4l JeS(* H( 'RFv8%UNAijP--z >oT
}5e_~{
qkkk.[gUï#
"VVy}|BJ?W}I+~.
={+-w_~]缷;zS_~;j;]p_ })O : blobs/sha256/f54a58bc1aac5ea1a25d796ae155dc228b3f0e11d046ae276b39c4bf2f13d8c4 0000444 0000000 0000000 00000001015 00000000000 022366 0 ustar 00 0000000 0000000 {
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 1469,
"digest": "sha256:feb5d9fea6a5e9606aa995e879d862b825965ba48de054caab5ef356dc6b3412"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 2479,
"digest": "sha256:2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54"
}
]
} blobs/sha256/faa03e786c97f07ef34423fccceeec2398ec8a5759259f94d99078f264e9d7af 0000444 0000000 0000000 00000005001 00000000000 022346 0 ustar 00 0000000 0000000 {"manifests":[{"digest":"sha256:f54a58bc1aac5ea1a25d796ae155dc228b3f0e11d046ae276b39c4bf2f13d8c4","mediaType":"application\/vnd.docker.distribution.manifest.v2+json","platform":{"architecture":"amd64","os":"linux"},"size":525},{"digest":"sha256:7b8b7289d0536a08eabdf71c20246e23f7116641db7e1d278592236ea4dcb30c","mediaType":"application\/vnd.docker.distribution.manifest.v2+json","platform":{"architecture":"arm","os":"linux","variant":"v5"},"size":525},{"digest":"sha256:f130bd2d67e6e9280ac6d0a6c83857bfaf70234e8ef4236876eccfbd30973b1c","mediaType":"application\/vnd.docker.distribution.manifest.v2+json","platform":{"architecture":"arm","os":"linux","variant":"v7"},"size":525},{"digest":"sha256:432f982638b3aefab73cc58ab28f5c16e96fdb504e8c134fc58dff4bae8bf338","mediaType":"application\/vnd.docker.distribution.manifest.v2+json","platform":{"architecture":"arm64","os":"linux","variant":"v8"},"size":525},{"digest":"sha256:995efde2e81b21d1ea7066aa77a59298a62a9e9fbb4b77f36c189774ec9b1089","mediaType":"application\/vnd.docker.distribution.manifest.v2+json","platform":{"architecture":"386","os":"linux"},"size":525},{"digest":"sha256:eb11b1a194ff8e236a01eff392c4e1296a53b0fb4780d8b0382f7996a15d5392","mediaType":"application\/vnd.docker.distribution.manifest.v2+json","platform":{"architecture":"mips64le","os":"linux"},"size":525},{"digest":"sha256:b836bb24a270b9cc935962d8228517fde0f16990e88893d935efcb1b14c0017a","mediaType":"application\/vnd.docker.distribution.manifest.v2+json","platform":{"architecture":"ppc64le","os":"linux"},"size":525},{"digest":"sha256:98c9722322be649df94780d3fbe594fce7996234b259f27eac9428b84050c849","mediaType":"application\/vnd.docker.distribution.manifest.v2+json","platform":{"architecture":"riscv64","os":"linux"},"size":525},{"digest":"sha256:c7b6944911848ce39b44ed660d95fb54d69bbd531de724c7ce6fc9f743c0b861","mediaType":"application\/vnd.docker.distribution.manifest.v2+json","platform":{"architecture":"s390x","os":"linux"},"size":525},{"digest":"sha256:fb353688bcf45fc724fde3d1dcd7935ddf56803e2b7027164a7acc28758002f6","mediaType":"application\/vnd.docker.distribution.manifest.v2+json","platform":{"architecture":"amd64","os":"windows","os.version":"10.0.20348.1249"},"size":946},{"digest":"sha256:c597fadc51747a7392f4d0312e9740aaee11793a47658a77a4ba51bd5d9b6be0","mediaType":"application\/vnd.docker.distribution.manifest.v2+json","platform":{"architecture":"amd64","os":"windows","os.version":"10.0.17763.3650"},"size":946}],"mediaType":"application\/vnd.docker.distribution.manifest.list.v2+json","schemaVersion":2} blobs/sha256/feb5d9fea6a5e9606aa995e879d862b825965ba48de054caab5ef356dc6b3412 0000444 0000000 0000000 00000002675 00000000000 022407 0 ustar 00 0000000 0000000 {"architecture":"amd64","config":{"Hostname":"","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/hello"],"Image":"sha256:b9935d4e8431fb1a7f0989304ec86b3329a99a25f5efdc7f09f3f8c41434ca6d","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"container":"8746661ca3c2f215da94e6d3f7dfdcafaff5ec0b21c9aff6af3dc379a82fbc72","container_config":{"Hostname":"8746661ca3c2","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\"/hello\"]"],"Image":"sha256:b9935d4e8431fb1a7f0989304ec86b3329a99a25f5efdc7f09f3f8c41434ca6d","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{}},"created":"2021-09-23T23:47:57.442225064Z","docker_version":"20.10.7","history":[{"created":"2021-09-23T23:47:57.098990892Z","created_by":"/bin/sh -c #(nop) COPY file:50563a97010fd7ce1ceebd1fa4f4891ac3decdf428333fb2683696f4358af6c2 in / "},{"created":"2021-09-23T23:47:57.442225064Z","created_by":"/bin/sh -c #(nop) CMD [\"/hello\"]","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:e07ee1baac5fae6a26f30cabfe54a36d3402f96afda318fe0a96cec4ca393359"]}} index.json 0000644 0000000 0000000 00000000511 00000000000 011204 0 ustar 00 0000000 0000000 {"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.docker.distribution.manifest.list.v2+json","digest":"sha256:faa03e786c97f07ef34423fccceeec2398ec8a5759259f94d99078f264e9d7af","size":2561,"annotations":{"io.containerd.image.name":"docker.io/library/hello-world:latest","org.opencontainers.image.ref.name":"latest"}}]} manifest.json 0000644 0000000 0000000 00000000331 00000000000 011703 0 ustar 00 0000000 0000000 [{"Config":"blobs/sha256/feb5d9fea6a5e9606aa995e879d862b825965ba48de054caab5ef356dc6b3412","RepoTags":["hello-world:latest"],"Layers":["blobs/sha256/2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54"]}] oci-layout 0000444 0000000 0000000 00000000036 00000000000 011212 0 ustar 00 0000000 0000000 {"imageLayoutVersion":"1.0.0"} oras-go-2.5.0/content/reader.go 0000664 0000000 0000000 00000007275 14576745303 0016361 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 (
"errors"
"fmt"
"io"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
var (
// ErrInvalidDescriptorSize is returned by ReadAll() when
// the descriptor has an invalid size.
ErrInvalidDescriptorSize = errors.New("invalid descriptor size")
// ErrMismatchedDigest is returned by ReadAll() when
// the descriptor has an invalid digest.
ErrMismatchedDigest = errors.New("mismatched digest")
// ErrTrailingData is returned by ReadAll() when
// there exists trailing data unread when the read terminates.
ErrTrailingData = errors.New("trailing data")
)
var (
// errEarlyVerify is returned by VerifyReader.Verify() when
// Verify() is called before completing reading the entire content blob.
errEarlyVerify = errors.New("early verify")
)
// VerifyReader reads the content described by its descriptor and verifies
// against its size and digest.
type VerifyReader struct {
base *io.LimitedReader
verifier digest.Verifier
verified bool
err error
}
// Read reads up to len(p) bytes into p. It returns the number of bytes
// read (0 <= n <= len(p)) and any error encountered.
func (vr *VerifyReader) Read(p []byte) (n int, err error) {
if vr.err != nil {
return 0, vr.err
}
n, err = vr.base.Read(p)
if err != nil {
if err == io.EOF && vr.base.N > 0 {
err = io.ErrUnexpectedEOF
}
vr.err = err
}
return
}
// Verify checks for remaining unread content and verifies the read content against the digest
func (vr *VerifyReader) Verify() error {
if vr.verified {
return nil
}
if vr.err == nil {
if vr.base.N > 0 {
return errEarlyVerify
}
} else if vr.err != io.EOF {
return vr.err
}
if err := ensureEOF(vr.base.R); err != nil {
vr.err = err
return vr.err
}
if !vr.verifier.Verified() {
vr.err = ErrMismatchedDigest
return vr.err
}
vr.verified = true
vr.err = io.EOF
return nil
}
// NewVerifyReader wraps r for reading content with verification against desc.
func NewVerifyReader(r io.Reader, desc ocispec.Descriptor) *VerifyReader {
verifier := desc.Digest.Verifier()
lr := &io.LimitedReader{
R: io.TeeReader(r, verifier),
N: desc.Size,
}
return &VerifyReader{
base: lr,
verifier: verifier,
}
}
// ReadAll safely reads the content described by the descriptor.
// The read content is verified against the size and the digest
// using a VerifyReader.
func ReadAll(r io.Reader, desc ocispec.Descriptor) ([]byte, error) {
if desc.Size < 0 {
return nil, ErrInvalidDescriptorSize
}
buf := make([]byte, desc.Size)
vr := NewVerifyReader(r, desc)
if n, err := io.ReadFull(vr, buf); err != nil {
if errors.Is(err, io.ErrUnexpectedEOF) {
return nil, fmt.Errorf("read failed: expected content size of %d, got %d, for digest %s: %w", desc.Size, n, desc.Digest.String(), err)
}
return nil, fmt.Errorf("read failed: %w", err)
}
if err := vr.Verify(); err != nil {
return nil, err
}
return buf, nil
}
// ensureEOF ensures the read operation ends with an EOF and no
// trailing data is present.
func ensureEOF(r io.Reader) error {
var peek [1]byte
_, err := io.ReadFull(r, peek[:])
if err != io.EOF {
return ErrTrailingData
}
return nil
}
oras-go-2.5.0/content/reader_test.go 0000664 0000000 0000000 00000015524 14576745303 0017414 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 (
"bytes"
_ "crypto/sha256"
"errors"
"io"
"testing"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
func TestVerifyReader_Read(t *testing.T) {
// matched content and descriptor with small buffer
content := []byte("example content")
desc := NewDescriptorFromBytes("test", content)
r := bytes.NewReader(content)
vr := NewVerifyReader(r, desc)
buf := make([]byte, 5)
n, err := vr.Read(buf)
if err != nil {
t.Fatal("Read() error = ", err)
}
if !bytes.Equal(buf, []byte("examp")) {
t.Fatalf("incorrect read content: %s", buf)
}
if n != 5 {
t.Fatalf("incorrect number of bytes read: %d", n)
}
// matched content and descriptor with sufficient buffer
content = []byte("foo foo")
desc = NewDescriptorFromBytes("test", content)
r = bytes.NewReader(content)
vr = NewVerifyReader(r, desc)
buf = make([]byte, len(content))
n, err = vr.Read(buf)
if err != nil {
t.Fatal("Read() error = ", err)
}
if n != len(content) {
t.Fatalf("incorrect number of bytes read: %d", n)
}
if !bytes.Equal(buf, content) {
t.Fatalf("incorrect read content: %s", buf)
}
// mismatched content and descriptor with sufficient buffer
r = bytes.NewReader([]byte("bar"))
vr = NewVerifyReader(r, desc)
buf = make([]byte, 5)
n, err = vr.Read(buf)
if err != nil {
t.Fatal("Read() error = ", err)
}
if n != 3 {
t.Fatalf("incorrect number of bytes read: %d", n)
}
}
func TestVerifyReader_Verify(t *testing.T) {
// matched content and descriptor
content := []byte("example content")
desc := NewDescriptorFromBytes("test", content)
r := bytes.NewReader(content)
vr := NewVerifyReader(r, desc)
buf := make([]byte, len(content))
if _, err := vr.Read(buf); err != nil {
t.Fatal("Read() error = ", err)
}
if err := vr.Verify(); err != nil {
t.Fatal("Verify() error = ", err)
}
if !bytes.Equal(buf, content) {
t.Fatalf("incorrect read content: %s", buf)
}
// mismatched content and descriptor, read size larger than descriptor size
content = []byte("foo")
r = bytes.NewReader(content)
desc = ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageLayer,
Digest: digest.FromBytes(content),
Size: int64(len(content)) - 1}
vr = NewVerifyReader(r, desc)
buf = make([]byte, len(content))
if _, err := vr.Read(buf); err != nil {
t.Fatal("Read() error = ", err)
}
if err := vr.Verify(); !errors.Is(err, ErrTrailingData) {
t.Fatalf("Verify() error = %v, want %v", err, ErrTrailingData)
}
// call vr.Verify again, the result should be the same
if err := vr.Verify(); !errors.Is(err, ErrTrailingData) {
t.Fatalf("2nd Verify() error = %v, want %v", err, ErrTrailingData)
}
// mismatched content and descriptor, read size smaller than descriptor size
content = []byte("foo")
r = bytes.NewReader(content)
desc = ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageLayer,
Digest: digest.FromBytes(content),
Size: int64(len(content)) + 1}
vr = NewVerifyReader(r, desc)
buf = make([]byte, len(content))
if _, err := vr.Read(buf); err != nil {
t.Fatal("Read() error = ", err)
}
if err := vr.Verify(); !errors.Is(err, errEarlyVerify) {
t.Fatalf("Verify() error = %v, want %v", err, errEarlyVerify)
}
// call vr.Verify again, the result should be the same
if err := vr.Verify(); !errors.Is(err, errEarlyVerify) {
t.Fatalf("Verify() error = %v, want %v", err, errEarlyVerify)
}
// mismatched content and descriptor, wrong digest
content = []byte("bar")
r = bytes.NewReader(content)
desc = NewDescriptorFromBytes("test", []byte("foo"))
vr = NewVerifyReader(r, desc)
buf = make([]byte, len(content))
if _, err := vr.Read(buf); err != nil {
t.Fatal("Read() error = ", err)
}
if err := vr.Verify(); !errors.Is(err, ErrMismatchedDigest) {
t.Fatalf("Verify() error = %v, want %v", err, ErrMismatchedDigest)
}
// call vr.Verify again, the result should be the same
if err := vr.Verify(); !errors.Is(err, ErrMismatchedDigest) {
t.Fatalf("2nd Verify() error = %v, want %v", err, ErrMismatchedDigest)
}
}
func TestReadAll_CorrectDescriptor(t *testing.T) {
content := []byte("example content")
desc := NewDescriptorFromBytes("test", content)
r := bytes.NewReader([]byte(content))
got, err := ReadAll(r, desc)
if err != nil {
t.Fatal("ReadAll() error = ", err)
}
if !bytes.Equal(got, content) {
t.Errorf("ReadAll() = %v, want %v", got, content)
}
}
func TestReadAll_ReadSizeSmallerThanDescriptorSize(t *testing.T) {
content := []byte("example content")
desc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageLayer,
Digest: digest.FromBytes(content),
Size: int64(len(content) + 1)}
r := bytes.NewReader([]byte(content))
_, err := ReadAll(r, desc)
if err == nil || !errors.Is(err, io.ErrUnexpectedEOF) {
t.Errorf("ReadAll() error = %v, want %v", err, io.ErrUnexpectedEOF)
}
}
func TestReadAll_ReadSizeLargerThanDescriptorSize(t *testing.T) {
content := []byte("example content")
desc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageLayer,
Digest: digest.FromBytes(content),
Size: int64(len(content) - 1)}
r := bytes.NewReader([]byte(content))
_, err := ReadAll(r, desc)
if err == nil || !errors.Is(err, ErrTrailingData) {
t.Errorf("ReadAll() error = %v, want %v", err, ErrTrailingData)
}
}
func TestReadAll_InvalidDigest(t *testing.T) {
content := []byte("example content")
desc := NewDescriptorFromBytes("test", []byte("another content"))
r := bytes.NewReader([]byte(content))
_, err := ReadAll(r, desc)
if err == nil || !errors.Is(err, ErrMismatchedDigest) {
t.Errorf("ReadAll() error = %v, want %v", err, ErrMismatchedDigest)
}
}
func TestReadAll_EmptyContent(t *testing.T) {
content := []byte("")
desc := NewDescriptorFromBytes("test", content)
r := bytes.NewReader([]byte(content))
got, err := ReadAll(r, desc)
if err != nil {
t.Fatal("ReadAll() error = ", err)
}
if !bytes.Equal(got, content) {
t.Errorf("ReadAll() = %v, want %v", got, content)
}
}
func TestReadAll_InvalidDescriptorSize(t *testing.T) {
content := []byte("example content")
desc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageLayer,
Digest: digest.FromBytes(content),
Size: -1,
}
r := bytes.NewReader([]byte(content))
_, err := ReadAll(r, desc)
if err == nil || !errors.Is(err, ErrInvalidDescriptorSize) {
t.Errorf("ReadAll() error = %v, want %v", err, ErrInvalidDescriptorSize)
}
}
oras-go-2.5.0/content/resolver.go 0000664 0000000 0000000 00000002531 14576745303 0016746 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 provides implementations to access content stores.
package content
import (
"context"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
// Resolver resolves reference tags.
type Resolver interface {
// Resolve resolves a reference to a descriptor.
Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error)
}
// Tagger tags reference tags.
type Tagger interface {
// Tag tags a descriptor with a reference string.
Tag(ctx context.Context, desc ocispec.Descriptor, reference string) error
}
// TagResolver provides reference tag indexing services.
type TagResolver interface {
Tagger
Resolver
}
// Untagger untags reference tags.
type Untagger interface {
// Untag untags the given reference string.
Untag(ctx context.Context, reference string) error
}
oras-go-2.5.0/content/storage.go 0000664 0000000 0000000 00000005106 14576745303 0016552 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 (
"context"
"io"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
// Fetcher fetches content.
type Fetcher interface {
// Fetch fetches the content identified by the descriptor.
Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error)
}
// Pusher pushes content.
type Pusher interface {
// Push pushes the content, matching the expected descriptor.
// Reader is preferred to Writer so that the suitable buffer size can be
// chosen by the underlying implementation. Furthermore, the implementation
// can also do reflection on the Reader for more advanced I/O optimization.
Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error
}
// Storage represents a content-addressable storage (CAS) where contents are
// accessed via Descriptors.
// The storage is designed to handle blobs of large sizes.
type Storage interface {
ReadOnlyStorage
Pusher
}
// ReadOnlyStorage represents a read-only Storage.
type ReadOnlyStorage interface {
Fetcher
// Exists returns true if the described content exists.
Exists(ctx context.Context, target ocispec.Descriptor) (bool, error)
}
// Deleter removes content.
// Deleter is an extension of Storage.
type Deleter interface {
// Delete removes the content identified by the descriptor.
Delete(ctx context.Context, target ocispec.Descriptor) error
}
// FetchAll safely fetches the content described by the descriptor.
// The fetched content is verified against the size and the digest.
func FetchAll(ctx context.Context, fetcher Fetcher, desc ocispec.Descriptor) ([]byte, error) {
rc, err := fetcher.Fetch(ctx, desc)
if err != nil {
return nil, err
}
defer rc.Close()
return ReadAll(rc, desc)
}
// FetcherFunc is the basic Fetch method defined in Fetcher.
type FetcherFunc func(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error)
// Fetch performs Fetch operation by the FetcherFunc.
func (fn FetcherFunc) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
return fn(ctx, target)
}
oras-go-2.5.0/content_test.go 0000664 0000000 0000000 00000176636 14576745303 0016166 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 oras_test
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strconv"
"strings"
"testing"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2"
"oras.land/oras-go/v2/content/memory"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/internal/cas"
"oras.land/oras-go/v2/registry/remote"
)
func TestTag_Memory(t *testing.T) {
target := memory.New()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
generateManifest(descs[0], descs[1:3]...) // Blob 3
ctx := context.Background()
for i := range blobs {
err := target.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
manifestDesc := descs[3]
ref := "foobar"
err := target.Tag(ctx, manifestDesc, ref)
if err != nil {
t.Fatal("fail to tag manifestDesc node", err)
}
// test Tag
gotDesc, err := oras.Tag(ctx, target, ref, "myTestingTag")
if err != nil {
t.Fatalf("failed to retag using oras.Tag with err: %v", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("oras.Tag() = %v, want %v", gotDesc, manifestDesc)
}
// verify tag
gotDesc, err = target.Resolve(ctx, "myTestingTag")
if err != nil {
t.Fatal("target.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("target.Resolve() = %v, want %v", gotDesc, manifestDesc)
}
}
func TestTag_Repository(t *testing.T) {
index := []byte(`{"manifests":[]}`)
indexDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageIndex,
Digest: digest.FromBytes(index),
Size: int64(len(index)),
}
src := "foobar"
dst := "myTag"
var gotIndex []byte
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && (r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String() || r.URL.Path == "/v2/test/manifests/"+src):
if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", indexDesc.MediaType)
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
if _, err := w.Write(index); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+dst:
if contentType := r.Header.Get("Content-Type"); contentType != indexDesc.MediaType {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotIndex = buf.Bytes()
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repoName := uri.Host + "/test"
repo, err := remote.NewRepository(repoName)
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
ctx := context.Background()
// test with manifest tag
gotDesc, err := oras.Tag(ctx, repo, src, dst)
if err != nil {
t.Fatalf("Repository.TagReference() error = %v", err)
}
if !bytes.Equal(gotIndex, index) {
t.Errorf("Repository.TagReference() = %v, want %v", gotIndex, index)
}
if !reflect.DeepEqual(gotDesc, indexDesc) {
t.Errorf("oras.Tag() = %v, want %v", gotDesc, indexDesc)
}
// test with manifest digest
gotDesc, err = oras.Tag(ctx, repo, indexDesc.Digest.String(), dst)
if err != nil {
t.Fatalf("Repository.TagReference() error = %v", err)
}
if !bytes.Equal(gotIndex, index) {
t.Errorf("Repository.TagReference() = %v, want %v", gotIndex, index)
}
if !reflect.DeepEqual(gotDesc, indexDesc) {
t.Errorf("oras.Tag() = %v, want %v", gotDesc, indexDesc)
}
// test with manifest tag@digest
tagDigestRef := src + "@" + indexDesc.Digest.String()
gotDesc, err = oras.Tag(ctx, repo, tagDigestRef, dst)
if err != nil {
t.Fatalf("Repository.TagReference() error = %v", err)
}
if !bytes.Equal(gotIndex, index) {
t.Errorf("Repository.TagReference() = %v, want %v", gotIndex, index)
}
if !reflect.DeepEqual(gotDesc, indexDesc) {
t.Errorf("oras.Tag() = %v, want %v", gotDesc, indexDesc)
}
// test with manifest FQDN
fqdnRef := repoName + ":" + tagDigestRef
gotDesc, err = oras.Tag(ctx, repo, fqdnRef, dst)
if err != nil {
t.Fatalf("Repository.TagReference() error = %v", err)
}
if !bytes.Equal(gotIndex, index) {
t.Errorf("Repository.TagReference() = %v, want %v", gotIndex, index)
}
if !reflect.DeepEqual(gotDesc, indexDesc) {
t.Errorf("oras.Tag() = %v, want %v", gotDesc, indexDesc)
}
}
func TestTagN_Memory(t *testing.T) {
target := memory.New()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
generateManifest(descs[0], descs[1:3]...) // Blob 3
ctx := context.Background()
for i := range blobs {
err := target.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
manifestDesc := descs[3]
srcRef := "foobar"
err := target.Tag(ctx, manifestDesc, srcRef)
if err != nil {
t.Fatalf("oras.Tag(%s) error = %v", srcRef, err)
}
// test TagN with empty dstReferences
_, err = oras.TagN(ctx, target, srcRef, nil, oras.DefaultTagNOptions)
if !errors.Is(err, errdef.ErrMissingReference) {
t.Fatalf("oras.TagN() error = %v, wantErr %v", err, errdef.ErrMissingReference)
}
// test TagN with single dstReferences
dstRef := "single"
gotDesc, err := oras.TagN(ctx, target, srcRef, []string{dstRef}, oras.DefaultTagNOptions)
if err != nil {
t.Fatalf("failed to retag using oras.Tag with err: %v", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("oras.TagN() = %v, want %v", gotDesc, manifestDesc)
}
// verify tag
gotDesc, err = target.Resolve(ctx, dstRef)
if err != nil {
t.Fatal("target.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("target.Resolve() = %v, want %v", gotDesc, manifestDesc)
}
// test TagN with single dstReferences and MaxMetadataBytes = 1
// should not return error
dstRef = "single2"
opts := oras.TagNOptions{
MaxMetadataBytes: 1,
}
gotDesc, err = oras.TagN(ctx, target, srcRef, []string{dstRef}, opts)
if err != nil {
t.Fatalf("failed to retag using oras.Tag with err: %v", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("oras.TagN() = %v, want %v", gotDesc, manifestDesc)
}
// verify tag
gotDesc, err = target.Resolve(ctx, dstRef)
if err != nil {
t.Fatal("target.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("target.Resolve() = %v, want %v", gotDesc, manifestDesc)
}
// test TagN with multiple references
dstRefs := []string{"foo", "bar", "baz"}
gotDesc, err = oras.TagN(ctx, target, srcRef, dstRefs, oras.DefaultTagNOptions)
if err != nil {
t.Fatal("oras.TagN() error =", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("oras.TagN() = %v, want %v", gotDesc, manifestDesc)
}
// verify multiple references
for _, ref := range dstRefs {
gotDesc, err := target.Resolve(ctx, ref)
if err != nil {
t.Fatal("target.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("target.Resolve() = %v, want %v", gotDesc, manifestDesc)
}
}
// test TagN with multiple references and MaxMetadataBytes = 1
// should not return error
dstRefs = []string{"tag1", "tag2", "tag3"}
opts = oras.TagNOptions{
MaxMetadataBytes: 1,
}
gotDesc, err = oras.TagN(ctx, target, srcRef, dstRefs, opts)
if err != nil {
t.Fatal("oras.TagN() error =", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("oras.TagN() = %v, want %v", gotDesc, manifestDesc)
}
// verify multiple references
for _, ref := range dstRefs {
gotDesc, err := target.Resolve(ctx, ref)
if err != nil {
t.Fatal("target.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("target.Resolve() = %v, want %v", gotDesc, manifestDesc)
}
}
}
func TestTagN_Repository(t *testing.T) {
index := []byte(`{"manifests":[]}`)
indexDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageIndex,
Digest: digest.FromBytes(index),
Size: int64(len(index)),
}
srcRef := "foobar"
refFoo := "foo"
refBar := "bar"
refTag1 := "tag1"
refTag2 := "tag2"
dstRefs := []string{refFoo, refBar}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && (r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String() ||
r.URL.Path == "/v2/test/manifests/"+srcRef):
if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", indexDesc.MediaType)
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
if _, err := w.Write(index); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
case r.Method == http.MethodHead &&
(r.URL.Path == "/v2/test/manifests/"+refFoo ||
r.URL.Path == "/v2/test/manifests/"+refBar ||
r.URL.Path == "/v2/test/manifests/"+refTag1 ||
r.URL.Path == "/v2/test/manifests/"+refTag2):
if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", indexDesc.MediaType)
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(int(indexDesc.Size)))
case r.Method == http.MethodPut &&
(r.URL.Path == "/v2/test/manifests/"+refFoo ||
r.URL.Path == "/v2/test/manifests/"+refBar ||
r.URL.Path == "/v2/test/manifests/"+refTag1 ||
r.URL.Path == "/v2/test/manifests/"+refTag2):
if contentType := r.Header.Get("Content-Type"); contentType != indexDesc.MediaType {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repoName := uri.Host + "/test"
repo, err := remote.NewRepository(repoName)
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
ctx := context.Background()
// test TagN with empty dstReferences
_, err = oras.TagN(ctx, repo, srcRef, nil, oras.DefaultTagNOptions)
if !errors.Is(err, errdef.ErrMissingReference) {
t.Fatalf("oras.TagN() error = %v, wantErr %v", err, errdef.ErrMissingReference)
}
// test TagN with single dstReferences
gotDesc, err := oras.TagN(ctx, repo, srcRef, []string{refTag1}, oras.DefaultTagNOptions)
if err != nil {
t.Fatalf("failed to retag using oras.Tag with err: %v", err)
}
if !reflect.DeepEqual(gotDesc, indexDesc) {
t.Errorf("oras.TagN() = %v, want %v", gotDesc, indexDesc)
}
// verify tag
gotDesc, err = repo.Resolve(ctx, refTag1)
if err != nil {
t.Fatal("target.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, indexDesc) {
t.Errorf("target.Resolve() = %v, want %v", gotDesc, indexDesc)
}
// test TagN with single dstReferences and MaxMetadataBytes = 1
// should not return error
opts := oras.TagNOptions{
MaxMetadataBytes: 1,
}
gotDesc, err = oras.TagN(ctx, repo, srcRef, []string{refTag2}, opts)
if err != nil {
t.Fatalf("failed to retag using oras.Tag with err: %v", err)
}
if !reflect.DeepEqual(gotDesc, indexDesc) {
t.Errorf("oras.TagN() = %v, want %v", gotDesc, indexDesc)
}
// verify tag
gotDesc, err = repo.Resolve(ctx, refTag2)
if err != nil {
t.Fatal("target.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, indexDesc) {
t.Errorf("target.Resolve() = %v, want %v", gotDesc, indexDesc)
}
// test TagN with multiple references
gotDesc, err = oras.TagN(ctx, repo, srcRef, dstRefs, oras.DefaultTagNOptions)
if err != nil {
t.Fatal("oras.TagN() error =", err)
}
if !reflect.DeepEqual(gotDesc, indexDesc) {
t.Errorf("oras.TagN() = %v, want %v", gotDesc, indexDesc)
}
// verify multiple references
for _, ref := range dstRefs {
gotDesc, err := repo.Resolve(ctx, ref)
if err != nil {
t.Fatal("target.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, indexDesc) {
t.Errorf("target.Resolve() = %v, want %v", gotDesc, indexDesc)
}
}
// test TagN with multiple references and MaxMetadataBytes = 1
// should return ErrSizeExceedsLimit
dstRefs = []string{refTag1, refTag2}
opts = oras.TagNOptions{
MaxMetadataBytes: 1,
}
_, err = oras.TagN(ctx, repo, srcRef, dstRefs, opts)
if !errors.Is(err, errdef.ErrSizeExceedsLimit) {
t.Fatalf("oras.TagN() error = %v, wantErr %v", err, errdef.ErrSizeExceedsLimit)
}
}
func TestResolve_Memory(t *testing.T) {
target := memory.New()
arc_1 := "test-arc-1"
os_1 := "test-os-1"
variant_1 := "v1"
variant_2 := "v2"
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
appendManifest := func(arc, os, variant string, mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
Platform: &ocispec.Platform{
Architecture: arc,
OS: os,
Variant: variant,
},
})
}
generateManifest := func(arc, os, variant string, config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendManifest(arc, os, variant, ocispec.MediaTypeImageManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte(`{"mediaType":"application/vnd.oci.image.config.v1+json",
"created":"2022-07-29T08:13:55Z",
"author":"test author",
"architecture":"test-arc-1",
"os":"test-os-1",
"variant":"v1"}`)) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
generateManifest(arc_1, os_1, variant_1, descs[0], descs[1:3]...) // Blob 3
ctx := context.Background()
for i := range blobs {
err := target.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
manifestDesc := descs[3]
ref := "foobar"
err := target.Tag(ctx, manifestDesc, ref)
if err != nil {
t.Fatal("fail to tag manifestDesc node", err)
}
// test Resolve with default resolve options
resolveOptions := oras.DefaultResolveOptions
gotDesc, err := oras.Resolve(ctx, target, ref, resolveOptions)
if err != nil {
t.Fatal("oras.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("oras.Resolve() = %v, want %v", gotDesc, manifestDesc)
}
// test Resolve with empty resolve options
gotDesc, err = oras.Resolve(ctx, target, ref, oras.ResolveOptions{})
if err != nil {
t.Fatal("oras.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("oras.Resolve() = %v, want %v", gotDesc, manifestDesc)
}
// test Resolve with MaxMetadataBytes = 1
resolveOptions = oras.ResolveOptions{
MaxMetadataBytes: 1,
}
gotDesc, err = oras.Resolve(ctx, target, ref, resolveOptions)
if err != nil {
t.Fatal("oras.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("oras.Resolve() = %v, want %v", gotDesc, manifestDesc)
}
// test Resolve with TargetPlatform
resolveOptions = oras.ResolveOptions{
TargetPlatform: &ocispec.Platform{
Architecture: arc_1,
OS: os_1,
},
}
gotDesc, err = oras.Resolve(ctx, target, ref, resolveOptions)
if err != nil {
t.Fatal("oras.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("oras.Resolve() = %v, want %v", gotDesc, manifestDesc)
}
// test Resolve with TargetPlatform and MaxMetadataBytes = 1
resolveOptions = oras.ResolveOptions{
TargetPlatform: &ocispec.Platform{
Architecture: arc_1,
OS: os_1,
},
MaxMetadataBytes: 1,
}
gotDesc, err = oras.Resolve(ctx, target, ref, resolveOptions)
if err != nil {
t.Fatal("oras.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("oras.Resolve() = %v, want %v", gotDesc, manifestDesc)
}
// test Resolve with TargetPlatform but there is no matching node
// Should return not found error
resolveOptions = oras.ResolveOptions{
TargetPlatform: &ocispec.Platform{
Architecture: arc_1,
OS: os_1,
Variant: variant_2,
},
}
_, err = oras.Resolve(ctx, target, ref, resolveOptions)
expected := fmt.Sprintf("%s: %v: platform in manifest does not match target platform", manifestDesc.Digest, errdef.ErrNotFound)
if err.Error() != expected {
t.Fatalf("oras.Resolve() error = %v, wantErr %v", err, expected)
}
}
func TestResolve_Repository(t *testing.T) {
arc_1 := "test-arc-1"
arc_2 := "test-arc-2"
os_1 := "test-os-1"
var digest_1 digest.Digest = "sha256:11ec3af9dfeb49c89ef71877ba85249be527e4dda9d1d74d99dc618d1a5fa151"
manifestDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest_1,
Size: 484,
Platform: &ocispec.Platform{
Architecture: arc_1,
OS: os_1,
},
}
index := []byte(`{"manifests":[{
"mediaType":"application/vnd.oci.image.manifest.v1+json",
"digest":"sha256:11ec3af9dfeb49c89ef71877ba85249be527e4dda9d1d74d99dc618d1a5fa151",
"size":484,
"platform":{"architecture":"test-arc-1","os":"test-os-1"}},{
"mediaType":"application/vnd.oci.image.manifest.v1+json",
"digest":"sha256:b955aefa63749f07fad84ab06a45a951368e3ac79799bc44a158fac1bb8ca208",
"size":337,
"platform":{"architecture":"test-arc-2","os":"test-os-2"}}]}`)
indexDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageIndex,
Digest: digest.FromBytes(index),
Size: int64(len(index)),
}
src := "foobar"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && (r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String() || r.URL.Path == "/v2/test/manifests/"+src):
if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", indexDesc.MediaType)
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
if _, err := w.Write(index); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repoName := uri.Host + "/test"
repo, err := remote.NewRepository(repoName)
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
ctx := context.Background()
// test Resolve with TargetPlatform
resolveOptions := oras.ResolveOptions{
TargetPlatform: &ocispec.Platform{
Architecture: arc_1,
OS: os_1,
},
}
gotDesc, err := oras.Resolve(ctx, repo, src, resolveOptions)
if err != nil {
t.Fatal("oras.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("oras.Resolve() = %v, want %v", gotDesc, manifestDesc)
}
// test Resolve with TargetPlatform and MaxMetadataBytes = 1
resolveOptions = oras.ResolveOptions{
TargetPlatform: &ocispec.Platform{
Architecture: arc_1,
OS: os_1,
},
MaxMetadataBytes: 1,
}
_, err = oras.Resolve(ctx, repo, src, resolveOptions)
if !errors.Is(err, errdef.ErrSizeExceedsLimit) {
t.Fatalf("oras.Resolve() error = %v, wantErr %v", err, errdef.ErrSizeExceedsLimit)
}
// test Resolve with TargetPlatform but there is no matching node
// Should return not found error
resolveOptions = oras.ResolveOptions{
TargetPlatform: &ocispec.Platform{
Architecture: arc_1,
OS: arc_2,
},
}
_, err = oras.Resolve(ctx, repo, src, resolveOptions)
expected := fmt.Sprintf("%s: %v: no matching manifest was found in the manifest list", indexDesc.Digest, errdef.ErrNotFound)
if err.Error() != expected {
t.Fatalf("oras.Resolve() error = %v, wantErr %v", err, expected)
}
}
func TestFetch_Memory(t *testing.T) {
target := memory.New()
arc_1 := "test-arc-1"
os_1 := "test-os-1"
variant_1 := "v1"
variant_2 := "v2"
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
appendManifest := func(arc, os, variant string, mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
Platform: &ocispec.Platform{
Architecture: arc,
OS: os,
Variant: variant,
},
})
}
generateManifest := func(arc, os, variant string, config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendManifest(arc, os, variant, ocispec.MediaTypeImageManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte(`{"mediaType":"application/vnd.oci.image.config.v1+json",
"created":"2022-07-29T08:13:55Z",
"author":"test author",
"architecture":"test-arc-1",
"os":"test-os-1",
"variant":"v1"}`)) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
generateManifest(arc_1, os_1, variant_1, descs[0], descs[1:3]...) // Blob 3
ctx := context.Background()
for i := range blobs {
err := target.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
manifestDesc := descs[3]
manifestTag := "foobar"
err := target.Tag(ctx, manifestDesc, manifestTag)
if err != nil {
t.Fatal("fail to tag manifestDesc node", err)
}
blobRef := "blob"
err = target.Tag(ctx, descs[2], blobRef)
if err != nil {
t.Fatal("fail to tag manifestDesc node", err)
}
// test Fetch with empty FetchOptions
gotDesc, rc, err := oras.Fetch(ctx, target, manifestTag, oras.FetchOptions{})
if err != nil {
t.Fatal("oras.Fetch() error =", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("oras.Fetch() = %v, want %v", gotDesc, manifestDesc)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("oras.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, blobs[3]) {
t.Errorf("oras.Fetch() = %v, want %v", got, blobs[3])
}
// test FetchBytes with default FetchBytes options
gotDesc, rc, err = oras.Fetch(ctx, target, manifestTag, oras.DefaultFetchOptions)
if err != nil {
t.Fatal("oras.Fetch() error =", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("oras.Fetch() = %v, want %v", gotDesc, manifestDesc)
}
got, err = io.ReadAll(rc)
if err != nil {
t.Fatal("oras.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, blobs[3]) {
t.Errorf("oras.Fetch() = %v, want %v", got, blobs[3])
}
// test FetchBytes with wrong reference
randomRef := "whatever"
_, _, err = oras.Fetch(ctx, target, randomRef, oras.DefaultFetchOptions)
if !errors.Is(err, errdef.ErrNotFound) {
t.Fatalf("oras.Fetch() error = %v, wantErr %v", err, errdef.ErrNotFound)
}
// test Fetch with TargetPlatform
opts := oras.FetchOptions{
ResolveOptions: oras.ResolveOptions{
TargetPlatform: &ocispec.Platform{
Architecture: arc_1,
OS: os_1,
},
},
}
gotDesc, rc, err = oras.Fetch(ctx, target, manifestTag, opts)
if err != nil {
t.Fatal("oras.Fetch() error =", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("oras.Fetch() = %v, want %v", gotDesc, manifestDesc)
}
got, err = io.ReadAll(rc)
if err != nil {
t.Fatal("oras.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, blobs[3]) {
t.Errorf("oras.Fetch() = %v, want %v", got, blobs[3])
}
// test Fetch with TargetPlatform but there is no matching node
// should return not found error
opts = oras.FetchOptions{
ResolveOptions: oras.ResolveOptions{
TargetPlatform: &ocispec.Platform{
Architecture: arc_1,
OS: os_1,
Variant: variant_2,
},
},
}
_, _, err = oras.Fetch(ctx, target, manifestTag, opts)
expected := fmt.Sprintf("%s: %v: platform in manifest does not match target platform", manifestDesc.Digest, errdef.ErrNotFound)
if err.Error() != expected {
t.Fatalf("oras.Fetch() error = %v, wantErr %v", err, expected)
}
// test FetchBytes on blob with TargetPlatform
// should return unsupported error
opts = oras.FetchOptions{
ResolveOptions: oras.ResolveOptions{
TargetPlatform: &ocispec.Platform{
Architecture: arc_1,
OS: os_1,
},
},
}
_, _, err = oras.Fetch(ctx, target, blobRef, opts)
if !errors.Is(err, errdef.ErrUnsupported) {
t.Fatalf("oras.Fetch() error = %v, wantErr %v", err, errdef.ErrUnsupported)
}
}
func TestFetch_Repository(t *testing.T) {
arc_1 := "test-arc-1"
arc_2 := "test-arc-2"
os_1 := "test-os-1"
os_2 := "test-os-2"
blob := []byte("hello world")
blobDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
manifest := []byte(`{"layers":[]}`)
manifestDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(manifest),
Size: int64(len(manifest)),
Platform: &ocispec.Platform{
Architecture: arc_1,
OS: os_1,
},
}
manifest2 := []byte("test manifest")
manifestDesc2 := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(manifest2),
Size: int64(len(manifest2)),
Platform: &ocispec.Platform{
Architecture: arc_2,
OS: os_2,
},
}
indexContent := ocispec.Index{
MediaType: ocispec.MediaTypeImageIndex,
Manifests: []ocispec.Descriptor{
manifestDesc,
manifestDesc2,
},
}
index, err := json.Marshal(indexContent)
if err != nil {
t.Fatal("failed to marshal index", err)
}
indexDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageIndex,
Digest: digest.FromBytes(index),
Size: int64(len(index)),
}
ref := "foobar"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && (r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String() || r.URL.Path == "/v2/test/manifests/"+ref):
if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", indexDesc.MediaType)
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
if _, err := w.Write(index); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
case r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String():
if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", manifestDesc.MediaType)
w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String())
if _, err := w.Write(manifest); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
case r.URL.Path == "/v2/test/blobs/"+blobDesc.Digest.String():
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String())
if _, err := w.Write(blob); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repoName := uri.Host + "/test"
repo, err := remote.NewRepository(repoName)
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
ctx := context.Background()
// test Fetch with empty option by valid manifest tag
gotDesc, rc, err := oras.Fetch(ctx, repo, ref, oras.FetchOptions{})
if err != nil {
t.Fatal("oras.Fetch() error =", err)
}
if !reflect.DeepEqual(gotDesc, indexDesc) {
t.Errorf("oras.Fetch() = %v, want %v", gotDesc, indexDesc)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("oras.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, index) {
t.Errorf("oras.Fetch() = %v, want %v", got, index)
}
// test Fetch with DefaultFetchOptions by valid manifest tag
gotDesc, rc, err = oras.Fetch(ctx, repo, ref, oras.DefaultFetchOptions)
if err != nil {
t.Fatal("oras.Fetch() error =", err)
}
if !reflect.DeepEqual(gotDesc, indexDesc) {
t.Errorf("oras.Fetch() = %v, want %v", gotDesc, indexDesc)
}
got, err = io.ReadAll(rc)
if err != nil {
t.Fatal("oras.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, index) {
t.Errorf("oras.Fetch() = %v, want %v", got, index)
}
// test Fetch with empty option by blob digest
gotDesc, rc, err = oras.Fetch(ctx, repo.Blobs(), blobDesc.Digest.String(), oras.FetchOptions{})
if err != nil {
t.Fatalf("oras.Fetch() error = %v", err)
}
if gotDesc.Digest != blobDesc.Digest || gotDesc.Size != blobDesc.Size {
t.Errorf("oras.Fetch() = %v, want %v", gotDesc, blobDesc)
}
got, err = io.ReadAll(rc)
if err != nil {
t.Fatal("oras.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, blob) {
t.Errorf("oras.Fetch() = %v, want %v", got, blob)
}
// test FetchBytes with DefaultFetchBytesOptions by blob digest
gotDesc, rc, err = oras.Fetch(ctx, repo.Blobs(), blobDesc.Digest.String(), oras.DefaultFetchOptions)
if err != nil {
t.Fatalf("oras.Fetch() error = %v", err)
}
if gotDesc.Digest != blobDesc.Digest || gotDesc.Size != blobDesc.Size {
t.Errorf("oras.Fetch() = %v, want %v", gotDesc, blobDesc)
}
got, err = io.ReadAll(rc)
if err != nil {
t.Fatal("oras.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, blob) {
t.Errorf("oras.Fetch() = %v, want %v", got, blob)
}
// test FetchBytes with wrong reference
randomRef := "whatever"
_, _, err = oras.Fetch(ctx, repo, randomRef, oras.DefaultFetchOptions)
if !errors.Is(err, errdef.ErrNotFound) {
t.Fatalf("oras.Fetch() error = %v, wantErr %v", err, errdef.ErrNotFound)
}
// test FetchBytes with TargetPlatform
opts := oras.FetchOptions{
ResolveOptions: oras.ResolveOptions{
TargetPlatform: &ocispec.Platform{
Architecture: arc_1,
OS: os_1,
},
},
}
gotDesc, rc, err = oras.Fetch(ctx, repo, ref, opts)
if err != nil {
t.Fatal("oras.Fetch() error =", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("oras.Fetch() = %v, want %v", gotDesc, manifestDesc)
}
got, err = io.ReadAll(rc)
if err != nil {
t.Fatal("oras.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, manifest) {
t.Errorf("oras.Fetch() = %v, want %v", got, manifest)
}
// test FetchBytes with TargetPlatform but there is no matching node
// Should return not found error
opts = oras.FetchOptions{
ResolveOptions: oras.ResolveOptions{
TargetPlatform: &ocispec.Platform{
Architecture: arc_1,
OS: arc_2,
},
},
}
_, _, err = oras.Fetch(ctx, repo, ref, opts)
expected := fmt.Sprintf("%s: %v: no matching manifest was found in the manifest list", indexDesc.Digest, errdef.ErrNotFound)
if err.Error() != expected {
t.Fatalf("oras.Fetch() error = %v, wantErr %v", err, expected)
}
// test FetchBytes on blob with TargetPlatform
// should return unsupported error
opts = oras.FetchOptions{
ResolveOptions: oras.ResolveOptions{
TargetPlatform: &ocispec.Platform{
Architecture: arc_1,
OS: os_1,
},
},
}
_, _, err = oras.Fetch(ctx, repo.Blobs(), blobDesc.Digest.String(), opts)
if !errors.Is(err, errdef.ErrUnsupported) {
t.Fatalf("oras.Fetch() error = %v, wantErr %v", err, errdef.ErrUnsupported)
}
}
func TestFetchBytes_Memory(t *testing.T) {
target := memory.New()
arc_1 := "test-arc-1"
os_1 := "test-os-1"
variant_1 := "v1"
variant_2 := "v2"
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
appendManifest := func(arc, os, variant string, mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
Platform: &ocispec.Platform{
Architecture: arc,
OS: os,
Variant: variant,
},
})
}
generateManifest := func(arc, os, variant string, config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendManifest(arc, os, variant, ocispec.MediaTypeImageManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte(`{"mediaType":"application/vnd.oci.image.config.v1+json",
"created":"2022-07-29T08:13:55Z",
"author":"test author",
"architecture":"test-arc-1",
"os":"test-os-1",
"variant":"v1"}`)) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
generateManifest(arc_1, os_1, variant_1, descs[0], descs[1:3]...) // Blob 3
ctx := context.Background()
for i := range blobs {
err := target.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
manifestDesc := descs[3]
manifestTag := "foobar"
err := target.Tag(ctx, manifestDesc, manifestTag)
if err != nil {
t.Fatal("fail to tag manifestDesc node", err)
}
blobRef := "blob"
err = target.Tag(ctx, descs[2], blobRef)
if err != nil {
t.Fatal("fail to tag manifestDesc node", err)
}
// test FetchBytes with empty FetchBytes options
gotDesc, gotBytes, err := oras.FetchBytes(ctx, target, manifestTag, oras.FetchBytesOptions{})
if err != nil {
t.Fatal("oras.FetchBytes() error =", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("oras.FetchBytes() = %v, want %v", gotDesc, manifestDesc)
}
if !bytes.Equal(gotBytes, blobs[3]) {
t.Errorf("oras.FetchBytes() = %v, want %v", gotBytes, blobs[3])
}
// test FetchBytes with default FetchBytes options
gotDesc, gotBytes, err = oras.FetchBytes(ctx, target, manifestTag, oras.DefaultFetchBytesOptions)
if err != nil {
t.Fatal("oras.FetchBytes() error =", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("oras.FetchBytes() = %v, want %v", gotDesc, manifestDesc)
}
if !bytes.Equal(gotBytes, blobs[3]) {
t.Errorf("oras.FetchBytes() = %v, want %v", gotBytes, blobs[3])
}
// test FetchBytes with wrong reference
randomRef := "whatever"
_, _, err = oras.FetchBytes(ctx, target, randomRef, oras.DefaultFetchBytesOptions)
if !errors.Is(err, errdef.ErrNotFound) {
t.Fatalf("oras.FetchBytes() error = %v, wantErr %v", err, errdef.ErrNotFound)
}
// test FetchBytes with MaxBytes = 1
_, _, err = oras.FetchBytes(ctx, target, manifestTag, oras.FetchBytesOptions{MaxBytes: 1})
if !errors.Is(err, errdef.ErrSizeExceedsLimit) {
t.Fatalf("oras.FetchBytes() error = %v, wantErr %v", err, errdef.ErrSizeExceedsLimit)
}
// test FetchBytes with TargetPlatform
opts := oras.FetchBytesOptions{
FetchOptions: oras.FetchOptions{
ResolveOptions: oras.ResolveOptions{
TargetPlatform: &ocispec.Platform{
Architecture: arc_1,
OS: os_1,
},
},
},
}
gotDesc, gotBytes, err = oras.FetchBytes(ctx, target, manifestTag, opts)
if err != nil {
t.Fatal("oras.FetchBytes() error =", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("oras.FetchBytes() = %v, want %v", gotDesc, manifestDesc)
}
if !bytes.Equal(gotBytes, blobs[3]) {
t.Errorf("oras.FetchBytes() = %v, want %v", gotBytes, blobs[3])
}
// test FetchBytes with TargetPlatform and MaxBytes = 1
// should return size exceed error
opts = oras.FetchBytesOptions{
FetchOptions: oras.FetchOptions{
ResolveOptions: oras.ResolveOptions{
TargetPlatform: &ocispec.Platform{
Architecture: arc_1,
OS: os_1,
},
},
},
MaxBytes: 1,
}
_, _, err = oras.FetchBytes(ctx, target, manifestTag, opts)
if !errors.Is(err, errdef.ErrSizeExceedsLimit) {
t.Fatalf("oras.FetchBytes() error = %v, wantErr %v", err, errdef.ErrSizeExceedsLimit)
}
// test FetchBytes with TargetPlatform but there is no matching node
// should return not found error
opts = oras.FetchBytesOptions{
FetchOptions: oras.FetchOptions{
ResolveOptions: oras.ResolveOptions{
TargetPlatform: &ocispec.Platform{
Architecture: arc_1,
OS: os_1,
Variant: variant_2,
},
},
},
}
_, _, err = oras.FetchBytes(ctx, target, manifestTag, opts)
expected := fmt.Sprintf("%s: %v: platform in manifest does not match target platform", manifestDesc.Digest, errdef.ErrNotFound)
if err.Error() != expected {
t.Fatalf("oras.FetchBytes() error = %v, wantErr %v", err, expected)
}
// test FetchBytes on blob with TargetPlatform
// should return unsupported error
opts = oras.FetchBytesOptions{
FetchOptions: oras.FetchOptions{
ResolveOptions: oras.ResolveOptions{
TargetPlatform: &ocispec.Platform{
Architecture: arc_1,
OS: os_1,
},
},
},
}
_, _, err = oras.FetchBytes(ctx, target, blobRef, opts)
if !errors.Is(err, errdef.ErrUnsupported) {
t.Fatalf("oras.FetchBytes() error = %v, wantErr %v", err, errdef.ErrUnsupported)
}
}
func TestFetchBytes_Repository(t *testing.T) {
arc_1 := "test-arc-1"
arc_2 := "test-arc-2"
os_1 := "test-os-1"
os_2 := "test-os-2"
blob := []byte("hello world")
blobDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
manifest := []byte(`{"layers":[]}`)
manifestDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(manifest),
Size: int64(len(manifest)),
Platform: &ocispec.Platform{
Architecture: arc_1,
OS: os_1,
},
}
manifest2 := []byte("test manifest")
manifestDesc2 := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(manifest2),
Size: int64(len(manifest2)),
Platform: &ocispec.Platform{
Architecture: arc_2,
OS: os_2,
},
}
indexContent := ocispec.Index{
MediaType: ocispec.MediaTypeImageIndex,
Manifests: []ocispec.Descriptor{
manifestDesc,
manifestDesc2,
},
}
index, err := json.Marshal(indexContent)
if err != nil {
t.Fatal("failed to marshal index", err)
}
indexDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageIndex,
Digest: digest.FromBytes(index),
Size: int64(len(index)),
}
ref := "foobar"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && (r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String() || r.URL.Path == "/v2/test/manifests/"+ref):
if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", indexDesc.MediaType)
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
if _, err := w.Write(index); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
case r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String():
if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", manifestDesc.MediaType)
w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String())
if _, err := w.Write(manifest); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
case r.URL.Path == "/v2/test/blobs/"+blobDesc.Digest.String():
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String())
if _, err := w.Write(blob); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repoName := uri.Host + "/test"
repo, err := remote.NewRepository(repoName)
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
ctx := context.Background()
// test FetchBytes with empty option by valid manifest tag
gotDesc, gotBytes, err := oras.FetchBytes(ctx, repo, ref, oras.FetchBytesOptions{})
if err != nil {
t.Fatalf("oras.FetchBytes() error = %v", err)
}
if !reflect.DeepEqual(gotDesc, indexDesc) {
t.Errorf("oras.FetchBytes() = %v, want %v", gotDesc, indexDesc)
}
if !bytes.Equal(gotBytes, index) {
t.Errorf("oras.FetchBytes() = %v, want %v", gotBytes, index)
}
// test FetchBytes with DefaultFetchBytesOptions by valid manifest tag
gotDesc, gotBytes, err = oras.FetchBytes(ctx, repo, ref, oras.DefaultFetchBytesOptions)
if err != nil {
t.Fatalf("oras.FetchBytes() error = %v", err)
}
if !reflect.DeepEqual(gotDesc, indexDesc) {
t.Errorf("oras.FetchBytes() = %v, want %v", gotDesc, indexDesc)
}
if !bytes.Equal(gotBytes, index) {
t.Errorf("oras.FetchBytes() = %v, want %v", gotBytes, index)
}
// test FetchBytes with empty option by blob digest
gotDesc, gotBytes, err = oras.FetchBytes(ctx, repo.Blobs(), blobDesc.Digest.String(), oras.FetchBytesOptions{})
if err != nil {
t.Fatalf("oras.FetchBytes() error = %v", err)
}
if gotDesc.Digest != blobDesc.Digest || gotDesc.Size != blobDesc.Size {
t.Errorf("oras.FetchBytes() = %v, want %v", gotDesc, blobDesc)
}
if !bytes.Equal(gotBytes, blob) {
t.Errorf("oras.FetchBytes() = %v, want %v", gotBytes, blob)
}
// test FetchBytes with DefaultFetchBytesOptions by blob digest
gotDesc, gotBytes, err = oras.FetchBytes(ctx, repo.Blobs(), blobDesc.Digest.String(), oras.DefaultFetchBytesOptions)
if err != nil {
t.Fatalf("oras.FetchBytes() error = %v", err)
}
if gotDesc.Digest != blobDesc.Digest || gotDesc.Size != blobDesc.Size {
t.Errorf("oras.FetchBytes() = %v, want %v", gotDesc, blobDesc)
}
if !bytes.Equal(gotBytes, blob) {
t.Errorf("oras.FetchBytes() = %v, want %v", gotBytes, blob)
}
// test FetchBytes with MaxBytes = 1
_, _, err = oras.FetchBytes(ctx, repo, ref, oras.FetchBytesOptions{MaxBytes: 1})
if !errors.Is(err, errdef.ErrSizeExceedsLimit) {
t.Fatalf("oras.FetchBytes() error = %v, wantErr %v", err, errdef.ErrSizeExceedsLimit)
}
// test FetchBytes with wrong reference
randomRef := "whatever"
_, _, err = oras.FetchBytes(ctx, repo, randomRef, oras.DefaultFetchBytesOptions)
if !errors.Is(err, errdef.ErrNotFound) {
t.Fatalf("oras.FetchBytes() error = %v, wantErr %v", err, errdef.ErrNotFound)
}
// test FetchBytes with TargetPlatform
opts := oras.FetchBytesOptions{
FetchOptions: oras.FetchOptions{
ResolveOptions: oras.ResolveOptions{
TargetPlatform: &ocispec.Platform{
Architecture: arc_1,
OS: os_1,
},
},
},
}
gotDesc, gotBytes, err = oras.FetchBytes(ctx, repo, ref, opts)
if err != nil {
t.Fatal("oras.FetchBytes() error =", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("oras.FetchBytes() = %v, want %v", gotDesc, manifestDesc)
}
if !bytes.Equal(gotBytes, manifest) {
t.Errorf("oras.FetchBytes() = %v, want %v", gotBytes, manifest)
}
// test FetchBytes with TargetPlatform and MaxBytes = 1
// should return size exceed error
opts = oras.FetchBytesOptions{
FetchOptions: oras.FetchOptions{
ResolveOptions: oras.ResolveOptions{
TargetPlatform: &ocispec.Platform{
Architecture: arc_1,
OS: os_1,
},
},
},
MaxBytes: 1,
}
_, _, err = oras.FetchBytes(ctx, repo, ref, opts)
if !errors.Is(err, errdef.ErrSizeExceedsLimit) {
t.Fatalf("oras.FetchBytes() error = %v, wantErr %v", err, errdef.ErrSizeExceedsLimit)
}
// test FetchBytes with TargetPlatform but there is no matching node
// Should return not found error
opts = oras.FetchBytesOptions{
FetchOptions: oras.FetchOptions{
ResolveOptions: oras.ResolveOptions{
TargetPlatform: &ocispec.Platform{
Architecture: arc_1,
OS: arc_2,
},
},
},
}
_, _, err = oras.FetchBytes(ctx, repo, ref, opts)
expected := fmt.Sprintf("%s: %v: no matching manifest was found in the manifest list", indexDesc.Digest, errdef.ErrNotFound)
if err.Error() != expected {
t.Fatalf("oras.FetchBytes() error = %v, wantErr %v", err, expected)
}
// test FetchBytes on blob with TargetPlatform
// should return unsupported error
opts = oras.FetchBytesOptions{
FetchOptions: oras.FetchOptions{
ResolveOptions: oras.ResolveOptions{
TargetPlatform: &ocispec.Platform{
Architecture: arc_1,
OS: os_1,
},
},
},
}
_, _, err = oras.FetchBytes(ctx, repo.Blobs(), blobDesc.Digest.String(), opts)
if !errors.Is(err, errdef.ErrUnsupported) {
t.Fatalf("oras.FetchBytes() error = %v, wantErr %v", err, errdef.ErrUnsupported)
}
}
func TestPushBytes_Memory(t *testing.T) {
s := cas.NewMemory()
content := []byte("hello world")
mediaType := "test"
descTest := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
descOctet := ocispec.Descriptor{
MediaType: "application/octet-stream",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
descEmpty := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(nil),
Size: 0,
}
ctx := context.Background()
// test PushBytes with specified media type
gotDesc, err := oras.PushBytes(ctx, s, mediaType, content)
if err != nil {
t.Fatal("oras.PushBytes() error =", err)
}
if !reflect.DeepEqual(gotDesc, descTest) {
t.Errorf("oras.PushBytes() = %v, want %v", gotDesc, descTest)
}
rc, err := s.Fetch(ctx, gotDesc)
if err != nil {
t.Fatal("Memory.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Memory.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Memory.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Memory.Fetch() = %v, want %v", got, content)
}
// test PushBytes with existing content
_, err = oras.PushBytes(ctx, s, mediaType, content)
if !errors.Is(err, errdef.ErrAlreadyExists) {
t.Errorf("oras.PushBytes() error = %v, wantErr %v", err, errdef.ErrAlreadyExists)
}
// test PushBytes with empty media type
gotDesc, err = oras.PushBytes(ctx, s, "", content)
if err != nil {
t.Fatal("oras.PushBytes() error =", err)
}
if !reflect.DeepEqual(gotDesc, descOctet) {
t.Errorf("oras.PushBytes() = %v, want %v", gotDesc, descOctet)
}
rc, err = s.Fetch(ctx, gotDesc)
if err != nil {
t.Fatal("Memory.Fetch() error =", err)
}
got, err = io.ReadAll(rc)
if err != nil {
t.Fatal("Memory.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Memory.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Memory.Fetch() = %v, want %v", got, content)
}
// test PushBytes with empty content
gotDesc, err = oras.PushBytes(ctx, s, mediaType, nil)
if err != nil {
t.Fatal("oras.PushBytes() error =", err)
}
if !reflect.DeepEqual(gotDesc, descEmpty) {
t.Errorf("oras.PushBytes() = %v, want %v", gotDesc, descEmpty)
}
rc, err = s.Fetch(ctx, gotDesc)
if err != nil {
t.Fatal("Memory.Fetch() error =", err)
}
got, err = io.ReadAll(rc)
if err != nil {
t.Fatal("Memory.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Memory.Fetch().Close() error =", err)
}
if !bytes.Equal(got, nil) {
t.Errorf("Memory.Fetch() = %v, want %v", got, nil)
}
}
func TestPushBytes_Repository(t *testing.T) {
blob := []byte("hello world")
blobMediaType := "test"
blobDesc := ocispec.Descriptor{
MediaType: blobMediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
var gotBlob []byte
index := []byte(`{"manifests":[]}`)
indexMediaType := ocispec.MediaTypeImageIndex
indexDesc := ocispec.Descriptor{
MediaType: indexMediaType,
Digest: digest.FromBytes(index),
Size: int64(len(index)),
}
var gotIndex []byte
uuid := "4fd53bc9-565d-4527-ab80-3e051ac4880c"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPost && r.URL.Path == "/v2/test/blobs/uploads/":
w.Header().Set("Location", "/v2/test/blobs/uploads/"+uuid)
w.WriteHeader(http.StatusAccepted)
return
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/blobs/uploads/"+uuid:
if contentType := r.Header.Get("Content-Type"); contentType != "application/octet-stream" {
w.WriteHeader(http.StatusBadRequest)
break
}
if contentDigest := r.URL.Query().Get("digest"); contentDigest != blobDesc.Digest.String() {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotBlob = buf.Bytes()
w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
return
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String():
if contentType := r.Header.Get("Content-Type"); contentType != indexDesc.MediaType {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotIndex = buf.Bytes()
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
return
default:
w.WriteHeader(http.StatusForbidden)
}
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := remote.NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
ctx := context.Background()
// test PushBytes with blob
gotDesc, err := oras.PushBytes(ctx, repo.Blobs(), blobMediaType, blob)
if err != nil {
t.Fatal("oras.PushBytes() error =", err)
}
if !reflect.DeepEqual(gotDesc, blobDesc) {
t.Errorf("oras.PushBytes() = %v, want %v", gotDesc, blobDesc)
}
if !bytes.Equal(gotBlob, blob) {
t.Errorf("oras.PushBytes() = %v, want %v", gotBlob, blob)
}
// test PushBytes with manifest
gotDesc, err = oras.PushBytes(ctx, repo, indexMediaType, index)
if err != nil {
t.Fatal("oras.PushBytes() error =", err)
}
if err != nil {
t.Fatal("oras.PushBytes() error =", err)
}
if !reflect.DeepEqual(gotDesc, indexDesc) {
t.Errorf("oras.PushBytes() = %v, want %v", gotDesc, indexDesc)
}
if !bytes.Equal(gotIndex, index) {
t.Errorf("oras.PushBytes() = %v, want %v", gotIndex, index)
}
}
func TestTagBytesN_Memory(t *testing.T) {
s := memory.New()
content := []byte("hello world")
mediaType := "test"
descTest := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
descOctet := ocispec.Descriptor{
MediaType: "application/octet-stream",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
descEmpty := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(nil),
Size: 0,
}
ctx := context.Background()
// test TagBytesN with no reference
gotDesc, err := oras.TagBytesN(ctx, s, mediaType, content, nil, oras.DefaultTagBytesNOptions)
if err != nil {
t.Fatal("oras.TagBytes() error =", err)
}
if !reflect.DeepEqual(gotDesc, descTest) {
t.Errorf("oras.TagBytes() = %v, want %v", gotDesc, descTest)
}
rc, err := s.Fetch(ctx, gotDesc)
if err != nil {
t.Fatal("Memory.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Memory.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Memory.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Memory.Fetch() = %v, want %v", got, content)
}
// test TagBytesN with multiple references
refs := []string{"foo", "bar", "baz"}
gotDesc, err = oras.TagBytesN(ctx, s, mediaType, content, refs, oras.DefaultTagBytesNOptions)
if err != nil {
t.Fatal("oras.TagBytes() error =", err)
}
if !reflect.DeepEqual(gotDesc, descTest) {
t.Fatalf("oras.TagBytes() = %v, want %v", gotDesc, descTest)
}
for _, ref := range refs {
gotDesc, err := s.Resolve(ctx, ref)
if err != nil {
t.Fatal("Memory.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, descTest) {
t.Fatalf("oras.PushBytes() = %v, want %v", gotDesc, descTest)
}
}
rc, err = s.Fetch(ctx, gotDesc)
if err != nil {
t.Fatal("Memory.Fetch() error =", err)
}
got, err = io.ReadAll(rc)
if err != nil {
t.Fatal("Memory.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Memory.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Memory.Fetch() = %v, want %v", got, content)
}
// test TagBytesN with empty media type and multiple references
gotDesc, err = oras.TagBytesN(ctx, s, "", content, refs, oras.DefaultTagBytesNOptions)
if err != nil {
t.Fatal("oras.TagBytes() error =", err)
}
if !reflect.DeepEqual(gotDesc, descOctet) {
t.Fatalf("oras.TagBytes() = %v, want %v", gotDesc, descOctet)
}
for _, ref := range refs {
gotDesc, err := s.Resolve(ctx, ref)
if err != nil {
t.Fatal("Memory.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, descOctet) {
t.Fatalf("oras.PushBytes() = %v, want %v", gotDesc, descOctet)
}
}
rc, err = s.Fetch(ctx, gotDesc)
if err != nil {
t.Fatal("Memory.Fetch() error =", err)
}
got, err = io.ReadAll(rc)
if err != nil {
t.Fatal("Memory.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Memory.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Memory.Fetch() = %v, want %v", got, content)
}
// test TagBytesN with empty content and multiple references
gotDesc, err = oras.TagBytesN(ctx, s, mediaType, nil, refs, oras.DefaultTagBytesNOptions)
if err != nil {
t.Fatal("oras.TagBytes() error =", err)
}
if !reflect.DeepEqual(gotDesc, descEmpty) {
t.Fatalf("oras.TagBytes() = %v, want %v", gotDesc, descEmpty)
}
for _, ref := range refs {
gotDesc, err := s.Resolve(ctx, ref)
if err != nil {
t.Fatal("Memory.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, descEmpty) {
t.Fatalf("oras.TagBytes() = %v, want %v", gotDesc, descEmpty)
}
}
rc, err = s.Fetch(ctx, gotDesc)
if err != nil {
t.Fatal("Memory.Fetch() error =", err)
}
got, err = io.ReadAll(rc)
if err != nil {
t.Fatal("Memory.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Memory.Fetch().Close() error =", err)
}
if !bytes.Equal(got, nil) {
t.Errorf("Memory.Fetch() = %v, want %v", got, nil)
}
}
func TestTagBytesN_Repository(t *testing.T) {
index := []byte(`{"manifests":[]}`)
indexMediaType := ocispec.MediaTypeImageIndex
indexDesc := ocispec.Descriptor{
MediaType: indexMediaType,
Digest: digest.FromBytes(index),
Size: int64(len(index)),
}
refFoo := "foo"
refBar := "bar"
refs := []string{refFoo, refBar}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPut &&
(r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String() ||
r.URL.Path == "/v2/test/manifests/"+refFoo ||
r.URL.Path == "/v2/test/manifests/"+refBar):
if contentType := r.Header.Get("Content-Type"); contentType != indexDesc.MediaType {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
return
case (r.Method == http.MethodHead || r.Method == http.MethodGet) &&
(r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String() ||
r.URL.Path == "/v2/test/manifests/"+refFoo ||
r.URL.Path == "/v2/test/manifests/"+refBar):
if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", indexDesc.MediaType)
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(int(indexDesc.Size)))
if r.Method == http.MethodGet {
if _, err := w.Write(index); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
}
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusForbidden)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := remote.NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
ctx := context.Background()
// test TagBytesN with no reference
gotDesc, err := oras.TagBytesN(ctx, repo, indexMediaType, index, nil, oras.DefaultTagBytesNOptions)
if err != nil {
t.Fatal("oras.TagBytes() error =", err)
}
if !reflect.DeepEqual(gotDesc, indexDesc) {
t.Errorf("oras.TagBytes() = %v, want %v", gotDesc, indexDesc)
}
rc, err := repo.Fetch(ctx, gotDesc)
if err != nil {
t.Fatal("Repository.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Repository.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Repository.Fetch().Close() error =", err)
}
if !bytes.Equal(got, index) {
t.Errorf("Repository.Fetch() = %v, want %v", got, index)
}
// test TagBytesN with multiple references
gotDesc, err = oras.TagBytesN(ctx, repo, indexMediaType, index, refs, oras.DefaultTagBytesNOptions)
if err != nil {
t.Fatal("oras.TagBytes() error =", err)
}
if !reflect.DeepEqual(gotDesc, indexDesc) {
t.Fatalf("oras.TagBytes() = %v, want %v", gotDesc, indexDesc)
}
for _, ref := range refs {
gotDesc, err := repo.Resolve(ctx, ref)
if err != nil {
t.Fatal("Repository.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, indexDesc) {
t.Fatalf("oras.TagBytes() = %v, want %v", gotDesc, indexDesc)
}
}
rc, err = repo.Fetch(ctx, gotDesc)
if err != nil {
t.Fatal("Repository.Fetch() error =", err)
}
got, err = io.ReadAll(rc)
if err != nil {
t.Fatal("Repository.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Repository.Fetch().Close() error =", err)
}
if !bytes.Equal(got, index) {
t.Errorf("Repository.Fetch() = %v, want %v", got, index)
}
}
func TestTagBytes(t *testing.T) {
s := memory.New()
content := []byte("hello world")
mediaType := "test"
descTest := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
ctx := context.Background()
ref := "foobar"
// test TagBytes
gotDesc, err := oras.TagBytes(ctx, s, mediaType, content, ref)
if err != nil {
t.Fatal("oras.TagBytes() error =", err)
}
if !reflect.DeepEqual(gotDesc, descTest) {
t.Errorf("oras.TagBytes() = %v, want %v", gotDesc, descTest)
}
gotDesc, err = s.Resolve(ctx, ref)
if err != nil {
t.Fatal("Memory.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, descTest) {
t.Fatalf("oras.TagBytes() = %v, want %v", gotDesc, descTest)
}
rc, err := s.Fetch(ctx, gotDesc)
if err != nil {
t.Fatal("Memory.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Memory.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Memory.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Memory.Fetch() = %v, want %v", got, content)
}
}
oras-go-2.5.0/copy.go 0000664 0000000 0000000 00000040731 14576745303 0014411 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 oras
import (
"context"
"errors"
"fmt"
"io"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"golang.org/x/sync/semaphore"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/internal/cas"
"oras.land/oras-go/v2/internal/descriptor"
"oras.land/oras-go/v2/internal/platform"
"oras.land/oras-go/v2/internal/registryutil"
"oras.land/oras-go/v2/internal/status"
"oras.land/oras-go/v2/internal/syncutil"
"oras.land/oras-go/v2/registry"
)
// defaultConcurrency is the default value of CopyGraphOptions.Concurrency.
const defaultConcurrency int = 3 // This value is consistent with dockerd and containerd.
// SkipNode signals to stop copying a node. When returned from PreCopy the blob must exist in the target.
// This can be used to signal that a blob has been made available in the target repository by "Mount()" or some other technique.
var SkipNode = errors.New("skip node")
// DefaultCopyOptions provides the default CopyOptions.
var DefaultCopyOptions CopyOptions = CopyOptions{
CopyGraphOptions: DefaultCopyGraphOptions,
}
// CopyOptions contains parameters for [oras.Copy].
type CopyOptions struct {
CopyGraphOptions
// MapRoot maps the resolved root node to a desired root node for copy.
// When MapRoot is provided, the descriptor resolved from the source
// reference will be passed to MapRoot, and the mapped descriptor will be
// used as the root node for copy.
MapRoot func(ctx context.Context, src content.ReadOnlyStorage, root ocispec.Descriptor) (ocispec.Descriptor, error)
}
// WithTargetPlatform configures opts.MapRoot to select the manifest whose
// platform matches the given platform. When MapRoot is provided, the platform
// selection will be applied on the mapped root node.
// - If the given platform is nil, no platform selection will be applied.
// - If the root node is a manifest, it will remain the same if platform
// matches, otherwise ErrNotFound will be returned.
// - If the root node is a manifest list, it will be mapped to the first
// matching manifest if exists, otherwise ErrNotFound will be returned.
// - Otherwise ErrUnsupported will be returned.
func (opts *CopyOptions) WithTargetPlatform(p *ocispec.Platform) {
if p == nil {
return
}
mapRoot := opts.MapRoot
opts.MapRoot = func(ctx context.Context, src content.ReadOnlyStorage, root ocispec.Descriptor) (desc ocispec.Descriptor, err error) {
if mapRoot != nil {
if root, err = mapRoot(ctx, src, root); err != nil {
return ocispec.Descriptor{}, err
}
}
return platform.SelectManifest(ctx, src, root, p)
}
}
// defaultCopyMaxMetadataBytes is the default value of
// CopyGraphOptions.MaxMetadataBytes.
const defaultCopyMaxMetadataBytes int64 = 4 * 1024 * 1024 // 4 MiB
// DefaultCopyGraphOptions provides the default CopyGraphOptions.
var DefaultCopyGraphOptions CopyGraphOptions
// CopyGraphOptions contains parameters for [oras.CopyGraph].
type CopyGraphOptions struct {
// Concurrency limits the maximum number of concurrent copy tasks.
// If less than or equal to 0, a default (currently 3) is used.
Concurrency int
// MaxMetadataBytes limits the maximum size of the metadata that can be
// cached in the memory.
// If less than or equal to 0, a default (currently 4 MiB) is used.
MaxMetadataBytes int64
// PreCopy handles the current descriptor before it is copied. PreCopy can
// return a SkipNode to signal that desc should be skipped when it already
// exists in the target.
PreCopy func(ctx context.Context, desc ocispec.Descriptor) error
// PostCopy handles the current descriptor after it is copied.
PostCopy func(ctx context.Context, desc ocispec.Descriptor) error
// OnCopySkipped will be called when the sub-DAG rooted by the current node
// is skipped.
OnCopySkipped func(ctx context.Context, desc ocispec.Descriptor) error
// MountFrom returns the candidate repositories that desc may be mounted from.
// The OCI references will be tried in turn. If mounting fails on all of them,
// then it falls back to a copy.
MountFrom func(ctx context.Context, desc ocispec.Descriptor) ([]string, error)
// OnMounted will be invoked when desc is mounted.
OnMounted func(ctx context.Context, desc ocispec.Descriptor) error
// FindSuccessors finds the successors of the current node.
// fetcher provides cached access to the source storage, and is suitable
// for fetching non-leaf nodes like manifests. Since anything fetched from
// fetcher will be cached in the memory, it is recommended to use original
// source storage to fetch large blobs.
// If FindSuccessors is nil, content.Successors will be used.
FindSuccessors func(ctx context.Context, fetcher content.Fetcher, desc ocispec.Descriptor) ([]ocispec.Descriptor, error)
}
// Copy copies a rooted directed acyclic graph (DAG) with the tagged root node
// in the source Target to the destination Target.
// The destination reference will be the same as the source reference if the
// destination reference is left blank.
//
// Returns the descriptor of the root node on successful copy.
func Copy(ctx context.Context, src ReadOnlyTarget, srcRef string, dst Target, dstRef string, opts CopyOptions) (ocispec.Descriptor, error) {
if src == nil {
return ocispec.Descriptor{}, errors.New("nil source target")
}
if dst == nil {
return ocispec.Descriptor{}, errors.New("nil destination target")
}
if dstRef == "" {
dstRef = srcRef
}
// use caching proxy on non-leaf nodes
if opts.MaxMetadataBytes <= 0 {
opts.MaxMetadataBytes = defaultCopyMaxMetadataBytes
}
proxy := cas.NewProxyWithLimit(src, cas.NewMemory(), opts.MaxMetadataBytes)
root, err := resolveRoot(ctx, src, srcRef, proxy)
if err != nil {
return ocispec.Descriptor{}, fmt.Errorf("failed to resolve %s: %w", srcRef, err)
}
if opts.MapRoot != nil {
proxy.StopCaching = true
root, err = opts.MapRoot(ctx, proxy, root)
if err != nil {
return ocispec.Descriptor{}, err
}
proxy.StopCaching = false
}
if err := prepareCopy(ctx, dst, dstRef, proxy, root, &opts); err != nil {
return ocispec.Descriptor{}, err
}
if err := copyGraph(ctx, src, dst, root, proxy, nil, nil, opts.CopyGraphOptions); err != nil {
return ocispec.Descriptor{}, err
}
return root, nil
}
// CopyGraph copies a rooted directed acyclic graph (DAG) from the source CAS to
// the destination CAS.
func CopyGraph(ctx context.Context, src content.ReadOnlyStorage, dst content.Storage, root ocispec.Descriptor, opts CopyGraphOptions) error {
return copyGraph(ctx, src, dst, root, nil, nil, nil, opts)
}
// copyGraph copies a rooted directed acyclic graph (DAG) from the source CAS to
// the destination CAS with specified caching, concurrency limiter and tracker.
func copyGraph(ctx context.Context, src content.ReadOnlyStorage, dst content.Storage, root ocispec.Descriptor,
proxy *cas.Proxy, limiter *semaphore.Weighted, tracker *status.Tracker, opts CopyGraphOptions) error {
if proxy == nil {
// use caching proxy on non-leaf nodes
if opts.MaxMetadataBytes <= 0 {
opts.MaxMetadataBytes = defaultCopyMaxMetadataBytes
}
proxy = cas.NewProxyWithLimit(src, cas.NewMemory(), opts.MaxMetadataBytes)
}
if limiter == nil {
// if Concurrency is not set or invalid, use the default concurrency
if opts.Concurrency <= 0 {
opts.Concurrency = defaultConcurrency
}
limiter = semaphore.NewWeighted(int64(opts.Concurrency))
}
if tracker == nil {
// track content status
tracker = status.NewTracker()
}
// if FindSuccessors is not provided, use the default one
if opts.FindSuccessors == nil {
opts.FindSuccessors = content.Successors
}
// traverse the graph
var fn syncutil.GoFunc[ocispec.Descriptor]
fn = func(ctx context.Context, region *syncutil.LimitedRegion, desc ocispec.Descriptor) (err error) {
// skip the descriptor if other go routine is working on it
done, committed := tracker.TryCommit(desc)
if !committed {
return nil
}
defer func() {
if err == nil {
// mark the content as done on success
close(done)
}
}()
// skip if a rooted sub-DAG exists
exists, err := dst.Exists(ctx, desc)
if err != nil {
return err
}
if exists {
if opts.OnCopySkipped != nil {
if err := opts.OnCopySkipped(ctx, desc); err != nil {
return err
}
}
return nil
}
// find successors while non-leaf nodes will be fetched and cached
successors, err := opts.FindSuccessors(ctx, proxy, desc)
if err != nil {
return err
}
successors = removeForeignLayers(successors)
if len(successors) != 0 {
// for non-leaf nodes, process successors and wait for them to complete
region.End()
if err := syncutil.Go(ctx, limiter, fn, successors...); err != nil {
return err
}
for _, node := range successors {
done, committed := tracker.TryCommit(node)
if committed {
return fmt.Errorf("%s: %s: successor not committed", desc.Digest, node.Digest)
}
select {
case <-done:
case <-ctx.Done():
return ctx.Err()
}
}
if err := region.Start(); err != nil {
return err
}
}
exists, err = proxy.Cache.Exists(ctx, desc)
if err != nil {
return err
}
if exists {
return copyNode(ctx, proxy.Cache, dst, desc, opts)
}
return mountOrCopyNode(ctx, src, dst, desc, opts)
}
return syncutil.Go(ctx, limiter, fn, root)
}
// mountOrCopyNode tries to mount the node, if not falls back to copying.
func mountOrCopyNode(ctx context.Context, src content.ReadOnlyStorage, dst content.Storage, desc ocispec.Descriptor, opts CopyGraphOptions) error {
// Need MountFrom and it must be a blob
if opts.MountFrom == nil || descriptor.IsManifest(desc) {
return copyNode(ctx, src, dst, desc, opts)
}
mounter, ok := dst.(registry.Mounter)
if !ok {
// mounting is not supported by the destination
return copyNode(ctx, src, dst, desc, opts)
}
sourceRepositories, err := opts.MountFrom(ctx, desc)
if err != nil {
// Technically this error is not fatal, we can still attempt to copy the node
// But for consistency with the other callbacks we bail out.
return err
}
if len(sourceRepositories) == 0 {
return copyNode(ctx, src, dst, desc, opts)
}
skipSource := errors.New("skip source")
for i, sourceRepository := range sourceRepositories {
// try mounting this source repository
var mountFailed bool
getContent := func() (io.ReadCloser, error) {
// the invocation of getContent indicates that mounting has failed
mountFailed = true
if i < len(sourceRepositories)-1 {
// If this is not the last one, skip this source and try next one
// We want to return an error that we will test for from mounter.Mount()
return nil, skipSource
}
// this is the last iteration so we need to actually get the content and do the copy
// but first call the PreCopy function
if opts.PreCopy != nil {
if err := opts.PreCopy(ctx, desc); err != nil {
return nil, err
}
}
return src.Fetch(ctx, desc)
}
// Mount or copy
if err := mounter.Mount(ctx, desc, sourceRepository, getContent); err != nil && !errors.Is(err, skipSource) {
return err
}
if !mountFailed {
// mounted, success
if opts.OnMounted != nil {
if err := opts.OnMounted(ctx, desc); err != nil {
return err
}
}
return nil
}
}
// we copied it
if opts.PostCopy != nil {
if err := opts.PostCopy(ctx, desc); err != nil {
return err
}
}
return nil
}
// doCopyNode copies a single content from the source CAS to the destination CAS.
func doCopyNode(ctx context.Context, src content.ReadOnlyStorage, dst content.Storage, desc ocispec.Descriptor) error {
rc, err := src.Fetch(ctx, desc)
if err != nil {
return err
}
defer rc.Close()
err = dst.Push(ctx, desc, rc)
if err != nil && !errors.Is(err, errdef.ErrAlreadyExists) {
return err
}
return nil
}
// copyNode copies a single content from the source CAS to the destination CAS,
// and apply the given options.
func copyNode(ctx context.Context, src content.ReadOnlyStorage, dst content.Storage, desc ocispec.Descriptor, opts CopyGraphOptions) error {
if opts.PreCopy != nil {
if err := opts.PreCopy(ctx, desc); err != nil {
if err == SkipNode {
return nil
}
return err
}
}
if err := doCopyNode(ctx, src, dst, desc); err != nil {
return err
}
if opts.PostCopy != nil {
return opts.PostCopy(ctx, desc)
}
return nil
}
// copyCachedNodeWithReference copies a single content with a reference from the
// source cache to the destination ReferencePusher.
func copyCachedNodeWithReference(ctx context.Context, src *cas.Proxy, dst registry.ReferencePusher, desc ocispec.Descriptor, dstRef string) error {
rc, err := src.FetchCached(ctx, desc)
if err != nil {
return err
}
defer rc.Close()
err = dst.PushReference(ctx, desc, rc, dstRef)
if err != nil && !errors.Is(err, errdef.ErrAlreadyExists) {
return err
}
return nil
}
// resolveRoot resolves the source reference to the root node.
func resolveRoot(ctx context.Context, src ReadOnlyTarget, srcRef string, proxy *cas.Proxy) (ocispec.Descriptor, error) {
refFetcher, ok := src.(registry.ReferenceFetcher)
if !ok {
return src.Resolve(ctx, srcRef)
}
// optimize performance for ReferenceFetcher targets
refProxy := ®istryutil.Proxy{
ReferenceFetcher: refFetcher,
Proxy: proxy,
}
root, rc, err := refProxy.FetchReference(ctx, srcRef)
if err != nil {
return ocispec.Descriptor{}, err
}
defer rc.Close()
// cache root if it is a non-leaf node
fetcher := content.FetcherFunc(func(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
if content.Equal(target, root) {
return rc, nil
}
return nil, errors.New("fetching only root node expected")
})
if _, err = content.Successors(ctx, fetcher, root); err != nil {
return ocispec.Descriptor{}, err
}
// TODO: optimize special case where root is a leaf node (i.e. a blob)
// and dst is a ReferencePusher.
return root, nil
}
// prepareCopy prepares the hooks for copy.
func prepareCopy(ctx context.Context, dst Target, dstRef string, proxy *cas.Proxy, root ocispec.Descriptor, opts *CopyOptions) error {
if refPusher, ok := dst.(registry.ReferencePusher); ok {
// optimize performance for ReferencePusher targets
preCopy := opts.PreCopy
opts.PreCopy = func(ctx context.Context, desc ocispec.Descriptor) error {
if preCopy != nil {
if err := preCopy(ctx, desc); err != nil {
return err
}
}
if !content.Equal(desc, root) {
// for non-root node, do nothing
return nil
}
// for root node, prepare optimized copy
if err := copyCachedNodeWithReference(ctx, proxy, refPusher, desc, dstRef); err != nil {
return err
}
if opts.PostCopy != nil {
if err := opts.PostCopy(ctx, desc); err != nil {
return err
}
}
// skip the regular copy workflow
return SkipNode
}
} else {
postCopy := opts.PostCopy
opts.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error {
if content.Equal(desc, root) {
// for root node, tag it after copying it
if err := dst.Tag(ctx, root, dstRef); err != nil {
return err
}
}
if postCopy != nil {
return postCopy(ctx, desc)
}
return nil
}
}
onCopySkipped := opts.OnCopySkipped
opts.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error {
if !content.Equal(desc, root) {
if onCopySkipped != nil {
return onCopySkipped(ctx, desc)
}
return nil
}
// enforce tagging when the skipped node is root
if refPusher, ok := dst.(registry.ReferencePusher); ok {
// NOTE: refPusher tags the node by copying it with the reference,
// so onCopySkipped shouldn't be invoked in this case
return copyCachedNodeWithReference(ctx, proxy, refPusher, desc, dstRef)
}
// invoke onCopySkipped before tagging
if onCopySkipped != nil {
if err := onCopySkipped(ctx, desc); err != nil {
return err
}
}
return dst.Tag(ctx, root, dstRef)
}
return nil
}
// removeForeignLayers in-place removes all foreign layers in the given slice.
func removeForeignLayers(descs []ocispec.Descriptor) []ocispec.Descriptor {
var j int
for i, desc := range descs {
if !descriptor.IsForeignLayer(desc) {
if i != j {
descs[j] = desc
}
j++
}
}
return descs[:j]
}
oras-go-2.5.0/copy_test.go 0000664 0000000 0000000 00000206312 14576745303 0015447 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 oras_test
import (
"bytes"
"context"
_ "crypto/sha256"
"encoding/json"
"errors"
"fmt"
"io"
"reflect"
"sync/atomic"
"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"
"oras.land/oras-go/v2/content/file"
"oras.land/oras-go/v2/content/memory"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/internal/cas"
"oras.land/oras-go/v2/internal/docker"
"oras.land/oras-go/v2/internal/spec"
)
// storageTracker tracks storage API counts.
type storageTracker struct {
content.Storage
fetch int64
push int64
exists int64
}
func (t *storageTracker) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
atomic.AddInt64(&t.fetch, 1)
return t.Storage.Fetch(ctx, target)
}
func (t *storageTracker) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error {
atomic.AddInt64(&t.push, 1)
return t.Storage.Push(ctx, expected, content)
}
func (t *storageTracker) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) {
atomic.AddInt64(&t.exists, 1)
return t.Storage.Exists(ctx, target)
}
func TestCopy_FullCopy(t *testing.T) {
src := memory.New()
dst := memory.New()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
generateManifest(descs[0], descs[1:3]...) // Blob 3
ctx := context.Background()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
root := descs[3]
ref := "foobar"
err := src.Tag(ctx, root, ref)
if err != nil {
t.Fatal("fail to tag root node", err)
}
// test copy
gotDesc, err := oras.Copy(ctx, src, ref, dst, "", oras.CopyOptions{})
if err != nil {
t.Fatalf("Copy() error = %v, wantErr %v", err, false)
}
if !reflect.DeepEqual(gotDesc, root) {
t.Errorf("Copy() = %v, want %v", gotDesc, root)
}
// verify contents
for i, desc := range descs {
exists, err := dst.Exists(ctx, desc)
if err != nil {
t.Fatalf("dst.Exists(%d) error = %v", i, err)
}
if !exists {
t.Errorf("dst.Exists(%d) = %v, want %v", i, exists, true)
}
}
// verify tag
gotDesc, err = dst.Resolve(ctx, ref)
if err != nil {
t.Fatal("dst.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, root) {
t.Errorf("dst.Resolve() = %v, want %v", gotDesc, root)
}
}
func TestCopy_ExistedRoot(t *testing.T) {
src := memory.New()
dst := memory.New()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
generateManifest(descs[0], descs[1:3]...) // Blob 3
ctx := context.Background()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
root := descs[3]
ref := "foobar"
newTag := "newtag"
err := src.Tag(ctx, root, ref)
if err != nil {
t.Fatal("fail to tag root node", err)
}
var skippedCount int64
copyOpts := oras.CopyOptions{
CopyGraphOptions: oras.CopyGraphOptions{
OnCopySkipped: func(ctx context.Context, desc ocispec.Descriptor) error {
atomic.AddInt64(&skippedCount, 1)
return nil
},
},
}
// copy with src tag
gotDesc, err := oras.Copy(ctx, src, ref, dst, "", copyOpts)
if err != nil {
t.Fatalf("Copy() error = %v, wantErr %v", err, false)
}
if !reflect.DeepEqual(gotDesc, root) {
t.Errorf("Copy() = %v, want %v", gotDesc, root)
}
// copy with new tag
gotDesc, err = oras.Copy(ctx, src, ref, dst, newTag, copyOpts)
if err != nil {
t.Fatalf("Copy() error = %v, wantErr %v", err, false)
}
if !reflect.DeepEqual(gotDesc, root) {
t.Errorf("Copy() = %v, want %v", gotDesc, root)
}
// verify contents
for i, desc := range descs {
exists, err := dst.Exists(ctx, desc)
if err != nil {
t.Fatalf("dst.Exists(%d) error = %v", i, err)
}
if !exists {
t.Errorf("dst.Exists(%d) = %v, want %v", i, exists, true)
}
}
// verify src tag
gotDesc, err = dst.Resolve(ctx, ref)
if err != nil {
t.Fatal("dst.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, root) {
t.Errorf("dst.Resolve() = %v, want %v", gotDesc, root)
}
// verify new tag
gotDesc, err = dst.Resolve(ctx, newTag)
if err != nil {
t.Fatal("dst.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, root) {
t.Errorf("dst.Resolve() = %v, want %v", gotDesc, root)
}
// verify invocation of onCopySkipped()
if got, want := skippedCount, int64(1); got != want {
t.Errorf("count(OnCopySkipped()) = %v, want %v", got, want)
}
}
func TestCopyGraph_FullCopy(t *testing.T) {
src := cas.NewMemory()
dst := cas.NewMemory()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 3
generateManifest(descs[0], descs[1:3]...) // Blob 4
generateManifest(descs[0], descs[3]) // Blob 5
generateManifest(descs[0], descs[1:4]...) // Blob 6
generateIndex(descs[4:6]...) // Blob 7
generateIndex(descs[6]) // Blob 8
generateIndex() // Blob 9
generateIndex(descs[7:10]...) // Blob 10
ctx := context.Background()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// test copy graph
srcTracker := &storageTracker{Storage: src}
dstTracker := &storageTracker{Storage: dst}
root := descs[len(descs)-1]
if err := oras.CopyGraph(ctx, srcTracker, dstTracker, root, oras.CopyGraphOptions{}); err != nil {
t.Fatalf("CopyGraph() error = %v, wantErr %v", err, false)
}
// verify contents
contents := dst.Map()
if got, want := len(contents), len(blobs); got != want {
t.Errorf("len(dst) = %v, wantErr %v", got, want)
}
for i := range blobs {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, false)
continue
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Errorf("content[%d] = %v, want %v", i, got, want)
}
}
// verify API counts
if got, want := srcTracker.fetch, int64(len(blobs)); got != want {
t.Errorf("count(src.Fetch()) = %v, want %v", got, want)
}
if got, want := srcTracker.push, int64(0); got != want {
t.Errorf("count(src.Push()) = %v, want %v", got, want)
}
if got, want := srcTracker.exists, int64(0); got != want {
t.Errorf("count(src.Exists()) = %v, want %v", got, want)
}
if got, want := dstTracker.fetch, int64(0); got != want {
t.Errorf("count(dst.Fetch()) = %v, want %v", got, want)
}
if got, want := dstTracker.push, int64(len(blobs)); got != want {
t.Errorf("count(dst.Push()) = %v, want %v", got, want)
}
if got, want := dstTracker.exists, int64(len(blobs)); got != want {
t.Errorf("count(dst.Exists()) = %v, want %v", got, want)
}
}
func TestCopyGraph_PartialCopy(t *testing.T) {
src := cas.NewMemory()
dst := cas.NewMemory()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
generateManifest(descs[0], descs[1:3]...) // Blob 3
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 4
generateManifest(descs[0], descs[4]) // Blob 5
generateIndex(descs[3], descs[5]) // Blob 6
ctx := context.Background()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// initial copy
root := descs[3]
if err := oras.CopyGraph(ctx, src, dst, root, oras.CopyGraphOptions{}); err != nil {
t.Fatalf("CopyGraph() error = %v, wantErr %v", err, false)
}
// verify contents
contents := dst.Map()
if got, want := len(contents), len(blobs[:4]); got != want {
t.Fatalf("len(dst) = %v, wantErr %v", got, want)
}
for i := range blobs[:4] {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Fatalf("content[%d] error = %v, wantErr %v", i, err, false)
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Fatalf("content[%d] = %v, want %v", i, got, want)
}
}
// test copy
srcTracker := &storageTracker{Storage: src}
dstTracker := &storageTracker{Storage: dst}
root = descs[len(descs)-1]
if err := oras.CopyGraph(ctx, srcTracker, dstTracker, root, oras.CopyGraphOptions{}); err != nil {
t.Fatalf("CopyGraph() error = %v, wantErr %v", err, false)
}
// verify contents
contents = dst.Map()
if got, want := len(contents), len(blobs); got != want {
t.Errorf("len(dst) = %v, wantErr %v", got, want)
}
for i := range blobs {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, false)
continue
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Errorf("content[%d] = %v, want %v", i, got, want)
}
}
// verify API counts
if got, want := srcTracker.fetch, int64(3); got != want {
t.Errorf("count(src.Fetch()) = %v, want %v", got, want)
}
if got, want := srcTracker.push, int64(0); got != want {
t.Errorf("count(src.Push()) = %v, want %v", got, want)
}
if got, want := srcTracker.exists, int64(0); got != want {
t.Errorf("count(src.Exists()) = %v, want %v", got, want)
}
if got, want := dstTracker.fetch, int64(0); got != want {
t.Errorf("count(dst.Fetch()) = %v, want %v", got, want)
}
if got, want := dstTracker.push, int64(3); got != want {
t.Errorf("count(dst.Push()) = %v, want %v", got, want)
}
if got, want := dstTracker.exists, int64(5); got != want {
t.Errorf("count(dst.Exists()) = %v, want %v", got, want)
}
}
func TestCopy_WithOptions(t *testing.T) {
src := memory.New()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
appendManifest := func(arc, os string, mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
Platform: &ocispec.Platform{
Architecture: arc,
OS: os,
},
})
}
generateManifest := func(arc, os string, config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendManifest(arc, os, ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
generateManifest("test-arc-1", "test-os-1", descs[0], descs[1:3]...) // Blob 3
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 4
generateManifest("test-arc-2", "test-os-2", descs[0], descs[4]) // Blob 5
generateIndex(descs[3], descs[5]) // Blob 6
ctx := context.Background()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
root := descs[6]
ref := "foobar"
err := src.Tag(ctx, root, ref)
if err != nil {
t.Fatal("fail to tag root node", err)
}
// test copy with media type filter
dst := memory.New()
opts := oras.DefaultCopyOptions
opts.MapRoot = func(ctx context.Context, src content.ReadOnlyStorage, root ocispec.Descriptor) (ocispec.Descriptor, error) {
if root.MediaType == ocispec.MediaTypeImageIndex {
return root, nil
} else {
return ocispec.Descriptor{}, errdef.ErrNotFound
}
}
gotDesc, err := oras.Copy(ctx, src, ref, dst, "", opts)
if err != nil {
t.Fatalf("Copy() error = %v, wantErr %v", err, false)
}
if !reflect.DeepEqual(gotDesc, root) {
t.Errorf("Copy() = %v, want %v", gotDesc, root)
}
// verify contents
for i, desc := range descs {
exists, err := dst.Exists(ctx, desc)
if err != nil {
t.Fatalf("dst.Exists(%d) error = %v", i, err)
}
if !exists {
t.Errorf("dst.Exists(%d) = %v, want %v", i, exists, true)
}
}
// verify tag
gotDesc, err = dst.Resolve(ctx, ref)
if err != nil {
t.Fatal("dst.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, root) {
t.Errorf("dst.Resolve() = %v, want %v", gotDesc, root)
}
// test copy with platform filter and hooks
dst = memory.New()
preCopyCount := int64(0)
postCopyCount := int64(0)
opts = oras.CopyOptions{
MapRoot: func(ctx context.Context, src content.ReadOnlyStorage, root ocispec.Descriptor) (ocispec.Descriptor, error) {
manifests, err := content.Successors(ctx, src, root)
if err != nil {
return ocispec.Descriptor{}, errdef.ErrNotFound
}
// platform filter
for _, m := range manifests {
if m.Platform.Architecture == "test-arc-2" && m.Platform.OS == "test-os-2" {
return m, nil
}
}
return ocispec.Descriptor{}, errdef.ErrNotFound
},
CopyGraphOptions: oras.CopyGraphOptions{
PreCopy: func(ctx context.Context, desc ocispec.Descriptor) error {
atomic.AddInt64(&preCopyCount, 1)
return nil
},
PostCopy: func(ctx context.Context, desc ocispec.Descriptor) error {
atomic.AddInt64(&postCopyCount, 1)
return nil
},
},
}
wantDesc := descs[5]
gotDesc, err = oras.Copy(ctx, src, ref, dst, "", opts)
if err != nil {
t.Fatalf("Copy() error = %v, wantErr %v", err, false)
}
if !reflect.DeepEqual(gotDesc, wantDesc) {
t.Errorf("Copy() = %v, want %v", gotDesc, wantDesc)
}
// verify contents
for i, desc := range append([]ocispec.Descriptor{descs[0]}, descs[4:6]...) {
exists, err := dst.Exists(ctx, desc)
if err != nil {
t.Fatalf("dst.Exists(%d) error = %v", i, err)
}
if !exists {
t.Errorf("dst.Exists(%d) = %v, want %v", i, exists, true)
}
}
// verify tag
gotDesc, err = dst.Resolve(ctx, ref)
if err != nil {
t.Fatal("dst.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, wantDesc) {
t.Errorf("dst.Resolve() = %v, want %v", gotDesc, wantDesc)
}
// verify API counts
if got, want := preCopyCount, int64(3); got != want {
t.Errorf("count(PreCopy()) = %v, want %v", got, want)
}
if got, want := postCopyCount, int64(3); got != want {
t.Errorf("count(PostCopy()) = %v, want %v", got, want)
}
// test copy with root filter, but no matching node can be found
dst = memory.New()
opts = oras.CopyOptions{
MapRoot: func(ctx context.Context, src content.ReadOnlyStorage, root ocispec.Descriptor) (ocispec.Descriptor, error) {
if root.MediaType == "test" {
return root, nil
} else {
return ocispec.Descriptor{}, errdef.ErrNotFound
}
},
CopyGraphOptions: oras.DefaultCopyGraphOptions,
}
_, err = oras.Copy(ctx, src, ref, dst, "", opts)
if !errors.Is(err, errdef.ErrNotFound) {
t.Fatalf("Copy() error = %v, wantErr %v", err, errdef.ErrNotFound)
}
// test copy with MaxMetadataBytes = 1
dst = memory.New()
opts = oras.CopyOptions{
CopyGraphOptions: oras.CopyGraphOptions{
MaxMetadataBytes: 1,
},
}
if _, err := oras.Copy(ctx, src, ref, dst, "", opts); !errors.Is(err, errdef.ErrSizeExceedsLimit) {
t.Fatalf("CopyGraph() error = %v, wantErr %v", err, errdef.ErrSizeExceedsLimit)
}
}
func TestCopy_WithTargetPlatformOptions(t *testing.T) {
src := memory.New()
arc_1 := "test-arc-1"
os_1 := "test-os-1"
variant_1 := "v1"
arc_2 := "test-arc-2"
os_2 := "test-os-2"
variant_2 := "v2"
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
appendManifest := func(arc, os, variant string, mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
Platform: &ocispec.Platform{
Architecture: arc,
OS: os,
Variant: variant,
},
})
}
generateManifest := func(arc, os, variant string, config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendManifest(arc, os, variant, ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte(`{"mediaType":"application/vnd.oci.image.config.v1+json",
"created":"2022-07-29T08:13:55Z",
"author":"test author",
"architecture":"test-arc-1",
"os":"test-os-1",
"variant":"v1"}`)) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
generateManifest(arc_1, os_1, variant_1, descs[0], descs[1:3]...) // Blob 3
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello1")) // Blob 4
generateManifest(arc_2, os_2, variant_1, descs[0], descs[4]) // Blob 5
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello2")) // Blob 6
generateManifest(arc_1, os_1, variant_2, descs[0], descs[6]) // Blob 7
generateIndex(descs[3], descs[5], descs[7]) // Blob 8
ctx := context.Background()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
root := descs[8]
ref := "foobar"
err := src.Tag(ctx, root, ref)
if err != nil {
t.Fatal("fail to tag root node", err)
}
// test copy with platform filter for the image index
dst := memory.New()
opts := oras.CopyOptions{}
targetPlatform := ocispec.Platform{
Architecture: arc_2,
OS: os_2,
}
opts.WithTargetPlatform(&targetPlatform)
wantDesc := descs[5]
gotDesc, err := oras.Copy(ctx, src, ref, dst, "", opts)
if err != nil {
t.Fatalf("Copy() error = %v, wantErr %v", err, false)
}
if !reflect.DeepEqual(gotDesc, wantDesc) {
t.Errorf("Copy() = %v, want %v", gotDesc, wantDesc)
}
// verify contents
for i, desc := range append([]ocispec.Descriptor{descs[0]}, descs[4:6]...) {
exists, err := dst.Exists(ctx, desc)
if err != nil {
t.Fatalf("dst.Exists(%d) error = %v", i, err)
}
if !exists {
t.Errorf("dst.Exists(%d) = %v, want %v", i, exists, true)
}
}
// verify tag
gotDesc, err = dst.Resolve(ctx, ref)
if err != nil {
t.Fatal("dst.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, wantDesc) {
t.Errorf("dst.Resolve() = %v, want %v", gotDesc, wantDesc)
}
// test copy with platform filter for the image index, and multiple
// manifests match the required platform. Should return the first matching
// entry.
dst = memory.New()
targetPlatform = ocispec.Platform{
Architecture: arc_1,
OS: os_1,
}
opts = oras.CopyOptions{}
opts.WithTargetPlatform(&targetPlatform)
wantDesc = descs[3]
gotDesc, err = oras.Copy(ctx, src, ref, dst, "", opts)
if err != nil {
t.Fatalf("Copy() error = %v, wantErr %v", err, false)
}
if !reflect.DeepEqual(gotDesc, wantDesc) {
t.Errorf("Copy() = %v, want %v", gotDesc, wantDesc)
}
// verify contents
for i, desc := range append([]ocispec.Descriptor{descs[0]}, descs[1:3]...) {
exists, err := dst.Exists(ctx, desc)
if err != nil {
t.Fatalf("dst.Exists(%d) error = %v", i, err)
}
if !exists {
t.Errorf("dst.Exists(%d) = %v, want %v", i, exists, true)
}
}
// verify tag
gotDesc, err = dst.Resolve(ctx, ref)
if err != nil {
t.Fatal("dst.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, wantDesc) {
t.Errorf("dst.Resolve() = %v, want %v", gotDesc, wantDesc)
}
// test copy with platform filter and existing MapRoot func for the image
// index, but there is no matching node. Should return not found error.
dst = memory.New()
opts = oras.CopyOptions{
MapRoot: func(ctx context.Context, src content.ReadOnlyStorage, root ocispec.Descriptor) (ocispec.Descriptor, error) {
if root.MediaType == ocispec.MediaTypeImageIndex {
return root, nil
} else {
return ocispec.Descriptor{}, errdef.ErrNotFound
}
},
}
targetPlatform = ocispec.Platform{
Architecture: arc_1,
OS: os_2,
}
opts.WithTargetPlatform(&targetPlatform)
_, err = oras.Copy(ctx, src, ref, dst, "", opts)
expected := fmt.Sprintf("%s: %v: no matching manifest was found in the manifest list", root.Digest, errdef.ErrNotFound)
if err.Error() != expected {
t.Fatalf("Copy() error = %v, wantErr %v", err, expected)
}
// test copy with platform filter for the manifest
dst = memory.New()
opts = oras.CopyOptions{}
targetPlatform = ocispec.Platform{
Architecture: arc_1,
OS: os_1,
}
opts.WithTargetPlatform(&targetPlatform)
root = descs[7]
err = src.Tag(ctx, root, ref)
if err != nil {
t.Fatal("fail to tag root node", err)
}
wantDesc = descs[7]
gotDesc, err = oras.Copy(ctx, src, ref, dst, "", opts)
if err != nil {
t.Fatalf("Copy() error = %v, wantErr %v", err, false)
}
if !reflect.DeepEqual(gotDesc, wantDesc) {
t.Errorf("Copy() = %v, want %v", gotDesc, wantDesc)
}
// verify contents
for i, desc := range append([]ocispec.Descriptor{descs[0]}, descs[6]) {
exists, err := dst.Exists(ctx, desc)
if err != nil {
t.Fatalf("dst.Exists(%d) error = %v", i, err)
}
if !exists {
t.Errorf("dst.Exists(%d) = %v, want %v", i, exists, true)
}
}
// verify tag
gotDesc, err = dst.Resolve(ctx, ref)
if err != nil {
t.Fatal("dst.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, wantDesc) {
t.Errorf("dst.Resolve() = %v, want %v", gotDesc, wantDesc)
}
// test copy with platform filter for the manifest, but there is no matching
// node. Should return not found error.
dst = memory.New()
opts = oras.CopyOptions{}
targetPlatform = ocispec.Platform{
Architecture: arc_1,
OS: os_1,
Variant: variant_2,
}
opts.WithTargetPlatform(&targetPlatform)
_, err = oras.Copy(ctx, src, ref, dst, "", opts)
expected = fmt.Sprintf("%s: %v: platform in manifest does not match target platform", root.Digest, errdef.ErrNotFound)
if err.Error() != expected {
t.Fatalf("Copy() error = %v, wantErr %v", err, expected)
}
// test copy with platform filter, but the node's media type is not
// supported. Should return unsupported error
dst = memory.New()
opts = oras.CopyOptions{}
targetPlatform = ocispec.Platform{
Architecture: arc_1,
OS: os_1,
}
opts.WithTargetPlatform(&targetPlatform)
root = descs[1]
err = src.Tag(ctx, root, ref)
if err != nil {
t.Fatal("fail to tag root node", err)
}
_, err = oras.Copy(ctx, src, ref, dst, "", opts)
if !errors.Is(err, errdef.ErrUnsupported) {
t.Fatalf("Copy() error = %v, wantErr %v", err, errdef.ErrUnsupported)
}
// generate incorrect test content
blobs = nil
descs = nil
appendBlob(docker.MediaTypeConfig, []byte(`{"mediaType":"application/vnd.oci.image.config.v1+json",
"created":"2022-07-29T08:13:55Z",
"author":"test author 1",
"architecture":"test-arc-1",
"os":"test-os-1",
"variant":"v1"}`)) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo1")) // Blob 1
generateManifest(arc_1, os_1, variant_1, descs[0], descs[1]) // Blob 2
generateIndex(descs[2]) // Blob 3
ctx = context.Background()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
dst = memory.New()
opts = oras.CopyOptions{}
targetPlatform = ocispec.Platform{
Architecture: arc_1,
OS: os_1,
}
opts.WithTargetPlatform(&targetPlatform)
// test copy with platform filter for the manifest, but the manifest is
// invalid by having docker mediaType config in the manifest and oci
// mediaType in the image config. Should return error.
root = descs[2]
err = src.Tag(ctx, root, ref)
if err != nil {
t.Fatal("fail to tag root node", err)
}
_, err = oras.Copy(ctx, src, ref, dst, "", opts)
expected = fmt.Sprintf("fail to recognize platform from unknown config %s: expect %s", docker.MediaTypeConfig, ocispec.MediaTypeImageConfig)
if err.Error() != expected {
t.Fatalf("Copy() error = %v, wantErr %v", err, expected)
}
// generate test content with null config blob
blobs = nil
descs = nil
appendBlob(ocispec.MediaTypeImageConfig, []byte("null")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo2")) // Blob 1
generateManifest(arc_1, os_1, variant_1, descs[0], descs[1]) // Blob 2
generateIndex(descs[2]) // Blob 3
ctx = context.Background()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
dst = memory.New()
opts = oras.CopyOptions{}
targetPlatform = ocispec.Platform{
Architecture: arc_1,
OS: os_1,
}
opts.WithTargetPlatform(&targetPlatform)
// test copy with platform filter for the manifest with null config blob
// should return not found error
root = descs[2]
err = src.Tag(ctx, root, ref)
if err != nil {
t.Fatal("fail to tag root node", err)
}
_, err = oras.Copy(ctx, src, ref, dst, "", opts)
expected = fmt.Sprintf("%s: %v: platform in manifest does not match target platform", root.Digest, errdef.ErrNotFound)
if err.Error() != expected {
t.Fatalf("Copy() error = %v, wantErr %v", err, expected)
}
// generate test content with empty config blob
blobs = nil
descs = nil
appendBlob(ocispec.MediaTypeImageConfig, []byte("")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo3")) // Blob 1
generateManifest(arc_1, os_1, variant_1, descs[0], descs[1]) // Blob 2
generateIndex(descs[2]) // Blob 3
ctx = context.Background()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
dst = memory.New()
opts = oras.CopyOptions{}
targetPlatform = ocispec.Platform{
Architecture: arc_1,
OS: os_1,
}
opts.WithTargetPlatform(&targetPlatform)
// test copy with platform filter for the manifest with empty config blob
// should return not found error
root = descs[2]
err = src.Tag(ctx, root, ref)
if err != nil {
t.Fatal("fail to tag root node", err)
}
_, err = oras.Copy(ctx, src, ref, dst, "", opts)
expected = fmt.Sprintf("%s: %v: platform in manifest does not match target platform", root.Digest, errdef.ErrNotFound)
if err.Error() != expected {
t.Fatalf("Copy() error = %v, wantErr %v", err, expected)
}
// test copy with no platform filter and nil opts.MapRoot
// opts.MapRoot should be nil
opts = oras.CopyOptions{}
opts.WithTargetPlatform(nil)
if opts.MapRoot != nil {
t.Fatal("opts.MapRoot not equal to nil when platform is not provided")
}
// test copy with no platform filter and custom opts.MapRoot
// should return ErrNotFound
opts = oras.CopyOptions{
MapRoot: func(ctx context.Context, src content.ReadOnlyStorage, root ocispec.Descriptor) (ocispec.Descriptor, error) {
if root.MediaType == "test" {
return root, nil
} else {
return ocispec.Descriptor{}, errdef.ErrNotFound
}
},
CopyGraphOptions: oras.DefaultCopyGraphOptions,
}
opts.WithTargetPlatform(nil)
_, err = oras.Copy(ctx, src, ref, dst, "", opts)
if !errors.Is(err, errdef.ErrNotFound) {
t.Fatalf("Copy() error = %v, wantErr %v", err, errdef.ErrNotFound)
}
}
func TestCopy_RestoreDuplicates(t *testing.T) {
src := memory.New()
temp := t.TempDir()
dst, err := file.New(temp)
if err != nil {
t.Fatal("file.New() error =", err)
}
defer dst.Close()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte, title string) {
blobs = append(blobs, blob)
desc := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
if title != "" {
desc.Annotations = map[string]string{
ocispec.AnnotationTitle: title,
}
}
descs = append(descs, desc)
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON, "")
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("{}"), "") // Blob 0
// 2 blobs with same content
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello"), "foo.txt") // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello"), "bar.txt") // Blob 2
generateManifest(descs[0], descs[1:3]...) // Blob 3
ctx := context.Background()
for i := range blobs {
exists, err := src.Exists(ctx, descs[i])
if err != nil {
t.Fatalf("failed to check existence in src: %d: %v", i, err)
}
if exists {
continue
}
if err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i])); err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
root := descs[3]
ref := "latest"
err = src.Tag(ctx, root, ref)
if err != nil {
t.Fatal("fail to tag root node", err)
}
// test copy
gotDesc, err := oras.Copy(ctx, src, ref, dst, "", oras.CopyOptions{})
if err != nil {
t.Fatalf("Copy() error = %v, wantErr %v", err, false)
}
if !reflect.DeepEqual(gotDesc, root) {
t.Errorf("Copy() = %v, want %v", gotDesc, root)
}
// verify contents
for i, desc := range descs {
exists, err := dst.Exists(ctx, desc)
if err != nil {
t.Fatalf("dst.Exists(%d) error = %v", i, err)
}
if !exists {
t.Errorf("dst.Exists(%d) = %v, want %v", i, exists, true)
}
}
// verify tag
gotDesc, err = dst.Resolve(ctx, ref)
if err != nil {
t.Fatal("dst.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, root) {
t.Errorf("dst.Resolve() = %v, want %v", gotDesc, root)
}
}
func TestCopy_DiscardDuplicates(t *testing.T) {
src := memory.New()
temp := t.TempDir()
dst, err := file.New(temp)
if err != nil {
t.Fatal("file.New() error =", err)
}
dst.ForceCAS = true
defer dst.Close()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte, title string) {
blobs = append(blobs, blob)
desc := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
if title != "" {
desc.Annotations = map[string]string{
ocispec.AnnotationTitle: title,
}
}
descs = append(descs, desc)
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON, "")
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("{}"), "") // Blob 0
// 2 blobs with same content
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello"), "foo.txt") // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello"), "bar.txt") // Blob 2
generateManifest(descs[0], descs[1:3]...) // Blob 3
ctx := context.Background()
for i := range blobs {
exists, err := src.Exists(ctx, descs[i])
if err != nil {
t.Fatalf("failed to check existence in src: %d: %v", i, err)
}
if exists {
continue
}
if err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i])); err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
root := descs[3]
ref := "latest"
err = src.Tag(ctx, root, ref)
if err != nil {
t.Fatal("fail to tag root node", err)
}
// test copy
gotDesc, err := oras.Copy(ctx, src, ref, dst, "", oras.CopyOptions{})
if err != nil {
t.Fatalf("Copy() error = %v, wantErr %v", err, false)
}
if !reflect.DeepEqual(gotDesc, root) {
t.Errorf("Copy() = %v, want %v", gotDesc, root)
}
// verify only one of foo.txt and bar.txt exists
fooExists, err := dst.Exists(ctx, descs[1])
if err != nil {
t.Fatalf("dst.Exists(foo) error = %v", err)
}
barExists, err := dst.Exists(ctx, descs[2])
if err != nil {
t.Fatalf("dst.Exists(bar) error = %v", err)
}
if fooExists == barExists {
t.Error("Only one of foo.txt and bar.txt should exist")
}
}
func TestCopyGraph_WithOptions(t *testing.T) {
src := cas.NewMemory()
dst := cas.NewMemory()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
generateManifest(descs[0], descs[1:3]...) // Blob 3
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 4
generateManifest(descs[0], descs[4]) // Blob 5
generateIndex(descs[3], descs[5]) // Blob 6
ctx := context.Background()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// initial copy
root := descs[3]
opts := oras.DefaultCopyGraphOptions
opts.FindSuccessors = func(ctx context.Context, fetcher content.Fetcher, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
successors, err := content.Successors(ctx, fetcher, desc)
if err != nil {
return nil, err
}
// filter media type
var filtered []ocispec.Descriptor
for _, s := range successors {
if s.MediaType != ocispec.MediaTypeImageConfig {
filtered = append(filtered, s)
}
}
return filtered, nil
}
if err := oras.CopyGraph(ctx, src, dst, root, opts); err != nil {
t.Fatalf("CopyGraph() error = %v, wantErr %v", err, false)
}
// verify contents
contents := dst.Map()
if got, want := len(contents), len(blobs[1:4]); got != want {
t.Fatalf("len(dst) = %v, wantErr %v", got, want)
}
for i := 1; i < 4; i++ {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Fatalf("content[%d] error = %v, wantErr %v", i, err, false)
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Fatalf("content[%d] = %v, want %v", i, got, want)
}
}
// test successor descriptors not obtained from src
root = descs[3]
opts = oras.DefaultCopyGraphOptions
opts.FindSuccessors = func(ctx context.Context, fetcher content.Fetcher, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
if content.Equal(desc, root) {
return descs[1:3], nil
}
return content.Successors(ctx, fetcher, desc)
}
if err := oras.CopyGraph(ctx, src, cas.NewMemory(), root, opts); err != nil {
t.Fatalf("CopyGraph() error = %v, wantErr %v", err, false)
}
// test partial copy
var preCopyCount int64
var postCopyCount int64
var skippedCount int64
opts = oras.CopyGraphOptions{
PreCopy: func(ctx context.Context, desc ocispec.Descriptor) error {
atomic.AddInt64(&preCopyCount, 1)
return nil
},
PostCopy: func(ctx context.Context, desc ocispec.Descriptor) error {
atomic.AddInt64(&postCopyCount, 1)
return nil
},
OnCopySkipped: func(ctx context.Context, desc ocispec.Descriptor) error {
atomic.AddInt64(&skippedCount, 1)
return nil
},
}
root = descs[len(descs)-1]
if err := oras.CopyGraph(ctx, src, dst, root, opts); err != nil {
t.Fatalf("CopyGraph() error = %v, wantErr %v", err, false)
}
// verify contents
contents = dst.Map()
if got, want := len(contents), len(blobs); got != want {
t.Errorf("len(dst) = %v, wantErr %v", got, want)
}
for i := range blobs {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, false)
continue
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Errorf("content[%d] = %v, want %v", i, got, want)
}
}
// verify API counts
if got, want := preCopyCount, int64(4); got != want {
t.Errorf("count(PreCopy()) = %v, want %v", got, want)
}
if got, want := postCopyCount, int64(4); got != want {
t.Errorf("count(PostCopy()) = %v, want %v", got, want)
}
if got, want := skippedCount, int64(1); got != want {
t.Errorf("count(OnCopySkipped()) = %v, want %v", got, want)
}
// test CopyGraph with MaxMetadataBytes = 1
root = descs[6]
dst = cas.NewMemory()
opts = oras.CopyGraphOptions{
MaxMetadataBytes: 1,
}
if err := oras.CopyGraph(ctx, src, dst, root, opts); !errors.Is(err, errdef.ErrSizeExceedsLimit) {
t.Fatalf("CopyGraph() error = %v, wantErr %v", err, errdef.ErrSizeExceedsLimit)
}
t.Run("SkipNode", func(t *testing.T) {
// test CopyGraph with PreCopy = 1
root = descs[6]
dst := &countingStorage{storage: cas.NewMemory()}
opts = oras.CopyGraphOptions{
PreCopy: func(ctx context.Context, desc ocispec.Descriptor) error {
if descs[1].Digest == desc.Digest {
// blob 1 is handled by us (really this would be a Mount but )
rc, err := src.Fetch(ctx, desc)
if err != nil {
t.Fatalf("Failed to fetch: %v", err)
}
defer rc.Close()
err = dst.storage.Push(ctx, desc, rc) // bypass the counters
if err != nil {
t.Fatalf("Failed to fetch: %v", err)
}
return oras.SkipNode
}
return nil
},
}
if err := oras.CopyGraph(ctx, src, dst, root, opts); err != nil {
t.Fatalf("CopyGraph() error = %v", err)
}
if got, expected := dst.numExists.Load(), int64(7); got != expected {
t.Errorf("count(Exists()) = %d, want %d", got, expected)
}
if got, expected := dst.numFetch.Load(), int64(0); got != expected {
t.Errorf("count(Fetch()) = %d, want %d", got, expected)
}
// 7 (exists) - 1 (skipped) = 6 pushes expected
if got, expected := dst.numPush.Load(), int64(6); got != expected {
// If we get >=7 then SkipNode did not short circuit the push like it is supposed to do.
t.Errorf("count(Push()) = %d, want %d", got, expected)
}
})
t.Run("MountFrom mounted", func(t *testing.T) {
root = descs[6]
dst := &countingStorage{storage: cas.NewMemory()}
var numMount atomic.Int64
dst.mount = func(ctx context.Context,
desc ocispec.Descriptor,
fromRepo string,
getContent func() (io.ReadCloser, error),
) error {
numMount.Add(1)
if expected := "source"; fromRepo != expected {
t.Fatalf("fromRepo = %v, want %v", fromRepo, expected)
}
rc, err := src.Fetch(ctx, desc)
if err != nil {
t.Fatalf("Failed to fetch content: %v", err)
}
defer rc.Close()
err = dst.storage.Push(ctx, desc, rc) // bypass the counters
if err != nil {
t.Fatalf("Failed to push content: %v", err)
}
return nil
}
opts = oras.CopyGraphOptions{}
var numPreCopy, numPostCopy, numOnMounted, numMountFrom atomic.Int64
opts.PreCopy = func(ctx context.Context, desc ocispec.Descriptor) error {
numPreCopy.Add(1)
return nil
}
opts.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error {
numPostCopy.Add(1)
return nil
}
opts.OnMounted = func(ctx context.Context, d ocispec.Descriptor) error {
numOnMounted.Add(1)
return nil
}
opts.MountFrom = func(ctx context.Context, desc ocispec.Descriptor) ([]string, error) {
numMountFrom.Add(1)
return []string{"source"}, nil
}
if err := oras.CopyGraph(ctx, src, dst, root, opts); err != nil {
t.Fatalf("CopyGraph() error = %v", err)
}
if got, expected := dst.numExists.Load(), int64(7); got != expected {
t.Errorf("count(Exists()) = %d, want %d", got, expected)
}
if got, expected := dst.numFetch.Load(), int64(0); got != expected {
t.Errorf("count(Fetch()) = %d, want %d", got, expected)
}
// 7 (exists) - 1 (skipped) = 6 pushes expected
if got, expected := dst.numPush.Load(), int64(3); got != expected {
// If we get >=7 then ErrSkipDesc did not short circuit the push like it is supposed to do.
t.Errorf("count(Push()) = %d, want %d", got, expected)
}
if got, expected := numMount.Load(), int64(4); got != expected {
t.Errorf("count(Mount()) = %d, want %d", got, expected)
}
if got, expected := numOnMounted.Load(), int64(4); got != expected {
t.Errorf("count(OnMounted()) = %d, want %d", got, expected)
}
if got, expected := numMountFrom.Load(), int64(4); got != expected {
t.Errorf("count(MountFrom()) = %d, want %d", got, expected)
}
if got, expected := numPreCopy.Load(), int64(3); got != expected {
t.Errorf("count(PreCopy()) = %d, want %d", got, expected)
}
if got, expected := numPostCopy.Load(), int64(3); got != expected {
t.Errorf("count(PostCopy()) = %d, want %d", got, expected)
}
})
t.Run("MountFrom copied", func(t *testing.T) {
root = descs[6]
dst := &countingStorage{storage: cas.NewMemory()}
var numMount atomic.Int64
dst.mount = func(ctx context.Context,
desc ocispec.Descriptor,
fromRepo string,
getContent func() (io.ReadCloser, error),
) error {
numMount.Add(1)
if expected := "source"; fromRepo != expected {
t.Fatalf("fromRepo = %v, want %v", fromRepo, expected)
}
rc, err := getContent()
if err != nil {
t.Fatalf("Failed to fetch content: %v", err)
}
defer rc.Close()
err = dst.storage.Push(ctx, desc, rc) // bypass the counters
if err != nil {
t.Fatalf("Failed to push content: %v", err)
}
return nil
}
opts = oras.CopyGraphOptions{}
var numPreCopy, numPostCopy, numOnMounted, numMountFrom atomic.Int64
opts.PreCopy = func(ctx context.Context, desc ocispec.Descriptor) error {
numPreCopy.Add(1)
return nil
}
opts.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error {
numPostCopy.Add(1)
return nil
}
opts.OnMounted = func(ctx context.Context, d ocispec.Descriptor) error {
numOnMounted.Add(1)
return nil
}
opts.MountFrom = func(ctx context.Context, desc ocispec.Descriptor) ([]string, error) {
numMountFrom.Add(1)
return []string{"source"}, nil
}
if err := oras.CopyGraph(ctx, src, dst, root, opts); err != nil {
t.Fatalf("CopyGraph() error = %v", err)
}
if got, expected := dst.numExists.Load(), int64(7); got != expected {
t.Errorf("count(Exists()) = %d, want %d", got, expected)
}
if got, expected := dst.numFetch.Load(), int64(0); got != expected {
t.Errorf("count(Fetch()) = %d, want %d", got, expected)
}
// 7 (exists) - 1 (skipped) = 6 pushes expected
if got, expected := dst.numPush.Load(), int64(3); got != expected {
// If we get >=7 then ErrSkipDesc did not short circuit the push like it is supposed to do.
t.Errorf("count(Push()) = %d, want %d", got, expected)
}
if got, expected := numMount.Load(), int64(4); got != expected {
t.Errorf("count(Mount()) = %d, want %d", got, expected)
}
if got, expected := numOnMounted.Load(), int64(0); got != expected {
t.Errorf("count(OnMounted()) = %d, want %d", got, expected)
}
if got, expected := numMountFrom.Load(), int64(4); got != expected {
t.Errorf("count(MountFrom()) = %d, want %d", got, expected)
}
if got, expected := numPreCopy.Load(), int64(7); got != expected {
t.Errorf("count(PreCopy()) = %d, want %d", got, expected)
}
if got, expected := numPostCopy.Load(), int64(7); got != expected {
t.Errorf("count(PostCopy()) = %d, want %d", got, expected)
}
})
t.Run("MountFrom mounted second try", func(t *testing.T) {
root = descs[6]
dst := &countingStorage{storage: cas.NewMemory()}
var numMount atomic.Int64
dst.mount = func(ctx context.Context,
desc ocispec.Descriptor,
fromRepo string,
getContent func() (io.ReadCloser, error),
) error {
numMount.Add(1)
switch fromRepo {
case "source":
rc, err := src.Fetch(ctx, desc)
if err != nil {
t.Fatalf("Failed to fetch content: %v", err)
}
defer rc.Close()
err = dst.storage.Push(ctx, desc, rc) // bypass the counters
if err != nil {
t.Fatalf("Failed to push content: %v", err)
}
return nil
case "missing/the/data":
// simulate a registry mount will fail, so it will request the content to start the copy.
rc, err := getContent()
if err != nil {
return fmt.Errorf("getContent failed: %w", err)
}
defer rc.Close()
err = dst.storage.Push(ctx, desc, rc) // bypass the counters
if err != nil {
t.Fatalf("Failed to push content: %v", err)
}
return nil
default:
t.Fatalf("fromRepo = %v, want either %v or %v", fromRepo, "missing/the/data", "source")
return nil
}
}
opts = oras.CopyGraphOptions{}
var numPreCopy, numPostCopy, numOnMounted, numMountFrom atomic.Int64
opts.PreCopy = func(ctx context.Context, desc ocispec.Descriptor) error {
numPreCopy.Add(1)
return nil
}
opts.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error {
numPostCopy.Add(1)
return nil
}
opts.OnMounted = func(ctx context.Context, d ocispec.Descriptor) error {
numOnMounted.Add(1)
return nil
}
opts.MountFrom = func(ctx context.Context, desc ocispec.Descriptor) ([]string, error) {
numMountFrom.Add(1)
return []string{"missing/the/data", "source"}, nil
}
if err := oras.CopyGraph(ctx, src, dst, root, opts); err != nil {
t.Fatalf("CopyGraph() error = %v", err)
}
if got, expected := dst.numExists.Load(), int64(7); got != expected {
t.Errorf("count(Exists()) = %d, want %d", got, expected)
}
if got, expected := dst.numFetch.Load(), int64(0); got != expected {
t.Errorf("count(Fetch()) = %d, want %d", got, expected)
}
// 7 (exists) - 1 (skipped) = 6 pushes expected
if got, expected := dst.numPush.Load(), int64(3); got != expected {
// If we get >=7 then ErrSkipDesc did not short circuit the push like it is supposed to do.
t.Errorf("count(Push()) = %d, want %d", got, expected)
}
if got, expected := numMount.Load(), int64(4*2); got != expected {
t.Errorf("count(Mount()) = %d, want %d", got, expected)
}
if got, expected := numOnMounted.Load(), int64(4); got != expected {
t.Errorf("count(OnMounted()) = %d, want %d", got, expected)
}
if got, expected := numMountFrom.Load(), int64(4); got != expected {
t.Errorf("count(MountFrom()) = %d, want %d", got, expected)
}
if got, expected := numPreCopy.Load(), int64(3); got != expected {
t.Errorf("count(PreCopy()) = %d, want %d", got, expected)
}
if got, expected := numPostCopy.Load(), int64(3); got != expected {
t.Errorf("count(PostCopy()) = %d, want %d", got, expected)
}
})
t.Run("MountFrom copied dst not a Mounter", func(t *testing.T) {
root = descs[6]
dst := cas.NewMemory()
opts = oras.CopyGraphOptions{}
var numPreCopy, numPostCopy, numOnMounted, numMountFrom atomic.Int64
opts.PreCopy = func(ctx context.Context, desc ocispec.Descriptor) error {
numPreCopy.Add(1)
return nil
}
opts.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error {
numPostCopy.Add(1)
return nil
}
opts.OnMounted = func(ctx context.Context, d ocispec.Descriptor) error {
numOnMounted.Add(1)
return nil
}
opts.MountFrom = func(ctx context.Context, desc ocispec.Descriptor) ([]string, error) {
numMountFrom.Add(1)
return []string{"source"}, nil
}
if err := oras.CopyGraph(ctx, src, dst, root, opts); err != nil {
t.Fatalf("CopyGraph() error = %v", err)
}
if got, expected := numOnMounted.Load(), int64(0); got != expected {
t.Errorf("count(OnMounted()) = %d, want %d", got, expected)
}
if got, expected := numMountFrom.Load(), int64(0); got != expected {
t.Errorf("count(MountFrom()) = %d, want %d", got, expected)
}
if got, expected := numPreCopy.Load(), int64(7); got != expected {
t.Errorf("count(PreCopy()) = %d, want %d", got, expected)
}
if got, expected := numPostCopy.Load(), int64(7); got != expected {
t.Errorf("count(PostCopy()) = %d, want %d", got, expected)
}
})
t.Run("MountFrom empty sourceRepositories", func(t *testing.T) {
root = descs[6]
dst := &countingStorage{storage: cas.NewMemory()}
opts = oras.CopyGraphOptions{}
var numMountFrom atomic.Int64
opts.MountFrom = func(ctx context.Context, desc ocispec.Descriptor) ([]string, error) {
numMountFrom.Add(1)
return nil, nil
}
if err := oras.CopyGraph(ctx, src, dst, root, opts); err != nil {
t.Fatalf("CopyGraph() error = %v", err)
}
if got, expected := dst.numExists.Load(), int64(7); got != expected {
t.Errorf("count(Exists()) = %d, want %d", got, expected)
}
if got, expected := dst.numFetch.Load(), int64(0); got != expected {
t.Errorf("count(Fetch()) = %d, want %d", got, expected)
}
if got, expected := dst.numPush.Load(), int64(7); got != expected {
t.Errorf("count(Push()) = %d, want %d", got, expected)
}
if got, expected := numMountFrom.Load(), int64(4); got != expected {
t.Errorf("count(MountFrom()) = %d, want %d", got, expected)
}
})
t.Run("MountFrom error", func(t *testing.T) {
root = descs[6]
dst := &countingStorage{storage: cas.NewMemory()}
opts = oras.CopyGraphOptions{}
var numMountFrom atomic.Int64
e := errors.New("mountFrom error")
opts.MountFrom = func(ctx context.Context, desc ocispec.Descriptor) ([]string, error) {
numMountFrom.Add(1)
return nil, e
}
if err := oras.CopyGraph(ctx, src, dst, root, opts); !errors.Is(err, e) {
t.Fatalf("CopyGraph() error = %v, wantErr %v", err, e)
}
if got, expected := dst.numExists.Load(), int64(7); got != expected {
t.Errorf("count(Exists()) = %d, want %d", got, expected)
}
if got, expected := dst.numFetch.Load(), int64(0); got != expected {
t.Errorf("count(Fetch()) = %d, want %d", got, expected)
}
if got, expected := dst.numPush.Load(), int64(0); got != expected {
t.Errorf("count(Push()) = %d, want %d", got, expected)
}
if got, expected := numMountFrom.Load(), int64(4); got != expected {
t.Errorf("count(MountFrom()) = %d, want %d", got, expected)
}
})
t.Run("MountFrom OnMounted error", func(t *testing.T) {
root = descs[6]
dst := &countingStorage{storage: cas.NewMemory()}
var numMount atomic.Int64
dst.mount = func(ctx context.Context,
desc ocispec.Descriptor,
fromRepo string,
getContent func() (io.ReadCloser, error),
) error {
numMount.Add(1)
if expected := "source"; fromRepo != expected {
t.Fatalf("fromRepo = %v, want %v", fromRepo, expected)
}
rc, err := src.Fetch(ctx, desc)
if err != nil {
t.Fatalf("Failed to fetch content: %v", err)
}
defer rc.Close()
err = dst.storage.Push(ctx, desc, rc) // bypass the counters
if err != nil {
t.Fatalf("Failed to push content: %v", err)
}
return nil
}
opts = oras.CopyGraphOptions{}
var numPreCopy, numPostCopy, numOnMounted, numMountFrom atomic.Int64
opts.PreCopy = func(ctx context.Context, desc ocispec.Descriptor) error {
numPreCopy.Add(1)
return nil
}
opts.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error {
numPostCopy.Add(1)
return nil
}
e := errors.New("onMounted error")
opts.OnMounted = func(ctx context.Context, d ocispec.Descriptor) error {
numOnMounted.Add(1)
return e
}
opts.MountFrom = func(ctx context.Context, desc ocispec.Descriptor) ([]string, error) {
numMountFrom.Add(1)
return []string{"source"}, nil
}
if err := oras.CopyGraph(ctx, src, dst, root, opts); !errors.Is(err, e) {
t.Fatalf("CopyGraph() error = %v, wantErr %v", err, e)
}
if got, expected := dst.numExists.Load(), int64(7); got != expected {
t.Errorf("count(Exists()) = %d, want %d", got, expected)
}
if got, expected := dst.numFetch.Load(), int64(0); got != expected {
t.Errorf("count(Fetch()) = %d, want %d", got, expected)
}
if got, expected := dst.numPush.Load(), int64(0); got != expected {
t.Errorf("count(Push()) = %d, want %d", got, expected)
}
if got, expected := numMount.Load(), int64(4); got != expected {
t.Errorf("count(Mount()) = %d, want %d", got, expected)
}
if got, expected := numOnMounted.Load(), int64(4); got != expected {
t.Errorf("count(OnMounted()) = %d, want %d", got, expected)
}
if got, expected := numMountFrom.Load(), int64(4); got != expected {
t.Errorf("count(MountFrom()) = %d, want %d", got, expected)
}
if got, expected := numPreCopy.Load(), int64(0); got != expected {
t.Errorf("count(PreCopy()) = %d, want %d", got, expected)
}
if got, expected := numPostCopy.Load(), int64(0); got != expected {
t.Errorf("count(PostCopy()) = %d, want %d", got, expected)
}
})
}
// countingStorage counts the calls to its content.Storage methods
type countingStorage struct {
storage content.Storage
mount mountFunc
numExists, numFetch, numPush atomic.Int64
}
func (cs *countingStorage) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) {
cs.numExists.Add(1)
return cs.storage.Exists(ctx, target)
}
func (cs *countingStorage) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
cs.numFetch.Add(1)
return cs.storage.Fetch(ctx, target)
}
func (cs *countingStorage) Push(ctx context.Context, target ocispec.Descriptor, r io.Reader) error {
cs.numPush.Add(1)
return cs.storage.Push(ctx, target, r)
}
type mountFunc func(context.Context, ocispec.Descriptor, string, func() (io.ReadCloser, error)) error
func (cs *countingStorage) Mount(ctx context.Context,
desc ocispec.Descriptor,
fromRepo string,
getContent func() (io.ReadCloser, error),
) error {
return cs.mount(ctx, desc, fromRepo, getContent)
}
func TestCopyGraph_WithConcurrencyLimit(t *testing.T) {
src := cas.NewMemory()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
MediaType: ocispec.MediaTypeImageManifest,
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(manifest.MediaType, manifestJSON)
}
generateArtifact := func(subject *ocispec.Descriptor, artifactType string, blobs ...ocispec.Descriptor) {
manifest := spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
Subject: subject,
Blobs: blobs,
ArtifactType: artifactType,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(manifest.MediaType, manifestJSON)
}
generateIndex := func(manifests ...ocispec.Descriptor) {
index := ocispec.Index{
MediaType: ocispec.MediaTypeImageIndex,
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(index.MediaType, indexJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
generateManifest(descs[0], descs[1:3]...) // Blob 3
generateArtifact(&descs[3], "artifact.1") // Blob 4
generateArtifact(&descs[3], "artifact.2") // Blob 5
generateArtifact(&descs[3], "artifact.3") // Blob 6
generateArtifact(&descs[3], "artifact.4") // Blob 7
generateIndex(descs[3:8]...) // Blob 8
ctx := context.Background()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// test different concurrency limit
root := descs[len(descs)-1]
directSuccessorsNum := 5
opts := oras.DefaultCopyGraphOptions
for i := 1; i <= directSuccessorsNum; i++ {
dst := cas.NewMemory()
opts.Concurrency = i
if err := oras.CopyGraph(ctx, src, dst, root, opts); err != nil {
t.Fatalf("CopyGraph(concurrency: %d) error = %v, wantErr %v", i, err, false)
}
// verify contents
contents := dst.Map()
if got, want := len(contents), len(blobs); got != want {
t.Errorf("len(dst) = %v, wantErr %v", got, want)
}
for i := range blobs {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, false)
continue
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Errorf("content[%d] = %v, want %v", i, got, want)
}
}
}
}
func TestCopyGraph_ForeignLayers(t *testing.T) {
src := cas.NewMemory()
dst := cas.NewMemory()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
desc := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
if mediaType == ocispec.MediaTypeImageLayerNonDistributable {
desc.URLs = append(desc.URLs, "http://127.0.0.1/dummy")
blob = nil
}
descs = append(descs, desc)
blobs = append(blobs, blob)
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayerNonDistributable, []byte("hello")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 2
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 3
generateManifest(descs[0], descs[1:4]...) // Blob 4
ctx := context.Background()
for i := range blobs {
if blobs[i] == nil {
continue
}
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// test copy
srcTracker := &storageTracker{Storage: src}
dstTracker := &storageTracker{Storage: dst}
root := descs[len(descs)-1]
if err := oras.CopyGraph(ctx, srcTracker, dstTracker, root, oras.CopyGraphOptions{}); err != nil {
t.Fatalf("CopyGraph() error = %v, wantErr %v", err, false)
}
// verify contents
contents := dst.Map()
if got, want := len(contents), len(blobs)-1; got != want {
t.Errorf("len(dst) = %v, wantErr %v", got, want)
}
for i := range blobs {
if blobs[i] == nil {
continue
}
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, false)
continue
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Errorf("content[%d] = %v, want %v", i, got, want)
}
}
// verify API counts
if got, want := srcTracker.fetch, int64(len(blobs)-1); got != want {
t.Errorf("count(src.Fetch()) = %v, want %v", got, want)
}
if got, want := srcTracker.push, int64(0); got != want {
t.Errorf("count(src.Push()) = %v, want %v", got, want)
}
if got, want := srcTracker.exists, int64(0); got != want {
t.Errorf("count(src.Exists()) = %v, want %v", got, want)
}
if got, want := dstTracker.fetch, int64(0); got != want {
t.Errorf("count(dst.Fetch()) = %v, want %v", got, want)
}
if got, want := dstTracker.push, int64(len(blobs)-1); got != want {
t.Errorf("count(dst.Push()) = %v, want %v", got, want)
}
if got, want := dstTracker.exists, int64(len(blobs)-1); got != want {
t.Errorf("count(dst.Exists()) = %v, want %v", got, want)
}
}
func TestCopyGraph_ForeignLayers_Mixed(t *testing.T) {
src := cas.NewMemory()
dst := cas.NewMemory()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
desc := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
if mediaType == ocispec.MediaTypeImageLayerNonDistributable {
desc.URLs = append(desc.URLs, "http://127.0.0.1/dummy")
blob = nil
}
descs = append(descs, desc)
blobs = append(blobs, blob)
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayerNonDistributable, []byte("hello")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 2
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 3
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 4
generateManifest(descs[0], descs[1:5]...) // Blob 5
ctx := context.Background()
for i := range blobs {
if blobs[i] == nil {
continue
}
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// test copy
srcTracker := &storageTracker{Storage: src}
dstTracker := &storageTracker{Storage: dst}
root := descs[len(descs)-1]
if err := oras.CopyGraph(ctx, srcTracker, dstTracker, root, oras.CopyGraphOptions{
Concurrency: 1,
}); err != nil {
t.Fatalf("CopyGraph() error = %v, wantErr %v", err, false)
}
// verify contents
contents := dst.Map()
if got, want := len(contents), len(blobs)-1; got != want {
t.Errorf("len(dst) = %v, wantErr %v", got, want)
}
for i := range blobs {
if blobs[i] == nil {
continue
}
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, false)
continue
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Errorf("content[%d] = %v, want %v", i, got, want)
}
}
// verify API counts
if got, want := srcTracker.fetch, int64(len(blobs)-1); got != want {
t.Errorf("count(src.Fetch()) = %v, want %v", got, want)
}
if got, want := srcTracker.push, int64(0); got != want {
t.Errorf("count(src.Push()) = %v, want %v", got, want)
}
if got, want := srcTracker.exists, int64(0); got != want {
t.Errorf("count(src.Exists()) = %v, want %v", got, want)
}
if got, want := dstTracker.fetch, int64(0); got != want {
t.Errorf("count(dst.Fetch()) = %v, want %v", got, want)
}
if got, want := dstTracker.push, int64(len(blobs)-1); got != want {
t.Errorf("count(dst.Push()) = %v, want %v", got, want)
}
if got, want := dstTracker.exists, int64(len(blobs)-1); got != want {
t.Errorf("count(dst.Exists()) = %v, want %v", got, want)
}
}
oras-go-2.5.0/errdef/ 0000775 0000000 0000000 00000000000 14576745303 0014352 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/errdef/errors.go 0000664 0000000 0000000 00000002143 14576745303 0016215 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 errdef
import "errors"
// Common errors used in ORAS
var (
ErrAlreadyExists = errors.New("already exists")
ErrInvalidDigest = errors.New("invalid digest")
ErrInvalidReference = errors.New("invalid reference")
ErrInvalidMediaType = errors.New("invalid media type")
ErrMissingReference = errors.New("missing reference")
ErrNotFound = errors.New("not found")
ErrSizeExceedsLimit = errors.New("size exceeds limit")
ErrUnsupported = errors.New("unsupported")
ErrUnsupportedVersion = errors.New("unsupported version")
)
oras-go-2.5.0/example_copy_test.go 0000664 0000000 0000000 00000027777 14576745303 0017202 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 oras_test
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strconv"
"strings"
"testing"
"github.com/opencontainers/go-digest"
specs "github.com/opencontainers/image-spec/specs-go"
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/content/oci"
"oras.land/oras-go/v2/internal/spec"
"oras.land/oras-go/v2/registry/remote"
)
var exampleMemoryStore oras.Target
var remoteHost string
var (
exampleManifest, _ = json.Marshal(spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
ArtifactType: "example/content"})
exampleManifestDescriptor = ocispec.Descriptor{
MediaType: spec.MediaTypeArtifactManifest,
Digest: digest.Digest(digest.FromBytes(exampleManifest)),
Size: int64(len(exampleManifest))}
exampleSignatureManifest, _ = json.Marshal(spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
ArtifactType: "example/signature",
Subject: &exampleManifestDescriptor})
exampleSignatureManifestDescriptor = ocispec.Descriptor{
MediaType: spec.MediaTypeArtifactManifest,
Digest: digest.FromBytes(exampleSignatureManifest),
Size: int64(len(exampleSignatureManifest))}
)
func pushBlob(ctx context.Context, mediaType string, blob []byte, target oras.Target) (desc ocispec.Descriptor, err error) {
desc = ocispec.Descriptor{ // Generate descriptor based on the media type and blob content
MediaType: mediaType,
Digest: digest.FromBytes(blob), // Calculate digest
Size: int64(len(blob)), // Include blob size
}
return desc, target.Push(ctx, desc, bytes.NewReader(blob)) // Push the blob to the registry target
}
func generateManifestContent(config ocispec.Descriptor, layers ...ocispec.Descriptor) ([]byte, error) {
content := ocispec.Manifest{
Config: config, // Set config blob
Layers: layers, // Set layer blobs
Versioned: specs.Versioned{SchemaVersion: 2},
}
return json.Marshal(content) // Get json content
}
func TestMain(m *testing.M) {
const exampleTag = "latest"
const exampleUploadUUid = "0bc84d80-837c-41d9-824e-1907463c53b3"
// Setup example local target
exampleMemoryStore = memory.New()
layerBlob := []byte("Hello layer")
ctx := context.Background()
layerDesc, err := pushBlob(ctx, ocispec.MediaTypeImageLayer, layerBlob, exampleMemoryStore) // push layer blob
if err != nil {
panic(err)
}
configBlob := []byte("Hello config")
configDesc, err := pushBlob(ctx, ocispec.MediaTypeImageConfig, configBlob, exampleMemoryStore) // push config blob
if err != nil {
panic(err)
}
manifestBlob, err := generateManifestContent(configDesc, layerDesc) // generate a image manifest
if err != nil {
panic(err)
}
manifestDesc, err := pushBlob(ctx, ocispec.MediaTypeImageManifest, manifestBlob, exampleMemoryStore) // push manifest blob
if err != nil {
panic(err)
}
err = exampleMemoryStore.Tag(ctx, manifestDesc, exampleTag)
if err != nil {
panic(err)
}
// Setup example remote target
httpsServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p := r.URL.Path
m := r.Method
switch {
case strings.Contains(p, "/blobs/uploads/") && m == "POST":
w.Header().Set("Content-Type", ocispec.MediaTypeImageManifest)
w.Header().Set("Location", p+exampleUploadUUid)
w.WriteHeader(http.StatusAccepted)
case strings.Contains(p, "/blobs/uploads/"+exampleUploadUUid) && m == "GET":
w.WriteHeader(http.StatusCreated)
case strings.Contains(p, "/manifests/"+string(exampleSignatureManifestDescriptor.Digest)):
w.Header().Set("Content-Type", spec.MediaTypeArtifactManifest)
w.Header().Set("Docker-Content-Digest", string(exampleSignatureManifestDescriptor.Digest))
w.Header().Set("Content-Length", strconv.Itoa(len(exampleSignatureManifest)))
w.Write(exampleSignatureManifest)
case strings.Contains(p, "/manifests/latest") && m == "PUT":
w.WriteHeader(http.StatusCreated)
case strings.Contains(p, "/manifests/"+string(exampleManifestDescriptor.Digest)),
strings.Contains(p, "/manifests/latest") && m == "HEAD":
w.Header().Set("Content-Type", spec.MediaTypeArtifactManifest)
w.Header().Set("Docker-Content-Digest", string(exampleManifestDescriptor.Digest))
w.Header().Set("Content-Length", strconv.Itoa(len(exampleManifest)))
if m == "GET" {
w.Write(exampleManifest)
}
case strings.Contains(p, "/v2/source/referrers/"):
var referrers []ocispec.Descriptor
if p == "/v2/source/referrers/"+exampleManifestDescriptor.Digest.String() {
referrers = []ocispec.Descriptor{exampleSignatureManifestDescriptor}
}
result := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: referrers,
}
w.Header().Set("Content-Type", ocispec.MediaTypeImageIndex)
if err := json.NewEncoder(w).Encode(result); err != nil {
panic(err)
}
case strings.Contains(p, "/manifests/") && (m == "HEAD" || m == "GET"):
w.Header().Set("Content-Type", ocispec.MediaTypeImageManifest)
w.Header().Set("Docker-Content-Digest", string(manifestDesc.Digest))
w.Header().Set("Content-Length", strconv.Itoa(len([]byte(manifestBlob))))
w.Write([]byte(manifestBlob))
case strings.Contains(p, "/blobs/") && (m == "GET" || m == "HEAD"):
arr := strings.Split(p, "/")
digest := arr[len(arr)-1]
var desc ocispec.Descriptor
var content []byte
switch digest {
case layerDesc.Digest.String():
desc = layerDesc
content = layerBlob
case configDesc.Digest.String():
desc = configDesc
content = configBlob
case manifestDesc.Digest.String():
desc = manifestDesc
content = manifestBlob
}
w.Header().Set("Content-Type", desc.MediaType)
w.Header().Set("Docker-Content-Digest", digest)
w.Header().Set("Content-Length", strconv.Itoa(len([]byte(content))))
w.Write([]byte(content))
case strings.Contains(p, "/manifests/") && m == "PUT":
w.WriteHeader(http.StatusCreated)
}
}))
defer httpsServer.Close()
u, err := url.Parse(httpsServer.URL)
if err != nil {
panic(err)
}
remoteHost = u.Host
http.DefaultTransport = httpsServer.Client().Transport
os.Exit(m.Run())
}
func ExampleCopy_remoteToRemote() {
reg, err := remote.NewRegistry(remoteHost)
if err != nil {
panic(err) // Handle error
}
ctx := context.Background()
src, err := reg.Repository(ctx, "source")
if err != nil {
panic(err) // Handle error
}
dst, err := reg.Repository(ctx, "target")
if err != nil {
panic(err) // Handle error
}
tagName := "latest"
desc, err := oras.Copy(ctx, src, tagName, dst, tagName, oras.DefaultCopyOptions)
if err != nil {
panic(err) // Handle error
}
fmt.Println(desc.Digest)
// Output:
// sha256:7cbb44b44e8ede5a89cf193db3f5f2fd019d89697e6b87e8ed2589e60649b0d1
}
func ExampleCopy_remoteToRemoteWithMount() {
reg, err := remote.NewRegistry(remoteHost)
if err != nil {
panic(err) // Handle error
}
ctx := context.Background()
src, err := reg.Repository(ctx, "source")
if err != nil {
panic(err) // Handle error
}
dst, err := reg.Repository(ctx, "target")
if err != nil {
panic(err) // Handle error
}
tagName := "latest"
opts := oras.CopyOptions{}
// optionally be notified that a mount occurred.
opts.OnMounted = func(ctx context.Context, desc ocispec.Descriptor) error {
// log.Println("Mounted", desc.Digest)
return nil
}
// Enable cross-repository blob mounting
opts.MountFrom = func(ctx context.Context, desc ocispec.Descriptor) ([]string, error) {
// the slice of source repositores may also come from a database of known locations of blobs
return []string{"source/repository/name"}, nil
}
desc, err := oras.Copy(ctx, src, tagName, dst, tagName, opts)
if err != nil {
panic(err) // Handle error
}
fmt.Println("Final", desc.Digest)
// Output:
// Final sha256:7cbb44b44e8ede5a89cf193db3f5f2fd019d89697e6b87e8ed2589e60649b0d1
}
func ExampleCopy_remoteToLocal() {
reg, err := remote.NewRegistry(remoteHost)
if err != nil {
panic(err) // Handle error
}
ctx := context.Background()
src, err := reg.Repository(ctx, "source")
if err != nil {
panic(err) // Handle error
}
dst := memory.New()
tagName := "latest"
desc, err := oras.Copy(ctx, src, tagName, dst, tagName, oras.DefaultCopyOptions)
if err != nil {
panic(err) // Handle error
}
fmt.Println(desc.Digest)
// Output:
// sha256:7cbb44b44e8ede5a89cf193db3f5f2fd019d89697e6b87e8ed2589e60649b0d1
}
func ExampleCopy_localToLocal() {
src := exampleMemoryStore
dst := memory.New()
tagName := "latest"
ctx := context.Background()
desc, err := oras.Copy(ctx, src, tagName, dst, tagName, oras.DefaultCopyOptions)
if err != nil {
panic(err) // Handle error
}
fmt.Println(desc.Digest)
// Output:
// sha256:7cbb44b44e8ede5a89cf193db3f5f2fd019d89697e6b87e8ed2589e60649b0d1
}
func ExampleCopy_localToOciFile() {
src := exampleMemoryStore
tempDir, err := os.MkdirTemp("", "oras_oci_example_*")
if err != nil {
panic(err) // Handle error
}
defer os.RemoveAll(tempDir)
dst, err := oci.New(tempDir)
if err != nil {
panic(err) // Handle error
}
tagName := "latest"
ctx := context.Background()
desc, err := oras.Copy(ctx, src, tagName, dst, tagName, oras.DefaultCopyOptions)
if err != nil {
panic(err) // Handle error
}
fmt.Println(desc.Digest)
// Output:
// sha256:7cbb44b44e8ede5a89cf193db3f5f2fd019d89697e6b87e8ed2589e60649b0d1
}
func ExampleCopy_localToRemote() {
src := exampleMemoryStore
reg, err := remote.NewRegistry(remoteHost)
if err != nil {
panic(err) // Handle error
}
ctx := context.Background()
dst, err := reg.Repository(ctx, "target")
if err != nil {
panic(err) // Handle error
}
tagName := "latest"
desc, err := oras.Copy(ctx, src, tagName, dst, tagName, oras.DefaultCopyOptions)
if err != nil {
panic(err) // Handle error
}
fmt.Println(desc.Digest)
// Output:
// sha256:7cbb44b44e8ede5a89cf193db3f5f2fd019d89697e6b87e8ed2589e60649b0d1
}
// ExampleCopyArtifactManifestRemoteToLocal gives an example of copying
// an artifact manifest from a remote repository into memory.
func Example_copyArtifactManifestRemoteToLocal() {
src, err := remote.NewRepository(fmt.Sprintf("%s/source", remoteHost))
if err != nil {
panic(err)
}
dst := memory.New()
ctx := context.Background()
exampleDigest := "sha256:70c29a81e235dda5c2cebb8ec06eafd3cca346cbd91f15ac74cefd98681c5b3d"
descriptor, err := src.Resolve(ctx, exampleDigest)
if err != nil {
panic(err)
}
err = oras.CopyGraph(ctx, src, dst, descriptor, oras.DefaultCopyGraphOptions)
if err != nil {
panic(err)
}
// verify that the artifact manifest described by the descriptor exists in dst
contentExists, err := dst.Exists(ctx, descriptor)
if err != nil {
panic(err)
}
fmt.Println(contentExists)
// Output:
// true
}
// ExampleExtendedCopyArtifactAndReferrersRemoteToLocal gives an example of
// copying an artifact along with its referrers from a remote repository into
// memory.
func Example_extendedCopyArtifactAndReferrersRemoteToLocal() {
src, err := remote.NewRepository(fmt.Sprintf("%s/source", remoteHost))
if err != nil {
panic(err)
}
dst := memory.New()
ctx := context.Background()
tagName := "latest"
// ExtendedCopy will copy the artifact tagged by "latest" along with all of its
// referrers from src to dst.
desc, err := oras.ExtendedCopy(ctx, src, tagName, dst, tagName, oras.DefaultExtendedCopyOptions)
if err != nil {
panic(err)
}
fmt.Println(desc.Digest)
// Output:
// sha256:f396bc4d300934a39ca28ab0d5ac8a3573336d7d63c654d783a68cd1e2057662
}
oras-go-2.5.0/example_pack_test.go 0000664 0000000 0000000 00000007755 14576745303 0017140 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 oras_test
import (
"context"
"fmt"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/content/memory"
)
// ExampleImageV11 demonstrates packing an OCI Image Manifest as defined in
// image-spec v1.1.0.
func ExamplePackManifest_imageV11() {
// 0. Create a storage
store := memory.New()
// 1. Set optional parameters
opts := oras.PackManifestOptions{
ManifestAnnotations: map[string]string{
// this timestamp will be automatically generated if not specified
// use a fixed value here in order to test the output
ocispec.AnnotationCreated: "2000-01-01T00:00:00Z",
},
}
ctx := context.Background()
// 2. Pack a manifest
artifactType := "application/vnd.example+type"
manifestDesc, err := oras.PackManifest(ctx, store, oras.PackManifestVersion1_1, artifactType, opts)
if err != nil {
panic(err)
}
fmt.Println("Manifest descriptor:", manifestDesc)
// 3. Verify the packed manifest
manifestData, err := content.FetchAll(ctx, store, manifestDesc)
if err != nil {
panic(err)
}
fmt.Println("Manifest content:", string(manifestData))
// Output:
// Manifest descriptor: {application/vnd.oci.image.manifest.v1+json sha256:c259a195a48d8029d75449579c81269ca6225cd5b57d36073a7de6458afdfdbd 528 [] map[org.opencontainers.image.created:2000-01-01T00:00:00Z] [] application/vnd.example+type}
// Manifest content: {"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","artifactType":"application/vnd.example+type","config":{"mediaType":"application/vnd.oci.empty.v1+json","digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2,"data":"e30="},"layers":[{"mediaType":"application/vnd.oci.empty.v1+json","digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2,"data":"e30="}],"annotations":{"org.opencontainers.image.created":"2000-01-01T00:00:00Z"}}
}
// ExampleImageV10 demonstrates packing an OCI Image Manifest as defined in
// image-spec v1.0.2.
func ExamplePackManifest_imageV10() {
// 0. Create a storage
store := memory.New()
// 1. Set optional parameters
opts := oras.PackManifestOptions{
ManifestAnnotations: map[string]string{
// this timestamp will be automatically generated if not specified
// use a fixed value here in order to test the output
ocispec.AnnotationCreated: "2000-01-01T00:00:00Z",
},
}
ctx := context.Background()
// 2. Pack a manifest
artifactType := "application/vnd.example+type"
manifestDesc, err := oras.PackManifest(ctx, store, oras.PackManifestVersion1_0, artifactType, opts)
if err != nil {
panic(err)
}
fmt.Println("Manifest descriptor:", manifestDesc)
// 3. Verify the packed manifest
manifestData, err := content.FetchAll(ctx, store, manifestDesc)
if err != nil {
panic(err)
}
fmt.Println("Manifest content:", string(manifestData))
// Output:
// Manifest descriptor: {application/vnd.oci.image.manifest.v1+json sha256:da221a11559704e4971c3dcf6564303707a333c8de8cb5475fc48b0072b36c19 308 [] map[org.opencontainers.image.created:2000-01-01T00:00:00Z] [] application/vnd.example+type}
// Manifest content: {"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.example+type","digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2},"layers":[],"annotations":{"org.opencontainers.image.created":"2000-01-01T00:00:00Z"}}
}
oras-go-2.5.0/example_test.go 0000664 0000000 0000000 00000013437 14576745303 0016134 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 oras_test
import (
"context"
"fmt"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
oras "oras.land/oras-go/v2"
"oras.land/oras-go/v2/content/file"
"oras.land/oras-go/v2/content/oci"
"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/retry"
)
// ExamplePullFilesFromRemoteRepository gives an example of pulling files from
// a remote repository to the local file system.
func Example_pullFilesFromRemoteRepository() {
// 0. Create a file store
fs, err := file.New("/tmp/")
if err != nil {
panic(err)
}
defer fs.Close()
// 1. Connect to a remote repository
ctx := context.Background()
reg := "myregistry.example.com"
repo, err := remote.NewRepository(reg + "/myrepo")
if err != nil {
panic(err)
}
// Note: The below code can be omitted if authentication is not required
repo.Client = &auth.Client{
Client: retry.DefaultClient,
Cache: auth.NewCache(),
Credential: auth.StaticCredential(reg, auth.Credential{
Username: "username",
Password: "password",
}),
}
// 2. Copy from the remote repository to the file store
tag := "latest"
manifestDescriptor, err := oras.Copy(ctx, repo, tag, fs, tag, oras.DefaultCopyOptions)
if err != nil {
panic(err)
}
fmt.Println("manifest descriptor:", manifestDescriptor)
}
// ExamplePullImageFromRemoteRepository gives an example of pulling an image
// from a remote repository to an OCI Image layout folder.
func Example_pullImageFromRemoteRepository() {
// 0. Create an OCI layout store
store, err := oci.New("/tmp/oci-layout-root")
if err != nil {
panic(err)
}
// 1. Connect to a remote repository
ctx := context.Background()
reg := "myregistry.example.com"
repo, err := remote.NewRepository(reg + "/myrepo")
if err != nil {
panic(err)
}
// Note: The below code can be omitted if authentication is not required
repo.Client = &auth.Client{
Client: retry.DefaultClient,
Cache: auth.NewCache(),
Credential: auth.StaticCredential(reg, auth.Credential{
Username: "username",
Password: "password",
}),
}
// 2. Copy from the remote repository to the OCI layout store
tag := "latest"
manifestDescriptor, err := oras.Copy(ctx, repo, tag, store, tag, oras.DefaultCopyOptions)
if err != nil {
panic(err)
}
fmt.Println("manifest descriptor:", manifestDescriptor)
}
// ExamplePullImageUsingDockerCredentials gives an example of pulling an image
// from a remote repository to an OCI Image layout folder using Docker
// credentials.
func Example_pullImageUsingDockerCredentials() {
// 0. Create an OCI layout store
store, err := oci.New("/tmp/oci-layout-root")
if err != nil {
panic(err)
}
// 1. Connect to a remote repository
ctx := context.Background()
reg := "docker.io"
repo, err := remote.NewRepository(reg + "/user/my-repo")
if err != nil {
panic(err)
}
// prepare authentication using Docker credentials
storeOpts := credentials.StoreOptions{}
credStore, err := credentials.NewStoreFromDocker(storeOpts)
if err != nil {
panic(err)
}
repo.Client = &auth.Client{
Client: retry.DefaultClient,
Cache: auth.NewCache(),
Credential: credentials.Credential(credStore), // Use the credentials store
}
// 2. Copy from the remote repository to the OCI layout store
tag := "latest"
manifestDescriptor, err := oras.Copy(ctx, repo, tag, store, tag, oras.DefaultCopyOptions)
if err != nil {
panic(err)
}
fmt.Println("manifest pulled:", manifestDescriptor.Digest, manifestDescriptor.MediaType)
}
// ExamplePushFilesToRemoteRepository gives an example of pushing local files
// to a remote repository.
func Example_pushFilesToRemoteRepository() {
// 0. Create a file store
fs, err := file.New("/tmp/")
if err != nil {
panic(err)
}
defer fs.Close()
ctx := context.Background()
// 1. Add files to the file store
mediaType := "application/vnd.test.file"
fileNames := []string{"/tmp/myfile"}
fileDescriptors := make([]v1.Descriptor, 0, len(fileNames))
for _, name := range fileNames {
fileDescriptor, err := fs.Add(ctx, name, mediaType, "")
if err != nil {
panic(err)
}
fileDescriptors = append(fileDescriptors, fileDescriptor)
fmt.Printf("file descriptor for %s: %v\n", name, fileDescriptor)
}
// 2. Pack the files and tag the packed manifest
artifactType := "application/vnd.test.artifact"
opts := oras.PackManifestOptions{
Layers: fileDescriptors,
}
manifestDescriptor, err := oras.PackManifest(ctx, fs, oras.PackManifestVersion1_1, artifactType, opts)
if err != nil {
panic(err)
}
fmt.Println("manifest descriptor:", manifestDescriptor)
tag := "latest"
if err = fs.Tag(ctx, manifestDescriptor, tag); err != nil {
panic(err)
}
// 3. Connect to a remote repository
reg := "myregistry.example.com"
repo, err := remote.NewRepository(reg + "/myrepo")
if err != nil {
panic(err)
}
// Note: The below code can be omitted if authentication is not required
repo.Client = &auth.Client{
Client: retry.DefaultClient,
Cache: auth.NewCache(),
Credential: auth.StaticCredential(reg, auth.Credential{
Username: "username",
Password: "password",
}),
}
// 4. Copy from the file store to the remote repository
_, err = oras.Copy(ctx, fs, tag, repo, tag, oras.DefaultCopyOptions)
if err != nil {
panic(err)
}
}
oras-go-2.5.0/extendedcopy.go 0000664 0000000 0000000 00000031265 14576745303 0016134 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 oras
import (
"context"
"encoding/json"
"errors"
"regexp"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"golang.org/x/sync/semaphore"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/internal/cas"
"oras.land/oras-go/v2/internal/container/set"
"oras.land/oras-go/v2/internal/copyutil"
"oras.land/oras-go/v2/internal/descriptor"
"oras.land/oras-go/v2/internal/docker"
"oras.land/oras-go/v2/internal/spec"
"oras.land/oras-go/v2/internal/status"
"oras.land/oras-go/v2/internal/syncutil"
"oras.land/oras-go/v2/registry"
)
// DefaultExtendedCopyOptions provides the default ExtendedCopyOptions.
var DefaultExtendedCopyOptions ExtendedCopyOptions = ExtendedCopyOptions{
ExtendedCopyGraphOptions: DefaultExtendedCopyGraphOptions,
}
// ExtendedCopyOptions contains parameters for [oras.ExtendedCopy].
type ExtendedCopyOptions struct {
ExtendedCopyGraphOptions
}
// DefaultExtendedCopyGraphOptions provides the default ExtendedCopyGraphOptions.
var DefaultExtendedCopyGraphOptions ExtendedCopyGraphOptions = ExtendedCopyGraphOptions{
CopyGraphOptions: DefaultCopyGraphOptions,
}
// ExtendedCopyGraphOptions contains parameters for [oras.ExtendedCopyGraph].
type ExtendedCopyGraphOptions struct {
CopyGraphOptions
// Depth limits the maximum depth of the directed acyclic graph (DAG) that
// will be extended-copied.
// If Depth is no specified, or the specified value is less than or
// equal to 0, the depth limit will be considered as infinity.
Depth int
// FindPredecessors finds the predecessors of the current node.
// If FindPredecessors is nil, src.Predecessors will be adapted and used.
FindPredecessors func(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) ([]ocispec.Descriptor, error)
}
// ExtendedCopy copies the directed acyclic graph (DAG) that are reachable from
// the given tagged node from the source GraphTarget to the destination Target.
// The destination reference will be the same as the source reference if the
// destination reference is left blank.
//
// Returns the descriptor of the tagged node on successful copy.
func ExtendedCopy(ctx context.Context, src ReadOnlyGraphTarget, srcRef string, dst Target, dstRef string, opts ExtendedCopyOptions) (ocispec.Descriptor, error) {
if src == nil {
return ocispec.Descriptor{}, errors.New("nil source graph target")
}
if dst == nil {
return ocispec.Descriptor{}, errors.New("nil destination target")
}
if dstRef == "" {
dstRef = srcRef
}
node, err := src.Resolve(ctx, srcRef)
if err != nil {
return ocispec.Descriptor{}, err
}
if err := ExtendedCopyGraph(ctx, src, dst, node, opts.ExtendedCopyGraphOptions); err != nil {
return ocispec.Descriptor{}, err
}
if err := dst.Tag(ctx, node, dstRef); err != nil {
return ocispec.Descriptor{}, err
}
return node, nil
}
// ExtendedCopyGraph copies the directed acyclic graph (DAG) that are reachable
// from the given node from the source GraphStorage to the destination Storage.
func ExtendedCopyGraph(ctx context.Context, src content.ReadOnlyGraphStorage, dst content.Storage, node ocispec.Descriptor, opts ExtendedCopyGraphOptions) error {
roots, err := findRoots(ctx, src, node, opts)
if err != nil {
return err
}
// if Concurrency is not set or invalid, use the default concurrency
if opts.Concurrency <= 0 {
opts.Concurrency = defaultConcurrency
}
limiter := semaphore.NewWeighted(int64(opts.Concurrency))
// use caching proxy on non-leaf nodes
if opts.MaxMetadataBytes <= 0 {
opts.MaxMetadataBytes = defaultCopyMaxMetadataBytes
}
proxy := cas.NewProxyWithLimit(src, cas.NewMemory(), opts.MaxMetadataBytes)
// track content status
tracker := status.NewTracker()
// copy the sub-DAGs rooted by the root nodes
return syncutil.Go(ctx, limiter, func(ctx context.Context, region *syncutil.LimitedRegion, root ocispec.Descriptor) error {
// As a root can be a predecessor of other roots, release the limit here
// for dispatching, to avoid dead locks where predecessor roots are
// handled first and are waiting for its successors to complete.
region.End()
if err := copyGraph(ctx, src, dst, root, proxy, limiter, tracker, opts.CopyGraphOptions); err != nil {
return err
}
return region.Start()
}, roots...)
}
// findRoots finds the root nodes reachable from the given node through a
// depth-first search.
func findRoots(ctx context.Context, storage content.ReadOnlyGraphStorage, node ocispec.Descriptor, opts ExtendedCopyGraphOptions) ([]ocispec.Descriptor, error) {
visited := set.New[descriptor.Descriptor]()
rootMap := make(map[descriptor.Descriptor]ocispec.Descriptor)
addRoot := func(key descriptor.Descriptor, val ocispec.Descriptor) {
if _, exists := rootMap[key]; !exists {
rootMap[key] = val
}
}
// if FindPredecessors is not provided, use the default one
if opts.FindPredecessors == nil {
opts.FindPredecessors = func(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
return src.Predecessors(ctx, desc)
}
}
var stack copyutil.Stack
// push the initial node to the stack, set the depth to 0
stack.Push(copyutil.NodeInfo{Node: node, Depth: 0})
for {
current, ok := stack.Pop()
if !ok {
// empty stack
break
}
currentNode := current.Node
currentKey := descriptor.FromOCI(currentNode)
if visited.Contains(currentKey) {
// skip the current node if it has been visited
continue
}
visited.Add(currentKey)
// stop finding predecessors if the target depth is reached
if opts.Depth > 0 && current.Depth == opts.Depth {
addRoot(currentKey, currentNode)
continue
}
predecessors, err := opts.FindPredecessors(ctx, storage, currentNode)
if err != nil {
return nil, err
}
// The current node has no predecessor node,
// which means it is a root node of a sub-DAG.
if len(predecessors) == 0 {
addRoot(currentKey, currentNode)
continue
}
// The current node has predecessor nodes, which means it is NOT a root node.
// Push the predecessor nodes to the stack and keep finding from there.
for _, predecessor := range predecessors {
predecessorKey := descriptor.FromOCI(predecessor)
if !visited.Contains(predecessorKey) {
// push the predecessor node with increased depth
stack.Push(copyutil.NodeInfo{Node: predecessor, Depth: current.Depth + 1})
}
}
}
roots := make([]ocispec.Descriptor, 0, len(rootMap))
for _, root := range rootMap {
roots = append(roots, root)
}
return roots, nil
}
// FilterAnnotation configures opts.FindPredecessors to filter the predecessors
// whose annotation matches a given regex pattern.
//
// A predecessor is kept if key is in its annotations and the annotation value
// matches regex.
// If regex is nil, predecessors whose annotations contain key will be kept,
// no matter of the annotation value.
//
// For performance consideration, when using both FilterArtifactType and
// FilterAnnotation, it's recommended to call FilterArtifactType first.
func (opts *ExtendedCopyGraphOptions) FilterAnnotation(key string, regex *regexp.Regexp) {
keep := func(desc ocispec.Descriptor) bool {
value, ok := desc.Annotations[key]
return ok && (regex == nil || regex.MatchString(value))
}
fp := opts.FindPredecessors
opts.FindPredecessors = func(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
var predecessors []ocispec.Descriptor
var err error
if fp == nil {
if rf, ok := src.(registry.ReferrerLister); ok {
// if src is a ReferrerLister, use Referrers() for possible memory saving
if err := rf.Referrers(ctx, desc, "", func(referrers []ocispec.Descriptor) error {
// for each page of the results, filter the referrers
for _, r := range referrers {
if keep(r) {
predecessors = append(predecessors, r)
}
}
return nil
}); err != nil {
return nil, err
}
return predecessors, nil
}
predecessors, err = src.Predecessors(ctx, desc)
} else {
predecessors, err = fp(ctx, src, desc)
}
if err != nil {
return nil, err
}
// Predecessor descriptors that are not from Referrers API are not
// guaranteed to include the annotations of the corresponding manifests.
var kept []ocispec.Descriptor
for _, p := range predecessors {
if p.Annotations == nil {
// If the annotations are not present in the descriptors,
// fetch it from the manifest content.
switch p.MediaType {
case docker.MediaTypeManifest, ocispec.MediaTypeImageManifest,
docker.MediaTypeManifestList, ocispec.MediaTypeImageIndex,
spec.MediaTypeArtifactManifest:
annotations, err := fetchAnnotations(ctx, src, p)
if err != nil {
return nil, err
}
p.Annotations = annotations
}
}
if keep(p) {
kept = append(kept, p)
}
}
return kept, nil
}
}
// fetchAnnotations fetches the annotations of the manifest described by desc.
func fetchAnnotations(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) (map[string]string, error) {
rc, err := src.Fetch(ctx, desc)
if err != nil {
return nil, err
}
defer rc.Close()
var manifest struct {
Annotations map[string]string `json:"annotations"`
}
if err := json.NewDecoder(rc).Decode(&manifest); err != nil {
return nil, err
}
if manifest.Annotations == nil {
// to differentiate with nil
return make(map[string]string), nil
}
return manifest.Annotations, nil
}
// FilterArtifactType configures opts.FindPredecessors to filter the
// predecessors whose artifact type matches a given regex pattern.
//
// A predecessor is kept if its artifact type matches regex.
// If regex is nil, all predecessors will be kept.
//
// For performance consideration, when using both FilterArtifactType and
// FilterAnnotation, it's recommended to call FilterArtifactType first.
func (opts *ExtendedCopyGraphOptions) FilterArtifactType(regex *regexp.Regexp) {
if regex == nil {
return
}
keep := func(desc ocispec.Descriptor) bool {
return regex.MatchString(desc.ArtifactType)
}
fp := opts.FindPredecessors
opts.FindPredecessors = func(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
var predecessors []ocispec.Descriptor
var err error
if fp == nil {
if rf, ok := src.(registry.ReferrerLister); ok {
// if src is a ReferrerLister, use Referrers() for possible memory saving
if err := rf.Referrers(ctx, desc, "", func(referrers []ocispec.Descriptor) error {
// for each page of the results, filter the referrers
for _, r := range referrers {
if keep(r) {
predecessors = append(predecessors, r)
}
}
return nil
}); err != nil {
return nil, err
}
return predecessors, nil
}
predecessors, err = src.Predecessors(ctx, desc)
} else {
predecessors, err = fp(ctx, src, desc)
}
if err != nil {
return nil, err
}
// predecessor descriptors that are not from Referrers API are not
// guaranteed to include the artifact type of the corresponding
// manifests.
var kept []ocispec.Descriptor
for _, p := range predecessors {
if p.ArtifactType == "" {
// if the artifact type is not present in the descriptors,
// fetch it from the manifest content.
switch p.MediaType {
case spec.MediaTypeArtifactManifest, ocispec.MediaTypeImageManifest:
artifactType, err := fetchArtifactType(ctx, src, p)
if err != nil {
return nil, err
}
p.ArtifactType = artifactType
}
}
if keep(p) {
kept = append(kept, p)
}
}
return kept, nil
}
}
// fetchArtifactType fetches the artifact type of the manifest described by desc.
func fetchArtifactType(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) (string, error) {
rc, err := src.Fetch(ctx, desc)
if err != nil {
return "", err
}
defer rc.Close()
switch desc.MediaType {
case spec.MediaTypeArtifactManifest:
var manifest spec.Artifact
if err := json.NewDecoder(rc).Decode(&manifest); err != nil {
return "", err
}
return manifest.ArtifactType, nil
case ocispec.MediaTypeImageManifest:
var manifest ocispec.Manifest
if err := json.NewDecoder(rc).Decode(&manifest); err != nil {
return "", err
}
return manifest.Config.MediaType, nil
default:
return "", nil
}
}
oras-go-2.5.0/extendedcopy_test.go 0000664 0000000 0000000 00000202605 14576745303 0017171 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 oras_test
import (
"bytes"
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"regexp"
"strconv"
"strings"
"testing"
"github.com/opencontainers/go-digest"
specs "github.com/opencontainers/image-spec/specs-go"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/content/memory"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/internal/spec"
"oras.land/oras-go/v2/registry/remote"
)
func TestExtendedCopy_FullCopy(t *testing.T) {
src := memory.New()
dst := memory.New()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(subject *ocispec.Descriptor, config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
Subject: subject,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateArtifactManifest := func(subject ocispec.Descriptor, blobs ...ocispec.Descriptor) {
manifest := spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
Subject: &subject,
Blobs: blobs,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(spec.MediaTypeArtifactManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
generateManifest(nil, descs[0], descs[1:3]...) // Blob 3
appendBlob(ocispec.MediaTypeImageLayer, []byte("sig_1")) // Blob 4
generateArtifactManifest(descs[3], descs[4]) // Blob 5
appendBlob(ocispec.MediaTypeImageLayer, []byte("sig_2")) // Blob 6
generateArtifactManifest(descs[5], descs[6]) // Blob 7
appendBlob(ocispec.MediaTypeImageLayer, []byte("baz")) // Blob 8
generateManifest(&descs[3], descs[0], descs[8]) // Blob 9
ctx := context.Background()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
manifest := descs[3]
ref := "foobar"
err := src.Tag(ctx, manifest, ref)
if err != nil {
t.Fatal("fail to tag root node", err)
}
// test extended copy
gotDesc, err := oras.ExtendedCopy(ctx, src, ref, dst, "", oras.ExtendedCopyOptions{})
if err != nil {
t.Fatalf("Copy() error = %v, wantErr %v", err, false)
}
if !reflect.DeepEqual(gotDesc, manifest) {
t.Errorf("Copy() = %v, want %v", gotDesc, manifest)
}
// verify contents
for i, desc := range descs {
exists, err := dst.Exists(ctx, desc)
if err != nil {
t.Fatalf("dst.Exists(%d) error = %v", i, err)
}
if !exists {
t.Errorf("dst.Exists(%d) = %v, want %v", i, exists, true)
}
}
// verify tag
gotDesc, err = dst.Resolve(ctx, ref)
if err != nil {
t.Fatal("dst.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, manifest) {
t.Errorf("dst.Resolve() = %v, want %v", gotDesc, manifest)
}
}
func TestExtendedCopyGraph_FullCopy(t *testing.T) {
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
generateArtifactManifest := func(subject ocispec.Descriptor, blobs ...ocispec.Descriptor) {
manifest := spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
Subject: &subject,
Blobs: blobs,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(spec.MediaTypeArtifactManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config_1")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
generateManifest(descs[0], descs[1:3]...) // Blob 3
appendBlob(ocispec.MediaTypeImageLayer, []byte("baz")) // Blob 4
generateManifest(descs[0], descs[4]) // Blob 5 (root)
appendBlob(ocispec.MediaTypeImageConfig, []byte("config_2")) // Blob 6
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 7
generateManifest(descs[6], descs[7]) // Blob 8
appendBlob(ocispec.MediaTypeImageLayer, []byte("sig_1")) // Blob 9
generateArtifactManifest(descs[8], descs[9]) // Blob 10
generateIndex(descs[3], descs[10]) // Blob 11 (root)
appendBlob(ocispec.MediaTypeImageLayer, []byte("goodbye")) // Blob 12
appendBlob(ocispec.MediaTypeImageLayer, []byte("sig_2")) // Blob 13
generateArtifactManifest(descs[12], descs[13]) // Blob 14 (root)
ctx := context.Background()
verifyCopy := func(dst content.Fetcher, copiedIndice []int, uncopiedIndice []int) {
for _, i := range copiedIndice {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, false)
continue
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Errorf("content[%d] = %v, want %v", i, got, want)
}
}
for _, i := range uncopiedIndice {
if _, err := content.FetchAll(ctx, dst, descs[i]); !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, errdef.ErrNotFound)
}
}
}
src := memory.New()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// test extended copy by descs[0]
dst := memory.New()
if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[0], oras.ExtendedCopyGraphOptions{}); err != nil {
t.Fatalf("ExtendedCopyGraph() error = %v, wantErr %v", err, false)
}
// graph rooted by descs[11] should be copied
copiedIndice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
uncopiedIndice := []int{12, 13, 14}
verifyCopy(dst, copiedIndice, uncopiedIndice)
// test extended copy by descs[4]
dst = memory.New()
if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[4], oras.ExtendedCopyGraphOptions{}); err != nil {
t.Fatalf("ExtendedCopyGraph() error = %v, wantErr %v", err, false)
}
// graph rooted by descs[5] should be copied
copiedIndice = []int{0, 4, 5}
uncopiedIndice = []int{1, 2, 3, 6, 7, 8, 9, 10, 11, 12, 13, 14}
verifyCopy(dst, copiedIndice, uncopiedIndice)
// test extended copy by descs[14]
dst = memory.New()
if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[14], oras.ExtendedCopyGraphOptions{}); err != nil {
t.Fatalf("ExtendedCopyGraph() error = %v, wantErr %v", err, false)
}
// graph rooted by descs[14] should be copied
copiedIndice = []int{12, 13, 14}
uncopiedIndice = []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
verifyCopy(dst, copiedIndice, uncopiedIndice)
}
func TestExtendedCopyGraph_PartialCopy(t *testing.T) {
src := memory.New()
dst := memory.New()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
generateManifest(descs[0], descs[1:3]...) // Blob 3
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 4
generateManifest(descs[0], descs[4]) // Blob 5
generateIndex(descs[3], descs[5]) // Blob 6 (root)
ctx := context.Background()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// test copy a part of the graph
root := descs[3]
if err := oras.CopyGraph(ctx, src, dst, root, oras.CopyGraphOptions{}); err != nil {
t.Fatalf("CopyGraph() error = %v, wantErr %v", err, false)
}
// blobs [0-3] should be copied
for i := range blobs[:4] {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Fatalf("content[%d] error = %v, wantErr %v", i, err, false)
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Fatalf("content[%d] = %v, want %v", i, got, want)
}
}
// test extended copy by descs[0]
if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[0], oras.ExtendedCopyGraphOptions{}); err != nil {
t.Fatalf("ExtendedCopyGraph() error = %v, wantErr %v", err, false)
}
// all blobs should be copied
for i := range blobs {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, false)
continue
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Errorf("content[%d] = %v, want %v", i, got, want)
}
}
}
func TestExtendedCopyGraph_artifactIndex(t *testing.T) {
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(subject *ocispec.Descriptor, config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Subject: subject,
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(subject *ocispec.Descriptor, manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Subject: subject,
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config_1")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("layer_1")) // Blob 1
generateManifest(nil, descs[0], descs[1]) // Blob 2
appendBlob(ocispec.MediaTypeImageConfig, []byte("config_2")) // Blob 3
appendBlob(ocispec.MediaTypeImageLayer, []byte("layer_2")) // Blob 4
generateManifest(nil, descs[3], descs[4]) // Blob 5
appendBlob(ocispec.MediaTypeImageLayer, []byte("{}")) // Blob 6
appendBlob(ocispec.MediaTypeImageLayer, []byte("sbom_1")) // Blob 7
generateManifest(&descs[2], descs[6], descs[7]) // Blob 8
appendBlob(ocispec.MediaTypeImageLayer, []byte("sbom_2")) // Blob 9
generateManifest(&descs[5], descs[6], descs[9]) // Blob 10
generateIndex(nil, []ocispec.Descriptor{descs[2], descs[5]}...) // Blob 11 (root)
generateIndex(&descs[11], []ocispec.Descriptor{descs[8], descs[10]}...) // Blob 12 (root)
ctx := context.Background()
verifyCopy := func(dst content.Fetcher, copiedIndice []int, uncopiedIndice []int) {
for _, i := range copiedIndice {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, false)
continue
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Errorf("content[%d] = %v, want %v", i, got, want)
}
}
for _, i := range uncopiedIndice {
if _, err := content.FetchAll(ctx, dst, descs[i]); !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, errdef.ErrNotFound)
}
}
}
src := memory.New()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// test extended copy by descs[0]
dst := memory.New()
if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[0], oras.ExtendedCopyGraphOptions{}); err != nil {
t.Fatalf("ExtendedCopyGraph() error = %v, wantErr %v", err, false)
}
// all blobs should be copied
copiedIndice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
uncopiedIndice := []int{}
verifyCopy(dst, copiedIndice, uncopiedIndice)
// test extended copy by descs[2]
dst = memory.New()
if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[2], oras.ExtendedCopyGraphOptions{}); err != nil {
t.Fatalf("ExtendedCopyGraph() error = %v, wantErr %v", err, false)
}
// all blobs should be copied
copiedIndice = []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
uncopiedIndice = []int{}
verifyCopy(dst, copiedIndice, uncopiedIndice)
// test extended copy by descs[8]
dst = memory.New()
if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[8], oras.ExtendedCopyGraphOptions{}); err != nil {
t.Fatalf("ExtendedCopyGraph() error = %v, wantErr %v", err, false)
}
// all blobs should be copied
copiedIndice = []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
uncopiedIndice = []int{}
verifyCopy(dst, copiedIndice, uncopiedIndice)
// test extended copy by descs[11]
dst = memory.New()
if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[11], oras.ExtendedCopyGraphOptions{}); err != nil {
t.Fatalf("ExtendedCopyGraph() error = %v, wantErr %v", err, false)
}
// all blobs should be copied
copiedIndice = []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
uncopiedIndice = []int{}
verifyCopy(dst, copiedIndice, uncopiedIndice)
}
func TestExtendedCopyGraph_WithDepthOption(t *testing.T) {
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
generateArtifactManifest := func(subject ocispec.Descriptor, blobs ...ocispec.Descriptor) {
manifest := spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
Subject: &subject,
Blobs: blobs,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(spec.MediaTypeArtifactManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config_1")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
generateManifest(descs[0], descs[1:3]...) // Blob 3
appendBlob(ocispec.MediaTypeImageLayer, []byte("baz")) // Blob 4
generateManifest(descs[0], descs[4]) // Blob 5 (root)
appendBlob(ocispec.MediaTypeImageConfig, []byte("config_2")) // Blob 6
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 7
generateManifest(descs[6], descs[7]) // Blob 8
appendBlob(ocispec.MediaTypeImageLayer, []byte("sig_1")) // Blob 9
generateArtifactManifest(descs[8], descs[9]) // Blob 10
generateIndex(descs[3], descs[10]) // Blob 11 (root)
appendBlob(ocispec.MediaTypeImageLayer, []byte("goodbye")) // Blob 12
appendBlob(ocispec.MediaTypeImageLayer, []byte("sig_2")) // Blob 13
generateArtifactManifest(descs[12], descs[13]) // Blob 14 (root)
ctx := context.Background()
verifyCopy := func(dst content.Fetcher, copiedIndice []int, uncopiedIndice []int) {
for _, i := range copiedIndice {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, false)
continue
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Errorf("content[%d] = %v, want %v", i, got, want)
}
}
for _, i := range uncopiedIndice {
if _, err := content.FetchAll(ctx, dst, descs[i]); !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, errdef.ErrNotFound)
}
}
}
src := memory.New()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// test extended copy by descs[0] with default depth 0
dst := memory.New()
opts := oras.ExtendedCopyGraphOptions{}
if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[0], opts); err != nil {
t.Fatalf("ExtendedCopyGraph() error = %v, wantErr %v", err, false)
}
// graph rooted by descs[11] should be copied
copiedIndice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
uncopiedIndice := []int{12, 13, 14}
verifyCopy(dst, copiedIndice, uncopiedIndice)
// test extended copy by descs[0] with depth of 1
dst = memory.New()
opts = oras.ExtendedCopyGraphOptions{Depth: 1}
if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[0], opts); err != nil {
t.Fatalf("ExtendedCopyGraph() error = %v, wantErr %v", err, false)
}
// graph rooted by descs[3] and descs[5] should be copied
copiedIndice = []int{0, 1, 2, 3, 4, 5}
uncopiedIndice = []int{6, 7, 8, 9, 10, 11, 12, 13, 14}
verifyCopy(dst, copiedIndice, uncopiedIndice)
// test extended copy by descs[0] with depth of 2
dst = memory.New()
opts = oras.ExtendedCopyGraphOptions{Depth: 2}
if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[0], opts); err != nil {
t.Fatalf("ExtendedCopyGraph() error = %v, wantErr %v", err, false)
}
// graph rooted by descs[11] should be copied
copiedIndice = []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
uncopiedIndice = []int{12, 13, 14}
verifyCopy(dst, copiedIndice, uncopiedIndice)
// test extended copy by descs[0] with depth -1
dst = memory.New()
opts = oras.ExtendedCopyGraphOptions{Depth: -1}
if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[0], opts); err != nil {
t.Fatalf("ExtendedCopyGraph() error = %v, wantErr %v", err, false)
}
// graph rooted by descs[11] should be copied
copiedIndice = []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
uncopiedIndice = []int{12, 13, 14}
verifyCopy(dst, copiedIndice, uncopiedIndice)
}
func TestExtendedCopyGraph_WithFindPredecessorsOption(t *testing.T) {
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
generateArtifactManifest := func(subject ocispec.Descriptor, blobs ...ocispec.Descriptor) {
manifest := spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
Subject: &subject,
Blobs: blobs,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(spec.MediaTypeArtifactManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config_1")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
generateManifest(descs[0], descs[1:3]...) // Blob 3
appendBlob(ocispec.MediaTypeImageLayer, []byte("sig_1")) // Blob 4
generateArtifactManifest(descs[3], descs[4]) // Blob 5 (root)
appendBlob(ocispec.MediaTypeImageLayer, []byte("baz")) // Blob 6
generateArtifactManifest(descs[3], descs[6]) // Blob 7 (root)
appendBlob(ocispec.MediaTypeImageConfig, []byte("config_2")) // Blob 8
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 9
generateManifest(descs[8], descs[9]) // Blob 10
generateIndex(descs[3], descs[10]) // Blob 11 (root)
ctx := context.Background()
verifyCopy := func(dst content.Fetcher, copiedIndice []int, uncopiedIndice []int) {
for _, i := range copiedIndice {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, false)
continue
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Errorf("content[%d] = %v, want %v", i, got, want)
}
}
for _, i := range uncopiedIndice {
if _, err := content.FetchAll(ctx, dst, descs[i]); !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, errdef.ErrNotFound)
}
}
}
src := memory.New()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// test extended copy by descs[3] with media type filter
dst := memory.New()
opts := oras.ExtendedCopyGraphOptions{
FindPredecessors: func(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
predecessors, err := src.Predecessors(ctx, desc)
if err != nil {
return nil, err
}
var filtered []ocispec.Descriptor
for _, p := range predecessors {
// filter media type
switch p.MediaType {
case spec.MediaTypeArtifactManifest:
filtered = append(filtered, p)
}
}
return filtered, nil
},
}
if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[3], opts); err != nil {
t.Fatalf("ExtendedCopyGraph() error = %v, wantErr %v", err, false)
}
// graph rooted by descs[5] and decs[7] should be copied
copiedIndice := []int{0, 1, 2, 3, 4, 5, 6, 7}
uncopiedIndice := []int{8, 9, 10, 11}
verifyCopy(dst, copiedIndice, uncopiedIndice)
}
func TestExtendedCopy_NotFound(t *testing.T) {
src := memory.New()
dst := memory.New()
ref := "foobar"
ctx := context.Background()
_, err := oras.ExtendedCopy(ctx, src, ref, dst, "", oras.ExtendedCopyOptions{})
if !errors.Is(err, errdef.ErrNotFound) {
t.Fatalf("ExtendedCopy() error = %v, wantErr %v", err, errdef.ErrNotFound)
}
}
func TestExtendedCopyGraph_FilterAnnotationWithRegex(t *testing.T) {
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateArtifactManifest := func(subject ocispec.Descriptor, key string, value string) {
manifest := spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
Subject: &subject,
Annotations: map[string]string{key: value},
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(spec.MediaTypeArtifactManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // descs[0]
generateArtifactManifest(descs[0], "bar", "bluebrown") // descs[1]
generateArtifactManifest(descs[0], "bar", "blackred") // descs[2]
generateArtifactManifest(descs[0], "bar", "blackviolet") // descs[3]
generateArtifactManifest(descs[0], "bar", "greengrey") // descs[4]
generateArtifactManifest(descs[0], "bar", "brownblack") // descs[5]
ctx := context.Background()
verifyCopy := func(dst content.Fetcher, copiedIndice []int, uncopiedIndice []int) {
for _, i := range copiedIndice {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, false)
continue
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Errorf("content[%d] = %v, want %v", i, got, want)
}
}
for _, i := range uncopiedIndice {
if _, err := content.FetchAll(ctx, dst, descs[i]); !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, errdef.ErrNotFound)
}
}
}
src := memory.New()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// test extended copy by descs[0] with annotation filter
dst := memory.New()
opts := oras.ExtendedCopyGraphOptions{}
exp := "black."
regex := regexp.MustCompile(exp)
opts.FilterAnnotation("bar", regex)
if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[0], opts); err != nil {
t.Fatalf("ExtendedCopyGraph() error = %v, wantErr %v", err, false)
}
copiedIndice := []int{0, 2, 3}
uncopiedIndice := []int{1, 4, 5}
verifyCopy(dst, copiedIndice, uncopiedIndice)
// test FilterAnnotation with key unavailable in predecessors' annotation
// should return nothing
dst = memory.New()
opts = oras.ExtendedCopyGraphOptions{}
exp = "black."
regex = regexp.MustCompile(exp)
opts.FilterAnnotation("bar1", regex)
if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[0], opts); err != nil {
t.Fatalf("ExtendedCopyGraph() error = %v, wantErr %v", err, false)
}
copiedIndice = []int{0}
uncopiedIndice = []int{1, 2, 3, 4, 5}
verifyCopy(dst, copiedIndice, uncopiedIndice)
//test FilterAnnotation with key available in predecessors' annotation, regex equal to nil
//should return all predecessors with the provided key
dst = memory.New()
opts = oras.ExtendedCopyGraphOptions{}
opts.FilterAnnotation("bar", nil)
if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[0], opts); err != nil {
t.Fatalf("ExtendedCopyGraph() error = %v, wantErr %v", err, false)
}
copiedIndice = []int{0, 1, 2, 3, 4, 5}
uncopiedIndice = []int{}
verifyCopy(dst, copiedIndice, uncopiedIndice)
}
func TestExtendedCopyGraph_FilterAnnotationWithMultipleRegex(t *testing.T) {
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateArtifactManifest := func(subject ocispec.Descriptor, key string, value string) {
manifest := spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
Subject: &subject,
Annotations: map[string]string{key: value},
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(spec.MediaTypeArtifactManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // descs[0]
generateArtifactManifest(descs[0], "bar", "bluebrown") // descs[1]
generateArtifactManifest(descs[0], "bar", "blackred") // descs[2]
generateArtifactManifest(descs[0], "bar", "blackviolet") // descs[3]
generateArtifactManifest(descs[0], "bar", "greengrey") // descs[4]
generateArtifactManifest(descs[0], "bar", "brownblack") // descs[5]
generateArtifactManifest(descs[0], "bar", "blackblack") // descs[6]
ctx := context.Background()
verifyCopy := func(dst content.Fetcher, copiedIndice []int, uncopiedIndice []int) {
for _, i := range copiedIndice {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, false)
continue
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Errorf("content[%d] = %v, want %v", i, got, want)
}
}
for _, i := range uncopiedIndice {
if _, err := content.FetchAll(ctx, dst, descs[i]); !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, errdef.ErrNotFound)
}
}
}
src := memory.New()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// test extended copy by descs[0] with two annotation filters
dst := memory.New()
opts := oras.ExtendedCopyGraphOptions{}
exp1 := "black."
exp2 := ".pink|red"
regex1 := regexp.MustCompile(exp1)
regex2 := regexp.MustCompile(exp2)
opts.FilterAnnotation("bar", regex1)
opts.FilterAnnotation("bar", regex2)
if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[0], opts); err != nil {
t.Fatalf("ExtendedCopyGraph() error = %v, wantErr %v", err, false)
}
copiedIndice := []int{0, 2}
uncopiedIndice := []int{1, 3, 4, 5, 6}
verifyCopy(dst, copiedIndice, uncopiedIndice)
// test extended copy by descs[0] with three annotation filters, nil included
dst = memory.New()
opts = oras.ExtendedCopyGraphOptions{}
exp1 = "black."
exp2 = ".pink|red"
regex1 = regexp.MustCompile(exp1)
regex2 = regexp.MustCompile(exp2)
opts.FilterAnnotation("bar", regex1)
opts.FilterAnnotation("bar", nil)
opts.FilterAnnotation("bar", regex2)
if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[0], opts); err != nil {
t.Fatalf("ExtendedCopyGraph() error = %v, wantErr %v", err, false)
}
copiedIndice = []int{0, 2}
uncopiedIndice = []int{1, 3, 4, 5, 6}
verifyCopy(dst, copiedIndice, uncopiedIndice)
// test extended copy by descs[0] with two annotation filters, the second filter has an unavailable key
dst = memory.New()
opts = oras.ExtendedCopyGraphOptions{}
exp1 = "black."
exp2 = ".pink|red"
regex1 = regexp.MustCompile(exp1)
regex2 = regexp.MustCompile(exp2)
opts.FilterAnnotation("bar", regex1)
opts.FilterAnnotation("test", regex2)
if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[0], opts); err != nil {
t.Fatalf("ExtendedCopyGraph() error = %v, wantErr %v", err, false)
}
copiedIndice = []int{0}
uncopiedIndice = []int{1, 2, 3, 4, 5, 6}
verifyCopy(dst, copiedIndice, uncopiedIndice)
}
func TestExtendedCopyGraph_FilterAnnotationWithRegex_AnnotationInDescriptor(t *testing.T) {
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType, key, value string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
Annotations: map[string]string{key: value},
})
}
generateArtifactManifest := func(subject ocispec.Descriptor, key string, value string) {
manifest := spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
Subject: &subject,
Annotations: map[string]string{key: value},
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(spec.MediaTypeArtifactManifest, key, value, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageLayer, "", "", []byte("foo")) // descs[0]
generateArtifactManifest(descs[0], "bar", "bluebrown") // descs[1]
generateArtifactManifest(descs[0], "bar", "blackred") // descs[2]
generateArtifactManifest(descs[0], "bar", "blackviolet") // descs[3]
generateArtifactManifest(descs[0], "bar", "greengrey") // descs[4]
generateArtifactManifest(descs[0], "bar", "brownblack") // descs[5]
ctx := context.Background()
verifyCopy := func(dst content.Fetcher, copiedIndice []int, uncopiedIndice []int) {
for _, i := range copiedIndice {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, false)
continue
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Errorf("content[%d] = %v, want %v", i, got, want)
}
}
for _, i := range uncopiedIndice {
if _, err := content.FetchAll(ctx, dst, descs[i]); !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, errdef.ErrNotFound)
}
}
}
src := memory.New()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// test extended copy by descs[0] with annotation filter
dst := memory.New()
opts := oras.ExtendedCopyGraphOptions{}
exp := "black."
regex := regexp.MustCompile(exp)
opts.FilterAnnotation("bar", regex)
if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[0], opts); err != nil {
t.Fatalf("ExtendedCopyGraph() error = %v, wantErr %v", err, false)
}
copiedIndice := []int{0, 2, 3}
uncopiedIndice := []int{1, 4, 5}
verifyCopy(dst, copiedIndice, uncopiedIndice)
}
func TestExtendedCopyGraph_FilterAnnotationWithMultipleRegex_Referrers(t *testing.T) {
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType, key, value string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
Annotations: map[string]string{key: value},
})
}
generateArtifactManifest := func(subject ocispec.Descriptor, key string, value string) {
manifest := spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
Subject: &subject,
Annotations: map[string]string{key: value},
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(spec.MediaTypeArtifactManifest, key, value, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageLayer, "", "", []byte("foo")) // descs[0]
generateArtifactManifest(descs[0], "bar", "bluebrown") // descs[1]
generateArtifactManifest(descs[0], "bar", "blackred") // descs[2]
generateArtifactManifest(descs[0], "bar", "blackviolet") // descs[3]
generateArtifactManifest(descs[0], "bar", "greengrey") // descs[4]
generateArtifactManifest(descs[0], "bar", "brownblack") // descs[5]
generateArtifactManifest(descs[0], "bar", "blackblack") // descs[6]
// set up test server
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p := r.URL.Path
var manifests []ocispec.Descriptor
switch {
case p == "/v2/test/referrers/"+descs[0].Digest.String():
manifests = descs[1:]
fallthrough
case strings.HasPrefix(p, "/v2/test/referrers/"):
result := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: manifests,
}
w.Header().Set("Content-Type", ocispec.MediaTypeImageIndex)
if err := json.NewEncoder(w).Encode(result); err != nil {
t.Errorf("failed to write response: %v", err)
}
case strings.Contains(p, descs[0].Digest.String()):
w.Header().Set("Content-Type", ocispec.MediaTypeImageLayer)
w.Header().Set("Content-Digest", descs[0].Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(len(blobs[0])))
w.Write(blobs[0])
case strings.Contains(p, descs[1].Digest.String()):
w.Header().Set("Content-Type", spec.MediaTypeArtifactManifest)
w.Header().Set("Content-Digest", descs[1].Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(len(blobs[1])))
w.Write(blobs[1])
case strings.Contains(p, descs[2].Digest.String()):
w.Header().Set("Content-Type", spec.MediaTypeArtifactManifest)
w.Header().Set("Content-Digest", descs[2].Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(len(blobs[2])))
w.Write(blobs[2])
case strings.Contains(p, descs[3].Digest.String()):
w.Header().Set("Content-Type", spec.MediaTypeArtifactManifest)
w.Header().Set("Content-Digest", descs[3].Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(len(blobs[3])))
w.Write(blobs[3])
case strings.Contains(p, descs[4].Digest.String()):
w.Header().Set("Content-Type", spec.MediaTypeArtifactManifest)
w.Header().Set("Content-Digest", descs[4].Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(len(blobs[4])))
w.Write(blobs[4])
case strings.Contains(p, descs[5].Digest.String()):
w.Header().Set("Content-Type", spec.MediaTypeArtifactManifest)
w.Header().Set("Content-Digest", descs[5].Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(len(blobs[5])))
w.Write(blobs[5])
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Errorf("invalid test http server: %v", err)
}
ctx := context.Background()
verifyCopy := func(dst content.Fetcher, copiedIndice []int, uncopiedIndice []int) {
for _, i := range copiedIndice {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, false)
continue
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Errorf("content[%d] = %v, want %v", i, got, want)
}
}
for _, i := range uncopiedIndice {
if _, err := content.FetchAll(ctx, dst, descs[i]); !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, errdef.ErrNotFound)
}
}
}
src, err := remote.NewRepository(uri.Host + "/test")
if err != nil {
t.Errorf("NewRepository() error = %v", err)
}
// test extended copy by descs[0] with two annotation filters
dst := memory.New()
opts := oras.ExtendedCopyGraphOptions{}
exp1 := "black."
exp2 := ".pink|red"
regex1 := regexp.MustCompile(exp1)
regex2 := regexp.MustCompile(exp2)
opts.FilterAnnotation("bar", regex1)
opts.FilterAnnotation("bar", regex2)
if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[0], opts); err != nil {
t.Fatalf("ExtendedCopyGraph() error = %v, wantErr %v", err, false)
}
copiedIndice := []int{0, 2}
uncopiedIndice := []int{1, 3, 4, 5, 6}
verifyCopy(dst, copiedIndice, uncopiedIndice)
// test extended copy by descs[0] with three annotation filters, nil included
dst = memory.New()
opts = oras.ExtendedCopyGraphOptions{}
exp1 = "black."
exp2 = ".pink|red"
regex1 = regexp.MustCompile(exp1)
regex2 = regexp.MustCompile(exp2)
opts.FilterAnnotation("bar", regex1)
opts.FilterAnnotation("bar", nil)
opts.FilterAnnotation("bar", regex2)
if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[0], opts); err != nil {
t.Fatalf("ExtendedCopyGraph() error = %v, wantErr %v", err, false)
}
copiedIndice = []int{0, 2}
uncopiedIndice = []int{1, 3, 4, 5, 6}
verifyCopy(dst, copiedIndice, uncopiedIndice)
// test extended copy by descs[0] with two annotation filters, the second filter has an unavailable key
dst = memory.New()
opts = oras.ExtendedCopyGraphOptions{}
exp1 = "black."
exp2 = ".pink|red"
regex1 = regexp.MustCompile(exp1)
regex2 = regexp.MustCompile(exp2)
opts.FilterAnnotation("bar", regex1)
opts.FilterAnnotation("test", regex2)
if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[0], opts); err != nil {
t.Fatalf("ExtendedCopyGraph() error = %v, wantErr %v", err, false)
}
copiedIndice = []int{0}
uncopiedIndice = []int{1, 2, 3, 4, 5, 6}
verifyCopy(dst, copiedIndice, uncopiedIndice)
}
func TestExtendedCopyGraph_FilterArtifactTypeWithRegex(t *testing.T) {
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateArtifactManifest := func(subject ocispec.Descriptor, artifactType string) {
manifest := spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
ArtifactType: artifactType,
Subject: &subject,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(spec.MediaTypeArtifactManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // descs[0]
generateArtifactManifest(descs[0], "good-bar-yellow") // descs[1]
generateArtifactManifest(descs[0], "bad-woo-red") // descs[2]
generateArtifactManifest(descs[0], "bad-bar-blue") // descs[3]
generateArtifactManifest(descs[0], "bad-bar-red") // descs[4]
generateArtifactManifest(descs[0], "good-woo-pink") // descs[5]
ctx := context.Background()
verifyCopy := func(dst content.Fetcher, copiedIndice []int, uncopiedIndice []int) {
for _, i := range copiedIndice {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, false)
continue
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Errorf("content[%d] = %v, want %v", i, got, want)
}
}
for _, i := range uncopiedIndice {
if _, err := content.FetchAll(ctx, dst, descs[i]); !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, errdef.ErrNotFound)
}
}
}
src := memory.New()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Errorf("failed to push test content to src: %d: %v", i, err)
}
}
// test extended copy by descs[0], include the predecessors whose artifact
// type matches exp.
exp := ".bar."
dst := memory.New()
opts := oras.ExtendedCopyGraphOptions{}
regex := regexp.MustCompile(exp)
opts.FilterArtifactType(regex)
if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[0], opts); err != nil {
t.Errorf("ExtendedCopyGraph() error = %v, wantErr %v", err, false)
}
copiedIndice := []int{0, 1, 3, 4}
uncopiedIndice := []int{2, 5}
verifyCopy(dst, copiedIndice, uncopiedIndice)
// test extended copy by descs[0] with no regex
// type matches exp.
opts = oras.ExtendedCopyGraphOptions{}
opts.FilterArtifactType(nil)
if opts.FindPredecessors != nil {
t.Fatal("FindPredecessors not nil!")
}
}
func TestExtendedCopyGraph_FilterArtifactTypeWithMultipleRegex(t *testing.T) {
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateArtifactManifest := func(subject ocispec.Descriptor, artifactType string) {
manifest := spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
ArtifactType: artifactType,
Subject: &subject,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(spec.MediaTypeArtifactManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // descs[0]
generateArtifactManifest(descs[0], "good-bar-yellow") // descs[1]
generateArtifactManifest(descs[0], "bad-woo-red") // descs[2]
generateArtifactManifest(descs[0], "bad-bar-blue") // descs[3]
generateArtifactManifest(descs[0], "bad-bar-red") // descs[4]
generateArtifactManifest(descs[0], "good-woo-pink") // descs[5]
ctx := context.Background()
verifyCopy := func(dst content.Fetcher, copiedIndice []int, uncopiedIndice []int) {
for _, i := range copiedIndice {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, false)
continue
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Errorf("content[%d] = %v, want %v", i, got, want)
}
}
for _, i := range uncopiedIndice {
if _, err := dst.Fetch(ctx, descs[i]); !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, errdef.ErrNotFound)
}
}
}
src := memory.New()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Errorf("failed to push test content to src: %d: %v", i, err)
}
}
// test extended copy by descs[0], include the predecessors whose artifact
// type matches exp1 and exp2.
exp1 := ".foo|bar."
exp2 := "bad."
dst := memory.New()
opts := oras.ExtendedCopyGraphOptions{}
regex1 := regexp.MustCompile(exp1)
regex2 := regexp.MustCompile(exp2)
opts.FilterArtifactType(regex1)
opts.FilterArtifactType(regex2)
if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[0], opts); err != nil {
t.Errorf("ExtendedCopyGraph() error = %v, wantErr %v", err, false)
}
copiedIndice := []int{0, 3, 4}
uncopiedIndice := []int{1, 2, 5}
verifyCopy(dst, copiedIndice, uncopiedIndice)
// test extended copy by descs[0], include the predecessors whose artifact
// type matches exp1 and exp2 and nil
exp1 = ".foo|bar."
exp2 = "bad."
dst = memory.New()
opts = oras.ExtendedCopyGraphOptions{}
regex1 = regexp.MustCompile(exp1)
regex2 = regexp.MustCompile(exp2)
opts.FilterArtifactType(regex1)
opts.FilterArtifactType(regex2)
opts.FilterArtifactType(nil)
if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[0], opts); err != nil {
t.Errorf("ExtendedCopyGraph() error = %v, wantErr %v", err, false)
}
copiedIndice = []int{0, 3, 4}
uncopiedIndice = []int{1, 2, 5}
verifyCopy(dst, copiedIndice, uncopiedIndice)
}
func TestExtendedCopyGraph_FilterArtifactTypeWithRegex_ArtifactTypeInDescriptor(t *testing.T) {
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, artifactType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
ArtifactType: artifactType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateArtifactManifest := func(subject ocispec.Descriptor, artifactType string) {
manifest := spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
ArtifactType: artifactType,
Subject: &subject,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(spec.MediaTypeArtifactManifest, artifactType, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageLayer, "", []byte("foo")) // descs[0]
generateArtifactManifest(descs[0], "good-bar-yellow") // descs[1]
generateArtifactManifest(descs[0], "bad-woo-red") // descs[2]
generateArtifactManifest(descs[0], "bad-bar-blue") // descs[3]
generateArtifactManifest(descs[0], "bad-bar-red") // descs[4]
generateArtifactManifest(descs[0], "good-woo-pink") // descs[5]
ctx := context.Background()
verifyCopy := func(dst content.Fetcher, copiedIndice []int, uncopiedIndice []int) {
for _, i := range copiedIndice {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, false)
continue
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Errorf("content[%d] = %v, want %v", i, got, want)
}
}
for _, i := range uncopiedIndice {
if _, err := content.FetchAll(ctx, dst, descs[i]); !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, errdef.ErrNotFound)
}
}
}
src := memory.New()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Errorf("failed to push test content to src: %d: %v", i, err)
}
}
// test extended copy by descs[0], include the predecessors whose artifact
// type matches exp.
exp := ".bar."
dst := memory.New()
opts := oras.ExtendedCopyGraphOptions{}
regex := regexp.MustCompile(exp)
opts.FilterArtifactType(regex)
if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[0], opts); err != nil {
t.Errorf("ExtendedCopyGraph() error = %v, wantErr %v", err, false)
}
copiedIndice := []int{0, 1, 3, 4}
uncopiedIndice := []int{2, 5}
verifyCopy(dst, copiedIndice, uncopiedIndice)
// test extended copy by descs[0] with no regex
// type matches exp.
opts = oras.ExtendedCopyGraphOptions{}
opts.FilterArtifactType(nil)
if opts.FindPredecessors != nil {
t.Fatal("FindPredecessors not nil!")
}
}
func TestExtendedCopyGraph_FilterArtifactTypeWithMultipleRegex_Referrers(t *testing.T) {
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, artifactType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
ArtifactType: artifactType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateArtifactManifest := func(subject ocispec.Descriptor, artifactType string) {
manifest := spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
ArtifactType: artifactType,
Subject: &subject,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(spec.MediaTypeArtifactManifest, artifactType, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageLayer, "", []byte("foo")) // descs[0]
generateArtifactManifest(descs[0], "good-bar-yellow") // descs[1]
generateArtifactManifest(descs[0], "bad-woo-red") // descs[2]
generateArtifactManifest(descs[0], "bad-bar-blue") // descs[3]
generateArtifactManifest(descs[0], "bad-bar-red") // descs[4]
generateArtifactManifest(descs[0], "good-woo-pink") // descs[5]
// set up test server
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p := r.URL.Path
var manifests []ocispec.Descriptor
switch {
case p == "/v2/test/referrers/"+descs[0].Digest.String():
manifests = descs[1:]
fallthrough
case strings.HasPrefix(p, "/v2/test/referrers/"):
result := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: manifests,
}
w.Header().Set("Content-Type", ocispec.MediaTypeImageIndex)
if err := json.NewEncoder(w).Encode(result); err != nil {
t.Errorf("failed to write response: %v", err)
}
case strings.Contains(p, descs[0].Digest.String()):
w.Header().Set("Content-Type", ocispec.MediaTypeImageLayer)
w.Header().Set("Content-Digest", descs[0].Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(len(blobs[0])))
w.Write(blobs[0])
case strings.Contains(p, descs[1].Digest.String()):
w.Header().Set("Content-Type", spec.MediaTypeArtifactManifest)
w.Header().Set("Content-Digest", descs[1].Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(len(blobs[1])))
w.Write(blobs[1])
case strings.Contains(p, descs[2].Digest.String()):
w.Header().Set("Content-Type", spec.MediaTypeArtifactManifest)
w.Header().Set("Content-Digest", descs[2].Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(len(blobs[2])))
w.Write(blobs[2])
case strings.Contains(p, descs[3].Digest.String()):
w.Header().Set("Content-Type", spec.MediaTypeArtifactManifest)
w.Header().Set("Content-Digest", descs[3].Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(len(blobs[3])))
w.Write(blobs[3])
case strings.Contains(p, descs[4].Digest.String()):
w.Header().Set("Content-Type", spec.MediaTypeArtifactManifest)
w.Header().Set("Content-Digest", descs[4].Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(len(blobs[4])))
w.Write(blobs[4])
case strings.Contains(p, descs[5].Digest.String()):
w.Header().Set("Content-Type", spec.MediaTypeArtifactManifest)
w.Header().Set("Content-Digest", descs[5].Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(len(blobs[5])))
w.Write(blobs[5])
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Errorf("invalid test http server: %v", err)
}
ctx := context.Background()
verifyCopy := func(dst content.Fetcher, copiedIndice []int, uncopiedIndice []int) {
for _, i := range copiedIndice {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, false)
continue
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Errorf("content[%d] = %v, want %v", i, got, want)
}
}
for _, i := range uncopiedIndice {
if _, err := content.FetchAll(ctx, dst, descs[i]); !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, errdef.ErrNotFound)
}
}
}
src, err := remote.NewRepository(uri.Host + "/test")
if err != nil {
t.Errorf("NewRepository() error = %v", err)
}
// test extended copy by descs[0], include the predecessors whose artifact
// type matches exp1 and exp2.
exp1 := ".foo|bar."
exp2 := "bad."
dst := memory.New()
opts := oras.ExtendedCopyGraphOptions{}
regex1 := regexp.MustCompile(exp1)
regex2 := regexp.MustCompile(exp2)
opts.FilterArtifactType(regex1)
opts.FilterArtifactType(regex2)
if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[0], opts); err != nil {
t.Errorf("ExtendedCopyGraph() error = %v, wantErr %v", err, false)
}
copiedIndice := []int{0, 3, 4}
uncopiedIndice := []int{1, 2, 5}
verifyCopy(dst, copiedIndice, uncopiedIndice)
}
func TestExtendedCopyGraph_FilterArtifactTypeAndAnnotationWithMultipleRegex(t *testing.T) {
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateArtifactManifest := func(subject ocispec.Descriptor, artifactType string, value string) {
manifest := spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
ArtifactType: artifactType,
Subject: &subject,
Annotations: map[string]string{"rank": value},
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(spec.MediaTypeArtifactManifest, manifestJSON)
}
generateImageManifest := func(subject, config ocispec.Descriptor, value string) {
manifest := ocispec.Manifest{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageManifest,
Config: config,
Subject: &subject,
Annotations: map[string]string{"rank": value},
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // descs[0]
generateArtifactManifest(descs[0], "good-bar-yellow", "1st") // descs[1]
generateArtifactManifest(descs[0], "bad-woo-red", "1st") // descs[2]
generateArtifactManifest(descs[0], "bad-bar-blue", "2nd") // descs[3]
generateArtifactManifest(descs[0], "bad-bar-red", "3rd") // descs[4]
appendBlob("good-woo-pink", []byte("bar")) // descs[5]
generateImageManifest(descs[0], descs[5], "3rd") // descs[6]
appendBlob("bad-bar-pink", []byte("baz")) // descs[7]
generateImageManifest(descs[0], descs[7], "4th") // descs[8]
appendBlob("bad-bar-orange", []byte("config!")) // descs[9]
generateImageManifest(descs[0], descs[9], "5th") // descs[10]
ctx := context.Background()
verifyCopy := func(dst content.Fetcher, copiedIndice []int, uncopiedIndice []int) {
for _, i := range copiedIndice {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, false)
continue
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Errorf("content[%d] = %v, want %v", i, got, want)
}
}
for _, i := range uncopiedIndice {
if _, err := dst.Fetch(ctx, descs[i]); !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, errdef.ErrNotFound)
}
}
}
src := memory.New()
for i := range blobs {
err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Errorf("failed to push test content to src: %d: %v", i, err)
}
}
// test extended copy by descs[0], include the predecessors whose artifact
// type and annotation match the regular expressions.
typeExp1 := ".foo|bar."
typeExp2 := "bad."
annotationExp1 := "[1-4]."
annotationExp2 := "2|4."
dst := memory.New()
opts := oras.ExtendedCopyGraphOptions{}
typeRegex1 := regexp.MustCompile(typeExp1)
typeRegex2 := regexp.MustCompile(typeExp2)
annotationRegex1 := regexp.MustCompile(annotationExp1)
annotationRegex2 := regexp.MustCompile(annotationExp2)
opts.FilterAnnotation("rank", annotationRegex1)
opts.FilterArtifactType(typeRegex1)
opts.FilterAnnotation("rank", annotationRegex2)
opts.FilterArtifactType(typeRegex2)
if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[0], opts); err != nil {
t.Errorf("ExtendedCopyGraph() error = %v, wantErr %v", err, false)
}
copiedIndice := []int{0, 3, 7, 8}
uncopiedIndice := []int{1, 2, 4, 5, 6, 9, 10}
verifyCopy(dst, copiedIndice, uncopiedIndice)
}
func TestExtendedCopyGraph_FilterArtifactTypeAndAnnotationWithMultipleRegex_Referrers(t *testing.T) {
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, artifactType string, blob []byte, value string) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
ArtifactType: artifactType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
Annotations: map[string]string{"rank": value},
})
}
generateArtifactManifest := func(subject ocispec.Descriptor, artifactType string, value string) {
manifest := spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
ArtifactType: artifactType,
Subject: &subject,
Annotations: map[string]string{"rank": value},
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(spec.MediaTypeArtifactManifest, artifactType, manifestJSON, value)
}
appendBlob(ocispec.MediaTypeImageLayer, "", []byte("foo"), "na") // descs[0]
generateArtifactManifest(descs[0], "good-bar-yellow", "1st") // descs[1]
generateArtifactManifest(descs[0], "bad-woo-red", "1st") // descs[2]
generateArtifactManifest(descs[0], "bad-bar-blue", "2nd") // descs[3]
generateArtifactManifest(descs[0], "bad-bar-red", "3rd") // descs[4]
generateArtifactManifest(descs[0], "good-woo-pink", "2nd") // descs[5]
generateArtifactManifest(descs[0], "good-foo-blue", "3rd") // descs[6]
generateArtifactManifest(descs[0], "bad-bar-orange", "4th") // descs[7]
generateArtifactManifest(descs[0], "bad-woo-white", "4th") // descs[8]
generateArtifactManifest(descs[0], "good-woo-orange", "na") // descs[9]
// set up test server
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p := r.URL.Path
var manifests []ocispec.Descriptor
switch {
case p == "/v2/test/referrers/"+descs[0].Digest.String():
manifests = descs[1:]
fallthrough
case strings.HasPrefix(p, "/v2/test/referrers/"):
result := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: manifests,
}
w.Header().Set("Content-Type", ocispec.MediaTypeImageIndex)
if err := json.NewEncoder(w).Encode(result); err != nil {
t.Errorf("failed to write response: %v", err)
}
case strings.Contains(p, descs[0].Digest.String()):
w.Header().Set("Content-Type", ocispec.MediaTypeImageLayer)
w.Header().Set("Content-Digest", descs[0].Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(len(blobs[0])))
w.Write(blobs[0])
case strings.Contains(p, descs[1].Digest.String()):
w.Header().Set("Content-Type", spec.MediaTypeArtifactManifest)
w.Header().Set("Content-Digest", descs[1].Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(len(blobs[1])))
w.Write(blobs[1])
case strings.Contains(p, descs[2].Digest.String()):
w.Header().Set("Content-Type", spec.MediaTypeArtifactManifest)
w.Header().Set("Content-Digest", descs[2].Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(len(blobs[2])))
w.Write(blobs[2])
case strings.Contains(p, descs[3].Digest.String()):
w.Header().Set("Content-Type", spec.MediaTypeArtifactManifest)
w.Header().Set("Content-Digest", descs[3].Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(len(blobs[3])))
w.Write(blobs[3])
case strings.Contains(p, descs[4].Digest.String()):
w.Header().Set("Content-Type", spec.MediaTypeArtifactManifest)
w.Header().Set("Content-Digest", descs[4].Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(len(blobs[4])))
w.Write(blobs[4])
case strings.Contains(p, descs[5].Digest.String()):
w.Header().Set("Content-Type", spec.MediaTypeArtifactManifest)
w.Header().Set("Content-Digest", descs[5].Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(len(blobs[5])))
w.Write(blobs[5])
case strings.Contains(p, descs[6].Digest.String()):
w.Header().Set("Content-Type", spec.MediaTypeArtifactManifest)
w.Header().Set("Content-Digest", descs[6].Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(len(blobs[6])))
w.Write(blobs[6])
case strings.Contains(p, descs[7].Digest.String()):
w.Header().Set("Content-Type", spec.MediaTypeArtifactManifest)
w.Header().Set("Content-Digest", descs[7].Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(len(blobs[7])))
w.Write(blobs[7])
case strings.Contains(p, descs[8].Digest.String()):
w.Header().Set("Content-Type", spec.MediaTypeArtifactManifest)
w.Header().Set("Content-Digest", descs[8].Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(len(blobs[8])))
w.Write(blobs[8])
case strings.Contains(p, descs[9].Digest.String()):
w.Header().Set("Content-Type", spec.MediaTypeArtifactManifest)
w.Header().Set("Content-Digest", descs[9].Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(len(blobs[9])))
w.Write(blobs[9])
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Errorf("invalid test http server: %v", err)
}
ctx := context.Background()
verifyCopy := func(dst content.Fetcher, copiedIndice []int, uncopiedIndice []int) {
for _, i := range copiedIndice {
got, err := content.FetchAll(ctx, dst, descs[i])
if err != nil {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, false)
continue
}
if want := blobs[i]; !bytes.Equal(got, want) {
t.Errorf("content[%d] = %v, want %v", i, got, want)
}
}
for _, i := range uncopiedIndice {
if _, err := content.FetchAll(ctx, dst, descs[i]); !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("content[%d] error = %v, wantErr %v", i, err, errdef.ErrNotFound)
}
}
}
src, err := remote.NewRepository(uri.Host + "/test")
if err != nil {
t.Errorf("NewRepository() error = %v", err)
}
// test extended copy by descs[0], include the predecessors whose artifact
// type and annotation match the regular expressions.
typeExp1 := ".foo|bar."
typeExp2 := "bad."
annotationExp1 := "[1-4]."
annotationExp2 := "2|4."
dst := memory.New()
opts := oras.ExtendedCopyGraphOptions{}
typeRegex1 := regexp.MustCompile(typeExp1)
typeRegex2 := regexp.MustCompile(typeExp2)
annotationRegex1 := regexp.MustCompile(annotationExp1)
annotationRegex2 := regexp.MustCompile(annotationExp2)
opts.FilterAnnotation("rank", annotationRegex1)
opts.FilterArtifactType(typeRegex1)
opts.FilterAnnotation("rank", annotationRegex2)
opts.FilterArtifactType(typeRegex2)
if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[0], opts); err != nil {
t.Errorf("ExtendedCopyGraph() error = %v, wantErr %v", err, false)
}
copiedIndice := []int{0, 3, 7}
uncopiedIndice := []int{1, 2, 4, 5, 6, 8, 9}
verifyCopy(dst, copiedIndice, uncopiedIndice)
}
oras-go-2.5.0/go.mod 0000664 0000000 0000000 00000000245 14576745303 0014212 0 ustar 00root root 0000000 0000000 module oras.land/oras-go/v2
go 1.21
require (
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.0
golang.org/x/sync v0.6.0
)
oras-go-2.5.0/go.sum 0000664 0000000 0000000 00000001025 14576745303 0014234 0 ustar 00root root 0000000 0000000 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
oras-go-2.5.0/internal/ 0000775 0000000 0000000 00000000000 14576745303 0014717 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/internal/cas/ 0000775 0000000 0000000 00000000000 14576745303 0015465 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/internal/cas/memory.go 0000664 0000000 0000000 00000005341 14576745303 0017327 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 cas
import (
"bytes"
"context"
"fmt"
"io"
"sync"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
contentpkg "oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/internal/descriptor"
)
// Memory is a memory based CAS.
type Memory struct {
content sync.Map // map[descriptor.Descriptor][]byte
}
// NewMemory creates a new Memory CAS.
func NewMemory() *Memory {
return &Memory{}
}
// Fetch fetches the content identified by the descriptor.
func (m *Memory) Fetch(_ context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
key := descriptor.FromOCI(target)
content, exists := m.content.Load(key)
if !exists {
return nil, fmt.Errorf("%s: %s: %w", key.Digest, key.MediaType, errdef.ErrNotFound)
}
return io.NopCloser(bytes.NewReader(content.([]byte))), nil
}
// Push pushes the content, matching the expected descriptor.
func (m *Memory) Push(_ context.Context, expected ocispec.Descriptor, content io.Reader) error {
key := descriptor.FromOCI(expected)
// check if the content exists in advance to avoid reading from the content.
if _, exists := m.content.Load(key); exists {
return fmt.Errorf("%s: %s: %w", key.Digest, key.MediaType, errdef.ErrAlreadyExists)
}
// read and try to store the content.
value, err := contentpkg.ReadAll(content, expected)
if err != nil {
return err
}
if _, exists := m.content.LoadOrStore(key, value); exists {
return fmt.Errorf("%s: %s: %w", key.Digest, key.MediaType, errdef.ErrAlreadyExists)
}
return nil
}
// Exists returns true if the described content exists.
func (m *Memory) Exists(_ context.Context, target ocispec.Descriptor) (bool, error) {
key := descriptor.FromOCI(target)
_, exists := m.content.Load(key)
return exists, nil
}
// Map dumps the memory into a built-in map structure.
// Like other operations, calling Map() is go-routine safe. However, it does not
// necessarily correspond to any consistent snapshot of the storage contents.
func (m *Memory) Map() map[descriptor.Descriptor][]byte {
res := make(map[descriptor.Descriptor][]byte)
m.content.Range(func(key, value interface{}) bool {
res[key.(descriptor.Descriptor)] = value.([]byte)
return true
})
return res
}
oras-go-2.5.0/internal/cas/memory_test.go 0000664 0000000 0000000 00000006340 14576745303 0020366 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 cas
import (
"bytes"
"context"
_ "crypto/sha256"
"errors"
"io"
"strings"
"testing"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/errdef"
)
func TestMemorySuccess(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
s := NewMemory()
ctx := context.Background()
err := s.Push(ctx, desc, bytes.NewReader(content))
if err != nil {
t.Fatal("Memory.Push() error =", err)
}
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Fatal("Memory.Exists() error =", err)
}
if !exists {
t.Errorf("Memory.Exists() = %v, want %v", exists, true)
}
rc, err := s.Fetch(ctx, desc)
if err != nil {
t.Fatal("Memory.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Memory.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Memory.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Memory.Fetch() = %v, want %v", got, content)
}
if got := len(s.Map()); got != 1 {
t.Errorf("Memory.Map() = %v, want %v", got, 1)
}
}
func TestMemoryNotFound(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
s := NewMemory()
ctx := context.Background()
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Error("Memory.Exists() error =", err)
}
if exists {
t.Errorf("Memory.Exists() = %v, want %v", exists, false)
}
_, err = s.Fetch(ctx, desc)
if !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("Memory.Fetch() error = %v, want %v", err, errdef.ErrNotFound)
}
}
func TestMemoryAlreadyExists(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
s := NewMemory()
ctx := context.Background()
err := s.Push(ctx, desc, bytes.NewReader(content))
if err != nil {
t.Fatal("Memory.Push() error =", err)
}
err = s.Push(ctx, desc, bytes.NewReader(content))
if !errors.Is(err, errdef.ErrAlreadyExists) {
t.Errorf("Memory.Push() error = %v, want %v", err, errdef.ErrAlreadyExists)
}
}
func TestMemoryBadPush(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
s := NewMemory()
ctx := context.Background()
err := s.Push(ctx, desc, strings.NewReader("foobar"))
if err == nil {
t.Errorf("Memory.Push() error = %v, wantErr %v", err, true)
}
}
oras-go-2.5.0/internal/cas/proxy.go 0000664 0000000 0000000 00000006277 14576745303 0017211 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 cas
import (
"context"
"io"
"sync"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/internal/ioutil"
)
// Proxy is a caching proxy for the storage.
// The first fetch call of a described content will read from the remote and
// cache the fetched content.
// The subsequent fetch call will read from the local cache.
type Proxy struct {
content.ReadOnlyStorage
Cache content.Storage
StopCaching bool
}
// NewProxy creates a proxy for the `base` storage, using the `cache` storage as
// the cache.
func NewProxy(base content.ReadOnlyStorage, cache content.Storage) *Proxy {
return &Proxy{
ReadOnlyStorage: base,
Cache: cache,
}
}
// NewProxyWithLimit creates a proxy for the `base` storage, using the `cache`
// storage with a push size limit as the cache.
func NewProxyWithLimit(base content.ReadOnlyStorage, cache content.Storage, pushLimit int64) *Proxy {
limitedCache := content.LimitStorage(cache, pushLimit)
return &Proxy{
ReadOnlyStorage: base,
Cache: limitedCache,
}
}
// Fetch fetches the content identified by the descriptor.
func (p *Proxy) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
if p.StopCaching {
return p.FetchCached(ctx, target)
}
rc, err := p.Cache.Fetch(ctx, target)
if err == nil {
return rc, nil
}
rc, err = p.ReadOnlyStorage.Fetch(ctx, target)
if err != nil {
return nil, err
}
pr, pw := io.Pipe()
var wg sync.WaitGroup
wg.Add(1)
var pushErr error
go func() {
defer wg.Done()
pushErr = p.Cache.Push(ctx, target, pr)
if pushErr != nil {
pr.CloseWithError(pushErr)
}
}()
closer := ioutil.CloserFunc(func() error {
rcErr := rc.Close()
if err := pw.Close(); err != nil {
return err
}
wg.Wait()
if pushErr != nil {
return pushErr
}
return rcErr
})
return struct {
io.Reader
io.Closer
}{
Reader: io.TeeReader(rc, pw),
Closer: closer,
}, nil
}
// FetchCached fetches the content identified by the descriptor.
// If the content is not cached, it will be fetched from the remote without
// caching.
func (p *Proxy) FetchCached(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
exists, err := p.Cache.Exists(ctx, target)
if err != nil {
return nil, err
}
if exists {
return p.Cache.Fetch(ctx, target)
}
return p.ReadOnlyStorage.Fetch(ctx, target)
}
// Exists returns true if the described content exists.
func (p *Proxy) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) {
exists, err := p.Cache.Exists(ctx, target)
if err == nil && exists {
return true, nil
}
return p.ReadOnlyStorage.Exists(ctx, target)
}
oras-go-2.5.0/internal/cas/proxy_test.go 0000664 0000000 0000000 00000021614 14576745303 0020240 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 cas
import (
"bytes"
"context"
_ "crypto/sha256"
"errors"
"io"
"testing"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/errdef"
)
func TestProxyCache(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
ctx := context.Background()
base := NewMemory()
err := base.Push(ctx, desc, bytes.NewReader(content))
if err != nil {
t.Fatal("Memory.Push() error =", err)
}
s := NewProxy(base, NewMemory())
// first fetch
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Fatal("Proxy.Exists() error =", err)
}
if !exists {
t.Errorf("Proxy.Exists() = %v, want %v", exists, true)
}
rc, err := s.Fetch(ctx, desc)
if err != nil {
t.Fatal("Proxy.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Proxy.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Proxy.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Proxy.Fetch() = %v, want %v", got, content)
}
// repeated fetch should not touch base CAS
// nil base will generate panic if the base CAS is touched
s.ReadOnlyStorage = nil
exists, err = s.Exists(ctx, desc)
if err != nil {
t.Fatal("Proxy.Exists() error =", err)
}
if !exists {
t.Errorf("Proxy.Exists() = %v, want %v", exists, true)
}
rc, err = s.Fetch(ctx, desc)
if err != nil {
t.Fatal("Proxy.Fetch() error =", err)
}
got, err = io.ReadAll(rc)
if err != nil {
t.Fatal("Proxy.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Proxy.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Proxy.Fetch() = %v, want %v", got, content)
}
}
func TestProxy_FetchCached_NotCachedContent(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
ctx := context.Background()
base := NewMemory()
err := base.Push(ctx, desc, bytes.NewReader(content))
if err != nil {
t.Fatal("Memory.Push() error =", err)
}
s := NewProxy(base, NewMemory())
// FetchCached should fetch from the base CAS
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Fatal("Proxy.Exists() error =", err)
}
if !exists {
t.Errorf("Proxy.Exists() = %v, want %v", exists, true)
}
rc, err := s.FetchCached(ctx, desc)
if err != nil {
t.Fatal("Proxy.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Proxy.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Proxy.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Proxy.Fetch() = %v, want %v", got, content)
}
// the content should not exist in the cache
exists, err = s.Cache.Exists(ctx, desc)
if err != nil {
t.Fatal("Proxy.Cache.Exists() error =", err)
}
if exists {
t.Errorf("Proxy.Cache.Exists()() = %v, want %v", exists, false)
}
}
func TestProxy_FetchCached_CachedContent(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
ctx := context.Background()
base := NewMemory()
err := base.Push(ctx, desc, bytes.NewReader(content))
if err != nil {
t.Fatal("Memory.Push() error =", err)
}
s := NewProxy(base, NewMemory())
// first fetch
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Fatal("Proxy.Exists() error =", err)
}
if !exists {
t.Errorf("Proxy.Exists() = %v, want %v", exists, true)
}
rc, err := s.Fetch(ctx, desc)
if err != nil {
t.Fatal("Proxy.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Proxy.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Proxy.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Proxy.Fetch() = %v, want %v", got, content)
}
// the subsequent FetchCached should not touch base CAS
// nil base will generate panic if the base CAS is touched
s.ReadOnlyStorage = nil
exists, err = s.Exists(ctx, desc)
if err != nil {
t.Fatal("Proxy.Exists() error =", err)
}
if !exists {
t.Errorf("Proxy.Exists() = %v, want %v", exists, true)
}
rc, err = s.FetchCached(ctx, desc)
if err != nil {
t.Fatal("Proxy.Fetch() error =", err)
}
got, err = io.ReadAll(rc)
if err != nil {
t.Fatal("Proxy.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Proxy.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Proxy.Fetch() = %v, want %v", got, content)
}
}
func TestProxy_StopCaching(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
ctx := context.Background()
base := NewMemory()
err := base.Push(ctx, desc, bytes.NewReader(content))
if err != nil {
t.Fatal("Memory.Push() error =", err)
}
s := NewProxy(base, NewMemory())
// FetchCached should fetch from the base CAS
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Fatal("Proxy.Exists() error =", err)
}
if !exists {
t.Errorf("Proxy.Exists() = %v, want %v", exists, true)
}
// test StopCaching
s.StopCaching = true
rc, err := s.Fetch(ctx, desc)
if err != nil {
t.Fatal("Proxy.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Proxy.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Proxy.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Proxy.Fetch() = %v, want %v", got, content)
}
// the content should not exist in the cache
exists, err = s.Cache.Exists(ctx, desc)
if err != nil {
t.Fatal("Proxy.Cache.Exists() error =", err)
}
if exists {
t.Errorf("Proxy.Cache.Exists()() = %v, want %v", exists, false)
}
}
func TestProxyWithLimit_WithinLimit(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
ctx := context.Background()
base := NewMemory()
err := base.Push(ctx, desc, bytes.NewReader(content))
if err != nil {
t.Fatal("Memory.Push() error =", err)
}
s := NewProxyWithLimit(base, NewMemory(), 4*1024*1024)
// first fetch
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Fatal("Proxy.Exists() error =", err)
}
if !exists {
t.Errorf("Proxy.Exists() = %v, want %v", exists, true)
}
rc, err := s.Fetch(ctx, desc)
if err != nil {
t.Fatal("Proxy.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Proxy.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Proxy.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Proxy.Fetch() = %v, want %v", got, content)
}
// repeated fetch should not touch base CAS
// nil base will generate panic if the base CAS is touched
s.ReadOnlyStorage = nil
exists, err = s.Exists(ctx, desc)
if err != nil {
t.Fatal("Proxy.Exists() error =", err)
}
if !exists {
t.Errorf("Proxy.Exists() = %v, want %v", exists, true)
}
rc, err = s.Fetch(ctx, desc)
if err != nil {
t.Fatal("Proxy.Fetch() error =", err)
}
got, err = io.ReadAll(rc)
if err != nil {
t.Fatal("Proxy.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Proxy.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Proxy.Fetch() = %v, want %v", got, content)
}
}
func TestProxyWithLimit_ExceedsLimit(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
ctx := context.Background()
base := NewMemory()
err := base.Push(ctx, desc, bytes.NewReader(content))
if err != nil {
t.Fatal("Memory.Push() error =", err)
}
s := NewProxyWithLimit(base, NewMemory(), 1)
// test fetch
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Fatal("Proxy.Exists() error =", err)
}
if !exists {
t.Errorf("Proxy.Exists() = %v, want %v", exists, true)
}
rc, err := s.Fetch(ctx, desc)
if err != nil {
t.Fatal("Proxy.Fetch() error =", err)
}
_, err = io.ReadAll(rc)
if !errors.Is(err, errdef.ErrSizeExceedsLimit) {
t.Fatalf("Proxy.Fetch().Read() error = %v, wantErr %v", err, errdef.ErrSizeExceedsLimit)
}
}
oras-go-2.5.0/internal/container/ 0000775 0000000 0000000 00000000000 14576745303 0016701 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/internal/container/set/ 0000775 0000000 0000000 00000000000 14576745303 0017474 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/internal/container/set/set.go 0000664 0000000 0000000 00000002025 14576745303 0020615 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 set
// Set represents a set data structure.
type Set[T comparable] map[T]struct{}
// New returns an initialized set.
func New[T comparable]() Set[T] {
return make(Set[T])
}
// Add adds item into the set s.
func (s Set[T]) Add(item T) {
s[item] = struct{}{}
}
// Contains returns true if the set s contains item.
func (s Set[T]) Contains(item T) bool {
_, ok := s[item]
return ok
}
// Delete deletes an item from the set.
func (s Set[T]) Delete(item T) {
delete(s, item)
}
oras-go-2.5.0/internal/container/set/set_test.go 0000664 0000000 0000000 00000003656 14576745303 0021667 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 set
import "testing"
func TestSet(t *testing.T) {
set := New[string]()
// test checking a non-existing key
key1 := "foo"
if got, want := set.Contains(key1), false; got != want {
t.Errorf("Set.Contains(%s) = %v, want %v", key1, got, want)
}
if got, want := len(set), 0; got != want {
t.Errorf("len(Set) = %v, want %v", got, want)
}
// test adding a new key
set.Add(key1)
if got, want := set.Contains(key1), true; got != want {
t.Errorf("Set.Contains(%s) = %v, want %v", key1, got, want)
}
if got, want := len(set), 1; got != want {
t.Errorf("len(Set) = %v, want %v", got, want)
}
// test adding an existing key
set.Add(key1)
if got, want := set.Contains(key1), true; got != want {
t.Errorf("Set.Contains(%s) = %v, want %v", key1, got, want)
}
if got, want := len(set), 1; got != want {
t.Errorf("len(Set) = %v, want %v", got, want)
}
// test adding another key
key2 := "bar"
set.Add(key2)
if got, want := set.Contains(key2), true; got != want {
t.Errorf("Set.Contains(%s) = %v, want %v", key2, got, want)
}
if got, want := len(set), 2; got != want {
t.Errorf("len(Set) = %v, want %v", got, want)
}
// test deleting a key
set.Delete(key1)
if got, want := set.Contains(key1), false; got != want {
t.Errorf("Set.Contains(%s) = %v, want %v", key1, got, want)
}
if got, want := len(set), 1; got != want {
t.Errorf("len(Set) = %v, want %v", got, want)
}
}
oras-go-2.5.0/internal/copyutil/ 0000775 0000000 0000000 00000000000 14576745303 0016567 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/internal/copyutil/stack.go 0000664 0000000 0000000 00000002657 14576745303 0020235 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 copyutil
import (
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
// NodeInfo represents information of a node that is being visited in
// ExtendedCopy.
type NodeInfo struct {
// Node represents a node in the graph.
Node ocispec.Descriptor
// Depth represents the depth of the node in the graph.
Depth int
}
// Stack represents a stack data structure that is used in ExtendedCopy for
// storing node information.
type Stack []NodeInfo
// IsEmpty returns true if the stack is empty, otherwise returns false.
func (s *Stack) IsEmpty() bool {
return len(*s) == 0
}
// Push pushes an item to the stack.
func (s *Stack) Push(i NodeInfo) {
*s = append(*s, i)
}
// Pop pops the top item out of the stack.
func (s *Stack) Pop() (NodeInfo, bool) {
if s.IsEmpty() {
return NodeInfo{}, false
}
last := len(*s) - 1
top := (*s)[last]
*s = (*s)[:last]
return top, true
}
oras-go-2.5.0/internal/copyutil/stack_test.go 0000664 0000000 0000000 00000002451 14576745303 0021264 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 copyutil
import (
"reflect"
"testing"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
func TestStack(t *testing.T) {
var stack Stack
isEmpty := stack.IsEmpty()
if !isEmpty {
t.Errorf("Stack.IsEmpty() = %v, want %v", isEmpty, true)
}
items := []NodeInfo{
{ocispec.Descriptor{}, 0},
{ocispec.Descriptor{}, 1},
{ocispec.Descriptor{}, 2},
}
for _, item := range items {
stack.Push(item)
}
i := len(items) - 1
for !stack.IsEmpty() {
got, ok := stack.Pop()
if !ok {
t.Fatalf("Stack.Pop() = %v, want %v", ok, true)
}
if !reflect.DeepEqual(got, items[i]) {
t.Errorf("Stack.Pop() = %v, want %v", got, items[i])
}
i--
}
_, ok := stack.Pop()
if ok {
t.Errorf("Stack.Pop() = %v, want %v", ok, false)
}
}
oras-go-2.5.0/internal/descriptor/ 0000775 0000000 0000000 00000000000 14576745303 0017075 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/internal/descriptor/descriptor.go 0000664 0000000 0000000 00000005075 14576745303 0021611 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 (
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/internal/docker"
"oras.land/oras-go/v2/internal/spec"
)
// DefaultMediaType is the media type used when no media type is specified.
const DefaultMediaType string = "application/octet-stream"
// Descriptor contains the minimun information to describe the disposition of
// targeted content.
// Since it only has strings and integers, Descriptor is a comparable struct.
type Descriptor struct {
// MediaType is the media type of the object this schema refers to.
MediaType string `json:"mediaType,omitempty"`
// Digest is the digest of the targeted content.
Digest digest.Digest `json:"digest"`
// Size specifies the size in bytes of the blob.
Size int64 `json:"size"`
}
// Empty is an empty descriptor
var Empty Descriptor
// FromOCI shrinks the OCI descriptor to the minimum.
func FromOCI(desc ocispec.Descriptor) Descriptor {
return Descriptor{
MediaType: desc.MediaType,
Digest: desc.Digest,
Size: desc.Size,
}
}
// IsForeignLayer checks if a descriptor describes a foreign layer.
func IsForeignLayer(desc ocispec.Descriptor) bool {
switch desc.MediaType {
case ocispec.MediaTypeImageLayerNonDistributable,
ocispec.MediaTypeImageLayerNonDistributableGzip,
ocispec.MediaTypeImageLayerNonDistributableZstd,
docker.MediaTypeForeignLayer:
return true
default:
return false
}
}
// IsManifest checks if a descriptor describes a manifest.
func IsManifest(desc ocispec.Descriptor) bool {
switch desc.MediaType {
case docker.MediaTypeManifest,
docker.MediaTypeManifestList,
ocispec.MediaTypeImageManifest,
ocispec.MediaTypeImageIndex,
spec.MediaTypeArtifactManifest:
return true
default:
return false
}
}
// Plain returns a plain descriptor that contains only MediaType, Digest and
// Size.
func Plain(desc ocispec.Descriptor) ocispec.Descriptor {
return ocispec.Descriptor{
MediaType: desc.MediaType,
Digest: desc.Digest,
Size: desc.Size,
}
}
oras-go-2.5.0/internal/docker/ 0000775 0000000 0000000 00000000000 14576745303 0016166 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/internal/docker/mediatype.go 0000664 0000000 0000000 00000001637 14576745303 0020505 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 docker
// docker media types
const (
MediaTypeConfig = "application/vnd.docker.container.image.v1+json"
MediaTypeManifestList = "application/vnd.docker.distribution.manifest.list.v2+json"
MediaTypeManifest = "application/vnd.docker.distribution.manifest.v2+json"
MediaTypeForeignLayer = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip"
)
oras-go-2.5.0/internal/fs/ 0000775 0000000 0000000 00000000000 14576745303 0015327 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/internal/fs/tarfs/ 0000775 0000000 0000000 00000000000 14576745303 0016446 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/internal/fs/tarfs/tarfs.go 0000664 0000000 0000000 00000007513 14576745303 0020122 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 tarfs
import (
"archive/tar"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"oras.land/oras-go/v2/errdef"
)
// blockSize is the size of each block in a tar archive.
const blockSize int64 = 512
// TarFS represents a file system (an fs.FS) based on a tar archive.
type TarFS struct {
path string
entries map[string]*entry
}
// entry represents an entry in a tar archive.
type entry struct {
header *tar.Header
pos int64
}
// New returns a file system (an fs.FS) for a tar archive located at path.
func New(path string) (*TarFS, error) {
pathAbs, err := filepath.Abs(path)
if err != nil {
return nil, fmt.Errorf("failed to resolve absolute path for %s: %w", path, err)
}
tarfs := &TarFS{
path: pathAbs,
entries: make(map[string]*entry),
}
if err := tarfs.indexEntries(); err != nil {
return nil, err
}
return tarfs, nil
}
// Open opens the named file.
// When Open returns an error, it should be of type *PathError
// with the Op field set to "open", the Path field set to name,
// and the Err field describing the problem.
//
// Open should reject attempts to open names that do not satisfy
// ValidPath(name), returning a *PathError with Err set to
// ErrInvalid or ErrNotExist.
func (tfs *TarFS) Open(name string) (file fs.File, openErr error) {
entry, err := tfs.getEntry(name)
if err != nil {
return nil, err
}
tarFile, err := os.Open(tfs.path)
if err != nil {
return nil, err
}
defer func() {
if openErr != nil {
tarFile.Close()
}
}()
if _, err := tarFile.Seek(entry.pos, io.SeekStart); err != nil {
return nil, err
}
tr := tar.NewReader(tarFile)
if _, err := tr.Next(); err != nil {
return nil, err
}
return &entryFile{
Reader: tr,
Closer: tarFile,
header: entry.header,
}, nil
}
// Stat returns a FileInfo describing the file.
// If there is an error, it should be of type *PathError.
func (tfs *TarFS) Stat(name string) (fs.FileInfo, error) {
entry, err := tfs.getEntry(name)
if err != nil {
return nil, err
}
return entry.header.FileInfo(), nil
}
// getEntry returns the named entry.
func (tfs *TarFS) getEntry(name string) (*entry, error) {
if !fs.ValidPath(name) {
return nil, &fs.PathError{Path: name, Err: fs.ErrInvalid}
}
entry, ok := tfs.entries[name]
if !ok {
return nil, &fs.PathError{Path: name, Err: fs.ErrNotExist}
}
if entry.header.Typeflag != tar.TypeReg {
// support regular files only
return nil, fmt.Errorf("%s: type flag %c is not supported: %w",
name, entry.header.Typeflag, errdef.ErrUnsupported)
}
return entry, nil
}
// indexEntries index entries in the tar archive.
func (tfs *TarFS) indexEntries() error {
tarFile, err := os.Open(tfs.path)
if err != nil {
return err
}
defer tarFile.Close()
tr := tar.NewReader(tarFile)
for {
header, err := tr.Next()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return err
}
pos, err := tarFile.Seek(0, io.SeekCurrent)
if err != nil {
return err
}
tfs.entries[header.Name] = &entry{
header: header,
pos: pos - blockSize,
}
}
return nil
}
// entryFile represents an entryFile in a tar archive and implements `fs.File`.
type entryFile struct {
io.Reader
io.Closer
header *tar.Header
}
// Stat returns a fs.FileInfo describing e.
func (e *entryFile) Stat() (fs.FileInfo, error) {
return e.header.FileInfo(), nil
}
oras-go-2.5.0/internal/fs/tarfs/tarfs_test.go 0000664 0000000 0000000 00000014675 14576745303 0021170 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 tarfs
import (
"bytes"
"errors"
"io"
"io/fs"
"path/filepath"
"testing"
"oras.land/oras-go/v2/errdef"
)
/*
testdata/test.tar contains:
foobar
foobar_link
foobar_symlink
dir/
hello
subdir/
world
*/
func TestTarFS_Open_Success(t *testing.T) {
testFiles := map[string][]byte{
"foobar": []byte("foobar"),
"dir/hello": []byte("hello"),
"dir/subdir/world": []byte("world"),
}
tarPath := "testdata/test.tar"
tfs, err := New(tarPath)
if err != nil {
t.Fatalf("New() error = %v, wantErr %v", err, nil)
}
tarPathAbs, err := filepath.Abs(tarPath)
if err != nil {
t.Fatal("error calling filepath.Abs(), error =", err)
}
if tfs.path != tarPathAbs {
t.Fatalf("TarFS.path = %s, want %s", tfs.path, tarPathAbs)
}
for name, data := range testFiles {
f, err := tfs.Open(name)
if err != nil {
t.Fatalf("TarFS.Open(%s) error = %v, wantErr %v", name, err, nil)
continue
}
got, err := io.ReadAll(f)
if err != nil {
t.Fatalf("failed to read %s: %v", name, err)
}
if err = f.Close(); err != nil {
t.Errorf("TarFS.Open(%s).Close() error = %v", name, err)
}
if want := data; !bytes.Equal(got, want) {
t.Errorf("TarFS.Open(%s) = %v, want %v", name, string(got), string(want))
}
}
}
func TestTarFS_Open_MoreThanOnce(t *testing.T) {
tfs, err := New("testdata/test.tar")
if err != nil {
t.Fatalf("New() error = %v, wantErr %v", err, nil)
}
name := "foobar"
data := []byte("foobar")
// open once
f1, err := tfs.Open(name)
if err != nil {
t.Fatalf("1st: TarFS.Open(%s) error = %v, wantErr %v", name, err, nil)
}
got, err := io.ReadAll(f1)
if err != nil {
t.Fatalf("1st: failed to read %s: %v", name, err)
}
if want := data; !bytes.Equal(got, want) {
t.Errorf("1st: TarFS.Open(%s) = %v, want %v", name, string(got), string(want))
}
// open twice
f2, err := tfs.Open(name)
if err != nil {
t.Fatalf("2nd: TarFS.Open(%s) error = %v, wantErr %v", name, err, nil)
}
got, err = io.ReadAll(f2)
if err != nil {
t.Fatalf("2nd: failed to read %s: %v", name, err)
}
if want := data; !bytes.Equal(got, want) {
t.Errorf("2nd: TarFS.Open(%s) = %v, want %v", name, string(got), string(want))
}
// close
if err = f1.Close(); err != nil {
t.Errorf("1st TarFS.Open(%s).Close() error = %v", name, err)
}
if err = f2.Close(); err != nil {
t.Errorf("2nd TarFS.Open(%s).Close() error = %v", name, err)
}
}
func TestTarFS_Open_NotExist(t *testing.T) {
testFiles := []string{
"dir/foo",
"subdir/bar",
"barfoo",
}
tfs, err := New("testdata/test.tar")
if err != nil {
t.Fatalf("New() error = %v, wantErr %v", err, nil)
}
for _, name := range testFiles {
_, err := tfs.Open(name)
if want := fs.ErrNotExist; !errors.Is(err, want) {
t.Errorf("TarFS.Open(%s) error = %v, wantErr %v", name, err, want)
}
}
}
func TestTarFS_Open_InvalidPath(t *testing.T) {
testFiles := []string{
"dir/",
"subdir/",
"dir/subdir/",
}
tfs, err := New("testdata/test.tar")
if err != nil {
t.Fatalf("New() error = %v, wantErr %v", err, nil)
}
for _, name := range testFiles {
_, err := tfs.Open(name)
if want := fs.ErrInvalid; !errors.Is(err, want) {
t.Errorf("TarFS.Open(%s) error = %v, wantErr %v", name, err, want)
}
}
}
func TestTarFS_Open_Unsupported(t *testing.T) {
testFiles := []string{
"foobar_link",
"foobar_symlink",
}
tfs, err := New("testdata/test.tar")
if err != nil {
t.Fatalf("New() error = %v, wantErr %v", err, nil)
}
for _, name := range testFiles {
_, err := tfs.Open(name)
if want := errdef.ErrUnsupported; !errors.Is(err, want) {
t.Errorf("TarFS.Open(%s) error = %v, wantErr %v", name, err, want)
}
}
}
func TestTarFS_Stat(t *testing.T) {
tfs, err := New("testdata/test.tar")
if err != nil {
t.Fatalf("New() error = %v, wantErr %v", err, nil)
}
name := "foobar"
fi, err := tfs.Stat(name)
if err != nil {
t.Fatal("Stat() error =", err)
}
if got, want := fi.Name(), "foobar"; got != want {
t.Errorf("Stat().want() = %v, want %v", got, want)
}
if got, want := fi.Size(), int64(6); got != want {
t.Errorf("Stat().Size() = %v, want %v", got, want)
}
name = "dir/hello"
fi, err = tfs.Stat(name)
if err != nil {
t.Fatal("Stat() error =", err)
}
if got, want := fi.Name(), "hello"; got != want {
t.Errorf("Stat().want() = %v, want %v", got, want)
}
if got, want := fi.Size(), int64(5); got != want {
t.Errorf("Stat().Size() = %v, want %v", got, want)
}
name = "dir/subdir/world"
fi, err = tfs.Stat(name)
if err != nil {
t.Fatal("Stat() error =", err)
}
if got, want := fi.Name(), "world"; got != want {
t.Errorf("Stat().want() = %v, want %v", got, want)
}
if got, want := fi.Size(), int64(5); got != want {
t.Errorf("Stat().Size() = %v, want %v", got, want)
}
}
func TestTarFS_Stat_NotExist(t *testing.T) {
testFiles := []string{
"dir/foo",
"subdir/bar",
"barfoo",
}
tfs, err := New("testdata/test.tar")
if err != nil {
t.Fatalf("New() error = %v, wantErr %v", err, nil)
}
for _, name := range testFiles {
_, err := tfs.Stat(name)
if want := fs.ErrNotExist; !errors.Is(err, want) {
t.Errorf("TarFS.Stat(%s) error = %v, wantErr %v", name, err, want)
}
}
}
func TestTarFS_Stat_InvalidPath(t *testing.T) {
testFiles := []string{
"dir/",
"subdir/",
"dir/subdir/",
}
tfs, err := New("testdata/test.tar")
if err != nil {
t.Fatalf("New() error = %v, wantErr %v", err, nil)
}
for _, name := range testFiles {
_, err := tfs.Stat(name)
if want := fs.ErrInvalid; !errors.Is(err, want) {
t.Errorf("TarFS.Stat(%s) error = %v, wantErr %v", name, err, want)
}
}
}
func TestTarFS_Stat_Unsupported(t *testing.T) {
testFiles := []string{
"foobar_link",
"foobar_symlink",
}
tfs, err := New("testdata/test.tar")
if err != nil {
t.Fatalf("New() error = %v, wantErr %v", err, nil)
}
for _, name := range testFiles {
_, err := tfs.Stat(name)
if want := errdef.ErrUnsupported; !errors.Is(err, want) {
t.Errorf("TarFS.Stat(%s) error = %v, wantErr %v", name, err, want)
}
}
}
oras-go-2.5.0/internal/fs/tarfs/testdata/ 0000775 0000000 0000000 00000000000 14576745303 0020257 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/internal/fs/tarfs/testdata/test.tar 0000664 0000000 0000000 00000024000 14576745303 0021742 0 ustar 00root root 0000000 0000000 dir/ 0000777 0001750 0001750 00000000000 14343624137 011205 5 ustar lixlei lixlei dir/hello 0000777 0001750 0001750 00000000005 14343624133 012225 0 ustar lixlei lixlei hello dir/subdir/ 0000777 0001750 0001750 00000000000 14343624150 012470 5 ustar lixlei lixlei dir/subdir/world 0000777 0001750 0001750 00000000005 14343624153 013543 0 ustar lixlei lixlei world foobar 0000777 0001750 0001750 00000000006 14343623722 011620 0 ustar lixlei lixlei foobar foobar_link 0000777 0001750 0001750 00000000000 14343623722 014021 1foobar ustar lixlei lixlei foobar_symlink 0000777 0001750 0001750 00000000000 14343623753 014557 2foobar ustar lixlei lixlei oras-go-2.5.0/internal/graph/ 0000775 0000000 0000000 00000000000 14576745303 0016020 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/internal/graph/memory.go 0000664 0000000 0000000 00000015031 14576745303 0017657 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 graph
import (
"context"
"errors"
"sync"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/internal/container/set"
"oras.land/oras-go/v2/internal/descriptor"
"oras.land/oras-go/v2/internal/status"
"oras.land/oras-go/v2/internal/syncutil"
)
// Memory is a memory based PredecessorFinder.
type Memory struct {
// nodes has the following properties and behaviors:
// 1. a node exists in Memory.nodes if and only if it exists in the memory
// 2. Memory.nodes saves the ocispec.Descriptor map keys, which are used by
// the other fields.
nodes map[descriptor.Descriptor]ocispec.Descriptor
// predecessors has the following properties and behaviors:
// 1. a node exists in Memory.predecessors if it has at least one predecessor
// in the memory, regardless of whether or not the node itself exists in
// the memory.
// 2. a node does not exist in Memory.predecessors, if it doesn't have any predecessors
// in the memory.
predecessors map[descriptor.Descriptor]set.Set[descriptor.Descriptor]
// successors has the following properties and behaviors:
// 1. a node exists in Memory.successors if and only if it exists in the memory.
// 2. a node's entry in Memory.successors is always consistent with the actual
// content of the node, regardless of whether or not each successor exists
// in the memory.
successors map[descriptor.Descriptor]set.Set[descriptor.Descriptor]
lock sync.RWMutex
}
// NewMemory creates a new memory PredecessorFinder.
func NewMemory() *Memory {
return &Memory{
nodes: make(map[descriptor.Descriptor]ocispec.Descriptor),
predecessors: make(map[descriptor.Descriptor]set.Set[descriptor.Descriptor]),
successors: make(map[descriptor.Descriptor]set.Set[descriptor.Descriptor]),
}
}
// Index indexes predecessors for each direct successor of the given node.
func (m *Memory) Index(ctx context.Context, fetcher content.Fetcher, node ocispec.Descriptor) error {
_, err := m.index(ctx, fetcher, node)
return err
}
// Index indexes predecessors for all the successors of the given node.
func (m *Memory) IndexAll(ctx context.Context, fetcher content.Fetcher, node ocispec.Descriptor) error {
// track content status
tracker := status.NewTracker()
var fn syncutil.GoFunc[ocispec.Descriptor]
fn = func(ctx context.Context, region *syncutil.LimitedRegion, desc ocispec.Descriptor) error {
// skip the node if other go routine is working on it
_, committed := tracker.TryCommit(desc)
if !committed {
return nil
}
successors, err := m.index(ctx, fetcher, desc)
if err != nil {
if errors.Is(err, errdef.ErrNotFound) {
// skip the node if it does not exist
return nil
}
return err
}
if len(successors) > 0 {
// traverse and index successors
return syncutil.Go(ctx, nil, fn, successors...)
}
return nil
}
return syncutil.Go(ctx, nil, fn, node)
}
// Predecessors returns the nodes directly pointing to the current node.
// Predecessors returns nil without error if the node does not exists in the
// store. Like other operations, calling Predecessors() is go-routine safe.
// However, it does not necessarily correspond to any consistent snapshot of
// the stored contents.
func (m *Memory) Predecessors(_ context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) {
m.lock.RLock()
defer m.lock.RUnlock()
key := descriptor.FromOCI(node)
set, exists := m.predecessors[key]
if !exists {
return nil, nil
}
var res []ocispec.Descriptor
for k := range set {
res = append(res, m.nodes[k])
}
return res, nil
}
// Remove removes the node from its predecessors and successors, and returns the
// dangling root nodes caused by the deletion.
func (m *Memory) Remove(node ocispec.Descriptor) []ocispec.Descriptor {
m.lock.Lock()
defer m.lock.Unlock()
nodeKey := descriptor.FromOCI(node)
var danglings []ocispec.Descriptor
// remove the node from its successors' predecessor list
for successorKey := range m.successors[nodeKey] {
predecessorEntry := m.predecessors[successorKey]
predecessorEntry.Delete(nodeKey)
// if none of the predecessors of the node still exists, we remove the
// predecessors entry and return it as a dangling node. Otherwise, we do
// not remove the entry.
if len(predecessorEntry) == 0 {
delete(m.predecessors, successorKey)
if _, exists := m.nodes[successorKey]; exists {
danglings = append(danglings, m.nodes[successorKey])
}
}
}
delete(m.successors, nodeKey)
delete(m.nodes, nodeKey)
return danglings
}
// DigestSet returns the set of node digest in memory.
func (m *Memory) DigestSet() set.Set[digest.Digest] {
m.lock.RLock()
defer m.lock.RUnlock()
s := set.New[digest.Digest]()
for desc := range m.nodes {
s.Add(desc.Digest)
}
return s
}
// index indexes predecessors for each direct successor of the given node.
func (m *Memory) index(ctx context.Context, fetcher content.Fetcher, node ocispec.Descriptor) ([]ocispec.Descriptor, error) {
successors, err := content.Successors(ctx, fetcher, node)
if err != nil {
return nil, err
}
m.lock.Lock()
defer m.lock.Unlock()
// index the node
nodeKey := descriptor.FromOCI(node)
m.nodes[nodeKey] = node
// for each successor, put it into the node's successors list, and
// put node into the succeesor's predecessors list
successorSet := set.New[descriptor.Descriptor]()
m.successors[nodeKey] = successorSet
for _, successor := range successors {
successorKey := descriptor.FromOCI(successor)
successorSet.Add(successorKey)
predecessorSet, exists := m.predecessors[successorKey]
if !exists {
predecessorSet = set.New[descriptor.Descriptor]()
m.predecessors[successorKey] = predecessorSet
}
predecessorSet.Add(nodeKey)
}
return successors, nil
}
// Exists checks if the node exists in the graph
func (m *Memory) Exists(node ocispec.Descriptor) bool {
m.lock.RLock()
defer m.lock.RUnlock()
nodeKey := descriptor.FromOCI(node)
_, exists := m.nodes[nodeKey]
return exists
}
oras-go-2.5.0/internal/graph/memory_test.go 0000664 0000000 0000000 00000066473 14576745303 0020736 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 graph
import (
"bytes"
"context"
"encoding/json"
"io"
"reflect"
"testing"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/internal/cas"
"oras.land/oras-go/v2/internal/descriptor"
)
// +------------------------------+
// | |
// | +-----------+ |
// | |A(manifest)| |
// | +-----+-----+ |
// | | |
// | +------------+ |
// | | | |
// | v v |
// | +-----+-----+ +---+----+ |
// | |B(manifest)| |C(layer)| |
// | +-----+-----+ +--------+ |
// | | |
// | v |
// | +---+----+ |
// | |D(layer)| |
// | +--------+ |
// | |
// |------------------------------+
func TestMemory_IndexAndRemove(t *testing.T) {
testFetcher := cas.NewMemory()
testMemory := NewMemory()
ctx := context.Background()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) ocispec.Descriptor {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
return descs[len(descs)-1]
}
generateManifest := func(layers ...ocispec.Descriptor) ocispec.Descriptor {
manifest := ocispec.Manifest{
Config: ocispec.Descriptor{MediaType: "test config"},
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
return appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
descC := appendBlob("layer node C", []byte("Node C is a layer")) // blobs[0], layer "C"
descD := appendBlob("layer node D", []byte("Node D is a layer")) // blobs[1], layer "D"
descB := generateManifest(descs[0:2]...) // blobs[2], manifest "B"
descA := generateManifest(descs[1:3]...) // blobs[3], manifest "A"
// prepare the content in the fetcher, so that it can be used to test Index
testContents := []ocispec.Descriptor{descC, descD, descB, descA}
for i := 0; i < len(blobs); i++ {
testFetcher.Push(ctx, testContents[i], bytes.NewReader(blobs[i]))
}
// make sure that testFetcher works
rc, err := testFetcher.Fetch(ctx, descA)
if err != nil {
t.Errorf("testFetcher.Fetch() error = %v", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Errorf("testFetcher.Fetch().Read() error = %v", err)
}
err = rc.Close()
if err != nil {
t.Errorf("testFetcher.Fetch().Close() error = %v", err)
}
if !bytes.Equal(got, blobs[3]) {
t.Errorf("testFetcher.Fetch() = %v, want %v", got, blobs[4])
}
nodeKeyA := descriptor.FromOCI(descA)
nodeKeyB := descriptor.FromOCI(descB)
nodeKeyC := descriptor.FromOCI(descC)
nodeKeyD := descriptor.FromOCI(descD)
// index and check the information of node D
testMemory.Index(ctx, testFetcher, descD)
// 1. verify its existence in testMemory.nodes
if _, exists := testMemory.nodes[nodeKeyD]; !exists {
t.Errorf("nodes entry of %s should exist", "D")
}
// 2. verify that the entry of D exists in testMemory.successors and it's empty
successorsD, exists := testMemory.successors[nodeKeyD]
if !exists {
t.Errorf("successor entry of %s should exist", "D")
}
if successorsD == nil {
t.Errorf("successors of %s should be an empty set, not nil", "D")
}
if len(successorsD) != 0 {
t.Errorf("successors of %s should be empty", "D")
}
// 3. there should be no entry of D in testMemory.predecessors yet
_, exists = testMemory.predecessors[nodeKeyD]
if exists {
t.Errorf("predecessor entry of %s should not exist yet", "D")
}
// index and check the information of node C
testMemory.Index(ctx, testFetcher, descC)
// 1. verify its existence in memory.nodes
if _, exists := testMemory.nodes[nodeKeyC]; !exists {
t.Errorf("nodes entry of %s should exist", "C")
}
// 2. verify that the entry of C exists in testMemory.successors and it's empty
successorsC, exists := testMemory.successors[nodeKeyC]
if !exists {
t.Errorf("successor entry of %s should exist", "C")
}
if successorsC == nil {
t.Errorf("successors of %s should be an empty set, not nil", "C")
}
if len(successorsC) != 0 {
t.Errorf("successors of %s should be empty", "C")
}
// 3. there should be no entry of C in testMemory.predecessors yet
_, exists = testMemory.predecessors[nodeKeyC]
if exists {
t.Errorf("predecessor entry of %s should not exist yet", "C")
}
// index and check the information of node A
testMemory.Index(ctx, testFetcher, descA)
// 1. verify its existence in testMemory.nodes
if _, exists := testMemory.nodes[nodeKeyA]; !exists {
t.Errorf("nodes entry of %s should exist", "A")
}
// 2. verify that the entry of A exists in testMemory.successors and it contains
// node B and node D
successorsA, exists := testMemory.successors[nodeKeyA]
if !exists {
t.Errorf("successor entry of %s should exist", "A")
}
if successorsA == nil {
t.Errorf("successors of %s should be a set, not nil", "A")
}
if !successorsA.Contains(nodeKeyB) {
t.Errorf("successors of %s should contain %s", "A", "B")
}
if !successorsA.Contains(nodeKeyD) {
t.Errorf("successors of %s should contain %s", "A", "D")
}
// 3. verify that node A exists in the predecessors lists of its successors.
// there should be an entry of D in testMemory.predecessors by now and it
// should contain A but not B
predecessorsD, exists := testMemory.predecessors[nodeKeyD]
if !exists {
t.Errorf("predecessor entry of %s should exist by now", "D")
}
if !predecessorsD.Contains(nodeKeyA) {
t.Errorf("predecessors of %s should contain %s", "D", "A")
}
if predecessorsD.Contains(nodeKeyB) {
t.Errorf("predecessors of %s should not contain %s yet", "D", "B")
}
// there should be an entry of B in testMemory.predecessors now
// and it should contain A
predecessorsB, exists := testMemory.predecessors[nodeKeyB]
if !exists {
t.Errorf("predecessor entry of %s should exist by now", "B")
}
if !predecessorsB.Contains(nodeKeyA) {
t.Errorf("predecessors of %s should contain %s", "B", "A")
}
// 4. there should be no entry of A in testMemory.predecessors
_, exists = testMemory.predecessors[nodeKeyA]
if exists {
t.Errorf("predecessor entry of %s should not exist", "A")
}
// index and check the information of node B
testMemory.Index(ctx, testFetcher, descB)
// 1. verify its existence in testMemory.nodes
if _, exists := testMemory.nodes[nodeKeyB]; !exists {
t.Errorf("nodes entry of %s should exist", "B")
}
// 2. verify that the entry of B exists in testMemory.successors and it contains
// node C and node D
successorsB, exists := testMemory.successors[nodeKeyB]
if !exists {
t.Errorf("successor entry of %s should exist", "B")
}
if successorsB == nil {
t.Errorf("successors of %s should be a set, not nil", "B")
}
if !successorsB.Contains(nodeKeyC) {
t.Errorf("successors of %s should contain %s", "B", "C")
}
if !successorsB.Contains(nodeKeyD) {
t.Errorf("successors of %s should contain %s", "B", "D")
}
// 3. verify that node B exists in the predecessors lists of its successors.
// there should be an entry of C in testMemory.predecessors by now
// and it should contain B
predecessorsC, exists := testMemory.predecessors[nodeKeyC]
if !exists {
t.Errorf("predecessor entry of %s should exist by now", "C")
}
if !predecessorsC.Contains(nodeKeyB) {
t.Errorf("predecessors of %s should contain %s", "C", "B")
}
// predecessors of D should have been updated now to have node A and B
if !predecessorsD.Contains(nodeKeyB) {
t.Errorf("predecessors of %s should contain %s", "D", "B")
}
if !predecessorsD.Contains(nodeKeyA) {
t.Errorf("predecessors of %s should contain %s", "D", "A")
}
// remove node B and check the stored information
testMemory.Remove(descB)
// 1. verify that node B no longer exists in testMemory.nodes
if _, exists := testMemory.nodes[nodeKeyB]; exists {
t.Errorf("nodes entry of %s should no longer exist", "B")
}
// 2. verify B' predecessors info: B's entry in testMemory.predecessors should
// still exist, since its predecessor A still exists
predecessorsB, exists = testMemory.predecessors[nodeKeyB]
if !exists {
t.Errorf("testDeletableMemory.predecessors should still contain the entry of %s", "B")
}
if !predecessorsB.Contains(nodeKeyA) {
t.Errorf("predecessors of %s should still contain %s", "B", "A")
}
// 3. verify B' successors info: B's entry in testMemory.successors should no
// longer exist
if _, exists := testMemory.successors[nodeKeyB]; exists {
t.Errorf("testDeletableMemory.successors should not contain the entry of %s", "B")
}
// 4. verify B' predecessors' successors info: B should still exist in A's
// successors
if !successorsA.Contains(nodeKeyB) {
t.Errorf("successors of %s should still contain %s", "A", "B")
}
// 5. verify B' successors' predecessors info: C's entry in testMemory.predecessors
// should no longer exist, since C's only predecessor B is already deleted
if _, exists = testMemory.predecessors[nodeKeyC]; exists {
t.Errorf("predecessor entry of %s should no longer exist by now, since all its predecessors have been deleted", "C")
}
// B should no longer exist in D's predecessors
if predecessorsD.Contains(nodeKeyB) {
t.Errorf("predecessors of %s should not contain %s", "D", "B")
}
// but A still exists in D's predecessors
if !predecessorsD.Contains(nodeKeyA) {
t.Errorf("predecessors of %s should still contain %s", "D", "A")
}
// remove node A and check the stored information
testMemory.Remove(descA)
// 1. verify that node A no longer exists in testMemory.nodes
if _, exists := testMemory.nodes[nodeKeyA]; exists {
t.Errorf("nodes entry of %s should no longer exist", "A")
}
// 2. verify A' successors info: A's entry in testMemory.successors should no
// longer exist
if _, exists := testMemory.successors[nodeKeyA]; exists {
t.Errorf("testDeletableMemory.successors should not contain the entry of %s", "A")
}
// 3. verify A' successors' predecessors info: D's entry in testMemory.predecessors
// should no longer exist, since all predecessors of D are already deleted
if _, exists = testMemory.predecessors[nodeKeyD]; exists {
t.Errorf("predecessor entry of %s should no longer exist by now, since all its predecessors have been deleted", "D")
}
// B's entry in testMemory.predecessors should no longer exist, since B's only
// predecessor A is already deleted
if _, exists = testMemory.predecessors[nodeKeyB]; exists {
t.Errorf("predecessor entry of %s should no longer exist by now, since all its predecessors have been deleted", "B")
}
}
// +-----------------------------------------------+
// | |
// | +--------+ |
// | |A(index)| |
// | +---+----+ |
// | | |
// | -+--------------+--------------+- |
// | | | | |
// | +-----v-----+ +-----v-----+ +-----v-----+ |
// | |B(manifest)| |C(manifest)| |D(manifest)| |
// | +--------+--+ ++---------++ +--+--------+ |
// | | | | | |
// | | | | | |
// | v v v v |
// | ++------++ ++------++ |
// | |E(layer)| |F(layer)| |
// | +--------+ +--------+ |
// | |
// +-----------------------------------------------+
func TestMemory_IndexAllAndPredecessors(t *testing.T) {
testFetcher := cas.NewMemory()
testMemory := NewMemory()
ctx := context.Background()
// generate test content
var blobs [][]byte
var descriptors []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) ocispec.Descriptor {
blobs = append(blobs, blob)
descriptors = append(descriptors, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
return descriptors[len(descriptors)-1]
}
generateManifest := func(layers ...ocispec.Descriptor) ocispec.Descriptor {
manifest := ocispec.Manifest{
Config: ocispec.Descriptor{MediaType: "test config"},
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
return appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(manifests ...ocispec.Descriptor) ocispec.Descriptor {
index := ocispec.Index{
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
return appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
descE := appendBlob("layer node E", []byte("Node E is a layer")) // blobs[0], layer "E"
descF := appendBlob("layer node F", []byte("Node F is a layer")) // blobs[1], layer "F"
descB := generateManifest(descriptors[0:1]...) // blobs[2], manifest "B"
descC := generateManifest(descriptors[0:2]...) // blobs[3], manifest "C"
descD := generateManifest(descriptors[1:2]...) // blobs[4], manifest "D"
descA := generateIndex(descriptors[2:5]...) // blobs[5], index "A"
// prepare the content in the fetcher, so that it can be used to test IndexAll
testContents := []ocispec.Descriptor{descE, descF, descB, descC, descD, descA}
for i := 0; i < len(blobs); i++ {
testFetcher.Push(ctx, testContents[i], bytes.NewReader(blobs[i]))
}
// make sure that testFetcher works
rc, err := testFetcher.Fetch(ctx, descA)
if err != nil {
t.Errorf("testFetcher.Fetch() error = %v", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Errorf("testFetcher.Fetch().Read() error = %v", err)
}
err = rc.Close()
if err != nil {
t.Errorf("testFetcher.Fetch().Close() error = %v", err)
}
if !bytes.Equal(got, blobs[5]) {
t.Errorf("testFetcher.Fetch() = %v, want %v", got, blobs[4])
}
nodeKeyA := descriptor.FromOCI(descA)
nodeKeyB := descriptor.FromOCI(descB)
nodeKeyC := descriptor.FromOCI(descC)
nodeKeyD := descriptor.FromOCI(descD)
nodeKeyE := descriptor.FromOCI(descE)
nodeKeyF := descriptor.FromOCI(descF)
// index node A into testMemory using IndexAll
testMemory.IndexAll(ctx, testFetcher, descA)
// check the information of node A
// 1. verify that node A exists in testMemory.nodes
if _, exists := testMemory.nodes[nodeKeyA]; !exists {
t.Errorf("nodes entry of %s should exist", "A")
}
// 2. verify that there is no entry of A in predecessors
if _, exists := testMemory.predecessors[nodeKeyA]; exists {
t.Errorf("there should be no entry of %s in predecessors", "A")
}
// 3. verify that A has successors B, C, D
successorsA, exists := testMemory.successors[nodeKeyA]
if !exists {
t.Errorf("there should be an entry of %s in successors", "A")
}
if !successorsA.Contains(nodeKeyB) {
t.Errorf("successors of %s should contain %s", "A", "B")
}
if !successorsA.Contains(nodeKeyC) {
t.Errorf("successors of %s should contain %s", "A", "C")
}
if !successorsA.Contains(nodeKeyD) {
t.Errorf("successors of %s should contain %s", "A", "D")
}
// check the information of node B
// 1. verify that node B exists in testMemory.nodes
if _, exists := testMemory.nodes[nodeKeyB]; !exists {
t.Errorf("nodes entry of %s should exist", "B")
}
// 2. verify that B has node A in its predecessors
predecessorsB := testMemory.predecessors[nodeKeyB]
if !predecessorsB.Contains(nodeKeyA) {
t.Errorf("predecessors of %s should contain %s", "B", "A")
}
// 3. verify that B has node E in its successors
successorsB := testMemory.successors[nodeKeyB]
if !successorsB.Contains(nodeKeyE) {
t.Errorf("successors of %s should contain %s", "B", "E")
}
// check the information of node C
// 1. verify that node C exists in testMemory.nodes
if _, exists := testMemory.nodes[nodeKeyC]; !exists {
t.Errorf("nodes entry of %s should exist", "C")
}
// 2. verify that C has node A in its predecessors
predecessorsC := testMemory.predecessors[nodeKeyC]
if !predecessorsC.Contains(nodeKeyA) {
t.Errorf("predecessors of %s should contain %s", "C", "A")
}
// 3. verify that C has node E and F in its successors
successorsC := testMemory.successors[nodeKeyC]
if !successorsC.Contains(nodeKeyE) {
t.Errorf("successors of %s should contain %s", "C", "E")
}
if !successorsC.Contains(nodeKeyF) {
t.Errorf("successors of %s should contain %s", "C", "F")
}
// check the information of node D
// 1. verify that node D exists in testMemory.nodes
if _, exists := testMemory.nodes[nodeKeyD]; !exists {
t.Errorf("nodes entry of %s should exist", "D")
}
// 2. verify that D has node A in its predecessors
predecessorsD := testMemory.predecessors[nodeKeyD]
if !predecessorsD.Contains(nodeKeyA) {
t.Errorf("predecessors of %s should contain %s", "D", "A")
}
// 3. verify that D has node F in its successors
successorsD := testMemory.successors[nodeKeyD]
if !successorsD.Contains(nodeKeyF) {
t.Errorf("successors of %s should contain %s", "D", "F")
}
// check the information of node E
// 1. verify that node E exists in testMemory.nodes
if _, exists := testMemory.nodes[nodeKeyE]; !exists {
t.Errorf("nodes entry of %s should exist", "E")
}
// 2. verify that E has node B and C in its predecessors
predecessorsE := testMemory.predecessors[nodeKeyE]
if !predecessorsE.Contains(nodeKeyB) {
t.Errorf("predecessors of %s should contain %s", "E", "B")
}
if !predecessorsE.Contains(nodeKeyC) {
t.Errorf("predecessors of %s should contain %s", "E", "C")
}
// 3. verify that E has an entry in successors and it's empty
successorsE, exists := testMemory.successors[nodeKeyE]
if !exists {
t.Errorf("entry %s should exist in testMemory.successors", "E")
}
if successorsE == nil {
t.Errorf("successors of %s should be an empty set, not nil", "E")
}
if len(successorsE) != 0 {
t.Errorf("successors of %s should be empty", "E")
}
// check the information of node F
// 1. verify that node F exists in testMemory.nodes
if _, exists := testMemory.nodes[nodeKeyF]; !exists {
t.Errorf("nodes entry of %s should exist", "F")
}
// 2. verify that F has node C and D in its predecessors
predecessorsF := testMemory.predecessors[nodeKeyF]
if !predecessorsF.Contains(nodeKeyC) {
t.Errorf("predecessors of %s should contain %s", "F", "C")
}
if !predecessorsF.Contains(nodeKeyD) {
t.Errorf("predecessors of %s should contain %s", "F", "D")
}
// 3. verify that F has an entry in successors and it's empty
successorsF, exists := testMemory.successors[nodeKeyF]
if !exists {
t.Errorf("entry %s should exist in testMemory.successors", "F")
}
if successorsF == nil {
t.Errorf("successors of %s should be an empty set, not nil", "F")
}
if len(successorsF) != 0 {
t.Errorf("successors of %s should be empty", "F")
}
// check that the Predecessors of node C is node A
predsC, err := testMemory.Predecessors(ctx, descC)
if err != nil {
t.Errorf("testFetcher.Predecessors() error = %v", err)
}
expectedLength := 1
if len(predsC) != expectedLength {
t.Errorf("%s should have length %d", "predsC", expectedLength)
}
if !reflect.DeepEqual(predsC[0], descA) {
t.Errorf("incorrect predecessor result")
}
// check that the Predecessors of node F are node C and node D
predsF, err := testMemory.Predecessors(ctx, descF)
if err != nil {
t.Errorf("testFetcher.Predecessors() error = %v", err)
}
expectedLength = 2
if len(predsF) != expectedLength {
t.Errorf("%s should have length %d", "predsF", expectedLength)
}
for _, pred := range predsF {
if !reflect.DeepEqual(pred, descC) && !reflect.DeepEqual(pred, descD) {
t.Errorf("incorrect predecessor results")
}
}
// remove node C and check the stored information
testMemory.Remove(descC)
if predecessorsE.Contains(nodeKeyC) {
t.Errorf("predecessors of %s should not contain %s", "E", "C")
}
if predecessorsF.Contains(nodeKeyC) {
t.Errorf("predecessors of %s should not contain %s", "F", "C")
}
if !successorsA.Contains(nodeKeyC) {
t.Errorf("successors of %s should still contain %s", "A", "C")
}
if _, exists := testMemory.successors[nodeKeyC]; exists {
t.Errorf("testMemory.successors should not contain the entry of %s", "C")
}
if _, exists := testMemory.predecessors[nodeKeyC]; !exists {
t.Errorf("entry %s in predecessors should still exists since it still has at least one predecessor node present", "C")
}
// remove node A and check the stored information
testMemory.Remove(descA)
if _, exists := testMemory.predecessors[nodeKeyB]; exists {
t.Errorf("entry %s in predecessors should no longer exists", "B")
}
if _, exists := testMemory.predecessors[nodeKeyC]; exists {
t.Errorf("entry %s in predecessors should no longer exists", "C")
}
if _, exists := testMemory.predecessors[nodeKeyD]; exists {
t.Errorf("entry %s in predecessors should no longer exists", "D")
}
if _, exists := testMemory.successors[nodeKeyA]; exists {
t.Errorf("testDeletableMemory.successors should not contain the entry of %s", "A")
}
// check that the Predecessors of node D is empty
predsD, err := testMemory.Predecessors(ctx, descD)
if err != nil {
t.Errorf("testFetcher.Predecessors() error = %v", err)
}
if predsD != nil {
t.Errorf("%s should be nil", "predsD")
}
// check that the Predecessors of node E is node B
predsE, err := testMemory.Predecessors(ctx, descE)
if err != nil {
t.Errorf("testFetcher.Predecessors() error = %v", err)
}
expectedLength = 1
if len(predsE) != expectedLength {
t.Errorf("%s should have length %d", "predsE", expectedLength)
}
if !reflect.DeepEqual(predsE[0], descB) {
t.Errorf("incorrect predecessor result")
}
}
func TestMemory_DigestSet(t *testing.T) {
testFetcher := cas.NewMemory()
testMemory := NewMemory()
ctx := context.Background()
// generate test content
var blobs [][]byte
var descriptors []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) ocispec.Descriptor {
blobs = append(blobs, blob)
descriptors = append(descriptors, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
return descriptors[len(descriptors)-1]
}
generateManifest := func(layers ...ocispec.Descriptor) ocispec.Descriptor {
manifest := ocispec.Manifest{
Config: ocispec.Descriptor{MediaType: "test config"},
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
return appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(manifests ...ocispec.Descriptor) ocispec.Descriptor {
index := ocispec.Index{
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
return appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
descE := appendBlob("layer node E", []byte("Node E is a layer")) // blobs[0], layer "E"
descF := appendBlob("layer node F", []byte("Node F is a layer")) // blobs[1], layer "F"
descB := generateManifest(descriptors[0:1]...) // blobs[2], manifest "B"
descC := generateManifest(descriptors[0:2]...) // blobs[3], manifest "C"
descD := generateManifest(descriptors[1:2]...) // blobs[4], manifest "D"
descA := generateIndex(descriptors[2:5]...) // blobs[5], index "A"
// prepare the content in the fetcher, so that it can be used to test IndexAll
testContents := []ocispec.Descriptor{descE, descF, descB, descC, descD, descA}
for i := 0; i < len(blobs); i++ {
testFetcher.Push(ctx, testContents[i], bytes.NewReader(blobs[i]))
}
// make sure that testFetcher works
rc, err := testFetcher.Fetch(ctx, descA)
if err != nil {
t.Errorf("testFetcher.Fetch() error = %v", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Errorf("testFetcher.Fetch().Read() error = %v", err)
}
err = rc.Close()
if err != nil {
t.Errorf("testFetcher.Fetch().Close() error = %v", err)
}
if !bytes.Equal(got, blobs[5]) {
t.Errorf("testFetcher.Fetch() = %v, want %v", got, blobs[4])
}
// index node A into testMemory using IndexAll
testMemory.IndexAll(ctx, testFetcher, descA)
digestSet := testMemory.DigestSet()
for i := 0; i < len(blobs); i++ {
if exists := digestSet.Contains(descriptors[i].Digest); exists != true {
t.Errorf("digest of blob[%d] should exist in digestSet", i)
}
}
}
func TestMemory_Exists(t *testing.T) {
testFetcher := cas.NewMemory()
testMemory := NewMemory()
ctx := context.Background()
// generate test content
var blobs [][]byte
var descriptors []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) ocispec.Descriptor {
blobs = append(blobs, blob)
descriptors = append(descriptors, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
return descriptors[len(descriptors)-1]
}
generateManifest := func(layers ...ocispec.Descriptor) ocispec.Descriptor {
manifest := ocispec.Manifest{
Config: ocispec.Descriptor{MediaType: "test config"},
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
return appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(manifests ...ocispec.Descriptor) ocispec.Descriptor {
index := ocispec.Index{
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
return appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
descE := appendBlob("layer node E", []byte("Node E is a layer")) // blobs[0], layer "E"
descF := appendBlob("layer node F", []byte("Node F is a layer")) // blobs[1], layer "F"
descB := generateManifest(descriptors[0:1]...) // blobs[2], manifest "B"
descC := generateManifest(descriptors[0:2]...) // blobs[3], manifest "C"
descD := generateManifest(descriptors[1:2]...) // blobs[4], manifest "D"
descA := generateIndex(descriptors[2:5]...) // blobs[5], index "A"
// prepare the content in the fetcher, so that it can be used to test IndexAll
testContents := []ocispec.Descriptor{descE, descF, descB, descC, descD, descA}
for i := 0; i < len(blobs); i++ {
testFetcher.Push(ctx, testContents[i], bytes.NewReader(blobs[i]))
}
// make sure that testFetcher works
rc, err := testFetcher.Fetch(ctx, descA)
if err != nil {
t.Errorf("testFetcher.Fetch() error = %v", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Errorf("testFetcher.Fetch().Read() error = %v", err)
}
err = rc.Close()
if err != nil {
t.Errorf("testFetcher.Fetch().Close() error = %v", err)
}
if !bytes.Equal(got, blobs[5]) {
t.Errorf("testFetcher.Fetch() = %v, want %v", got, blobs[4])
}
// index node A into testMemory using IndexAll
testMemory.IndexAll(ctx, testFetcher, descA)
for i := 0; i < len(blobs); i++ {
if exists := testMemory.Exists(descriptors[i]); exists != true {
t.Errorf("digest of blob[%d] should exist in digestSet", i)
}
}
}
oras-go-2.5.0/internal/httputil/ 0000775 0000000 0000000 00000000000 14576745303 0016574 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/internal/httputil/seek.go 0000664 0000000 0000000 00000005734 14576745303 0020063 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 httputil
import (
"errors"
"fmt"
"io"
"net/http"
)
// Client is an interface for a HTTP client.
// This interface is defined inside this package to prevent potential import
// loop.
type Client interface {
// Do sends an HTTP request and returns an HTTP response.
Do(*http.Request) (*http.Response, error)
}
// readSeekCloser seeks http body by starting new connections.
type readSeekCloser struct {
client Client
req *http.Request
rc io.ReadCloser
size int64
offset int64
closed bool
}
// NewReadSeekCloser returns a seeker to make the HTTP response seekable.
// Callers should ensure that the server supports Range request.
func NewReadSeekCloser(client Client, req *http.Request, respBody io.ReadCloser, size int64) io.ReadSeekCloser {
return &readSeekCloser{
client: client,
req: req,
rc: respBody,
size: size,
}
}
// Read reads the content body and counts offset.
func (rsc *readSeekCloser) Read(p []byte) (n int, err error) {
if rsc.closed {
return 0, errors.New("read: already closed")
}
n, err = rsc.rc.Read(p)
rsc.offset += int64(n)
return
}
// Seek starts a new connection to the remote for reading if position changes.
func (rsc *readSeekCloser) Seek(offset int64, whence int) (int64, error) {
if rsc.closed {
return 0, errors.New("seek: already closed")
}
switch whence {
case io.SeekCurrent:
offset += rsc.offset
case io.SeekStart:
// no-op
case io.SeekEnd:
offset += rsc.size
default:
return 0, errors.New("seek: invalid whence")
}
if offset < 0 {
return 0, errors.New("seek: an attempt was made to move the pointer before the beginning of the content")
}
if offset == rsc.offset {
return offset, nil
}
if offset >= rsc.size {
rsc.rc.Close()
rsc.rc = http.NoBody
rsc.offset = offset
return offset, nil
}
req := rsc.req.Clone(rsc.req.Context())
req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", offset, rsc.size-1))
resp, err := rsc.client.Do(req)
if err != nil {
return 0, fmt.Errorf("seek: %s %q: %w", req.Method, req.URL, err)
}
if resp.StatusCode != http.StatusPartialContent {
resp.Body.Close()
return 0, fmt.Errorf("seek: %s %q: unexpected status code %d", resp.Request.Method, resp.Request.URL, resp.StatusCode)
}
rsc.rc.Close()
rsc.rc = resp.Body
rsc.offset = offset
return offset, nil
}
// Close closes the content body.
func (rsc *readSeekCloser) Close() error {
if rsc.closed {
return nil
}
rsc.closed = true
return rsc.rc.Close()
}
oras-go-2.5.0/internal/httputil/seek_test.go 0000664 0000000 0000000 00000012144 14576745303 0021113 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 httputil
import (
"bytes"
"fmt"
"io"
"math"
"net/http"
"net/http/httptest"
"testing"
)
func Test_readSeekCloser_Read(t *testing.T) {
content := []byte("hello world")
path := "/testpath"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != path {
w.WriteHeader(http.StatusNotFound)
return
}
if _, err := w.Write(content); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
}))
defer ts.Close()
client := ts.Client()
resp, err := client.Get(ts.URL + path)
if err != nil {
t.Fatalf("failed to do request: %v", err)
}
rsc := NewReadSeekCloser(client, resp.Request, resp.Body, int64(len(content)))
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(rsc); err != nil {
t.Errorf("fail to read: %v", err)
}
if got := buf.Bytes(); !bytes.Equal(got, content) {
t.Errorf("readSeekCloser.Read() = %v, want %v", got, content)
}
if err := rsc.Close(); err != nil {
t.Errorf("fail to close: %v", err)
}
if !rsc.(*readSeekCloser).closed {
t.Errorf("readSeekCloser not closed")
}
}
func Test_readSeekCloser_Seek(t *testing.T) {
content := []byte("hello world")
path := "/testpath"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != path {
w.WriteHeader(http.StatusNotFound)
return
}
rangeHeader := r.Header.Get("Range")
if rangeHeader == "" {
w.WriteHeader(http.StatusOK)
if _, err := w.Write(content); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
return
}
var start, end int
_, err := fmt.Sscanf(rangeHeader, "bytes=%d-%d", &start, &end)
if err != nil {
t.Errorf("invalid range header: %s", rangeHeader)
w.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
return
}
if start < 0 || start > end || start >= len(content) {
t.Errorf("invalid range: %s", rangeHeader)
w.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
return
}
end++
if end > len(content) {
end = len(content)
}
w.WriteHeader(http.StatusPartialContent)
if _, err := w.Write(content[start:end]); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
}))
defer ts.Close()
client := ts.Client()
resp, err := client.Get(ts.URL + path)
if err != nil {
t.Fatalf("failed to do request: %v", err)
}
rsc := NewReadSeekCloser(client, resp.Request, resp.Body, int64(len(content)))
tests := []struct {
name string
offset int64
whence int
wantOffset int64
n int64
want []byte
skipSeek bool
}{
{
name: "read from initial response",
n: 3,
want: []byte("hel"),
skipSeek: true,
},
{
name: "seek to skip",
offset: 2,
whence: io.SeekCurrent,
wantOffset: 5,
n: 4,
want: []byte(" wor"),
},
{
name: "seek to the beginning",
offset: 0,
whence: io.SeekStart,
wantOffset: 0,
n: 5,
want: []byte("hello"),
},
{
name: "seek to middle",
offset: 6,
whence: io.SeekStart,
wantOffset: 6,
n: math.MaxInt64,
want: []byte("world"),
},
{
name: "seek from end",
offset: -4,
whence: io.SeekEnd,
wantOffset: 7,
n: 3,
want: []byte("orl"),
},
{
name: "seek to the end",
offset: 0,
whence: io.SeekEnd,
wantOffset: 11,
n: 5,
want: nil,
},
{
name: "seek beyond the end",
offset: 42,
whence: io.SeekStart,
wantOffset: 42,
n: 10,
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if !tt.skipSeek {
got, err := rsc.Seek(tt.offset, tt.whence)
if err != nil {
t.Errorf("readSeekCloser.Seek() error = %v", err)
}
if got != tt.wantOffset {
t.Errorf("readSeekCloser.Read() = %v, want %v", got, tt.wantOffset)
}
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(io.LimitReader(rsc, tt.n)); err != nil {
t.Errorf("fail to read: %v", err)
}
if got := buf.Bytes(); !bytes.Equal(got, tt.want) {
t.Errorf("readSeekCloser.Read() = %v, want %v", got, tt.want)
}
})
}
_, err = rsc.Seek(-1, io.SeekStart)
if err == nil {
t.Errorf("readSeekCloser.Seek() error = %v, wantErr %v", err, true)
}
if err := rsc.Close(); err != nil {
t.Errorf("fail to close: %v", err)
}
if !rsc.(*readSeekCloser).closed {
t.Errorf("readSeekCloser not closed")
}
_, err = rsc.Seek(0, io.SeekStart)
if err == nil {
t.Errorf("readSeekCloser.Seek() error = %v, wantErr %v", err, true)
}
}
oras-go-2.5.0/internal/interfaces/ 0000775 0000000 0000000 00000000000 14576745303 0017042 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/internal/interfaces/registry.go 0000664 0000000 0000000 00000001475 14576745303 0021250 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 interfaces
import "oras.land/oras-go/v2/registry"
// ReferenceParser provides reference parsing.
type ReferenceParser interface {
// ParseReference parses a reference to a fully qualified reference.
ParseReference(reference string) (registry.Reference, error)
}
oras-go-2.5.0/internal/ioutil/ 0000775 0000000 0000000 00000000000 14576745303 0016224 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/internal/ioutil/io.go 0000664 0000000 0000000 00000003740 14576745303 0017166 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 ioutil
import (
"fmt"
"io"
"reflect"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content"
)
// CloserFunc is the basic Close method defined in io.Closer.
type CloserFunc func() error
// Close performs close operation by the CloserFunc.
func (fn CloserFunc) Close() error {
return fn()
}
// CopyBuffer copies from src to dst through the provided buffer
// until either EOF is reached on src, or an error occurs.
// The copied content is verified against the size and the digest.
func CopyBuffer(dst io.Writer, src io.Reader, buf []byte, desc ocispec.Descriptor) error {
// verify while copying
vr := content.NewVerifyReader(src, desc)
if _, err := io.CopyBuffer(dst, vr, buf); err != nil {
return fmt.Errorf("copy failed: %w", err)
}
return vr.Verify()
}
// Types returned by `io.NopCloser()`.
var (
nopCloserType = reflect.TypeOf(io.NopCloser(nil))
nopCloserWriterToType = reflect.TypeOf(io.NopCloser(struct {
io.Reader
io.WriterTo
}{}))
)
// UnwrapNopCloser unwraps the reader wrapped by `io.NopCloser()`.
// Similar implementation can be found in the built-in package `net/http`.
// Reference: https://github.com/golang/go/blob/go1.22.1/src/net/http/transfer.go#L1090-L1105
func UnwrapNopCloser(r io.Reader) io.Reader {
switch reflect.TypeOf(r) {
case nopCloserType, nopCloserWriterToType:
return reflect.ValueOf(r).Field(0).Interface().(io.Reader)
default:
return r
}
}
oras-go-2.5.0/internal/ioutil/io_test.go 0000664 0000000 0000000 00000006475 14576745303 0020235 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 ioutil
import (
"bytes"
"errors"
"io"
"os"
"reflect"
"testing"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content"
)
func TestUnwrapNopCloser(t *testing.T) {
var reader struct {
io.Reader
}
var readerWithWriterTo struct {
io.Reader
io.WriterTo
}
tests := []struct {
name string
rc io.Reader
want io.Reader
}{
{
name: "nil",
},
{
name: "no-op closer with plain io.Reader",
rc: io.NopCloser(reader),
want: reader,
},
{
name: "no-op closer with io.WriteTo",
rc: io.NopCloser(readerWithWriterTo),
want: readerWithWriterTo,
},
{
name: "any ReadCloser",
rc: os.Stdin,
want: os.Stdin,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := UnwrapNopCloser(tt.rc); !reflect.DeepEqual(got, tt.want) {
t.Errorf("UnwrapNopCloser() = %v, want %v", got, tt.want)
}
})
}
}
func TestCopyBuffer(t *testing.T) {
blob := []byte("foo")
type args struct {
src io.Reader
buf []byte
desc ocispec.Descriptor
}
tests := []struct {
name string
args args
wantDst string
wantErr error
}{
{
name: "exact buffer size, no errors",
args: args{bytes.NewReader(blob), make([]byte, 3), content.NewDescriptorFromBytes("test", blob)},
wantDst: "foo",
wantErr: nil,
},
{
name: "small buffer size, no errors",
args: args{bytes.NewReader(blob), make([]byte, 1), content.NewDescriptorFromBytes("test", blob)},
wantDst: "foo",
wantErr: nil,
},
{
name: "big buffer size, no errors",
args: args{bytes.NewReader(blob), make([]byte, 5), content.NewDescriptorFromBytes("test", blob)},
wantDst: "foo",
wantErr: nil,
},
{
name: "wrong digest",
args: args{bytes.NewReader(blob), make([]byte, 3), content.NewDescriptorFromBytes("test", []byte("bar"))},
wantDst: "foo",
wantErr: content.ErrMismatchedDigest,
},
{
name: "wrong size, descriptor size is smaller",
args: args{bytes.NewReader(blob), make([]byte, 3), content.NewDescriptorFromBytes("test", []byte("fo"))},
wantDst: "foo",
wantErr: content.ErrTrailingData,
},
{
name: "wrong size, descriptor size is larger",
args: args{bytes.NewReader(blob), make([]byte, 3), content.NewDescriptorFromBytes("test", []byte("fooo"))},
wantDst: "foo",
wantErr: io.ErrUnexpectedEOF,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dst := &bytes.Buffer{}
err := CopyBuffer(dst, tt.args.src, tt.args.buf, tt.args.desc)
if !errors.Is(err, tt.wantErr) {
t.Errorf("CopyBuffer() error = %v, wantErr %v", err, tt.wantErr)
return
}
gotDst := dst.String()
if err == nil && gotDst != tt.wantDst {
t.Errorf("CopyBuffer() = %v, want %v", gotDst, tt.wantDst)
}
})
}
}
oras-go-2.5.0/internal/manifestutil/ 0000775 0000000 0000000 00000000000 14576745303 0017423 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/internal/manifestutil/parser.go 0000664 0000000 0000000 00000005060 14576745303 0021247 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 manifestutil
import (
"context"
"encoding/json"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/internal/docker"
"oras.land/oras-go/v2/internal/spec"
)
// Config returns the config of desc, if present.
func Config(ctx context.Context, fetcher content.Fetcher, desc ocispec.Descriptor) (*ocispec.Descriptor, error) {
switch desc.MediaType {
case docker.MediaTypeManifest, ocispec.MediaTypeImageManifest:
content, err := content.FetchAll(ctx, fetcher, desc)
if err != nil {
return nil, err
}
// OCI manifest schema can be used to marshal docker manifest
var manifest ocispec.Manifest
if err := json.Unmarshal(content, &manifest); err != nil {
return nil, err
}
return &manifest.Config, nil
default:
return nil, nil
}
}
// Manifest returns the manifests of desc, if present.
func Manifests(ctx context.Context, fetcher content.Fetcher, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
switch desc.MediaType {
case docker.MediaTypeManifestList, ocispec.MediaTypeImageIndex:
content, err := content.FetchAll(ctx, fetcher, desc)
if err != nil {
return nil, err
}
// OCI manifest index schema can be used to marshal docker manifest list
var index ocispec.Index
if err := json.Unmarshal(content, &index); err != nil {
return nil, err
}
return index.Manifests, nil
default:
return nil, nil
}
}
// Subject returns the subject of desc, if present.
func Subject(ctx context.Context, fetcher content.Fetcher, desc ocispec.Descriptor) (*ocispec.Descriptor, error) {
switch desc.MediaType {
case ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex, spec.MediaTypeArtifactManifest:
content, err := content.FetchAll(ctx, fetcher, desc)
if err != nil {
return nil, err
}
var manifest struct {
Subject *ocispec.Descriptor `json:"subject,omitempty"`
}
if err := json.Unmarshal(content, &manifest); err != nil {
return nil, err
}
return manifest.Subject, nil
default:
return nil, nil
}
}
oras-go-2.5.0/internal/manifestutil/parser_test.go 0000664 0000000 0000000 00000024340 14576745303 0022310 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 manifestutil
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"reflect"
"testing"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"golang.org/x/sync/errgroup"
"oras.land/oras-go/v2/content/memory"
"oras.land/oras-go/v2/internal/cas"
"oras.land/oras-go/v2/internal/container/set"
"oras.land/oras-go/v2/internal/docker"
)
var ErrBadFetch = errors.New("bad fetch error")
// testStorage implements Fetcher
type testStorage struct {
store *memory.Store
badFetch set.Set[digest.Digest]
}
func (s *testStorage) Push(ctx context.Context, expected ocispec.Descriptor, reader io.Reader) error {
return s.store.Push(ctx, expected, reader)
}
func (s *testStorage) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
if s.badFetch.Contains(target.Digest) {
return nil, ErrBadFetch
}
return s.store.Fetch(ctx, target)
}
// func (s *testStorage) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) {
// return s.store.Exists(ctx, target)
// }
// func (s *testStorage) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) {
// return s.store.Predecessors(ctx, node)
// }
func TestConfig(t *testing.T) {
storage := cas.NewMemory()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(mediaType string, config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(mediaType, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
generateManifest(ocispec.MediaTypeImageManifest, descs[0], descs[1]) // Blob 2
generateManifest(docker.MediaTypeManifest, descs[0], descs[1]) // Blob 3
generateManifest("whatever", descs[0], descs[1]) // Blob 4
ctx := context.Background()
for i := range blobs {
err := storage.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
tests := []struct {
name string
desc ocispec.Descriptor
want *ocispec.Descriptor
wantErr bool
}{
{
name: "OCI Image Manifest",
desc: descs[2],
want: &descs[0],
},
{
name: "Docker Manifest",
desc: descs[3],
want: &descs[0],
wantErr: false,
},
{
name: "Other media type",
desc: descs[4],
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Config(ctx, storage, tt.desc)
if (err != nil) != tt.wantErr {
t.Errorf("Config() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Config() = %v, want %v", got, tt.want)
}
})
}
}
func TestManifests(t *testing.T) {
storage := cas.NewMemory()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(subject *ocispec.Descriptor, config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Subject: subject,
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(mediaType string, subject *ocispec.Descriptor, manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Subject: subject,
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(mediaType, indexJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 3
generateManifest(nil, descs[0], descs[1:3]...) // Blob 4
generateManifest(nil, descs[0], descs[3]) // Blob 5
appendBlob(ocispec.MediaTypeImageConfig, []byte("{}")) // Blob 6
appendBlob("test/sig", []byte("sig")) // Blob 7
generateManifest(&descs[4], descs[5], descs[6]) // Blob 8
generateIndex(ocispec.MediaTypeImageIndex, &descs[8], descs[4:6]...) // Blob 9
generateIndex(docker.MediaTypeManifestList, nil, descs[4:6]...) // Blob 10
generateIndex("whatever", &descs[8], descs[4:6]...) // Blob 11
ctx := context.Background()
for i := range blobs {
err := storage.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
tests := []struct {
name string
desc ocispec.Descriptor
want []ocispec.Descriptor
wantErr bool
}{
{
name: "OCI Image Index",
desc: descs[9],
want: descs[4:6],
},
{
name: "Docker Manifest List",
desc: descs[10],
want: descs[4:6],
wantErr: false,
},
{
name: "Other media type",
desc: descs[11],
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Manifests(ctx, storage, tt.desc)
if (err != nil) != tt.wantErr {
t.Errorf("Manifests() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Manifests() = %v, want %v", got, tt.want)
}
})
}
}
func TestSubject(t *testing.T) {
storage := cas.NewMemory()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateManifest := func(config ocispec.Descriptor, subject *ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Config: config,
Subject: subject,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifestJSON)
}
appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("blob")) // Blob 1
generateManifest(descs[0], nil, descs[1]) // Blob 2, manifest
generateManifest(descs[0], &descs[2], descs[1]) // Blob 3, referrer of blob 2
ctx := context.Background()
for i := range blobs {
err := storage.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
got, err := Subject(ctx, storage, descs[3])
if err != nil {
t.Fatalf("error when getting subject: %v", err)
}
if !reflect.DeepEqual(*got, descs[2]) {
t.Errorf("Subject() = %v, want %v", got, descs[2])
}
got, err = Subject(ctx, storage, descs[0])
if err != nil {
t.Fatalf("error when getting subject: %v", err)
}
if got != nil {
t.Errorf("Subject() = %v, want %v", got, nil)
}
}
func TestSubject_ErrorPath(t *testing.T) {
s := testStorage{
store: memory.New(),
badFetch: set.New[digest.Digest](),
}
ctx := context.Background()
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, artifactType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
ArtifactType: artifactType,
Annotations: map[string]string{"test": "content"},
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
generateImageManifest := func(config ocispec.Descriptor, subject *ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
MediaType: ocispec.MediaTypeImageManifest,
Config: config,
Subject: subject,
Layers: layers,
Annotations: map[string]string{"test": "content"},
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageManifest, manifest.Config.MediaType, manifestJSON)
}
appendBlob("image manifest", "image config", []byte("config")) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, "layer", []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, "layer", []byte("bar")) // Blob 2
appendBlob(ocispec.MediaTypeImageLayer, "layer", []byte("hello")) // Blob 3
generateImageManifest(descs[0], nil, descs[1]) // Blob 4
generateImageManifest(descs[0], &descs[4], descs[2]) // Blob 5
s.badFetch.Add(descs[5].Digest)
eg, egCtx := errgroup.WithContext(ctx)
for i := range blobs {
eg.Go(func(i int) func() error {
return func() error {
err := s.Push(egCtx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
return fmt.Errorf("failed to push test content to src: %d: %v", i, err)
}
return nil
}
}(i))
}
if err := eg.Wait(); err != nil {
t.Fatal(err)
}
_, err := Subject(ctx, &s, descs[5])
if !errors.Is(err, ErrBadFetch) {
t.Errorf("Store.Referrers() error = %v, want %v", err, ErrBadFetch)
}
}
oras-go-2.5.0/internal/platform/ 0000775 0000000 0000000 00000000000 14576745303 0016543 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/internal/platform/platform.go 0000664 0000000 0000000 00000010652 14576745303 0020722 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 platform
import (
"context"
"encoding/json"
"fmt"
"io"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/internal/docker"
"oras.land/oras-go/v2/internal/manifestutil"
)
// Match checks whether the current platform matches the target platform.
// Match will return true if all of the following conditions are met.
// - Architecture and OS exactly match.
// - Variant and OSVersion exactly match if target platform provided.
// - OSFeatures of the target platform are the subsets of the OSFeatures
// array of the current platform.
//
// Note: Variant, OSVersion and OSFeatures are optional fields, will skip
// the comparison if the target platform does not provide specific value.
func Match(got *ocispec.Platform, want *ocispec.Platform) bool {
if got == nil && want == nil {
return true
}
if got == nil || want == nil {
return false
}
if got.Architecture != want.Architecture || got.OS != want.OS {
return false
}
if want.OSVersion != "" && got.OSVersion != want.OSVersion {
return false
}
if want.Variant != "" && got.Variant != want.Variant {
return false
}
if len(want.OSFeatures) != 0 && !isSubset(want.OSFeatures, got.OSFeatures) {
return false
}
return true
}
// isSubset returns true if all items in slice A are present in slice B.
func isSubset(a, b []string) bool {
set := make(map[string]bool, len(b))
for _, v := range b {
set[v] = true
}
for _, v := range a {
if _, ok := set[v]; !ok {
return false
}
}
return true
}
// SelectManifest implements platform filter and returns the descriptor of the
// first matched manifest if the root is a manifest list. If the root is a
// manifest, then return the root descriptor if platform matches.
func SelectManifest(ctx context.Context, src content.ReadOnlyStorage, root ocispec.Descriptor, p *ocispec.Platform) (ocispec.Descriptor, error) {
switch root.MediaType {
case docker.MediaTypeManifestList, ocispec.MediaTypeImageIndex:
manifests, err := manifestutil.Manifests(ctx, src, root)
if err != nil {
return ocispec.Descriptor{}, err
}
// platform filter
for _, m := range manifests {
if Match(m.Platform, p) {
return m, nil
}
}
return ocispec.Descriptor{}, fmt.Errorf("%s: %w: no matching manifest was found in the manifest list", root.Digest, errdef.ErrNotFound)
case docker.MediaTypeManifest, ocispec.MediaTypeImageManifest:
// config will be non-nil for docker manifest and OCI image manifest
config, err := manifestutil.Config(ctx, src, root)
if err != nil {
return ocispec.Descriptor{}, err
}
configMediaType := docker.MediaTypeConfig
if root.MediaType == ocispec.MediaTypeImageManifest {
configMediaType = ocispec.MediaTypeImageConfig
}
cfgPlatform, err := getPlatformFromConfig(ctx, src, *config, configMediaType)
if err != nil {
return ocispec.Descriptor{}, err
}
if Match(cfgPlatform, p) {
return root, nil
}
return ocispec.Descriptor{}, fmt.Errorf("%s: %w: platform in manifest does not match target platform", root.Digest, errdef.ErrNotFound)
default:
return ocispec.Descriptor{}, fmt.Errorf("%s: %s: %w", root.Digest, root.MediaType, errdef.ErrUnsupported)
}
}
// getPlatformFromConfig returns a platform object which is made up from the
// fields in config blob.
func getPlatformFromConfig(ctx context.Context, src content.ReadOnlyStorage, desc ocispec.Descriptor, targetConfigMediaType string) (*ocispec.Platform, error) {
if desc.MediaType != targetConfigMediaType {
return nil, fmt.Errorf("fail to recognize platform from unknown config %s: expect %s", desc.MediaType, targetConfigMediaType)
}
rc, err := src.Fetch(ctx, desc)
if err != nil {
return nil, err
}
defer rc.Close()
var platform ocispec.Platform
if err = json.NewDecoder(rc).Decode(&platform); err != nil && err != io.EOF {
return nil, err
}
return &platform, nil
}
oras-go-2.5.0/internal/platform/platform_test.go 0000664 0000000 0000000 00000034572 14576745303 0021770 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 platform
import (
"bytes"
"context"
_ "crypto/sha256"
"encoding/json"
"errors"
"fmt"
"reflect"
"testing"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/internal/cas"
"oras.land/oras-go/v2/internal/docker"
)
func TestMatch(t *testing.T) {
tests := []struct {
got ocispec.Platform
want ocispec.Platform
isMatched bool
}{{
ocispec.Platform{Architecture: "amd64", OS: "linux"},
ocispec.Platform{Architecture: "amd64", OS: "linux"},
true,
}, {
ocispec.Platform{Architecture: "amd64", OS: "linux"},
ocispec.Platform{Architecture: "amd64", OS: "LINUX"},
false,
}, {
ocispec.Platform{Architecture: "amd64", OS: "linux"},
ocispec.Platform{Architecture: "arm64", OS: "linux"},
false,
}, {
ocispec.Platform{Architecture: "arm", OS: "linux"},
ocispec.Platform{Architecture: "arm", OS: "linux", Variant: "v7"},
false,
}, {
ocispec.Platform{Architecture: "arm", OS: "linux", Variant: "v7"},
ocispec.Platform{Architecture: "arm", OS: "linux"},
true,
}, {
ocispec.Platform{Architecture: "arm", OS: "linux", Variant: "v7"},
ocispec.Platform{Architecture: "arm", OS: "linux", Variant: "v7"},
true,
}, {
ocispec.Platform{Architecture: "amd64", OS: "windows", OSVersion: "10.0.20348.768"},
ocispec.Platform{Architecture: "amd64", OS: "windows", OSVersion: "10.0.20348.700"},
false,
}, {
ocispec.Platform{Architecture: "amd64", OS: "windows"},
ocispec.Platform{Architecture: "amd64", OS: "windows", OSVersion: "10.0.20348.768"},
false,
}, {
ocispec.Platform{Architecture: "amd64", OS: "windows", OSVersion: "10.0.20348.768"},
ocispec.Platform{Architecture: "amd64", OS: "windows"},
true,
}, {
ocispec.Platform{Architecture: "amd64", OS: "windows", OSVersion: "10.0.20348.768"},
ocispec.Platform{Architecture: "amd64", OS: "windows", OSVersion: "10.0.20348.768"},
true,
}, {
ocispec.Platform{Architecture: "arm", OS: "linux", OSFeatures: []string{"a", "d"}},
ocispec.Platform{Architecture: "arm", OS: "linux", OSFeatures: []string{"a", "c"}},
false,
}, {
ocispec.Platform{Architecture: "arm", OS: "linux"},
ocispec.Platform{Architecture: "arm", OS: "linux", OSFeatures: []string{"a"}},
false,
}, {
ocispec.Platform{Architecture: "arm", OS: "linux", OSFeatures: []string{"a"}},
ocispec.Platform{Architecture: "arm", OS: "linux"},
true,
}, {
ocispec.Platform{Architecture: "arm", OS: "linux", OSFeatures: []string{"a", "b"}},
ocispec.Platform{Architecture: "arm", OS: "linux", OSFeatures: []string{"a", "b"}},
true,
}, {
ocispec.Platform{Architecture: "arm", OS: "linux", OSFeatures: []string{"a", "d", "c", "b"}},
ocispec.Platform{Architecture: "arm", OS: "linux", OSFeatures: []string{"d", "c", "a", "b"}},
true,
}}
for _, tt := range tests {
gotPlatformJSON, _ := json.Marshal(tt.got)
wantPlatformJSON, _ := json.Marshal(tt.want)
name := string(gotPlatformJSON) + string(wantPlatformJSON)
t.Run(name, func(t *testing.T) {
if actual := Match(&tt.got, &tt.want); actual != tt.isMatched {
t.Errorf("Match() = %v, want %v", actual, tt.isMatched)
}
})
}
}
func TestSelectManifest(t *testing.T) {
storage := cas.NewMemory()
arc_1 := "test-arc-1"
os_1 := "test-os-1"
variant_1 := "v1"
arc_2 := "test-arc-2"
os_2 := "test-os-2"
variant_2 := "v2"
// generate test content
var blobs [][]byte
var descs []ocispec.Descriptor
appendBlob := func(mediaType string, blob []byte) {
blobs = append(blobs, blob)
descs = append(descs, ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
})
}
appendManifest := func(arc, os, variant string, hasPlatform bool, mediaType string, blob []byte) {
blobs = append(blobs, blob)
desc := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
if hasPlatform {
desc.Platform = &ocispec.Platform{
Architecture: arc,
OS: os,
Variant: variant,
}
}
descs = append(descs, desc)
}
generateManifest := func(arc, os, variant string, hasPlatform bool, subject *ocispec.Descriptor, config ocispec.Descriptor, layers ...ocispec.Descriptor) {
manifest := ocispec.Manifest{
Subject: subject,
Config: config,
Layers: layers,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatal(err)
}
appendManifest(arc, os, variant, hasPlatform, ocispec.MediaTypeImageManifest, manifestJSON)
}
generateIndex := func(subject *ocispec.Descriptor, manifests ...ocispec.Descriptor) {
index := ocispec.Index{
Subject: subject,
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatal(err)
}
appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
}
appendBlob("test/subject", []byte("dummy subject")) // Blob 0
appendBlob(ocispec.MediaTypeImageConfig, []byte(`{"mediaType":"application/vnd.oci.image.config.v1+json",
"created":"2022-07-29T08:13:55Z",
"author":"test author",
"architecture":"test-arc-1",
"os":"test-os-1",
"variant":"v1"}`)) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 2
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 3
generateManifest(arc_1, os_1, variant_1, true, &descs[0], descs[1], descs[2:4]...) // Blob 4
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello1")) // Blob 5
generateManifest(arc_2, os_2, variant_1, true, nil, descs[1], descs[5]) // Blob 6
appendBlob(ocispec.MediaTypeImageLayer, []byte("hello2")) // Blob 7
generateManifest(arc_1, os_1, variant_2, true, nil, descs[1], descs[7]) // Blob 8
generateIndex(&descs[0], descs[4], descs[6], descs[8]) // Blob 9
ctx := context.Background()
for i := range blobs {
err := storage.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// test SelectManifest on image index, only one matching manifest found
root := descs[9]
targetPlatform := ocispec.Platform{
Architecture: arc_2,
OS: os_2,
}
wantDesc := descs[6]
gotDesc, err := SelectManifest(ctx, storage, root, &targetPlatform)
if err != nil {
t.Fatalf("SelectManifest() error = %v, wantErr %v", err, false)
}
if !reflect.DeepEqual(gotDesc, wantDesc) {
t.Errorf("SelectManifest() = %v, want %v", gotDesc, wantDesc)
}
// test SelectManifest on image index,
// and multiple manifests match the required platform.
// Should return the first matching entry.
targetPlatform = ocispec.Platform{
Architecture: arc_1,
OS: os_1,
}
wantDesc = descs[4]
gotDesc, err = SelectManifest(ctx, storage, root, &targetPlatform)
if err != nil {
t.Fatalf("SelectManifest() error = %v, wantErr %v", err, false)
}
if !reflect.DeepEqual(gotDesc, wantDesc) {
t.Errorf("SelectManifest() = %v, want %v", gotDesc, wantDesc)
}
// test SelectManifest on manifest
root = descs[8]
targetPlatform = ocispec.Platform{
Architecture: arc_1,
OS: os_1,
}
wantDesc = descs[8]
gotDesc, err = SelectManifest(ctx, storage, root, &targetPlatform)
if err != nil {
t.Fatalf("SelectManifest() error = %v, wantErr %v", err, false)
}
if !reflect.DeepEqual(gotDesc, wantDesc) {
t.Errorf("SelectManifest() = %v, want %v", gotDesc, wantDesc)
}
// test SelectManifest on manifest, but there is no matching node.
// Should return not found error.
root = descs[8]
targetPlatform = ocispec.Platform{
Architecture: arc_1,
OS: os_1,
Variant: variant_2,
}
_, err = SelectManifest(ctx, storage, root, &targetPlatform)
expected := fmt.Sprintf("%s: %v: platform in manifest does not match target platform", root.Digest, errdef.ErrNotFound)
if err.Error() != expected {
t.Fatalf("SelectManifest() error = %v, wantErr %v", err, expected)
}
// test SelectManifest on manifest, but the node's media type is not
// supported. Should return unsupported error
targetPlatform = ocispec.Platform{
Architecture: arc_1,
OS: os_1,
}
root = descs[2]
_, err = SelectManifest(ctx, storage, root, &targetPlatform)
if !errors.Is(err, errdef.ErrUnsupported) {
t.Fatalf("SelectManifest() error = %v, wantErr %v", err, errdef.ErrUnsupported)
}
// generate test content without platform
storage = cas.NewMemory()
blobs = nil
descs = nil
appendBlob("test/subject", []byte("dummy subject")) // Blob 0
appendBlob(ocispec.MediaTypeImageConfig, []byte(`{"mediaType":"application/vnd.oci.image.config.v1+json",
"created":"2022-07-29T08:13:55Z",
"author":"test author",
"architecture":"test-arc-1",
"os":"test-os-1",
"variant":"v1"}`)) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 2
generateManifest(arc_1, os_1, variant_1, false, &descs[0], descs[1], descs[2]) // Blob 3
generateIndex(&descs[0], descs[3]) // Blob 4
ctx = context.Background()
for i := range blobs {
err := storage.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// Test SelectManifest on an image index when no platform exists in the manifest list and a target platform is provided
root = descs[4]
targetPlatform = ocispec.Platform{
Architecture: arc_1,
OS: os_1,
}
_, err = SelectManifest(ctx, storage, root, &targetPlatform)
expected = fmt.Sprintf("%s: %v: no matching manifest was found in the manifest list", root.Digest, errdef.ErrNotFound)
if err.Error() != expected {
t.Fatalf("SelectManifest() error = %v, wantErr %v", err, expected)
}
// Test SelectManifest on an image index when no platform exists in the manifest list and no target platform is provided
wantDesc = descs[3]
gotDesc, err = SelectManifest(ctx, storage, root, nil)
if err != nil {
t.Fatalf("SelectManifest() error = %v, wantErr %v", err, false)
}
if !reflect.DeepEqual(gotDesc, wantDesc) {
t.Errorf("SelectManifest() = %v, want %v", gotDesc, wantDesc)
}
// generate incorrect test content
storage = cas.NewMemory()
blobs = nil
descs = nil
appendBlob("test/subject", []byte("dummy subject")) // Blob 0
appendBlob(docker.MediaTypeConfig, []byte(`{"mediaType":"application/vnd.oci.image.config.v1+json",
"created":"2022-07-29T08:13:55Z",
"author":"test author 1",
"architecture":"test-arc-1",
"os":"test-os-1",
"variant":"v1"}`)) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo1")) // Blob 2
generateManifest(arc_1, os_1, variant_1, true, &descs[0], descs[1], descs[2]) // Blob 3
generateIndex(&descs[0], descs[3]) // Blob 4
ctx = context.Background()
for i := range blobs {
err := storage.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// test SelectManifest on manifest, but the manifest is
// invalid by having docker mediaType config in the manifest and oci
// mediaType in the image config. Should return error.
root = descs[3]
targetPlatform = ocispec.Platform{
Architecture: arc_1,
OS: os_1,
}
_, err = SelectManifest(ctx, storage, root, &targetPlatform)
expected = fmt.Sprintf("fail to recognize platform from unknown config %s: expect %s", docker.MediaTypeConfig, ocispec.MediaTypeImageConfig)
if err.Error() != expected {
t.Fatalf("SelectManifest() error = %v, wantErr %v", err, expected)
}
// generate test content with null config blob
storage = cas.NewMemory()
blobs = nil
descs = nil
appendBlob("test/subject", []byte("dummy subject")) // Blob 0
appendBlob(ocispec.MediaTypeImageConfig, []byte("null")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo2")) // Blob 2
generateManifest(arc_1, os_1, variant_1, true, &descs[0], descs[1], descs[2]) // Blob 3
generateIndex(nil, descs[3]) // Blob 4
ctx = context.Background()
for i := range blobs {
err := storage.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// test SelectManifest on manifest with null config blob,
// should return not found error.
root = descs[3]
targetPlatform = ocispec.Platform{
Architecture: arc_1,
OS: os_1,
}
_, err = SelectManifest(ctx, storage, root, &targetPlatform)
expected = fmt.Sprintf("%s: %v: platform in manifest does not match target platform", root.Digest, errdef.ErrNotFound)
if err.Error() != expected {
t.Fatalf("SelectManifest() error = %v, wantErr %v", err, expected)
}
// generate test content with empty config blob
storage = cas.NewMemory()
blobs = nil
descs = nil
appendBlob("test/subject", []byte("dummy subject")) // Blob 0
appendBlob(ocispec.MediaTypeImageConfig, []byte("")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo3")) // Blob 2
generateManifest(arc_1, os_1, variant_1, true, nil, descs[1], descs[2]) // Blob 3
generateIndex(&descs[0], descs[3]) // Blob 4
ctx = context.Background()
for i := range blobs {
err := storage.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
if err != nil {
t.Fatalf("failed to push test content to src: %d: %v", i, err)
}
}
// test SelectManifest on manifest with empty config blob
// should return not found error
root = descs[3]
targetPlatform = ocispec.Platform{
Architecture: arc_1,
OS: os_1,
}
_, err = SelectManifest(ctx, storage, root, &targetPlatform)
expected = fmt.Sprintf("%s: %v: platform in manifest does not match target platform", root.Digest, errdef.ErrNotFound)
if err.Error() != expected {
t.Fatalf("SelectManifest() error = %v, wantErr %v", err, expected)
}
}
oras-go-2.5.0/internal/registryutil/ 0000775 0000000 0000000 00000000000 14576745303 0017465 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/internal/registryutil/proxy.go 0000664 0000000 0000000 00000005151 14576745303 0021177 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 registryutil
import (
"context"
"io"
"sync"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/internal/cas"
"oras.land/oras-go/v2/internal/ioutil"
"oras.land/oras-go/v2/registry"
)
// ReferenceStorage represents a CAS that supports registry.ReferenceFetcher.
type ReferenceStorage interface {
content.ReadOnlyStorage
registry.ReferenceFetcher
}
// Proxy is a caching proxy dedicated for registry.ReferenceFetcher.
// The first fetch call of a described content will read from the remote and
// cache the fetched content.
// The subsequent fetch call will read from the local cache.
type Proxy struct {
registry.ReferenceFetcher
*cas.Proxy
}
// NewProxy creates a proxy for the `base` ReferenceStorage, using the `cache`
// storage as the cache.
func NewProxy(base ReferenceStorage, cache content.Storage) *Proxy {
return &Proxy{
ReferenceFetcher: base,
Proxy: cas.NewProxy(base, cache),
}
}
// FetchReference fetches the content identified by the reference from the
// remote and cache the fetched content.
func (p *Proxy) FetchReference(ctx context.Context, reference string) (ocispec.Descriptor, io.ReadCloser, error) {
target, rc, err := p.ReferenceFetcher.FetchReference(ctx, reference)
if err != nil {
return ocispec.Descriptor{}, nil, err
}
// skip caching if the content already exists in cache
exists, err := p.Cache.Exists(ctx, target)
if err != nil {
return ocispec.Descriptor{}, nil, err
}
if exists {
return target, rc, nil
}
// cache content while reading
pr, pw := io.Pipe()
var wg sync.WaitGroup
wg.Add(1)
var pushErr error
go func() {
defer wg.Done()
pushErr = p.Cache.Push(ctx, target, pr)
if pushErr != nil {
pr.CloseWithError(pushErr)
}
}()
closer := ioutil.CloserFunc(func() error {
rcErr := rc.Close()
if err := pw.Close(); err != nil {
return err
}
wg.Wait()
if pushErr != nil {
return pushErr
}
return rcErr
})
return target, struct {
io.Reader
io.Closer
}{
Reader: io.TeeReader(rc, pw),
Closer: closer,
}, nil
}
oras-go-2.5.0/internal/registryutil/proxy_test.go 0000664 0000000 0000000 00000011132 14576745303 0022232 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 registryutil_test
import (
"bytes"
"context"
"io"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strconv"
"strings"
"testing"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/internal/cas"
"oras.land/oras-go/v2/internal/registryutil"
"oras.land/oras-go/v2/registry/remote"
)
func TestProxy_FetchReference(t *testing.T) {
content := []byte(`{"manifests":[]}`)
desc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageIndex,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
ref := "foobar"
// prepare repository server
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodHead && r.Method != http.MethodGet {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
switch r.URL.Path {
case "/v2/test/manifests/" + desc.Digest.String(),
"/v2/test/manifests/" + ref:
if accept := r.Header.Get("Accept"); !strings.Contains(accept, desc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", desc.MediaType)
w.Header().Set("Docker-Content-Digest", desc.Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(int(desc.Size)))
if r.Method == http.MethodGet {
if _, err := w.Write(content); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
}
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repoName := uri.Host + "/test"
repo, err := remote.NewRepository(repoName)
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
s := registryutil.NewProxy(repo, cas.NewMemory())
ctx := context.Background()
// first FetchReference
exists, err := s.Exists(ctx, desc)
if err != nil {
t.Fatal("Proxy.Exists() error =", err)
}
if !exists {
t.Errorf("Proxy.Exists() = %v, want %v", exists, true)
}
gotDesc, rc, err := s.FetchReference(ctx, ref)
if err != nil {
t.Fatal("Proxy.FetchReference() error =", err)
}
if !reflect.DeepEqual(gotDesc, desc) {
t.Errorf("Proxy.FetchReference() = %v, want %v", gotDesc, desc)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Proxy.FetchReference().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Proxy.FetchReference().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Proxy.FetchReference() = %v, want %v", got, content)
}
// the subsequent fetch should not touch base CAS
// nil base will generate panic if the base CAS is touched
s.ReadOnlyStorage = nil
exists, err = s.Exists(ctx, desc)
if err != nil {
t.Fatal("Proxy.Exists() error =", err)
}
if !exists {
t.Errorf("Proxy.Exists() = %v, want %v", exists, true)
}
rc, err = s.Fetch(ctx, desc)
if err != nil {
t.Fatal("Proxy.Fetch() error =", err)
}
got, err = io.ReadAll(rc)
if err != nil {
t.Fatal("Proxy.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Proxy.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Proxy.Fetch() = %v, want %v", got, content)
}
// repeated FetchReference should succeed
exists, err = s.Exists(ctx, desc)
if err != nil {
t.Fatal("Proxy.Exists() error =", err)
}
if !exists {
t.Errorf("Proxy.Exists() = %v, want %v", exists, true)
}
gotDesc, rc, err = s.FetchReference(ctx, ref)
if err != nil {
t.Fatal("Proxy.FetchReference() error =", err)
}
if !reflect.DeepEqual(gotDesc, desc) {
t.Errorf("Proxy.FetchReference() = %v, want %v", gotDesc, desc)
}
got, err = io.ReadAll(rc)
if err != nil {
t.Fatal("Proxy.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Proxy.Fetch().Close() error =", err)
}
if !bytes.Equal(got, content) {
t.Errorf("Proxy.Fetch() = %v, want %v", got, content)
}
}
oras-go-2.5.0/internal/resolver/ 0000775 0000000 0000000 00000000000 14576745303 0016560 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/internal/resolver/memory.go 0000664 0000000 0000000 00000005023 14576745303 0020417 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 resolver
import (
"context"
"maps"
"sync"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/internal/container/set"
)
// Memory is a memory based resolver.
type Memory struct {
lock sync.RWMutex
index map[string]ocispec.Descriptor
tags map[digest.Digest]set.Set[string]
}
// NewMemory creates a new Memory resolver.
func NewMemory() *Memory {
return &Memory{
index: make(map[string]ocispec.Descriptor),
tags: make(map[digest.Digest]set.Set[string]),
}
}
// Resolve resolves a reference to a descriptor.
func (m *Memory) Resolve(_ context.Context, reference string) (ocispec.Descriptor, error) {
m.lock.RLock()
defer m.lock.RUnlock()
desc, ok := m.index[reference]
if !ok {
return ocispec.Descriptor{}, errdef.ErrNotFound
}
return desc, nil
}
// Tag tags a descriptor with a reference string.
func (m *Memory) Tag(_ context.Context, desc ocispec.Descriptor, reference string) error {
m.lock.Lock()
defer m.lock.Unlock()
m.index[reference] = desc
tagSet, ok := m.tags[desc.Digest]
if !ok {
tagSet = set.New[string]()
m.tags[desc.Digest] = tagSet
}
tagSet.Add(reference)
return nil
}
// Untag removes a reference from index map.
func (m *Memory) Untag(reference string) {
m.lock.Lock()
defer m.lock.Unlock()
desc, ok := m.index[reference]
if !ok {
return
}
delete(m.index, reference)
tagSet := m.tags[desc.Digest]
tagSet.Delete(reference)
if len(tagSet) == 0 {
delete(m.tags, desc.Digest)
}
}
// Map dumps the memory into a built-in map structure.
// Like other operations, calling Map() is go-routine safe.
func (m *Memory) Map() map[string]ocispec.Descriptor {
m.lock.RLock()
defer m.lock.RUnlock()
return maps.Clone(m.index)
}
// TagSet returns the set of tags of the descriptor.
func (m *Memory) TagSet(desc ocispec.Descriptor) set.Set[string] {
m.lock.RLock()
defer m.lock.RUnlock()
tagSet := m.tags[desc.Digest]
return maps.Clone(tagSet)
}
oras-go-2.5.0/internal/resolver/memory_test.go 0000664 0000000 0000000 00000004745 14576745303 0021470 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 resolver
import (
"context"
_ "crypto/sha256"
"errors"
"reflect"
"testing"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/errdef"
)
func TestMemorySuccess(t *testing.T) {
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
ref := "foobar"
s := NewMemory()
ctx := context.Background()
err := s.Tag(ctx, desc, ref)
if err != nil {
t.Fatal("Memory.Tag() error =", err)
}
got, err := s.Resolve(ctx, ref)
if err != nil {
t.Fatal("Memory.Resolve() error =", err)
}
if !reflect.DeepEqual(got, desc) {
t.Errorf("Memory.Resolve() = %v, want %v", got, desc)
}
if got := len(s.Map()); got != 1 {
t.Errorf("Memory.Map() = %v, want %v", got, 1)
}
s.Untag(ref)
_, err = s.Resolve(ctx, ref)
if !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("Memory.Resolve() error = %v, want %v", err, errdef.ErrNotFound)
}
if got := len(s.Map()); got != 0 {
t.Errorf("Memory.Map() = %v, want %v", got, 0)
}
}
func TestMemoryNotFound(t *testing.T) {
ref := "foobar"
s := NewMemory()
ctx := context.Background()
_, err := s.Resolve(ctx, ref)
if !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("Memory.Resolve() error = %v, want %v", err, errdef.ErrNotFound)
}
}
func TestTagSet(t *testing.T) {
refFoo := "foo"
refBar := "bar"
s := NewMemory()
ctx := context.Background()
content := []byte("hello world")
desc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
s.Tag(ctx, desc, refFoo)
s.Tag(ctx, desc, refBar)
tagSet := s.TagSet(desc)
if !tagSet.Contains(refFoo) {
t.Fatalf("tagSet should contain %s", refFoo)
}
if !tagSet.Contains(refBar) {
t.Fatalf("tagSet should contain %s", refFoo)
}
if len(tagSet) != 2 {
t.Fatalf("expect size = %d, got %d", 2, len(tagSet))
}
}
oras-go-2.5.0/internal/spec/ 0000775 0000000 0000000 00000000000 14576745303 0015651 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/internal/spec/artifact.go 0000664 0000000 0000000 00000004776 14576745303 0020013 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 spec
import ocispec "github.com/opencontainers/image-spec/specs-go/v1"
const (
// AnnotationArtifactCreated is the annotation key for the date and time on which the artifact was built, conforming to RFC 3339.
AnnotationArtifactCreated = "org.opencontainers.artifact.created"
// AnnotationArtifactDescription is the annotation key for the human readable description for the artifact.
AnnotationArtifactDescription = "org.opencontainers.artifact.description"
// AnnotationReferrersFiltersApplied is the annotation key for the comma separated list of filters applied by the registry in the referrers listing.
AnnotationReferrersFiltersApplied = "org.opencontainers.referrers.filtersApplied"
)
// MediaTypeArtifactManifest specifies the media type for a content descriptor.
const MediaTypeArtifactManifest = "application/vnd.oci.artifact.manifest.v1+json"
// Artifact describes an artifact manifest.
// This structure provides `application/vnd.oci.artifact.manifest.v1+json` mediatype when marshalled to JSON.
//
// This manifest type was introduced in image-spec v1.1.0-rc1 and was removed in
// image-spec v1.1.0-rc3. It is not part of the current image-spec and is kept
// here for Go compatibility.
//
// Reference: https://github.com/opencontainers/image-spec/pull/999
type Artifact struct {
// MediaType is the media type of the object this schema refers to.
MediaType string `json:"mediaType"`
// ArtifactType is the IANA media type of the artifact this schema refers to.
ArtifactType string `json:"artifactType"`
// Blobs is a collection of blobs referenced by this manifest.
Blobs []ocispec.Descriptor `json:"blobs,omitempty"`
// Subject (reference) is an optional link from the artifact to another manifest forming an association between the artifact and the other manifest.
Subject *ocispec.Descriptor `json:"subject,omitempty"`
// Annotations contains arbitrary metadata for the artifact manifest.
Annotations map[string]string `json:"annotations,omitempty"`
}
oras-go-2.5.0/internal/status/ 0000775 0000000 0000000 00000000000 14576745303 0016242 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/internal/status/tracker.go 0000664 0000000 0000000 00000002626 14576745303 0020232 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package status
import (
"sync"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/internal/descriptor"
)
// Tracker tracks content status described by a descriptor.
type Tracker struct {
status sync.Map // map[descriptor.Descriptor]chan struct{}
}
// NewTracker creates a new content status tracker.
func NewTracker() *Tracker {
return &Tracker{}
}
// TryCommit tries to commit the work for the target descriptor.
// Returns true if committed. A channel is also returned for sending
// notifications. Once the work is done, the channel should be closed.
// Returns false if the work is done or still in progress.
func (t *Tracker) TryCommit(target ocispec.Descriptor) (chan struct{}, bool) {
key := descriptor.FromOCI(target)
status, exists := t.status.LoadOrStore(key, make(chan struct{}))
return status.(chan struct{}), !exists
}
oras-go-2.5.0/internal/status/tracker_test.go 0000664 0000000 0000000 00000003000 14576745303 0021254 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package status
import (
"testing"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
func TestTracker_TryCommit(t *testing.T) {
tracker := NewTracker()
var desc ocispec.Descriptor
notify, committed := tracker.TryCommit(desc)
if !committed {
t.Fatalf("Tracker.TryCommit() got = %v, want %v", committed, true)
}
done, committed := tracker.TryCommit(desc)
if committed {
t.Fatalf("Tracker.TryCommit() got = %v, want %v", committed, false)
}
done2, committed := tracker.TryCommit(desc)
if committed {
t.Fatalf("Tracker.TryCommit() got = %v, want %v", committed, false)
}
// status: working in progress
select {
case <-done:
t.Fatalf("unexpected done")
default:
}
select {
case <-done2:
t.Fatalf("unexpected done")
default:
}
// mark status as done
close(notify)
// status: done
select {
case <-done:
default:
t.Fatalf("unexpected in progress")
}
select {
case <-done2:
default:
t.Fatalf("unexpected in progress")
}
}
oras-go-2.5.0/internal/syncutil/ 0000775 0000000 0000000 00000000000 14576745303 0016571 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/internal/syncutil/limit.go 0000664 0000000 0000000 00000004042 14576745303 0020236 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 syncutil
import (
"context"
"golang.org/x/sync/errgroup"
"golang.org/x/sync/semaphore"
)
// LimitedRegion provides a way to bound concurrent access to a code block.
type LimitedRegion struct {
ctx context.Context
limiter *semaphore.Weighted
ended bool
}
// LimitRegion creates a new LimitedRegion.
func LimitRegion(ctx context.Context, limiter *semaphore.Weighted) *LimitedRegion {
if limiter == nil {
return nil
}
return &LimitedRegion{
ctx: ctx,
limiter: limiter,
ended: true,
}
}
// Start starts the region with concurrency limit.
func (lr *LimitedRegion) Start() error {
if lr == nil || !lr.ended {
return nil
}
if err := lr.limiter.Acquire(lr.ctx, 1); err != nil {
return err
}
lr.ended = false
return nil
}
// End ends the region with concurrency limit.
func (lr *LimitedRegion) End() {
if lr == nil || lr.ended {
return
}
lr.limiter.Release(1)
lr.ended = true
}
// GoFunc represents a function that can be invoked by Go.
type GoFunc[T any] func(ctx context.Context, region *LimitedRegion, t T) error
// Go concurrently invokes fn on items.
func Go[T any](ctx context.Context, limiter *semaphore.Weighted, fn GoFunc[T], items ...T) error {
eg, egCtx := errgroup.WithContext(ctx)
for _, item := range items {
region := LimitRegion(ctx, limiter)
if err := region.Start(); err != nil {
return err
}
eg.Go(func(t T) func() error {
return func() error {
defer region.End()
return fn(egCtx, region, t)
}
}(item))
}
return eg.Wait()
}
oras-go-2.5.0/internal/syncutil/limitgroup.go 0000664 0000000 0000000 00000004104 14576745303 0021312 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 syncutil
import (
"context"
"golang.org/x/sync/errgroup"
)
// A LimitedGroup is a collection of goroutines working on subtasks that are part of
// the same overall task.
type LimitedGroup struct {
grp *errgroup.Group
ctx context.Context
}
// LimitGroup returns a new LimitedGroup and an associated Context derived from ctx.
//
// The number of active goroutines in this group is limited to the given limit.
// A negative value indicates no limit.
//
// The derived Context is canceled the first time a function passed to Go
// returns a non-nil error or the first time Wait returns, whichever occurs
// first.
func LimitGroup(ctx context.Context, limit int) (*LimitedGroup, context.Context) {
grp, ctx := errgroup.WithContext(ctx)
grp.SetLimit(limit)
return &LimitedGroup{grp: grp, ctx: ctx}, ctx
}
// Go calls the given function in a new goroutine.
// It blocks until the new goroutine can be added without the number of
// active goroutines in the group exceeding the configured limit.
//
// The first call to return a non-nil error cancels the group's context.
// After which, any subsequent calls to Go will not execute their given function.
// The error will be returned by Wait.
func (g *LimitedGroup) Go(f func() error) {
g.grp.Go(func() error {
select {
case <-g.ctx.Done():
return g.ctx.Err()
default:
return f()
}
})
}
// Wait blocks until all function calls from the Go method have returned, then
// returns the first non-nil error (if any) from them.
func (g *LimitedGroup) Wait() error {
return g.grp.Wait()
}
oras-go-2.5.0/internal/syncutil/merge.go 0000664 0000000 0000000 00000007370 14576745303 0020226 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 syncutil
import "sync"
// mergeStatus represents the merge status of an item.
type mergeStatus struct {
// main indicates if items are being merged by the current go-routine.
main bool
// err represents the error of the merge operation.
err error
}
// Merge represents merge operations on items.
// The state transfer is shown as below:
//
// +----------+
// | Start +--------+-------------+
// +----+-----+ | |
// | | |
// v v v
// +----+-----+ +----+----+ +----+----+
// +-------+ Prepare +<--+ Pending +-->+ Waiting |
// | +----+-----+ +---------+ +----+----+
// | | |
// | v |
// | + ---+---- + |
// On Error | Resolve | |
// | + ---+---- + |
// | | |
// | v |
// | +----+-----+ |
// +------>+ Complete +<---------------------+
// +----+-----+
// |
// v
// +----+-----+
// | End |
// +----------+
type Merge[T any] struct {
lock sync.Mutex
committed bool
items []T
status chan mergeStatus
pending []T
pendingStatus chan mergeStatus
}
// Do merges concurrent operations of items into a single call of prepare and
// resolve.
// If Do is called multiple times concurrently, only one of the calls will be
// selected to invoke prepare and resolve.
func (m *Merge[T]) Do(item T, prepare func() error, resolve func(items []T) error) error {
status := <-m.assign(item)
if status.main {
err := prepare()
items := m.commit()
if err == nil {
err = resolve(items)
}
m.complete(err)
return err
}
return status.err
}
// assign adds a new item into the item list.
func (m *Merge[T]) assign(item T) <-chan mergeStatus {
m.lock.Lock()
defer m.lock.Unlock()
if m.committed {
if m.pendingStatus == nil {
m.pendingStatus = make(chan mergeStatus, 1)
}
m.pending = append(m.pending, item)
return m.pendingStatus
}
if m.status == nil {
m.status = make(chan mergeStatus, 1)
m.status <- mergeStatus{main: true}
}
m.items = append(m.items, item)
return m.status
}
// commit closes the assignment window, and the assigned items will be ready
// for resolve.
func (m *Merge[T]) commit() []T {
m.lock.Lock()
defer m.lock.Unlock()
m.committed = true
return m.items
}
// complete completes the previous merge, and moves the pending items to the
// stage for the next merge.
func (m *Merge[T]) complete(err error) {
// notify results
if err == nil {
close(m.status)
} else {
remaining := len(m.items) - 1
status := m.status
for remaining > 0 {
status <- mergeStatus{err: err}
remaining--
}
}
// move pending items to the stage
m.lock.Lock()
defer m.lock.Unlock()
m.committed = false
m.items = m.pending
m.status = m.pendingStatus
m.pending = nil
m.pendingStatus = nil
if m.status != nil {
m.status <- mergeStatus{main: true}
}
}
oras-go-2.5.0/internal/syncutil/merge_test.go 0000664 0000000 0000000 00000002700 14576745303 0021255 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 syncutil
import (
"context"
"testing"
"golang.org/x/sync/errgroup"
)
func TestMerge(t *testing.T) {
var merge Merge[int]
// generate expected result
size := 100
factor := -1
var expected int
for i := 1; i < size; i++ {
expected += i * factor
}
// test merge
ctx := context.Background()
eg, _ := errgroup.WithContext(ctx)
var result int
for i := 1; i < size; i++ {
eg.Go(func(num int) func() error {
return func() error {
var f int
getFactor := func() error {
f = factor
return nil
}
calculate := func(items []int) error {
for _, item := range items {
result += item * f
}
return nil
}
return merge.Do(num, getFactor, calculate)
}
}(i))
}
if err := eg.Wait(); err != nil {
t.Errorf("Merge.Do() error = %v, wantErr %v", err, nil)
}
if result != expected {
t.Errorf("Merge.Do() = %v, want %v", result, expected)
}
}
oras-go-2.5.0/internal/syncutil/once.go 0000664 0000000 0000000 00000005131 14576745303 0020044 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 syncutil
import (
"context"
"sync"
"sync/atomic"
)
// Once is an object that will perform exactly one action.
// Unlike sync.Once, this Once allows the action to have return values.
type Once struct {
result interface{}
err error
status chan bool
}
// NewOnce creates a new Once instance.
func NewOnce() *Once {
status := make(chan bool, 1)
status <- true
return &Once{
status: status,
}
}
// Do calls the function f if and only if Do is being called first time or all
// previous function calls are cancelled, deadline exceeded, or panicking.
// When `once.Do(ctx, f)` is called multiple times, the return value of the
// first call of the function f is stored, and is directly returned for other
// calls.
// Besides the return value of the function f, including the error, Do returns
// true if the function f passed is called first and is not cancelled, deadline
// exceeded, or panicking. Otherwise, returns false.
func (o *Once) Do(ctx context.Context, f func() (interface{}, error)) (bool, interface{}, error) {
defer func() {
if r := recover(); r != nil {
o.status <- true
panic(r)
}
}()
for {
select {
case inProgress := <-o.status:
if !inProgress {
return false, o.result, o.err
}
result, err := f()
if err == context.Canceled || err == context.DeadlineExceeded {
o.status <- true
return false, nil, err
}
o.result, o.err = result, err
close(o.status)
return true, result, err
case <-ctx.Done():
return false, nil, ctx.Err()
}
}
}
// OnceOrRetry is an object that will perform exactly one success action.
type OnceOrRetry struct {
done atomic.Bool
lock sync.Mutex
}
// OnceOrRetry calls the function f if and only if Do is being called for the
// first time for this instance of Once or all previous calls to Do are failed.
func (o *OnceOrRetry) Do(f func() error) error {
// fast path
if o.done.Load() {
return nil
}
// slow path
o.lock.Lock()
defer o.lock.Unlock()
if o.done.Load() {
return nil
}
if err := f(); err != nil {
return err
}
o.done.Store(true)
return nil
}
oras-go-2.5.0/internal/syncutil/once_test.go 0000664 0000000 0000000 00000015431 14576745303 0021107 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 syncutil
import (
"context"
"errors"
"io"
"reflect"
"strconv"
"sync"
"sync/atomic"
"testing"
"time"
)
func TestOnce_Do(t *testing.T) {
var f []func() (interface{}, error)
for i := 0; i < 100; i++ {
f = append(f, func(i int) func() (interface{}, error) {
return func() (interface{}, error) {
return i + 1, errors.New(strconv.Itoa(i))
}
}(i))
}
once := NewOnce()
first := make([]bool, len(f))
result := make([]interface{}, len(f))
err := make([]error, len(f))
var wg sync.WaitGroup
for i := 0; i < len(f); i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
ctx := context.Background()
first[i], result[i], err[i] = once.Do(ctx, f[i])
}(i)
}
wg.Wait()
target := 0
for i := 0; i < len(f); i++ {
if first[i] {
target = i
break
}
}
targetErr := err[target]
if targetErr == nil || targetErr.Error() != strconv.Itoa(target) {
t.Errorf("Once.Do(%d) error = %v, wantErr %v", target, targetErr, strconv.Itoa(target))
}
wantResult := target + 1
wantErr := targetErr
for i := 0; i < len(f); i++ {
wantFirst := false
if i == target {
wantFirst = true
}
if first[i] != wantFirst {
t.Errorf("Once.Do(%d) first = %v, want %v", i, first[i], wantFirst)
}
if err[i] != wantErr {
t.Errorf("Once.Do(%d) error = %v, wantErr %v", i, err[i], wantErr)
}
if !reflect.DeepEqual(result[i], wantResult) {
t.Errorf("Once.Do(%d) result = %v, want %v", i, result[i], wantResult)
}
}
}
func TestOnce_Do_Cancel_Context(t *testing.T) {
once := NewOnce()
var wg sync.WaitGroup
var (
first bool
result interface{}
err error
)
wg.Add(1)
go func() {
defer wg.Done()
ctx := context.Background()
first, result, err = once.Do(ctx, func() (interface{}, error) {
time.Sleep(200 * time.Millisecond)
return "foo", io.EOF
})
}()
time.Sleep(100 * time.Millisecond)
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
cancel()
first2, result2, err2 := once.Do(ctx, func() (interface{}, error) {
return "bar", nil
})
wg.Wait()
if wantFirst := true; first != wantFirst {
t.Fatalf("Once.Do() first = %v, want %v", first, wantFirst)
}
if wantErr := io.EOF; err != wantErr {
t.Fatalf("Once.Do() error = %v, wantErr %v", err, wantErr)
}
if wantResult := "foo"; !reflect.DeepEqual(result, wantResult) {
t.Fatalf("Once.Do() result = %v, want %v", result, wantResult)
}
if wantFirst := false; first2 != wantFirst {
t.Fatalf("Once.Do() first = %v, want %v", first2, wantFirst)
}
if wantErr := context.Canceled; err2 != wantErr {
t.Fatalf("Once.Do() error = %v, wantErr %v", err2, wantErr)
}
if wantResult := interface{}(nil); !reflect.DeepEqual(result2, wantResult) {
t.Fatalf("Once.Do() result = %v, want %v", result2, wantResult)
}
}
func TestOnce_Do_Cancel_Function(t *testing.T) {
ctx := context.Background()
once := NewOnce()
first, result, err := once.Do(ctx, func() (interface{}, error) {
return "foo", context.Canceled
})
if wantFirst := false; first != wantFirst {
t.Fatalf("Once.Do() first = %v, want %v", first, wantFirst)
}
if wantErr := context.Canceled; err != wantErr {
t.Fatalf("Once.Do() error = %v, wantErr %v", err, wantErr)
}
if wantResult := interface{}(nil); !reflect.DeepEqual(result, wantResult) {
t.Fatalf("Once.Do() result = %v, want %v", result, wantResult)
}
first, result, err = once.Do(ctx, func() (interface{}, error) {
return "bar", io.EOF
})
if wantFirst := true; first != wantFirst {
t.Fatalf("Once.Do() first = %v, want %v", first, wantFirst)
}
if wantErr := io.EOF; err != wantErr {
t.Fatalf("Once.Do() error = %v, wantErr %v", err, wantErr)
}
if wantResult := "bar"; !reflect.DeepEqual(result, wantResult) {
t.Fatalf("Once.Do() result = %v, want %v", result, wantResult)
}
}
func TestOnce_Do_Cancel_Panic(t *testing.T) {
ctx := context.Background()
once := NewOnce()
func() {
defer func() {
got := recover()
want := "foo"
if got != want {
t.Fatalf("Once.Do() panic = %v, want %v", got, want)
}
}()
once.Do(ctx, func() (interface{}, error) {
panic("foo")
})
}()
first, result, err := once.Do(ctx, func() (interface{}, error) {
return "bar", io.EOF
})
if wantFirst := true; first != wantFirst {
t.Fatalf("Once.Do() first = %v, want %v", first, wantFirst)
}
if wantErr := io.EOF; err != wantErr {
t.Fatalf("Once.Do() error = %v, wantErr %v", err, wantErr)
}
if wantResult := "bar"; !reflect.DeepEqual(result, wantResult) {
t.Fatalf("Once.Do() result = %v, want %v", result, wantResult)
}
}
func TestOnceOrRetry_Do(t *testing.T) {
var once OnceOrRetry
var count atomic.Int32
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
err := once.Do(func() error {
count.Add(1)
return nil
})
if err != nil {
t.Errorf("OnceOrRetry.Do() error = %v, wantErr %v", err, nil)
}
}()
}
wg.Wait()
if got := count.Load(); got != 1 {
t.Fatal("OnceOrRetry.Do() called more than once")
}
}
func TestOnceOrRetry_Do_Fail(t *testing.T) {
var once OnceOrRetry
var wg sync.WaitGroup
// test failure
for i := 0; i < 100; i++ {
wg.Add(1)
go func(wantErr error) {
defer wg.Done()
err := once.Do(func() error {
return wantErr
})
if err != wantErr {
t.Errorf("OnceOrRetry.Do() error = %v, wantErr %v", err, wantErr)
}
}(errors.New(strconv.Itoa(i)))
}
wg.Wait()
// retry after failure
err := once.Do(func() error {
return nil
})
if err != nil {
t.Fatalf("OnceOrRetry.Do() error = %v, wantErr %v", err, nil)
}
// no retry after success
err = once.Do(func() error {
t.Fatal("OnceOrRetry.Do() called twice")
return nil
})
if err != nil {
t.Fatalf("OnceOrRetry.Do() error = %v, wantErr %v", err, nil)
}
}
func TestOnceOrRetry_Do_Panic(t *testing.T) {
var once OnceOrRetry
// test panic
func() {
defer func() {
if r := recover(); r == nil {
t.Fatal("OnceOrRetry.Do() did not panic")
}
}()
_ = once.Do(func() error {
panic("failed")
})
}()
// retry after panic
err := once.Do(func() error {
return nil
})
if err != nil {
t.Fatalf("OnceOrRetry.Do() error = %v, wantErr %v", err, nil)
}
// no retry after success
err = once.Do(func() error {
t.Fatal("OnceOrRetry.Do() called twice")
return nil
})
if err != nil {
t.Fatalf("OnceOrRetry.Do() error = %v, wantErr %v", err, nil)
}
}
oras-go-2.5.0/internal/syncutil/pool.go 0000664 0000000 0000000 00000003035 14576745303 0020072 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 syncutil
import "sync"
// poolItem represents an item in Pool.
type poolItem[T any] struct {
value T
refCount int
}
// Pool is a scalable pool with items identified by keys.
type Pool[T any] struct {
// New optionally specifies a function to generate a value when Get would
// otherwise return nil.
// It may not be changed concurrently with calls to Get.
New func() T
lock sync.Mutex
items map[any]*poolItem[T]
}
// Get gets the value identified by key.
// The caller should invoke the returned function after using the returned item.
func (p *Pool[T]) Get(key any) (*T, func()) {
p.lock.Lock()
defer p.lock.Unlock()
item, ok := p.items[key]
if !ok {
if p.items == nil {
p.items = make(map[any]*poolItem[T])
}
item = &poolItem[T]{}
if p.New != nil {
item.value = p.New()
}
p.items[key] = item
}
item.refCount++
return &item.value, func() {
p.lock.Lock()
defer p.lock.Unlock()
item.refCount--
if item.refCount <= 0 {
delete(p.items, key)
}
}
}
oras-go-2.5.0/internal/syncutil/pool_test.go 0000664 0000000 0000000 00000003152 14576745303 0021131 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 syncutil
import (
"sync"
"sync/atomic"
"testing"
)
func TestPool(t *testing.T) {
var pool Pool[int64]
numbers := [][]int{
{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
{-1, -2, -3, -4, -5, -6, -7, -8, -9, -10},
}
// generate expected result
expected := make([]int, len(numbers))
for i, nums := range numbers {
for _, num := range nums {
expected[i] += num
}
}
// test pool
for i, nums := range numbers {
val, done := pool.Get(i)
*val = 0
var wg sync.WaitGroup
for _, num := range nums {
wg.Add(1)
go func(n int) {
defer wg.Done()
val, done := pool.Get(i)
defer done()
atomic.AddInt64(val, int64(n))
}(num)
}
wg.Wait()
item := pool.items[i]
if got := item.value; got != int64(expected[i]) {
t.Errorf("Pool.Get(%v).value = %v, want %v", i, got, expected[i])
}
if got := item.refCount; got != 1 {
t.Errorf("Pool.Get(%v).refCount = %v, want %v", i, got, 1)
}
// item should be cleaned up after done
done()
got := pool.items[i]
if got != nil {
t.Errorf("Pool.Get(%v) = %v, want %v", i, got, nil)
}
}
}
oras-go-2.5.0/pack.go 0000664 0000000 0000000 00000042473 14576745303 0014362 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 oras
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"maps"
"regexp"
"time"
specs "github.com/opencontainers/image-spec/specs-go"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/internal/spec"
)
const (
// MediaTypeUnknownConfig is the default config mediaType used
// - for [Pack] when PackOptions.PackImageManifest is true and
// PackOptions.ConfigDescriptor is not specified.
// - for [PackManifest] when packManifestVersion is PackManifestVersion1_0
// and PackManifestOptions.ConfigDescriptor is not specified.
MediaTypeUnknownConfig = "application/vnd.unknown.config.v1+json"
// MediaTypeUnknownArtifact is the default artifactType used for [Pack]
// when PackOptions.PackImageManifest is false and artifactType is
// not specified.
MediaTypeUnknownArtifact = "application/vnd.unknown.artifact.v1"
)
var (
// ErrInvalidDateTimeFormat is returned by [Pack] and [PackManifest] when
// AnnotationArtifactCreated or AnnotationCreated is provided, but its value
// is not in RFC 3339 format.
// Reference: https://www.rfc-editor.org/rfc/rfc3339#section-5.6
ErrInvalidDateTimeFormat = errors.New("invalid date and time format")
// ErrMissingArtifactType is returned by [PackManifest] when
// packManifestVersion is PackManifestVersion1_1 and artifactType is
// empty and the config media type is set to
// "application/vnd.oci.empty.v1+json".
ErrMissingArtifactType = errors.New("missing artifact type")
)
// PackManifestVersion represents the manifest version used for [PackManifest].
type PackManifestVersion int
const (
// PackManifestVersion1_0 represents the OCI Image Manifest defined in
// image-spec v1.0.2.
// Reference: https://github.com/opencontainers/image-spec/blob/v1.0.2/manifest.md
PackManifestVersion1_0 PackManifestVersion = 1
// PackManifestVersion1_1_RC4 represents the OCI Image Manifest defined
// in image-spec v1.1.0-rc4.
// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc4/manifest.md
//
// Deprecated: This constant is deprecated and not recommended for future use.
// Use [PackManifestVersion1_1] instead.
PackManifestVersion1_1_RC4 PackManifestVersion = PackManifestVersion1_1
// PackManifestVersion1_1 represents the OCI Image Manifest defined in
// image-spec v1.1.0.
// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md
PackManifestVersion1_1 PackManifestVersion = 2
)
// PackManifestOptions contains optional parameters for [PackManifest].
type PackManifestOptions struct {
// Subject is the subject of the manifest.
// This option is only valid when PackManifestVersion is
// NOT PackManifestVersion1_0.
Subject *ocispec.Descriptor
// Layers is the layers of the manifest.
Layers []ocispec.Descriptor
// ManifestAnnotations is the annotation map of the manifest.
ManifestAnnotations map[string]string
// ConfigDescriptor is a pointer to the descriptor of the config blob.
// If not nil, ConfigAnnotations will be ignored.
ConfigDescriptor *ocispec.Descriptor
// ConfigAnnotations is the annotation map of the config descriptor.
// This option is valid only when ConfigDescriptor is nil.
ConfigAnnotations map[string]string
}
// mediaTypeRegexp checks the format of media types.
// References:
// - https://github.com/opencontainers/image-spec/blob/v1.1.0/schema/defs-descriptor.json#L7
// - https://datatracker.ietf.org/doc/html/rfc6838#section-4.2
var mediaTypeRegexp = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}$`)
// PackManifest generates an OCI Image Manifest based on the given parameters
// and pushes the packed manifest to a content storage using pusher. The version
// of the manifest to be packed is determined by packManifestVersion
// (Recommended value: PackManifestVersion1_1).
//
// - If packManifestVersion is [PackManifestVersion1_1]:
// artifactType MUST NOT be empty unless opts.ConfigDescriptor is specified.
// - If packManifestVersion is [PackManifestVersion1_0]:
// if opts.ConfigDescriptor is nil, artifactType will be used as the
// config media type; if artifactType is empty,
// "application/vnd.unknown.config.v1+json" will be used.
// if opts.ConfigDescriptor is NOT nil, artifactType will be ignored.
//
// artifactType and opts.ConfigDescriptor.MediaType MUST comply with RFC 6838.
//
// If succeeded, returns a descriptor of the packed manifest.
func PackManifest(ctx context.Context, pusher content.Pusher, packManifestVersion PackManifestVersion, artifactType string, opts PackManifestOptions) (ocispec.Descriptor, error) {
switch packManifestVersion {
case PackManifestVersion1_0:
return packManifestV1_0(ctx, pusher, artifactType, opts)
case PackManifestVersion1_1:
return packManifestV1_1(ctx, pusher, artifactType, opts)
default:
return ocispec.Descriptor{}, fmt.Errorf("PackManifestVersion(%v): %w", packManifestVersion, errdef.ErrUnsupported)
}
}
// PackOptions contains optional parameters for [Pack].
//
// Deprecated: This type is deprecated and not recommended for future use.
// Use [PackManifestOptions] instead.
type PackOptions struct {
// Subject is the subject of the manifest.
Subject *ocispec.Descriptor
// ManifestAnnotations is the annotation map of the manifest.
ManifestAnnotations map[string]string
// PackImageManifest controls whether to pack an OCI Image Manifest or not.
// - If true, pack an OCI Image Manifest.
// - If false, pack an OCI Artifact Manifest (deprecated).
//
// Default value: false.
PackImageManifest bool
// ConfigDescriptor is a pointer to the descriptor of the config blob.
// If not nil, artifactType will be implied by the mediaType of the
// specified ConfigDescriptor, and ConfigAnnotations will be ignored.
// This option is valid only when PackImageManifest is true.
ConfigDescriptor *ocispec.Descriptor
// ConfigAnnotations is the annotation map of the config descriptor.
// This option is valid only when PackImageManifest is true
// and ConfigDescriptor is nil.
ConfigAnnotations map[string]string
}
// Pack packs the given blobs, generates a manifest for the pack,
// and pushes it to a content storage.
//
// When opts.PackImageManifest is true, artifactType will be used as the
// the config descriptor mediaType of the image manifest.
//
// If succeeded, returns a descriptor of the manifest.
//
// Deprecated: This method is deprecated and not recommended for future use.
// Use [PackManifest] instead.
func Pack(ctx context.Context, pusher content.Pusher, artifactType string, blobs []ocispec.Descriptor, opts PackOptions) (ocispec.Descriptor, error) {
if opts.PackImageManifest {
return packManifestV1_1_RC2(ctx, pusher, artifactType, blobs, opts)
}
return packArtifact(ctx, pusher, artifactType, blobs, opts)
}
// packArtifact packs an Artifact manifest as defined in image-spec v1.1.0-rc2.
// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/artifact.md
func packArtifact(ctx context.Context, pusher content.Pusher, artifactType string, blobs []ocispec.Descriptor, opts PackOptions) (ocispec.Descriptor, error) {
if artifactType == "" {
artifactType = MediaTypeUnknownArtifact
}
annotations, err := ensureAnnotationCreated(opts.ManifestAnnotations, spec.AnnotationArtifactCreated)
if err != nil {
return ocispec.Descriptor{}, err
}
manifest := spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
ArtifactType: artifactType,
Blobs: blobs,
Subject: opts.Subject,
Annotations: annotations,
}
return pushManifest(ctx, pusher, manifest, manifest.MediaType, manifest.ArtifactType, manifest.Annotations)
}
// packManifestV1_0 packs an image manifest defined in image-spec v1.0.2.
// Reference: https://github.com/opencontainers/image-spec/blob/v1.0.2/manifest.md
func packManifestV1_0(ctx context.Context, pusher content.Pusher, artifactType string, opts PackManifestOptions) (ocispec.Descriptor, error) {
if opts.Subject != nil {
return ocispec.Descriptor{}, fmt.Errorf("subject is not supported for manifest version %v: %w", PackManifestVersion1_0, errdef.ErrUnsupported)
}
// prepare config
var configDesc ocispec.Descriptor
if opts.ConfigDescriptor != nil {
if err := validateMediaType(opts.ConfigDescriptor.MediaType); err != nil {
return ocispec.Descriptor{}, fmt.Errorf("invalid config mediaType format: %w", err)
}
configDesc = *opts.ConfigDescriptor
} else {
if artifactType == "" {
artifactType = MediaTypeUnknownConfig
} else if err := validateMediaType(artifactType); err != nil {
return ocispec.Descriptor{}, fmt.Errorf("invalid artifactType format: %w", err)
}
var err error
configDesc, err = pushCustomEmptyConfig(ctx, pusher, artifactType, opts.ConfigAnnotations)
if err != nil {
return ocispec.Descriptor{}, err
}
}
annotations, err := ensureAnnotationCreated(opts.ManifestAnnotations, ocispec.AnnotationCreated)
if err != nil {
return ocispec.Descriptor{}, err
}
if opts.Layers == nil {
opts.Layers = []ocispec.Descriptor{} // make it an empty array to prevent potential server-side bugs
}
manifest := ocispec.Manifest{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
Config: configDesc,
MediaType: ocispec.MediaTypeImageManifest,
Layers: opts.Layers,
Annotations: annotations,
}
return pushManifest(ctx, pusher, manifest, manifest.MediaType, manifest.Config.MediaType, manifest.Annotations)
}
// packManifestV1_1_RC2 packs an image manifest as defined in image-spec
// v1.1.0-rc2.
// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/manifest.md
func packManifestV1_1_RC2(ctx context.Context, pusher content.Pusher, configMediaType string, layers []ocispec.Descriptor, opts PackOptions) (ocispec.Descriptor, error) {
if configMediaType == "" {
configMediaType = MediaTypeUnknownConfig
}
// prepare config
var configDesc ocispec.Descriptor
if opts.ConfigDescriptor != nil {
configDesc = *opts.ConfigDescriptor
} else {
var err error
configDesc, err = pushCustomEmptyConfig(ctx, pusher, configMediaType, opts.ConfigAnnotations)
if err != nil {
return ocispec.Descriptor{}, err
}
}
annotations, err := ensureAnnotationCreated(opts.ManifestAnnotations, ocispec.AnnotationCreated)
if err != nil {
return ocispec.Descriptor{}, err
}
if layers == nil {
layers = []ocispec.Descriptor{} // make it an empty array to prevent potential server-side bugs
}
manifest := ocispec.Manifest{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
Config: configDesc,
MediaType: ocispec.MediaTypeImageManifest,
Layers: layers,
Subject: opts.Subject,
Annotations: annotations,
}
return pushManifest(ctx, pusher, manifest, manifest.MediaType, manifest.Config.MediaType, manifest.Annotations)
}
// packManifestV1_1 packs an image manifest defined in image-spec v1.1.0.
// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md#guidelines-for-artifact-usage
func packManifestV1_1(ctx context.Context, pusher content.Pusher, artifactType string, opts PackManifestOptions) (ocispec.Descriptor, error) {
if artifactType == "" && (opts.ConfigDescriptor == nil || opts.ConfigDescriptor.MediaType == ocispec.MediaTypeEmptyJSON) {
// artifactType MUST be set when config.mediaType is set to the empty value
return ocispec.Descriptor{}, ErrMissingArtifactType
}
if artifactType != "" {
if err := validateMediaType(artifactType); err != nil {
return ocispec.Descriptor{}, fmt.Errorf("invalid artifactType format: %w", err)
}
}
// prepare config
var emptyBlobExists bool
var configDesc ocispec.Descriptor
if opts.ConfigDescriptor != nil {
if err := validateMediaType(opts.ConfigDescriptor.MediaType); err != nil {
return ocispec.Descriptor{}, fmt.Errorf("invalid config mediaType format: %w", err)
}
configDesc = *opts.ConfigDescriptor
} else {
// use the empty descriptor for config
configDesc = ocispec.DescriptorEmptyJSON
configDesc.Annotations = opts.ConfigAnnotations
configBytes := ocispec.DescriptorEmptyJSON.Data
// push config
if err := pushIfNotExist(ctx, pusher, configDesc, configBytes); err != nil {
return ocispec.Descriptor{}, fmt.Errorf("failed to push config: %w", err)
}
emptyBlobExists = true
}
annotations, err := ensureAnnotationCreated(opts.ManifestAnnotations, ocispec.AnnotationCreated)
if err != nil {
return ocispec.Descriptor{}, err
}
if len(opts.Layers) == 0 {
// use the empty descriptor as the single layer
layerDesc := ocispec.DescriptorEmptyJSON
layerData := ocispec.DescriptorEmptyJSON.Data
if !emptyBlobExists {
if err := pushIfNotExist(ctx, pusher, layerDesc, layerData); err != nil {
return ocispec.Descriptor{}, fmt.Errorf("failed to push layer: %w", err)
}
}
opts.Layers = []ocispec.Descriptor{layerDesc}
}
manifest := ocispec.Manifest{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
Config: configDesc,
MediaType: ocispec.MediaTypeImageManifest,
Layers: opts.Layers,
Subject: opts.Subject,
ArtifactType: artifactType,
Annotations: annotations,
}
return pushManifest(ctx, pusher, manifest, manifest.MediaType, manifest.ArtifactType, manifest.Annotations)
}
// pushIfNotExist pushes data described by desc if it does not exist in the
// target.
func pushIfNotExist(ctx context.Context, pusher content.Pusher, desc ocispec.Descriptor, data []byte) error {
if ros, ok := pusher.(content.ReadOnlyStorage); ok {
exists, err := ros.Exists(ctx, desc)
if err != nil {
return fmt.Errorf("failed to check existence: %s: %s: %w", desc.Digest.String(), desc.MediaType, err)
}
if exists {
return nil
}
}
if err := pusher.Push(ctx, desc, bytes.NewReader(data)); err != nil && !errors.Is(err, errdef.ErrAlreadyExists) {
return fmt.Errorf("failed to push: %s: %s: %w", desc.Digest.String(), desc.MediaType, err)
}
return nil
}
// pushManifest marshals manifest into JSON bytes and pushes it.
func pushManifest(ctx context.Context, pusher content.Pusher, manifest any, mediaType string, artifactType string, annotations map[string]string) (ocispec.Descriptor, error) {
manifestJSON, err := json.Marshal(manifest)
if err != nil {
return ocispec.Descriptor{}, fmt.Errorf("failed to marshal manifest: %w", err)
}
manifestDesc := content.NewDescriptorFromBytes(mediaType, manifestJSON)
// populate ArtifactType and Annotations of the manifest into manifestDesc
manifestDesc.ArtifactType = artifactType
manifestDesc.Annotations = annotations
// push manifest
if err := pusher.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON)); err != nil && !errors.Is(err, errdef.ErrAlreadyExists) {
return ocispec.Descriptor{}, fmt.Errorf("failed to push manifest: %w", err)
}
return manifestDesc, nil
}
// pushCustomEmptyConfig generates and pushes an empty config blob.
func pushCustomEmptyConfig(ctx context.Context, pusher content.Pusher, mediaType string, annotations map[string]string) (ocispec.Descriptor, error) {
// Use an empty JSON object here, because some registries may not accept
// empty config blob.
// As of September 2022, GAR is known to return 400 on empty blob upload.
// See https://github.com/oras-project/oras-go/issues/294 for details.
configBytes := []byte("{}")
configDesc := content.NewDescriptorFromBytes(mediaType, configBytes)
configDesc.Annotations = annotations
// push config
if err := pushIfNotExist(ctx, pusher, configDesc, configBytes); err != nil {
return ocispec.Descriptor{}, fmt.Errorf("failed to push config: %w", err)
}
return configDesc, nil
}
// ensureAnnotationCreated ensures that annotationCreatedKey is in annotations,
// and that its value conforms to RFC 3339. Otherwise returns a new annotation
// map with annotationCreatedKey created.
func ensureAnnotationCreated(annotations map[string]string, annotationCreatedKey string) (map[string]string, error) {
if createdTime, ok := annotations[annotationCreatedKey]; ok {
// if annotationCreatedKey is provided, validate its format
if _, err := time.Parse(time.RFC3339, createdTime); err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidDateTimeFormat, err)
}
return annotations, nil
}
// copy the original annotation map
copied := make(map[string]string, len(annotations)+1)
maps.Copy(copied, annotations)
// set creation time in RFC 3339 format
// reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/annotations.md#pre-defined-annotation-keys
now := time.Now().UTC()
copied[annotationCreatedKey] = now.Format(time.RFC3339)
return copied, nil
}
// validateMediaType validates the format of mediaType.
func validateMediaType(mediaType string) error {
if !mediaTypeRegexp.MatchString(mediaType) {
return fmt.Errorf("%s: %w", mediaType, errdef.ErrInvalidMediaType)
}
return nil
}
oras-go-2.5.0/pack_test.go 0000664 0000000 0000000 00000103641 14576745303 0015414 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 oras
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"reflect"
"testing"
"time"
"github.com/opencontainers/go-digest"
specs "github.com/opencontainers/image-spec/specs-go"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/content/memory"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/internal/spec"
)
func Test_Pack_Artifact_NoOption(t *testing.T) {
s := memory.New()
// prepare test content
blobs := []ocispec.Descriptor{
content.NewDescriptorFromBytes("test", []byte("hello world")),
content.NewDescriptorFromBytes("test", []byte("goodbye world")),
}
artifactType := "application/vnd.test"
// test Pack
ctx := context.Background()
manifestDesc, err := Pack(ctx, s, artifactType, blobs, PackOptions{})
if err != nil {
t.Fatal("Oras.Pack() error =", err)
}
// verify blobs
var manifest spec.Artifact
rc, err := s.Fetch(ctx, manifestDesc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
if err := json.NewDecoder(rc).Decode(&manifest); err != nil {
t.Fatal("error decoding manifest, error =", err)
}
if err := rc.Close(); err != nil {
t.Fatal("Store.Fetch().Close() error =", err)
}
if !reflect.DeepEqual(manifest.Blobs, blobs) {
t.Errorf("Store.Fetch() = %v, want %v", manifest.Blobs, blobs)
}
// verify media type
if got := manifest.MediaType; got != spec.MediaTypeArtifactManifest {
t.Fatalf("got media type = %s, want %s", got, spec.MediaTypeArtifactManifest)
}
// verify artifact type
if got := manifest.ArtifactType; got != artifactType {
t.Fatalf("got artifact type = %s, want %s", got, artifactType)
}
// verify created time annotation
createdTime, ok := manifest.Annotations[spec.AnnotationArtifactCreated]
if !ok {
t.Errorf("Annotation %s = %v, want %v", spec.AnnotationArtifactCreated, ok, true)
}
_, err = time.Parse(time.RFC3339, createdTime)
if err != nil {
t.Errorf("error parsing created time: %s, error = %v", createdTime, err)
}
// verify descriptor artifact type
if want := manifest.ArtifactType; !reflect.DeepEqual(manifestDesc.ArtifactType, want) {
t.Errorf("got descriptor artifactType = %v, want %v", manifestDesc.ArtifactType, want)
}
// verify descriptor annotations
if want := manifest.Annotations; !reflect.DeepEqual(manifestDesc.Annotations, want) {
t.Errorf("got descriptor annotations = %v, want %v", manifestDesc.Annotations, want)
}
}
func Test_Pack_Artifact_WithOptions(t *testing.T) {
s := memory.New()
// prepare test content
blobs := []ocispec.Descriptor{
content.NewDescriptorFromBytes("test", []byte("hello world")),
content.NewDescriptorFromBytes("test", []byte("goodbye world")),
}
artifactType := "application/vnd.test"
annotations := map[string]string{
spec.AnnotationArtifactCreated: "2000-01-01T00:00:00Z",
"foo": "bar",
}
subjectManifest := []byte(`{"layers":[]}`)
subjectDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(subjectManifest),
Size: int64(len(subjectManifest)),
ArtifactType: artifactType,
Annotations: annotations,
}
configBytes := []byte("{}")
configDesc := content.NewDescriptorFromBytes("application/vnd.test.config", configBytes)
configAnnotations := map[string]string{"foo": "bar"}
// test Pack
ctx := context.Background()
opts := PackOptions{
Subject: &subjectDesc,
ManifestAnnotations: annotations,
ConfigDescriptor: &configDesc, // should not work
ConfigAnnotations: configAnnotations, // should not work
}
manifestDesc, err := Pack(ctx, s, artifactType, blobs, opts)
if err != nil {
t.Fatal("Oras.Pack() error =", err)
}
expectedManifest := spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
ArtifactType: artifactType,
Blobs: blobs,
Subject: opts.Subject,
Annotations: annotations,
}
expectedManifestBytes, err := json.Marshal(expectedManifest)
if err != nil {
t.Fatal("failed to marshal manifest:", err)
}
// verify manifest
rc, err := s.Fetch(ctx, manifestDesc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, expectedManifestBytes) {
t.Errorf("Store.Fetch() = %v, want %v", got, expectedManifestBytes)
}
// verify descriptor
expectedManifestDesc := content.NewDescriptorFromBytes(expectedManifest.MediaType, expectedManifestBytes)
expectedManifestDesc.ArtifactType = expectedManifest.ArtifactType
expectedManifestDesc.Annotations = expectedManifest.Annotations
if !reflect.DeepEqual(manifestDesc, expectedManifestDesc) {
t.Errorf("Pack() = %v, want %v", manifestDesc, expectedManifestDesc)
}
}
func Test_Pack_Artifact_NoBlob(t *testing.T) {
s := memory.New()
// test Pack
ctx := context.Background()
artifactType := "application/vnd.test"
manifestDesc, err := Pack(ctx, s, artifactType, nil, PackOptions{})
if err != nil {
t.Fatal("Oras.Pack() error =", err)
}
var manifest spec.Artifact
rc, err := s.Fetch(ctx, manifestDesc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
if err := json.NewDecoder(rc).Decode(&manifest); err != nil {
t.Fatal("error decoding manifest, error =", err)
}
if err := rc.Close(); err != nil {
t.Fatal("Store.Fetch().Close() error =", err)
}
// verify blobs
var expectedBlobs []ocispec.Descriptor
if !reflect.DeepEqual(manifest.Blobs, expectedBlobs) {
t.Errorf("Store.Fetch() = %v, want %v", manifest.Blobs, expectedBlobs)
}
}
func Test_Pack_Artifact_NoArtifactType(t *testing.T) {
s := memory.New()
ctx := context.Background()
manifestDesc, err := Pack(ctx, s, "", nil, PackOptions{})
if err != nil {
t.Fatal("Oras.Pack() error =", err)
}
var manifest spec.Artifact
rc, err := s.Fetch(ctx, manifestDesc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
if err := json.NewDecoder(rc).Decode(&manifest); err != nil {
t.Fatal("error decoding manifest, error =", err)
}
if err := rc.Close(); err != nil {
t.Fatal("Store.Fetch().Close() error =", err)
}
// verify artifact type
if manifestDesc.ArtifactType != MediaTypeUnknownArtifact {
t.Fatalf("got artifact type = %s, want %s", manifestDesc.ArtifactType, MediaTypeUnknownArtifact)
}
if manifest.ArtifactType != MediaTypeUnknownArtifact {
t.Fatalf("got artifact type = %s, want %s", manifest.ArtifactType, MediaTypeUnknownArtifact)
}
}
func Test_Pack_Artifact_InvalidDateTimeFormat(t *testing.T) {
s := memory.New()
ctx := context.Background()
opts := PackOptions{
ManifestAnnotations: map[string]string{
spec.AnnotationArtifactCreated: "2000/01/01 00:00:00",
},
}
artifactType := "application/vnd.test"
_, err := Pack(ctx, s, artifactType, nil, opts)
if wantErr := ErrInvalidDateTimeFormat; !errors.Is(err, wantErr) {
t.Errorf("Oras.Pack() error = %v, wantErr = %v", err, wantErr)
}
}
func Test_Pack_ImageV1_1_RC2(t *testing.T) {
s := memory.New()
// prepare test content
layers := []ocispec.Descriptor{
content.NewDescriptorFromBytes("test", []byte("hello world")),
content.NewDescriptorFromBytes("test", []byte("goodbye world")),
}
// test Pack
ctx := context.Background()
artifactType := "application/vnd.test"
manifestDesc, err := Pack(ctx, s, artifactType, layers, PackOptions{PackImageManifest: true})
if err != nil {
t.Fatal("Oras.Pack() error =", err)
}
var manifest ocispec.Manifest
rc, err := s.Fetch(ctx, manifestDesc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
if err := json.NewDecoder(rc).Decode(&manifest); err != nil {
t.Fatal("error decoding manifest, error =", err)
}
if err := rc.Close(); err != nil {
t.Fatal("Store.Fetch().Close() error =", err)
}
// verify media type
got := manifest.MediaType
if got != ocispec.MediaTypeImageManifest {
t.Fatalf("got media type = %s, want %s", got, ocispec.MediaTypeImageManifest)
}
// verify config
expectedConfigBytes := []byte("{}")
expectedConfig := ocispec.Descriptor{
MediaType: artifactType,
Digest: digest.FromBytes(expectedConfigBytes),
Size: int64(len(expectedConfigBytes)),
}
if !reflect.DeepEqual(manifest.Config, expectedConfig) {
t.Errorf("got config = %v, want %v", manifest.Config, expectedConfig)
}
// verify layers
if !reflect.DeepEqual(manifest.Layers, layers) {
t.Errorf("got layers = %v, want %v", manifest.Layers, layers)
}
// verify created time annotation
createdTime, ok := manifest.Annotations[ocispec.AnnotationCreated]
if !ok {
t.Errorf("Annotation %s = %v, want %v", ocispec.AnnotationCreated, ok, true)
}
_, err = time.Parse(time.RFC3339, createdTime)
if err != nil {
t.Errorf("error parsing created time: %s, error = %v", createdTime, err)
}
// verify descriptor annotations
if want := manifest.Annotations; !reflect.DeepEqual(manifestDesc.Annotations, want) {
t.Errorf("got descriptor annotations = %v, want %v", manifestDesc.Annotations, want)
}
}
func Test_Pack_ImageV1_1_RC2_WithOptions(t *testing.T) {
s := memory.New()
// prepare test content
layers := []ocispec.Descriptor{
content.NewDescriptorFromBytes("test", []byte("hello world")),
content.NewDescriptorFromBytes("test", []byte("goodbye world")),
}
configBytes := []byte("{}")
configDesc := content.NewDescriptorFromBytes("application/vnd.test.config", configBytes)
configAnnotations := map[string]string{"foo": "bar"}
annotations := map[string]string{
ocispec.AnnotationCreated: "2000-01-01T00:00:00Z",
"foo": "bar",
}
artifactType := "application/vnd.test"
subjectManifest := []byte(`{"layers":[]}`)
subjectDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(subjectManifest),
Size: int64(len(subjectManifest)),
}
// test Pack with ConfigDescriptor
ctx := context.Background()
opts := PackOptions{
PackImageManifest: true,
Subject: &subjectDesc,
ConfigDescriptor: &configDesc,
ConfigAnnotations: configAnnotations,
ManifestAnnotations: annotations,
}
manifestDesc, err := Pack(ctx, s, artifactType, layers, opts)
if err != nil {
t.Fatal("Oras.Pack() error =", err)
}
expectedManifest := ocispec.Manifest{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageManifest,
Subject: &subjectDesc,
Config: configDesc,
Layers: layers,
Annotations: annotations,
}
expectedManifestBytes, err := json.Marshal(expectedManifest)
if err != nil {
t.Fatal("failed to marshal manifest:", err)
}
rc, err := s.Fetch(ctx, manifestDesc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, expectedManifestBytes) {
t.Errorf("Store.Fetch() = %v, want %v", string(got), string(expectedManifestBytes))
}
// verify descriptor
expectedManifestDesc := content.NewDescriptorFromBytes(expectedManifest.MediaType, expectedManifestBytes)
expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType
expectedManifestDesc.Annotations = expectedManifest.Annotations
if !reflect.DeepEqual(manifestDesc, expectedManifestDesc) {
t.Errorf("Pack() = %v, want %v", manifestDesc, expectedManifestDesc)
}
// test Pack without ConfigDescriptor
opts = PackOptions{
PackImageManifest: true,
Subject: &subjectDesc,
ConfigAnnotations: configAnnotations,
ManifestAnnotations: annotations,
}
manifestDesc, err = Pack(ctx, s, artifactType, layers, opts)
if err != nil {
t.Fatal("Oras.Pack() error =", err)
}
expectedConfigDesc := content.NewDescriptorFromBytes(artifactType, configBytes)
expectedConfigDesc.Annotations = configAnnotations
expectedManifest = ocispec.Manifest{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageManifest,
Subject: &subjectDesc,
Config: expectedConfigDesc,
Layers: layers,
Annotations: annotations,
}
expectedManifestBytes, err = json.Marshal(expectedManifest)
if err != nil {
t.Fatal("failed to marshal manifest:", err)
}
rc, err = s.Fetch(ctx, manifestDesc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got, err = io.ReadAll(rc)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, expectedManifestBytes) {
t.Errorf("Store.Fetch() = %v, want %v", string(got), string(expectedManifestBytes))
}
// verify descriptor
expectedManifestDesc = content.NewDescriptorFromBytes(expectedManifest.MediaType, expectedManifestBytes)
expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType
expectedManifestDesc.Annotations = expectedManifest.Annotations
if !reflect.DeepEqual(manifestDesc, expectedManifestDesc) {
t.Errorf("Pack() = %v, want %v", manifestDesc, expectedManifestDesc)
}
}
func Test_Pack_ImageV1_1_RC2_NoArtifactType(t *testing.T) {
s := memory.New()
ctx := context.Background()
manifestDesc, err := Pack(ctx, s, "", nil, PackOptions{PackImageManifest: true})
if err != nil {
t.Fatal("Oras.Pack() error =", err)
}
var manifest ocispec.Manifest
rc, err := s.Fetch(ctx, manifestDesc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
if err := json.NewDecoder(rc).Decode(&manifest); err != nil {
t.Fatal("error decoding manifest, error =", err)
}
if err := rc.Close(); err != nil {
t.Fatal("Store.Fetch().Close() error =", err)
}
// verify artifact type and config media type
if manifestDesc.ArtifactType != MediaTypeUnknownConfig {
t.Fatalf("got artifact type = %s, want %s", manifestDesc.ArtifactType, MediaTypeUnknownConfig)
}
if manifest.Config.MediaType != MediaTypeUnknownConfig {
t.Fatalf("got artifact type = %s, want %s", manifest.Config.MediaType, MediaTypeUnknownConfig)
}
}
func Test_Pack_ImageV1_1_RC2_NoLayer(t *testing.T) {
s := memory.New()
// test Pack
ctx := context.Background()
manifestDesc, err := Pack(ctx, s, "", nil, PackOptions{PackImageManifest: true})
if err != nil {
t.Fatal("Oras.Pack() error =", err)
}
var manifest ocispec.Manifest
rc, err := s.Fetch(ctx, manifestDesc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
if err := json.NewDecoder(rc).Decode(&manifest); err != nil {
t.Fatal("error decoding manifest, error =", err)
}
if err := rc.Close(); err != nil {
t.Fatal("Store.Fetch().Close() error =", err)
}
// verify layers
expectedLayers := []ocispec.Descriptor{}
if !reflect.DeepEqual(manifest.Layers, expectedLayers) {
t.Errorf("got layers = %v, want %v", manifest.Layers, expectedLayers)
}
}
func Test_Pack_ImageV1_1_RC2_InvalidDateTimeFormat(t *testing.T) {
s := memory.New()
ctx := context.Background()
opts := PackOptions{
PackImageManifest: true,
ManifestAnnotations: map[string]string{
ocispec.AnnotationCreated: "2000/01/01 00:00:00",
},
}
_, err := Pack(ctx, s, "", nil, opts)
if wantErr := ErrInvalidDateTimeFormat; !errors.Is(err, wantErr) {
t.Errorf("Oras.Pack() error = %v, wantErr = %v", err, wantErr)
}
}
func Test_PackManifest_ImageV1_0(t *testing.T) {
s := memory.New()
// test Pack
ctx := context.Background()
artifactType := "application/vnd.test"
manifestDesc, err := PackManifest(ctx, s, PackManifestVersion1_0, artifactType, PackManifestOptions{})
if err != nil {
t.Fatal("Oras.PackManifest() error =", err)
}
var manifest ocispec.Manifest
rc, err := s.Fetch(ctx, manifestDesc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
if err := json.NewDecoder(rc).Decode(&manifest); err != nil {
t.Fatal("error decoding manifest, error =", err)
}
if err := rc.Close(); err != nil {
t.Fatal("Store.Fetch().Close() error =", err)
}
// verify media type
got := manifest.MediaType
if got != ocispec.MediaTypeImageManifest {
t.Fatalf("got media type = %s, want %s", got, ocispec.MediaTypeImageManifest)
}
// verify config
expectedConfigBytes := []byte("{}")
expectedConfig := ocispec.Descriptor{
MediaType: artifactType,
Digest: digest.FromBytes(expectedConfigBytes),
Size: int64(len(expectedConfigBytes)),
}
if !reflect.DeepEqual(manifest.Config, expectedConfig) {
t.Errorf("got config = %v, want %v", manifest.Config, expectedConfig)
}
// verify layers
expectedLayers := []ocispec.Descriptor{}
if !reflect.DeepEqual(manifest.Layers, expectedLayers) {
t.Errorf("got layers = %v, want %v", manifest.Layers, expectedLayers)
}
// verify created time annotation
createdTime, ok := manifest.Annotations[ocispec.AnnotationCreated]
if !ok {
t.Errorf("Annotation %s = %v, want %v", ocispec.AnnotationCreated, ok, true)
}
_, err = time.Parse(time.RFC3339, createdTime)
if err != nil {
t.Errorf("error parsing created time: %s, error = %v", createdTime, err)
}
// verify descriptor annotations
if want := manifest.Annotations; !reflect.DeepEqual(manifestDesc.Annotations, want) {
t.Errorf("got descriptor annotations = %v, want %v", manifestDesc.Annotations, want)
}
}
func Test_PackManifest_ImageV1_0_WithOptions(t *testing.T) {
s := memory.New()
// prepare test content
layers := []ocispec.Descriptor{
content.NewDescriptorFromBytes("test", []byte("hello world")),
content.NewDescriptorFromBytes("test", []byte("goodbye world")),
}
configBytes := []byte("{}")
configDesc := content.NewDescriptorFromBytes("application/vnd.test.config", configBytes)
configAnnotations := map[string]string{"foo": "bar"}
annotations := map[string]string{
ocispec.AnnotationCreated: "2000-01-01T00:00:00Z",
"foo": "bar",
}
artifactType := "application/vnd.test"
// test PackManifest with ConfigDescriptor
ctx := context.Background()
opts := PackManifestOptions{
Layers: layers,
ConfigDescriptor: &configDesc,
ConfigAnnotations: configAnnotations,
ManifestAnnotations: annotations,
}
manifestDesc, err := PackManifest(ctx, s, PackManifestVersion1_0, artifactType, opts)
if err != nil {
t.Fatal("Oras.PackManifest() error =", err)
}
expectedManifest := ocispec.Manifest{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageManifest,
Config: configDesc,
Layers: layers,
Annotations: annotations,
}
expectedManifestBytes, err := json.Marshal(expectedManifest)
if err != nil {
t.Fatal("failed to marshal manifest:", err)
}
rc, err := s.Fetch(ctx, manifestDesc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, expectedManifestBytes) {
t.Errorf("Store.Fetch() = %v, want %v", string(got), string(expectedManifestBytes))
}
// verify descriptor
expectedManifestDesc := content.NewDescriptorFromBytes(expectedManifest.MediaType, expectedManifestBytes)
expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType
expectedManifestDesc.Annotations = expectedManifest.Annotations
if !reflect.DeepEqual(manifestDesc, expectedManifestDesc) {
t.Errorf("Pack() = %v, want %v", manifestDesc, expectedManifestDesc)
}
// test PackManifest without ConfigDescriptor
opts = PackManifestOptions{
Layers: layers,
ConfigAnnotations: configAnnotations,
ManifestAnnotations: annotations,
}
manifestDesc, err = PackManifest(ctx, s, PackManifestVersion1_0, artifactType, opts)
if err != nil {
t.Fatal("Oras.PackManifest() error =", err)
}
expectedConfigDesc := content.NewDescriptorFromBytes(artifactType, configBytes)
expectedConfigDesc.Annotations = configAnnotations
expectedManifest = ocispec.Manifest{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageManifest,
Config: expectedConfigDesc,
Layers: layers,
Annotations: annotations,
}
expectedManifestBytes, err = json.Marshal(expectedManifest)
if err != nil {
t.Fatal("failed to marshal manifest:", err)
}
rc, err = s.Fetch(ctx, manifestDesc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got, err = io.ReadAll(rc)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, expectedManifestBytes) {
t.Errorf("Store.Fetch() = %v, want %v", string(got), string(expectedManifestBytes))
}
// verify descriptor
expectedManifestDesc = content.NewDescriptorFromBytes(expectedManifest.MediaType, expectedManifestBytes)
expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType
expectedManifestDesc.Annotations = expectedManifest.Annotations
if !reflect.DeepEqual(manifestDesc, expectedManifestDesc) {
t.Errorf("PackManifest() = %v, want %v", manifestDesc, expectedManifestDesc)
}
}
func Test_PackManifest_ImageV1_0_SubjectUnsupported(t *testing.T) {
s := memory.New()
// prepare test content
artifactType := "application/vnd.test"
subjectManifest := []byte(`{"layers":[]}`)
subjectDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(subjectManifest),
Size: int64(len(subjectManifest)),
}
// test Pack with ConfigDescriptor
ctx := context.Background()
opts := PackManifestOptions{
Subject: &subjectDesc,
}
_, err := PackManifest(ctx, s, PackManifestVersion1_0, artifactType, opts)
if wantErr := errdef.ErrUnsupported; !errors.Is(err, wantErr) {
t.Errorf("Oras.PackManifest() error = %v, wantErr %v", err, wantErr)
}
}
func Test_PackManifest_ImageV1_0_NoArtifactType(t *testing.T) {
s := memory.New()
ctx := context.Background()
manifestDesc, err := PackManifest(ctx, s, PackManifestVersion1_0, "", PackManifestOptions{})
if err != nil {
t.Fatal("Oras.PackManifest() error =", err)
}
var manifest ocispec.Manifest
rc, err := s.Fetch(ctx, manifestDesc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
if err := json.NewDecoder(rc).Decode(&manifest); err != nil {
t.Fatal("error decoding manifest, error =", err)
}
if err := rc.Close(); err != nil {
t.Fatal("Store.Fetch().Close() error =", err)
}
// verify artifact type and config media type
if manifestDesc.ArtifactType != MediaTypeUnknownConfig {
t.Fatalf("got artifact type = %s, want %s", manifestDesc.ArtifactType, MediaTypeUnknownConfig)
}
if manifest.Config.MediaType != MediaTypeUnknownConfig {
t.Fatalf("got artifact type = %s, want %s", manifest.Config.MediaType, MediaTypeUnknownConfig)
}
}
func Test_PackManifest_ImageV1_0_InvalidMediaType(t *testing.T) {
s := memory.New()
ctx := context.Background()
// test invalid artifact type + valid config media type
artifactType := "random"
configBytes := []byte("{}")
configDesc := content.NewDescriptorFromBytes("application/vnd.test.config", configBytes)
opts := PackManifestOptions{
ConfigDescriptor: &configDesc,
}
_, err := PackManifest(ctx, s, PackManifestVersion1_0, artifactType, opts)
if err != nil {
t.Error("Oras.PackManifest() error =", err)
}
// test invalid config media type + valid artifact type
artifactType = "application/vnd.test"
configDesc = content.NewDescriptorFromBytes("random", configBytes)
opts = PackManifestOptions{
ConfigDescriptor: &configDesc,
}
_, err = PackManifest(ctx, s, PackManifestVersion1_0, artifactType, opts)
if wantErr := errdef.ErrInvalidMediaType; !errors.Is(err, wantErr) {
t.Errorf("Oras.PackManifest() error = %v, wantErr = %v", err, wantErr)
}
}
func Test_PackManifest_ImageV1_0_InvalidDateTimeFormat(t *testing.T) {
s := memory.New()
ctx := context.Background()
opts := PackManifestOptions{
ManifestAnnotations: map[string]string{
ocispec.AnnotationCreated: "2000/01/01 00:00:00",
},
}
_, err := PackManifest(ctx, s, PackManifestVersion1_0, "", opts)
if wantErr := ErrInvalidDateTimeFormat; !errors.Is(err, wantErr) {
t.Errorf("Oras.PackManifest() error = %v, wantErr = %v", err, wantErr)
}
}
func Test_PackManifest_ImageV1_1(t *testing.T) {
s := memory.New()
// test PackManifest
ctx := context.Background()
artifactType := "application/vnd.test"
manifestDesc, err := PackManifest(ctx, s, PackManifestVersion1_1, artifactType, PackManifestOptions{})
if err != nil {
t.Fatal("Oras.PackManifest() error =", err)
}
var manifest ocispec.Manifest
rc, err := s.Fetch(ctx, manifestDesc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
if err := json.NewDecoder(rc).Decode(&manifest); err != nil {
t.Fatal("error decoding manifest, error =", err)
}
if err := rc.Close(); err != nil {
t.Fatal("Store.Fetch().Close() error =", err)
}
// verify layers
expectedLayers := []ocispec.Descriptor{ocispec.DescriptorEmptyJSON}
if !reflect.DeepEqual(manifest.Layers, expectedLayers) {
t.Errorf("got layers = %v, want %v", manifest.Layers, expectedLayers)
}
}
func Test_PackManifest_ImageV1_1_WithOptions(t *testing.T) {
s := memory.New()
// prepare test content
layers := []ocispec.Descriptor{
content.NewDescriptorFromBytes("test", []byte("hello world")),
content.NewDescriptorFromBytes("test", []byte("goodbye world")),
}
configBytes := []byte("config")
configDesc := content.NewDescriptorFromBytes("application/vnd.test.config", configBytes)
configAnnotations := map[string]string{"foo": "bar"}
annotations := map[string]string{
ocispec.AnnotationCreated: "2000-01-01T00:00:00Z",
"foo": "bar",
}
artifactType := "application/vnd.test"
subjectManifest := []byte(`{"layers":[]}`)
subjectDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(subjectManifest),
Size: int64(len(subjectManifest)),
}
// test PackManifest with ConfigDescriptor
ctx := context.Background()
opts := PackManifestOptions{
Subject: &subjectDesc,
Layers: layers,
ConfigDescriptor: &configDesc,
ConfigAnnotations: configAnnotations,
ManifestAnnotations: annotations,
}
manifestDesc, err := PackManifest(ctx, s, PackManifestVersion1_1, artifactType, opts)
if err != nil {
t.Fatal("Oras.PackManifest() error =", err)
}
expectedManifest := ocispec.Manifest{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageManifest,
ArtifactType: artifactType,
Subject: &subjectDesc,
Config: configDesc,
Layers: layers,
Annotations: annotations,
}
expectedManifestBytes, err := json.Marshal(expectedManifest)
if err != nil {
t.Fatal("failed to marshal manifest:", err)
}
rc, err := s.Fetch(ctx, manifestDesc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got, err := io.ReadAll(rc)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, expectedManifestBytes) {
t.Errorf("Store.Fetch() = %v, want %v", string(got), string(expectedManifestBytes))
}
// verify descriptor
expectedManifestDesc := content.NewDescriptorFromBytes(expectedManifest.MediaType, expectedManifestBytes)
expectedManifestDesc.ArtifactType = expectedManifest.ArtifactType
expectedManifestDesc.Annotations = expectedManifest.Annotations
if !reflect.DeepEqual(manifestDesc, expectedManifestDesc) {
t.Errorf("PackManifest() = %v, want %v", manifestDesc, expectedManifestDesc)
}
// test PackManifest with ConfigDescriptor, but without artifactType
opts = PackManifestOptions{
Subject: &subjectDesc,
Layers: layers,
ConfigDescriptor: &configDesc,
ConfigAnnotations: configAnnotations,
ManifestAnnotations: annotations,
}
manifestDesc, err = PackManifest(ctx, s, PackManifestVersion1_1, "", opts)
if err != nil {
t.Fatal("Oras.PackManifest() error =", err)
}
expectedManifest = ocispec.Manifest{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageManifest,
Subject: &subjectDesc,
Config: configDesc,
Layers: layers,
Annotations: annotations,
}
expectedManifestBytes, err = json.Marshal(expectedManifest)
if err != nil {
t.Fatal("failed to marshal manifest:", err)
}
rc, err = s.Fetch(ctx, manifestDesc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got, err = io.ReadAll(rc)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, expectedManifestBytes) {
t.Errorf("Store.Fetch() = %v, want %v", string(got), string(expectedManifestBytes))
}
// verify descriptor
expectedManifestDesc = content.NewDescriptorFromBytes(expectedManifest.MediaType, expectedManifestBytes)
expectedManifestDesc.ArtifactType = expectedManifest.ArtifactType
expectedManifestDesc.Annotations = expectedManifest.Annotations
if !reflect.DeepEqual(manifestDesc, expectedManifestDesc) {
t.Errorf("PackManifest() = %v, want %v", manifestDesc, expectedManifestDesc)
}
// test Pack without ConfigDescriptor
opts = PackManifestOptions{
Subject: &subjectDesc,
Layers: layers,
ConfigAnnotations: configAnnotations,
ManifestAnnotations: annotations,
}
manifestDesc, err = PackManifest(ctx, s, PackManifestVersion1_1, artifactType, opts)
if err != nil {
t.Fatal("Oras.PackManifest() error =", err)
}
expectedConfigDesc := ocispec.DescriptorEmptyJSON
expectedConfigDesc.Annotations = configAnnotations
expectedManifest = ocispec.Manifest{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageManifest,
ArtifactType: artifactType,
Subject: &subjectDesc,
Config: expectedConfigDesc,
Layers: layers,
Annotations: annotations,
}
expectedManifestBytes, err = json.Marshal(expectedManifest)
if err != nil {
t.Fatal("failed to marshal manifest:", err)
}
rc, err = s.Fetch(ctx, manifestDesc)
if err != nil {
t.Fatal("Store.Fetch() error =", err)
}
got, err = io.ReadAll(rc)
if err != nil {
t.Fatal("Store.Fetch().Read() error =", err)
}
err = rc.Close()
if err != nil {
t.Error("Store.Fetch().Close() error =", err)
}
if !bytes.Equal(got, expectedManifestBytes) {
t.Errorf("Store.Fetch() = %v, want %v", string(got), string(expectedManifestBytes))
}
// verify descriptor
expectedManifestDesc = content.NewDescriptorFromBytes(expectedManifest.MediaType, expectedManifestBytes)
expectedManifestDesc.ArtifactType = expectedManifest.ArtifactType
expectedManifestDesc.Annotations = expectedManifest.Annotations
if !reflect.DeepEqual(manifestDesc, expectedManifestDesc) {
t.Errorf("PackManifest() = %v, want %v", manifestDesc, expectedManifestDesc)
}
}
func Test_PackManifest_ImageV1_1_NoArtifactType(t *testing.T) {
s := memory.New()
ctx := context.Background()
// test no artifact type and no config
_, err := PackManifest(ctx, s, PackManifestVersion1_1, "", PackManifestOptions{})
if wantErr := ErrMissingArtifactType; !errors.Is(err, wantErr) {
t.Errorf("Oras.PackManifest() error = %v, wantErr = %v", err, wantErr)
}
// test no artifact type and config with empty media type
opts := PackManifestOptions{
ConfigDescriptor: &ocispec.Descriptor{
MediaType: ocispec.DescriptorEmptyJSON.MediaType,
},
}
_, err = PackManifest(ctx, s, PackManifestVersion1_1, "", opts)
if wantErr := ErrMissingArtifactType; !errors.Is(err, wantErr) {
t.Errorf("Oras.PackManifest() error = %v, wantErr = %v", err, wantErr)
}
}
func Test_PackManifest_ImageV1_1_InvalidMediaType(t *testing.T) {
s := memory.New()
ctx := context.Background()
// test invalid artifact type + valid config media type
artifactType := "random"
configBytes := []byte("{}")
configDesc := content.NewDescriptorFromBytes("application/vnd.test.config", configBytes)
opts := PackManifestOptions{
ConfigDescriptor: &configDesc,
}
_, err := PackManifest(ctx, s, PackManifestVersion1_1, artifactType, opts)
if wantErr := errdef.ErrInvalidMediaType; !errors.Is(err, wantErr) {
t.Errorf("Oras.PackManifest() error = %v, wantErr = %v", err, wantErr)
}
// test invalid config media type + invalid artifact type
artifactType = "application/vnd.test"
configDesc = content.NewDescriptorFromBytes("random", configBytes)
opts = PackManifestOptions{
ConfigDescriptor: &configDesc,
}
_, err = PackManifest(ctx, s, PackManifestVersion1_1, artifactType, opts)
if wantErr := errdef.ErrInvalidMediaType; !errors.Is(err, wantErr) {
t.Errorf("Oras.PackManifest() error = %v, wantErr = %v", err, wantErr)
}
}
func Test_PackManifest_ImageV1_1_InvalidDateTimeFormat(t *testing.T) {
s := memory.New()
ctx := context.Background()
opts := PackManifestOptions{
ManifestAnnotations: map[string]string{
ocispec.AnnotationCreated: "2000/01/01 00:00:00",
},
}
artifactType := "application/vnd.test"
_, err := PackManifest(ctx, s, PackManifestVersion1_1, artifactType, opts)
if wantErr := ErrInvalidDateTimeFormat; !errors.Is(err, wantErr) {
t.Errorf("Oras.PackManifest() error = %v, wantErr = %v", err, wantErr)
}
}
func Test_PackManifest_UnsupportedPackManifestVersion(t *testing.T) {
s := memory.New()
ctx := context.Background()
_, err := PackManifest(ctx, s, -1, "", PackManifestOptions{})
if wantErr := errdef.ErrUnsupported; !errors.Is(err, wantErr) {
t.Errorf("Oras.PackManifest() error = %v, wantErr = %v", err, wantErr)
}
}
oras-go-2.5.0/registry/ 0000775 0000000 0000000 00000000000 14576745303 0014753 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/registry/example_reference_test.go 0000664 0000000 0000000 00000002556 14576745303 0022022 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 registry_test
import (
_ "crypto/sha256" // required to parse sha256 digest. See [Reference.Digest]
"fmt"
"oras.land/oras-go/v2/registry"
)
// ExampleParseReference_digest demonstrates parsing a reference string with
// digest and print its components.
func ExampleParseReference_digest() {
rawRef := "ghcr.io/oras-project/oras-go@sha256:601d05a48832e7946dab8f49b14953549bebf42e42f4d7973b1a5a287d77ab76"
ref, err := registry.ParseReference(rawRef)
if err != nil {
panic(err)
}
fmt.Println("Registry:", ref.Registry)
fmt.Println("Repository:", ref.Repository)
digest, err := ref.Digest()
if err != nil {
panic(err)
}
fmt.Println("Digest:", digest)
// Output:
// Registry: ghcr.io
// Repository: oras-project/oras-go
// Digest: sha256:601d05a48832e7946dab8f49b14953549bebf42e42f4d7973b1a5a287d77ab76
}
oras-go-2.5.0/registry/example_test.go 0000664 0000000 0000000 00000005251 14576745303 0017777 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 registry_test gives examples code of functions in the registry package.
package registry_test
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"
"oras.land/oras-go/v2/registry"
. "oras.land/oras-go/v2/registry/internal/doc"
"oras.land/oras-go/v2/registry/remote"
)
const _ = ExampleUnplayable
const exampleRepositoryName = "example"
var host string
func TestMain(m *testing.M) {
// Setup a local HTTPS registry
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p := r.URL.Path
m := r.Method
switch {
case p == "/v2/_catalog" && m == "GET":
result := struct {
Repositories []string `json:"repositories"`
}{
Repositories: []string{"public/repo1", "public/repo2", "internal/repo3"},
}
json.NewEncoder(w).Encode(result)
case p == fmt.Sprintf("/v2/%s/tags/list", exampleRepositoryName) && m == "GET":
result := struct {
Tags []string `json:"tags"`
}{
Tags: []string{"tag1", "tag2"},
}
json.NewEncoder(w).Encode(result)
}
}))
defer ts.Close()
u, err := url.Parse(ts.URL)
if err != nil {
panic(err)
}
host = u.Host
http.DefaultTransport = ts.Client().Transport
os.Exit(m.Run())
}
// ExampleRepositories gives example snippets for listing repositories in the registry without pagination.
func ExampleRepositories() {
reg, err := remote.NewRegistry(host)
if err != nil {
panic(err) // Handle error
}
ctx := context.Background()
repos, err := registry.Repositories(ctx, reg)
if err != nil {
panic(err) // Handle error
}
for _, repo := range repos {
fmt.Println(repo)
}
// Output:
// public/repo1
// public/repo2
// internal/repo3
}
// ExampleTags gives example snippets for listing tags in the repository without pagination.
func ExampleTags() {
repo, err := remote.NewRepository(fmt.Sprintf("%s/%s", host, exampleRepositoryName))
if err != nil {
panic(err) // Handle error
}
ctx := context.Background()
tags, err := registry.Tags(ctx, repo)
if err != nil {
panic(err) // Handle error
}
for _, tag := range tags {
fmt.Println(tag)
}
// Output:
// tag1
// tag2
}
oras-go-2.5.0/registry/internal/ 0000775 0000000 0000000 00000000000 14576745303 0016567 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/registry/internal/doc/ 0000775 0000000 0000000 00000000000 14576745303 0017334 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/registry/internal/doc/doc.go 0000664 0000000 0000000 00000002114 14576745303 0020426 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 doc provides the constant can be used in example test files. Most of
// the example tests in this repo are fail to run on the GoDoc website because
// of the missing variables. In order to avoid confusing users, these tests
// should be unplayable on the GoDoc website, and this can be achieved by
// leveraging a dot import in the test files.
// Reference: https://pkg.go.dev/go/doc#Example
package doc
// ExampleUnplayable is used in examples test files in order to make example
// tests unplayable.
const ExampleUnplayable = true
oras-go-2.5.0/registry/reference.go 0000664 0000000 0000000 00000021741 14576745303 0017245 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 registry
import (
"fmt"
"net/url"
"regexp"
"strings"
"github.com/opencontainers/go-digest"
"oras.land/oras-go/v2/errdef"
)
// regular expressions for components.
var (
// repositoryRegexp is adapted from the distribution implementation. The
// repository name set under OCI distribution spec is a subset of the docker
// spec. For maximum compatability, the docker spec is verified client-side.
// Further checks are left to the server-side.
//
// References:
// - https://github.com/distribution/distribution/blob/v2.7.1/reference/regexp.go#L53
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#pulling-manifests
repositoryRegexp = regexp.MustCompile(`^[a-z0-9]+(?:(?:[._]|__|[-]*)[a-z0-9]+)*(?:/[a-z0-9]+(?:(?:[._]|__|[-]*)[a-z0-9]+)*)*$`)
// tagRegexp checks the tag name.
// The docker and OCI spec have the same regular expression.
//
// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#pulling-manifests
tagRegexp = regexp.MustCompile(`^[\w][\w.-]{0,127}$`)
)
// Reference references either a resource descriptor (where Reference.Reference
// is a tag or a digest), or a resource repository (where Reference.Reference
// is the empty string).
type Reference struct {
// Registry is the name of the registry. It is usually the domain name of
// the registry optionally with a port.
Registry string
// Repository is the name of the repository.
Repository string
// Reference is the reference of the object in the repository. This field
// can take any one of the four valid forms (see ParseReference). In the
// case where it's the empty string, it necessarily implies valid form D,
// and where it is non-empty, then it is either a tag, or a digest
// (implying one of valid forms A, B, or C).
Reference string
}
// ParseReference parses a string (artifact) into an `artifact reference`.
// Corresponding cryptographic hash implementations are required to be imported
// as specified by https://pkg.go.dev/github.com/opencontainers/go-digest#readme-usage
// if the string contains a digest.
//
// Note: An "image" is an "artifact", however, an "artifact" is not necessarily
// an "image".
//
// The token `artifact` is composed of other tokens, and those in turn are
// composed of others. This definition recursivity requires a notation capable
// of recursion, thus the following two forms have been adopted:
//
// 1. Backus–Naur Form (BNF) has been adopted to address the recursive nature
// of the definition.
// 2. Token opacity is revealed via its label letter-casing. That is, "opaque"
// tokens (i.e., tokens that are not final, and must therefore be further
// broken down into their constituents) are denoted in *lowercase*, while
// final tokens (i.e., leaf-node tokens that are final) are denoted in
// *uppercase*.
//
// Finally, note that a number of the opaque tokens are polymorphic in nature;
// that is, they can take on one of numerous forms, not restricted to a single
// defining form.
//
// The top-level token, `artifact`, is composed of two (opaque) tokens; namely
// `socketaddr` and `path`:
//
// ::= "/"
//
// The former is described as follows:
//
// ::= | ":"
// ::= |
// ::= |
//
// The latter, which is of greater interest here, is described as follows:
//
// ::= |
// ::= "@" | ":" "@" | ":"
// ::= ":"
//
// This second token--`path`--can take on exactly four forms, each of which will
// now be illustrated:
//
// <--- path --------------------------------------------> | - Decode `path`
// <=== REPOSITORY ===> <--- reference ------------------> | - Decode `reference`
// <=== REPOSITORY ===> @ <=================== digest ===> | - Valid Form A
// <=== REPOSITORY ===> : @ <=== digest ===> | - Valid Form B (tag is dropped)
// <=== REPOSITORY ===> : <=== TAG ======================> | - Valid Form C
// <=== REPOSITORY ======================================> | - Valid Form D
//
// Note: In the case of Valid Form B, TAG is dropped without any validation or
// further consideration.
func ParseReference(artifact string) (Reference, error) {
parts := strings.SplitN(artifact, "/", 2)
if len(parts) == 1 {
// Invalid Form
return Reference{}, fmt.Errorf("%w: missing registry or repository", errdef.ErrInvalidReference)
}
registry, path := parts[0], parts[1]
var isTag bool
var repository string
var reference string
if index := strings.Index(path, "@"); index != -1 {
// `digest` found; Valid Form A (if not B)
isTag = false
repository = path[:index]
reference = path[index+1:]
if index = strings.Index(repository, ":"); index != -1 {
// `tag` found (and now dropped without validation) since `the
// `digest` already present; Valid Form B
repository = repository[:index]
}
} else if index = strings.Index(path, ":"); index != -1 {
// `tag` found; Valid Form C
isTag = true
repository = path[:index]
reference = path[index+1:]
} else {
// empty `reference`; Valid Form D
repository = path
}
ref := Reference{
Registry: registry,
Repository: repository,
Reference: reference,
}
if err := ref.ValidateRegistry(); err != nil {
return Reference{}, err
}
if err := ref.ValidateRepository(); err != nil {
return Reference{}, err
}
if len(ref.Reference) == 0 {
return ref, nil
}
validator := ref.ValidateReferenceAsDigest
if isTag {
validator = ref.ValidateReferenceAsTag
}
if err := validator(); err != nil {
return Reference{}, err
}
return ref, nil
}
// Validate the entire reference object; the registry, the repository, and the
// reference.
func (r Reference) Validate() error {
if err := r.ValidateRegistry(); err != nil {
return err
}
if err := r.ValidateRepository(); err != nil {
return err
}
return r.ValidateReference()
}
// ValidateRegistry validates the registry.
func (r Reference) ValidateRegistry() error {
if uri, err := url.ParseRequestURI("dummy://" + r.Registry); err != nil || uri.Host == "" || uri.Host != r.Registry {
return fmt.Errorf("%w: invalid registry %q", errdef.ErrInvalidReference, r.Registry)
}
return nil
}
// ValidateRepository validates the repository.
func (r Reference) ValidateRepository() error {
if !repositoryRegexp.MatchString(r.Repository) {
return fmt.Errorf("%w: invalid repository %q", errdef.ErrInvalidReference, r.Repository)
}
return nil
}
// ValidateReferenceAsTag validates the reference as a tag.
func (r Reference) ValidateReferenceAsTag() error {
if !tagRegexp.MatchString(r.Reference) {
return fmt.Errorf("%w: invalid tag %q", errdef.ErrInvalidReference, r.Reference)
}
return nil
}
// ValidateReferenceAsDigest validates the reference as a digest.
func (r Reference) ValidateReferenceAsDigest() error {
if _, err := r.Digest(); err != nil {
return fmt.Errorf("%w: invalid digest %q: %v", errdef.ErrInvalidReference, r.Reference, err)
}
return nil
}
// ValidateReference where the reference is first tried as an ampty string, then
// as a digest, and if that fails, as a tag.
func (r Reference) ValidateReference() error {
if len(r.Reference) == 0 {
return nil
}
if index := strings.IndexByte(r.Reference, ':'); index != -1 {
return r.ValidateReferenceAsDigest()
}
return r.ValidateReferenceAsTag()
}
// Host returns the host name of the registry.
func (r Reference) Host() string {
if r.Registry == "docker.io" {
return "registry-1.docker.io"
}
return r.Registry
}
// ReferenceOrDefault returns the reference or the default reference if empty.
func (r Reference) ReferenceOrDefault() string {
if r.Reference == "" {
return "latest"
}
return r.Reference
}
// Digest returns the reference as a digest.
// Corresponding cryptographic hash implementations are required to be imported
// as specified by https://pkg.go.dev/github.com/opencontainers/go-digest#readme-usage
func (r Reference) Digest() (digest.Digest, error) {
return digest.Parse(r.Reference)
}
// String implements `fmt.Stringer` and returns the reference string.
// The resulted string is meaningful only if the reference is valid.
func (r Reference) String() string {
if r.Repository == "" {
return r.Registry
}
ref := r.Registry + "/" + r.Repository
if r.Reference == "" {
return ref
}
if d, err := r.Digest(); err == nil {
return ref + "@" + d.String()
}
return ref + ":" + r.Reference
}
oras-go-2.5.0/registry/reference_test.go 0000664 0000000 0000000 00000007272 14576745303 0020307 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 registry
import (
_ "crypto/sha256"
"fmt"
"reflect"
"testing"
)
const ValidDigest = "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
const InvalidDigest = "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde"
// For a definition of what a "valid form [ABCD]" means, see reference.go.
func TestParseReferenceGoodies(t *testing.T) {
tests := []struct {
name string
image string
wantTemplate Reference
}{
{
name: "digest reference (valid form A)",
image: fmt.Sprintf("hello-world@%s", ValidDigest),
wantTemplate: Reference{
Repository: "hello-world",
Reference: ValidDigest,
},
},
{
name: "tag with digest (valid form B)",
image: fmt.Sprintf("hello-world:v2@%s", ValidDigest),
wantTemplate: Reference{
Repository: "hello-world",
Reference: ValidDigest,
},
},
{
name: "empty tag with digest (valid form B)",
image: fmt.Sprintf("hello-world:@%s", ValidDigest),
wantTemplate: Reference{
Repository: "hello-world",
Reference: ValidDigest,
},
},
{
name: "tag reference (valid form C)",
image: "hello-world:v1",
wantTemplate: Reference{
Repository: "hello-world",
Reference: "v1",
},
},
{
name: "basic reference (valid form D)",
image: "hello-world",
wantTemplate: Reference{
Repository: "hello-world",
},
},
}
registries := []string{
"localhost",
"registry.example.com",
"localhost:5000",
"127.0.0.1:5000",
"[::1]:5000",
}
for _, tt := range tests {
want := tt.wantTemplate
for _, registry := range registries {
want.Registry = registry
t.Run(tt.name, func(t *testing.T) {
got, err := ParseReference(fmt.Sprintf("%s/%s", registry, tt.image))
if err != nil {
t.Errorf("ParseReference() encountered unexpected error: %v", err)
return
}
if !reflect.DeepEqual(got, want) {
t.Errorf("ParseReference() = %v, want %v", got, tt.wantTemplate)
}
})
}
}
}
func TestParseReferenceUglies(t *testing.T) {
tests := []struct {
name string
raw string
want Reference
}{
{
name: "no repo name",
raw: "localhost",
},
{
name: "missing registry",
raw: "hello-world:linux",
},
{
name: "missing registry (issue #698)",
raw: "/hello-world:linux",
},
{
name: "invalid repo name",
raw: "localhost/UPPERCASE/test",
},
{
name: "invalid port",
raw: "localhost:v1/hello-world",
},
{
name: "invalid digest",
raw: fmt.Sprintf("registry.example.com/foobar@%s", InvalidDigest),
},
{
name: "invalid digest prefix: colon instead of the at sign",
raw: fmt.Sprintf("registry.example.com/hello-world:foobar:%s", ValidDigest),
},
{
name: "invalid digest prefix: double at sign",
raw: fmt.Sprintf("registry.example.com/hello-world@@%s", ValidDigest),
},
{
name: "invalid digest prefix: space",
raw: fmt.Sprintf("registry.example.com/hello-world @%s", ValidDigest),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if _, err := ParseReference(tt.raw); err == nil {
t.Errorf("ParseReference() expected an error, but got none")
return
}
})
}
}
oras-go-2.5.0/registry/registry.go 0000664 0000000 0000000 00000004021 14576745303 0017147 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 registry provides high-level operations to manage registries.
package registry
import "context"
// Registry represents a collection of repositories.
type Registry interface {
// Repositories lists the name of repositories available in the registry.
// Since the returned repositories may be paginated by the underlying
// implementation, a function should be passed in to process the paginated
// repository list.
// `last` argument is the `last` parameter when invoking the catalog API.
// If `last` is NOT empty, the entries in the response start after the
// repo specified by `last`. Otherwise, the response starts from the top
// of the Repositories list.
// Note: When implemented by a remote registry, the catalog API is called.
// However, not all registries supports pagination or conforms the
// specification.
// Reference: https://docs.docker.com/registry/spec/api/#catalog
// See also `Repositories()` in this package.
Repositories(ctx context.Context, last string, fn func(repos []string) error) error
// Repository returns a repository reference by the given name.
Repository(ctx context.Context, name string) (Repository, error)
}
// Repositories lists the name of repositories available in the registry.
func Repositories(ctx context.Context, reg Registry) ([]string, error) {
var res []string
if err := reg.Repositories(ctx, "", func(repos []string) error {
res = append(res, repos...)
return nil
}); err != nil {
return nil, err
}
return res, nil
}
oras-go-2.5.0/registry/remote/ 0000775 0000000 0000000 00000000000 14576745303 0016246 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/registry/remote/auth/ 0000775 0000000 0000000 00000000000 14576745303 0017207 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/registry/remote/auth/cache.go 0000664 0000000 0000000 00000017451 14576745303 0020611 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 auth
import (
"context"
"strings"
"sync"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/internal/syncutil"
)
// DefaultCache is the sharable cache used by DefaultClient.
var DefaultCache Cache = NewCache()
// Cache caches the auth-scheme and auth-token for the "Authorization" header in
// accessing the remote registry.
// Precisely, the header is `Authorization: auth-scheme auth-token`.
// The `auth-token` is a generic term as `token68` in RFC 7235 section 2.1.
type Cache interface {
// GetScheme returns the auth-scheme part cached for the given registry.
// A single registry is assumed to have a consistent scheme.
// If a registry has different schemes per path, the auth client is still
// workable. However, the cache may not be effective as the cache cannot
// correctly guess the scheme.
GetScheme(ctx context.Context, registry string) (Scheme, error)
// GetToken returns the auth-token part cached for the given registry of a
// given scheme.
// The underlying implementation MAY cache the token for all schemes for the
// given registry.
GetToken(ctx context.Context, registry string, scheme Scheme, key string) (string, error)
// Set fetches the token using the given fetch function and caches the token
// for the given scheme with the given key for the given registry.
// The return values of the fetch function is returned by this function.
// The underlying implementation MAY combine the fetch operation if the Set
// function is invoked multiple times at the same time.
Set(ctx context.Context, registry string, scheme Scheme, key string, fetch func(context.Context) (string, error)) (string, error)
}
// cacheEntry is a cache entry for a single registry.
type cacheEntry struct {
scheme Scheme
tokens sync.Map // map[string]string
}
// concurrentCache is a cache suitable for concurrent invocation.
type concurrentCache struct {
status sync.Map // map[string]*syncutil.Once
cache sync.Map // map[string]*cacheEntry
}
// NewCache creates a new go-routine safe cache instance.
func NewCache() Cache {
return &concurrentCache{}
}
// GetScheme returns the auth-scheme part cached for the given registry.
func (cc *concurrentCache) GetScheme(ctx context.Context, registry string) (Scheme, error) {
entry, ok := cc.cache.Load(registry)
if !ok {
return SchemeUnknown, errdef.ErrNotFound
}
return entry.(*cacheEntry).scheme, nil
}
// GetToken returns the auth-token part cached for the given registry of a given
// scheme.
func (cc *concurrentCache) GetToken(ctx context.Context, registry string, scheme Scheme, key string) (string, error) {
entryValue, ok := cc.cache.Load(registry)
if !ok {
return "", errdef.ErrNotFound
}
entry := entryValue.(*cacheEntry)
if entry.scheme != scheme {
return "", errdef.ErrNotFound
}
if token, ok := entry.tokens.Load(key); ok {
return token.(string), nil
}
return "", errdef.ErrNotFound
}
// Set fetches the token using the given fetch function and caches the token
// for the given scheme with the given key for the given registry.
// Set combines the fetch operation if the Set is invoked multiple times at the
// same time.
func (cc *concurrentCache) Set(ctx context.Context, registry string, scheme Scheme, key string, fetch func(context.Context) (string, error)) (string, error) {
// fetch token
statusKey := strings.Join([]string{
registry,
scheme.String(),
key,
}, " ")
statusValue, _ := cc.status.LoadOrStore(statusKey, syncutil.NewOnce())
fetchOnce := statusValue.(*syncutil.Once)
fetchedFirst, result, err := fetchOnce.Do(ctx, func() (interface{}, error) {
return fetch(ctx)
})
if fetchedFirst {
cc.status.Delete(statusKey)
}
if err != nil {
return "", err
}
token := result.(string)
if !fetchedFirst {
return token, nil
}
// cache token
newEntry := &cacheEntry{
scheme: scheme,
}
entryValue, exists := cc.cache.LoadOrStore(registry, newEntry)
entry := entryValue.(*cacheEntry)
if exists && entry.scheme != scheme {
// there is a scheme change, which is not expected in most scenarios.
// force invalidating all previous cache.
entry = newEntry
cc.cache.Store(registry, entry)
}
entry.tokens.Store(key, token)
return token, nil
}
// noCache is a cache implementation that does not do cache at all.
type noCache struct{}
// GetScheme always returns not found error as it has no cache.
func (noCache) GetScheme(ctx context.Context, registry string) (Scheme, error) {
return SchemeUnknown, errdef.ErrNotFound
}
// GetToken always returns not found error as it has no cache.
func (noCache) GetToken(ctx context.Context, registry string, scheme Scheme, key string) (string, error) {
return "", errdef.ErrNotFound
}
// Set calls fetch directly without caching.
func (noCache) Set(ctx context.Context, registry string, scheme Scheme, key string, fetch func(context.Context) (string, error)) (string, error) {
return fetch(ctx)
}
// hostCache is an auth cache that ignores scopes. Uses only the registry's hostname to find a token.
type hostCache struct {
Cache
}
// GetToken implements Cache.
func (c *hostCache) GetToken(ctx context.Context, registry string, scheme Scheme, key string) (string, error) {
return c.Cache.GetToken(ctx, registry, scheme, "")
}
// Set implements Cache.
func (c *hostCache) Set(ctx context.Context, registry string, scheme Scheme, key string, fetch func(context.Context) (string, error)) (string, error) {
return c.Cache.Set(ctx, registry, scheme, "", fetch)
}
// fallbackCache tries the primary cache then falls back to the secondary cache.
type fallbackCache struct {
primary Cache
secondary Cache
}
// GetScheme implements Cache.
func (fc *fallbackCache) GetScheme(ctx context.Context, registry string) (Scheme, error) {
scheme, err := fc.primary.GetScheme(ctx, registry)
if err == nil {
return scheme, nil
}
// fallback
return fc.secondary.GetScheme(ctx, registry)
}
// GetToken implements Cache.
func (fc *fallbackCache) GetToken(ctx context.Context, registry string, scheme Scheme, key string) (string, error) {
token, err := fc.primary.GetToken(ctx, registry, scheme, key)
if err == nil {
return token, nil
}
// fallback
return fc.secondary.GetToken(ctx, registry, scheme, key)
}
// Set implements Cache.
func (fc *fallbackCache) Set(ctx context.Context, registry string, scheme Scheme, key string, fetch func(context.Context) (string, error)) (string, error) {
token, err := fc.primary.Set(ctx, registry, scheme, key, fetch)
if err != nil {
return "", err
}
return fc.secondary.Set(ctx, registry, scheme, key, func(ctx context.Context) (string, error) {
return token, nil
})
}
// NewSingleContextCache creates a host-based cache for optimizing the auth flow for non-compliant registries.
// It is intended to be used in a single context, such as pulling from a single repository.
// This cache should not be shared.
//
// Note: [NewCache] should be used for compliant registries as it can be shared
// across context and will generally make less re-authentication requests.
func NewSingleContextCache() Cache {
cache := NewCache()
return &fallbackCache{
primary: cache,
// We can re-use the came concurrentCache here because the key space is different
// (keys are always empty for the hostCache) so there is no collision.
// Even if there is a collision it is not an issue.
// Re-using saves a little memory.
secondary: &hostCache{cache},
}
}
oras-go-2.5.0/registry/remote/auth/cache_test.go 0000664 0000000 0000000 00000043306 14576745303 0021646 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 auth
import (
"context"
"errors"
"strconv"
"sync"
"sync/atomic"
"testing"
"time"
"oras.land/oras-go/v2/errdef"
)
func Test_concurrentCache_GetScheme(t *testing.T) {
cache := NewCache()
// no entry in the cache
ctx := context.Background()
registry := "localhost:5000"
got, err := cache.GetScheme(ctx, registry)
if want := errdef.ErrNotFound; err != want {
t.Fatalf("concurrentCache.GetScheme() error = %v, wantErr %v", err, want)
}
if got != SchemeUnknown {
t.Errorf("concurrentCache.GetScheme() = %v, want %v", got, SchemeUnknown)
}
// set an cache entry
scheme := SchemeBasic
_, err = cache.Set(ctx, registry, scheme, "", func(c context.Context) (string, error) {
return "foo", nil
})
if err != nil {
t.Fatalf("failed to set cache: %v", err)
}
// verify cache
got, err = cache.GetScheme(ctx, registry)
if err != nil {
t.Fatalf("concurrentCache.GetScheme() error = %v", err)
}
if got != scheme {
t.Errorf("concurrentCache.GetScheme() = %v, want %v", got, scheme)
}
// set cache entry again
scheme = SchemeBearer
_, err = cache.Set(ctx, registry, scheme, "", func(c context.Context) (string, error) {
return "bar", nil
})
if err != nil {
t.Fatalf("failed to set cache: %v", err)
}
// verify cache
got, err = cache.GetScheme(ctx, registry)
if err != nil {
t.Fatalf("concurrentCache.GetScheme() error = %v", err)
}
if got != scheme {
t.Errorf("concurrentCache.GetScheme() = %v, want %v", got, scheme)
}
// test other registry
registry = "localhost:5001"
got, err = cache.GetScheme(ctx, registry)
if want := errdef.ErrNotFound; err != want {
t.Fatalf("concurrentCache.GetScheme() error = %v, wantErr %v", err, want)
}
if got != SchemeUnknown {
t.Errorf("concurrentCache.GetScheme() = %v, want %v", got, SchemeUnknown)
}
}
func Test_concurrentCache_GetToken(t *testing.T) {
cache := NewCache()
// no entry in the cache
ctx := context.Background()
registry := "localhost:5000"
scheme := SchemeBearer
key := "1st key"
got, err := cache.GetToken(ctx, registry, scheme, key)
if want := errdef.ErrNotFound; err != want {
t.Fatalf("concurrentCache.GetToken() error = %v, wantErr %v", err, want)
}
if got != "" {
t.Errorf("concurrentCache.GetToken() = %v, want %v", got, "")
}
// set an cache entry
_, err = cache.Set(ctx, registry, scheme, key, func(c context.Context) (string, error) {
return "foo", nil
})
if err != nil {
t.Fatalf("failed to set cache: %v", err)
}
// verify cache
got, err = cache.GetToken(ctx, registry, scheme, key)
if err != nil {
t.Fatalf("concurrentCache.GetToken() error = %v", err)
}
if want := "foo"; got != want {
t.Errorf("concurrentCache.GetToken() = %v, want %v", got, want)
}
// set cache entry again
_, err = cache.Set(ctx, registry, scheme, key, func(c context.Context) (string, error) {
return "bar", nil
})
if err != nil {
t.Fatalf("failed to set cache: %v", err)
}
// verify cache
got, err = cache.GetToken(ctx, registry, scheme, key)
if err != nil {
t.Fatalf("concurrentCache.GetToken() error = %v", err)
}
if want := "bar"; got != want {
t.Errorf("concurrentCache.GetToken() = %v, want %v", got, want)
}
// test other key
key = "2nd key"
got, err = cache.GetToken(ctx, registry, scheme, key)
if want := errdef.ErrNotFound; err != want {
t.Fatalf("concurrentCache.GetToken() error = %v, wantErr %v", err, want)
}
if got != "" {
t.Errorf("concurrentCache.GetToken() = %v, want %v", got, "")
}
// set an cache entry
_, err = cache.Set(ctx, registry, scheme, key, func(c context.Context) (string, error) {
return "hello world", nil
})
if err != nil {
t.Fatalf("failed to set cache: %v", err)
}
// verify cache
got, err = cache.GetToken(ctx, registry, scheme, key)
if err != nil {
t.Fatalf("concurrentCache.GetToken() error = %v", err)
}
if want := "hello world"; got != want {
t.Errorf("concurrentCache.GetToken() = %v, want %v", got, want)
}
// verify cache of the previous key as keys should not interference each
// other
key = "1st key"
got, err = cache.GetToken(ctx, registry, scheme, key)
if err != nil {
t.Fatalf("concurrentCache.GetToken() error = %v", err)
}
if want := "bar"; got != want {
t.Errorf("concurrentCache.GetToken() = %v, want %v", got, want)
}
// test other registry
registry = "localhost:5001"
got, err = cache.GetToken(ctx, registry, scheme, key)
if want := errdef.ErrNotFound; err != want {
t.Fatalf("concurrentCache.GetToken() error = %v, wantErr %v", err, want)
}
if got != "" {
t.Errorf("concurrentCache.GetToken() = %v, want %v", got, "")
}
// set an cache entry
_, err = cache.Set(ctx, registry, scheme, key, func(c context.Context) (string, error) {
return "foobar", nil
})
if err != nil {
t.Fatalf("failed to set cache: %v", err)
}
// verify cache
got, err = cache.GetToken(ctx, registry, scheme, key)
if err != nil {
t.Fatalf("concurrentCache.GetToken() error = %v", err)
}
if want := "foobar"; got != want {
t.Errorf("concurrentCache.GetToken() = %v, want %v", got, want)
}
// verify cache of the previous registry as registries should not
// interference each other
registry = "localhost:5000"
got, err = cache.GetToken(ctx, registry, scheme, key)
if err != nil {
t.Fatalf("concurrentCache.GetToken() error = %v", err)
}
if want := "bar"; got != want {
t.Errorf("concurrentCache.GetToken() = %v, want %v", got, want)
}
// test other scheme
scheme = SchemeBasic
got, err = cache.GetToken(ctx, registry, scheme, key)
if want := errdef.ErrNotFound; err != want {
t.Fatalf("concurrentCache.GetToken() error = %v, wantErr %v", err, want)
}
if got != "" {
t.Errorf("concurrentCache.GetToken() = %v, want %v", got, "")
}
// set an cache entry
_, err = cache.Set(ctx, registry, scheme, key, func(c context.Context) (string, error) {
return "new scheme", nil
})
if err != nil {
t.Fatalf("failed to set cache: %v", err)
}
// verify cache
got, err = cache.GetToken(ctx, registry, scheme, key)
if err != nil {
t.Fatalf("concurrentCache.GetToken() error = %v", err)
}
if want := "new scheme"; got != want {
t.Errorf("concurrentCache.GetToken() = %v, want %v", got, want)
}
// cache of the previous scheme should be invalidated due to scheme change.
got, err = cache.GetToken(ctx, registry, SchemeBearer, key)
if want := errdef.ErrNotFound; err != want {
t.Fatalf("concurrentCache.GetToken() error = %v, wantErr %v", err, want)
}
if got != "" {
t.Errorf("concurrentCache.GetToken() = %v, want %v", got, "")
}
}
func Test_concurrentCache_Set(t *testing.T) {
registries := []string{
"localhost:5000",
"localhost:5001",
}
scheme := SchemeBearer
keys := []string{
"foo",
"bar",
}
count := len(registries) * len(keys)
ctx := context.Background()
cache := NewCache()
// first round of fetch
fetch := func(i int) func(context.Context) (string, error) {
return func(context.Context) (string, error) {
return strconv.Itoa(i), nil
}
}
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
for j := 0; j < count; j++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
registry := registries[i&1]
key := keys[(i>>1)&1]
got, err := cache.Set(ctx, registry, scheme, key, fetch(i))
if err != nil {
t.Errorf("concurrentCache.Set() error = %v", err)
}
if want := strconv.Itoa(i); got != want {
t.Errorf("concurrentCache.Set() = %v, want %v", got, want)
}
}(j)
}
}
wg.Wait()
for i := 0; i < count; i++ {
registry := registries[i&1]
key := keys[(i>>1)&1]
gotScheme, err := cache.GetScheme(ctx, registry)
if err != nil {
t.Fatalf("concurrentCache.GetScheme() error = %v", err)
}
if want := scheme; gotScheme != want {
t.Errorf("concurrentCache.GetScheme() = %v, want %v", gotScheme, want)
}
gotToken, err := cache.GetToken(ctx, registry, scheme, key)
if err != nil {
t.Fatalf("concurrentCache.GetToken() error = %v", err)
}
if want := strconv.Itoa(i); gotToken != want {
t.Errorf("concurrentCache.GetToken() = %v, want %v", gotToken, want)
}
}
// repeated fetch
fetch = func(i int) func(context.Context) (string, error) {
return func(context.Context) (string, error) {
return strconv.Itoa(i) + " repeated", nil
}
}
for i := 0; i < 10; i++ {
for j := 0; j < count; j++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
registry := registries[i&1]
key := keys[(i>>1)&1]
got, err := cache.Set(ctx, registry, scheme, key, fetch(i))
if err != nil {
t.Errorf("concurrentCache.Set() error = %v", err)
}
if want := strconv.Itoa(i) + " repeated"; got != want {
t.Errorf("concurrentCache.Set() = %v, want %v", got, want)
}
}(j)
}
}
wg.Wait()
for i := 0; i < count; i++ {
registry := registries[i&1]
key := keys[(i>>1)&1]
gotScheme, err := cache.GetScheme(ctx, registry)
if err != nil {
t.Fatalf("concurrentCache.GetScheme() error = %v", err)
}
if want := scheme; gotScheme != want {
t.Errorf("concurrentCache.GetScheme() = %v, want %v", gotScheme, want)
}
gotToken, err := cache.GetToken(ctx, registry, scheme, key)
if err != nil {
t.Fatalf("concurrentCache.GetToken() error = %v", err)
}
if want := strconv.Itoa(i) + " repeated"; gotToken != want {
t.Errorf("concurrentCache.GetToken() = %v, want %v", gotToken, want)
}
}
}
func Test_concurrentCache_Set_Fetch_Once(t *testing.T) {
registries := []string{
"localhost:5000",
"localhost:5001",
}
schemes := []Scheme{
SchemeBasic,
SchemeBearer,
}
keys := []string{
"foo",
"bar",
}
count := make([]int64, len(registries)*len(schemes)*len(keys))
fetch := func(i int) func(context.Context) (string, error) {
return func(context.Context) (string, error) {
time.Sleep(500 * time.Millisecond)
atomic.AddInt64(&count[i], 1)
return strconv.Itoa(i), nil
}
}
ctx := context.Background()
cache := NewCache()
// first round of fetch
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
for j := 0; j < len(count); j++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
registry := registries[i&1]
scheme := schemes[(i>>1)&1]
key := keys[(i>>2)&1]
got, err := cache.Set(ctx, registry, scheme, key, fetch(i))
if err != nil {
t.Errorf("concurrentCache.Set() error = %v", err)
}
if want := strconv.Itoa(i); got != want {
t.Errorf("concurrentCache.Set() = %v, want %v", got, want)
}
}(j)
}
}
wg.Wait()
for i := 0; i < len(count); i++ {
if got := count[i]; got != 1 {
t.Errorf("fetch is called more than once: %d", got)
}
}
// repeated fetch
for i := 0; i < 10; i++ {
for j := 0; j < len(count); j++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
registry := registries[i&1]
scheme := schemes[(i>>1)&1]
key := keys[(i>>2)&1]
got, err := cache.Set(ctx, registry, scheme, key, fetch(i))
if err != nil {
t.Errorf("concurrentCache.Set() error = %v", err)
}
if want := strconv.Itoa(i); got != want {
t.Errorf("concurrentCache.Set() = %v, want %v", got, want)
}
}(j)
}
}
wg.Wait()
for i := 0; i < len(count); i++ {
if got := count[i]; got != 2 {
t.Errorf("fetch is called more than once: %d", got)
}
}
}
func Test_concurrentCache_Set_Fetch_Failure(t *testing.T) {
registries := []string{
"localhost:5000",
"localhost:5001",
}
scheme := SchemeBearer
keys := []string{
"foo",
"bar",
}
count := len(registries) * len(keys)
ctx := context.Background()
cache := NewCache()
// first round of fetch
fetch := func(i int) func(context.Context) (string, error) {
return func(context.Context) (string, error) {
return "", errors.New(strconv.Itoa(i))
}
}
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
for j := 0; j < count; j++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
registry := registries[i&1]
key := keys[(i>>1)&1]
_, err := cache.Set(ctx, registry, scheme, key, fetch(i))
if want := strconv.Itoa(i); err == nil || err.Error() != want {
t.Errorf("concurrentCache.Set() error = %v, wantErr %v", err, want)
}
}(j)
}
}
wg.Wait()
for i := 0; i < count; i++ {
registry := registries[i&1]
key := keys[(i>>1)&1]
_, err := cache.GetScheme(ctx, registry)
if want := errdef.ErrNotFound; err != want {
t.Fatalf("concurrentCache.GetScheme() error = %v, wantErr %v", err, want)
}
_, err = cache.GetToken(ctx, registry, scheme, key)
if want := errdef.ErrNotFound; err != want {
t.Errorf("concurrentCache.GetToken() error = %v, wantErr %v", err, want)
}
}
// repeated fetch
fetch = func(i int) func(context.Context) (string, error) {
return func(context.Context) (string, error) {
return strconv.Itoa(i), nil
}
}
for i := 0; i < 10; i++ {
for j := 0; j < count; j++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
registry := registries[i&1]
key := keys[(i>>1)&1]
got, err := cache.Set(ctx, registry, scheme, key, fetch(i))
if err != nil {
t.Errorf("concurrentCache.Set() error = %v", err)
}
if want := strconv.Itoa(i); got != want {
t.Errorf("concurrentCache.Set() = %v, want %v", got, want)
}
}(j)
}
}
wg.Wait()
for i := 0; i < count; i++ {
registry := registries[i&1]
key := keys[(i>>1)&1]
gotScheme, err := cache.GetScheme(ctx, registry)
if err != nil {
t.Fatalf("concurrentCache.GetScheme() error = %v", err)
}
if want := scheme; gotScheme != want {
t.Errorf("concurrentCache.GetScheme() = %v, want %v", gotScheme, want)
}
gotToken, err := cache.GetToken(ctx, registry, scheme, key)
if err != nil {
t.Fatalf("concurrentCache.GetToken() error = %v", err)
}
if want := strconv.Itoa(i); gotToken != want {
t.Errorf("concurrentCache.GetToken() = %v, want %v", gotToken, want)
}
}
}
func Test_hostCache(t *testing.T) {
base := NewCache()
// no entry in the cache
ctx := context.Background()
hc := hostCache{base}
fetch := func(i int) func(context.Context) (string, error) {
return func(context.Context) (string, error) {
return strconv.Itoa(i), nil
}
}
// The key is ignored in the hostCache implementation.
{ // Set the token to 100
gotToken, err := hc.Set(ctx, "reg.example.com", SchemeBearer, "key1", fetch(100))
if err != nil {
t.Fatalf("hostCache.Set() error = %v", err)
}
if want := strconv.Itoa(100); gotToken != want {
t.Errorf("hostCache.Set() = %v, want %v", gotToken, want)
}
}
{ // Overwrite the token entry to 101
gotToken, err := hc.Set(ctx, "reg.example.com", SchemeBearer, "key2", fetch(101))
if err != nil {
t.Fatalf("hostCache.Set() error = %v", err)
}
if want := strconv.Itoa(101); gotToken != want {
t.Errorf("hostCache.Set() = %v, want %v", gotToken, want)
}
}
{ // Add entry for another host
gotToken, err := hc.Set(ctx, "reg2.example.com", SchemeBearer, "key3", fetch(102))
if err != nil {
t.Fatalf("hostCache.Set() error = %v", err)
}
if want := strconv.Itoa(102); gotToken != want {
t.Errorf("hostCache.Set() = %v, want %v", gotToken, want)
}
}
{ // Ensure the token for key1 is 101 now
gotToken, err := hc.GetToken(ctx, "reg.example.com", SchemeBearer, "key1")
if err != nil {
t.Fatalf("hostCache.GetToken() error = %v", err)
}
if want := strconv.Itoa(101); gotToken != want {
t.Errorf("hostCache.GetToken() = %v, want %v", gotToken, want)
}
}
{ // Make sure GetScheme still works
gotScheme, err := hc.GetScheme(ctx, "reg.example.com")
if err != nil {
t.Fatalf("hostCache.GetScheme() error = %v", err)
}
if want := SchemeBearer; gotScheme != want {
t.Errorf("hostCache.GetScheme() = %v, want %v", gotScheme, want)
}
}
}
func Test_fallbackCache(t *testing.T) {
// no entry in the cache
ctx := context.Background()
scc := NewSingleContextCache()
fetch := func(i int) func(context.Context) (string, error) {
return func(context.Context) (string, error) {
return strconv.Itoa(i), nil
}
}
// Test that fallback works
{ // Set the token to 100
gotToken, err := scc.Set(ctx, "reg.example.com", SchemeBearer, "key1", fetch(100))
if err != nil {
t.Fatalf("hostCache.Set() error = %v", err)
}
if want := strconv.Itoa(100); gotToken != want {
t.Errorf("hostCache.Set() = %v, want %v", gotToken, want)
}
}
{ // Ensure the token for key2 falls back to 100
gotToken, err := scc.GetToken(ctx, "reg.example.com", SchemeBearer, "key2")
if err != nil {
t.Fatalf("hostCache.GetToken() error = %v", err)
}
if want := strconv.Itoa(100); gotToken != want {
t.Errorf("hostCache.GetToken() = %v, want %v", gotToken, want)
}
}
{ // Make sure GetScheme works as expected
gotScheme, err := scc.GetScheme(ctx, "reg.example.com")
if err != nil {
t.Fatalf("hostCache.GetScheme() error = %v", err)
}
if want := SchemeBearer; gotScheme != want {
t.Errorf("hostCache.GetScheme() = %v, want %v", gotScheme, want)
}
}
{ // Make sure GetScheme falls back
gotScheme, err := scc.GetScheme(ctx, "reg.example.com")
if err != nil {
t.Fatalf("hostCache.GetScheme() error = %v", err)
}
if want := SchemeBearer; gotScheme != want {
t.Errorf("hostCache.GetScheme() = %v, want %v", gotScheme, want)
}
}
{ // Check GetScheme fallback
// scc.(*fallbackCache).primary = NewCache()
gotScheme, err := scc.GetScheme(ctx, "reg2.example.com")
if !errors.Is(err, errdef.ErrNotFound) {
t.Fatalf("hostCache.GetScheme() error = %v", err)
}
if want := SchemeUnknown; gotScheme != want {
t.Errorf("hostCache.GetScheme() = %v, want %v", gotScheme, want)
}
}
}
oras-go-2.5.0/registry/remote/auth/challenge.go 0000664 0000000 0000000 00000010454 14576745303 0021464 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 auth
import (
"strconv"
"strings"
)
// Scheme define the authentication method.
type Scheme byte
const (
// SchemeUnknown represents unknown or unsupported schemes
SchemeUnknown Scheme = iota
// SchemeBasic represents the "Basic" HTTP authentication scheme.
// Reference: https://tools.ietf.org/html/rfc7617
SchemeBasic
// SchemeBearer represents the Bearer token in OAuth 2.0.
// Reference: https://tools.ietf.org/html/rfc6750
SchemeBearer
)
// parseScheme parse the authentication scheme from the given string
// case-insensitively.
func parseScheme(scheme string) Scheme {
switch {
case strings.EqualFold(scheme, "basic"):
return SchemeBasic
case strings.EqualFold(scheme, "bearer"):
return SchemeBearer
}
return SchemeUnknown
}
// String return the string for the scheme.
func (s Scheme) String() string {
switch s {
case SchemeBasic:
return "Basic"
case SchemeBearer:
return "Bearer"
}
return "Unknown"
}
// parseChallenge parses the "WWW-Authenticate" header returned by the remote
// registry, and extracts parameters if scheme is Bearer.
// References:
// - https://docs.docker.com/registry/spec/auth/token/#how-to-authenticate
// - https://tools.ietf.org/html/rfc7235#section-2.1
func parseChallenge(header string) (scheme Scheme, params map[string]string) {
// as defined in RFC 7235 section 2.1, we have
// challenge = auth-scheme [ 1*SP ( token68 / #auth-param ) ]
// auth-scheme = token
// auth-param = token BWS "=" BWS ( token / quoted-string )
//
// since we focus parameters only on Bearer, we have
// challenge = auth-scheme [ 1*SP #auth-param ]
schemeString, rest := parseToken(header)
scheme = parseScheme(schemeString)
// fast path for non bearer challenge
if scheme != SchemeBearer {
return
}
// parse params for bearer auth.
// combining RFC 7235 section 2.1 with RFC 7230 section 7, we have
// #auth-param => auth-param *( OWS "," OWS auth-param )
var key, value string
for {
key, rest = parseToken(skipSpace(rest))
if key == "" {
return
}
rest = skipSpace(rest)
if rest == "" || rest[0] != '=' {
return
}
rest = skipSpace(rest[1:])
if rest == "" {
return
}
if rest[0] == '"' {
prefix, err := strconv.QuotedPrefix(rest)
if err != nil {
return
}
value, err = strconv.Unquote(prefix)
if err != nil {
return
}
rest = rest[len(prefix):]
} else {
value, rest = parseToken(rest)
if value == "" {
return
}
}
if params == nil {
params = map[string]string{
key: value,
}
} else {
params[key] = value
}
rest = skipSpace(rest)
if rest == "" || rest[0] != ',' {
return
}
rest = rest[1:]
}
}
// isNotTokenChar reports whether rune is not a `tchar` defined in RFC 7230
// section 3.2.6.
func isNotTokenChar(r rune) bool {
// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
// / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
// / DIGIT / ALPHA
// ; any VCHAR, except delimiters
return (r < 'A' || r > 'Z') && (r < 'a' || r > 'z') &&
(r < '0' || r > '9') && !strings.ContainsRune("!#$%&'*+-.^_`|~", r)
}
// parseToken finds the next token from the given string. If no token found,
// an empty token is returned and the whole of the input is returned in rest.
// Note: Since token = 1*tchar, empty string is not a valid token.
func parseToken(s string) (token, rest string) {
if i := strings.IndexFunc(s, isNotTokenChar); i != -1 {
return s[:i], s[i:]
}
return s, ""
}
// skipSpace skips "bad" whitespace (BWS) defined in RFC 7230 section 3.2.3.
func skipSpace(s string) string {
// OWS = *( SP / HTAB )
// ; optional whitespace
// BWS = OWS
// ; "bad" whitespace
if i := strings.IndexFunc(s, func(r rune) bool {
return r != ' ' && r != '\t'
}); i != -1 {
return s[i:]
}
return s
}
oras-go-2.5.0/registry/remote/auth/challenge_test.go 0000664 0000000 0000000 00000011547 14576745303 0022527 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 auth
import (
"reflect"
"testing"
)
func Test_parseChallenge(t *testing.T) {
tests := []struct {
name string
header string
wantScheme Scheme
wantParams map[string]string
}{
{
name: "empty header",
},
{
name: "unknown scheme",
header: "foo bar",
wantScheme: SchemeUnknown,
},
{
name: "basic challenge",
header: `Basic realm="Test Registry"`,
wantScheme: SchemeBasic,
},
{
name: "basic challenge with no parameters",
header: "Basic",
wantScheme: SchemeBasic,
},
{
name: "basic challenge with no parameters but spaces",
header: "Basic ",
wantScheme: SchemeBasic,
},
{
name: "bearer challenge",
header: `Bearer realm="https://auth.example.io/token",service="registry.example.io",scope="repository:library/hello-world:pull,push"`,
wantScheme: SchemeBearer,
wantParams: map[string]string{
"realm": "https://auth.example.io/token",
"service": "registry.example.io",
"scope": "repository:library/hello-world:pull,push",
},
},
{
name: "bearer challenge with multiple scopes",
header: `Bearer realm="https://auth.example.io/token",service="registry.example.io",scope="repository:library/alpine:pull,push repository:ubuntu:pull"`,
wantScheme: SchemeBearer,
wantParams: map[string]string{
"realm": "https://auth.example.io/token",
"service": "registry.example.io",
"scope": "repository:library/alpine:pull,push repository:ubuntu:pull",
},
},
{
name: "bearer challenge with no parameters",
header: "Bearer",
wantScheme: SchemeBearer,
},
{
name: "bearer challenge with no parameters but spaces",
header: "Bearer ",
wantScheme: SchemeBearer,
},
{
name: "bearer challenge with white spaces",
header: `Bearer realm = "https://auth.example.io/token" ,service=registry.example.io, scope ="repository:library/hello-world:pull,push" `,
wantScheme: SchemeBearer,
wantParams: map[string]string{
"realm": "https://auth.example.io/token",
"service": "registry.example.io",
"scope": "repository:library/hello-world:pull,push",
},
},
{
name: "bad bearer challenge (incomplete parameter with spaces)",
header: `Bearer realm="https://auth.example.io/token",service`,
wantScheme: SchemeBearer,
wantParams: map[string]string{
"realm": "https://auth.example.io/token",
},
},
{
name: "bad bearer challenge (incomplete parameter with no value)",
header: `Bearer realm="https://auth.example.io/token",service=`,
wantScheme: SchemeBearer,
wantParams: map[string]string{
"realm": "https://auth.example.io/token",
},
},
{
name: "bad bearer challenge (incomplete parameter with spaces)",
header: `Bearer realm="https://auth.example.io/token",service= `,
wantScheme: SchemeBearer,
wantParams: map[string]string{
"realm": "https://auth.example.io/token",
},
},
{
name: "bad bearer challenge (incomplete quote)",
header: `Bearer realm="https://auth.example.io/token",service="registry`,
wantScheme: SchemeBearer,
wantParams: map[string]string{
"realm": "https://auth.example.io/token",
},
},
{
name: "bearer challenge with empty parameter value",
header: `Bearer realm="https://auth.example.io/token",empty="",service="registry.example.io",scope="repository:library/hello-world:pull,push"`,
wantScheme: SchemeBearer,
wantParams: map[string]string{
"realm": "https://auth.example.io/token",
"empty": "",
"service": "registry.example.io",
"scope": "repository:library/hello-world:pull,push",
},
},
{
name: "bearer challenge with escaping parameter value",
header: `Bearer foo="foo\"bar",hello="\"hello world\""`,
wantScheme: SchemeBearer,
wantParams: map[string]string{
"foo": `foo"bar`,
"hello": `"hello world"`,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotScheme, gotParams := parseChallenge(tt.header)
if gotScheme != tt.wantScheme {
t.Errorf("parseChallenge() gotScheme = %v, want %v", gotScheme, tt.wantScheme)
}
if !reflect.DeepEqual(gotParams, tt.wantParams) {
t.Errorf("parseChallenge() gotParams = %v, want %v", gotParams, tt.wantParams)
}
})
}
}
oras-go-2.5.0/registry/remote/auth/client.go 0000664 0000000 0000000 00000033054 14576745303 0021021 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 auth provides authentication for a client to a remote registry.
package auth
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"oras.land/oras-go/v2/registry/remote/internal/errutil"
"oras.land/oras-go/v2/registry/remote/retry"
)
// ErrBasicCredentialNotFound is returned when the credential is not found for
// basic auth.
var ErrBasicCredentialNotFound = errors.New("basic credential not found")
// DefaultClient is the default auth-decorated client.
var DefaultClient = &Client{
Client: retry.DefaultClient,
Header: http.Header{
"User-Agent": {"oras-go"},
},
Cache: DefaultCache,
}
// maxResponseBytes specifies the default limit on how many response bytes are
// allowed in the server's response from authorization service servers.
// A typical response message from authorization service servers is around 1 to
// 4 KiB. Since the size of a token must be smaller than the HTTP header size
// limit, which is usually 16 KiB. As specified by the distribution, the
// response may contain 2 identical tokens, that is, 16 x 2 = 32 KiB.
// Hence, 128 KiB should be sufficient.
// References: https://docs.docker.com/registry/spec/auth/token/
var maxResponseBytes int64 = 128 * 1024 // 128 KiB
// defaultClientID specifies the default client ID used in OAuth2.
// See also ClientID.
var defaultClientID = "oras-go"
// CredentialFunc represents a function that resolves the credential for the
// given registry (i.e. host:port).
//
// [EmptyCredential] is a valid return value and should not be considered as
// an error.
type CredentialFunc func(ctx context.Context, hostport string) (Credential, error)
// StaticCredential specifies static credentials for the given host.
func StaticCredential(registry string, cred Credential) CredentialFunc {
if registry == "docker.io" {
// it is expected that traffic targeting "docker.io" will be redirected
// to "registry-1.docker.io"
// reference: https://github.com/moby/moby/blob/v24.0.0-beta.2/registry/config.go#L25-L48
registry = "registry-1.docker.io"
}
return func(_ context.Context, hostport string) (Credential, error) {
if hostport == registry {
return cred, nil
}
return EmptyCredential, nil
}
}
// Client is an auth-decorated HTTP client.
// Its zero value is a usable client that uses http.DefaultClient with no cache.
type Client struct {
// Client is the underlying HTTP client used to access the remote
// server.
// If nil, http.DefaultClient is used.
// It is possible to use the default retry client from the package
// `oras.land/oras-go/v2/registry/remote/retry`. That client is already available
// in the DefaultClient.
// It is also possible to use a custom client. For example, github.com/hashicorp/go-retryablehttp
// is a popular HTTP client that supports retries.
Client *http.Client
// Header contains the custom headers to be added to each request.
Header http.Header
// Credential specifies the function for resolving the credential for the
// given registry (i.e. host:port).
// EmptyCredential is a valid return value and should not be considered as
// an error.
// If nil, the credential is always resolved to EmptyCredential.
Credential CredentialFunc
// Cache caches credentials for direct accessing the remote registry.
// If nil, no cache is used.
Cache Cache
// ClientID used in fetching OAuth2 token as a required field.
// If empty, a default client ID is used.
// Reference: https://docs.docker.com/registry/spec/auth/oauth/#getting-a-token
ClientID string
// ForceAttemptOAuth2 controls whether to follow OAuth2 with password grant
// instead the distribution spec when authenticating using username and
// password.
// References:
// - https://docs.docker.com/registry/spec/auth/jwt/
// - https://docs.docker.com/registry/spec/auth/oauth/
ForceAttemptOAuth2 bool
}
// client returns an HTTP client used to access the remote registry.
// http.DefaultClient is return if the client is not configured.
func (c *Client) client() *http.Client {
if c.Client == nil {
return http.DefaultClient
}
return c.Client
}
// send adds headers to the request and sends the request to the remote server.
func (c *Client) send(req *http.Request) (*http.Response, error) {
for key, values := range c.Header {
req.Header[key] = append(req.Header[key], values...)
}
return c.client().Do(req)
}
// credential resolves the credential for the given registry.
func (c *Client) credential(ctx context.Context, reg string) (Credential, error) {
if c.Credential == nil {
return EmptyCredential, nil
}
return c.Credential(ctx, reg)
}
// cache resolves the cache.
// noCache is return if the cache is not configured.
func (c *Client) cache() Cache {
if c.Cache == nil {
return noCache{}
}
return c.Cache
}
// SetUserAgent sets the user agent for all out-going requests.
func (c *Client) SetUserAgent(userAgent string) {
if c.Header == nil {
c.Header = http.Header{}
}
c.Header.Set("User-Agent", userAgent)
}
// Do sends the request to the remote server, attempting to resolve
// authentication if 'Authorization' header is not set.
//
// On authentication failure due to bad credential,
// - Do returns error if it fails to fetch token for bearer auth.
// - Do returns the registry response without error for basic auth.
func (c *Client) Do(originalReq *http.Request) (*http.Response, error) {
if auth := originalReq.Header.Get("Authorization"); auth != "" {
return c.send(originalReq)
}
ctx := originalReq.Context()
req := originalReq.Clone(ctx)
// attempt cached auth token
var attemptedKey string
cache := c.cache()
host := originalReq.Host
scheme, err := cache.GetScheme(ctx, host)
if err == nil {
switch scheme {
case SchemeBasic:
token, err := cache.GetToken(ctx, host, SchemeBasic, "")
if err == nil {
req.Header.Set("Authorization", "Basic "+token)
}
case SchemeBearer:
scopes := GetAllScopesForHost(ctx, host)
attemptedKey = strings.Join(scopes, " ")
token, err := cache.GetToken(ctx, host, SchemeBearer, attemptedKey)
if err == nil {
req.Header.Set("Authorization", "Bearer "+token)
}
}
}
resp, err := c.send(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusUnauthorized {
return resp, nil
}
// attempt again with credentials for recognized schemes
challenge := resp.Header.Get("Www-Authenticate")
scheme, params := parseChallenge(challenge)
switch scheme {
case SchemeBasic:
resp.Body.Close()
token, err := cache.Set(ctx, host, SchemeBasic, "", func(ctx context.Context) (string, error) {
return c.fetchBasicAuth(ctx, host)
})
if err != nil {
return nil, fmt.Errorf("%s %q: %w", resp.Request.Method, resp.Request.URL, err)
}
req = originalReq.Clone(ctx)
req.Header.Set("Authorization", "Basic "+token)
case SchemeBearer:
resp.Body.Close()
scopes := GetAllScopesForHost(ctx, host)
if paramScope := params["scope"]; paramScope != "" {
// merge hinted scopes with challenged scopes
scopes = append(scopes, strings.Split(paramScope, " ")...)
scopes = CleanScopes(scopes)
}
key := strings.Join(scopes, " ")
// attempt the cache again if there is a scope change
if key != attemptedKey {
if token, err := cache.GetToken(ctx, host, SchemeBearer, key); err == nil {
req = originalReq.Clone(ctx)
req.Header.Set("Authorization", "Bearer "+token)
if err := rewindRequestBody(req); err != nil {
return nil, err
}
resp, err := c.send(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusUnauthorized {
return resp, nil
}
resp.Body.Close()
}
}
// attempt with credentials
realm := params["realm"]
service := params["service"]
token, err := cache.Set(ctx, host, SchemeBearer, key, func(ctx context.Context) (string, error) {
return c.fetchBearerToken(ctx, host, realm, service, scopes)
})
if err != nil {
return nil, fmt.Errorf("%s %q: %w", resp.Request.Method, resp.Request.URL, err)
}
req = originalReq.Clone(ctx)
req.Header.Set("Authorization", "Bearer "+token)
default:
return resp, nil
}
if err := rewindRequestBody(req); err != nil {
return nil, err
}
return c.send(req)
}
// fetchBasicAuth fetches a basic auth token for the basic challenge.
func (c *Client) fetchBasicAuth(ctx context.Context, registry string) (string, error) {
cred, err := c.credential(ctx, registry)
if err != nil {
return "", fmt.Errorf("failed to resolve credential: %w", err)
}
if cred == EmptyCredential {
return "", ErrBasicCredentialNotFound
}
if cred.Username == "" || cred.Password == "" {
return "", errors.New("missing username or password for basic auth")
}
auth := cred.Username + ":" + cred.Password
return base64.StdEncoding.EncodeToString([]byte(auth)), nil
}
// fetchBearerToken fetches an access token for the bearer challenge.
func (c *Client) fetchBearerToken(ctx context.Context, registry, realm, service string, scopes []string) (string, error) {
cred, err := c.credential(ctx, registry)
if err != nil {
return "", err
}
if cred.AccessToken != "" {
return cred.AccessToken, nil
}
if cred == EmptyCredential || (cred.RefreshToken == "" && !c.ForceAttemptOAuth2) {
return c.fetchDistributionToken(ctx, realm, service, scopes, cred.Username, cred.Password)
}
return c.fetchOAuth2Token(ctx, realm, service, scopes, cred)
}
// fetchDistributionToken fetches an access token as defined by the distribution
// specification.
// It fetches anonymous tokens if no credential is provided.
// References:
// - https://docs.docker.com/registry/spec/auth/jwt/
// - https://docs.docker.com/registry/spec/auth/token/
func (c *Client) fetchDistributionToken(ctx context.Context, realm, service string, scopes []string, username, password string) (string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, realm, nil)
if err != nil {
return "", err
}
if username != "" || password != "" {
req.SetBasicAuth(username, password)
}
q := req.URL.Query()
if service != "" {
q.Set("service", service)
}
for _, scope := range scopes {
q.Add("scope", scope)
}
req.URL.RawQuery = q.Encode()
resp, err := c.send(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", errutil.ParseErrorResponse(resp)
}
// As specified in https://docs.docker.com/registry/spec/auth/token/ section
// "Token Response Fields", the token is either in `token` or
// `access_token`. If both present, they are identical.
var result struct {
Token string `json:"token"`
AccessToken string `json:"access_token"`
}
lr := io.LimitReader(resp.Body, maxResponseBytes)
if err := json.NewDecoder(lr).Decode(&result); err != nil {
return "", fmt.Errorf("%s %q: failed to decode response: %w", resp.Request.Method, resp.Request.URL, err)
}
if result.AccessToken != "" {
return result.AccessToken, nil
}
if result.Token != "" {
return result.Token, nil
}
return "", fmt.Errorf("%s %q: empty token returned", resp.Request.Method, resp.Request.URL)
}
// fetchOAuth2Token fetches an OAuth2 access token.
// Reference: https://docs.docker.com/registry/spec/auth/oauth/
func (c *Client) fetchOAuth2Token(ctx context.Context, realm, service string, scopes []string, cred Credential) (string, error) {
form := url.Values{}
if cred.RefreshToken != "" {
form.Set("grant_type", "refresh_token")
form.Set("refresh_token", cred.RefreshToken)
} else if cred.Username != "" && cred.Password != "" {
form.Set("grant_type", "password")
form.Set("username", cred.Username)
form.Set("password", cred.Password)
} else {
return "", errors.New("missing username or password for bearer auth")
}
form.Set("service", service)
clientID := c.ClientID
if clientID == "" {
clientID = defaultClientID
}
form.Set("client_id", clientID)
if len(scopes) != 0 {
form.Set("scope", strings.Join(scopes, " "))
}
body := strings.NewReader(form.Encode())
req, err := http.NewRequestWithContext(ctx, http.MethodPost, realm, body)
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := c.send(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", errutil.ParseErrorResponse(resp)
}
var result struct {
AccessToken string `json:"access_token"`
}
lr := io.LimitReader(resp.Body, maxResponseBytes)
if err := json.NewDecoder(lr).Decode(&result); err != nil {
return "", fmt.Errorf("%s %q: failed to decode response: %w", resp.Request.Method, resp.Request.URL, err)
}
if result.AccessToken != "" {
return result.AccessToken, nil
}
return "", fmt.Errorf("%s %q: empty token returned", resp.Request.Method, resp.Request.URL)
}
// rewindRequestBody tries to rewind the request body if exists.
func rewindRequestBody(req *http.Request) error {
if req.Body == nil || req.Body == http.NoBody {
return nil
}
if req.GetBody == nil {
return fmt.Errorf("%s %q: request body is not rewindable", req.Method, req.URL)
}
body, err := req.GetBody()
if err != nil {
return fmt.Errorf("%s %q: failed to get request body: %w", req.Method, req.URL, err)
}
req.Body = body
return nil
}
oras-go-2.5.0/registry/remote/auth/client_test.go 0000664 0000000 0000000 00000413414 14576745303 0022062 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 auth
import (
"context"
"encoding/base64"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strings"
"sync/atomic"
"testing"
"oras.land/oras-go/v2/registry/remote/errcode"
)
func TestClient_SetUserAgent(t *testing.T) {
wantUserAgent := "test agent"
var requestCount, wantRequestCount int64
var successCount, wantSuccessCount int64
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&requestCount, 1)
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
if userAgent := r.UserAgent(); userAgent != wantUserAgent {
t.Errorf("unexpected User-Agent: %v, want %v", userAgent, wantUserAgent)
return
}
atomic.AddInt64(&successCount, 1)
}))
defer ts.Close()
var client Client
client.SetUserAgent(wantUserAgent)
req, err := http.NewRequest(http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount++; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
}
func TestClient_Do_Basic_Auth(t *testing.T) {
username := "test_user"
password := "test_password"
var requestCount, wantRequestCount int64
var successCount, wantSuccessCount int64
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&requestCount, 1)
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
header := "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password))
if auth := r.Header.Get("Authorization"); auth != header {
w.Header().Set("Www-Authenticate", `Basic realm="Test Server"`)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&successCount, 1)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
client := &Client{
Credential: func(ctx context.Context, reg string) (Credential, error) {
if reg != uri.Host {
err := fmt.Errorf("registry mismatch: got %v, want %v", reg, uri.Host)
t.Error(err)
return EmptyCredential, err
}
return Credential{
Username: username,
Password: password,
}, nil
},
}
// first request
req, err := http.NewRequest(http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount += 2; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
// credential change
username = "test_user2"
password = "test_password2"
req, err = http.NewRequest(http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err = client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount += 2; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
}
func TestClient_Do_Basic_Auth_Cached(t *testing.T) {
username := "test_user"
password := "test_password"
var requestCount, wantRequestCount int64
var successCount, wantSuccessCount int64
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&requestCount, 1)
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
header := "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password))
if auth := r.Header.Get("Authorization"); auth != header {
w.Header().Set("Www-Authenticate", `Basic realm="Test Server"`)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&successCount, 1)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
client := &Client{
Credential: func(ctx context.Context, reg string) (Credential, error) {
if reg != uri.Host {
err := fmt.Errorf("registry mismatch: got %v, want %v", reg, uri.Host)
t.Error(err)
return EmptyCredential, err
}
return Credential{
Username: username,
Password: password,
}, nil
},
Cache: NewCache(),
}
// first request
req, err := http.NewRequest(http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount += 2; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
// repeated request
req, err = http.NewRequest(http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err = client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount++; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
// credential change
username = "test_user2"
password = "test_password2"
req, err = http.NewRequest(http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err = client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount += 2; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
}
func TestClient_Do_Bearer_AccessToken(t *testing.T) {
accessToken := "test/access/token"
var requestCount, wantRequestCount int64
var successCount, wantSuccessCount int64
as := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Error("unexecuted attempt of authorization service")
w.WriteHeader(http.StatusUnauthorized)
}))
defer as.Close()
var service string
scope := "repository:test:pull,push"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&requestCount, 1)
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
header := "Bearer " + accessToken
if auth := r.Header.Get("Authorization"); auth != header {
challenge := fmt.Sprintf("Bearer realm=%q,service=%q,scope=%q", as.URL, service, scope)
w.Header().Set("Www-Authenticate", challenge)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&successCount, 1)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
service = uri.Host
client := &Client{
Credential: func(ctx context.Context, reg string) (Credential, error) {
if reg != uri.Host {
err := fmt.Errorf("registry mismatch: got %v, want %v", reg, uri.Host)
t.Error(err)
return EmptyCredential, err
}
return Credential{
AccessToken: accessToken,
}, nil
},
}
// first request
req, err := http.NewRequest(http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount += 2; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
// credential change
accessToken = "test/access/token/2"
req, err = http.NewRequest(http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err = client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount += 2; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
}
func TestClient_Do_Bearer_AccessToken_Cached(t *testing.T) {
accessToken := "test/access/token"
var requestCount, wantRequestCount int64
var successCount, wantSuccessCount int64
as := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Error("unexecuted attempt of authorization service")
w.WriteHeader(http.StatusUnauthorized)
}))
defer as.Close()
var service string
scope := "repository:test:pull,push"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&requestCount, 1)
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
header := "Bearer " + accessToken
if auth := r.Header.Get("Authorization"); auth != header {
challenge := fmt.Sprintf("Bearer realm=%q,service=%q,scope=%q", as.URL, service, scope)
w.Header().Set("Www-Authenticate", challenge)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&successCount, 1)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
service = uri.Host
client := &Client{
Credential: func(ctx context.Context, reg string) (Credential, error) {
if reg != uri.Host {
err := fmt.Errorf("registry mismatch: got %v, want %v", reg, uri.Host)
t.Error(err)
return EmptyCredential, err
}
return Credential{
AccessToken: accessToken,
}, nil
},
Cache: NewCache(),
}
// first request
ctx := WithScopes(context.Background(), scope)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount += 2; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
// repeated request
req, err = http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err = client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount++; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
// credential change
accessToken = "test/access/token/2"
req, err = http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err = client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount += 2; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
}
func TestClient_Do_Bearer_AccessToken_Cached_PerHost(t *testing.T) {
as := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Error("unexecuted attempt of authorization service")
w.WriteHeader(http.StatusUnauthorized)
}))
defer as.Close()
// set up server 1
var requestCount1, wantRequestCount1 int64
var successCount1, wantSuccessCount1 int64
var service1 string
scope1 := "repository:test:pull"
accessToken1 := "test/access/token/1"
ts1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&requestCount1, 1)
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
header := "Bearer " + accessToken1
if auth := r.Header.Get("Authorization"); auth != header {
challenge := fmt.Sprintf("Bearer realm=%q,service=%q,scope=%q", as.URL, service1, scope1)
w.Header().Set("Www-Authenticate", challenge)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&successCount1, 1)
}))
defer ts1.Close()
uri1, err := url.Parse(ts1.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
service1 = uri1.Host
client1 := &Client{
Credential: StaticCredential(uri1.Host, Credential{
AccessToken: accessToken1,
}),
Cache: NewCache(),
}
// set up server 2
var requestCount2, wantRequestCount2 int64
var successCount2, wantSuccessCount2 int64
var service2 string
scope2 := "repository:test:pull,push"
accessToken2 := "test/access/token/2"
ts2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&requestCount2, 1)
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
header := "Bearer " + accessToken2
if auth := r.Header.Get("Authorization"); auth != header {
challenge := fmt.Sprintf("Bearer realm=%q,service=%q,scope=%q", as.URL, service2, scope2)
w.Header().Set("Www-Authenticate", challenge)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&successCount2, 1)
}))
defer ts2.Close()
uri2, err := url.Parse(ts2.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
service2 = uri2.Host
client2 := &Client{
Credential: StaticCredential(uri2.Host, Credential{
AccessToken: accessToken2,
}),
Cache: NewCache(),
}
ctx := context.Background()
ctx = WithScopesForHost(ctx, uri1.Host, scope1)
ctx = WithScopesForHost(ctx, uri2.Host, scope2)
// first request to server 1
req1, err := http.NewRequestWithContext(ctx, http.MethodGet, ts1.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp1, err := client1.Do(req1)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp1.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp1.StatusCode, http.StatusOK)
}
if wantRequestCount1 += 2; requestCount1 != wantRequestCount1 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount1, wantRequestCount1)
}
if wantSuccessCount1++; successCount1 != wantSuccessCount1 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount1, wantSuccessCount1)
}
// first request to server 2
req2, err := http.NewRequestWithContext(ctx, http.MethodGet, ts2.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp2, err := client2.Do(req2)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp2.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp2.StatusCode, http.StatusOK)
}
if wantRequestCount2 += 2; requestCount1 != wantRequestCount2 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount1, wantRequestCount1)
}
if wantSuccessCount2++; successCount2 != wantSuccessCount2 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount2, wantSuccessCount2)
}
// repeated request to server 1
req1, err = http.NewRequestWithContext(ctx, http.MethodGet, ts1.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp1, err = client1.Do(req1)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp1.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp1.StatusCode, http.StatusOK)
}
if wantRequestCount1++; requestCount1 != wantRequestCount1 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount1, wantRequestCount1)
}
if wantSuccessCount1++; successCount1 != wantSuccessCount1 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount1, wantSuccessCount1)
}
// repeated request to server 2
req2, err = http.NewRequestWithContext(ctx, http.MethodGet, ts2.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp2, err = client2.Do(req2)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp2.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp2.StatusCode, http.StatusOK)
}
if wantRequestCount2++; requestCount2 != wantRequestCount2 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount2, wantRequestCount2)
}
if wantSuccessCount2++; successCount2 != wantSuccessCount2 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount2, wantSuccessCount2)
}
// credential change for server 1
accessToken1 = "test/access/token/1/new"
req1, err = http.NewRequestWithContext(ctx, http.MethodGet, ts1.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
client1.Credential = StaticCredential(uri1.Host, Credential{
AccessToken: accessToken1,
})
resp1, err = client1.Do(req1)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp1.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp1.StatusCode, http.StatusOK)
}
if wantRequestCount1 += 2; requestCount1 != wantRequestCount1 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount1, wantRequestCount1)
}
if wantSuccessCount1++; successCount1 != wantSuccessCount1 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount1, wantSuccessCount1)
}
// credential change for server 2
accessToken2 = "test/access/token/2/new"
req2, err = http.NewRequestWithContext(ctx, http.MethodGet, ts2.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
client2.Credential = StaticCredential(uri2.Host, Credential{
AccessToken: accessToken2,
})
resp2, err = client2.Do(req2)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp2.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp2.StatusCode, http.StatusOK)
}
if wantRequestCount2 += 2; requestCount2 != wantRequestCount2 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount2, wantRequestCount2)
}
if wantSuccessCount2++; successCount2 != wantSuccessCount2 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount2, wantSuccessCount2)
}
}
func TestClient_Do_Bearer_Auth(t *testing.T) {
username := "test_user"
password := "test_password"
accessToken := "test/access/token"
var requestCount, wantRequestCount int64
var successCount, wantSuccessCount int64
var authCount, wantAuthCount int64
var service string
scopes := []string{
"repository:dst:pull,push",
"repository:src:pull",
}
as := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Error("unexecuted attempt of authorization service")
w.WriteHeader(http.StatusUnauthorized)
return
}
header := "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password))
if auth := r.Header.Get("Authorization"); auth != header {
t.Errorf("unexpected auth: got %s, want %s", auth, header)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.URL.Query().Get("service"); got != service {
t.Errorf("unexpected service: got %s, want %s", got, service)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.URL.Query()["scope"]; !reflect.DeepEqual(got, scopes) {
t.Errorf("unexpected scope: got %s, want %s", got, scopes)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&authCount, 1)
if _, err := fmt.Fprintf(w, `{"access_token":%q}`, accessToken); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
}))
defer as.Close()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&requestCount, 1)
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
header := "Bearer " + accessToken
if auth := r.Header.Get("Authorization"); auth != header {
challenge := fmt.Sprintf("Bearer realm=%q,service=%q,scope=%q", as.URL, service, strings.Join(scopes, " "))
w.Header().Set("Www-Authenticate", challenge)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&successCount, 1)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
service = uri.Host
client := &Client{
Credential: func(ctx context.Context, reg string) (Credential, error) {
if reg != uri.Host {
err := fmt.Errorf("registry mismatch: got %v, want %v", reg, uri.Host)
t.Error(err)
return EmptyCredential, err
}
return Credential{
Username: username,
Password: password,
}, nil
},
}
// first request
req, err := http.NewRequest(http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount += 2; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
if wantAuthCount++; authCount != wantAuthCount {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount, wantAuthCount)
}
// credential change
username = "test_user2"
password = "test_password2"
accessToken = "test/access/token/2"
req, err = http.NewRequest(http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err = client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount += 2; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
if wantAuthCount++; authCount != wantAuthCount {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount, wantAuthCount)
}
}
func TestClient_Do_Bearer_Auth_Cached(t *testing.T) {
username := "test_user"
password := "test_password"
accessToken := "test/access/token"
var requestCount, wantRequestCount int64
var successCount, wantSuccessCount int64
var authCount, wantAuthCount int64
var service string
scopes := []string{
"repository:dst:pull,push",
"repository:src:pull",
}
as := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Error("unexecuted attempt of authorization service")
w.WriteHeader(http.StatusUnauthorized)
return
}
header := "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password))
if auth := r.Header.Get("Authorization"); auth != header {
t.Errorf("unexpected auth: got %s, want %s", auth, header)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.URL.Query().Get("service"); got != service {
t.Errorf("unexpected service: got %s, want %s", got, service)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.URL.Query()["scope"]; !reflect.DeepEqual(got, scopes) {
t.Errorf("unexpected scope: got %s, want %s", got, scopes)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&authCount, 1)
if _, err := fmt.Fprintf(w, `{"access_token":%q}`, accessToken); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
}))
defer as.Close()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&requestCount, 1)
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
header := "Bearer " + accessToken
if auth := r.Header.Get("Authorization"); auth != header {
challenge := fmt.Sprintf("Bearer realm=%q,service=%q,scope=%q", as.URL, service, strings.Join(scopes, " "))
w.Header().Set("Www-Authenticate", challenge)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&successCount, 1)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
service = uri.Host
client := &Client{
Credential: func(ctx context.Context, reg string) (Credential, error) {
if reg != uri.Host {
err := fmt.Errorf("registry mismatch: got %v, want %v", reg, uri.Host)
t.Error(err)
return EmptyCredential, err
}
return Credential{
Username: username,
Password: password,
}, nil
},
Cache: NewCache(),
}
// first request
ctx := WithScopes(context.Background(), scopes...)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount += 2; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
if wantAuthCount++; authCount != wantAuthCount {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount, wantAuthCount)
}
// repeated request
req, err = http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err = client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount++; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
if authCount != wantAuthCount {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount, wantAuthCount)
}
// credential change
username = "test_user2"
password = "test_password2"
accessToken = "test/access/token/2"
req, err = http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err = client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount += 2; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
if wantAuthCount++; authCount != wantAuthCount {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount, wantAuthCount)
}
}
func TestClient_Do_Bearer_Auth_Cached_PerHost(t *testing.T) {
// set up server 1
username1 := "test_user1"
password1 := "test_password1"
accessToken1 := "test/access/token/1"
var requestCount1, wantRequestCount1 int64
var successCount1, wantSuccessCount1 int64
var authCount1, wantAuthCount1 int64
var service1 string
scopes1 := []string{
"repository:src:pull",
}
as1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Error("unexecuted attempt of authorization service")
w.WriteHeader(http.StatusUnauthorized)
return
}
header := "Basic " + base64.StdEncoding.EncodeToString([]byte(username1+":"+password1))
if auth := r.Header.Get("Authorization"); auth != header {
t.Errorf("unexpected auth: got %s, want %s", auth, header)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.URL.Query().Get("service"); got != service1 {
t.Errorf("unexpected service: got %s, want %s", got, service1)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.URL.Query()["scope"]; !reflect.DeepEqual(got, scopes1) {
t.Errorf("unexpected scope: got %s, want %s", got, scopes1)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&authCount1, 1)
if _, err := fmt.Fprintf(w, `{"access_token":%q}`, accessToken1); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
}))
defer as1.Close()
ts1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&requestCount1, 1)
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
header := "Bearer " + accessToken1
if auth := r.Header.Get("Authorization"); auth != header {
challenge := fmt.Sprintf("Bearer realm=%q,service=%q,scope=%q", as1.URL, service1, strings.Join(scopes1, " "))
w.Header().Set("Www-Authenticate", challenge)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&successCount1, 1)
}))
defer ts1.Close()
uri1, err := url.Parse(ts1.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
service1 = uri1.Host
client1 := &Client{
Credential: StaticCredential(uri1.Host, Credential{
Username: username1,
Password: password1,
}),
Cache: NewCache(),
}
// set up server 2
username2 := "test_user2"
password2 := "test_password2"
accessToken2 := "test/access/token/1"
var requestCount2, wantRequestCount2 int64
var successCount2, wantSuccessCount2 int64
var authCount2, wantAuthCount2 int64
var service2 string
scopes2 := []string{
"repository:dst:pull,push",
}
as2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Error("unexecuted attempt of authorization service")
w.WriteHeader(http.StatusUnauthorized)
return
}
header := "Basic " + base64.StdEncoding.EncodeToString([]byte(username2+":"+password2))
if auth := r.Header.Get("Authorization"); auth != header {
t.Errorf("unexpected auth: got %s, want %s", auth, header)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.URL.Query().Get("service"); got != service2 {
t.Errorf("unexpected service: got %s, want %s", got, service2)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.URL.Query()["scope"]; !reflect.DeepEqual(got, scopes2) {
t.Errorf("unexpected scope: got %s, want %s", got, scopes2)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&authCount2, 1)
if _, err := fmt.Fprintf(w, `{"access_token":%q}`, accessToken2); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
}))
defer as1.Close()
ts2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&requestCount2, 1)
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
header := "Bearer " + accessToken2
if auth := r.Header.Get("Authorization"); auth != header {
challenge := fmt.Sprintf("Bearer realm=%q,service=%q,scope=%q", as2.URL, service2, strings.Join(scopes2, " "))
w.Header().Set("Www-Authenticate", challenge)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&successCount2, 1)
}))
defer ts2.Close()
uri2, err := url.Parse(ts2.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
service2 = uri2.Host
client2 := &Client{
Credential: StaticCredential(uri2.Host, Credential{
Username: username2,
Password: password2,
}),
Cache: NewCache(),
}
ctx := context.Background()
ctx = WithScopesForHost(ctx, uri1.Host, scopes1...)
ctx = WithScopesForHost(ctx, uri2.Host, scopes2...)
// first request to server 1
req1, err := http.NewRequestWithContext(ctx, http.MethodGet, ts1.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp1, err := client1.Do(req1)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp1.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp1.StatusCode, http.StatusOK)
}
if wantRequestCount1 += 2; requestCount1 != wantRequestCount1 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount1, wantRequestCount1)
}
if wantSuccessCount1++; successCount1 != wantSuccessCount1 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount1, wantSuccessCount1)
}
if wantAuthCount1++; authCount1 != wantAuthCount1 {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount1, wantAuthCount1)
}
// first request to server 2
req2, err := http.NewRequestWithContext(ctx, http.MethodGet, ts2.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp2, err := client2.Do(req2)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp2.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp2.StatusCode, http.StatusOK)
}
if wantRequestCount2 += 2; requestCount2 != wantRequestCount2 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount2, wantRequestCount2)
}
if wantSuccessCount2++; successCount2 != wantSuccessCount2 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount2, wantSuccessCount2)
}
if wantAuthCount2++; authCount2 != wantAuthCount2 {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount2, wantAuthCount2)
}
// repeated request to server 1
req1, err = http.NewRequestWithContext(ctx, http.MethodGet, ts1.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp1, err = client1.Do(req1)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp1.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp1.StatusCode, http.StatusOK)
}
if wantRequestCount1++; requestCount1 != wantRequestCount1 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount1, wantRequestCount1)
}
if wantSuccessCount1++; successCount1 != wantSuccessCount1 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount1, wantSuccessCount1)
}
if authCount1 != wantAuthCount1 {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount1, wantAuthCount1)
}
// repeated request to server 2
req2, err = http.NewRequestWithContext(ctx, http.MethodGet, ts2.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp2, err = client2.Do(req2)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp2.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp2.StatusCode, http.StatusOK)
}
if wantRequestCount2++; requestCount2 != wantRequestCount2 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount2, wantRequestCount2)
}
if wantSuccessCount2++; successCount2 != wantSuccessCount2 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount2, wantSuccessCount2)
}
if authCount2 != wantAuthCount2 {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount2, wantAuthCount2)
}
// credential change for server 1
username1 = "test_user1_new"
password1 = "test_password1_new"
accessToken1 = "test/access/token/1/new"
req1, err = http.NewRequestWithContext(ctx, http.MethodGet, ts1.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
client1.Credential = StaticCredential(uri1.Host, Credential{
Username: username1,
Password: password1,
})
resp1, err = client1.Do(req1)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp1.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp1.StatusCode, http.StatusOK)
}
if wantRequestCount1 += 2; requestCount1 != wantRequestCount1 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount1, wantRequestCount1)
}
if wantSuccessCount1++; successCount1 != wantSuccessCount1 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount1, wantSuccessCount1)
}
if wantAuthCount1++; authCount1 != wantAuthCount1 {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount1, wantAuthCount1)
}
// credential change for server 2
username2 = "test_user2_new"
password2 = "test_password2_new"
accessToken2 = "test/access/token/2/new"
req2, err = http.NewRequestWithContext(ctx, http.MethodGet, ts2.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
client2.Credential = StaticCredential(uri2.Host, Credential{
Username: username2,
Password: password2,
})
resp2, err = client2.Do(req2)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp2.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp2.StatusCode, http.StatusOK)
}
if wantRequestCount2 += 2; requestCount2 != wantRequestCount2 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount2, wantRequestCount2)
}
if wantSuccessCount2++; successCount2 != wantSuccessCount2 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount2, wantSuccessCount2)
}
if wantAuthCount2++; authCount2 != wantAuthCount2 {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount2, wantAuthCount2)
}
}
func TestClient_Do_Bearer_OAuth2_Password(t *testing.T) {
username := "test_user"
password := "test_password"
accessToken := "test/access/token"
var requestCount, wantRequestCount int64
var successCount, wantSuccessCount int64
var authCount, wantAuthCount int64
var service string
scopes := []string{
"repository:dst:pull,push",
"repository:src:pull",
}
as := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/" {
t.Error("unexecuted attempt of authorization service")
w.WriteHeader(http.StatusUnauthorized)
return
}
if err := r.ParseForm(); err != nil {
t.Errorf("failed to parse form: %v", err)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("grant_type"); got != "password" {
t.Errorf("unexpected grant type: %v, want %v", got, "password")
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("service"); got != service {
t.Errorf("unexpected service: %v, want %v", got, service)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("client_id"); got != defaultClientID {
t.Errorf("unexpected client id: %v, want %v", got, defaultClientID)
w.WriteHeader(http.StatusUnauthorized)
return
}
scope := strings.Join(scopes, " ")
if got := r.PostForm.Get("scope"); got != scope {
t.Errorf("unexpected scope: %v, want %v", got, scope)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("username"); got != username {
t.Errorf("unexpected username: %v, want %v", got, username)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("password"); got != password {
t.Errorf("unexpected password: %v, want %v", got, password)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&authCount, 1)
if _, err := fmt.Fprintf(w, `{"access_token":%q}`, accessToken); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
}))
defer as.Close()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&requestCount, 1)
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
header := "Bearer " + accessToken
if auth := r.Header.Get("Authorization"); auth != header {
challenge := fmt.Sprintf("Bearer realm=%q,service=%q,scope=%q", as.URL, service, strings.Join(scopes, " "))
w.Header().Set("Www-Authenticate", challenge)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&successCount, 1)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
service = uri.Host
client := &Client{
Credential: func(ctx context.Context, reg string) (Credential, error) {
if reg != uri.Host {
err := fmt.Errorf("registry mismatch: got %v, want %v", reg, uri.Host)
t.Error(err)
return EmptyCredential, err
}
return Credential{
Username: username,
Password: password,
}, nil
},
ForceAttemptOAuth2: true,
}
// first request
req, err := http.NewRequest(http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount += 2; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
if wantAuthCount++; authCount != wantAuthCount {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount, wantAuthCount)
}
// credential change
username = "test_user2"
password = "test_password2"
accessToken = "test/access/token/2"
req, err = http.NewRequest(http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err = client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount += 2; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
if wantAuthCount++; authCount != wantAuthCount {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount, wantAuthCount)
}
}
func TestClient_Do_Bearer_OAuth2_Password_Cached(t *testing.T) {
username := "test_user"
password := "test_password"
accessToken := "test/access/token"
var requestCount, wantRequestCount int64
var successCount, wantSuccessCount int64
var authCount, wantAuthCount int64
var service string
scopes := []string{
"repository:dst:pull,push",
"repository:src:pull",
}
as := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/" {
t.Error("unexecuted attempt of authorization service")
w.WriteHeader(http.StatusUnauthorized)
return
}
if err := r.ParseForm(); err != nil {
t.Errorf("failed to parse form: %v", err)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("grant_type"); got != "password" {
t.Errorf("unexpected grant type: %v, want %v", got, "password")
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("service"); got != service {
t.Errorf("unexpected service: %v, want %v", got, service)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("client_id"); got != defaultClientID {
t.Errorf("unexpected client id: %v, want %v", got, defaultClientID)
w.WriteHeader(http.StatusUnauthorized)
return
}
scope := strings.Join(scopes, " ")
if got := r.PostForm.Get("scope"); got != scope {
t.Errorf("unexpected scope: %v, want %v", got, scope)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("username"); got != username {
t.Errorf("unexpected username: %v, want %v", got, username)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("password"); got != password {
t.Errorf("unexpected password: %v, want %v", got, password)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&authCount, 1)
if _, err := fmt.Fprintf(w, `{"access_token":%q}`, accessToken); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
}))
defer as.Close()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&requestCount, 1)
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
header := "Bearer " + accessToken
if auth := r.Header.Get("Authorization"); auth != header {
challenge := fmt.Sprintf("Bearer realm=%q,service=%q,scope=%q", as.URL, service, strings.Join(scopes, " "))
w.Header().Set("Www-Authenticate", challenge)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&successCount, 1)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
service = uri.Host
client := &Client{
Credential: func(ctx context.Context, reg string) (Credential, error) {
if reg != uri.Host {
err := fmt.Errorf("registry mismatch: got %v, want %v", reg, uri.Host)
t.Error(err)
return EmptyCredential, err
}
return Credential{
Username: username,
Password: password,
}, nil
},
ForceAttemptOAuth2: true,
Cache: NewCache(),
}
// first request
ctx := WithScopes(context.Background(), scopes...)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount += 2; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
if wantAuthCount++; authCount != wantAuthCount {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount, wantAuthCount)
}
// repeated request
req, err = http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err = client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount++; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
if authCount != wantAuthCount {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount, wantAuthCount)
}
// credential change
username = "test_user2"
password = "test_password2"
accessToken = "test/access/token/2"
req, err = http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err = client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount += 2; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
if wantAuthCount++; authCount != wantAuthCount {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount, wantAuthCount)
}
}
func TestClient_Do_Bearer_OAuth2_Password_Cached_PerHost(t *testing.T) {
// set up server 1
username1 := "test_user1"
password1 := "test_password1"
accessToken1 := "test/access/token/1"
var requestCount1, wantRequestCount1 int64
var successCount1, wantSuccessCount1 int64
var authCount1, wantAuthCount1 int64
var service1 string
scopes1 := []string{
"repository:src:pull",
}
as1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/" {
t.Error("unexecuted attempt of authorization service")
w.WriteHeader(http.StatusUnauthorized)
return
}
if err := r.ParseForm(); err != nil {
t.Errorf("failed to parse form: %v", err)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("grant_type"); got != "password" {
t.Errorf("unexpected grant type: %v, want %v", got, "password")
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("service"); got != service1 {
t.Errorf("unexpected service: %v, want %v", got, service1)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("client_id"); got != defaultClientID {
t.Errorf("unexpected client id: %v, want %v", got, defaultClientID)
w.WriteHeader(http.StatusUnauthorized)
return
}
scope := strings.Join(scopes1, " ")
if got := r.PostForm.Get("scope"); got != scope {
t.Errorf("unexpected scope: %v, want %v", got, scope)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("username"); got != username1 {
t.Errorf("unexpected username: %v, want %v", got, username1)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("password"); got != password1 {
t.Errorf("unexpected password: %v, want %v", got, password1)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&authCount1, 1)
if _, err := fmt.Fprintf(w, `{"access_token":%q}`, accessToken1); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
}))
defer as1.Close()
ts1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&requestCount1, 1)
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
header := "Bearer " + accessToken1
if auth := r.Header.Get("Authorization"); auth != header {
challenge := fmt.Sprintf("Bearer realm=%q,service=%q,scope=%q", as1.URL, service1, strings.Join(scopes1, " "))
w.Header().Set("Www-Authenticate", challenge)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&successCount1, 1)
}))
defer ts1.Close()
uri1, err := url.Parse(ts1.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
service1 = uri1.Host
client1 := &Client{
Credential: StaticCredential(uri1.Host, Credential{
Username: username1,
Password: password1,
}),
ForceAttemptOAuth2: true,
Cache: NewCache(),
}
// set up server 2
username2 := "test_user2"
password2 := "test_password2"
accessToken2 := "test/access/token/2"
var requestCount2, wantRequestCount2 int64
var successCount2, wantSuccessCount2 int64
var authCount2, wantAuthCount2 int64
var service2 string
scopes2 := []string{
"repository:dst:pull,push",
}
as2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/" {
t.Error("unexecuted attempt of authorization service")
w.WriteHeader(http.StatusUnauthorized)
return
}
if err := r.ParseForm(); err != nil {
t.Errorf("failed to parse form: %v", err)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("grant_type"); got != "password" {
t.Errorf("unexpected grant type: %v, want %v", got, "password")
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("service"); got != service2 {
t.Errorf("unexpected service: %v, want %v", got, service2)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("client_id"); got != defaultClientID {
t.Errorf("unexpected client id: %v, want %v", got, defaultClientID)
w.WriteHeader(http.StatusUnauthorized)
return
}
scope := strings.Join(scopes2, " ")
if got := r.PostForm.Get("scope"); got != scope {
t.Errorf("unexpected scope: %v, want %v", got, scope)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("username"); got != username2 {
t.Errorf("unexpected username: %v, want %v", got, username2)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("password"); got != password2 {
t.Errorf("unexpected password: %v, want %v", got, password2)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&authCount2, 1)
if _, err := fmt.Fprintf(w, `{"access_token":%q}`, accessToken2); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
}))
defer as2.Close()
ts2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&requestCount2, 1)
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
header := "Bearer " + accessToken2
if auth := r.Header.Get("Authorization"); auth != header {
challenge := fmt.Sprintf("Bearer realm=%q,service=%q,scope=%q", as2.URL, service2, strings.Join(scopes2, " "))
w.Header().Set("Www-Authenticate", challenge)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&successCount2, 1)
}))
defer ts2.Close()
uri2, err := url.Parse(ts2.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
service2 = uri2.Host
client2 := &Client{
Credential: StaticCredential(uri2.Host, Credential{
Username: username2,
Password: password2,
}),
ForceAttemptOAuth2: true,
Cache: NewCache(),
}
ctx := context.Background()
ctx = WithScopesForHost(ctx, uri1.Host, scopes1...)
ctx = WithScopesForHost(ctx, uri2.Host, scopes2...)
// first request to server 1
req1, err := http.NewRequestWithContext(ctx, http.MethodGet, ts1.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp1, err := client1.Do(req1)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp1.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp1.StatusCode, http.StatusOK)
}
if wantRequestCount1 += 2; requestCount1 != wantRequestCount1 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount1, wantRequestCount1)
}
if wantSuccessCount1++; successCount1 != wantSuccessCount1 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount1, wantSuccessCount1)
}
if wantAuthCount1++; authCount1 != wantAuthCount1 {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount1, wantAuthCount1)
}
// first request to server 2
req2, err := http.NewRequestWithContext(ctx, http.MethodGet, ts2.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp2, err := client2.Do(req2)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp2.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp2.StatusCode, http.StatusOK)
}
if wantRequestCount2 += 2; requestCount2 != wantRequestCount2 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount2, wantRequestCount2)
}
if wantSuccessCount2++; successCount2 != wantSuccessCount2 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount2, wantSuccessCount2)
}
if wantAuthCount2++; authCount2 != wantAuthCount2 {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount2, wantAuthCount2)
}
// repeated request to server 1
req1, err = http.NewRequestWithContext(ctx, http.MethodGet, ts1.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp1, err = client1.Do(req1)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp1.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp1.StatusCode, http.StatusOK)
}
if wantRequestCount1++; requestCount1 != wantRequestCount1 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount1, wantRequestCount1)
}
if wantSuccessCount1++; successCount1 != wantSuccessCount1 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount1, wantSuccessCount1)
}
if authCount1 != wantAuthCount1 {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount1, wantAuthCount1)
}
// repeated request to server 2
req2, err = http.NewRequestWithContext(ctx, http.MethodGet, ts2.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp2, err = client2.Do(req2)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp2.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp2.StatusCode, http.StatusOK)
}
if wantRequestCount2++; requestCount2 != wantRequestCount2 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount2, wantRequestCount2)
}
if wantSuccessCount2++; successCount2 != wantSuccessCount2 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount2, wantSuccessCount2)
}
if authCount2 != wantAuthCount2 {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount2, wantAuthCount2)
}
// credential change for server 1
username1 = "test_user1_new"
password1 = "test_password1_new"
accessToken1 = "test/access/token/1/new"
req1, err = http.NewRequestWithContext(ctx, http.MethodGet, ts1.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
client1.Credential = StaticCredential(uri1.Host, Credential{
Username: username1,
Password: password1,
})
resp1, err = client1.Do(req1)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp1.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp1.StatusCode, http.StatusOK)
}
if wantRequestCount1 += 2; requestCount1 != wantRequestCount1 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount1, wantRequestCount1)
}
if wantSuccessCount1++; successCount1 != wantSuccessCount1 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount1, wantSuccessCount1)
}
if wantAuthCount1++; authCount1 != wantAuthCount1 {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount1, wantAuthCount1)
}
// credential change for server 2
username2 = "test_user2_new"
password2 = "test_password2_new"
accessToken2 = "test/access/token/2/new"
req2, err = http.NewRequestWithContext(ctx, http.MethodGet, ts2.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
client2.Credential = StaticCredential(uri2.Host, Credential{
Username: username2,
Password: password2,
})
resp2, err = client2.Do(req2)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp2.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp2.StatusCode, http.StatusOK)
}
if wantRequestCount2 += 2; requestCount2 != wantRequestCount2 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount2, wantRequestCount2)
}
if wantSuccessCount2++; successCount2 != wantSuccessCount2 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount2, wantSuccessCount2)
}
if wantAuthCount2++; authCount2 != wantAuthCount2 {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount2, wantAuthCount2)
}
}
func TestClient_Do_Bearer_OAuth2_RefreshToken(t *testing.T) {
refreshToken := "test/refresh/token"
accessToken := "test/access/token"
var requestCount, wantRequestCount int64
var successCount, wantSuccessCount int64
var authCount, wantAuthCount int64
var service string
scopes := []string{
"repository:dst:pull,push",
"repository:src:pull",
}
as := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/" {
t.Error("unexecuted attempt of authorization service")
w.WriteHeader(http.StatusUnauthorized)
return
}
if err := r.ParseForm(); err != nil {
t.Errorf("failed to parse form: %v", err)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("grant_type"); got != "refresh_token" {
t.Errorf("unexpected grant type: %v, want %v", got, "refresh_token")
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("service"); got != service {
t.Errorf("unexpected service: %v, want %v", got, service)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("client_id"); got != defaultClientID {
t.Errorf("unexpected client id: %v, want %v", got, defaultClientID)
w.WriteHeader(http.StatusUnauthorized)
return
}
scope := strings.Join(scopes, " ")
if got := r.PostForm.Get("scope"); got != scope {
t.Errorf("unexpected scope: %v, want %v", got, scope)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("refresh_token"); got != refreshToken {
t.Errorf("unexpected refresh token: %v, want %v", got, refreshToken)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&authCount, 1)
if _, err := fmt.Fprintf(w, `{"access_token":%q}`, accessToken); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
}))
defer as.Close()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&requestCount, 1)
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
header := "Bearer " + accessToken
if auth := r.Header.Get("Authorization"); auth != header {
challenge := fmt.Sprintf("Bearer realm=%q,service=%q,scope=%q", as.URL, service, strings.Join(scopes, " "))
w.Header().Set("Www-Authenticate", challenge)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&successCount, 1)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
service = uri.Host
client := &Client{
Credential: func(ctx context.Context, reg string) (Credential, error) {
if reg != uri.Host {
err := fmt.Errorf("registry mismatch: got %v, want %v", reg, uri.Host)
t.Error(err)
return EmptyCredential, err
}
return Credential{
RefreshToken: refreshToken,
}, nil
},
}
// first request
req, err := http.NewRequest(http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount += 2; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
if wantAuthCount++; authCount != wantAuthCount {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount, wantAuthCount)
}
// credential change
refreshToken = "test/refresh/token/2"
accessToken = "test/access/token/2"
req, err = http.NewRequest(http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err = client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount += 2; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
if wantAuthCount++; authCount != wantAuthCount {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount, wantAuthCount)
}
}
func TestClient_Do_Bearer_OAuth2_RefreshToken_Cached(t *testing.T) {
refreshToken := "test/refresh/token"
accessToken := "test/access/token"
var requestCount, wantRequestCount int64
var successCount, wantSuccessCount int64
var authCount, wantAuthCount int64
var service string
scopes := []string{
"repository:dst:pull,push",
"repository:src:pull",
}
as := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/" {
t.Error("unexecuted attempt of authorization service")
w.WriteHeader(http.StatusUnauthorized)
return
}
if err := r.ParseForm(); err != nil {
t.Errorf("failed to parse form: %v", err)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("grant_type"); got != "refresh_token" {
t.Errorf("unexpected grant type: %v, want %v", got, "refresh_token")
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("service"); got != service {
t.Errorf("unexpected service: %v, want %v", got, service)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("client_id"); got != defaultClientID {
t.Errorf("unexpected client id: %v, want %v", got, defaultClientID)
w.WriteHeader(http.StatusUnauthorized)
return
}
scope := strings.Join(scopes, " ")
if got := r.PostForm.Get("scope"); got != scope {
t.Errorf("unexpected scope: %v, want %v", got, scope)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("refresh_token"); got != refreshToken {
t.Errorf("unexpected refresh token: %v, want %v", got, refreshToken)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&authCount, 1)
if _, err := fmt.Fprintf(w, `{"access_token":%q}`, accessToken); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
}))
defer as.Close()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&requestCount, 1)
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
header := "Bearer " + accessToken
if auth := r.Header.Get("Authorization"); auth != header {
challenge := fmt.Sprintf("Bearer realm=%q,service=%q,scope=%q", as.URL, service, strings.Join(scopes, " "))
w.Header().Set("Www-Authenticate", challenge)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&successCount, 1)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
service = uri.Host
client := &Client{
Credential: func(ctx context.Context, reg string) (Credential, error) {
if reg != uri.Host {
err := fmt.Errorf("registry mismatch: got %v, want %v", reg, uri.Host)
t.Error(err)
return EmptyCredential, err
}
return Credential{
RefreshToken: refreshToken,
}, nil
},
Cache: NewCache(),
}
// first request
ctx := WithScopes(context.Background(), scopes...)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount += 2; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
if wantAuthCount++; authCount != wantAuthCount {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount, wantAuthCount)
}
// repeated request
req, err = http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err = client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount++; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
if authCount != wantAuthCount {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount, wantAuthCount)
}
// credential change
refreshToken = "test/refresh/token/2"
accessToken = "test/access/token/2"
req, err = http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err = client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount += 2; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
if wantAuthCount++; authCount != wantAuthCount {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount, wantAuthCount)
}
}
func TestClient_Do_Bearer_OAuth2_RefreshToken_Cached_PerHost(t *testing.T) {
// set up server 1
refreshToken1 := "test/refresh/token/1"
accessToken1 := "test/access/token/1"
var requestCount1, wantRequestCount1 int64
var successCount1, wantSuccessCount1 int64
var authCount1, wantAuthCount1 int64
var service1 string
scopes1 := []string{
"repository:src:pull",
}
as1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/" {
t.Error("unexecuted attempt of authorization service")
w.WriteHeader(http.StatusUnauthorized)
return
}
if err := r.ParseForm(); err != nil {
t.Errorf("failed to parse form: %v", err)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("grant_type"); got != "refresh_token" {
t.Errorf("unexpected grant type: %v, want %v", got, "refresh_token")
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("service"); got != service1 {
t.Errorf("unexpected service: %v, want %v", got, service1)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("client_id"); got != defaultClientID {
t.Errorf("unexpected client id: %v, want %v", got, defaultClientID)
w.WriteHeader(http.StatusUnauthorized)
return
}
scope := strings.Join(scopes1, " ")
if got := r.PostForm.Get("scope"); got != scope {
t.Errorf("unexpected scope: %v, want %v", got, scope)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("refresh_token"); got != refreshToken1 {
t.Errorf("unexpected refresh token: %v, want %v", got, refreshToken1)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&authCount1, 1)
if _, err := fmt.Fprintf(w, `{"access_token":%q}`, accessToken1); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
}))
defer as1.Close()
ts1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&requestCount1, 1)
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
header := "Bearer " + accessToken1
if auth := r.Header.Get("Authorization"); auth != header {
challenge := fmt.Sprintf("Bearer realm=%q,service=%q,scope=%q", as1.URL, service1, strings.Join(scopes1, " "))
w.Header().Set("Www-Authenticate", challenge)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&successCount1, 1)
}))
defer ts1.Close()
uri1, err := url.Parse(ts1.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
service1 = uri1.Host
client1 := &Client{
Credential: StaticCredential(uri1.Host, Credential{
RefreshToken: refreshToken1,
}),
Cache: NewCache(),
}
// set up server 2
refreshToken2 := "test/refresh/token/1"
accessToken2 := "test/access/token/1"
var requestCount2, wantRequestCount2 int64
var successCount2, wantSuccessCount2 int64
var authCount2, wantAuthCount2 int64
var service2 string
scopes2 := []string{
"repository:dst:pull,push",
}
as2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/" {
t.Error("unexecuted attempt of authorization service")
w.WriteHeader(http.StatusUnauthorized)
return
}
if err := r.ParseForm(); err != nil {
t.Errorf("failed to parse form: %v", err)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("grant_type"); got != "refresh_token" {
t.Errorf("unexpected grant type: %v, want %v", got, "refresh_token")
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("service"); got != service2 {
t.Errorf("unexpected service: %v, want %v", got, service2)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("client_id"); got != defaultClientID {
t.Errorf("unexpected client id: %v, want %v", got, defaultClientID)
w.WriteHeader(http.StatusUnauthorized)
return
}
scope := strings.Join(scopes2, " ")
if got := r.PostForm.Get("scope"); got != scope {
t.Errorf("unexpected scope: %v, want %v", got, scope)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("refresh_token"); got != refreshToken2 {
t.Errorf("unexpected refresh token: %v, want %v", got, refreshToken2)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&authCount2, 1)
if _, err := fmt.Fprintf(w, `{"access_token":%q}`, accessToken2); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
}))
defer as2.Close()
ts2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&requestCount2, 1)
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
header := "Bearer " + accessToken2
if auth := r.Header.Get("Authorization"); auth != header {
challenge := fmt.Sprintf("Bearer realm=%q,service=%q,scope=%q", as2.URL, service2, strings.Join(scopes2, " "))
w.Header().Set("Www-Authenticate", challenge)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&successCount2, 1)
}))
defer ts2.Close()
uri2, err := url.Parse(ts2.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
service2 = uri2.Host
client2 := &Client{
Credential: StaticCredential(uri2.Host, Credential{
RefreshToken: refreshToken2,
}),
Cache: NewCache(),
}
ctx := context.Background()
ctx = WithScopesForHost(ctx, uri1.Host, scopes1...)
ctx = WithScopesForHost(ctx, uri2.Host, scopes2...)
// first request to server 1
req1, err := http.NewRequestWithContext(ctx, http.MethodGet, ts1.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp1, err := client1.Do(req1)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp1.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp1.StatusCode, http.StatusOK)
}
if wantRequestCount1 += 2; requestCount1 != wantRequestCount1 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount1, wantRequestCount1)
}
if wantSuccessCount1++; successCount1 != wantSuccessCount1 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount1, wantSuccessCount1)
}
if wantAuthCount1++; authCount1 != wantAuthCount1 {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount1, wantAuthCount1)
}
// first request to server 2
req2, err := http.NewRequestWithContext(ctx, http.MethodGet, ts2.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp2, err := client2.Do(req2)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp2.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp2.StatusCode, http.StatusOK)
}
if wantRequestCount2 += 2; requestCount2 != wantRequestCount2 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount2, wantRequestCount2)
}
if wantSuccessCount2++; successCount2 != wantSuccessCount2 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount2, wantSuccessCount2)
}
if wantAuthCount2++; authCount2 != wantAuthCount2 {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount2, wantAuthCount2)
}
// repeated request to server 1
req1, err = http.NewRequestWithContext(ctx, http.MethodGet, ts1.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp1, err = client1.Do(req1)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp1.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp1.StatusCode, http.StatusOK)
}
if wantRequestCount1++; requestCount1 != wantRequestCount1 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount1, wantRequestCount1)
}
if wantSuccessCount1++; successCount1 != wantSuccessCount1 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount1, wantSuccessCount1)
}
if authCount1 != wantAuthCount1 {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount1, wantAuthCount1)
}
// repeated request to server 2
req2, err = http.NewRequestWithContext(ctx, http.MethodGet, ts2.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp2, err = client2.Do(req2)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp2.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp2.StatusCode, http.StatusOK)
}
if wantRequestCount2++; requestCount2 != wantRequestCount2 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount2, wantRequestCount2)
}
if wantSuccessCount2++; successCount2 != wantSuccessCount2 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount2, wantSuccessCount2)
}
if authCount2 != wantAuthCount2 {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount2, wantAuthCount2)
}
// credential change to server 1
refreshToken1 = "test/refresh/token/1/new"
accessToken1 = "test/access/token/1/new"
req1, err = http.NewRequestWithContext(ctx, http.MethodGet, ts1.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
client1.Credential = StaticCredential(uri1.Host, Credential{
RefreshToken: refreshToken1,
})
resp1, err = client1.Do(req1)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp1.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp1.StatusCode, http.StatusOK)
}
if wantRequestCount1 += 2; requestCount1 != wantRequestCount1 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount1, wantRequestCount1)
}
if wantSuccessCount1++; successCount1 != wantSuccessCount1 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount1, wantSuccessCount1)
}
if wantAuthCount1++; authCount1 != wantAuthCount1 {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount1, wantAuthCount1)
}
// credential change to server 2
refreshToken2 = "test/refresh/token/2/new"
accessToken2 = "test/access/token/2/new"
req2, err = http.NewRequestWithContext(ctx, http.MethodGet, ts2.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
client2.Credential = StaticCredential(uri2.Host, Credential{
RefreshToken: refreshToken2,
})
resp2, err = client2.Do(req2)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp2.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp2.StatusCode, http.StatusOK)
}
if wantRequestCount2 += 2; requestCount2 != wantRequestCount2 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount2, wantRequestCount2)
}
if wantSuccessCount2++; successCount2 != wantSuccessCount2 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount2, wantSuccessCount2)
}
if wantAuthCount2++; authCount2 != wantAuthCount2 {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount2, wantAuthCount2)
}
}
func TestClient_Do_Token_Expire(t *testing.T) {
refreshToken := "test/refresh/token"
accessToken := "test/access/token"
var requestCount, wantRequestCount int64
var successCount, wantSuccessCount int64
var authCount, wantAuthCount int64
var service string
scopes := []string{
"repository:dst:pull,push",
"repository:src:pull",
}
as := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/" {
t.Error("unexecuted attempt of authorization service")
w.WriteHeader(http.StatusUnauthorized)
return
}
if err := r.ParseForm(); err != nil {
t.Errorf("failed to parse form: %v", err)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("grant_type"); got != "refresh_token" {
t.Errorf("unexpected grant type: %v, want %v", got, "refresh_token")
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("service"); got != service {
t.Errorf("unexpected service: %v, want %v", got, service)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("client_id"); got != defaultClientID {
t.Errorf("unexpected client id: %v, want %v", got, defaultClientID)
w.WriteHeader(http.StatusUnauthorized)
return
}
scope := strings.Join(scopes, " ")
if got := r.PostForm.Get("scope"); got != scope {
t.Errorf("unexpected scope: %v, want %v", got, scope)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("refresh_token"); got != refreshToken {
t.Errorf("unexpected refresh token: %v, want %v", got, refreshToken)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&authCount, 1)
if _, err := fmt.Fprintf(w, `{"access_token":%q}`, accessToken); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
}))
defer as.Close()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&requestCount, 1)
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
header := "Bearer " + accessToken
if auth := r.Header.Get("Authorization"); auth != header {
challenge := fmt.Sprintf("Bearer realm=%q,service=%q,scope=%q", as.URL, service, strings.Join(scopes, " "))
w.Header().Set("Www-Authenticate", challenge)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&successCount, 1)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
service = uri.Host
client := &Client{
Credential: func(ctx context.Context, reg string) (Credential, error) {
if reg != uri.Host {
err := fmt.Errorf("registry mismatch: got %v, want %v", reg, uri.Host)
t.Error(err)
return EmptyCredential, err
}
return Credential{
RefreshToken: refreshToken,
}, nil
},
Cache: NewCache(),
}
// first request
ctx := WithScopes(context.Background(), scopes...)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount += 2; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
if wantAuthCount++; authCount != wantAuthCount {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount, wantAuthCount)
}
// invalidate the access token and request again
accessToken = "test/access/token/2"
req, err = http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err = client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount += 2; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
if wantAuthCount++; authCount != wantAuthCount {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount, wantAuthCount)
}
}
func TestClient_Do_Token_Expire_PerHost(t *testing.T) {
// set up server 1
refreshToken1 := "test/refresh/token/1"
accessToken1 := "test/access/token/1"
var requestCount1, wantRequestCount1 int64
var successCount1, wantSuccessCount1 int64
var authCount1, wantAuthCount1 int64
var service1 string
scopes1 := []string{
"repository:src:pull",
}
as1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/" {
t.Error("unexecuted attempt of authorization service")
w.WriteHeader(http.StatusUnauthorized)
return
}
if err := r.ParseForm(); err != nil {
t.Errorf("failed to parse form: %v", err)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("grant_type"); got != "refresh_token" {
t.Errorf("unexpected grant type: %v, want %v", got, "refresh_token")
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("service"); got != service1 {
t.Errorf("unexpected service: %v, want %v", got, service1)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("client_id"); got != defaultClientID {
t.Errorf("unexpected client id: %v, want %v", got, defaultClientID)
w.WriteHeader(http.StatusUnauthorized)
return
}
scope := strings.Join(scopes1, " ")
if got := r.PostForm.Get("scope"); got != scope {
t.Errorf("unexpected scope: %v, want %v", got, scope)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("refresh_token"); got != refreshToken1 {
t.Errorf("unexpected refresh token: %v, want %v", got, refreshToken1)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&authCount1, 1)
if _, err := fmt.Fprintf(w, `{"access_token":%q}`, accessToken1); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
}))
defer as1.Close()
ts1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&requestCount1, 1)
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
header := "Bearer " + accessToken1
if auth := r.Header.Get("Authorization"); auth != header {
challenge := fmt.Sprintf("Bearer realm=%q,service=%q,scope=%q", as1.URL, service1, strings.Join(scopes1, " "))
w.Header().Set("Www-Authenticate", challenge)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&successCount1, 1)
}))
defer ts1.Close()
uri1, err := url.Parse(ts1.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
service1 = uri1.Host
client1 := &Client{
Credential: StaticCredential(uri1.Host, Credential{
RefreshToken: refreshToken1,
}),
Cache: NewCache(),
}
// set up server 2
refreshToken2 := "test/refresh/token/2"
accessToken2 := "test/access/token/2"
var requestCount2, wantRequestCount2 int64
var successCount2, wantSuccessCount2 int64
var authCount2, wantAuthCount2 int64
var service2 string
scopes2 := []string{
"repository:dst:pull,push",
}
as2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/" {
t.Error("unexecuted attempt of authorization service")
w.WriteHeader(http.StatusUnauthorized)
return
}
if err := r.ParseForm(); err != nil {
t.Errorf("failed to parse form: %v", err)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("grant_type"); got != "refresh_token" {
t.Errorf("unexpected grant type: %v, want %v", got, "refresh_token")
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("service"); got != service2 {
t.Errorf("unexpected service: %v, want %v", got, service2)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("client_id"); got != defaultClientID {
t.Errorf("unexpected client id: %v, want %v", got, defaultClientID)
w.WriteHeader(http.StatusUnauthorized)
return
}
scope := strings.Join(scopes2, " ")
if got := r.PostForm.Get("scope"); got != scope {
t.Errorf("unexpected scope: %v, want %v", got, scope)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("refresh_token"); got != refreshToken2 {
t.Errorf("unexpected refresh token: %v, want %v", got, refreshToken2)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&authCount2, 1)
if _, err := fmt.Fprintf(w, `{"access_token":%q}`, accessToken2); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
}))
defer as2.Close()
ts2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&requestCount2, 1)
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
header := "Bearer " + accessToken2
if auth := r.Header.Get("Authorization"); auth != header {
challenge := fmt.Sprintf("Bearer realm=%q,service=%q,scope=%q", as2.URL, service2, strings.Join(scopes2, " "))
w.Header().Set("Www-Authenticate", challenge)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&successCount2, 1)
}))
defer ts2.Close()
uri2, err := url.Parse(ts2.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
service2 = uri2.Host
client2 := &Client{
Credential: StaticCredential(uri2.Host, Credential{
RefreshToken: refreshToken2,
}),
Cache: NewCache(),
}
ctx := context.Background()
ctx = WithScopesForHost(ctx, uri1.Host, scopes1...)
ctx = WithScopesForHost(ctx, uri2.Host, scopes2...)
// first request to server 1
req1, err := http.NewRequestWithContext(ctx, http.MethodGet, ts1.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp1, err := client1.Do(req1)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp1.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp1.StatusCode, http.StatusOK)
}
if wantRequestCount1 += 2; requestCount1 != wantRequestCount1 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount1, wantRequestCount1)
}
if wantSuccessCount1++; successCount1 != wantSuccessCount1 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount1, wantSuccessCount1)
}
if wantAuthCount1++; authCount1 != wantAuthCount1 {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount1, wantAuthCount1)
}
// first request to server 2
req2, err := http.NewRequestWithContext(ctx, http.MethodGet, ts2.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp2, err := client2.Do(req2)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp2.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp2.StatusCode, http.StatusOK)
}
if wantRequestCount2 += 2; requestCount2 != wantRequestCount2 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount2, wantRequestCount2)
}
if wantSuccessCount2++; successCount2 != wantSuccessCount2 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount2, wantSuccessCount2)
}
if wantAuthCount2++; authCount2 != wantAuthCount2 {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount2, wantAuthCount2)
}
// invalidate the access token and request again to server 1
accessToken1 = "test/access/token/1/new"
req1, err = http.NewRequestWithContext(ctx, http.MethodGet, ts1.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp1, err = client1.Do(req1)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp1.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp1.StatusCode, http.StatusOK)
}
if wantRequestCount1 += 2; requestCount1 != wantRequestCount1 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount1, wantRequestCount1)
}
if wantSuccessCount1++; successCount1 != wantSuccessCount1 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount1, wantSuccessCount1)
}
if wantAuthCount1++; authCount1 != wantAuthCount1 {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount1, wantAuthCount1)
}
// invalidate the access token and request again to server 2
accessToken2 = "test/access/token/2/new"
req2, err = http.NewRequestWithContext(ctx, http.MethodGet, ts2.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp2, err = client2.Do(req2)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp2.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp2.StatusCode, http.StatusOK)
}
if wantRequestCount2 += 2; requestCount2 != wantRequestCount2 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount2, wantRequestCount2)
}
if wantSuccessCount2++; successCount2 != wantSuccessCount2 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount2, wantSuccessCount2)
}
if wantAuthCount2++; authCount2 != wantAuthCount2 {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount2, wantAuthCount2)
}
}
func TestClient_Do_Scope_Hint_Mismatch(t *testing.T) {
username := "test_user"
password := "test_password"
accessToken := "test/access/token"
var requestCount, wantRequestCount int64
var successCount, wantSuccessCount int64
var authCount, wantAuthCount int64
var service string
scopes := []string{
"repository:dst:pull,push",
"repository:src:pull",
}
scope := "repository:test:delete"
as := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/" {
t.Error("unexecuted attempt of authorization service")
w.WriteHeader(http.StatusUnauthorized)
return
}
if err := r.ParseForm(); err != nil {
t.Errorf("failed to parse form: %v", err)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("grant_type"); got != "password" {
t.Errorf("unexpected grant type: %v, want %v", got, "password")
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("service"); got != service {
t.Errorf("unexpected service: %v, want %v", got, service)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("client_id"); got != defaultClientID {
t.Errorf("unexpected client id: %v, want %v", got, defaultClientID)
w.WriteHeader(http.StatusUnauthorized)
return
}
scopes := CleanScopes(append([]string{scope}, scopes...))
scope := strings.Join(scopes, " ")
if got := r.PostForm.Get("scope"); got != scope {
t.Errorf("unexpected scope: %v, want %v", got, scope)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("username"); got != username {
t.Errorf("unexpected username: %v, want %v", got, username)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("password"); got != password {
t.Errorf("unexpected password: %v, want %v", got, password)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&authCount, 1)
if _, err := fmt.Fprintf(w, `{"access_token":%q}`, accessToken); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
}))
defer as.Close()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&requestCount, 1)
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
header := "Bearer " + accessToken
if auth := r.Header.Get("Authorization"); auth != header {
challenge := fmt.Sprintf("Bearer realm=%q,service=%q,scope=%q", as.URL, service, scope)
w.Header().Set("Www-Authenticate", challenge)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&successCount, 1)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
service = uri.Host
client := &Client{
Credential: func(ctx context.Context, reg string) (Credential, error) {
if reg != uri.Host {
err := fmt.Errorf("registry mismatch: got %v, want %v", reg, uri.Host)
t.Error(err)
return EmptyCredential, err
}
return Credential{
Username: username,
Password: password,
}, nil
},
ForceAttemptOAuth2: true,
Cache: NewCache(),
}
// first request
ctx := WithScopes(context.Background(), scopes...)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount += 2; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
if wantAuthCount++; authCount != wantAuthCount {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount, wantAuthCount)
}
// repeated request
// although the actual scope does not match the hinted scopes, the client
// with cache cannot avoid a request to obtain a challenge but can prevent
// a repeated call to the authorization server.
req, err = http.NewRequestWithContext(ctx, http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err = client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount += 2; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
if authCount != wantAuthCount {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount, wantAuthCount)
}
}
func TestClient_Do_Scope_Hint_Mismatch_PerHost(t *testing.T) {
// set up server 1
username1 := "test_user1"
password1 := "test_password1"
accessToken1 := "test/access/token/1"
var requestCount1, wantRequestCount1 int64
var successCount1, wantSuccessCount1 int64
var authCount1, wantAuthCount1 int64
var service1 string
scopes1 := []string{
"repository:dst:pull,push",
"repository:src:pull",
}
scope1 := "repository:test1:delete"
as1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/" {
t.Error("unexecuted attempt of authorization service")
w.WriteHeader(http.StatusUnauthorized)
return
}
if err := r.ParseForm(); err != nil {
t.Errorf("failed to parse form: %v", err)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("grant_type"); got != "password" {
t.Errorf("unexpected grant type: %v, want %v", got, "password")
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("service"); got != service1 {
t.Errorf("unexpected service: %v, want %v", got, service1)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("client_id"); got != defaultClientID {
t.Errorf("unexpected client id: %v, want %v", got, defaultClientID)
w.WriteHeader(http.StatusUnauthorized)
return
}
scopes := CleanScopes(append([]string{scope1}, scopes1...))
scope := strings.Join(scopes, " ")
if got := r.PostForm.Get("scope"); got != scope {
t.Errorf("unexpected scope: %v, want %v", got, scope)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("username"); got != username1 {
t.Errorf("unexpected username: %v, want %v", got, username1)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("password"); got != password1 {
t.Errorf("unexpected password: %v, want %v", got, password1)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&authCount1, 1)
if _, err := fmt.Fprintf(w, `{"access_token":%q}`, accessToken1); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
}))
defer as1.Close()
ts1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&requestCount1, 1)
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
header := "Bearer " + accessToken1
if auth := r.Header.Get("Authorization"); auth != header {
challenge := fmt.Sprintf("Bearer realm=%q,service=%q,scope=%q", as1.URL, service1, scope1)
w.Header().Set("Www-Authenticate", challenge)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&successCount1, 1)
}))
defer ts1.Close()
uri1, err := url.Parse(ts1.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
service1 = uri1.Host
client1 := &Client{
Credential: StaticCredential(uri1.Host, Credential{
Username: username1,
Password: password1,
}),
ForceAttemptOAuth2: true,
Cache: NewCache(),
}
// set up server 1
username2 := "test_user2"
password2 := "test_password2"
accessToken2 := "test/access/token/2"
var requestCount2, wantRequestCount2 int64
var successCount2, wantSuccessCount2 int64
var authCount2, wantAuthCount2 int64
var service2 string
scopes2 := []string{
"repository:dst:pull,push",
"repository:src:pull",
}
scope2 := "repository:test2:delete"
as2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/" {
t.Error("unexecuted attempt of authorization service")
w.WriteHeader(http.StatusUnauthorized)
return
}
if err := r.ParseForm(); err != nil {
t.Errorf("failed to parse form: %v", err)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("grant_type"); got != "password" {
t.Errorf("unexpected grant type: %v, want %v", got, "password")
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("service"); got != service2 {
t.Errorf("unexpected service: %v, want %v", got, service2)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("client_id"); got != defaultClientID {
t.Errorf("unexpected client id: %v, want %v", got, defaultClientID)
w.WriteHeader(http.StatusUnauthorized)
return
}
scopes := CleanScopes(append([]string{scope2}, scopes2...))
scope := strings.Join(scopes, " ")
if got := r.PostForm.Get("scope"); got != scope {
t.Errorf("unexpected scope: %v, want %v", got, scope)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("username"); got != username2 {
t.Errorf("unexpected username: %v, want %v", got, username2)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.PostForm.Get("password"); got != password2 {
t.Errorf("unexpected password: %v, want %v", got, password2)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&authCount2, 1)
if _, err := fmt.Fprintf(w, `{"access_token":%q}`, accessToken2); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
}))
defer as2.Close()
ts2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&requestCount2, 1)
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
header := "Bearer " + accessToken2
if auth := r.Header.Get("Authorization"); auth != header {
challenge := fmt.Sprintf("Bearer realm=%q,service=%q,scope=%q", as2.URL, service2, scope2)
w.Header().Set("Www-Authenticate", challenge)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&successCount2, 1)
}))
defer ts1.Close()
uri2, err := url.Parse(ts2.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
service2 = uri2.Host
client2 := &Client{
Credential: StaticCredential(uri2.Host, Credential{
Username: username2,
Password: password2,
}),
ForceAttemptOAuth2: true,
Cache: NewCache(),
}
ctx := context.Background()
ctx = WithScopesForHost(ctx, uri1.Host, scopes1...)
ctx = WithScopesForHost(ctx, uri2.Host, scopes2...)
// first request to server 1
req1, err := http.NewRequestWithContext(ctx, http.MethodGet, ts1.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp1, err := client1.Do(req1)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp1.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp1.StatusCode, http.StatusOK)
}
if wantRequestCount1 += 2; requestCount1 != wantRequestCount1 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount1, wantRequestCount1)
}
if wantSuccessCount1++; successCount1 != wantSuccessCount1 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount1, wantSuccessCount1)
}
if wantAuthCount1++; authCount1 != wantAuthCount1 {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount1, wantAuthCount1)
}
// first request to server 1
req2, err := http.NewRequestWithContext(ctx, http.MethodGet, ts2.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp2, err := client2.Do(req2)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp2.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp2.StatusCode, http.StatusOK)
}
if wantRequestCount2 += 2; requestCount2 != wantRequestCount2 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount2, wantRequestCount2)
}
if wantSuccessCount2++; successCount2 != wantSuccessCount2 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount2, wantSuccessCount2)
}
if wantAuthCount2++; authCount2 != wantAuthCount2 {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount2, wantAuthCount2)
}
// repeated request to server 1
// although the actual scope does not match the hinted scopes, the client
// with cache cannot avoid a request to obtain a challenge but can prevent
// a repeated call to the authorization server.
req1, err = http.NewRequestWithContext(ctx, http.MethodGet, ts1.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp1, err = client1.Do(req1)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp1.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp1.StatusCode, http.StatusOK)
}
if wantRequestCount1 += 2; requestCount1 != wantRequestCount1 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount1, wantRequestCount1)
}
if wantSuccessCount1++; successCount1 != wantSuccessCount1 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount1, wantSuccessCount1)
}
if authCount1 != wantAuthCount1 {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount1, wantAuthCount1)
}
// repeated request to server 2
// although the actual scope does not match the hinted scopes, the client
// with cache cannot avoid a request to obtain a challenge but can prevent
// a repeated call to the authorization server.
req2, err = http.NewRequestWithContext(ctx, http.MethodGet, ts2.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp2, err = client2.Do(req2)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp2.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp2.StatusCode, http.StatusOK)
}
if wantRequestCount2 += 2; requestCount2 != wantRequestCount2 {
t.Errorf("unexpected number of requests: %d, want %d", requestCount2, wantRequestCount2)
}
if wantSuccessCount2++; successCount2 != wantSuccessCount2 {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount2, wantSuccessCount2)
}
if authCount2 != wantAuthCount2 {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount2, wantAuthCount2)
}
}
func TestClient_Do_Invalid_Credential_Basic(t *testing.T) {
username := "test_user"
password := "test_password"
var requestCount, wantRequestCount int64
var successCount, wantSuccessCount int64
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&requestCount, 1)
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
header := "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password))
if auth := r.Header.Get("Authorization"); auth != header {
w.Header().Set("Www-Authenticate", `Basic realm="Test Server"`)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&successCount, 1)
t.Error("authentication should fail but succeeded")
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
client := &Client{
Credential: func(ctx context.Context, reg string) (Credential, error) {
if reg != uri.Host {
err := fmt.Errorf("registry mismatch: got %v, want %v", reg, uri.Host)
t.Error(err)
return EmptyCredential, err
}
return Credential{
Username: username,
Password: "bad credential",
}, nil
},
}
// request should fail
req, err := http.NewRequest(http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusUnauthorized {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusUnauthorized)
}
if wantRequestCount += 2; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
}
func TestClient_Do_Invalid_Credential_Bearer(t *testing.T) {
username := "test_user"
password := "test_password"
accessToken := "test/access/token"
var requestCount, wantRequestCount int64
var successCount, wantSuccessCount int64
var authCount, wantAuthCount int64
var service string
scopes := []string{
"repository:dst:pull,push",
"repository:src:pull",
}
as := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Error("unexecuted attempt of authorization service")
w.WriteHeader(http.StatusUnauthorized)
return
}
header := "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password))
if auth := r.Header.Get("Authorization"); auth != header {
atomic.AddInt64(&authCount, 1)
w.WriteHeader(http.StatusUnauthorized)
return
}
t.Error("authentication should fail but succeeded")
}))
defer as.Close()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&requestCount, 1)
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
header := "Bearer " + accessToken
if auth := r.Header.Get("Authorization"); auth != header {
challenge := fmt.Sprintf("Bearer realm=%q,service=%q,scope=%q", as.URL, service, strings.Join(scopes, " "))
w.Header().Set("Www-Authenticate", challenge)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&successCount, 1)
t.Error("authentication should fail but succeeded")
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
service = uri.Host
client := &Client{
Credential: func(ctx context.Context, reg string) (Credential, error) {
if reg != uri.Host {
err := fmt.Errorf("registry mismatch: got %v, want %v", reg, uri.Host)
t.Error(err)
return EmptyCredential, err
}
return Credential{
Username: username,
Password: "bad credential",
}, nil
},
}
// request should fail
req, err := http.NewRequest(http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
_, err = client.Do(req)
if err == nil {
t.Fatalf("Client.Do() error = %v, wantErr %v", err, true)
}
if wantRequestCount++; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
if wantAuthCount++; authCount != wantAuthCount {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount, wantAuthCount)
}
}
func TestClient_Do_Anonymous_Pull(t *testing.T) {
accessToken := "test/access/token"
var requestCount, wantRequestCount int64
var successCount, wantSuccessCount int64
var authCount, wantAuthCount int64
var service string
scope := "repository:test:pull"
as := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Error("unexecuted attempt of authorization service")
w.WriteHeader(http.StatusUnauthorized)
return
}
if auth := r.Header.Get("Authorization"); auth != "" {
t.Errorf("unexpected auth: got %s, want %s", auth, "")
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.URL.Query().Get("service"); got != service {
t.Errorf("unexpected service: got %s, want %s", got, service)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.URL.Query().Get("scope"); got != scope {
t.Errorf("unexpected scope: got %s, want %s", got, scope)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&authCount, 1)
if _, err := fmt.Fprintf(w, `{"access_token":%q}`, accessToken); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
}))
defer as.Close()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&requestCount, 1)
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
header := "Bearer " + accessToken
if auth := r.Header.Get("Authorization"); auth != header {
challenge := fmt.Sprintf("Bearer realm=%q,service=%q,scope=%q", as.URL, service, scope)
w.Header().Set("Www-Authenticate", challenge)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&successCount, 1)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
service = uri.Host
// request with the default client
req, err := http.NewRequest(http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err := DefaultClient.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount += 2; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
if wantAuthCount++; authCount != wantAuthCount {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount, wantAuthCount)
}
}
func TestClient_Do_Scheme_Change(t *testing.T) {
username := "test_user"
password := "test_password"
accessToken := "test/access/token"
var requestCount, wantRequestCount int64
var successCount, wantSuccessCount int64
var authCount, wantAuthCount int64
var service string
scope := "repository:test:pull"
challengeBearerAuth := true
as := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Error("unexecuted attempt of authorization service")
w.WriteHeader(http.StatusUnauthorized)
return
}
header := "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password))
if auth := r.Header.Get("Authorization"); auth != header {
t.Errorf("unexpected auth: got %s, want %s", auth, header)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.URL.Query().Get("service"); got != service {
t.Errorf("unexpected service: got %s, want %s", got, service)
w.WriteHeader(http.StatusUnauthorized)
return
}
if got := r.URL.Query().Get("scope"); got != scope {
t.Errorf("unexpected scope: got %s, want %s", got, scope)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&authCount, 1)
if _, err := fmt.Fprintf(w, `{"access_token":%q}`, accessToken); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
}))
defer as.Close()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt64(&requestCount, 1)
if r.Method != http.MethodGet || r.URL.Path != "/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
bearerHeader := "Bearer " + accessToken
basicHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password))
header := r.Header.Get("Authorization")
if (challengeBearerAuth && header != bearerHeader) || (!challengeBearerAuth && header != basicHeader) {
var challenge string
if challengeBearerAuth {
challenge = fmt.Sprintf("Bearer realm=%q,service=%q,scope=%q", as.URL, service, scope)
} else {
challenge = `Basic realm="Test Server"`
}
w.Header().Set("Www-Authenticate", challenge)
w.WriteHeader(http.StatusUnauthorized)
return
}
atomic.AddInt64(&successCount, 1)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
service = uri.Host
client := &Client{
Credential: func(ctx context.Context, reg string) (Credential, error) {
if reg != uri.Host {
err := fmt.Errorf("registry mismatch: got %v, want %v", reg, uri.Host)
t.Error(err)
return EmptyCredential, err
}
return Credential{
Username: username,
Password: password,
}, nil
},
Cache: NewCache(),
}
// request with bearer auth
req, err := http.NewRequest(http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount += 2; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
if wantAuthCount++; authCount != wantAuthCount {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount, wantAuthCount)
}
// change to basic auth
challengeBearerAuth = false
req, err = http.NewRequest(http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err = client.Do(req)
if err != nil {
t.Fatalf("Client.Do() error = %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Client.Do() = %v, want %v", resp.StatusCode, http.StatusOK)
}
if wantRequestCount += 2; requestCount != wantRequestCount {
t.Errorf("unexpected number of requests: %d, want %d", requestCount, wantRequestCount)
}
if wantSuccessCount++; successCount != wantSuccessCount {
t.Errorf("unexpected number of successful requests: %d, want %d", successCount, wantSuccessCount)
}
if authCount != wantAuthCount {
t.Errorf("unexpected number of auth requests: %d, want %d", authCount, wantAuthCount)
}
}
func TestStaticCredential(t *testing.T) {
tests := []struct {
name string
registry string
target string
cred Credential
want Credential
}{
{
name: "Matched credential for regular registry",
registry: "registry.example.com",
target: "registry.example.com",
cred: Credential{
Username: "username",
Password: "password",
},
want: Credential{
Username: "username",
Password: "password",
},
},
{
name: "Matched credential for docker.io",
registry: "docker.io",
target: "registry-1.docker.io",
cred: Credential{
Username: "username",
Password: "password",
},
want: Credential{
Username: "username",
Password: "password",
},
},
{
name: "Mismatched credential for regular registry",
registry: "registry.example.com",
target: "whatever.example.com",
cred: Credential{
Username: "username",
Password: "password",
},
want: EmptyCredential,
},
{
name: "Mismatched credential for docker.io",
registry: "docker.io",
target: "whatever.docker.io",
cred: Credential{
Username: "username",
Password: "password",
},
want: EmptyCredential,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &Client{
Credential: StaticCredential(tt.registry, tt.cred),
}
ctx := context.Background()
got, err := client.Credential(ctx, tt.target)
if err != nil {
t.Fatal("Client.Credential() error =", err)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Client.Credential() = %v, want %v", got, tt.want)
}
})
}
}
func TestClient_StaticCredential_basicAuth(t *testing.T) {
testUsername := "username"
testPassword := "password"
// create a test server
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusNotFound)
t.Fatal("unexpected access")
}
switch path {
case "/basicAuth":
wantedAuthHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte(testUsername+":"+testPassword))
authHeader := r.Header.Get("Authorization")
if authHeader != wantedAuthHeader {
w.Header().Set("Www-Authenticate", `Basic realm="Test Server"`)
w.WriteHeader(http.StatusUnauthorized)
}
default:
w.WriteHeader(http.StatusNotAcceptable)
}
}))
defer ts.Close()
host := ts.URL
uri, _ := url.Parse(host)
hostAddress := uri.Host
basicAuthURL := fmt.Sprintf("%s/basicAuth", host)
// create a test client with the correct credentials
clientValid := &Client{
Credential: StaticCredential(hostAddress, Credential{
Username: testUsername,
Password: testPassword,
}),
}
req, err := http.NewRequest(http.MethodGet, basicAuthURL, nil)
if err != nil {
t.Fatalf("could not create request, err = %v", err)
}
respValid, err := clientValid.Do(req)
if err != nil {
t.Fatalf("could not send request, err = %v", err)
}
if respValid.StatusCode != 200 {
t.Errorf("incorrect status code: %d, expected 200", respValid.StatusCode)
}
// create a test client with incorrect credentials
clientInvalid := &Client{
Credential: StaticCredential(hostAddress, Credential{
Username: "foo",
Password: "bar",
}),
}
respInvalid, err := clientInvalid.Do(req)
if err != nil {
t.Fatalf("could not send request, err = %v", err)
}
if respInvalid.StatusCode != 401 {
t.Errorf("incorrect status code: %d, expected 401", respInvalid.StatusCode)
}
}
func TestClient_StaticCredential_withAccessToken(t *testing.T) {
var host string
testAccessToken := "test/access/token"
scope := "repository:test:pull,push"
// create an authorization server
as := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
t.Error("unexecuted attempt of authorization service")
}))
defer as.Close()
// create a test server
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusNotFound)
t.Fatal("unexpected access")
}
switch path {
case "/accessToken":
wantedAuthHeader := "Bearer " + testAccessToken
if auth := r.Header.Get("Authorization"); auth != wantedAuthHeader {
challenge := fmt.Sprintf("Bearer realm=%q,service=%q,scope=%q", as.URL, host, scope)
w.Header().Set("Www-Authenticate", challenge)
w.WriteHeader(http.StatusUnauthorized)
}
default:
w.WriteHeader(http.StatusNotAcceptable)
}
}))
defer ts.Close()
host = ts.URL
uri, _ := url.Parse(host)
hostAddress := uri.Host
accessTokenURL := fmt.Sprintf("%s/accessToken", host)
// create a test client with the correct credentials
clientValid := &Client{
Credential: StaticCredential(hostAddress, Credential{
AccessToken: testAccessToken,
}),
}
req, err := http.NewRequest(http.MethodGet, accessTokenURL, nil)
if err != nil {
t.Fatalf("could not create request, err = %v", err)
}
respValid, err := clientValid.Do(req)
if err != nil {
t.Fatalf("could not send request, err = %v", err)
}
if respValid.StatusCode != 200 {
t.Errorf("incorrect status code: %d, expected 200", respValid.StatusCode)
}
// create a test client with incorrect credentials
clientInvalid := &Client{
Credential: StaticCredential(hostAddress, Credential{
AccessToken: "foo",
}),
}
respInvalid, err := clientInvalid.Do(req)
if err != nil {
t.Fatalf("could not send request, err = %v", err)
}
if respInvalid.StatusCode != 401 {
t.Errorf("incorrect status code: %d, expected 401", respInvalid.StatusCode)
}
}
func TestClient_StaticCredential_withRefreshToken(t *testing.T) {
var host string
testAccessToken := "test/access/token"
testRefreshToken := "test/refresh/token"
scope := "repository:test:pull,push"
// create an authorization server
as := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet && r.Method != http.MethodPost {
w.WriteHeader(http.StatusUnauthorized)
t.Error("unexecuted attempt of authorization service")
}
if err := r.ParseForm(); err != nil {
w.WriteHeader(http.StatusUnauthorized)
t.Error("failed to parse form")
}
if got := r.PostForm.Get("service"); got != host {
w.WriteHeader(http.StatusUnauthorized)
}
// handles refresh token requests
if got := r.PostForm.Get("grant_type"); got != "refresh_token" {
w.WriteHeader(http.StatusUnauthorized)
}
if got := r.PostForm.Get("scope"); got != scope {
w.WriteHeader(http.StatusUnauthorized)
}
if got := r.PostForm.Get("refresh_token"); got != testRefreshToken {
w.WriteHeader(http.StatusUnauthorized)
}
// writes back access token
if _, err := fmt.Fprintf(w, `{"access_token":%q}`, testAccessToken); err != nil {
t.Fatalf("could not write back access token, error = %v", err)
}
}))
defer as.Close()
// create a test server
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusNotFound)
panic("unexpected access")
}
switch path {
case "/refreshToken":
wantedAuthHeader := "Bearer " + testAccessToken
if auth := r.Header.Get("Authorization"); auth != wantedAuthHeader {
challenge := fmt.Sprintf("Bearer realm=%q,service=%q,scope=%q", as.URL, host, scope)
w.Header().Set("Www-Authenticate", challenge)
w.WriteHeader(http.StatusUnauthorized)
}
default:
w.WriteHeader(http.StatusNotAcceptable)
}
}))
defer ts.Close()
host = ts.URL
uri, _ := url.Parse(host)
hostAddress := uri.Host
refreshTokenURL := fmt.Sprintf("%s/refreshToken", host)
// create a test client with the correct credentials
clientValid := &Client{
Credential: StaticCredential(hostAddress, Credential{
RefreshToken: testRefreshToken,
}),
}
req, err := http.NewRequest(http.MethodGet, refreshTokenURL, nil)
if err != nil {
t.Fatalf("could not create request, err = %v", err)
}
respValid, err := clientValid.Do(req)
if err != nil {
t.Fatalf("could not send request, err = %v", err)
}
if respValid.StatusCode != 200 {
t.Errorf("incorrect status code: %d, expected 200", respValid.StatusCode)
}
// create a test client with incorrect credentials
clientInvalid := &Client{
Credential: StaticCredential(hostAddress, Credential{
RefreshToken: "bar",
}),
}
_, err = clientInvalid.Do(req)
var expectedError *errcode.ErrorResponse
if !errors.As(err, &expectedError) || expectedError.StatusCode != http.StatusUnauthorized {
t.Errorf("incorrect error: %v, expected %v", err, expectedError)
}
}
func TestClient_fetchBasicAuth(t *testing.T) {
c := &Client{
Credential: func(ctx context.Context, registry string) (Credential, error) {
return EmptyCredential, nil
},
}
_, err := c.fetchBasicAuth(context.Background(), "")
if err != ErrBasicCredentialNotFound {
t.Errorf("incorrect error: %v, expected %v", err, ErrBasicCredentialNotFound)
}
}
oras-go-2.5.0/registry/remote/auth/credential.go 0000664 0000000 0000000 00000002545 14576745303 0021656 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 auth
// EmptyCredential represents an empty credential.
var EmptyCredential Credential
// Credential contains authentication credentials used to access remote
// registries.
type Credential struct {
// Username is the name of the user for the remote registry.
Username string
// Password is the secret associated with the username.
Password string
// RefreshToken is a bearer token to be sent to the authorization service
// for fetching access tokens.
// A refresh token is often referred as an identity token.
// Reference: https://docs.docker.com/registry/spec/auth/oauth/
RefreshToken string
// AccessToken is a bearer token to be sent to the registry.
// An access token is often referred as a registry token.
// Reference: https://docs.docker.com/registry/spec/auth/token/
AccessToken string
}
oras-go-2.5.0/registry/remote/auth/example_test.go 0000664 0000000 0000000 00000020272 14576745303 0022233 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 auth_test includes the testable examples for the http client.
package auth_test
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
. "oras.land/oras-go/v2/registry/internal/doc"
"oras.land/oras-go/v2/registry/remote/auth"
)
const (
username = "test_user"
password = "test_password"
accessToken = "test/access/token"
refreshToken = "test/refresh/token"
_ = ExampleUnplayable
)
var (
host string
expectedHostAddress string
targetURL string
clientConfigTargetURL string
basicAuthTargetURL string
accessTokenTargetURL string
refreshTokenTargetURL string
tokenScopes = []string{
"repository:dst:pull,push",
"repository:src:pull",
}
)
func TestMain(m *testing.M) {
// create an authorization server
as := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet && r.Method != http.MethodPost {
w.WriteHeader(http.StatusUnauthorized)
panic("unexecuted attempt of authorization service")
}
if err := r.ParseForm(); err != nil {
w.WriteHeader(http.StatusUnauthorized)
panic("failed to parse form")
}
if got := r.PostForm.Get("service"); got != host {
w.WriteHeader(http.StatusUnauthorized)
}
// handles refresh token requests
if got := r.PostForm.Get("grant_type"); got != "refresh_token" {
w.WriteHeader(http.StatusUnauthorized)
}
scope := strings.Join(tokenScopes, " ")
if got := r.PostForm.Get("scope"); got != scope {
w.WriteHeader(http.StatusUnauthorized)
}
if got := r.PostForm.Get("refresh_token"); got != refreshToken {
w.WriteHeader(http.StatusUnauthorized)
}
// writes back access token
if _, err := fmt.Fprintf(w, `{"access_token":%q}`, accessToken); err != nil {
panic(err)
}
}))
defer as.Close()
// create a test server
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusNotFound)
panic("unexpected access")
}
switch path {
case "/basicAuth":
wantedAuthHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password))
authHeader := r.Header.Get("Authorization")
if authHeader != wantedAuthHeader {
w.Header().Set("Www-Authenticate", `Basic realm="Test Server"`)
w.WriteHeader(http.StatusUnauthorized)
}
case "/clientConfig":
wantedAuthHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password))
authHeader := r.Header.Get("Authorization")
if authHeader != wantedAuthHeader {
w.Header().Set("Www-Authenticate", `Basic realm="Test Server"`)
w.WriteHeader(http.StatusUnauthorized)
}
case "/accessToken":
wantedAuthHeader := "Bearer " + accessToken
if auth := r.Header.Get("Authorization"); auth != wantedAuthHeader {
challenge := fmt.Sprintf("Bearer realm=%q,service=%q,scope=%q", as.URL, host, strings.Join(tokenScopes, " "))
w.Header().Set("Www-Authenticate", challenge)
w.WriteHeader(http.StatusUnauthorized)
}
case "/refreshToken":
wantedAuthHeader := "Bearer " + accessToken
if auth := r.Header.Get("Authorization"); auth != wantedAuthHeader {
challenge := fmt.Sprintf("Bearer realm=%q,service=%q,scope=%q", as.URL, host, strings.Join(tokenScopes, " "))
w.Header().Set("Www-Authenticate", challenge)
w.WriteHeader(http.StatusUnauthorized)
}
case "/simple":
w.WriteHeader(http.StatusOK)
default:
w.WriteHeader(http.StatusNotAcceptable)
}
}))
defer ts.Close()
host = ts.URL
uri, _ := url.Parse(host)
expectedHostAddress = uri.Host
targetURL = fmt.Sprintf("%s/simple", host)
basicAuthTargetURL = fmt.Sprintf("%s/basicAuth", host)
clientConfigTargetURL = fmt.Sprintf("%s/clientConfig", host)
accessTokenTargetURL = fmt.Sprintf("%s/accessToken", host)
refreshTokenTargetURL = fmt.Sprintf("%s/refreshToken", host)
http.DefaultClient = ts.Client()
os.Exit(m.Run())
}
// ExampleClient_Do_minimalClient gives an example of a minimal working client.
func ExampleClient_Do_minimalClient() {
var client auth.Client
// targetURL can be any URL. For example, https://registry.wabbit-networks.io/v2/
req, err := http.NewRequest(http.MethodGet, targetURL, nil)
if err != nil {
panic(err)
}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
fmt.Println(resp.StatusCode)
// Output:
// 200
}
// ExampleClient_Do_basicAuth gives an example of using client with credentials.
func ExampleClient_Do_basicAuth() {
client := &auth.Client{
// expectedHostAddress is of form ipaddr:port
Credential: auth.StaticCredential(expectedHostAddress, auth.Credential{
Username: username,
Password: password,
}),
}
// basicAuthTargetURL can be any URL. For example, https://registry.wabbit-networks.io/v2/
req, err := http.NewRequest(http.MethodGet, basicAuthTargetURL, nil)
if err != nil {
panic(err)
}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
fmt.Println(resp.StatusCode)
// Output:
// 200
}
// ExampleClient_Do_clientConfigurations shows the client configurations available,
// including using cache, setting user agent and configuring OAuth2.
func ExampleClient_Do_clientConfigurations() {
client := &auth.Client{
// expectedHostAddress is of form ipaddr:port
Credential: auth.StaticCredential(expectedHostAddress, auth.Credential{
Username: username,
Password: password,
}),
// ForceAttemptOAuth2 controls whether to follow OAuth2 with password grant.
ForceAttemptOAuth2: true,
// Cache caches credentials for accessing the remote registry.
Cache: auth.NewCache(),
}
// SetUserAgent sets the user agent for all out-going requests.
client.SetUserAgent("example user agent")
// Tokens carry restrictions about what resources they can access and how.
// Such restrictions are represented and enforced as Scopes.
// Reference: https://docs.docker.com/registry/spec/auth/scope/
scopes := []string{
"repository:dst:pull,push",
"repository:src:pull",
}
// WithScopes returns a context with scopes added.
ctx := auth.WithScopes(context.Background(), scopes...)
// clientConfigTargetURL can be any URL. For example, https://registry.wabbit-networks.io/v2/
req, err := http.NewRequestWithContext(ctx, http.MethodGet, clientConfigTargetURL, nil)
if err != nil {
panic(err)
}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
fmt.Println(resp.StatusCode)
// Output:
// 200
}
// ExampleClient_Do_withAccessToken gives an example of using client with an access token.
func ExampleClient_Do_withAccessToken() {
client := &auth.Client{
// expectedHostAddress is of form ipaddr:port
Credential: auth.StaticCredential(expectedHostAddress, auth.Credential{
AccessToken: accessToken,
}),
}
// accessTokenTargetURL can be any URL. For example, https://registry.wabbit-networks.io/v2/
req, err := http.NewRequest(http.MethodGet, accessTokenTargetURL, nil)
if err != nil {
panic(err)
}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
fmt.Println(resp.StatusCode)
// Output:
// 200
}
// ExampleClient_Do_withRefreshToken gives an example of using client with a refresh token.
func ExampleClient_Do_withRefreshToken() {
client := &auth.Client{
// expectedHostAddress is of form ipaddr:port
Credential: auth.StaticCredential(expectedHostAddress, auth.Credential{
RefreshToken: refreshToken,
}),
}
// refreshTokenTargetURL can be any URL. For example, https://registry.wabbit-networks.io/v2/
req, err := http.NewRequest(http.MethodGet, refreshTokenTargetURL, nil)
if err != nil {
panic(err)
}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
fmt.Println(resp.StatusCode)
// Output:
// 200
}
oras-go-2.5.0/registry/remote/auth/scope.go 0000664 0000000 0000000 00000024337 14576745303 0020660 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 auth
import (
"context"
"slices"
"strings"
"oras.land/oras-go/v2/registry"
)
// Actions used in scopes.
// Reference: https://docs.docker.com/registry/spec/auth/scope/
const (
// ActionPull represents generic read access for resources of the repository
// type.
ActionPull = "pull"
// ActionPush represents generic write access for resources of the
// repository type.
ActionPush = "push"
// ActionDelete represents the delete permission for resources of the
// repository type.
ActionDelete = "delete"
)
// ScopeRegistryCatalog is the scope for registry catalog access.
const ScopeRegistryCatalog = "registry:catalog:*"
// ScopeRepository returns a repository scope with given actions.
// Reference: https://docs.docker.com/registry/spec/auth/scope/
func ScopeRepository(repository string, actions ...string) string {
actions = cleanActions(actions)
if repository == "" || len(actions) == 0 {
return ""
}
return strings.Join([]string{
"repository",
repository,
strings.Join(actions, ","),
}, ":")
}
// AppendRepositoryScope returns a new context containing scope hints for the
// auth client to fetch bearer tokens with the given actions on the repository.
// If called multiple times, the new scopes will be appended to the existing
// scopes. The resulted scopes are de-duplicated.
//
// For example, uploading blob to the repository "hello-world" does HEAD request
// first then POST and PUT. The HEAD request will return a challenge for scope
// `repository:hello-world:pull`, and the auth client will fetch a token for
// that challenge. Later, the POST request will return a challenge for scope
// `repository:hello-world:push`, and the auth client will fetch a token for
// that challenge again. By invoking AppendRepositoryScope with the actions
// [ActionPull] and [ActionPush] for the repository `hello-world`,
// the auth client with cache is hinted to fetch a token via a single token
// fetch request for all the HEAD, POST, PUT requests.
func AppendRepositoryScope(ctx context.Context, ref registry.Reference, actions ...string) context.Context {
if len(actions) == 0 {
return ctx
}
scope := ScopeRepository(ref.Repository, actions...)
return AppendScopesForHost(ctx, ref.Host(), scope)
}
// scopesContextKey is the context key for scopes.
type scopesContextKey struct{}
// WithScopes returns a context with scopes added. Scopes are de-duplicated.
// Scopes are used as hints for the auth client to fetch bearer tokens with
// larger scopes.
//
// For example, uploading blob to the repository "hello-world" does HEAD request
// first then POST and PUT. The HEAD request will return a challenge for scope
// `repository:hello-world:pull`, and the auth client will fetch a token for
// that challenge. Later, the POST request will return a challenge for scope
// `repository:hello-world:push`, and the auth client will fetch a token for
// that challenge again. By invoking WithScopes with the scope
// `repository:hello-world:pull,push`, the auth client with cache is hinted to
// fetch a token via a single token fetch request for all the HEAD, POST, PUT
// requests.
//
// Passing an empty list of scopes will virtually remove the scope hints in the
// context.
//
// Reference: https://docs.docker.com/registry/spec/auth/scope/
func WithScopes(ctx context.Context, scopes ...string) context.Context {
scopes = CleanScopes(scopes)
return context.WithValue(ctx, scopesContextKey{}, scopes)
}
// AppendScopes appends additional scopes to the existing scopes in the context
// and returns a new context. The resulted scopes are de-duplicated.
// The append operation does modify the existing scope in the context passed in.
func AppendScopes(ctx context.Context, scopes ...string) context.Context {
if len(scopes) == 0 {
return ctx
}
return WithScopes(ctx, append(GetScopes(ctx), scopes...)...)
}
// GetScopes returns the scopes in the context.
func GetScopes(ctx context.Context) []string {
if scopes, ok := ctx.Value(scopesContextKey{}).([]string); ok {
return slices.Clone(scopes)
}
return nil
}
// scopesForHostContextKey is the context key for per-host scopes.
type scopesForHostContextKey string
// WithScopesForHost returns a context with per-host scopes added.
// Scopes are de-duplicated.
// Scopes are used as hints for the auth client to fetch bearer tokens with
// larger scopes.
//
// For example, uploading blob to the repository "hello-world" does HEAD request
// first then POST and PUT. The HEAD request will return a challenge for scope
// `repository:hello-world:pull`, and the auth client will fetch a token for
// that challenge. Later, the POST request will return a challenge for scope
// `repository:hello-world:push`, and the auth client will fetch a token for
// that challenge again. By invoking WithScopesForHost with the scope
// `repository:hello-world:pull,push`, the auth client with cache is hinted to
// fetch a token via a single token fetch request for all the HEAD, POST, PUT
// requests.
//
// Passing an empty list of scopes will virtually remove the scope hints in the
// context for the given host.
//
// Reference: https://docs.docker.com/registry/spec/auth/scope/
func WithScopesForHost(ctx context.Context, host string, scopes ...string) context.Context {
scopes = CleanScopes(scopes)
return context.WithValue(ctx, scopesForHostContextKey(host), scopes)
}
// AppendScopesForHost appends additional scopes to the existing scopes
// in the context for the given host and returns a new context.
// The resulted scopes are de-duplicated.
// The append operation does modify the existing scope in the context passed in.
func AppendScopesForHost(ctx context.Context, host string, scopes ...string) context.Context {
if len(scopes) == 0 {
return ctx
}
oldScopes := GetScopesForHost(ctx, host)
return WithScopesForHost(ctx, host, append(oldScopes, scopes...)...)
}
// GetScopesForHost returns the scopes in the context for the given host,
// excluding global scopes added by [WithScopes] and [AppendScopes].
func GetScopesForHost(ctx context.Context, host string) []string {
if scopes, ok := ctx.Value(scopesForHostContextKey(host)).([]string); ok {
return slices.Clone(scopes)
}
return nil
}
// GetAllScopesForHost returns the scopes in the context for the given host,
// including global scopes added by [WithScopes] and [AppendScopes].
func GetAllScopesForHost(ctx context.Context, host string) []string {
scopes := GetScopesForHost(ctx, host)
globalScopes := GetScopes(ctx)
if len(scopes) == 0 {
return globalScopes
}
if len(globalScopes) == 0 {
return scopes
}
// re-clean the scopes
allScopes := append(scopes, globalScopes...)
return CleanScopes(allScopes)
}
// CleanScopes merges and sort the actions in ascending order if the scopes have
// the same resource type and name. The final scopes are sorted in ascending
// order. In other words, the scopes passed in are de-duplicated and sorted.
// Therefore, the output of this function is deterministic.
//
// If there is a wildcard `*` in the action, other actions in the same resource
// type and name are ignored.
func CleanScopes(scopes []string) []string {
// fast paths
switch len(scopes) {
case 0:
return nil
case 1:
scope := scopes[0]
i := strings.LastIndex(scope, ":")
if i == -1 {
return []string{scope}
}
actionList := strings.Split(scope[i+1:], ",")
actionList = cleanActions(actionList)
if len(actionList) == 0 {
return nil
}
actions := strings.Join(actionList, ",")
scope = scope[:i+1] + actions
return []string{scope}
}
// slow path
var result []string
// merge recognizable scopes
resourceTypes := make(map[string]map[string]map[string]struct{})
for _, scope := range scopes {
// extract resource type
i := strings.Index(scope, ":")
if i == -1 {
result = append(result, scope)
continue
}
resourceType := scope[:i]
// extract resource name and actions
rest := scope[i+1:]
i = strings.LastIndex(rest, ":")
if i == -1 {
result = append(result, scope)
continue
}
resourceName := rest[:i]
actions := rest[i+1:]
if actions == "" {
// drop scope since no action found
continue
}
// add to the intermediate map for de-duplication
namedActions := resourceTypes[resourceType]
if namedActions == nil {
namedActions = make(map[string]map[string]struct{})
resourceTypes[resourceType] = namedActions
}
actionSet := namedActions[resourceName]
if actionSet == nil {
actionSet = make(map[string]struct{})
namedActions[resourceName] = actionSet
}
for _, action := range strings.Split(actions, ",") {
if action != "" {
actionSet[action] = struct{}{}
}
}
}
// reconstruct scopes
for resourceType, namedActions := range resourceTypes {
for resourceName, actionSet := range namedActions {
if len(actionSet) == 0 {
continue
}
var actions []string
for action := range actionSet {
if action == "*" {
actions = []string{"*"}
break
}
actions = append(actions, action)
}
slices.Sort(actions)
scope := resourceType + ":" + resourceName + ":" + strings.Join(actions, ",")
result = append(result, scope)
}
}
// sort and return
slices.Sort(result)
return result
}
// cleanActions removes the duplicated actions and sort in ascending order.
// If there is a wildcard `*` in the action, other actions are ignored.
func cleanActions(actions []string) []string {
// fast paths
switch len(actions) {
case 0:
return nil
case 1:
if actions[0] == "" {
return nil
}
return actions
}
// slow path
slices.Sort(actions)
n := 0
for i := 0; i < len(actions); i++ {
if actions[i] == "*" {
return []string{"*"}
}
if actions[i] != actions[n] {
n++
if n != i {
actions[n] = actions[i]
}
}
}
n++
if actions[0] == "" {
if n == 1 {
return nil
}
return actions[1:n]
}
return actions[:n]
}
oras-go-2.5.0/registry/remote/auth/scope_test.go 0000664 0000000 0000000 00000041152 14576745303 0021711 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 auth
import (
"context"
"reflect"
"testing"
"oras.land/oras-go/v2/registry"
)
func TestScopeRepository(t *testing.T) {
tests := []struct {
name string
repository string
actions []string
want string
}{
{
name: "empty repository",
actions: []string{
"pull",
},
},
{
name: "nil actions",
repository: "foo",
},
{
name: "empty actions",
repository: "foo",
actions: []string{},
},
{
name: "empty actions list",
repository: "foo",
actions: []string{},
},
{
name: "empty actions",
repository: "foo",
actions: []string{
"",
},
},
{
name: "single action",
repository: "foo",
actions: []string{
"pull",
},
want: "repository:foo:pull",
},
{
name: "multiple actions",
repository: "foo",
actions: []string{
"pull",
"push",
},
want: "repository:foo:pull,push",
},
{
name: "unordered actions",
repository: "foo",
actions: []string{
"push",
"pull",
},
want: "repository:foo:pull,push",
},
{
name: "duplicated actions",
repository: "foo",
actions: []string{
"push",
"pull",
"pull",
"delete",
"push",
},
want: "repository:foo:delete,pull,push",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ScopeRepository(tt.repository, tt.actions...); got != tt.want {
t.Errorf("ScopeRepository() = %v, want %v", got, tt.want)
}
})
}
}
func TestWithScopeHints(t *testing.T) {
ctx := context.Background()
ref1, err := registry.ParseReference("registry.example.com/foo")
if err != nil {
t.Fatal("registry.ParseReference() error =", err)
}
ref2, err := registry.ParseReference("docker.io/foo")
if err != nil {
t.Fatal("registry.ParseReference() error =", err)
}
// with single scope
want1 := []string{
"repository:foo:pull",
}
want2 := []string{
"repository:foo:push",
}
ctx = AppendRepositoryScope(ctx, ref1, ActionPull)
ctx = AppendRepositoryScope(ctx, ref2, ActionPush)
if got := GetScopesForHost(ctx, ref1.Host()); !reflect.DeepEqual(got, want1) {
t.Errorf("GetScopesPerRegistry(WithScopeHints()) = %v, want %v", got, want1)
}
if got := GetScopesForHost(ctx, ref2.Host()); !reflect.DeepEqual(got, want2) {
t.Errorf("GetScopesPerRegistry(WithScopeHints()) = %v, want %v", got, want2)
}
// with duplicated scopes
scopes1 := []string{
ActionDelete,
ActionDelete,
ActionPull,
}
want1 = []string{
"repository:foo:delete,pull",
}
scopes2 := []string{
ActionPush,
ActionPush,
ActionDelete,
}
want2 = []string{
"repository:foo:delete,push",
}
ctx = AppendRepositoryScope(ctx, ref1, scopes1...)
ctx = AppendRepositoryScope(ctx, ref2, scopes2...)
if got := GetScopesForHost(ctx, ref1.Host()); !reflect.DeepEqual(got, want1) {
t.Errorf("GetScopesPerRegistry(WithScopeHints()) = %v, want %v", got, want1)
}
if got := GetScopesForHost(ctx, ref2.Host()); !reflect.DeepEqual(got, want2) {
t.Errorf("GetScopesPerRegistry(WithScopeHints()) = %v, want %v", got, want2)
}
// append empty scopes
ctx = AppendRepositoryScope(ctx, ref1)
ctx = AppendRepositoryScope(ctx, ref2)
if got := GetScopesForHost(ctx, ref1.Host()); !reflect.DeepEqual(got, want1) {
t.Errorf("GetScopesPerRegistry(WithScopeHints()) = %v, want %v", got, want1)
}
if got := GetScopesForHost(ctx, ref2.Host()); !reflect.DeepEqual(got, want2) {
t.Errorf("GetScopesPerRegistry(WithScopeHints()) = %v, want %v", got, want2)
}
}
func TestWithScopes(t *testing.T) {
ctx := context.Background()
// with single scope
want := []string{
"repository:foo:pull",
}
ctx = WithScopes(ctx, want...)
if got := GetScopes(ctx); !reflect.DeepEqual(got, want) {
t.Errorf("GetScopes(WithScopes()) = %v, want %v", got, want)
}
// overwrite scopes
want = []string{
"repository:bar:push",
}
ctx = WithScopes(ctx, want...)
if got := GetScopes(ctx); !reflect.DeepEqual(got, want) {
t.Errorf("GetScopes(WithScopes()) = %v, want %v", got, want)
}
// overwrite scopes with de-duplication
scopes := []string{
"repository:hello-world:push",
"repository:alpine:delete",
"repository:hello-world:pull",
"repository:alpine:delete",
}
want = []string{
"repository:alpine:delete",
"repository:hello-world:pull,push",
}
ctx = WithScopes(ctx, scopes...)
if got := GetScopes(ctx); !reflect.DeepEqual(got, want) {
t.Errorf("GetScopes(WithScopes()) = %v, want %v", got, want)
}
// clean scopes
want = nil
ctx = WithScopes(ctx, want...)
if got := GetScopes(ctx); !reflect.DeepEqual(got, want) {
t.Errorf("GetScopes(WithScopes()) = %v, want %v", got, want)
}
}
func TestAppendScopes(t *testing.T) {
ctx := context.Background()
// append single scope
want := []string{
"repository:foo:pull",
}
ctx = AppendScopes(ctx, want...)
if got := GetScopes(ctx); !reflect.DeepEqual(got, want) {
t.Errorf("GetScopes(AppendScopes()) = %v, want %v", got, want)
}
// append scopes with de-duplication
scopes := []string{
"repository:hello-world:push",
"repository:alpine:delete",
"repository:hello-world:pull",
"repository:alpine:delete",
}
want = []string{
"repository:alpine:delete",
"repository:foo:pull",
"repository:hello-world:pull,push",
}
ctx = AppendScopes(ctx, scopes...)
if got := GetScopes(ctx); !reflect.DeepEqual(got, want) {
t.Errorf("GetScopes(AppendScopes()) = %v, want %v", got, want)
}
// append empty scopes
ctx = AppendScopes(ctx)
if got := GetScopes(ctx); !reflect.DeepEqual(got, want) {
t.Errorf("GetScopes(AppendScopes()) = %v, want %v", got, want)
}
}
func TestWithScopesPerHost(t *testing.T) {
ctx := context.Background()
reg1 := "registry1.example.com"
reg2 := "registry2.example.com"
// with single scope
want1 := []string{
"repository:foo:pull",
}
want2 := []string{
"repository:foo:push",
}
ctx = WithScopesForHost(ctx, reg1, want1...)
ctx = WithScopesForHost(ctx, reg2, want2...)
if got := GetScopesForHost(ctx, reg1); !reflect.DeepEqual(got, want1) {
t.Errorf("GetScopesPerRegistry(WithScopesPerRegistry()) = %v, want %v", got, want1)
}
if got := GetScopesForHost(ctx, reg2); !reflect.DeepEqual(got, want2) {
t.Errorf("GetScopesPerRegistry(WithScopesPerRegistry()) = %v, want %v", got, want2)
}
// overwrite scopes
want1 = []string{
"repository:bar:push",
}
want2 = []string{
"repository:bar:pull",
}
ctx = WithScopesForHost(ctx, reg1, want1...)
ctx = WithScopesForHost(ctx, reg2, want2...)
if got := GetScopesForHost(ctx, reg1); !reflect.DeepEqual(got, want1) {
t.Errorf("GetScopesPerRegistry(WithScopesPerRegistry()) = %v, want %v", got, want1)
}
if got := GetScopesForHost(ctx, reg2); !reflect.DeepEqual(got, want2) {
t.Errorf("GetScopesPerRegistry(WithScopesPerRegistry()) = %v, want %v", got, want2)
}
// overwrite scopes with de-duplication
scopes1 := []string{
"repository:hello-world:push",
"repository:alpine:delete",
"repository:hello-world:pull",
"repository:alpine:delete",
}
want1 = []string{
"repository:alpine:delete",
"repository:hello-world:pull,push",
}
scopes2 := []string{
"repository:goodbye-world:push",
"repository:nginx:delete",
"repository:goodbye-world:pull",
"repository:nginx:delete",
}
want2 = []string{
"repository:goodbye-world:pull,push",
"repository:nginx:delete",
}
ctx = WithScopesForHost(ctx, reg1, scopes1...)
ctx = WithScopesForHost(ctx, reg2, scopes2...)
if got := GetScopesForHost(ctx, reg1); !reflect.DeepEqual(got, want1) {
t.Errorf("GetScopesPerRegistry(WithScopesPerRegistry()) = %v, want %v", got, want1)
}
if got := GetScopesForHost(ctx, reg2); !reflect.DeepEqual(got, want2) {
t.Errorf("GetScopesPerRegistry(WithScopesPerRegistry()) = %v, want %v", got, want2)
}
// clean scopes
var want []string
ctx = WithScopesForHost(ctx, reg1, want...)
ctx = WithScopesForHost(ctx, reg2, want...)
if got := GetScopesForHost(ctx, reg1); !reflect.DeepEqual(got, want) {
t.Errorf("GetScopesPerRegistry(WithScopesPerRegistry()) = %v, want %v", got, want)
}
if got := GetScopesForHost(ctx, reg2); !reflect.DeepEqual(got, want) {
t.Errorf("GetScopesPerRegistry(WithScopesPerRegistry()) = %v, want %v", got, want)
}
}
func TestAppendScopesPerHost(t *testing.T) {
ctx := context.Background()
reg1 := "registry1.example.com"
reg2 := "registry2.example.com"
// with single scope
want1 := []string{
"repository:foo:pull",
}
want2 := []string{
"repository:foo:push",
}
ctx = AppendScopesForHost(ctx, reg1, want1...)
ctx = AppendScopesForHost(ctx, reg2, want2...)
if got := GetScopesForHost(ctx, reg1); !reflect.DeepEqual(got, want1) {
t.Errorf("GetScopesPerRegistry(AppendScopesPerRegistry()) = %v, want %v", got, want1)
}
if got := GetScopesForHost(ctx, reg2); !reflect.DeepEqual(got, want2) {
t.Errorf("GetScopesPerRegistry(AppendScopesPerRegistry()) = %v, want %v", got, want2)
}
// append scopes with de-duplication
scopes1 := []string{
"repository:hello-world:push",
"repository:alpine:delete",
"repository:hello-world:pull",
"repository:alpine:delete",
}
want1 = []string{
"repository:alpine:delete",
"repository:foo:pull",
"repository:hello-world:pull,push",
}
scopes2 := []string{
"repository:goodbye-world:push",
"repository:nginx:delete",
"repository:goodbye-world:pull",
"repository:nginx:delete",
}
want2 = []string{
"repository:foo:push",
"repository:goodbye-world:pull,push",
"repository:nginx:delete",
}
ctx = AppendScopesForHost(ctx, reg1, scopes1...)
ctx = AppendScopesForHost(ctx, reg2, scopes2...)
if got := GetScopesForHost(ctx, reg1); !reflect.DeepEqual(got, want1) {
t.Errorf("GetScopesPerRegistry(AppendScopesPerRegistry()) = %v, want %v", got, want1)
}
if got := GetScopesForHost(ctx, reg2); !reflect.DeepEqual(got, want2) {
t.Errorf("GetScopesPerRegistry(AppendScopesPerRegistry()) = %v, want %v", got, want2)
}
// append empty scopes
ctx = AppendScopesForHost(ctx, reg1)
ctx = AppendScopesForHost(ctx, reg2)
if got := GetScopesForHost(ctx, reg1); !reflect.DeepEqual(got, want1) {
t.Errorf("GetScopesPerRegistry(AppendScopesPerRegistry()) = %v, want %v", got, want1)
}
if got := GetScopesForHost(ctx, reg2); !reflect.DeepEqual(got, want2) {
t.Errorf("GetScopesPerRegistry(AppendScopesPerRegistry()) = %v, want %v", got, want2)
}
}
func TestCleanScopes(t *testing.T) {
tests := []struct {
name string
scopes []string
want []string
}{
{
name: "nil scope",
},
{
name: "empty scope",
scopes: []string{},
},
{
name: "single scope",
scopes: []string{
"repository:foo:pull",
},
want: []string{
"repository:foo:pull",
},
},
{
name: "single scope with unordered actions",
scopes: []string{
"repository:foo:push,pull,delete",
},
want: []string{
"repository:foo:delete,pull,push",
},
},
{
name: "single scope with duplicated actions",
scopes: []string{
"repository:foo:push,pull,push,pull,push,push,pull",
},
want: []string{
"repository:foo:pull,push",
},
},
{
name: "single scope with wild cards",
scopes: []string{
"repository:foo:pull,*,push",
},
want: []string{
"repository:foo:*",
},
},
{
name: "single scope with no actions",
scopes: []string{
"repository:foo:,",
},
want: nil,
},
{
name: "multiple scopes",
scopes: []string{
"repository:bar:push",
"repository:foo:pull",
},
want: []string{
"repository:bar:push",
"repository:foo:pull",
},
},
{
name: "multiple unordered scopes",
scopes: []string{
"repository:foo:pull",
"repository:bar:push",
},
want: []string{
"repository:bar:push",
"repository:foo:pull",
},
},
{
name: "multiple scopes with duplicates",
scopes: []string{
"repository:foo:pull",
"repository:bar:push",
"repository:foo:push",
"repository:bar:push,delete,pull",
"repository:bar:delete,pull",
"repository:foo:pull",
"registry:catalog:*",
"registry:catalog:pull",
},
want: []string{
"registry:catalog:*",
"repository:bar:delete,pull,push",
"repository:foo:pull,push",
},
},
{
name: "multiple scopes with no actions",
scopes: []string{
"repository:foo:,",
"repository:bar:,",
},
want: nil,
},
{
name: "single unknown or invalid scope",
scopes: []string{
"unknown",
},
want: []string{
"unknown",
},
},
{
name: "multiple unknown or invalid scopes",
scopes: []string{
"repository:foo:pull",
"unknown",
"invalid:scope",
"no:actions:",
"repository:foo:push",
},
want: []string{
"invalid:scope",
"repository:foo:pull,push",
"unknown",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := CleanScopes(tt.scopes); !reflect.DeepEqual(got, tt.want) {
t.Errorf("CleanScopes() = %v, want %v", got, tt.want)
}
})
}
}
func Test_cleanActions(t *testing.T) {
tests := []struct {
name string
actions []string
want []string
}{
{
name: "nil action",
},
{
name: "empty action",
actions: []string{},
},
{
name: "single action",
actions: []string{
"pull",
},
want: []string{
"pull",
},
},
{
name: "single empty action",
actions: []string{
"",
},
},
{
name: "multiple actions",
actions: []string{
"pull",
"push",
},
want: []string{
"pull",
"push",
},
},
{
name: "multiple actions with empty action",
actions: []string{
"pull",
"",
"push",
},
want: []string{
"pull",
"push",
},
},
{
name: "multiple actions with all empty action",
actions: []string{
"",
"",
"",
},
want: nil,
},
{
name: "unordered actions",
actions: []string{
"push",
"pull",
"delete",
},
want: []string{
"delete",
"pull",
"push",
},
},
{
name: "wildcard",
actions: []string{
"*",
},
want: []string{
"*",
},
},
{
name: "wildcard at the begining",
actions: []string{
"*",
"push",
"pull",
"delete",
},
want: []string{
"*",
},
},
{
name: "wildcard in the middle",
actions: []string{
"push",
"pull",
"*",
"delete",
},
want: []string{
"*",
},
},
{
name: "wildcard at the end",
actions: []string{
"push",
"pull",
"delete",
"*",
},
want: []string{
"*",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := cleanActions(tt.actions); !reflect.DeepEqual(got, tt.want) {
t.Errorf("cleanActions() = %v, want %v", got, tt.want)
}
})
}
}
func Test_getAllScopesForHost(t *testing.T) {
host := "registry.example.com"
tests := []struct {
name string
scopes []string
globalScopes []string
want []string
}{
{
name: "Empty per-host scopes",
scopes: []string{},
globalScopes: []string{
"repository:hello-world:push",
"repository:alpine:delete",
"repository:hello-world:pull",
"repository:alpine:delete",
},
want: []string{
"repository:alpine:delete",
"repository:hello-world:pull,push",
},
},
{
name: "Empty global scopes",
scopes: []string{
"repository:hello-world:push",
"repository:alpine:delete",
"repository:hello-world:pull",
"repository:alpine:delete",
},
globalScopes: []string{},
want: []string{
"repository:alpine:delete",
"repository:hello-world:pull,push",
},
},
{
name: "Per-host scopes + global scopes",
scopes: []string{
"repository:hello-world:push",
"repository:alpine:delete",
"repository:hello-world:pull",
"repository:alpine:delete",
},
globalScopes: []string{
"repository:foo:pull",
"repository:hello-world:pull",
"repository:alpine:pull",
},
want: []string{
"repository:alpine:delete,pull",
"repository:foo:pull",
"repository:hello-world:pull,push",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
ctx = WithScopesForHost(ctx, host, tt.scopes...)
ctx = WithScopes(ctx, tt.globalScopes...)
if got := GetAllScopesForHost(ctx, host); !reflect.DeepEqual(got, tt.want) {
t.Errorf("getAllScopesForHost() = %v, want %v", got, tt.want)
}
})
}
}
oras-go-2.5.0/registry/remote/credentials/ 0000775 0000000 0000000 00000000000 14576745303 0020543 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/registry/remote/credentials/example_test.go 0000664 0000000 0000000 00000013027 14576745303 0023567 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 credentials_test
import (
"context"
"fmt"
"net/http"
"oras.land/oras-go/v2/registry/remote"
"oras.land/oras-go/v2/registry/remote/auth"
credentials "oras.land/oras-go/v2/registry/remote/credentials"
)
func ExampleNewNativeStore() {
ns := credentials.NewNativeStore("pass")
ctx := context.Background()
// save credentials into the store
err := ns.Put(ctx, "localhost:5000", auth.Credential{
Username: "username-example",
Password: "password-example",
})
if err != nil {
panic(err)
}
// get credentials from the store
cred, err := ns.Get(ctx, "localhost:5000")
if err != nil {
panic(err)
}
fmt.Println(cred)
// delete the credentials from the store
err = ns.Delete(ctx, "localhost:5000")
if err != nil {
panic(err)
}
}
func ExampleNewFileStore() {
fs, err := credentials.NewFileStore("example/path/config.json")
if err != nil {
panic(err)
}
ctx := context.Background()
// save credentials into the store
err = fs.Put(ctx, "localhost:5000", auth.Credential{
Username: "username-example",
Password: "password-example",
})
if err != nil {
panic(err)
}
// get credentials from the store
cred, err := fs.Get(ctx, "localhost:5000")
if err != nil {
panic(err)
}
fmt.Println(cred)
// delete the credentials from the store
err = fs.Delete(ctx, "localhost:5000")
if err != nil {
panic(err)
}
}
func ExampleNewStore() {
// NewStore returns a Store based on the given configuration file. It will
// automatically determine which Store (file store or native store) to use.
// If the native store is not available, you can save your credentials in
// the configuration file by specifying AllowPlaintextPut: true, but keep
// in mind that this is an unsafe workaround.
// See the documentation for details.
store, err := credentials.NewStore("example/path/config.json", credentials.StoreOptions{
AllowPlaintextPut: true,
})
if err != nil {
panic(err)
}
ctx := context.Background()
// save credentials into the store
err = store.Put(ctx, "localhost:5000", auth.Credential{
Username: "username-example",
Password: "password-example",
})
if err != nil {
panic(err)
}
// get credentials from the store
cred, err := store.Get(ctx, "localhost:5000")
if err != nil {
panic(err)
}
fmt.Println(cred)
// delete the credentials from the store
err = store.Delete(ctx, "localhost:5000")
if err != nil {
panic(err)
}
}
func ExampleNewStoreFromDocker() {
ds, err := credentials.NewStoreFromDocker(credentials.StoreOptions{
AllowPlaintextPut: true,
})
if err != nil {
panic(err)
}
ctx := context.Background()
// save credentials into the store
err = ds.Put(ctx, "localhost:5000", auth.Credential{
Username: "username-example",
Password: "password-example",
})
if err != nil {
panic(err)
}
// get credentials from the store
cred, err := ds.Get(ctx, "localhost:5000")
if err != nil {
panic(err)
}
fmt.Println(cred)
// delete the credentials from the store
err = ds.Delete(ctx, "localhost:5000")
if err != nil {
panic(err)
}
}
func ExampleNewStoreWithFallbacks_configAsPrimaryStoreDockerAsFallback() {
primaryStore, err := credentials.NewStore("example/path/config.json", credentials.StoreOptions{
AllowPlaintextPut: true,
})
if err != nil {
panic(err)
}
fallbackStore, err := credentials.NewStoreFromDocker(credentials.StoreOptions{})
sf := credentials.NewStoreWithFallbacks(primaryStore, fallbackStore)
ctx := context.Background()
// save credentials into the store
err = sf.Put(ctx, "localhost:5000", auth.Credential{
Username: "username-example",
Password: "password-example",
})
if err != nil {
panic(err)
}
// get credentials from the store
cred, err := sf.Get(ctx, "localhost:5000")
if err != nil {
panic(err)
}
fmt.Println(cred)
// delete the credentials from the store
err = sf.Delete(ctx, "localhost:5000")
if err != nil {
panic(err)
}
}
func ExampleLogin() {
store, err := credentials.NewStore("example/path/config.json", credentials.StoreOptions{
AllowPlaintextPut: true,
})
if err != nil {
panic(err)
}
registry, err := remote.NewRegistry("localhost:5000")
if err != nil {
panic(err)
}
cred := auth.Credential{
Username: "username-example",
Password: "password-example",
}
err = credentials.Login(context.Background(), store, registry, cred)
if err != nil {
panic(err)
}
fmt.Println("Login succeeded")
}
func ExampleLogout() {
store, err := credentials.NewStore("example/path/config.json", credentials.StoreOptions{})
if err != nil {
panic(err)
}
err = credentials.Logout(context.Background(), store, "localhost:5000")
if err != nil {
panic(err)
}
fmt.Println("Logout succeeded")
}
func ExampleCredential() {
store, err := credentials.NewStore("example/path/config.json", credentials.StoreOptions{})
if err != nil {
panic(err)
}
client := auth.DefaultClient
client.Credential = credentials.Credential(store)
request, err := http.NewRequest(http.MethodGet, "localhost:5000", nil)
if err != nil {
panic(err)
}
_, err = client.Do(request)
if err != nil {
panic(err)
}
}
oras-go-2.5.0/registry/remote/credentials/file_store.go 0000664 0000000 0000000 00000006371 14576745303 0023234 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 credentials
import (
"context"
"errors"
"fmt"
"strings"
"oras.land/oras-go/v2/registry/remote/auth"
"oras.land/oras-go/v2/registry/remote/credentials/internal/config"
)
// FileStore implements a credentials store using the docker configuration file
// to keep the credentials in plain-text.
//
// Reference: https://docs.docker.com/engine/reference/commandline/cli/#docker-cli-configuration-file-configjson-properties
type FileStore struct {
// DisablePut disables putting credentials in plaintext.
// If DisablePut is set to true, Put() will return ErrPlaintextPutDisabled.
DisablePut bool
config *config.Config
}
var (
// ErrPlaintextPutDisabled is returned by Put() when DisablePut is set
// to true.
ErrPlaintextPutDisabled = errors.New("putting plaintext credentials is disabled")
// ErrBadCredentialFormat is returned by Put() when the credential format
// is bad.
ErrBadCredentialFormat = errors.New("bad credential format")
)
// NewFileStore creates a new file credentials store.
//
// Reference: https://docs.docker.com/engine/reference/commandline/cli/#docker-cli-configuration-file-configjson-properties
func NewFileStore(configPath string) (*FileStore, error) {
cfg, err := config.Load(configPath)
if err != nil {
return nil, err
}
return newFileStore(cfg), nil
}
// newFileStore creates a file credentials store based on the given config instance.
func newFileStore(cfg *config.Config) *FileStore {
return &FileStore{config: cfg}
}
// Get retrieves credentials from the store for the given server address.
func (fs *FileStore) Get(_ context.Context, serverAddress string) (auth.Credential, error) {
return fs.config.GetCredential(serverAddress)
}
// Put saves credentials into the store for the given server address.
// Returns ErrPlaintextPutDisabled if fs.DisablePut is set to true.
func (fs *FileStore) Put(_ context.Context, serverAddress string, cred auth.Credential) error {
if fs.DisablePut {
return ErrPlaintextPutDisabled
}
if err := validateCredentialFormat(cred); err != nil {
return err
}
return fs.config.PutCredential(serverAddress, cred)
}
// Delete removes credentials from the store for the given server address.
func (fs *FileStore) Delete(_ context.Context, serverAddress string) error {
return fs.config.DeleteCredential(serverAddress)
}
// validateCredentialFormat validates the format of cred.
func validateCredentialFormat(cred auth.Credential) error {
if strings.ContainsRune(cred.Username, ':') {
// Username and password will be encoded in the base64(username:password)
// format in the file. The decoded result will be wrong if username
// contains colon(s).
return fmt.Errorf("%w: colons(:) are not allowed in username", ErrBadCredentialFormat)
}
return nil
}
oras-go-2.5.0/registry/remote/credentials/file_store_test.go 0000664 0000000 0000000 00000055227 14576745303 0024277 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 credentials
import (
"context"
"encoding/json"
"errors"
"os"
"path/filepath"
"reflect"
"testing"
"oras.land/oras-go/v2/registry/remote/auth"
"oras.land/oras-go/v2/registry/remote/credentials/internal/config/configtest"
)
func TestNewFileStore_badPath(t *testing.T) {
tempDir := t.TempDir()
tests := []struct {
name string
configPath string
wantErr bool
}{
{
name: "Path is a directory",
configPath: tempDir,
wantErr: true,
},
{
name: "Empty file name",
configPath: filepath.Join(tempDir, ""),
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := NewFileStore(tt.configPath)
if (err != nil) != tt.wantErr {
t.Errorf("NewFileStore() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
func TestNewFileStore_badFormat(t *testing.T) {
tests := []struct {
name string
configPath string
wantErr bool
}{
{
name: "Bad JSON format",
configPath: "testdata/bad_config",
wantErr: true,
},
{
name: "Invalid auths format",
configPath: "testdata/invalid_auths_config.json",
wantErr: true,
},
{
name: "No auths field",
configPath: "testdata/no_auths_config.json",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := NewFileStore(tt.configPath)
if (err != nil) != tt.wantErr {
t.Errorf("NewFileStore() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
func TestFileStore_Get_validConfig(t *testing.T) {
ctx := context.Background()
fs, err := NewFileStore("testdata/valid_auths_config.json")
if err != nil {
t.Fatal("NewFileStore() error =", err)
}
tests := []struct {
name string
serverAddress string
want auth.Credential
wantErr bool
}{
{
name: "Username and password",
serverAddress: "registry1.example.com",
want: auth.Credential{
Username: "username",
Password: "password",
},
},
{
name: "Identity token",
serverAddress: "registry2.example.com",
want: auth.Credential{
RefreshToken: "identity_token",
},
},
{
name: "Registry token",
serverAddress: "registry3.example.com",
want: auth.Credential{
AccessToken: "registry_token",
},
},
{
name: "Username and password, identity token and registry token",
serverAddress: "registry4.example.com",
want: auth.Credential{
Username: "username",
Password: "password",
RefreshToken: "identity_token",
AccessToken: "registry_token",
},
},
{
name: "Empty credential",
serverAddress: "registry5.example.com",
want: auth.EmptyCredential,
},
{
name: "Username and password, no auth",
serverAddress: "registry6.example.com",
want: auth.Credential{
Username: "username",
Password: "password",
},
},
{
name: "Auth overriding Username and password",
serverAddress: "registry7.example.com",
want: auth.Credential{
Username: "username",
Password: "password",
},
},
{
name: "Not in auths",
serverAddress: "foo.example.com",
want: auth.EmptyCredential,
},
{
name: "No record",
serverAddress: "registry999.example.com",
want: auth.EmptyCredential,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := fs.Get(ctx, tt.serverAddress)
if (err != nil) != tt.wantErr {
t.Errorf("FileStore.Get() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("FileStore.Get() = %v, want %v", got, tt.want)
}
})
}
}
func TestFileStore_Get_invalidConfig(t *testing.T) {
ctx := context.Background()
fs, err := NewFileStore("testdata/invalid_auths_entry_config.json")
if err != nil {
t.Fatal("NewFileStore() error =", err)
}
tests := []struct {
name string
serverAddress string
want auth.Credential
wantErr bool
}{
{
name: "Invalid auth encode",
serverAddress: "registry1.example.com",
want: auth.EmptyCredential,
wantErr: true,
},
{
name: "Invalid auths format",
serverAddress: "registry2.example.com",
want: auth.EmptyCredential,
wantErr: true,
},
{
name: "Invalid type",
serverAddress: "registry3.example.com",
want: auth.EmptyCredential,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := fs.Get(ctx, tt.serverAddress)
if (err != nil) != tt.wantErr {
t.Errorf("FileStore.Get() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("FileStore.Get() = %v, want %v", got, tt.want)
}
})
}
}
func TestFileStore_Get_emptyConfig(t *testing.T) {
ctx := context.Background()
fs, err := NewFileStore("testdata/empty_config.json")
if err != nil {
t.Fatal("NewFileStore() error =", err)
}
tests := []struct {
name string
serverAddress string
want auth.Credential
wantErr error
}{
{
name: "Not found",
serverAddress: "registry.example.com",
want: auth.EmptyCredential,
wantErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := fs.Get(ctx, tt.serverAddress)
if !errors.Is(err, tt.wantErr) {
t.Errorf("FileStore.Get() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("FileStore.Get() = %v, want %v", got, tt.want)
}
})
}
}
func TestFileStore_Get_notExistConfig(t *testing.T) {
ctx := context.Background()
fs, err := NewFileStore("whatever")
if err != nil {
t.Fatal("NewFileStore() error =", err)
}
tests := []struct {
name string
serverAddress string
want auth.Credential
wantErr error
}{
{
name: "Not found",
serverAddress: "registry.example.com",
want: auth.EmptyCredential,
wantErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := fs.Get(ctx, tt.serverAddress)
if !errors.Is(err, tt.wantErr) {
t.Errorf("FileStore.Get() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("FileStore.Get() = %v, want %v", got, tt.want)
}
})
}
}
func TestFileStore_Put_notExistConfig(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.json")
ctx := context.Background()
fs, err := NewFileStore(configPath)
if err != nil {
t.Fatal("NewFileStore() error =", err)
}
server := "test.example.com"
cred := auth.Credential{
Username: "username",
Password: "password",
RefreshToken: "refresh_token",
AccessToken: "access_token",
}
// test put
if err := fs.Put(ctx, server, cred); err != nil {
t.Fatalf("FileStore.Put() error = %v", err)
}
// verify config file
configFile, err := os.Open(configPath)
if err != nil {
t.Fatalf("failed to open config file: %v", err)
}
defer configFile.Close()
var cfg configtest.Config
if err := json.NewDecoder(configFile).Decode(&cfg); err != nil {
t.Fatalf("failed to decode config file: %v", err)
}
want := configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{
server: {
Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=",
IdentityToken: "refresh_token",
RegistryToken: "access_token",
},
},
}
if !reflect.DeepEqual(cfg, want) {
t.Errorf("Decoded config = %v, want %v", cfg, want)
}
// verify get
got, err := fs.Get(ctx, server)
if err != nil {
t.Fatalf("FileStore.Get() error = %v", err)
}
if want := cred; !reflect.DeepEqual(got, want) {
t.Errorf("FileStore.Get() = %v, want %v", got, want)
}
}
func TestFileStore_Put_addNew(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.json")
ctx := context.Background()
// prepare test content
server1 := "registry1.example.com"
cred1 := auth.Credential{
Username: "username",
Password: "password",
RefreshToken: "refresh_token",
AccessToken: "access_token",
}
cfg := configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{
server1: {
SomeAuthField: "whatever",
Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=",
IdentityToken: cred1.RefreshToken,
RegistryToken: cred1.AccessToken,
},
},
SomeConfigField: 123,
}
jsonStr, err := json.Marshal(cfg)
if err != nil {
t.Fatalf("failed to marshal config: %v", err)
}
if err := os.WriteFile(configPath, jsonStr, 0666); err != nil {
t.Fatalf("failed to write config file: %v", err)
}
// test put
fs, err := NewFileStore(configPath)
if err != nil {
t.Fatal("NewFileStore() error =", err)
}
server2 := "registry2.example.com"
cred2 := auth.Credential{
Username: "username_2",
Password: "password_2",
RefreshToken: "refresh_token_2",
AccessToken: "access_token_2",
}
if err := fs.Put(ctx, server2, cred2); err != nil {
t.Fatalf("FileStore.Put() error = %v", err)
}
// verify config file
configFile, err := os.Open(configPath)
if err != nil {
t.Fatalf("failed to open config file: %v", err)
}
defer configFile.Close()
var gotCfg configtest.Config
if err := json.NewDecoder(configFile).Decode(&gotCfg); err != nil {
t.Fatalf("failed to decode config file: %v", err)
}
wantCfg := configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{
server1: {
SomeAuthField: "whatever",
Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=",
IdentityToken: cred1.RefreshToken,
RegistryToken: cred1.AccessToken,
},
server2: {
Auth: "dXNlcm5hbWVfMjpwYXNzd29yZF8y",
IdentityToken: "refresh_token_2",
RegistryToken: "access_token_2",
},
},
SomeConfigField: cfg.SomeConfigField,
}
if !reflect.DeepEqual(gotCfg, wantCfg) {
t.Errorf("Decoded config = %v, want %v", gotCfg, wantCfg)
}
// verify get
got, err := fs.Get(ctx, server1)
if err != nil {
t.Fatalf("FileStore.Get() error = %v", err)
}
if want := cred1; !reflect.DeepEqual(got, want) {
t.Errorf("FileStore.Get(%s) = %v, want %v", server1, got, want)
}
got, err = fs.Get(ctx, server2)
if err != nil {
t.Fatalf("FileStore.Get() error = %v", err)
}
if want := cred2; !reflect.DeepEqual(got, want) {
t.Errorf("FileStore.Get(%s) = %v, want %v", server2, got, want)
}
}
func TestFileStore_Put_updateOld(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.json")
ctx := context.Background()
// prepare test content
server := "registry.example.com"
cfg := configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{
server: {
SomeAuthField: "whatever",
Username: "foo",
Password: "bar",
IdentityToken: "refresh_token",
},
},
SomeConfigField: 123,
}
jsonStr, err := json.Marshal(cfg)
if err != nil {
t.Fatalf("failed to marshal config: %v", err)
}
if err := os.WriteFile(configPath, jsonStr, 0666); err != nil {
t.Fatalf("failed to write config file: %v", err)
}
// test put
fs, err := NewFileStore(configPath)
if err != nil {
t.Fatal("NewFileStore() error =", err)
}
cred := auth.Credential{
Username: "username",
Password: "password",
AccessToken: "access_token",
}
if err := fs.Put(ctx, server, cred); err != nil {
t.Fatalf("FileStore.Put() error = %v", err)
}
// verify config file
configFile, err := os.Open(configPath)
if err != nil {
t.Fatalf("failed to open config file: %v", err)
}
defer configFile.Close()
var gotCfg configtest.Config
if err := json.NewDecoder(configFile).Decode(&gotCfg); err != nil {
t.Fatalf("failed to decode config file: %v", err)
}
wantCfg := configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{
server: {
Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=",
RegistryToken: "access_token",
},
},
SomeConfigField: cfg.SomeConfigField,
}
if !reflect.DeepEqual(gotCfg, wantCfg) {
t.Errorf("Decoded config = %v, want %v", gotCfg, wantCfg)
}
// verify get
got, err := fs.Get(ctx, server)
if err != nil {
t.Fatalf("FileStore.Get() error = %v", err)
}
if want := cred; !reflect.DeepEqual(got, want) {
t.Errorf("FileStore.Get(%s) = %v, want %v", server, got, want)
}
}
func TestFileStore_Put_disablePut(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.json")
ctx := context.Background()
fs, err := NewFileStore(configPath)
if err != nil {
t.Fatal("NewFileStore() error =", err)
}
fs.DisablePut = true
server := "test.example.com"
cred := auth.Credential{
Username: "username",
Password: "password",
RefreshToken: "refresh_token",
AccessToken: "access_token",
}
err = fs.Put(ctx, server, cred)
if wantErr := ErrPlaintextPutDisabled; !errors.Is(err, wantErr) {
t.Errorf("FileStore.Put() error = %v, wantErr %v", err, wantErr)
}
}
func TestFileStore_Put_usernameContainsColon(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.json")
ctx := context.Background()
fs, err := NewFileStore(configPath)
if err != nil {
t.Fatal("NewFileStore() error =", err)
}
serverAddr := "test.example.com"
cred := auth.Credential{
Username: "x:y",
Password: "z",
}
if err := fs.Put(ctx, serverAddr, cred); err == nil {
t.Fatal("FileStore.Put() error is nil, want", ErrBadCredentialFormat)
}
}
func TestFileStore_Put_passwordContainsColon(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.json")
ctx := context.Background()
fs, err := NewFileStore(configPath)
if err != nil {
t.Fatal("NewFileStore() error =", err)
}
serverAddr := "test.example.com"
cred := auth.Credential{
Username: "y",
Password: "y:z",
}
if err := fs.Put(ctx, serverAddr, cred); err != nil {
t.Fatal("FileStore.Put() error =", err)
}
got, err := fs.Get(ctx, serverAddr)
if err != nil {
t.Fatal("FileStore.Get() error =", err)
}
if !reflect.DeepEqual(got, cred) {
t.Errorf("FileStore.Get() = %v, want %v", got, cred)
}
}
func TestFileStore_Delete(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.json")
ctx := context.Background()
// prepare test content
server1 := "registry1.example.com"
cred1 := auth.Credential{
Username: "username",
Password: "password",
RefreshToken: "refresh_token",
AccessToken: "access_token",
}
server2 := "registry2.example.com"
cred2 := auth.Credential{
Username: "username_2",
Password: "password_2",
RefreshToken: "refresh_token_2",
AccessToken: "access_token_2",
}
cfg := configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{
server1: {
Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=",
IdentityToken: cred1.RefreshToken,
RegistryToken: cred1.AccessToken,
},
server2: {
Auth: "dXNlcm5hbWVfMjpwYXNzd29yZF8y",
IdentityToken: "refresh_token_2",
RegistryToken: "access_token_2",
},
},
SomeConfigField: 123,
}
jsonStr, err := json.Marshal(cfg)
if err != nil {
t.Fatalf("failed to marshal config: %v", err)
}
if err := os.WriteFile(configPath, jsonStr, 0666); err != nil {
t.Fatalf("failed to write config file: %v", err)
}
fs, err := NewFileStore(configPath)
if err != nil {
t.Fatal("NewFileStore() error =", err)
}
// test get
got, err := fs.Get(ctx, server1)
if err != nil {
t.Fatalf("FileStore.Get() error = %v", err)
}
if want := cred1; !reflect.DeepEqual(got, want) {
t.Errorf("FileStore.Get(%s) = %v, want %v", server1, got, want)
}
got, err = fs.Get(ctx, server2)
if err != nil {
t.Fatalf("FileStore.Get() error = %v", err)
}
if want := cred2; !reflect.DeepEqual(got, want) {
t.Errorf("FileStore.Get(%s) = %v, want %v", server2, got, want)
}
// test delete
if err := fs.Delete(ctx, server1); err != nil {
t.Fatalf("FileStore.Delete() error = %v", err)
}
// verify config file
configFile, err := os.Open(configPath)
if err != nil {
t.Fatalf("failed to open config file: %v", err)
}
defer configFile.Close()
var gotCfg configtest.Config
if err := json.NewDecoder(configFile).Decode(&gotCfg); err != nil {
t.Fatalf("failed to decode config file: %v", err)
}
wantCfg := configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{
server2: cfg.AuthConfigs[server2],
},
SomeConfigField: cfg.SomeConfigField,
}
if !reflect.DeepEqual(gotCfg, wantCfg) {
t.Errorf("Decoded config = %v, want %v", gotCfg, wantCfg)
}
// test get again
got, err = fs.Get(ctx, server1)
if err != nil {
t.Fatalf("FileStore.Get() error = %v", err)
}
if want := auth.EmptyCredential; !reflect.DeepEqual(got, want) {
t.Errorf("FileStore.Get(%s) = %v, want %v", server1, got, want)
}
got, err = fs.Get(ctx, server2)
if err != nil {
t.Fatalf("FileStore.Get() error = %v", err)
}
if want := cred2; !reflect.DeepEqual(got, want) {
t.Errorf("FileStore.Get(%s) = %v, want %v", server2, got, want)
}
}
func TestFileStore_Delete_lastConfig(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.json")
ctx := context.Background()
// prepare test content
server := "registry1.example.com"
cred := auth.Credential{
Username: "username",
Password: "password",
RefreshToken: "refresh_token",
AccessToken: "access_token",
}
cfg := configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{
server: {
Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=",
IdentityToken: cred.RefreshToken,
RegistryToken: cred.AccessToken,
},
},
SomeConfigField: 123,
}
jsonStr, err := json.Marshal(cfg)
if err != nil {
t.Fatalf("failed to marshal config: %v", err)
}
if err := os.WriteFile(configPath, jsonStr, 0666); err != nil {
t.Fatalf("failed to write config file: %v", err)
}
fs, err := NewFileStore(configPath)
if err != nil {
t.Fatal("NewFileStore() error =", err)
}
// test get
got, err := fs.Get(ctx, server)
if err != nil {
t.Fatalf("FileStore.Get() error = %v", err)
}
if want := cred; !reflect.DeepEqual(got, want) {
t.Errorf("FileStore.Get(%s) = %v, want %v", server, got, want)
}
// test delete
if err := fs.Delete(ctx, server); err != nil {
t.Fatalf("FileStore.Delete() error = %v", err)
}
// verify config file
configFile, err := os.Open(configPath)
if err != nil {
t.Fatalf("failed to open config file: %v", err)
}
defer configFile.Close()
var gotCfg configtest.Config
if err := json.NewDecoder(configFile).Decode(&gotCfg); err != nil {
t.Fatalf("failed to decode config file: %v", err)
}
wantCfg := configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{},
SomeConfigField: cfg.SomeConfigField,
}
if !reflect.DeepEqual(gotCfg, wantCfg) {
t.Errorf("Decoded config = %v, want %v", gotCfg, wantCfg)
}
// test get again
got, err = fs.Get(ctx, server)
if err != nil {
t.Fatalf("FileStore.Get() error = %v", err)
}
if want := auth.EmptyCredential; !reflect.DeepEqual(got, want) {
t.Errorf("FileStore.Get(%s) = %v, want %v", server, got, want)
}
}
func TestFileStore_Delete_notExistRecord(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.json")
ctx := context.Background()
// prepare test content
server := "registry1.example.com"
cred := auth.Credential{
Username: "username",
Password: "password",
RefreshToken: "refresh_token",
AccessToken: "access_token",
}
cfg := configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{
server: {
Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=",
IdentityToken: cred.RefreshToken,
RegistryToken: cred.AccessToken,
},
},
SomeConfigField: 123,
}
jsonStr, err := json.Marshal(cfg)
if err != nil {
t.Fatalf("failed to marshal config: %v", err)
}
if err := os.WriteFile(configPath, jsonStr, 0666); err != nil {
t.Fatalf("failed to write config file: %v", err)
}
fs, err := NewFileStore(configPath)
if err != nil {
t.Fatal("NewFileStore() error =", err)
}
// test get
got, err := fs.Get(ctx, server)
if err != nil {
t.Fatalf("FileStore.Get() error = %v", err)
}
if want := cred; !reflect.DeepEqual(got, want) {
t.Errorf("FileStore.Get(%s) = %v, want %v", server, got, want)
}
// test delete
if err := fs.Delete(ctx, "test.example.com"); err != nil {
t.Fatalf("FileStore.Delete() error = %v", err)
}
// verify config file
configFile, err := os.Open(configPath)
if err != nil {
t.Fatalf("failed to open config file: %v", err)
}
defer configFile.Close()
var gotCfg configtest.Config
if err := json.NewDecoder(configFile).Decode(&gotCfg); err != nil {
t.Fatalf("failed to decode config file: %v", err)
}
wantCfg := configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{
server: cfg.AuthConfigs[server],
},
SomeConfigField: cfg.SomeConfigField,
}
if !reflect.DeepEqual(gotCfg, wantCfg) {
t.Errorf("Decoded config = %v, want %v", gotCfg, wantCfg)
}
// test get again
got, err = fs.Get(ctx, server)
if err != nil {
t.Fatalf("FileStore.Get() error = %v", err)
}
if want := cred; !reflect.DeepEqual(got, want) {
t.Errorf("FileStore.Get(%s) = %v, want %v", server, got, want)
}
}
func TestFileStore_Delete_notExistConfig(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.json")
ctx := context.Background()
fs, err := NewFileStore(configPath)
if err != nil {
t.Fatal("NewFileStore() error =", err)
}
server := "test.example.com"
// test delete
if err := fs.Delete(ctx, server); err != nil {
t.Fatalf("FileStore.Delete() error = %v", err)
}
// verify config file is not created
_, err = os.Stat(configPath)
if wantErr := os.ErrNotExist; !errors.Is(err, wantErr) {
t.Errorf("Stat(%s) error = %v, wantErr %v", configPath, err, wantErr)
}
}
func Test_validateCredentialFormat(t *testing.T) {
tests := []struct {
name string
cred auth.Credential
wantErr error
}{
{
name: "Username contains colon",
cred: auth.Credential{
Username: "x:y",
Password: "z",
},
wantErr: ErrBadCredentialFormat,
},
{
name: "Password contains colon",
cred: auth.Credential{
Username: "x",
Password: "y:z",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := validateCredentialFormat(tt.cred); !errors.Is(err, tt.wantErr) {
t.Errorf("validateCredentialFormat() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
oras-go-2.5.0/registry/remote/credentials/internal/ 0000775 0000000 0000000 00000000000 14576745303 0022357 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/registry/remote/credentials/internal/config/ 0000775 0000000 0000000 00000000000 14576745303 0023624 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/registry/remote/credentials/internal/config/config.go 0000664 0000000 0000000 00000026043 14576745303 0025425 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 config
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"oras.land/oras-go/v2/registry/remote/auth"
"oras.land/oras-go/v2/registry/remote/credentials/internal/ioutil"
)
const (
// configFieldAuths is the "auths" field in the config file.
// Reference: https://github.com/docker/cli/blob/v24.0.0-beta.2/cli/config/configfile/file.go#L19
configFieldAuths = "auths"
// configFieldCredentialsStore is the "credsStore" field in the config file.
configFieldCredentialsStore = "credsStore"
// configFieldCredentialHelpers is the "credHelpers" field in the config file.
configFieldCredentialHelpers = "credHelpers"
)
// ErrInvalidConfigFormat is returned when the config format is invalid.
var ErrInvalidConfigFormat = errors.New("invalid config format")
// AuthConfig contains authorization information for connecting to a Registry.
// References:
// - https://github.com/docker/cli/blob/v24.0.0-beta.2/cli/config/configfile/file.go#L17-L45
// - https://github.com/docker/cli/blob/v24.0.0-beta.2/cli/config/types/authconfig.go#L3-L22
type AuthConfig struct {
// Auth is a base64-encoded string of "{username}:{password}".
Auth string `json:"auth,omitempty"`
// IdentityToken is used to authenticate the user and get an access token
// for the registry.
IdentityToken string `json:"identitytoken,omitempty"`
// RegistryToken is a bearer token to be sent to a registry.
RegistryToken string `json:"registrytoken,omitempty"`
Username string `json:"username,omitempty"` // legacy field for compatibility
Password string `json:"password,omitempty"` // legacy field for compatibility
}
// NewAuthConfig creates an authConfig based on cred.
func NewAuthConfig(cred auth.Credential) AuthConfig {
return AuthConfig{
Auth: encodeAuth(cred.Username, cred.Password),
IdentityToken: cred.RefreshToken,
RegistryToken: cred.AccessToken,
}
}
// Credential returns an auth.Credential based on ac.
func (ac AuthConfig) Credential() (auth.Credential, error) {
cred := auth.Credential{
Username: ac.Username,
Password: ac.Password,
RefreshToken: ac.IdentityToken,
AccessToken: ac.RegistryToken,
}
if ac.Auth != "" {
var err error
// override username and password
cred.Username, cred.Password, err = decodeAuth(ac.Auth)
if err != nil {
return auth.EmptyCredential, fmt.Errorf("failed to decode auth field: %w: %v", ErrInvalidConfigFormat, err)
}
}
return cred, nil
}
// Config represents a docker configuration file.
// References:
// - https://docs.docker.com/engine/reference/commandline/cli/#docker-cli-configuration-file-configjson-properties
// - https://github.com/docker/cli/blob/v24.0.0-beta.2/cli/config/configfile/file.go#L17-L44
type Config struct {
// path is the path to the config file.
path string
// rwLock is a read-write-lock for the file store.
rwLock sync.RWMutex
// content is the content of the config file.
// Reference: https://github.com/docker/cli/blob/v24.0.0-beta.2/cli/config/configfile/file.go#L17-L44
content map[string]json.RawMessage
// authsCache is a cache of the auths field of the config.
// Reference: https://github.com/docker/cli/blob/v24.0.0-beta.2/cli/config/configfile/file.go#L19
authsCache map[string]json.RawMessage
// credentialsStore is the credsStore field of the config.
// Reference: https://github.com/docker/cli/blob/v24.0.0-beta.2/cli/config/configfile/file.go#L28
credentialsStore string
// credentialHelpers is the credHelpers field of the config.
// Reference: https://github.com/docker/cli/blob/v24.0.0-beta.2/cli/config/configfile/file.go#L29
credentialHelpers map[string]string
}
// Load loads Config from the given config path.
func Load(configPath string) (*Config, error) {
cfg := &Config{path: configPath}
configFile, err := os.Open(configPath)
if err != nil {
if os.IsNotExist(err) {
// init content and caches if the content file does not exist
cfg.content = make(map[string]json.RawMessage)
cfg.authsCache = make(map[string]json.RawMessage)
return cfg, nil
}
return nil, fmt.Errorf("failed to open config file at %s: %w", configPath, err)
}
defer configFile.Close()
// decode config content if the config file exists
if err := json.NewDecoder(configFile).Decode(&cfg.content); err != nil {
return nil, fmt.Errorf("failed to decode config file at %s: %w: %v", configPath, ErrInvalidConfigFormat, err)
}
if credsStoreBytes, ok := cfg.content[configFieldCredentialsStore]; ok {
if err := json.Unmarshal(credsStoreBytes, &cfg.credentialsStore); err != nil {
return nil, fmt.Errorf("failed to unmarshal creds store field: %w: %v", ErrInvalidConfigFormat, err)
}
}
if credHelpersBytes, ok := cfg.content[configFieldCredentialHelpers]; ok {
if err := json.Unmarshal(credHelpersBytes, &cfg.credentialHelpers); err != nil {
return nil, fmt.Errorf("failed to unmarshal cred helpers field: %w: %v", ErrInvalidConfigFormat, err)
}
}
if authsBytes, ok := cfg.content[configFieldAuths]; ok {
if err := json.Unmarshal(authsBytes, &cfg.authsCache); err != nil {
return nil, fmt.Errorf("failed to unmarshal auths field: %w: %v", ErrInvalidConfigFormat, err)
}
}
if cfg.authsCache == nil {
cfg.authsCache = make(map[string]json.RawMessage)
}
return cfg, nil
}
// GetAuthConfig returns an auth.Credential for serverAddress.
func (cfg *Config) GetCredential(serverAddress string) (auth.Credential, error) {
cfg.rwLock.RLock()
defer cfg.rwLock.RUnlock()
authCfgBytes, ok := cfg.authsCache[serverAddress]
if !ok {
// NOTE: the auth key for the server address may have been stored with
// a http/https prefix in legacy config files, e.g. "registry.example.com"
// can be stored as "https://registry.example.com/".
var matched bool
for addr, auth := range cfg.authsCache {
if toHostname(addr) == serverAddress {
matched = true
authCfgBytes = auth
break
}
}
if !matched {
return auth.EmptyCredential, nil
}
}
var authCfg AuthConfig
if err := json.Unmarshal(authCfgBytes, &authCfg); err != nil {
return auth.EmptyCredential, fmt.Errorf("failed to unmarshal auth field: %w: %v", ErrInvalidConfigFormat, err)
}
return authCfg.Credential()
}
// PutAuthConfig puts cred for serverAddress.
func (cfg *Config) PutCredential(serverAddress string, cred auth.Credential) error {
cfg.rwLock.Lock()
defer cfg.rwLock.Unlock()
authCfg := NewAuthConfig(cred)
authCfgBytes, err := json.Marshal(authCfg)
if err != nil {
return fmt.Errorf("failed to marshal auth field: %w", err)
}
cfg.authsCache[serverAddress] = authCfgBytes
return cfg.saveFile()
}
// DeleteAuthConfig deletes the corresponding credential for serverAddress.
func (cfg *Config) DeleteCredential(serverAddress string) error {
cfg.rwLock.Lock()
defer cfg.rwLock.Unlock()
if _, ok := cfg.authsCache[serverAddress]; !ok {
// no ops
return nil
}
delete(cfg.authsCache, serverAddress)
return cfg.saveFile()
}
// GetCredentialHelper returns the credential helpers for serverAddress.
func (cfg *Config) GetCredentialHelper(serverAddress string) string {
return cfg.credentialHelpers[serverAddress]
}
// CredentialsStore returns the configured credentials store.
func (cfg *Config) CredentialsStore() string {
cfg.rwLock.RLock()
defer cfg.rwLock.RUnlock()
return cfg.credentialsStore
}
// Path returns the path to the config file.
func (cfg *Config) Path() string {
return cfg.path
}
// SetCredentialsStore puts the configured credentials store.
func (cfg *Config) SetCredentialsStore(credsStore string) error {
cfg.rwLock.Lock()
defer cfg.rwLock.Unlock()
cfg.credentialsStore = credsStore
return cfg.saveFile()
}
// IsAuthConfigured returns whether there is authentication configured in this
// config file or not.
func (cfg *Config) IsAuthConfigured() bool {
return cfg.credentialsStore != "" ||
len(cfg.credentialHelpers) > 0 ||
len(cfg.authsCache) > 0
}
// saveFile saves Config into the file.
func (cfg *Config) saveFile() (returnErr error) {
// marshal content
// credentialHelpers is skipped as it's never set
if cfg.credentialsStore != "" {
credsStoreBytes, err := json.Marshal(cfg.credentialsStore)
if err != nil {
return fmt.Errorf("failed to marshal creds store: %w", err)
}
cfg.content[configFieldCredentialsStore] = credsStoreBytes
} else {
// omit empty
delete(cfg.content, configFieldCredentialsStore)
}
authsBytes, err := json.Marshal(cfg.authsCache)
if err != nil {
return fmt.Errorf("failed to marshal credentials: %w", err)
}
cfg.content[configFieldAuths] = authsBytes
jsonBytes, err := json.MarshalIndent(cfg.content, "", "\t")
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
// write the content to a ingest file for atomicity
configDir := filepath.Dir(cfg.path)
if err := os.MkdirAll(configDir, 0700); err != nil {
return fmt.Errorf("failed to make directory %s: %w", configDir, err)
}
ingest, err := ioutil.Ingest(configDir, bytes.NewReader(jsonBytes))
if err != nil {
return fmt.Errorf("failed to save config file: %w", err)
}
defer func() {
if returnErr != nil {
// clean up the ingest file in case of error
os.Remove(ingest)
}
}()
// overwrite the config file
if err := os.Rename(ingest, cfg.path); err != nil {
return fmt.Errorf("failed to save config file: %w", err)
}
return nil
}
// encodeAuth base64-encodes username and password into base64(username:password).
func encodeAuth(username, password string) string {
if username == "" && password == "" {
return ""
}
return base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
}
// decodeAuth decodes a base64 encoded string and returns username and password.
func decodeAuth(authStr string) (username string, password string, err error) {
if authStr == "" {
return "", "", nil
}
decoded, err := base64.StdEncoding.DecodeString(authStr)
if err != nil {
return "", "", err
}
decodedStr := string(decoded)
username, password, ok := strings.Cut(decodedStr, ":")
if !ok {
return "", "", fmt.Errorf("auth '%s' does not conform the base64(username:password) format", decodedStr)
}
return username, password, nil
}
// toHostname normalizes a server address to just its hostname, removing
// the scheme and the path parts.
// It is used to match keys in the auths map, which may be either stored as
// hostname or as hostname including scheme (in legacy docker config files).
// Reference: https://github.com/docker/cli/blob/v24.0.6/cli/config/credentials/file_store.go#L71
func toHostname(addr string) string {
addr = strings.TrimPrefix(addr, "http://")
addr = strings.TrimPrefix(addr, "https://")
addr, _, _ = strings.Cut(addr, "/")
return addr
}
oras-go-2.5.0/registry/remote/credentials/internal/config/config_test.go 0000664 0000000 0000000 00000111157 14576745303 0026465 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 config
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"reflect"
"testing"
"oras.land/oras-go/v2/registry/remote/auth"
"oras.land/oras-go/v2/registry/remote/credentials/internal/config/configtest"
)
func TestLoad_badPath(t *testing.T) {
tempDir := t.TempDir()
tests := []struct {
name string
configPath string
wantErr bool
}{
{
name: "Path is a directory",
configPath: tempDir,
wantErr: true,
},
{
name: "Empty file name",
configPath: filepath.Join(tempDir, ""),
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := Load(tt.configPath)
if (err != nil) != tt.wantErr {
t.Errorf("Load() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
func TestLoad_badFormat(t *testing.T) {
tests := []struct {
name string
configPath string
wantErr bool
}{
{
name: "Bad JSON format",
configPath: "../../testdata/bad_config",
wantErr: true,
},
{
name: "Invalid auths format",
configPath: "../../testdata/invalid_auths_config.json",
wantErr: true,
},
{
name: "No auths field",
configPath: "../../testdata/no_auths_config.json",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := Load(tt.configPath)
if (err != nil) != tt.wantErr {
t.Errorf("Load() error = %v, wantErr %v", err, tt.wantErr)
return
}
})
}
}
func TestConfig_GetCredential_validConfig(t *testing.T) {
cfg, err := Load("../../testdata/valid_auths_config.json")
if err != nil {
t.Fatal("Load() error =", err)
}
tests := []struct {
name string
serverAddress string
want auth.Credential
wantErr bool
}{
{
name: "Username and password",
serverAddress: "registry1.example.com",
want: auth.Credential{
Username: "username",
Password: "password",
},
},
{
name: "Identity token",
serverAddress: "registry2.example.com",
want: auth.Credential{
RefreshToken: "identity_token",
},
},
{
name: "Registry token",
serverAddress: "registry3.example.com",
want: auth.Credential{
AccessToken: "registry_token",
},
},
{
name: "Username and password, identity token and registry token",
serverAddress: "registry4.example.com",
want: auth.Credential{
Username: "username",
Password: "password",
RefreshToken: "identity_token",
AccessToken: "registry_token",
},
},
{
name: "Empty credential",
serverAddress: "registry5.example.com",
want: auth.EmptyCredential,
},
{
name: "Username and password, no auth",
serverAddress: "registry6.example.com",
want: auth.Credential{
Username: "username",
Password: "password",
},
},
{
name: "Auth overriding Username and password",
serverAddress: "registry7.example.com",
want: auth.Credential{
Username: "username",
Password: "password",
},
},
{
name: "Not in auths",
serverAddress: "foo.example.com",
want: auth.EmptyCredential,
},
{
name: "No record",
serverAddress: "registry999.example.com",
want: auth.EmptyCredential,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := cfg.GetCredential(tt.serverAddress)
if (err != nil) != tt.wantErr {
t.Errorf("Config.GetCredential() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Config.GetCredential() = %v, want %v", got, tt.want)
}
})
}
}
func TestConfig_GetCredential_legacyConfig(t *testing.T) {
cfg, err := Load("../../testdata/legacy_auths_config.json")
if err != nil {
t.Fatal("Load() error =", err)
}
tests := []struct {
name string
serverAddress string
want auth.Credential
wantErr bool
}{
{
name: "Regular address matched",
serverAddress: "registry1.example.com",
want: auth.Credential{
Username: "username1",
Password: "password1",
},
},
{
name: "Another entry for the same address matched",
serverAddress: "https://registry1.example.com/",
want: auth.Credential{
Username: "foo",
Password: "bar",
},
},
{
name: "Address with different scheme unmached",
serverAddress: "http://registry1.example.com/",
want: auth.EmptyCredential,
},
{
name: "Address with http prefix matched",
serverAddress: "registry2.example.com",
want: auth.Credential{
Username: "username2",
Password: "password2",
},
},
{
name: "Address with https prefix matched",
serverAddress: "registry3.example.com",
want: auth.Credential{
Username: "username3",
Password: "password3",
},
},
{
name: "Address with http prefix and / suffix matched",
serverAddress: "registry4.example.com",
want: auth.Credential{
Username: "username4",
Password: "password4",
},
},
{
name: "Address with https prefix and / suffix matched",
serverAddress: "registry5.example.com",
want: auth.Credential{
Username: "username5",
Password: "password5",
},
},
{
name: "Address with https prefix and path suffix matched",
serverAddress: "registry6.example.com",
want: auth.Credential{
Username: "username6",
Password: "password6",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := cfg.GetCredential(tt.serverAddress)
if (err != nil) != tt.wantErr {
t.Errorf("Config.GetCredential() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Config.GetCredential() = %v, want %v", got, tt.want)
}
})
}
}
func TestConfig_GetCredential_invalidConfig(t *testing.T) {
cfg, err := Load("../../testdata/invalid_auths_entry_config.json")
if err != nil {
t.Fatal("Load() error =", err)
}
tests := []struct {
name string
serverAddress string
want auth.Credential
wantErr bool
}{
{
name: "Invalid auth encode",
serverAddress: "registry1.example.com",
want: auth.EmptyCredential,
wantErr: true,
},
{
name: "Invalid auths format",
serverAddress: "registry2.example.com",
want: auth.EmptyCredential,
wantErr: true,
},
{
name: "Invalid type",
serverAddress: "registry3.example.com",
want: auth.EmptyCredential,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := cfg.GetCredential(tt.serverAddress)
if (err != nil) != tt.wantErr {
t.Errorf("Config.GetCredential() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Config.GetCredential() = %v, want %v", got, tt.want)
}
})
}
}
func TestConfig_GetCredential_emptyConfig(t *testing.T) {
cfg, err := Load("../../testdata/empty_config.json")
if err != nil {
t.Fatal("Load() error =", err)
}
tests := []struct {
name string
serverAddress string
want auth.Credential
wantErr error
}{
{
name: "Not found",
serverAddress: "registry.example.com",
want: auth.EmptyCredential,
wantErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := cfg.GetCredential(tt.serverAddress)
if !errors.Is(err, tt.wantErr) {
t.Errorf("Config.GetCredential() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Config.GetCredential() = %v, want %v", got, tt.want)
}
})
}
}
func TestConfig_GetCredential_notExistConfig(t *testing.T) {
cfg, err := Load("whatever")
if err != nil {
t.Fatal("Load() error =", err)
}
tests := []struct {
name string
serverAddress string
want auth.Credential
wantErr error
}{
{
name: "Not found",
serverAddress: "registry.example.com",
want: auth.EmptyCredential,
wantErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := cfg.GetCredential(tt.serverAddress)
if !errors.Is(err, tt.wantErr) {
t.Errorf("Config.GetCredential() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Config.GetCredential() = %v, want %v", got, tt.want)
}
})
}
}
func TestConfig_PutCredential_notExistConfig(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.json")
cfg, err := Load(configPath)
if err != nil {
t.Fatal("Load() error =", err)
}
server := "test.example.com"
cred := auth.Credential{
Username: "username",
Password: "password",
RefreshToken: "refresh_token",
AccessToken: "access_token",
}
// test put
if err := cfg.PutCredential(server, cred); err != nil {
t.Fatalf("Config.PutCredential() error = %v", err)
}
// verify config file
configFile, err := os.Open(configPath)
if err != nil {
t.Fatalf("failed to open config file: %v", err)
}
defer configFile.Close()
var testCfg configtest.Config
if err := json.NewDecoder(configFile).Decode(&testCfg); err != nil {
t.Fatalf("failed to decode config file: %v", err)
}
want := configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{
server: {
Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=",
IdentityToken: "refresh_token",
RegistryToken: "access_token",
},
},
}
if !reflect.DeepEqual(testCfg, want) {
t.Errorf("Decoded config = %v, want %v", testCfg, want)
}
// verify get
got, err := cfg.GetCredential(server)
if err != nil {
t.Fatalf("Config.GetCredential() error = %v", err)
}
if want := cred; !reflect.DeepEqual(got, want) {
t.Errorf("Config.GetCredential() = %v, want %v", got, want)
}
}
func TestConfig_PutCredential_addNew(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.json")
// prepare test content
server1 := "registry1.example.com"
cred1 := auth.Credential{
Username: "username",
Password: "password",
RefreshToken: "refresh_token",
AccessToken: "access_token",
}
testCfg := configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{
server1: {
SomeAuthField: "whatever",
Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=",
IdentityToken: cred1.RefreshToken,
RegistryToken: cred1.AccessToken,
},
},
SomeConfigField: 123,
}
jsonStr, err := json.Marshal(testCfg)
if err != nil {
t.Fatalf("failed to marshal config: %v", err)
}
if err := os.WriteFile(configPath, jsonStr, 0666); err != nil {
t.Fatalf("failed to write config file: %v", err)
}
// test put
cfg, err := Load(configPath)
if err != nil {
t.Fatal("Load() error =", err)
}
server2 := "registry2.example.com"
cred2 := auth.Credential{
Username: "username_2",
Password: "password_2",
RefreshToken: "refresh_token_2",
AccessToken: "access_token_2",
}
if err := cfg.PutCredential(server2, cred2); err != nil {
t.Fatalf("Config.PutCredential() error = %v", err)
}
// verify config file
configFile, err := os.Open(configPath)
if err != nil {
t.Fatalf("failed to open config file: %v", err)
}
defer configFile.Close()
var gotCfg configtest.Config
if err := json.NewDecoder(configFile).Decode(&gotCfg); err != nil {
t.Fatalf("failed to decode config file: %v", err)
}
wantTestCfg := configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{
server1: {
SomeAuthField: "whatever",
Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=",
IdentityToken: cred1.RefreshToken,
RegistryToken: cred1.AccessToken,
},
server2: {
Auth: "dXNlcm5hbWVfMjpwYXNzd29yZF8y",
IdentityToken: "refresh_token_2",
RegistryToken: "access_token_2",
},
},
SomeConfigField: testCfg.SomeConfigField,
}
if !reflect.DeepEqual(gotCfg, wantTestCfg) {
t.Errorf("Decoded config = %v, want %v", gotCfg, wantTestCfg)
}
// verify get
got, err := cfg.GetCredential(server1)
if err != nil {
t.Fatalf("Config.GetCredential() error = %v", err)
}
if want := cred1; !reflect.DeepEqual(got, want) {
t.Errorf("Config.GetCredential(%s) = %v, want %v", server1, got, want)
}
got, err = cfg.GetCredential(server2)
if err != nil {
t.Fatalf("Config.GetCredential() error = %v", err)
}
if want := cred2; !reflect.DeepEqual(got, want) {
t.Errorf("Config.GetCredential(%s) = %v, want %v", server2, got, want)
}
}
func TestConfig_PutCredential_updateOld(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.json")
// prepare test content
server := "registry.example.com"
testCfg := configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{
server: {
SomeAuthField: "whatever",
Username: "foo",
Password: "bar",
IdentityToken: "refresh_token",
},
},
SomeConfigField: 123,
}
jsonStr, err := json.Marshal(testCfg)
if err != nil {
t.Fatalf("failed to marshal config: %v", err)
}
if err := os.WriteFile(configPath, jsonStr, 0666); err != nil {
t.Fatalf("failed to write config file: %v", err)
}
// test put
cfg, err := Load(configPath)
if err != nil {
t.Fatal("Load() error =", err)
}
cred := auth.Credential{
Username: "username",
Password: "password",
AccessToken: "access_token",
}
if err := cfg.PutCredential(server, cred); err != nil {
t.Fatalf("Config.PutCredential() error = %v", err)
}
// verify config file
configFile, err := os.Open(configPath)
if err != nil {
t.Fatalf("failed to open config file: %v", err)
}
defer configFile.Close()
var gotCfg configtest.Config
if err := json.NewDecoder(configFile).Decode(&gotCfg); err != nil {
t.Fatalf("failed to decode config file: %v", err)
}
wantCfg := configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{
server: {
Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=",
RegistryToken: "access_token",
},
},
SomeConfigField: testCfg.SomeConfigField,
}
if !reflect.DeepEqual(gotCfg, wantCfg) {
t.Errorf("Decoded config = %v, want %v", gotCfg, wantCfg)
}
// verify get
got, err := cfg.GetCredential(server)
if err != nil {
t.Fatalf("Config.GetCredential() error = %v", err)
}
if want := cred; !reflect.DeepEqual(got, want) {
t.Errorf("Config.GetCredential(%s) = %v, want %v", server, got, want)
}
}
func TestConfig_DeleteCredential(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.json")
// prepare test content
server1 := "registry1.example.com"
cred1 := auth.Credential{
Username: "username",
Password: "password",
RefreshToken: "refresh_token",
AccessToken: "access_token",
}
server2 := "registry2.example.com"
cred2 := auth.Credential{
Username: "username_2",
Password: "password_2",
RefreshToken: "refresh_token_2",
AccessToken: "access_token_2",
}
testCfg := configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{
server1: {
Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=",
IdentityToken: cred1.RefreshToken,
RegistryToken: cred1.AccessToken,
},
server2: {
Auth: "dXNlcm5hbWVfMjpwYXNzd29yZF8y",
IdentityToken: "refresh_token_2",
RegistryToken: "access_token_2",
},
},
SomeConfigField: 123,
}
jsonStr, err := json.Marshal(testCfg)
if err != nil {
t.Fatalf("failed to marshal config: %v", err)
}
if err := os.WriteFile(configPath, jsonStr, 0666); err != nil {
t.Fatalf("failed to write config file: %v", err)
}
cfg, err := Load(configPath)
if err != nil {
t.Fatal("Load() error =", err)
}
// test get
got, err := cfg.GetCredential(server1)
if err != nil {
t.Fatalf("FileStore.GetCredential() error = %v", err)
}
if want := cred1; !reflect.DeepEqual(got, want) {
t.Errorf("FileStore.GetCredential(%s) = %v, want %v", server1, got, want)
}
got, err = cfg.GetCredential(server2)
if err != nil {
t.Fatalf("FileStore.GetCredential() error = %v", err)
}
if want := cred2; !reflect.DeepEqual(got, want) {
t.Errorf("FileStore.Get(%s) = %v, want %v", server2, got, want)
}
// test delete
if err := cfg.DeleteCredential(server1); err != nil {
t.Fatalf("Config.DeleteCredential() error = %v", err)
}
// verify config file
configFile, err := os.Open(configPath)
if err != nil {
t.Fatalf("failed to open config file: %v", err)
}
defer configFile.Close()
var gotTestCfg configtest.Config
if err := json.NewDecoder(configFile).Decode(&gotTestCfg); err != nil {
t.Fatalf("failed to decode config file: %v", err)
}
wantTestCfg := configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{
server2: testCfg.AuthConfigs[server2],
},
SomeConfigField: testCfg.SomeConfigField,
}
if !reflect.DeepEqual(gotTestCfg, wantTestCfg) {
t.Errorf("Decoded config = %v, want %v", gotTestCfg, wantTestCfg)
}
// test get again
got, err = cfg.GetCredential(server1)
if err != nil {
t.Fatalf("Config.GetCredential() error = %v", err)
}
if want := auth.EmptyCredential; !reflect.DeepEqual(got, want) {
t.Errorf("Config.GetCredential(%s) = %v, want %v", server1, got, want)
}
got, err = cfg.GetCredential(server2)
if err != nil {
t.Fatalf("Config.GetCredential() error = %v", err)
}
if want := cred2; !reflect.DeepEqual(got, want) {
t.Errorf("Config.GetCredential(%s) = %v, want %v", server2, got, want)
}
}
func TestConfig_DeleteCredential_lastConfig(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.json")
// prepare test content
server := "registry1.example.com"
cred := auth.Credential{
Username: "username",
Password: "password",
RefreshToken: "refresh_token",
AccessToken: "access_token",
}
testCfg := configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{
server: {
Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=",
IdentityToken: cred.RefreshToken,
RegistryToken: cred.AccessToken,
},
},
SomeConfigField: 123,
}
jsonStr, err := json.Marshal(testCfg)
if err != nil {
t.Fatalf("failed to marshal config: %v", err)
}
if err := os.WriteFile(configPath, jsonStr, 0666); err != nil {
t.Fatalf("failed to write config file: %v", err)
}
cfg, err := Load(configPath)
if err != nil {
t.Fatal("Load() error =", err)
}
// test get
got, err := cfg.GetCredential(server)
if err != nil {
t.Fatalf("Config.GetCredential() error = %v", err)
}
if want := cred; !reflect.DeepEqual(got, want) {
t.Errorf("Config.GetCredential(%s) = %v, want %v", server, got, want)
}
// test delete
if err := cfg.DeleteCredential(server); err != nil {
t.Fatalf("Config.DeleteCredential() error = %v", err)
}
// verify config file
configFile, err := os.Open(configPath)
if err != nil {
t.Fatalf("failed to open config file: %v", err)
}
defer configFile.Close()
var gotTestCfg configtest.Config
if err := json.NewDecoder(configFile).Decode(&gotTestCfg); err != nil {
t.Fatalf("failed to decode config file: %v", err)
}
wantTestCfg := configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{},
SomeConfigField: testCfg.SomeConfigField,
}
if !reflect.DeepEqual(gotTestCfg, wantTestCfg) {
t.Errorf("Decoded config = %v, want %v", gotTestCfg, wantTestCfg)
}
// test get again
got, err = cfg.GetCredential(server)
if err != nil {
t.Fatalf("Config.GetCredential() error = %v", err)
}
if want := auth.EmptyCredential; !reflect.DeepEqual(got, want) {
t.Errorf("Config.GetCredential(%s) = %v, want %v", server, got, want)
}
}
func TestConfig_DeleteCredential_notExistRecord(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.json")
// prepare test content
server := "registry1.example.com"
cred := auth.Credential{
Username: "username",
Password: "password",
RefreshToken: "refresh_token",
AccessToken: "access_token",
}
testCfg := configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{
server: {
Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=",
IdentityToken: cred.RefreshToken,
RegistryToken: cred.AccessToken,
},
},
SomeConfigField: 123,
}
jsonStr, err := json.Marshal(testCfg)
if err != nil {
t.Fatalf("failed to marshal config: %v", err)
}
if err := os.WriteFile(configPath, jsonStr, 0666); err != nil {
t.Fatalf("failed to write config file: %v", err)
}
cfg, err := Load(configPath)
if err != nil {
t.Fatal("Load() error =", err)
}
// test get
got, err := cfg.GetCredential(server)
if err != nil {
t.Fatalf("Config.GetCredential() error = %v", err)
}
if want := cred; !reflect.DeepEqual(got, want) {
t.Errorf("Config.GetCredential(%s) = %v, want %v", server, got, want)
}
// test delete
if err := cfg.DeleteCredential("test.example.com"); err != nil {
t.Fatalf("Config.DeleteCredential() error = %v", err)
}
// verify config file
configFile, err := os.Open(configPath)
if err != nil {
t.Fatalf("failed to open config file: %v", err)
}
defer configFile.Close()
var gotTestCfg configtest.Config
if err := json.NewDecoder(configFile).Decode(&gotTestCfg); err != nil {
t.Fatalf("failed to decode config file: %v", err)
}
wantTestCfg := configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{
server: testCfg.AuthConfigs[server],
},
SomeConfigField: testCfg.SomeConfigField,
}
if !reflect.DeepEqual(gotTestCfg, wantTestCfg) {
t.Errorf("Decoded config = %v, want %v", gotTestCfg, wantTestCfg)
}
// test get again
got, err = cfg.GetCredential(server)
if err != nil {
t.Fatalf("Config.GetCredential() error = %v", err)
}
if want := cred; !reflect.DeepEqual(got, want) {
t.Errorf("Config.GetCredential(%s) = %v, want %v", server, got, want)
}
}
func TestConfig_DeleteCredential_notExistConfig(t *testing.T) {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.json")
cfg, err := Load(configPath)
if err != nil {
t.Fatal("Load() error =", err)
}
server := "test.example.com"
// test delete
if err := cfg.DeleteCredential(server); err != nil {
t.Fatalf("Config.DeleteCredential() error = %v", err)
}
// verify config file is not created
_, err = os.Stat(configPath)
if wantErr := os.ErrNotExist; !errors.Is(err, wantErr) {
t.Errorf("Stat(%s) error = %v, wantErr %v", configPath, err, wantErr)
}
}
func TestConfig_GetCredentialHelper(t *testing.T) {
cfg, err := Load("../../testdata/credHelpers_config.json")
if err != nil {
t.Fatal("Load() error =", err)
}
tests := []struct {
name string
serverAddress string
want string
}{
{
name: "Get cred helper: registry_helper1",
serverAddress: "registry1.example.com",
want: "registry1-helper",
},
{
name: "Get cred helper: registry_helper2",
serverAddress: "registry2.example.com",
want: "registry2-helper",
},
{
name: "Empty cred helper configured",
serverAddress: "registry3.example.com",
want: "",
},
{
name: "No cred helper configured",
serverAddress: "whatever.example.com",
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := cfg.GetCredentialHelper(tt.serverAddress); got != tt.want {
t.Errorf("Config.GetCredentialHelper() = %v, want %v", got, tt.want)
}
})
}
}
func TestConfig_CredentialsStore(t *testing.T) {
tests := []struct {
name string
configPath string
want string
}{
{
name: "creds store configured",
configPath: "../../testdata/credsStore_config.json",
want: "teststore",
},
{
name: "No creds store configured",
configPath: "../../testdata/credsHelpers_config.json",
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg, err := Load(tt.configPath)
if err != nil {
t.Fatal("Load() error =", err)
}
if got := cfg.CredentialsStore(); got != tt.want {
t.Errorf("Config.CredentialsStore() = %v, want %v", got, tt.want)
}
})
}
}
func TestConfig_SetCredentialsStore(t *testing.T) {
// prepare test content
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.json")
testCfg := configtest.Config{
SomeConfigField: 123,
}
jsonStr, err := json.Marshal(testCfg)
if err != nil {
t.Fatalf("failed to marshal config: %v", err)
}
if err := os.WriteFile(configPath, jsonStr, 0666); err != nil {
t.Fatalf("failed to write config file: %v", err)
}
// test SetCredentialsStore
cfg, err := Load(configPath)
if err != nil {
t.Fatal("Load() error =", err)
}
credsStore := "testStore"
if err := cfg.SetCredentialsStore(credsStore); err != nil {
t.Fatal("Config.SetCredentialsStore() error =", err)
}
// verify
if got := cfg.credentialsStore; got != credsStore {
t.Errorf("Config.credentialsStore = %v, want %v", got, credsStore)
}
// verify config file
configFile, err := os.Open(configPath)
if err != nil {
t.Fatalf("failed to open config file: %v", err)
}
var gotTestCfg1 configtest.Config
if err := json.NewDecoder(configFile).Decode(&gotTestCfg1); err != nil {
t.Fatalf("failed to decode config file: %v", err)
}
if err := configFile.Close(); err != nil {
t.Fatal("failed to close config file:", err)
}
wantTestCfg1 := configtest.Config{
AuthConfigs: make(map[string]configtest.AuthConfig),
CredentialsStore: credsStore,
SomeConfigField: testCfg.SomeConfigField,
}
if !reflect.DeepEqual(gotTestCfg1, wantTestCfg1) {
t.Errorf("Decoded config = %v, want %v", gotTestCfg1, wantTestCfg1)
}
// test SetCredentialsStore: set as empty
if err := cfg.SetCredentialsStore(""); err != nil {
t.Fatal("Config.SetCredentialsStore() error =", err)
}
// verify
if got := cfg.credentialsStore; got != "" {
t.Errorf("Config.credentialsStore = %v, want empty", got)
}
// verify config file
configFile, err = os.Open(configPath)
if err != nil {
t.Fatalf("failed to open config file: %v", err)
}
var gotTestCfg2 configtest.Config
if err := json.NewDecoder(configFile).Decode(&gotTestCfg2); err != nil {
t.Fatalf("failed to decode config file: %v", err)
}
if err := configFile.Close(); err != nil {
t.Fatal("failed to close config file:", err)
}
wantTestCfg2 := configtest.Config{
AuthConfigs: make(map[string]configtest.AuthConfig),
SomeConfigField: testCfg.SomeConfigField,
}
if !reflect.DeepEqual(gotTestCfg2, wantTestCfg2) {
t.Errorf("Decoded config = %v, want %v", gotTestCfg2, wantTestCfg2)
}
}
func TestConfig_IsAuthConfigured(t *testing.T) {
tempDir := t.TempDir()
tests := []struct {
name string
fileName string
shouldCreateFile bool
cfg configtest.Config
want bool
}{
{
name: "not existing file",
fileName: "config.json",
shouldCreateFile: false,
cfg: configtest.Config{},
want: false,
},
{
name: "no auth",
fileName: "config.json",
shouldCreateFile: true,
cfg: configtest.Config{
SomeConfigField: 123,
},
want: false,
},
{
name: "empty auths exist",
fileName: "empty_auths.json",
shouldCreateFile: true,
cfg: configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{},
},
want: false,
},
{
name: "auths exist, but no credential",
fileName: "no_cred_auths.json",
shouldCreateFile: true,
cfg: configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{
"test.example.com": {},
},
},
want: true,
},
{
name: "auths exist",
fileName: "auths.json",
shouldCreateFile: true,
cfg: configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{
"test.example.com": {
Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=",
},
},
},
want: true,
},
{
name: "credsStore exists",
fileName: "credsStore.json",
shouldCreateFile: true,
cfg: configtest.Config{
CredentialsStore: "teststore",
},
want: true,
},
{
name: "empty credHelpers exist",
fileName: "empty_credsStore.json",
shouldCreateFile: true,
cfg: configtest.Config{
CredentialHelpers: map[string]string{},
},
want: false,
},
{
name: "credHelpers exist",
fileName: "credsStore.json",
shouldCreateFile: true,
cfg: configtest.Config{
CredentialHelpers: map[string]string{
"test.example.com": "testhelper",
},
},
want: true,
},
{
name: "all exist",
fileName: "credsStore.json",
shouldCreateFile: true,
cfg: configtest.Config{
SomeConfigField: 123,
AuthConfigs: map[string]configtest.AuthConfig{
"test.example.com": {},
},
CredentialsStore: "teststore",
CredentialHelpers: map[string]string{
"test.example.com": "testhelper",
},
},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// prepare test content
configPath := filepath.Join(tempDir, tt.fileName)
if tt.shouldCreateFile {
jsonStr, err := json.Marshal(tt.cfg)
if err != nil {
t.Fatalf("failed to marshal config: %v", err)
}
if err := os.WriteFile(configPath, jsonStr, 0666); err != nil {
t.Fatalf("failed to write config file: %v", err)
}
}
cfg, err := Load(configPath)
if err != nil {
t.Fatal("Load() error =", err)
}
if got := cfg.IsAuthConfigured(); got != tt.want {
t.Errorf("IsAuthConfigured() = %v, want %v", got, tt.want)
}
})
}
}
func TestConfig_saveFile(t *testing.T) {
tempDir := t.TempDir()
tests := []struct {
name string
fileName string
shouldCreateFile bool
oldCfg configtest.Config
newCfg configtest.Config
wantCfg configtest.Config
}{
{
name: "set credsStore in a non-existing file",
fileName: "config.json",
oldCfg: configtest.Config{},
newCfg: configtest.Config{
CredentialsStore: "teststore",
},
wantCfg: configtest.Config{
AuthConfigs: make(map[string]configtest.AuthConfig),
CredentialsStore: "teststore",
},
shouldCreateFile: false,
},
{
name: "set credsStore in empty file",
fileName: "empty.json",
oldCfg: configtest.Config{},
newCfg: configtest.Config{
CredentialsStore: "teststore",
},
wantCfg: configtest.Config{
AuthConfigs: make(map[string]configtest.AuthConfig),
CredentialsStore: "teststore",
},
shouldCreateFile: true,
},
{
name: "set credsStore in a no-auth-configured file",
fileName: "empty.json",
oldCfg: configtest.Config{
SomeConfigField: 123,
},
newCfg: configtest.Config{
CredentialsStore: "teststore",
},
wantCfg: configtest.Config{
SomeConfigField: 123,
AuthConfigs: make(map[string]configtest.AuthConfig),
CredentialsStore: "teststore",
},
shouldCreateFile: true,
},
{
name: "Set credsStore and credHelpers in an auth-configured file",
fileName: "auth_configured.json",
oldCfg: configtest.Config{
SomeConfigField: 123,
AuthConfigs: map[string]configtest.AuthConfig{
"registry1.example.com": {
SomeAuthField: "something",
Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=",
},
},
CredentialsStore: "oldstore",
CredentialHelpers: map[string]string{
"registry2.example.com": "testhelper",
},
},
newCfg: configtest.Config{
AuthConfigs: make(map[string]configtest.AuthConfig),
SomeConfigField: 123,
CredentialsStore: "newstore",
CredentialHelpers: map[string]string{
"xxx": "yyy",
},
},
wantCfg: configtest.Config{
SomeConfigField: 123,
AuthConfigs: map[string]configtest.AuthConfig{
"registry1.example.com": {
SomeAuthField: "something",
Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=",
},
},
CredentialsStore: "newstore",
CredentialHelpers: map[string]string{
"registry2.example.com": "testhelper", // cred helpers will not be updated
},
},
shouldCreateFile: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// prepare test content
configPath := filepath.Join(tempDir, tt.fileName)
if tt.shouldCreateFile {
jsonStr, err := json.Marshal(tt.oldCfg)
if err != nil {
t.Fatalf("failed to marshal config: %v", err)
}
if err := os.WriteFile(configPath, jsonStr, 0666); err != nil {
t.Fatalf("failed to write config file: %v", err)
}
}
cfg, err := Load(configPath)
if err != nil {
t.Fatal("Load() error =", err)
}
cfg.credentialsStore = tt.newCfg.CredentialsStore
cfg.credentialHelpers = tt.newCfg.CredentialHelpers
if err := cfg.saveFile(); err != nil {
t.Fatal("saveFile() error =", err)
}
// verify config file
configFile, err := os.Open(configPath)
if err != nil {
t.Fatalf("failed to open config file: %v", err)
}
defer configFile.Close()
var gotCfg configtest.Config
if err := json.NewDecoder(configFile).Decode(&gotCfg); err != nil {
t.Fatalf("failed to decode config file: %v", err)
}
if !reflect.DeepEqual(gotCfg, tt.wantCfg) {
t.Errorf("Decoded config = %v, want %v", gotCfg, tt.wantCfg)
}
})
}
}
func Test_encodeAuth(t *testing.T) {
tests := []struct {
name string
username string
password string
want string
}{
{
name: "Username and password",
username: "username",
password: "password",
want: "dXNlcm5hbWU6cGFzc3dvcmQ=",
},
{
name: "Username only",
username: "username",
password: "",
want: "dXNlcm5hbWU6",
},
{
name: "Password only",
username: "",
password: "password",
want: "OnBhc3N3b3Jk",
},
{
name: "Empty username and empty password",
username: "",
password: "",
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := encodeAuth(tt.username, tt.password); got != tt.want {
t.Errorf("encodeAuth() = %v, want %v", got, tt.want)
}
})
}
}
func Test_decodeAuth(t *testing.T) {
tests := []struct {
name string
authStr string
username string
password string
wantErr bool
}{
{
name: "Valid base64",
authStr: "dXNlcm5hbWU6cGFzc3dvcmQ=", // username:password
username: "username",
password: "password",
},
{
name: "Valid base64, username only",
authStr: "dXNlcm5hbWU6", // username:
username: "username",
},
{
name: "Valid base64, password only",
authStr: "OnBhc3N3b3Jk", // :password
password: "password",
},
{
name: "Valid base64, bad format",
authStr: "d2hhdGV2ZXI=", // whatever
username: "",
password: "",
wantErr: true,
},
{
name: "Invalid base64",
authStr: "whatever",
username: "",
password: "",
wantErr: true,
},
{
name: "Empty string",
authStr: "",
username: "",
password: "",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotUsername, gotPassword, err := decodeAuth(tt.authStr)
if (err != nil) != tt.wantErr {
t.Errorf("decodeAuth() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotUsername != tt.username {
t.Errorf("decodeAuth() got = %v, want %v", gotUsername, tt.username)
}
if gotPassword != tt.password {
t.Errorf("decodeAuth() got1 = %v, want %v", gotPassword, tt.password)
}
})
}
}
func Test_toHostname(t *testing.T) {
tests := []struct {
name string
addr string
want string
}{
{
addr: "http://test.example.com",
want: "test.example.com",
},
{
addr: "http://test.example.com/",
want: "test.example.com",
},
{
addr: "http://test.example.com/foo/bar",
want: "test.example.com",
},
{
addr: "https://test.example.com",
want: "test.example.com",
},
{
addr: "https://test.example.com/",
want: "test.example.com",
},
{
addr: "http://test.example.com/foo/bar",
want: "test.example.com",
},
{
addr: "test.example.com",
want: "test.example.com",
},
{
addr: "test.example.com/foo/bar/",
want: "test.example.com",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := toHostname(tt.addr); got != tt.want {
t.Errorf("toHostname() = %v, want %v", got, tt.want)
}
})
}
}
func TestConfig_Path(t *testing.T) {
mockedPath := "/path/to/config.json"
config := Config{
path: mockedPath,
}
if got := config.Path(); got != mockedPath {
t.Errorf("Config.Path() = %v, want %v", got, mockedPath)
}
}
oras-go-2.5.0/registry/remote/credentials/internal/config/configtest/ 0000775 0000000 0000000 00000000000 14576745303 0025771 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/registry/remote/credentials/internal/config/configtest/config.go 0000664 0000000 0000000 00000003003 14576745303 0027561 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 configtest
// Config represents the structure of a config file for testing purpose.
type Config struct {
AuthConfigs map[string]AuthConfig `json:"auths"`
CredentialsStore string `json:"credsStore,omitempty"`
CredentialHelpers map[string]string `json:"credHelpers,omitempty"`
SomeConfigField int `json:"some_config_field"`
}
// AuthConfig represents the structure of the "auths" field of a config file
// for testing purpose.
type AuthConfig struct {
SomeAuthField string `json:"some_auth_field,omitempty"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Auth string `json:"auth,omitempty"`
// IdentityToken is used to authenticate the user and get
// an access token for the registry.
IdentityToken string `json:"identitytoken,omitempty"`
// RegistryToken is a bearer token to be sent to a registry
RegistryToken string `json:"registrytoken,omitempty"`
}
oras-go-2.5.0/registry/remote/credentials/internal/executer/ 0000775 0000000 0000000 00000000000 14576745303 0024203 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/registry/remote/credentials/internal/executer/executer.go 0000664 0000000 0000000 00000004554 14576745303 0026366 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 executer is an abstraction for the docker credential helper protocol
// binaries. It is used by nativeStore to interact with installed binaries.
package executer
import (
"bytes"
"context"
"errors"
"io"
"os"
"os/exec"
"oras.land/oras-go/v2/registry/remote/credentials/trace"
)
// dockerDesktopHelperName is the name of the docker credentials helper
// execuatable.
const dockerDesktopHelperName = "docker-credential-desktop.exe"
// Executer is an interface that simulates an executable binary.
type Executer interface {
Execute(ctx context.Context, input io.Reader, action string) ([]byte, error)
}
// executable implements the Executer interface.
type executable struct {
name string
}
// New returns a new Executer instance.
func New(name string) Executer {
return &executable{
name: name,
}
}
// Execute operates on an executable binary and supports context.
func (c *executable) Execute(ctx context.Context, input io.Reader, action string) ([]byte, error) {
cmd := exec.CommandContext(ctx, c.name, action)
cmd.Stdin = input
cmd.Stderr = os.Stderr
trace := trace.ContextExecutableTrace(ctx)
if trace != nil && trace.ExecuteStart != nil {
trace.ExecuteStart(c.name, action)
}
output, err := cmd.Output()
if trace != nil && trace.ExecuteDone != nil {
trace.ExecuteDone(c.name, action, err)
}
if err != nil {
switch execErr := err.(type) {
case *exec.ExitError:
if errMessage := string(bytes.TrimSpace(output)); errMessage != "" {
return nil, errors.New(errMessage)
}
case *exec.Error:
// check if the error is caused by Docker Desktop not running
if execErr.Err == exec.ErrNotFound && c.name == dockerDesktopHelperName {
return nil, errors.New("credentials store is configured to `desktop.exe` but Docker Desktop seems not running")
}
}
return nil, err
}
return output, nil
}
oras-go-2.5.0/registry/remote/credentials/internal/ioutil/ 0000775 0000000 0000000 00000000000 14576745303 0023664 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/registry/remote/credentials/internal/ioutil/ioutil.go 0000664 0000000 0000000 00000002652 14576745303 0025525 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 ioutil
import (
"fmt"
"io"
"os"
)
// Ingest writes content into a temporary ingest file with the file name format
// "oras_credstore_temp_{randomString}".
func Ingest(dir string, content io.Reader) (path string, ingestErr error) {
tempFile, err := os.CreateTemp(dir, "oras_credstore_temp_*")
if err != nil {
return "", fmt.Errorf("failed to create ingest file: %w", err)
}
path = tempFile.Name()
defer func() {
if err := tempFile.Close(); err != nil && ingestErr == nil {
ingestErr = fmt.Errorf("failed to close ingest file: %w", err)
}
// remove the temp file in case of error.
if ingestErr != nil {
os.Remove(path)
}
}()
if err := tempFile.Chmod(0600); err != nil {
return "", fmt.Errorf("failed to ensure permission: %w", err)
}
if _, err := io.Copy(tempFile, content); err != nil {
return "", fmt.Errorf("failed to ingest: %w", err)
}
return
}
oras-go-2.5.0/registry/remote/credentials/memory_store.go 0000664 0000000 0000000 00000003135 14576745303 0023620 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 credentials
import (
"context"
"sync"
"oras.land/oras-go/v2/registry/remote/auth"
)
// memoryStore is a store that keeps credentials in memory.
type memoryStore struct {
store sync.Map
}
// NewMemoryStore creates a new in-memory credentials store.
func NewMemoryStore() Store {
return &memoryStore{}
}
// Get retrieves credentials from the store for the given server address.
func (ms *memoryStore) Get(_ context.Context, serverAddress string) (auth.Credential, error) {
cred, found := ms.store.Load(serverAddress)
if !found {
return auth.EmptyCredential, nil
}
return cred.(auth.Credential), nil
}
// Put saves credentials into the store for the given server address.
func (ms *memoryStore) Put(_ context.Context, serverAddress string, cred auth.Credential) error {
ms.store.Store(serverAddress, cred)
return nil
}
// Delete removes credentials from the store for the given server address.
func (ms *memoryStore) Delete(_ context.Context, serverAddress string) error {
ms.store.Delete(serverAddress)
return nil
}
oras-go-2.5.0/registry/remote/credentials/memory_store_test.go 0000664 0000000 0000000 00000013033 14576745303 0024655 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 credentials
import (
"context"
"reflect"
"testing"
"oras.land/oras-go/v2/registry/remote/auth"
)
func TestMemoryStore_Get_notExistRecord(t *testing.T) {
ctx := context.Background()
ms := NewMemoryStore()
serverAddress := "registry.example.com"
got, err := ms.Get(ctx, serverAddress)
if err != nil {
t.Errorf("MemoryStore.Get() error = %v", err)
return
}
if !reflect.DeepEqual(got, auth.EmptyCredential) {
t.Errorf("MemoryStore.Get() = %v, want %v", got, auth.EmptyCredential)
}
}
func TestMemoryStore_Get_validRecord(t *testing.T) {
ctx := context.Background()
ms := NewMemoryStore().(*memoryStore)
serverAddress := "registry.example.com"
want := auth.Credential{
Username: "username",
Password: "password",
RefreshToken: "identity_token",
AccessToken: "registry_token",
}
ms.store.Store(serverAddress, want)
got, err := ms.Get(ctx, serverAddress)
if err != nil {
t.Errorf("MemoryStore.Get() error = %v", err)
return
}
if !reflect.DeepEqual(got, want) {
t.Errorf("MemoryStore.Get() = %v, want %v", got, want)
}
}
func TestMemoryStore_Put_addNew(t *testing.T) {
ctx := context.Background()
ms := NewMemoryStore()
// Test Put
server1 := "registry.example.com"
cred1 := auth.Credential{
Username: "username",
Password: "password",
RefreshToken: "identity_token",
AccessToken: "registry_token",
}
if err := ms.Put(ctx, server1, cred1); err != nil {
t.Errorf("MemoryStore.Put() error = %v", err)
return
}
server2 := "registry2.example.com"
cred2 := auth.Credential{
Username: "username2",
Password: "password2",
RefreshToken: "identity_token2",
AccessToken: "registry_token2",
}
if err := ms.Put(ctx, server2, cred2); err != nil {
t.Errorf("MemoryStore.Put() error = %v", err)
return
}
// Verify Content
got1, err := ms.Get(ctx, server1)
if err != nil {
t.Errorf("MemoryStore.Get() error = %v", err)
return
}
if !reflect.DeepEqual(got1, cred1) {
t.Errorf("MemoryStore.Get() = %v, want %v", got1, cred1)
return
}
got2, err := ms.Get(ctx, server2)
if err != nil {
t.Errorf("MemoryStore.Get() error = %v", err)
return
}
if !reflect.DeepEqual(got2, cred2) {
t.Errorf("MemoryStore.Get() = %v, want %v", got2, cred2)
return
}
}
func TestMemoryStore_Put_update(t *testing.T) {
ctx := context.Background()
ms := NewMemoryStore()
// Test Put
serverAddress := "registry.example.com"
cred1 := auth.Credential{
Username: "username",
Password: "password",
RefreshToken: "identity_token",
AccessToken: "registry_token",
}
if err := ms.Put(ctx, serverAddress, cred1); err != nil {
t.Errorf("MemoryStore.Put() error = %v", err)
return
}
cred2 := auth.Credential{
Username: "username2",
Password: "password2",
RefreshToken: "identity_token2",
AccessToken: "registry_token2",
}
if err := ms.Put(ctx, serverAddress, cred2); err != nil {
t.Errorf("MemoryStore.Put() error = %v", err)
return
}
got, err := ms.Get(ctx, serverAddress)
if err != nil {
t.Errorf("MemoryStore.Get() error = %v", err)
return
}
if !reflect.DeepEqual(got, cred2) {
t.Errorf("MemoryStore.Get() = %v, want %v", got, cred2)
return
}
}
func TestMemoryStore_Delete_existRecord(t *testing.T) {
ctx := context.Background()
ms := NewMemoryStore()
// Test Put
serverAddress := "registry.example.com"
cred := auth.Credential{
Username: "username",
Password: "password",
RefreshToken: "identity_token",
AccessToken: "registry_token",
}
if err := ms.Put(ctx, serverAddress, cred); err != nil {
t.Errorf("MemoryStore.Put() error = %v", err)
return
}
// Test Get
got, err := ms.Get(ctx, serverAddress)
if err != nil {
t.Errorf("MemoryStore.Get() error = %v", err)
return
}
if !reflect.DeepEqual(got, cred) {
t.Errorf("MemoryStore.Get(%s) = %v, want %v", serverAddress, got, cred)
return
}
// Test Delete
if err := ms.Delete(ctx, serverAddress); err != nil {
t.Errorf("MemoryStore.Delete() error = %v", err)
return
}
// Test Get again
got, err = ms.Get(ctx, serverAddress)
if err != nil {
t.Errorf("MemoryStore.Get() error = %v", err)
return
}
if !reflect.DeepEqual(got, auth.EmptyCredential) {
t.Errorf("MemoryStore.Get() = %v, want %v", got, auth.EmptyCredential)
return
}
}
func TestMemoryStore_Delete_notExistRecord(t *testing.T) {
ctx := context.Background()
ms := NewMemoryStore()
// Test Put
serverAddress := "registry.example.com"
cred := auth.Credential{
Username: "username",
Password: "password",
RefreshToken: "identity_token",
AccessToken: "registry_token",
}
if err := ms.Put(ctx, serverAddress, cred); err != nil {
t.Errorf("MemoryStore.Put() error = %v", err)
return
}
// Test Delete
if err := ms.Delete(ctx, serverAddress); err != nil {
t.Errorf("MemoryStore.Delete() error = %v", err)
return
}
// Test Delete again
// Expect no error if target record does not exist
if err := ms.Delete(ctx, serverAddress); err != nil {
t.Errorf("MemoryStore.Delete() error = %v", err)
return
}
}
oras-go-2.5.0/registry/remote/credentials/native_store.go 0000664 0000000 0000000 00000010536 14576745303 0023601 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 credentials
import (
"bytes"
"context"
"encoding/json"
"os/exec"
"strings"
"oras.land/oras-go/v2/registry/remote/auth"
"oras.land/oras-go/v2/registry/remote/credentials/internal/executer"
)
const (
remoteCredentialsPrefix = "docker-credential-"
emptyUsername = ""
errCredentialsNotFoundMessage = "credentials not found in native keychain"
)
// dockerCredentials mimics how docker credential helper binaries store
// credential information.
// Reference:
// - https://docs.docker.com/engine/reference/commandline/login/#credential-helper-protocol
type dockerCredentials struct {
ServerURL string `json:"ServerURL"`
Username string `json:"Username"`
Secret string `json:"Secret"`
}
// nativeStore implements a credentials store using native keychain to keep
// credentials secure.
type nativeStore struct {
exec executer.Executer
}
// NewNativeStore creates a new native store that uses a remote helper program to
// manage credentials.
//
// The argument of NewNativeStore can be the native keychains
// ("wincred" for Windows, "pass" for linux and "osxkeychain" for macOS),
// or any program that follows the docker-credentials-helper protocol.
//
// Reference:
// - https://docs.docker.com/engine/reference/commandline/login#credentials-store
func NewNativeStore(helperSuffix string) Store {
return &nativeStore{
exec: executer.New(remoteCredentialsPrefix + helperSuffix),
}
}
// NewDefaultNativeStore returns a native store based on the platform-default
// docker credentials helper and a bool indicating if the native store is
// available.
// - Windows: "wincred"
// - Linux: "pass" or "secretservice"
// - macOS: "osxkeychain"
//
// Reference:
// - https://docs.docker.com/engine/reference/commandline/login/#credentials-store
func NewDefaultNativeStore() (Store, bool) {
if helper := getDefaultHelperSuffix(); helper != "" {
return NewNativeStore(helper), true
}
return nil, false
}
// Get retrieves credentials from the store for the given server.
func (ns *nativeStore) Get(ctx context.Context, serverAddress string) (auth.Credential, error) {
var cred auth.Credential
out, err := ns.exec.Execute(ctx, strings.NewReader(serverAddress), "get")
if err != nil {
if err.Error() == errCredentialsNotFoundMessage {
// do not return an error if the credentials are not in the keychain.
return auth.EmptyCredential, nil
}
return auth.EmptyCredential, err
}
var dockerCred dockerCredentials
if err := json.Unmarshal(out, &dockerCred); err != nil {
return auth.EmptyCredential, err
}
// bearer auth is used if the username is ""
if dockerCred.Username == emptyUsername {
cred.RefreshToken = dockerCred.Secret
} else {
cred.Username = dockerCred.Username
cred.Password = dockerCred.Secret
}
return cred, nil
}
// Put saves credentials into the store.
func (ns *nativeStore) Put(ctx context.Context, serverAddress string, cred auth.Credential) error {
dockerCred := &dockerCredentials{
ServerURL: serverAddress,
Username: cred.Username,
Secret: cred.Password,
}
if cred.RefreshToken != "" {
dockerCred.Username = emptyUsername
dockerCred.Secret = cred.RefreshToken
}
credJSON, err := json.Marshal(dockerCred)
if err != nil {
return err
}
_, err = ns.exec.Execute(ctx, bytes.NewReader(credJSON), "store")
return err
}
// Delete removes credentials from the store for the given server.
func (ns *nativeStore) Delete(ctx context.Context, serverAddress string) error {
_, err := ns.exec.Execute(ctx, strings.NewReader(serverAddress), "erase")
return err
}
// getDefaultHelperSuffix returns the default credential helper suffix.
func getDefaultHelperSuffix() string {
platformDefault := getPlatformDefaultHelperSuffix()
if _, err := exec.LookPath(remoteCredentialsPrefix + platformDefault); err == nil {
return platformDefault
}
return ""
}
oras-go-2.5.0/registry/remote/credentials/native_store_darwin.go 0000664 0000000 0000000 00000001476 14576745303 0025150 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 credentials
// getPlatformDefaultHelperSuffix returns the platform default credential
// helper suffix.
// Reference: https://docs.docker.com/engine/reference/commandline/login/#default-behavior
func getPlatformDefaultHelperSuffix() string {
return "osxkeychain"
}
oras-go-2.5.0/registry/remote/credentials/native_store_generic.go 0000664 0000000 0000000 00000001535 14576745303 0025274 0 ustar 00root root 0000000 0000000 //go:build !windows && !darwin && !linux
/*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 credentials
// getPlatformDefaultHelperSuffix returns the platform default credential
// helper suffix.
// Reference: https://docs.docker.com/engine/reference/commandline/login/#default-behavior
func getPlatformDefaultHelperSuffix() string {
return ""
}
oras-go-2.5.0/registry/remote/credentials/native_store_linux.go 0000664 0000000 0000000 00000001630 14576745303 0025013 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 credentials
import "os/exec"
// getPlatformDefaultHelperSuffix returns the platform default credential
// helper suffix.
// Reference: https://docs.docker.com/engine/reference/commandline/login/#default-behavior
func getPlatformDefaultHelperSuffix() string {
if _, err := exec.LookPath("pass"); err == nil {
return "pass"
}
return "secretservice"
}
oras-go-2.5.0/registry/remote/credentials/native_store_test.go 0000664 0000000 0000000 00000030161 14576745303 0024634 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 credentials
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"strings"
"testing"
"oras.land/oras-go/v2/registry/remote/auth"
"oras.land/oras-go/v2/registry/remote/credentials/trace"
)
const (
basicAuthHost = "localhost:2333"
bearerAuthHost = "localhost:666"
exeErrorHost = "localhost:500/exeError"
jsonErrorHost = "localhost:500/jsonError"
noCredentialsHost = "localhost:404"
traceHost = "localhost:808"
testUsername = "test_username"
testPassword = "test_password"
testRefreshToken = "test_token"
)
var (
errCommandExited = fmt.Errorf("exited with error")
errExecute = fmt.Errorf("Execute failed")
errCredentialsNotFound = fmt.Errorf(errCredentialsNotFoundMessage)
)
// testExecuter implements the Executer interface for testing purpose.
// It simulates interactions between the docker client and a remote
// credentials helper.
type testExecuter struct{}
// Execute mocks the behavior of a credential helper binary. It returns responses
// and errors based on the input.
func (e *testExecuter) Execute(ctx context.Context, input io.Reader, action string) ([]byte, error) {
in, err := io.ReadAll(input)
if err != nil {
return nil, err
}
inS := string(in)
switch action {
case "get":
switch inS {
case basicAuthHost:
return []byte(`{"Username": "test_username", "Secret": "test_password"}`), nil
case bearerAuthHost:
return []byte(`{"Username": "", "Secret": "test_token"}`), nil
case exeErrorHost:
return []byte("Execute failed"), errExecute
case jsonErrorHost:
return []byte("json.Unmarshal failed"), nil
case noCredentialsHost:
return []byte("credentials not found"), errCredentialsNotFound
case traceHost:
traceHook := trace.ContextExecutableTrace(ctx)
if traceHook != nil {
if traceHook.ExecuteStart != nil {
traceHook.ExecuteStart("testExecuter", "get")
}
if traceHook.ExecuteDone != nil {
traceHook.ExecuteDone("testExecuter", "get", nil)
}
}
return []byte(`{"Username": "test_username", "Secret": "test_password"}`), nil
default:
return []byte("program failed"), errCommandExited
}
case "store":
var c dockerCredentials
err := json.NewDecoder(strings.NewReader(inS)).Decode(&c)
if err != nil {
return []byte("program failed"), errCommandExited
}
switch c.ServerURL {
case basicAuthHost, bearerAuthHost, exeErrorHost:
return nil, nil
case traceHost:
traceHook := trace.ContextExecutableTrace(ctx)
if traceHook != nil {
if traceHook.ExecuteStart != nil {
traceHook.ExecuteStart("testExecuter", "store")
}
if traceHook.ExecuteDone != nil {
traceHook.ExecuteDone("testExecuter", "store", nil)
}
}
return nil, nil
default:
return []byte("program failed"), errCommandExited
}
case "erase":
switch inS {
case basicAuthHost, bearerAuthHost:
return nil, nil
case traceHost:
traceHook := trace.ContextExecutableTrace(ctx)
if traceHook != nil {
if traceHook.ExecuteStart != nil {
traceHook.ExecuteStart("testExecuter", "erase")
}
if traceHook.ExecuteDone != nil {
traceHook.ExecuteDone("testExecuter", "erase", nil)
}
}
return nil, nil
default:
return []byte("program failed"), errCommandExited
}
}
return []byte(fmt.Sprintf("unknown argument %q with %q", action, inS)), errCommandExited
}
func TestNativeStore_interface(t *testing.T) {
var ns interface{} = &nativeStore{}
if _, ok := ns.(Store); !ok {
t.Error("&NativeStore{} does not conform Store")
}
}
func TestNativeStore_basicAuth(t *testing.T) {
ns := &nativeStore{
&testExecuter{},
}
// Put
err := ns.Put(context.Background(), basicAuthHost, auth.Credential{Username: testUsername, Password: testPassword})
if err != nil {
t.Fatalf("basic auth test ns.Put fails: %v", err)
}
// Get
cred, err := ns.Get(context.Background(), basicAuthHost)
if err != nil {
t.Fatalf("basic auth test ns.Get fails: %v", err)
}
if cred.Username != testUsername {
t.Fatal("incorrect username")
}
if cred.Password != testPassword {
t.Fatal("incorrect password")
}
// Delete
err = ns.Delete(context.Background(), basicAuthHost)
if err != nil {
t.Fatalf("basic auth test ns.Delete fails: %v", err)
}
}
func TestNativeStore_refreshToken(t *testing.T) {
ns := &nativeStore{
&testExecuter{},
}
// Put
err := ns.Put(context.Background(), bearerAuthHost, auth.Credential{RefreshToken: testRefreshToken})
if err != nil {
t.Fatalf("refresh token test ns.Put fails: %v", err)
}
// Get
cred, err := ns.Get(context.Background(), bearerAuthHost)
if err != nil {
t.Fatalf("refresh token test ns.Get fails: %v", err)
}
if cred.Username != "" {
t.Fatalf("expect username to be empty, got %s", cred.Username)
}
if cred.RefreshToken != testRefreshToken {
t.Fatal("incorrect refresh token")
}
// Delete
err = ns.Delete(context.Background(), basicAuthHost)
if err != nil {
t.Fatalf("refresh token test ns.Delete fails: %v", err)
}
}
func TestNativeStore_errorHandling(t *testing.T) {
ns := &nativeStore{
&testExecuter{},
}
// Get Error: Execute error
_, err := ns.Get(context.Background(), exeErrorHost)
if err != errExecute {
t.Fatalf("got error: %v, should get exeErr", err)
}
// Get Error: json.Unmarshal
_, err = ns.Get(context.Background(), jsonErrorHost)
if err == nil {
t.Fatalf("should get error from json.Unmarshal")
}
// Get: Should not return error when credentials are not found
_, err = ns.Get(context.Background(), noCredentialsHost)
if err != nil {
t.Fatalf("should not get error when no credentials are found")
}
}
func TestNewDefaultNativeStore(t *testing.T) {
defaultHelper := getDefaultHelperSuffix()
wantOK := (defaultHelper != "")
if _, ok := NewDefaultNativeStore(); ok != wantOK {
t.Errorf("NewDefaultNativeStore() = %v, want %v", ok, wantOK)
}
}
func TestNativeStore_trace(t *testing.T) {
ns := &nativeStore{
&testExecuter{},
}
// create trace hooks that write to buffer
buffer := bytes.Buffer{}
traceHook := &trace.ExecutableTrace{
ExecuteStart: func(executableName string, action string) {
buffer.WriteString(fmt.Sprintf("test trace, start the execution of executable %s with action %s ", executableName, action))
},
ExecuteDone: func(executableName string, action string, err error) {
buffer.WriteString(fmt.Sprintf("test trace, completed the execution of executable %s with action %s and got err %v", executableName, action, err))
},
}
ctx := trace.WithExecutableTrace(context.Background(), traceHook)
// Test ns.Put trace
err := ns.Put(ctx, traceHost, auth.Credential{Username: testUsername, Password: testPassword})
if err != nil {
t.Fatalf("trace test ns.Put fails: %v", err)
}
bufferContent := buffer.String()
if bufferContent != "test trace, start the execution of executable testExecuter with action store test trace, completed the execution of executable testExecuter with action store and got err " {
t.Fatalf("incorrect buffer content: %s", bufferContent)
}
buffer.Reset()
// Test ns.Get trace
_, err = ns.Get(ctx, traceHost)
if err != nil {
t.Fatalf("trace test ns.Get fails: %v", err)
}
bufferContent = buffer.String()
if bufferContent != "test trace, start the execution of executable testExecuter with action get test trace, completed the execution of executable testExecuter with action get and got err " {
t.Fatalf("incorrect buffer content: %s", bufferContent)
}
buffer.Reset()
// Test ns.Delete trace
err = ns.Delete(ctx, traceHost)
if err != nil {
t.Fatalf("trace test ns.Delete fails: %v", err)
}
bufferContent = buffer.String()
if bufferContent != "test trace, start the execution of executable testExecuter with action erase test trace, completed the execution of executable testExecuter with action erase and got err " {
t.Fatalf("incorrect buffer content: %s", bufferContent)
}
}
// This test ensures that a nil trace will not cause an error.
func TestNativeStore_noTrace(t *testing.T) {
ns := &nativeStore{
&testExecuter{},
}
// Put
err := ns.Put(context.Background(), traceHost, auth.Credential{Username: testUsername, Password: testPassword})
if err != nil {
t.Fatalf("basic auth test ns.Put fails: %v", err)
}
// Get
cred, err := ns.Get(context.Background(), traceHost)
if err != nil {
t.Fatalf("basic auth test ns.Get fails: %v", err)
}
if cred.Username != testUsername {
t.Fatal("incorrect username")
}
if cred.Password != testPassword {
t.Fatal("incorrect password")
}
// Delete
err = ns.Delete(context.Background(), traceHost)
if err != nil {
t.Fatalf("basic auth test ns.Delete fails: %v", err)
}
}
// This test ensures that an empty trace will not cause an error.
func TestNativeStore_emptyTrace(t *testing.T) {
ns := &nativeStore{
&testExecuter{},
}
traceHook := &trace.ExecutableTrace{}
ctx := trace.WithExecutableTrace(context.Background(), traceHook)
// Put
err := ns.Put(ctx, traceHost, auth.Credential{Username: testUsername, Password: testPassword})
if err != nil {
t.Fatalf("basic auth test ns.Put fails: %v", err)
}
// Get
cred, err := ns.Get(ctx, traceHost)
if err != nil {
t.Fatalf("basic auth test ns.Get fails: %v", err)
}
if cred.Username != testUsername {
t.Fatal("incorrect username")
}
if cred.Password != testPassword {
t.Fatal("incorrect password")
}
// Delete
err = ns.Delete(ctx, traceHost)
if err != nil {
t.Fatalf("basic auth test ns.Delete fails: %v", err)
}
}
func TestNativeStore_multipleTrace(t *testing.T) {
ns := &nativeStore{
&testExecuter{},
}
// create trace hooks that write to buffer
buffer := bytes.Buffer{}
trace1 := &trace.ExecutableTrace{
ExecuteStart: func(executableName string, action string) {
buffer.WriteString(fmt.Sprintf("trace 1 start %s, %s ", executableName, action))
},
ExecuteDone: func(executableName string, action string, err error) {
buffer.WriteString(fmt.Sprintf("trace 1 done %s, %s, %v ", executableName, action, err))
},
}
ctx := context.Background()
ctx = trace.WithExecutableTrace(ctx, trace1)
trace2 := &trace.ExecutableTrace{
ExecuteStart: func(executableName string, action string) {
buffer.WriteString(fmt.Sprintf("trace 2 start %s, %s ", executableName, action))
},
ExecuteDone: func(executableName string, action string, err error) {
buffer.WriteString(fmt.Sprintf("trace 2 done %s, %s, %v ", executableName, action, err))
},
}
ctx = trace.WithExecutableTrace(ctx, trace2)
trace3 := &trace.ExecutableTrace{}
ctx = trace.WithExecutableTrace(ctx, trace3)
// Test ns.Put trace
err := ns.Put(ctx, traceHost, auth.Credential{Username: testUsername, Password: testPassword})
if err != nil {
t.Fatalf("trace test ns.Put fails: %v", err)
}
bufferContent := buffer.String()
if bufferContent != "trace 2 start testExecuter, store trace 1 start testExecuter, store trace 2 done testExecuter, store, trace 1 done testExecuter, store, " {
t.Fatalf("incorrect buffer content: %s", bufferContent)
}
buffer.Reset()
// Test ns.Get trace
_, err = ns.Get(ctx, traceHost)
if err != nil {
t.Fatalf("trace test ns.Get fails: %v", err)
}
bufferContent = buffer.String()
if bufferContent != "trace 2 start testExecuter, get trace 1 start testExecuter, get trace 2 done testExecuter, get, trace 1 done testExecuter, get, " {
t.Fatalf("incorrect buffer content: %s", bufferContent)
}
buffer.Reset()
// Test ns.Delete trace
err = ns.Delete(ctx, traceHost)
if err != nil {
t.Fatalf("trace test ns.Delete fails: %v", err)
}
bufferContent = buffer.String()
if bufferContent != "trace 2 start testExecuter, erase trace 1 start testExecuter, erase trace 2 done testExecuter, erase, trace 1 done testExecuter, erase, " {
t.Fatalf("incorrect buffer content: %s", bufferContent)
}
}
oras-go-2.5.0/registry/remote/credentials/native_store_windows.go 0000664 0000000 0000000 00000001472 14576745303 0025352 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 credentials
// getPlatformDefaultHelperSuffix returns the platform default credential
// helper suffix.
// Reference: https://docs.docker.com/engine/reference/commandline/login/#default-behavior
func getPlatformDefaultHelperSuffix() string {
return "wincred"
}
oras-go-2.5.0/registry/remote/credentials/registry.go 0000664 0000000 0000000 00000007425 14576745303 0022752 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 credentials
import (
"context"
"errors"
"fmt"
"oras.land/oras-go/v2/registry/remote"
"oras.land/oras-go/v2/registry/remote/auth"
)
// ErrClientTypeUnsupported is thrown by Login() when the registry's client type
// is not supported.
var ErrClientTypeUnsupported = errors.New("client type not supported")
// Login provides the login functionality with the given credentials. The target
// registry's client should be nil or of type *auth.Client. Login uses
// a client local to the function and will not modify the original client of
// the registry.
func Login(ctx context.Context, store Store, reg *remote.Registry, cred auth.Credential) error {
// create a clone of the original registry for login purpose
regClone := *reg
// we use the original client if applicable, otherwise use a default client
var authClient auth.Client
if reg.Client == nil {
authClient = *auth.DefaultClient
authClient.Cache = nil // no cache
} else if client, ok := reg.Client.(*auth.Client); ok {
authClient = *client
} else {
return ErrClientTypeUnsupported
}
regClone.Client = &authClient
// update credentials with the client
authClient.Credential = auth.StaticCredential(reg.Reference.Registry, cred)
// validate and store the credential
if err := regClone.Ping(ctx); err != nil {
return fmt.Errorf("failed to validate the credentials for %s: %w", regClone.Reference.Registry, err)
}
hostname := ServerAddressFromRegistry(regClone.Reference.Registry)
if err := store.Put(ctx, hostname, cred); err != nil {
return fmt.Errorf("failed to store the credentials for %s: %w", hostname, err)
}
return nil
}
// Logout provides the logout functionality given the registry name.
func Logout(ctx context.Context, store Store, registryName string) error {
registryName = ServerAddressFromRegistry(registryName)
if err := store.Delete(ctx, registryName); err != nil {
return fmt.Errorf("failed to delete the credential for %s: %w", registryName, err)
}
return nil
}
// Credential returns a Credential() function that can be used by auth.Client.
func Credential(store Store) auth.CredentialFunc {
return func(ctx context.Context, hostport string) (auth.Credential, error) {
hostport = ServerAddressFromHostname(hostport)
if hostport == "" {
return auth.EmptyCredential, nil
}
return store.Get(ctx, hostport)
}
}
// ServerAddressFromRegistry maps a registry to a server address, which is used as
// a key for credentials store. The Docker CLI expects that the credentials of
// the registry 'docker.io' will be added under the key "https://index.docker.io/v1/".
// See: https://github.com/moby/moby/blob/v24.0.2/registry/config.go#L25-L48
func ServerAddressFromRegistry(registry string) string {
if registry == "docker.io" {
return "https://index.docker.io/v1/"
}
return registry
}
// ServerAddressFromHostname maps a hostname to a server address, which is used as
// a key for credentials store. It is expected that the traffic targetting the
// host "registry-1.docker.io" will be redirected to "https://index.docker.io/v1/".
// See: https://github.com/moby/moby/blob/v24.0.2/registry/config.go#L25-L48
func ServerAddressFromHostname(hostname string) string {
if hostname == "registry-1.docker.io" {
return "https://index.docker.io/v1/"
}
return hostname
}
oras-go-2.5.0/registry/remote/credentials/registry_test.go 0000664 0000000 0000000 00000015227 14576745303 0024010 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 credentials
import (
"context"
"encoding/base64"
"errors"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"testing"
"oras.land/oras-go/v2/registry/remote"
"oras.land/oras-go/v2/registry/remote/auth"
)
// testStore implements the Store interface, used for testing purpose.
type testStore struct {
storage map[string]auth.Credential
}
func (t *testStore) Get(ctx context.Context, serverAddress string) (auth.Credential, error) {
return t.storage[serverAddress], nil
}
func (t *testStore) Put(ctx context.Context, serverAddress string, cred auth.Credential) error {
if len(t.storage) == 0 {
t.storage = make(map[string]auth.Credential)
}
t.storage[serverAddress] = cred
return nil
}
func (t *testStore) Delete(ctx context.Context, serverAddress string) error {
delete(t.storage, serverAddress)
return nil
}
func TestLogin(t *testing.T) {
// create a test registry
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
wantedAuthHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte(testUsername+":"+testPassword))
authHeader := r.Header.Get("Authorization")
if authHeader != wantedAuthHeader {
w.Header().Set("Www-Authenticate", `Basic realm="Test Server"`)
w.WriteHeader(http.StatusUnauthorized)
}
}))
defer ts.Close()
uri, _ := url.Parse(ts.URL)
reg, err := remote.NewRegistry(uri.Host)
if err != nil {
t.Fatalf("cannot create test registry: %v", err)
}
reg.PlainHTTP = true
// create a test store
s := &testStore{}
tests := []struct {
name string
ctx context.Context
registry *remote.Registry
cred auth.Credential
wantErr bool
}{
{
name: "login succeeds",
ctx: context.Background(),
cred: auth.Credential{Username: testUsername, Password: testPassword},
wantErr: false,
},
{
name: "login fails (incorrect password)",
ctx: context.Background(),
cred: auth.Credential{Username: testUsername, Password: "whatever"},
wantErr: true,
},
{
name: "login fails (nil context makes remote.Ping fails)",
ctx: nil,
cred: auth.Credential{Username: testUsername, Password: testPassword},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// login to test registry
err := Login(tt.ctx, s, reg, tt.cred)
if (err != nil) != tt.wantErr {
t.Fatalf("Login() error = %v, wantErr %v", err, tt.wantErr)
}
if err != nil {
return
}
if got := s.storage[reg.Reference.Registry]; !reflect.DeepEqual(got, tt.cred) {
t.Fatalf("Stored credential = %v, want %v", got, tt.cred)
}
s.Delete(tt.ctx, reg.Reference.Registry)
})
}
}
func TestLogin_unsupportedClient(t *testing.T) {
var testClient http.Client
reg, err := remote.NewRegistry("whatever")
if err != nil {
t.Fatalf("cannot create test registry: %v", err)
}
reg.PlainHTTP = true
reg.Client = &testClient
ctx := context.Background()
s := &testStore{}
cred := auth.EmptyCredential
err = Login(ctx, s, reg, cred)
if wantErr := ErrClientTypeUnsupported; !errors.Is(err, wantErr) {
t.Errorf("Login() error = %v, wantErr %v", err, wantErr)
}
}
func TestLogout(t *testing.T) {
// create a test store
s := &testStore{}
s.storage = map[string]auth.Credential{
"localhost:2333": {Username: "test_user", Password: "test_word"},
"https://index.docker.io/v1/": {Username: "user", Password: "word"},
}
tests := []struct {
name string
ctx context.Context
store Store
registryName string
wantErr bool
}{
{
name: "logout of regular registry",
ctx: context.Background(),
registryName: "localhost:2333",
wantErr: false,
},
{
name: "logout of docker.io",
ctx: context.Background(),
registryName: "docker.io",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := Logout(tt.ctx, s, tt.registryName); (err != nil) != tt.wantErr {
t.Fatalf("Logout() error = %v, wantErr %v", err, tt.wantErr)
}
if s.storage[tt.registryName] != auth.EmptyCredential {
t.Error("Credentials are not deleted")
}
})
}
}
func Test_mapHostname(t *testing.T) {
tests := []struct {
name string
host string
want string
}{
{
name: "map docker.io to https://index.docker.io/v1/",
host: "docker.io",
want: "https://index.docker.io/v1/",
},
{
name: "do not map other host names",
host: "localhost:2333",
want: "localhost:2333",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ServerAddressFromRegistry(tt.host); got != tt.want {
t.Errorf("mapHostname() = %v, want %v", got, tt.want)
}
})
}
}
func TestCredential(t *testing.T) {
// create a test store
s := &testStore{}
s.storage = map[string]auth.Credential{
"localhost:2333": {Username: "test_user", Password: "test_word"},
"https://index.docker.io/v1/": {Username: "user", Password: "word"},
}
// create a test client using Credential
testClient := &auth.Client{}
testClient.Credential = Credential(s)
tests := []struct {
name string
registry string
wantCredential auth.Credential
}{
{
name: "get credentials for localhost:2333",
registry: "localhost:2333",
wantCredential: auth.Credential{Username: "test_user", Password: "test_word"},
},
{
name: "get credentials for registry-1.docker.io",
registry: "registry-1.docker.io",
wantCredential: auth.Credential{Username: "user", Password: "word"},
},
{
name: "get credentials for a registry not stored",
registry: "localhost:6666",
wantCredential: auth.EmptyCredential,
},
{
name: "get credentials for an empty string",
registry: "",
wantCredential: auth.EmptyCredential,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := testClient.Credential(context.Background(), tt.registry)
if err != nil {
t.Errorf("could not get credential: %v", err)
}
if !reflect.DeepEqual(got, tt.wantCredential) {
t.Errorf("Credential() = %v, want %v", got, tt.wantCredential)
}
})
}
}
oras-go-2.5.0/registry/remote/credentials/store.go 0000664 0000000 0000000 00000022724 14576745303 0022235 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 credentials supports reading, saving, and removing credentials from
// Docker configuration files and external credential stores that follow
// the Docker credential helper protocol.
//
// Reference: https://docs.docker.com/engine/reference/commandline/login/#credential-stores
package credentials
import (
"context"
"fmt"
"os"
"path/filepath"
"oras.land/oras-go/v2/internal/syncutil"
"oras.land/oras-go/v2/registry/remote/auth"
"oras.land/oras-go/v2/registry/remote/credentials/internal/config"
)
const (
dockerConfigDirEnv = "DOCKER_CONFIG"
dockerConfigFileDir = ".docker"
dockerConfigFileName = "config.json"
)
// Store is the interface that any credentials store must implement.
type Store interface {
// Get retrieves credentials from the store for the given server address.
Get(ctx context.Context, serverAddress string) (auth.Credential, error)
// Put saves credentials into the store for the given server address.
Put(ctx context.Context, serverAddress string, cred auth.Credential) error
// Delete removes credentials from the store for the given server address.
Delete(ctx context.Context, serverAddress string) error
}
// DynamicStore dynamically determines which store to use based on the settings
// in the config file.
type DynamicStore struct {
config *config.Config
options StoreOptions
detectedCredsStore string
setCredsStoreOnce syncutil.OnceOrRetry
}
// StoreOptions provides options for NewStore.
type StoreOptions struct {
// AllowPlaintextPut allows saving credentials in plaintext in the config
// file.
// - If AllowPlaintextPut is set to false (default value), Put() will
// return an error when native store is not available.
// - If AllowPlaintextPut is set to true, Put() will save credentials in
// plaintext in the config file when native store is not available.
AllowPlaintextPut bool
// DetectDefaultNativeStore enables detecting the platform-default native
// credentials store when the config file has no authentication information.
//
// If DetectDefaultNativeStore is set to true, the store will detect and set
// the default native credentials store in the "credsStore" field of the
// config file.
// - Windows: "wincred"
// - Linux: "pass" or "secretservice"
// - macOS: "osxkeychain"
//
// References:
// - https://docs.docker.com/engine/reference/commandline/login/#credentials-store
// - https://docs.docker.com/engine/reference/commandline/cli/#docker-cli-configuration-file-configjson-properties
DetectDefaultNativeStore bool
}
// NewStore returns a Store based on the given configuration file.
//
// For Get(), Put() and Delete(), the returned Store will dynamically determine
// which underlying credentials store to use for the given server address.
// The underlying credentials store is determined in the following order:
// 1. Native server-specific credential helper
// 2. Native credentials store
// 3. The plain-text config file itself
//
// References:
// - https://docs.docker.com/engine/reference/commandline/login/#credentials-store
// - https://docs.docker.com/engine/reference/commandline/cli/#docker-cli-configuration-file-configjson-properties
func NewStore(configPath string, opts StoreOptions) (*DynamicStore, error) {
cfg, err := config.Load(configPath)
if err != nil {
return nil, err
}
ds := &DynamicStore{
config: cfg,
options: opts,
}
if opts.DetectDefaultNativeStore && !cfg.IsAuthConfigured() {
// no authentication configured, detect the default credentials store
ds.detectedCredsStore = getDefaultHelperSuffix()
}
return ds, nil
}
// NewStoreFromDocker returns a Store based on the default docker config file.
// - If the $DOCKER_CONFIG environment variable is set,
// $DOCKER_CONFIG/config.json will be used.
// - Otherwise, the default location $HOME/.docker/config.json will be used.
//
// NewStoreFromDocker internally calls [NewStore].
//
// References:
// - https://docs.docker.com/engine/reference/commandline/cli/#configuration-files
// - https://docs.docker.com/engine/reference/commandline/cli/#change-the-docker-directory
func NewStoreFromDocker(opt StoreOptions) (*DynamicStore, error) {
configPath, err := getDockerConfigPath()
if err != nil {
return nil, err
}
return NewStore(configPath, opt)
}
// Get retrieves credentials from the store for the given server address.
func (ds *DynamicStore) Get(ctx context.Context, serverAddress string) (auth.Credential, error) {
return ds.getStore(serverAddress).Get(ctx, serverAddress)
}
// Put saves credentials into the store for the given server address.
// Put returns ErrPlaintextPutDisabled if native store is not available and
// [StoreOptions].AllowPlaintextPut is set to false.
func (ds *DynamicStore) Put(ctx context.Context, serverAddress string, cred auth.Credential) error {
if err := ds.getStore(serverAddress).Put(ctx, serverAddress, cred); err != nil {
return err
}
// save the detected creds store back to the config file on first put
return ds.setCredsStoreOnce.Do(func() error {
if ds.detectedCredsStore != "" {
if err := ds.config.SetCredentialsStore(ds.detectedCredsStore); err != nil {
return fmt.Errorf("failed to set credsStore: %w", err)
}
}
return nil
})
}
// Delete removes credentials from the store for the given server address.
func (ds *DynamicStore) Delete(ctx context.Context, serverAddress string) error {
return ds.getStore(serverAddress).Delete(ctx, serverAddress)
}
// IsAuthConfigured returns whether there is authentication configured in the
// config file or not.
//
// IsAuthConfigured returns true when:
// - The "credsStore" field is not empty
// - Or the "credHelpers" field is not empty
// - Or there is any entry in the "auths" field
func (ds *DynamicStore) IsAuthConfigured() bool {
return ds.config.IsAuthConfigured()
}
// ConfigPath returns the path to the config file.
func (ds *DynamicStore) ConfigPath() string {
return ds.config.Path()
}
// getHelperSuffix returns the credential helper suffix for the given server
// address.
func (ds *DynamicStore) getHelperSuffix(serverAddress string) string {
// 1. Look for a server-specific credential helper first
if helper := ds.config.GetCredentialHelper(serverAddress); helper != "" {
return helper
}
// 2. Then look for the configured native store
if credsStore := ds.config.CredentialsStore(); credsStore != "" {
return credsStore
}
// 3. Use the detected default store
return ds.detectedCredsStore
}
// getStore returns a store for the given server address.
func (ds *DynamicStore) getStore(serverAddress string) Store {
if helper := ds.getHelperSuffix(serverAddress); helper != "" {
return NewNativeStore(helper)
}
fs := newFileStore(ds.config)
fs.DisablePut = !ds.options.AllowPlaintextPut
return fs
}
// getDockerConfigPath returns the path to the default docker config file.
func getDockerConfigPath() (string, error) {
// first try the environment variable
configDir := os.Getenv(dockerConfigDirEnv)
if configDir == "" {
// then try home directory
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("failed to get user home directory: %w", err)
}
configDir = filepath.Join(homeDir, dockerConfigFileDir)
}
return filepath.Join(configDir, dockerConfigFileName), nil
}
// storeWithFallbacks is a store that has multiple fallback stores.
type storeWithFallbacks struct {
stores []Store
}
// NewStoreWithFallbacks returns a new store based on the given stores.
// - Get() searches the primary and the fallback stores
// for the credentials and returns when it finds the
// credentials in any of the stores.
// - Put() saves the credentials into the primary store.
// - Delete() deletes the credentials from the primary store.
func NewStoreWithFallbacks(primary Store, fallbacks ...Store) Store {
if len(fallbacks) == 0 {
return primary
}
return &storeWithFallbacks{
stores: append([]Store{primary}, fallbacks...),
}
}
// Get retrieves credentials from the StoreWithFallbacks for the given server.
// It searches the primary and the fallback stores for the credentials of serverAddress
// and returns when it finds the credentials in any of the stores.
func (sf *storeWithFallbacks) Get(ctx context.Context, serverAddress string) (auth.Credential, error) {
for _, s := range sf.stores {
cred, err := s.Get(ctx, serverAddress)
if err != nil {
return auth.EmptyCredential, err
}
if cred != auth.EmptyCredential {
return cred, nil
}
}
return auth.EmptyCredential, nil
}
// Put saves credentials into the StoreWithFallbacks. It puts
// the credentials into the primary store.
func (sf *storeWithFallbacks) Put(ctx context.Context, serverAddress string, cred auth.Credential) error {
return sf.stores[0].Put(ctx, serverAddress, cred)
}
// Delete removes credentials from the StoreWithFallbacks for the given server.
// It deletes the credentials from the primary store.
func (sf *storeWithFallbacks) Delete(ctx context.Context, serverAddress string) error {
return sf.stores[0].Delete(ctx, serverAddress)
}
oras-go-2.5.0/registry/remote/credentials/store_test.go 0000664 0000000 0000000 00000064434 14576745303 0023300 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 credentials
import (
"context"
"encoding/json"
"errors"
"os"
"path/filepath"
"reflect"
"testing"
"oras.land/oras-go/v2/registry/remote/auth"
"oras.land/oras-go/v2/registry/remote/credentials/internal/config/configtest"
)
type badStore struct{}
var errBadStore = errors.New("bad store!")
// Get retrieves credentials from the store for the given server address.
func (s *badStore) Get(ctx context.Context, serverAddress string) (auth.Credential, error) {
return auth.EmptyCredential, errBadStore
}
// Put saves credentials into the store for the given server address.
func (s *badStore) Put(ctx context.Context, serverAddress string, cred auth.Credential) error {
return errBadStore
}
// Delete removes credentials from the store for the given server address.
func (s *badStore) Delete(ctx context.Context, serverAddress string) error {
return errBadStore
}
func Test_DynamicStore_IsAuthConfigured(t *testing.T) {
tempDir := t.TempDir()
tests := []struct {
name string
fileName string
shouldCreateFile bool
cfg configtest.Config
want bool
}{
{
name: "not existing file",
fileName: "config.json",
shouldCreateFile: false,
cfg: configtest.Config{},
want: false,
},
{
name: "no auth",
fileName: "config.json",
shouldCreateFile: true,
cfg: configtest.Config{
SomeConfigField: 123,
},
want: false,
},
{
name: "empty auths exist",
fileName: "empty_auths.json",
shouldCreateFile: true,
cfg: configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{},
},
want: false,
},
{
name: "auths exist, but no credential",
fileName: "no_cred_auths.json",
shouldCreateFile: true,
cfg: configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{
"test.example.com": {},
},
},
want: true,
},
{
name: "auths exist",
fileName: "auths.json",
shouldCreateFile: true,
cfg: configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{
"test.example.com": {
Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=",
},
},
},
want: true,
},
{
name: "credsStore exists",
fileName: "credsStore.json",
shouldCreateFile: true,
cfg: configtest.Config{
CredentialsStore: "teststore",
},
want: true,
},
{
name: "empty credHelpers exist",
fileName: "empty_credsStore.json",
shouldCreateFile: true,
cfg: configtest.Config{
CredentialHelpers: map[string]string{},
},
want: false,
},
{
name: "credHelpers exist",
fileName: "credsStore.json",
shouldCreateFile: true,
cfg: configtest.Config{
CredentialHelpers: map[string]string{
"test.example.com": "testhelper",
},
},
want: true,
},
{
name: "all exist",
fileName: "credsStore.json",
shouldCreateFile: true,
cfg: configtest.Config{
SomeConfigField: 123,
AuthConfigs: map[string]configtest.AuthConfig{
"test.example.com": {},
},
CredentialsStore: "teststore",
CredentialHelpers: map[string]string{
"test.example.com": "testhelper",
},
},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// prepare test content
configPath := filepath.Join(tempDir, tt.fileName)
if tt.shouldCreateFile {
jsonStr, err := json.Marshal(tt.cfg)
if err != nil {
t.Fatalf("failed to marshal config: %v", err)
}
if err := os.WriteFile(configPath, jsonStr, 0666); err != nil {
t.Fatalf("failed to write config file: %v", err)
}
}
ds, err := NewStore(configPath, StoreOptions{})
if err != nil {
t.Fatal("newStore() error =", err)
}
if got := ds.IsAuthConfigured(); got != tt.want {
t.Errorf("DynamicStore.IsAuthConfigured() = %v, want %v", got, tt.want)
}
})
}
}
func Test_DynamicStore_authConfigured(t *testing.T) {
// prepare test content
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "auth_configured.json")
config := configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{
"xxx": {},
},
SomeConfigField: 123,
}
jsonStr, err := json.Marshal(config)
if err != nil {
t.Fatalf("failed to marshal config: %v", err)
}
if err := os.WriteFile(configPath, jsonStr, 0666); err != nil {
t.Fatalf("failed to write config file: %v", err)
}
ds, err := NewStore(configPath, StoreOptions{AllowPlaintextPut: true})
if err != nil {
t.Fatal("NewStore() error =", err)
}
// test IsAuthConfigured
authConfigured := ds.IsAuthConfigured()
if want := true; authConfigured != want {
t.Errorf("DynamicStore.IsAuthConfigured() = %v, want %v", authConfigured, want)
}
serverAddr := "test.example.com"
cred := auth.Credential{
Username: "username",
Password: "password",
}
ctx := context.Background()
// test put
if err := ds.Put(ctx, serverAddr, cred); err != nil {
t.Fatal("DynamicStore.Get() error =", err)
}
// Put() should not set detected store back to config
if got := ds.detectedCredsStore; got != "" {
t.Errorf("ds.detectedCredsStore = %v, want empty", got)
}
if got := ds.config.CredentialsStore(); got != "" {
t.Errorf("ds.config.CredentialsStore() = %v, want empty", got)
}
// test get
got, err := ds.Get(ctx, serverAddr)
if err != nil {
t.Fatal("DynamicStore.Get() error =", err)
}
if want := cred; got != want {
t.Errorf("DynamicStore.Get() = %v, want %v", got, want)
}
// test delete
err = ds.Delete(ctx, serverAddr)
if err != nil {
t.Fatal("DynamicStore.Delete() error =", err)
}
// verify delete
got, err = ds.Get(ctx, serverAddr)
if err != nil {
t.Fatal("DynamicStore.Get() error =", err)
}
if want := auth.EmptyCredential; got != want {
t.Errorf("DynamicStore.Get() = %v, want %v", got, want)
}
}
func Test_DynamicStore_authConfigured_DetectDefaultNativeStore(t *testing.T) {
// prepare test content
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "auth_configured.json")
config := configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{
"xxx": {},
},
SomeConfigField: 123,
}
jsonStr, err := json.Marshal(config)
if err != nil {
t.Fatalf("failed to marshal config: %v", err)
}
if err := os.WriteFile(configPath, jsonStr, 0666); err != nil {
t.Fatalf("failed to write config file: %v", err)
}
opts := StoreOptions{
AllowPlaintextPut: true,
DetectDefaultNativeStore: true,
}
ds, err := NewStore(configPath, opts)
if err != nil {
t.Fatal("NewStore() error =", err)
}
// test IsAuthConfigured
authConfigured := ds.IsAuthConfigured()
if want := true; authConfigured != want {
t.Errorf("DynamicStore.IsAuthConfigured() = %v, want %v", authConfigured, want)
}
serverAddr := "test.example.com"
cred := auth.Credential{
Username: "username",
Password: "password",
}
ctx := context.Background()
// test put
if err := ds.Put(ctx, serverAddr, cred); err != nil {
t.Fatal("DynamicStore.Get() error =", err)
}
// Put() should not set detected store back to config
if got := ds.detectedCredsStore; got != "" {
t.Errorf("ds.detectedCredsStore = %v, want empty", got)
}
if got := ds.config.CredentialsStore(); got != "" {
t.Errorf("ds.config.CredentialsStore() = %v, want empty", got)
}
// test get
got, err := ds.Get(ctx, serverAddr)
if err != nil {
t.Fatal("DynamicStore.Get() error =", err)
}
if want := cred; got != want {
t.Errorf("DynamicStore.Get() = %v, want %v", got, want)
}
// test delete
err = ds.Delete(ctx, serverAddr)
if err != nil {
t.Fatal("DynamicStore.Delete() error =", err)
}
// verify delete
got, err = ds.Get(ctx, serverAddr)
if err != nil {
t.Fatal("DynamicStore.Get() error =", err)
}
if want := auth.EmptyCredential; got != want {
t.Errorf("DynamicStore.Get() = %v, want %v", got, want)
}
}
func Test_DynamicStore_noAuthConfigured(t *testing.T) {
// prepare test content
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "no_auth_configured.json")
cfg := configtest.Config{
SomeConfigField: 123,
}
jsonStr, err := json.Marshal(cfg)
if err != nil {
t.Fatalf("failed to marshal config: %v", err)
}
if err := os.WriteFile(configPath, jsonStr, 0666); err != nil {
t.Fatalf("failed to write config file: %v", err)
}
ds, err := NewStore(configPath, StoreOptions{AllowPlaintextPut: true})
if err != nil {
t.Fatal("NewStore() error =", err)
}
// test IsAuthConfigured
authConfigured := ds.IsAuthConfigured()
if want := false; authConfigured != want {
t.Errorf("DynamicStore.IsAuthConfigured() = %v, want %v", authConfigured, want)
}
serverAddr := "test.example.com"
cred := auth.Credential{
Username: "username",
Password: "password",
}
ctx := context.Background()
// Get() should not set detected store back to config
if _, err := ds.Get(ctx, serverAddr); err != nil {
t.Fatal("DynamicStore.Get() error =", err)
}
// test put
if err := ds.Put(ctx, serverAddr, cred); err != nil {
t.Fatal("DynamicStore.Put() error =", err)
}
// Put() should not set detected store back to config
if got := ds.detectedCredsStore; got != "" {
t.Errorf("ds.detectedCredsStore = %v, want empty", got)
}
if got := ds.config.CredentialsStore(); got != "" {
t.Errorf("ds.config.CredentialsStore() = %v, want empty", got)
}
// test get
got, err := ds.Get(ctx, serverAddr)
if err != nil {
t.Fatal("DynamicStore.Get() error =", err)
}
if want := cred; got != want {
t.Errorf("DynamicStore.Get() = %v, want %v", got, want)
}
// test delete
err = ds.Delete(ctx, serverAddr)
if err != nil {
t.Fatal("DynamicStore.Delete() error =", err)
}
// verify delete
got, err = ds.Get(ctx, serverAddr)
if err != nil {
t.Fatal("DynamicStore.Get() error =", err)
}
if want := auth.EmptyCredential; got != want {
t.Errorf("DynamicStore.Get() = %v, want %v", got, want)
}
}
func Test_DynamicStore_noAuthConfigured_DetectDefaultNativeStore(t *testing.T) {
// prepare test content
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "no_auth_configured.json")
cfg := configtest.Config{
SomeConfigField: 123,
}
jsonStr, err := json.Marshal(cfg)
if err != nil {
t.Fatalf("failed to marshal config: %v", err)
}
if err := os.WriteFile(configPath, jsonStr, 0666); err != nil {
t.Fatalf("failed to write config file: %v", err)
}
opts := StoreOptions{
AllowPlaintextPut: true,
DetectDefaultNativeStore: true,
}
ds, err := NewStore(configPath, opts)
if err != nil {
t.Fatal("NewStore() error =", err)
}
// test IsAuthConfigured
authConfigured := ds.IsAuthConfigured()
if want := false; authConfigured != want {
t.Errorf("DynamicStore.IsAuthConfigured() = %v, want %v", authConfigured, want)
}
serverAddr := "test.example.com"
cred := auth.Credential{
Username: "username",
Password: "password",
}
ctx := context.Background()
// Get() should set detectedCredsStore only, but should not save it back to config
if _, err := ds.Get(ctx, serverAddr); err != nil {
t.Fatal("DynamicStore.Get() error =", err)
}
if defaultStore := getDefaultHelperSuffix(); defaultStore != "" {
if got := ds.detectedCredsStore; got != defaultStore {
t.Errorf("ds.detectedCredsStore = %v, want %v", got, defaultStore)
}
}
if got := ds.config.CredentialsStore(); got != "" {
t.Errorf("ds.config.CredentialsStore() = %v, want empty", got)
}
// test put
if err := ds.Put(ctx, serverAddr, cred); err != nil {
t.Fatal("DynamicStore.Put() error =", err)
}
// Put() should set the detected store back to config
if got := ds.config.CredentialsStore(); got != ds.detectedCredsStore {
t.Errorf("ds.config.CredentialsStore() = %v, want %v", got, ds.detectedCredsStore)
}
// test get
got, err := ds.Get(ctx, serverAddr)
if err != nil {
t.Fatal("DynamicStore.Get() error =", err)
}
if want := cred; got != want {
t.Errorf("DynamicStore.Get() = %v, want %v", got, want)
}
// test delete
err = ds.Delete(ctx, serverAddr)
if err != nil {
t.Fatal("DynamicStore.Delete() error =", err)
}
// verify delete
got, err = ds.Get(ctx, serverAddr)
if err != nil {
t.Fatal("DynamicStore.Get() error =", err)
}
if want := auth.EmptyCredential; got != want {
t.Errorf("DynamicStore.Get() = %v, want %v", got, want)
}
}
func Test_DynamicStore_fileStore_AllowPlainTextPut(t *testing.T) {
// prepare test content
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.json")
serverAddr := "newtest.example.com"
cred := auth.Credential{
Username: "username",
Password: "password",
}
ctx := context.Background()
cfg := configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{
"test.example.com": {},
},
SomeConfigField: 123,
}
jsonStr, err := json.Marshal(cfg)
if err != nil {
t.Fatalf("failed to marshal config: %v", err)
}
if err := os.WriteFile(configPath, jsonStr, 0666); err != nil {
t.Fatalf("failed to write config file: %v", err)
}
// test default option
ds, err := NewStore(configPath, StoreOptions{})
if err != nil {
t.Fatal("NewStore() error =", err)
}
err = ds.Put(ctx, serverAddr, cred)
if wantErr := ErrPlaintextPutDisabled; !errors.Is(err, wantErr) {
t.Errorf("DynamicStore.Put() error = %v, wantErr %v", err, wantErr)
}
// test AllowPlainTextPut = true
ds, err = NewStore(configPath, StoreOptions{AllowPlaintextPut: true})
if err != nil {
t.Fatal("NewStore() error =", err)
}
if err := ds.Put(ctx, serverAddr, cred); err != nil {
t.Error("DynamicStore.Put() error =", err)
}
// verify config file
configFile, err := os.Open(configPath)
if err != nil {
t.Fatalf("failed to open config file: %v", err)
}
defer configFile.Close()
var gotCfg configtest.Config
if err := json.NewDecoder(configFile).Decode(&gotCfg); err != nil {
t.Fatalf("failed to decode config file: %v", err)
}
wantCfg := configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{
"test.example.com": {},
serverAddr: {
Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=",
},
},
SomeConfigField: cfg.SomeConfigField,
}
if !reflect.DeepEqual(gotCfg, wantCfg) {
t.Errorf("Decoded config = %v, want %v", gotCfg, wantCfg)
}
}
func Test_DynamicStore_getHelperSuffix(t *testing.T) {
tests := []struct {
name string
configPath string
serverAddress string
want string
}{
{
name: "Get cred helper: registry_helper1",
configPath: "testdata/credHelpers_config.json",
serverAddress: "registry1.example.com",
want: "registry1-helper",
},
{
name: "Get cred helper: registry_helper2",
configPath: "testdata/credHelpers_config.json",
serverAddress: "registry2.example.com",
want: "registry2-helper",
},
{
name: "Empty cred helper configured",
configPath: "testdata/credHelpers_config.json",
serverAddress: "registry3.example.com",
want: "",
},
{
name: "No cred helper and creds store configured",
configPath: "testdata/credHelpers_config.json",
serverAddress: "whatever.example.com",
want: "",
},
{
name: "Choose cred helper over creds store",
configPath: "testdata/credsStore_config.json",
serverAddress: "test.example.com",
want: "test-helper",
},
{
name: "No cred helper configured, choose cred store",
configPath: "testdata/credsStore_config.json",
serverAddress: "whatever.example.com",
want: "teststore",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ds, err := NewStore(tt.configPath, StoreOptions{})
if err != nil {
t.Fatal("NewStore() error =", err)
}
if got := ds.getHelperSuffix(tt.serverAddress); got != tt.want {
t.Errorf("DynamicStore.getHelperSuffix() = %v, want %v", got, tt.want)
}
})
}
}
func Test_DynamicStore_ConfigPath(t *testing.T) {
path := "../../testdata/credsStore_config.json"
var err error
store, err := NewStore(path, StoreOptions{})
if err != nil {
t.Fatal("NewFileStore() error =", err)
}
got := store.ConfigPath()
if got != path {
t.Errorf("Config.GetPath() = %v, want %v", got, path)
}
}
func Test_DynamicStore_getStore_nativeStore(t *testing.T) {
tests := []struct {
name string
configPath string
serverAddress string
}{
{
name: "Cred helper configured for registry1.example.com",
configPath: "testdata/credHelpers_config.json",
serverAddress: "registry1.example.com",
},
{
name: "Cred helper configured for registry2.example.com",
configPath: "testdata/credHelpers_config.json",
serverAddress: "registry2.example.com",
},
{
name: "Cred helper configured for test.example.com",
configPath: "testdata/credsStore_config.json",
serverAddress: "test.example.com",
},
{
name: "No cred helper configured, use creds store",
configPath: "testdata/credsStore_config.json",
serverAddress: "whaterver.example.com",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ds, err := NewStore(tt.configPath, StoreOptions{})
if err != nil {
t.Fatal("NewStore() error =", err)
}
gotStore := ds.getStore(tt.serverAddress)
if _, ok := gotStore.(*nativeStore); !ok {
t.Errorf("gotStore is not a native store")
}
})
}
}
func Test_DynamicStore_getStore_fileStore(t *testing.T) {
tests := []struct {
name string
configPath string
serverAddress string
}{
{
name: "Empty cred helper configured for registry3.example.com",
configPath: "testdata/credHelpers_config.json",
serverAddress: "registry3.example.com",
},
{
name: "No cred helper configured",
configPath: "testdata/credHelpers_config.json",
serverAddress: "whatever.example.com",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ds, err := NewStore(tt.configPath, StoreOptions{})
if err != nil {
t.Fatal("NewStore() error =", err)
}
gotStore := ds.getStore(tt.serverAddress)
gotFS1, ok := gotStore.(*FileStore)
if !ok {
t.Errorf("gotStore is not a file store")
}
// get again, the two file stores should be based on the same config instance
gotStore = ds.getStore(tt.serverAddress)
gotFS2, ok := gotStore.(*FileStore)
if !ok {
t.Errorf("gotStore is not a file store")
}
if gotFS1.config != gotFS2.config {
t.Errorf("gotFS1 and gotFS2 are not based on the same config")
}
})
}
}
func Test_storeWithFallbacks_Get(t *testing.T) {
// prepare test content
server1 := "foo.registry.com"
cred1 := auth.Credential{
Username: "username",
Password: "password",
}
server2 := "bar.registry.com"
cred2 := auth.Credential{
RefreshToken: "identity_token",
}
primaryStore := &testStore{}
fallbackStore1 := &testStore{
storage: map[string]auth.Credential{
server1: cred1,
},
}
fallbackStore2 := &testStore{
storage: map[string]auth.Credential{
server2: cred2,
},
}
sf := NewStoreWithFallbacks(primaryStore, fallbackStore1, fallbackStore2)
ctx := context.Background()
// test Get()
got1, err := sf.Get(ctx, server1)
if err != nil {
t.Fatalf("storeWithFallbacks.Get(%s) error = %v", server1, err)
}
if want := cred1; got1 != cred1 {
t.Errorf("storeWithFallbacks.Get(%s) = %v, want %v", server1, got1, want)
}
got2, err := sf.Get(ctx, server2)
if err != nil {
t.Fatalf("storeWithFallbacks.Get(%s) error = %v", server2, err)
}
if want := cred2; got2 != cred2 {
t.Errorf("storeWithFallbacks.Get(%s) = %v, want %v", server2, got2, want)
}
// test Get(): no credential found
got, err := sf.Get(ctx, "whaterver")
if err != nil {
t.Fatal("storeWithFallbacks.Get() error =", err)
}
if want := auth.EmptyCredential; got != want {
t.Errorf("storeWithFallbacks.Get() = %v, want %v", got, want)
}
}
func Test_storeWithFallbacks_Get_throwError(t *testing.T) {
badStore := &badStore{}
goodStore := &testStore{}
sf := NewStoreWithFallbacks(badStore, goodStore)
ctx := context.Background()
// test Get(): should throw error
_, err := sf.Get(ctx, "whatever")
if wantErr := errBadStore; !errors.Is(err, wantErr) {
t.Errorf("storeWithFallback.Get() error = %v, wantErr %v", err, wantErr)
}
}
func Test_storeWithFallbacks_Put(t *testing.T) {
// prepare test content
cfg := configtest.Config{
SomeConfigField: 123,
}
jsonStr, err := json.Marshal(cfg)
if err != nil {
t.Fatalf("failed to marshal config: %v", err)
}
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "no_auth_configured.json")
if err := os.WriteFile(configPath, jsonStr, 0666); err != nil {
t.Fatalf("failed to write config file: %v", err)
}
opts := StoreOptions{
AllowPlaintextPut: true,
}
primaryStore, err := NewStore(configPath, opts) // plaintext enabled
if err != nil {
t.Fatalf("NewStore(%s) error = %v", configPath, err)
}
badStore := &badStore{} // bad store
sf := NewStoreWithFallbacks(primaryStore, badStore)
ctx := context.Background()
server := "example.registry.com"
cred := auth.Credential{
Username: "username",
Password: "password",
}
// test Put()
if err := sf.Put(ctx, server, cred); err != nil {
t.Fatal("storeWithFallbacks.Put() error =", err)
}
// verify Get()
got, err := sf.Get(ctx, server)
if err != nil {
t.Fatal("storeWithFallbacks.Get() error =", err)
}
if want := cred; got != want {
t.Errorf("storeWithFallbacks.Get() = %v, want %v", got, want)
}
}
func Test_storeWithFallbacks_Put_throwError(t *testing.T) {
badStore := &badStore{}
goodStore := &testStore{}
sf := NewStoreWithFallbacks(badStore, goodStore)
ctx := context.Background()
// test Put(): should thrown error
err := sf.Put(ctx, "whatever", auth.Credential{})
if wantErr := errBadStore; !errors.Is(err, wantErr) {
t.Errorf("storeWithFallback.Put() error = %v, wantErr %v", err, wantErr)
}
}
func Test_storeWithFallbacks_Delete(t *testing.T) {
// prepare test content
server1 := "foo.registry.com"
cred1 := auth.Credential{
Username: "username",
Password: "password",
}
server2 := "bar.registry.com"
cred2 := auth.Credential{
RefreshToken: "identity_token",
}
primaryStore := &testStore{
storage: map[string]auth.Credential{
server1: cred1,
server2: cred2,
},
}
badStore := &badStore{}
sf := NewStoreWithFallbacks(primaryStore, badStore)
ctx := context.Background()
// test Delete(): server1
if err := sf.Delete(ctx, server1); err != nil {
t.Fatal("storeWithFallback.Delete()")
}
// verify primary store
if want := map[string]auth.Credential{server2: cred2}; !reflect.DeepEqual(primaryStore.storage, want) {
t.Errorf("primaryStore.storage = %v, want %v", primaryStore.storage, want)
}
// test Delete(): server2
if err := sf.Delete(ctx, server2); err != nil {
t.Fatal("storeWithFallback.Delete()")
}
// verify primary store
if want := map[string]auth.Credential{}; !reflect.DeepEqual(primaryStore.storage, want) {
t.Errorf("primaryStore.storage = %v, want %v", primaryStore.storage, want)
}
}
func Test_storeWithFallbacks_Delete_throwError(t *testing.T) {
badStore := &badStore{}
goodStore := &testStore{}
sf := NewStoreWithFallbacks(badStore, goodStore)
ctx := context.Background()
// test Delete(): should throw error
err := sf.Delete(ctx, "whatever")
if wantErr := errBadStore; !errors.Is(err, wantErr) {
t.Errorf("storeWithFallback.Delete() error = %v, wantErr %v", err, wantErr)
}
}
func Test_getDockerConfigPath_env(t *testing.T) {
dir, err := os.Getwd()
if err != nil {
t.Fatal("os.Getwd() error =", err)
}
t.Setenv("DOCKER_CONFIG", dir)
got, err := getDockerConfigPath()
if err != nil {
t.Fatal("getDockerConfigPath() error =", err)
}
if want := filepath.Join(dir, "config.json"); got != want {
t.Errorf("getDockerConfigPath() = %v, want %v", got, want)
}
}
func Test_getDockerConfigPath_homeDir(t *testing.T) {
t.Setenv("DOCKER_CONFIG", "")
got, err := getDockerConfigPath()
if err != nil {
t.Fatal("getDockerConfigPath() error =", err)
}
homeDir, err := os.UserHomeDir()
if err != nil {
t.Fatal("os.UserHomeDir()")
}
if want := filepath.Join(homeDir, ".docker", "config.json"); got != want {
t.Errorf("getDockerConfigPath() = %v, want %v", got, want)
}
}
func TestNewStoreFromDocker(t *testing.T) {
// prepare test content
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.json")
t.Setenv("DOCKER_CONFIG", tempDir)
serverAddr1 := "test.example.com"
cred1 := auth.Credential{
Username: "foo",
Password: "bar",
}
config := configtest.Config{
AuthConfigs: map[string]configtest.AuthConfig{
serverAddr1: {
Auth: "Zm9vOmJhcg==",
},
},
SomeConfigField: 123,
}
jsonStr, err := json.Marshal(config)
if err != nil {
t.Fatalf("failed to marshal config: %v", err)
}
if err := os.WriteFile(configPath, jsonStr, 0666); err != nil {
t.Fatalf("failed to write config file: %v", err)
}
ctx := context.Background()
ds, err := NewStoreFromDocker(StoreOptions{AllowPlaintextPut: true})
if err != nil {
t.Fatal("NewStoreFromDocker() error =", err)
}
// test getting an existing credential
got, err := ds.Get(ctx, serverAddr1)
if err != nil {
t.Fatal("DynamicStore.Get() error =", err)
}
if want := cred1; got != want {
t.Errorf("DynamicStore.Get() = %v, want %v", got, want)
}
// test putting a new credential
serverAddr2 := "newtest.example.com"
cred2 := auth.Credential{
Username: "username",
Password: "password",
}
if err := ds.Put(ctx, serverAddr2, cred2); err != nil {
t.Fatal("DynamicStore.Get() error =", err)
}
// test getting the new credential
got, err = ds.Get(ctx, serverAddr2)
if err != nil {
t.Fatal("DynamicStore.Get() error =", err)
}
if want := cred2; got != want {
t.Errorf("DynamicStore.Get() = %v, want %v", got, want)
}
// test deleting the old credential
err = ds.Delete(ctx, serverAddr1)
if err != nil {
t.Fatal("DynamicStore.Delete() error =", err)
}
// verify delete
got, err = ds.Get(ctx, serverAddr1)
if err != nil {
t.Fatal("DynamicStore.Get() error =", err)
}
if want := auth.EmptyCredential; got != want {
t.Errorf("DynamicStore.Get() = %v, want %v", got, want)
}
}
oras-go-2.5.0/registry/remote/credentials/testdata/ 0000775 0000000 0000000 00000000000 14576745303 0022354 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/registry/remote/credentials/testdata/bad_config 0000664 0000000 0000000 00000000005 14576745303 0024345 0 ustar 00root root 0000000 0000000 bad
oras-go-2.5.0/registry/remote/credentials/testdata/credHelpers_config.json 0000664 0000000 0000000 00000000560 14576745303 0027035 0 ustar 00root root 0000000 0000000 {
"auths": {
"registry1.example.com": {
"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="
},
"registry3.example.com": {
"auth": "Zm9vOmJhcg=="
}
},
"credHelpers": {
"registry1.example.com": "registry1-helper",
"registry2.example.com": "registry2-helper",
"registry3.example.com": ""
}
}
oras-go-2.5.0/registry/remote/credentials/testdata/credsStore_config.json 0000664 0000000 0000000 00000000150 14576745303 0026705 0 ustar 00root root 0000000 0000000 {
"credHelpers": {
"test.example.com": "test-helper"
},
"credsStore": "teststore"
}
oras-go-2.5.0/registry/remote/credentials/testdata/empty.json 0000664 0000000 0000000 00000000000 14576745303 0024373 0 ustar 00root root 0000000 0000000 oras-go-2.5.0/registry/remote/credentials/testdata/invalid_auths_config.json 0000664 0000000 0000000 00000000040 14576745303 0027420 0 ustar 00root root 0000000 0000000 {
"auths": "whaterver"
}
oras-go-2.5.0/registry/remote/credentials/testdata/invalid_auths_entry_config.json 0000664 0000000 0000000 00000000352 14576745303 0030647 0 ustar 00root root 0000000 0000000 {
"auths": {
"registry1.example.com": {
"auth": "username:password"
},
"registry2.example.com": "whatever",
"registry3.example.com": {
"identitytoken": 123
}
}
}
oras-go-2.5.0/registry/remote/credentials/testdata/legacy_auths_config.json 0000664 0000000 0000000 00000001346 14576745303 0027250 0 ustar 00root root 0000000 0000000 {
"auths": {
"registry1.example.com": {
"auth": "dXNlcm5hbWUxOnBhc3N3b3JkMQ=="
},
"http://registry2.example.com": {
"auth": "dXNlcm5hbWUyOnBhc3N3b3JkMg=="
},
"https://registry3.example.com": {
"auth": "dXNlcm5hbWUzOnBhc3N3b3JkMw=="
},
"http://registry4.example.com/": {
"auth": "dXNlcm5hbWU0OnBhc3N3b3JkNA=="
},
"https://registry5.example.com/": {
"auth": "dXNlcm5hbWU1OnBhc3N3b3JkNQ=="
},
"https://registry6.example.com/path/": {
"auth": "dXNlcm5hbWU2OnBhc3N3b3JkNg=="
},
"https://registry1.example.com/": {
"auth": "Zm9vOmJhcg=="
}
}
}
oras-go-2.5.0/registry/remote/credentials/testdata/no_auths_config.json 0000664 0000000 0000000 00000000025 14576745303 0026411 0 ustar 00root root 0000000 0000000 {
"key": "val"
}
oras-go-2.5.0/registry/remote/credentials/testdata/valid_auths_config.json 0000664 0000000 0000000 00000001435 14576745303 0027102 0 ustar 00root root 0000000 0000000 {
"auths": {
"registry1.example.com": {
"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="
},
"registry2.example.com": {
"identitytoken": "identity_token"
},
"registry3.example.com": {
"registrytoken": "registry_token"
},
"registry4.example.com": {
"auth": "dXNlcm5hbWU6cGFzc3dvcmQ=",
"identitytoken": "identity_token",
"registrytoken": "registry_token"
},
"registry5.example.com": {},
"registry6.example.com": {
"username": "username",
"password": "password"
},
"registry7.example.com": {
"auth": "dXNlcm5hbWU6cGFzc3dvcmQ=",
"username": "foo",
"password": "bar"
}
}
}
oras-go-2.5.0/registry/remote/credentials/trace/ 0000775 0000000 0000000 00000000000 14576745303 0021641 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/registry/remote/credentials/trace/example_test.go 0000664 0000000 0000000 00000004256 14576745303 0024671 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 trace_test
import (
"context"
"fmt"
"oras.land/oras-go/v2/registry/remote/auth"
"oras.land/oras-go/v2/registry/remote/credentials"
"oras.land/oras-go/v2/registry/remote/credentials/trace"
)
// An example on how to use ExecutableTrace with Stores.
func Example() {
// ExecutableTrace works with all Stores that may invoke executables, for
// example the Store returned from NewStore and NewNativeStore.
store, err := credentials.NewStore("example/path/config.json", credentials.StoreOptions{})
if err != nil {
panic(err)
}
// Define ExecutableTrace and add it to the context. The 'action' argument
// refers to one of 'store', 'get' and 'erase' defined by the docker
// credential helper protocol.
// Reference: https://docs.docker.com/engine/reference/commandline/login/#credential-helper-protocol
traceHooks := &trace.ExecutableTrace{
ExecuteStart: func(executableName string, action string) {
fmt.Printf("executable %s, action %s started", executableName, action)
},
ExecuteDone: func(executableName string, action string, err error) {
fmt.Printf("executable %s, action %s finished", executableName, action)
},
}
ctx := trace.WithExecutableTrace(context.Background(), traceHooks)
// Get, Put and Delete credentials from store. If any credential helper
// executable is run, traceHooks is executed.
err = store.Put(ctx, "localhost:5000", auth.Credential{Username: "testUsername", Password: "testPassword"})
if err != nil {
panic(err)
}
cred, err := store.Get(ctx, "localhost:5000")
if err != nil {
panic(err)
}
fmt.Println(cred)
err = store.Delete(ctx, "localhost:5000")
if err != nil {
panic(err)
}
}
oras-go-2.5.0/registry/remote/credentials/trace/trace.go 0000664 0000000 0000000 00000006612 14576745303 0023273 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 trace
import "context"
// executableTraceContextKey is a value key used to retrieve the ExecutableTrace
// from Context.
type executableTraceContextKey struct{}
// ExecutableTrace is a set of hooks used to trace the execution of binary
// executables. Any particular hook may be nil.
type ExecutableTrace struct {
// ExecuteStart is called before the execution of the executable. The
// executableName parameter is the name of the credential helper executable
// used with NativeStore. The action parameter is one of "store", "get" and
// "erase".
//
// Reference:
// - https://docs.docker.com/engine/reference/commandline/login#credentials-store
ExecuteStart func(executableName string, action string)
// ExecuteDone is called after the execution of an executable completes.
// The executableName parameter is the name of the credential helper
// executable used with NativeStore. The action parameter is one of "store",
// "get" and "erase". The err parameter is the error (if any) returned from
// the execution.
//
// Reference:
// - https://docs.docker.com/engine/reference/commandline/login#credentials-store
ExecuteDone func(executableName string, action string, err error)
}
// ContextExecutableTrace returns the ExecutableTrace associated with the
// context. If none, it returns nil.
func ContextExecutableTrace(ctx context.Context) *ExecutableTrace {
trace, _ := ctx.Value(executableTraceContextKey{}).(*ExecutableTrace)
return trace
}
// WithExecutableTrace takes a Context and an ExecutableTrace, and returns a
// Context with the ExecutableTrace added as a Value. If the Context has a
// previously added trace, the hooks defined in the new trace will be added
// in addition to the previous ones. The recent hooks will be called first.
func WithExecutableTrace(ctx context.Context, trace *ExecutableTrace) context.Context {
if trace == nil {
return ctx
}
if oldTrace := ContextExecutableTrace(ctx); oldTrace != nil {
trace.compose(oldTrace)
}
return context.WithValue(ctx, executableTraceContextKey{}, trace)
}
// compose takes an oldTrace and modifies the existing trace to include
// the hooks defined in the oldTrace. The hooks in the existing trace will
// be called first.
func (trace *ExecutableTrace) compose(oldTrace *ExecutableTrace) {
if oldStart := oldTrace.ExecuteStart; oldStart != nil {
start := trace.ExecuteStart
if start != nil {
trace.ExecuteStart = func(executableName, action string) {
start(executableName, action)
oldStart(executableName, action)
}
} else {
trace.ExecuteStart = oldStart
}
}
if oldDone := oldTrace.ExecuteDone; oldDone != nil {
done := trace.ExecuteDone
if done != nil {
trace.ExecuteDone = func(executableName, action string, err error) {
done(executableName, action, err)
oldDone(executableName, action, err)
}
} else {
trace.ExecuteDone = oldDone
}
}
}
oras-go-2.5.0/registry/remote/errcode/ 0000775 0000000 0000000 00000000000 14576745303 0017671 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/registry/remote/errcode/errors.go 0000664 0000000 0000000 00000007004 14576745303 0021535 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 errcode
import (
"fmt"
"net/http"
"net/url"
"strings"
"unicode"
)
// References:
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#error-codes
// - https://docs.docker.com/registry/spec/api/#errors-2
const (
ErrorCodeBlobUnknown = "BLOB_UNKNOWN"
ErrorCodeBlobUploadInvalid = "BLOB_UPLOAD_INVALID"
ErrorCodeBlobUploadUnknown = "BLOB_UPLOAD_UNKNOWN"
ErrorCodeDigestInvalid = "DIGEST_INVALID"
ErrorCodeManifestBlobUnknown = "MANIFEST_BLOB_UNKNOWN"
ErrorCodeManifestInvalid = "MANIFEST_INVALID"
ErrorCodeManifestUnknown = "MANIFEST_UNKNOWN"
ErrorCodeNameInvalid = "NAME_INVALID"
ErrorCodeNameUnknown = "NAME_UNKNOWN"
ErrorCodeSizeInvalid = "SIZE_INVALID"
ErrorCodeUnauthorized = "UNAUTHORIZED"
ErrorCodeDenied = "DENIED"
ErrorCodeUnsupported = "UNSUPPORTED"
)
// Error represents a response inner error returned by the remote
// registry.
// References:
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#error-codes
// - https://docs.docker.com/registry/spec/api/#errors-2
type Error struct {
Code string `json:"code"`
Message string `json:"message"`
Detail any `json:"detail,omitempty"`
}
// Error returns a error string describing the error.
func (e Error) Error() string {
code := strings.Map(func(r rune) rune {
if r == '_' {
return ' '
}
return unicode.ToLower(r)
}, e.Code)
if e.Message == "" {
return code
}
if e.Detail == nil {
return fmt.Sprintf("%s: %s", code, e.Message)
}
return fmt.Sprintf("%s: %s: %v", code, e.Message, e.Detail)
}
// Errors represents a list of response inner errors returned by the remote
// server.
// References:
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#error-codes
// - https://docs.docker.com/registry/spec/api/#errors-2
type Errors []Error
// Error returns a error string describing the error.
func (errs Errors) Error() string {
switch len(errs) {
case 0:
return ""
case 1:
return errs[0].Error()
}
var errmsgs []string
for _, err := range errs {
errmsgs = append(errmsgs, err.Error())
}
return strings.Join(errmsgs, "; ")
}
// Unwrap returns the inner error only when there is exactly one error.
func (errs Errors) Unwrap() error {
if len(errs) == 1 {
return errs[0]
}
return nil
}
// ErrorResponse represents an error response.
type ErrorResponse struct {
Method string
URL *url.URL
StatusCode int
Errors Errors
}
// Error returns a error string describing the error.
func (err *ErrorResponse) Error() string {
var errmsg string
if len(err.Errors) > 0 {
errmsg = err.Errors.Error()
} else {
errmsg = http.StatusText(err.StatusCode)
}
return fmt.Sprintf("%s %q: response status code %d: %s", err.Method, err.URL, err.StatusCode, errmsg)
}
// Unwrap returns the internal errors of err if any.
func (err *ErrorResponse) Unwrap() error {
if len(err.Errors) == 0 {
return nil
}
return err.Errors
}
oras-go-2.5.0/registry/remote/example_test.go 0000664 0000000 0000000 00000077702 14576745303 0021304 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 remote_test includes all the testable examples for remote repository type
package remote_test
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strconv"
"strings"
"testing"
"github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/specs-go"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/internal/spec"
. "oras.land/oras-go/v2/registry/internal/doc"
"oras.land/oras-go/v2/registry/remote"
)
const (
_ = ExampleUnplayable
exampleRepositoryName = "example"
exampleTag = "latest"
exampleConfig = "Example config content"
exampleLayer = "Example layer content"
exampleUploadUUid = "0bc84d80-837c-41d9-824e-1907463c53b3"
// For ExampleRepository_Push_artifactReferenceManifest:
ManifestDigest = "sha256:a3f9d449466b9b7194c3a76ca4890d792e11eb4e62e59aa8b4c3cce0a56f129d"
ReferenceManifestDigest = "sha256:2d30397701742b04550891851529abe6b071e4fae920a91897d34612662a3bf6"
// For Example_pushAndIgnoreReferrersIndexError:
referrersAPIUnavailableRepositoryName = "no-referrers-api"
referrerDigest = "sha256:4caba1e18385eb152bd92e9fee1dc01e47c436e594123b3c2833acfcad9883e2"
referrersTag = "sha256-c824a9aa7d2e3471306648c6d4baa1abbcb97ff0276181ab4722ca27127cdba0"
referrerIndexDigest = "sha256:7baac5147dd58d56fdbaad5a888fa919235a3a90cb71aaa8b56ee5d19f4cd838"
)
var (
exampleLayerDescriptor = content.NewDescriptorFromBytes(ocispec.MediaTypeImageLayer, []byte(exampleLayer))
exampleLayerDigest = exampleLayerDescriptor.Digest.String()
exampleManifest, _ = json.Marshal(ocispec.Manifest{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
Config: content.NewDescriptorFromBytes(ocispec.MediaTypeImageConfig, []byte(exampleConfig)),
Layers: []ocispec.Descriptor{
exampleLayerDescriptor,
},
})
exampleManifestDescriptor = content.NewDescriptorFromBytes(ocispec.MediaTypeImageManifest, exampleManifest)
exampleManifestDigest = exampleManifestDescriptor.Digest.String()
exampleSignatureManifest, _ = json.Marshal(spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
ArtifactType: "example/signature",
Subject: &exampleManifestDescriptor})
exampleSignatureManifestDescriptor = ocispec.Descriptor{
MediaType: spec.MediaTypeArtifactManifest,
ArtifactType: "example/signature",
Digest: digest.FromBytes(exampleSignatureManifest),
Size: int64(len(exampleSignatureManifest))}
exampleSBoMManifest, _ = json.Marshal(spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
ArtifactType: "example/SBoM",
Subject: &exampleManifestDescriptor})
exampleSBoMManifestDescriptor = ocispec.Descriptor{
MediaType: spec.MediaTypeArtifactManifest,
ArtifactType: "example/SBoM",
Digest: digest.FromBytes(exampleSBoMManifest),
Size: int64(len(exampleSBoMManifest))}
exampleReferrerDescriptors = [][]ocispec.Descriptor{
{exampleSBoMManifestDescriptor}, // page 0
{exampleSignatureManifestDescriptor}, // page 1
}
blobContent = "example blob content"
blobDescriptor = ocispec.Descriptor{
MediaType: "application/tar",
Digest: digest.FromBytes([]byte(blobContent)),
Size: int64(len(blobContent))}
exampleManifestWithBlobs, _ = json.Marshal(spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
ArtifactType: "example/manifest",
Blobs: []ocispec.Descriptor{blobDescriptor},
Subject: &exampleManifestDescriptor})
exampleManifestWithBlobsDescriptor = ocispec.Descriptor{
MediaType: spec.MediaTypeArtifactManifest,
ArtifactType: "example/manifest",
Digest: digest.FromBytes(exampleManifestWithBlobs),
Size: int64(len(exampleManifestWithBlobs))}
subjectDescriptor = content.NewDescriptorFromBytes(ocispec.MediaTypeImageManifest, []byte(`{"layers":[]}`))
referrerManifestContent, _ = json.Marshal(ocispec.Manifest{
Versioned: specs.Versioned{SchemaVersion: 2},
MediaType: ocispec.MediaTypeImageManifest,
Subject: &subjectDescriptor,
Config: ocispec.DescriptorEmptyJSON,
})
referrerDescriptor = content.NewDescriptorFromBytes(ocispec.MediaTypeImageManifest, referrerManifestContent)
referrerIndex, _ = json.Marshal(ocispec.Index{
Manifests: []ocispec.Descriptor{},
})
)
var host string
func TestMain(m *testing.M) {
// Setup a local HTTPS registry
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
p := r.URL.Path
m := r.Method
switch {
case p == "/v2/_catalog" && m == "GET":
result := struct {
Repositories []string `json:"repositories"`
}{
Repositories: []string{"public/repo1", "public/repo2", "internal/repo3"},
}
json.NewEncoder(w).Encode(result)
case p == fmt.Sprintf("/v2/%s/tags/list", exampleRepositoryName) && m == "GET":
result := struct {
Tags []string `json:"tags"`
}{
Tags: []string{"tag1", "tag2"},
}
json.NewEncoder(w).Encode(result)
case p == fmt.Sprintf("/v2/%s/blobs/uploads/", exampleRepositoryName):
w.Header().Set("Location", p+exampleUploadUUid)
w.Header().Set("Docker-Upload-UUID", exampleUploadUUid)
w.WriteHeader(http.StatusAccepted)
case p == fmt.Sprintf("/v2/%s/blobs/uploads/%s", exampleRepositoryName, exampleUploadUUid):
w.WriteHeader(http.StatusCreated)
case p == fmt.Sprintf("/v2/%s/manifests/%s", exampleRepositoryName, exampleTag) && m == "PUT":
w.WriteHeader(http.StatusCreated)
case p == fmt.Sprintf("/v2/%s/manifests/%s", exampleRepositoryName, ManifestDigest) && m == "PUT":
w.WriteHeader(http.StatusCreated)
case p == fmt.Sprintf("/v2/%s/manifests/%s", exampleRepositoryName, ReferenceManifestDigest) && m == "PUT":
w.Header().Set("OCI-Subject", "sha256:a3f9d449466b9b7194c3a76ca4890d792e11eb4e62e59aa8b4c3cce0a56f129d")
w.WriteHeader(http.StatusCreated)
case p == fmt.Sprintf("/v2/%s/manifests/%s", exampleRepositoryName, exampleSignatureManifestDescriptor.Digest) && m == "GET":
w.Header().Set("Content-Type", spec.MediaTypeArtifactManifest)
w.Header().Set("Content-Digest", string(exampleSignatureManifestDescriptor.Digest))
w.Header().Set("Content-Length", strconv.Itoa(len(exampleSignatureManifest)))
w.Write(exampleSignatureManifest)
case p == fmt.Sprintf("/v2/%s/manifests/%s", exampleRepositoryName, exampleSBoMManifestDescriptor.Digest) && m == "GET":
w.Header().Set("Content-Type", spec.MediaTypeArtifactManifest)
w.Header().Set("Content-Digest", string(exampleSBoMManifestDescriptor.Digest))
w.Header().Set("Content-Length", strconv.Itoa(len(exampleSBoMManifest)))
w.Write(exampleSBoMManifest)
case p == fmt.Sprintf("/v2/%s/manifests/%s", exampleRepositoryName, exampleManifestWithBlobsDescriptor.Digest) && m == "GET":
w.Header().Set("Content-Type", spec.MediaTypeArtifactManifest)
w.Header().Set("Content-Digest", string(exampleManifestWithBlobsDescriptor.Digest))
w.Header().Set("Content-Length", strconv.Itoa(len(exampleManifestWithBlobs)))
w.Write(exampleManifestWithBlobs)
case p == fmt.Sprintf("/v2/%s/blobs/%s", exampleRepositoryName, blobDescriptor.Digest) && m == "GET":
w.Header().Set("Content-Type", spec.MediaTypeArtifactManifest)
w.Header().Set("Content-Digest", string(blobDescriptor.Digest))
w.Header().Set("Content-Length", strconv.Itoa(len(blobContent)))
w.Write([]byte(blobContent))
case p == fmt.Sprintf("/v2/%s/referrers/%s", exampleRepositoryName, "sha256:0000000000000000000000000000000000000000000000000000000000000000"):
result := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
}
w.Header().Set("Content-Type", ocispec.MediaTypeImageIndex)
if err := json.NewEncoder(w).Encode(result); err != nil {
panic(err)
}
case p == fmt.Sprintf("/v2/%s/referrers/%s", exampleRepositoryName, exampleManifestDescriptor.Digest.String()):
q := r.URL.Query()
var referrers []ocispec.Descriptor
switch q.Get("test") {
case "page1":
referrers = exampleReferrerDescriptors[1]
default:
referrers = exampleReferrerDescriptors[0]
w.Header().Set("Link", fmt.Sprintf(`<%s?n=1&test=page1>; rel="next"`, p))
}
result := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: referrers,
}
w.Header().Set("Content-Type", ocispec.MediaTypeImageIndex)
if err := json.NewEncoder(w).Encode(result); err != nil {
panic(err)
}
case p == fmt.Sprintf("/v2/%s/manifests/%s", exampleRepositoryName, exampleTag) || p == fmt.Sprintf("/v2/%s/manifests/%s", exampleRepositoryName, exampleManifestDigest):
w.Header().Set("Content-Type", ocispec.MediaTypeImageManifest)
w.Header().Set("Docker-Content-Digest", exampleManifestDigest)
w.Header().Set("Content-Length", strconv.Itoa(len([]byte(exampleManifest))))
w.Header().Set("Warning", `299 - "This image is deprecated and will be removed soon."`)
if m == "GET" {
w.Write([]byte(exampleManifest))
}
case p == fmt.Sprintf("/v2/%s/blobs/%s", exampleRepositoryName, exampleLayerDigest):
w.Header().Set("Content-Type", ocispec.MediaTypeImageLayer)
w.Header().Set("Docker-Content-Digest", string(exampleLayerDigest))
w.Header().Set("Accept-Ranges", "bytes")
var start, end = 0, len(exampleLayer) - 1
if h := r.Header.Get("Range"); h != "" {
w.WriteHeader(http.StatusPartialContent)
indices := strings.Split(strings.Split(h, "=")[1], "-")
var err error
start, err = strconv.Atoi(indices[0])
if err != nil {
panic(err)
}
end, err = strconv.Atoi(indices[1])
if err != nil {
panic(err)
}
}
resultBlob := exampleLayer[start : end+1]
w.Header().Set("Content-Length", strconv.Itoa(len([]byte(resultBlob))))
if m == "GET" {
w.Write([]byte(resultBlob))
}
case p == fmt.Sprintf("/v2/%s/referrers/%s", referrersAPIUnavailableRepositoryName, "sha256:0000000000000000000000000000000000000000000000000000000000000000"):
w.WriteHeader(http.StatusNotFound)
case p == fmt.Sprintf("/v2/%s/manifests/%s", referrersAPIUnavailableRepositoryName, referrerDigest) && m == http.MethodPut:
w.WriteHeader(http.StatusCreated)
case p == fmt.Sprintf("/v2/%s/manifests/%s", referrersAPIUnavailableRepositoryName, referrersTag) && m == http.MethodGet:
w.Write(referrerIndex)
w.Header().Set("Content-Type", ocispec.MediaTypeImageIndex)
w.Header().Set("Content-Length", strconv.Itoa(len(referrerIndex)))
w.Header().Set("Docker-Content-Digest", digest.Digest(string(referrerIndex)).String())
w.WriteHeader(http.StatusCreated)
case p == fmt.Sprintf("/v2/%s/manifests/%s", referrersAPIUnavailableRepositoryName, referrersTag) && m == http.MethodPut:
w.WriteHeader(http.StatusCreated)
case p == fmt.Sprintf("/v2/%s/manifests/%s", referrersAPIUnavailableRepositoryName, referrerIndexDigest) && m == http.MethodDelete:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}))
defer ts.Close()
u, err := url.Parse(ts.URL)
if err != nil {
panic(err)
}
host = u.Host
http.DefaultTransport = ts.Client().Transport
os.Exit(m.Run())
}
// ExampleRepository_Tags gives example snippets for listing tags in a repository.
func ExampleRepository_Tags() {
repo, err := remote.NewRepository(fmt.Sprintf("%s/%s", host, exampleRepositoryName))
if err != nil {
panic(err)
}
ctx := context.Background()
err = repo.Tags(ctx, "", func(tags []string) error {
for _, tag := range tags {
fmt.Println(tag)
}
return nil
})
if err != nil {
panic(err)
}
// Output:
// tag1
// tag2
}
// ExampleRepository_Push gives example snippets for pushing a layer.
func ExampleRepository_Push() {
repo, err := remote.NewRepository(fmt.Sprintf("%s/%s", host, exampleRepositoryName))
if err != nil {
panic(err)
}
ctx := context.Background()
// 1. assemble a descriptor
layer := []byte("Example layer content")
descriptor := content.NewDescriptorFromBytes(ocispec.MediaTypeImageLayer, layer)
// 2. push the descriptor and blob content
err = repo.Push(ctx, descriptor, bytes.NewReader(layer))
if err != nil {
panic(err)
}
fmt.Println("Push finished")
// Output:
// Push finished
}
// ExampleRepository_Push_artifactReferenceManifest gives an example snippet for pushing a reference manifest.
func ExampleRepository_Push_artifactReferenceManifest() {
repo, err := remote.NewRepository(fmt.Sprintf("%s/%s", host, exampleRepositoryName))
if err != nil {
panic(err)
}
ctx := context.Background()
// 1. assemble the referenced artifact manifest
manifest := ocispec.Manifest{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageManifest,
Config: content.NewDescriptorFromBytes(ocispec.MediaTypeImageConfig, []byte("config bytes")),
}
manifestContent, err := json.Marshal(manifest)
if err != nil {
panic(err)
}
manifestDescriptor := content.NewDescriptorFromBytes(ocispec.MediaTypeImageManifest, manifestContent)
// 2. push the manifest descriptor and content
err = repo.Push(ctx, manifestDescriptor, bytes.NewReader(manifestContent))
if err != nil {
panic(err)
}
// 3. assemble the reference artifact manifest
referenceManifest := spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
ArtifactType: "sbom/example",
Subject: &manifestDescriptor,
}
referenceManifestContent, err := json.Marshal(referenceManifest)
if err != nil {
panic(err)
}
referenceManifestDescriptor := content.NewDescriptorFromBytes(spec.MediaTypeArtifactManifest, referenceManifestContent)
// 4. push the reference manifest descriptor and content
err = repo.Push(ctx, referenceManifestDescriptor, bytes.NewReader(referenceManifestContent))
if err != nil {
panic(err)
}
fmt.Println("Push finished")
// Output:
// Push finished
}
// ExampleRepository_Resolve_byTag gives example snippets for resolving a tag to a manifest descriptor.
func ExampleRepository_Resolve_byTag() {
repo, err := remote.NewRepository(fmt.Sprintf("%s/%s", host, exampleRepositoryName))
if err != nil {
panic(err)
}
ctx := context.Background()
tag := "latest"
descriptor, err := repo.Resolve(ctx, tag)
if err != nil {
panic(err)
}
fmt.Println(descriptor.MediaType)
fmt.Println(descriptor.Digest)
fmt.Println(descriptor.Size)
// Output:
// application/vnd.oci.image.manifest.v1+json
// sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7
// 337
}
// ExampleRepository_Resolve_byDigest gives example snippets for resolving a digest to a manifest descriptor.
func ExampleRepository_Resolve_byDigest() {
repo, err := remote.NewRepository(fmt.Sprintf("%s/%s", host, exampleRepositoryName))
if err != nil {
panic(err)
}
ctx := context.Background()
exampleDigest := "sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7"
descriptor, err := repo.Resolve(ctx, exampleDigest)
if err != nil {
panic(err)
}
fmt.Println(descriptor.MediaType)
fmt.Println(descriptor.Digest)
fmt.Println(descriptor.Size)
// Output:
// application/vnd.oci.image.manifest.v1+json
// sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7
// 337
}
// ExampleRepository_Fetch_byTag gives example snippets for downloading a manifest by tag.
func ExampleRepository_Fetch_manifestByTag() {
repo, err := remote.NewRepository(fmt.Sprintf("%s/%s", host, exampleRepositoryName))
if err != nil {
panic(err)
}
ctx := context.Background()
tag := "latest"
descriptor, err := repo.Resolve(ctx, tag)
if err != nil {
panic(err)
}
rc, err := repo.Fetch(ctx, descriptor)
if err != nil {
panic(err)
}
defer rc.Close() // don't forget to close
pulledBlob, err := content.ReadAll(rc, descriptor)
if err != nil {
panic(err)
}
fmt.Println(string(pulledBlob))
// Output:
// {"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:569224ae188c06e97b9fcadaeb2358fb0fb7c4eb105d49aee2620b2719abea43","size":22},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar","digest":"sha256:ef79e47691ad1bc702d7a256da6323ec369a8fc3159b4f1798a47136f3b38c10","size":21}]}
}
// ExampleRepository_Fetch_manifestByDigest gives example snippets for downloading a manifest by digest.
func ExampleRepository_Fetch_manifestByDigest() {
repo, err := remote.NewRepository(fmt.Sprintf("%s/%s", host, exampleRepositoryName))
if err != nil {
panic(err)
}
ctx := context.Background()
exampleDigest := "sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7"
// resolve the blob descriptor to obtain the size of the blob
descriptor, err := repo.Resolve(ctx, exampleDigest)
if err != nil {
panic(err)
}
rc, err := repo.Fetch(ctx, descriptor)
if err != nil {
panic(err)
}
defer rc.Close() // don't forget to close
pulled, err := content.ReadAll(rc, descriptor)
if err != nil {
panic(err)
}
fmt.Println(string(pulled))
// Output:
// {"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:569224ae188c06e97b9fcadaeb2358fb0fb7c4eb105d49aee2620b2719abea43","size":22},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar","digest":"sha256:ef79e47691ad1bc702d7a256da6323ec369a8fc3159b4f1798a47136f3b38c10","size":21}]}
}
// ExampleRepository_Fetch_artifactReferenceManifest gives an example of fetching
// the referrers of a given manifest by using the Referrers API.
func ExampleRepository_Fetch_artifactReferenceManifest() {
repo, err := remote.NewRepository(fmt.Sprintf("%s/%s", host, exampleRepositoryName))
if err != nil {
panic(err)
}
ctx := context.Background()
// resolve a manifest by tag
tag := "latest"
descriptor, err := repo.Resolve(ctx, tag)
if err != nil {
panic(err)
}
// find its referrers by calling Referrers
if err := repo.Referrers(ctx, descriptor, "", func(referrers []ocispec.Descriptor) error {
// for each page of the results, do the following:
for _, referrer := range referrers {
// for each item in this page, pull the manifest and verify its content
rc, err := repo.Fetch(ctx, referrer)
if err != nil {
panic(err)
}
defer rc.Close() // don't forget to close
pulledBlob, err := content.ReadAll(rc, referrer)
if err != nil {
panic(err)
}
fmt.Println(string(pulledBlob))
}
return nil
}); err != nil {
panic(err)
}
// Output:
// {"mediaType":"application/vnd.oci.artifact.manifest.v1+json","artifactType":"example/SBoM","subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7","size":337}}
// {"mediaType":"application/vnd.oci.artifact.manifest.v1+json","artifactType":"example/signature","subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7","size":337}}
}
// ExampleRepository_fetchArtifactBlobs gives an example of pulling the blobs
// of an artifact manifest.
func ExampleRepository_fetchArtifactBlobs() {
repo, err := remote.NewRepository(fmt.Sprintf("%s/%s", host, exampleRepositoryName))
if err != nil {
panic(err)
}
ctx := context.Background()
// 1. Fetch the artifact manifest by digest.
exampleDigest := "sha256:f3550fd0947402d140fd0470702abc92c69f7e9b08d5ca2438f42f8a0ea3fd97"
descriptor, rc, err := repo.FetchReference(ctx, exampleDigest)
if err != nil {
panic(err)
}
defer rc.Close()
pulledContent, err := content.ReadAll(rc, descriptor)
if err != nil {
panic(err)
}
fmt.Println(string(pulledContent))
// 2. Parse the pulled manifest and fetch its blobs.
var pulledManifest spec.Artifact
if err := json.Unmarshal(pulledContent, &pulledManifest); err != nil {
panic(err)
}
for _, blob := range pulledManifest.Blobs {
content, err := content.FetchAll(ctx, repo, blob)
if err != nil {
panic(err)
}
fmt.Println(string(content))
}
// Output:
// {"mediaType":"application/vnd.oci.artifact.manifest.v1+json","artifactType":"example/manifest","blobs":[{"mediaType":"application/tar","digest":"sha256:8d6497c94694a292c04f85cd055d8b5c03eda835dd311e20dfbbf029ff9748cc","size":20}],"subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7","size":337}}
// example blob content
}
// ExampleRepository_FetchReference_manifestByTag gives example snippets for downloading a manifest by tag with only one API call.
func ExampleRepository_FetchReference_manifestByTag() {
repo, err := remote.NewRepository(fmt.Sprintf("%s/%s", host, exampleRepositoryName))
if err != nil {
panic(err)
}
ctx := context.Background()
tag := "latest"
descriptor, rc, err := repo.FetchReference(ctx, tag)
if err != nil {
panic(err)
}
defer rc.Close() // don't forget to close
pulledBlob, err := content.ReadAll(rc, descriptor)
if err != nil {
panic(err)
}
fmt.Println(string(pulledBlob))
// Output:
// {"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:569224ae188c06e97b9fcadaeb2358fb0fb7c4eb105d49aee2620b2719abea43","size":22},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar","digest":"sha256:ef79e47691ad1bc702d7a256da6323ec369a8fc3159b4f1798a47136f3b38c10","size":21}]}
}
// ExampleRepository_FetchReference_manifestByDigest gives example snippets for downloading a manifest by digest.
func ExampleRepository_FetchReference_manifestByDigest() {
repo, err := remote.NewRepository(fmt.Sprintf("%s/%s", host, exampleRepositoryName))
if err != nil {
panic(err)
}
ctx := context.Background()
exampleDigest := "sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7"
descriptor, rc, err := repo.FetchReference(ctx, exampleDigest)
if err != nil {
panic(err)
}
defer rc.Close() // don't forget to close
pulled, err := content.ReadAll(rc, descriptor)
if err != nil {
panic(err)
}
fmt.Println(string(pulled))
// Output:
// {"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:569224ae188c06e97b9fcadaeb2358fb0fb7c4eb105d49aee2620b2719abea43","size":22},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar","digest":"sha256:ef79e47691ad1bc702d7a256da6323ec369a8fc3159b4f1798a47136f3b38c10","size":21}]}
}
// ExampleRepository_Fetch_layer gives example snippets for downloading a layer blob by digest.
func ExampleRepository_Fetch_layer() {
repo, err := remote.NewRepository(fmt.Sprintf("%s/%s", host, exampleRepositoryName))
if err != nil {
panic(err)
}
ctx := context.Background()
descriptor, err := repo.Blobs().Resolve(ctx, exampleLayerDigest)
if err != nil {
panic(err)
}
rc, err := repo.Fetch(ctx, descriptor)
if err != nil {
panic(err)
}
defer rc.Close() // don't forget to close
// option 1: sequential fetch
pulledBlob, err := content.ReadAll(rc, descriptor)
if err != nil {
panic(err)
}
fmt.Println(string(pulledBlob))
// option 2: random access, if the remote registry supports
if seeker, ok := rc.(io.ReadSeeker); ok {
offset := int64(8)
_, err = seeker.Seek(offset, io.SeekStart)
if err != nil {
panic(err)
}
pulledBlob, err := io.ReadAll(rc)
if err != nil {
panic(err)
}
if descriptor.Size-offset != int64(len(pulledBlob)) {
panic("wrong content")
}
fmt.Println(string(pulledBlob))
}
// Output:
// Example layer content
// layer content
}
// ExampleRepository_Tag gives example snippets for tagging a descriptor.
func ExampleRepository_Tag() {
repo, err := remote.NewRepository(fmt.Sprintf("%s/%s", host, exampleRepositoryName))
if err != nil {
panic(err)
}
ctx := context.Background()
exampleDigest := "sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7"
descriptor, err := repo.Resolve(ctx, exampleDigest)
if err != nil {
panic(err)
}
tag := "latest"
err = repo.Tag(ctx, descriptor, tag)
if err != nil {
panic(err)
}
fmt.Println("Succeed")
// Output:
// Succeed
}
// ExampleRegistry_Repositories gives example snippets for listing respositories in a HTTPS registry with pagination.
func ExampleRegistry_Repositories() {
reg, err := remote.NewRegistry(host)
if err != nil {
panic(err)
}
// Override the `host` variable to play with local registry.
// Uncomment below line to reset HTTP option:
// reg.PlainHTTP = true
ctx := context.Background()
err = reg.Repositories(ctx, "", func(repos []string) error {
for _, repo := range repos {
fmt.Println(repo)
}
return nil
})
if err != nil {
panic(err)
}
// Output:
// public/repo1
// public/repo2
// internal/repo3
}
func Example_pullByTag() {
repo, err := remote.NewRepository(fmt.Sprintf("%s/%s", host, exampleRepositoryName))
if err != nil {
panic(err)
}
ctx := context.Background()
// 1. resolve the descriptor
tag := "latest"
descriptor, err := repo.Resolve(ctx, tag)
if err != nil {
panic(err)
}
fmt.Println(descriptor.Digest)
fmt.Println(descriptor.Size)
// 2. fetch the content byte[] from the repository
pulledBlob, err := content.FetchAll(ctx, repo, descriptor)
if err != nil {
panic(err)
}
fmt.Println(string(pulledBlob))
// Output:
// sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7
// 337
// {"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:569224ae188c06e97b9fcadaeb2358fb0fb7c4eb105d49aee2620b2719abea43","size":22},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar","digest":"sha256:ef79e47691ad1bc702d7a256da6323ec369a8fc3159b4f1798a47136f3b38c10","size":21}]}
}
func Example_pullByDigest() {
repo, err := remote.NewRepository(fmt.Sprintf("%s/%s", host, exampleRepositoryName))
if err != nil {
panic(err)
}
ctx := context.Background()
exampleDigest := "sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7"
// 1. resolve the descriptor
descriptor, err := repo.Resolve(ctx, exampleDigest)
if err != nil {
panic(err)
}
fmt.Println(descriptor.Digest)
fmt.Println(descriptor.Size)
// 2. fetch the content byte[] from the repository
pulledBlob, err := content.FetchAll(ctx, repo, descriptor)
if err != nil {
panic(err)
}
fmt.Println(string(pulledBlob))
// Output:
// sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7
// 337
// {"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:569224ae188c06e97b9fcadaeb2358fb0fb7c4eb105d49aee2620b2719abea43","size":22},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar","digest":"sha256:ef79e47691ad1bc702d7a256da6323ec369a8fc3159b4f1798a47136f3b38c10","size":21}]}
}
func Example_handleWarning() {
repo, err := remote.NewRepository(fmt.Sprintf("%s/%s", host, exampleRepositoryName))
if err != nil {
panic(err)
}
// 1. specify HandleWarning
repo.HandleWarning = func(warning remote.Warning) {
fmt.Printf("Warning from %s: %s\n", repo.Reference.Repository, warning.Text)
}
ctx := context.Background()
exampleDigest := "sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7"
// 2. resolve the descriptor
descriptor, err := repo.Resolve(ctx, exampleDigest)
if err != nil {
panic(err)
}
fmt.Println(descriptor.Digest)
fmt.Println(descriptor.Size)
// 3. fetch the content byte[] from the repository
pulledBlob, err := content.FetchAll(ctx, repo, descriptor)
if err != nil {
panic(err)
}
fmt.Println(string(pulledBlob))
// Output:
// Warning from example: This image is deprecated and will be removed soon.
// sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7
// 337
// Warning from example: This image is deprecated and will be removed soon.
// {"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:569224ae188c06e97b9fcadaeb2358fb0fb7c4eb105d49aee2620b2719abea43","size":22},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar","digest":"sha256:ef79e47691ad1bc702d7a256da6323ec369a8fc3159b4f1798a47136f3b38c10","size":21}]}
}
// Example_pushAndTag gives example snippet of pushing an OCI image with a tag.
func Example_pushAndTag() {
repo, err := remote.NewRepository(fmt.Sprintf("%s/%s", host, exampleRepositoryName))
if err != nil {
panic(err)
}
ctx := context.Background()
// Assemble the below OCI image, push and tag it
// +---------------------------------------------------+
// | +----------------+ |
// | +--> "Hello Config" | |
// | +-------------+ | +---+ Config +---+ |
// | (latest)+--> ... +--+ |
// | ++ Manifest ++ | +----------------+ |
// | +--> "Hello Layer" | |
// | +---+ Layer +---+ |
// | |
// +--------+ localhost:5000/example/registry +--------+
generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) ([]byte, error) {
content := ocispec.Manifest{
Config: config,
Layers: layers,
Versioned: specs.Versioned{SchemaVersion: 2},
}
return json.Marshal(content)
}
// 1. assemble descriptors and manifest
layerBlob := []byte("Hello layer")
layerDesc := content.NewDescriptorFromBytes(ocispec.MediaTypeImageLayer, layerBlob)
configBlob := []byte("Hello config")
configDesc := content.NewDescriptorFromBytes(ocispec.MediaTypeImageConfig, configBlob)
manifestBlob, err := generateManifest(configDesc, layerDesc)
if err != nil {
panic(err)
}
manifestDesc := content.NewDescriptorFromBytes(ocispec.MediaTypeImageManifest, manifestBlob)
// 2. push and tag
err = repo.Push(ctx, layerDesc, bytes.NewReader(layerBlob))
if err != nil {
panic(err)
}
err = repo.Push(ctx, configDesc, bytes.NewReader(configBlob))
if err != nil {
panic(err)
}
err = repo.PushReference(ctx, manifestDesc, bytes.NewReader(manifestBlob), "latest")
if err != nil {
panic(err)
}
fmt.Println("Succeed")
// Output:
// Succeed
}
// Example_tagReference gives example snippets for tagging
// a manifest.
func Example_tagReference() {
reg, err := remote.NewRegistry(host)
if err != nil {
panic(err)
}
ctx := context.Background()
repo, err := reg.Repository(ctx, exampleRepositoryName)
if err != nil {
panic(err)
}
// tag a manifest referenced by the exampleDigest below
exampleDigest := "sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7"
tag := "latest"
desc, err := oras.Tag(ctx, repo, exampleDigest, tag)
if err != nil {
panic(err)
}
fmt.Println("Tagged", desc.Digest, "as", tag)
// Output:
// Tagged sha256:b53dc03a49f383ba230d8ac2b78a9c4aec132e4a9f36cc96524df98163202cc7 as latest
}
// Example_pushAndIgnoreReferrersIndexError gives example snippets on how to
// ignore referrer index deletion error during push a referrer manifest.
func Example_pushAndIgnoreReferrersIndexError() {
repo, err := remote.NewRepository(fmt.Sprintf("%s/%s", host, referrersAPIUnavailableRepositoryName))
if err != nil {
panic(err)
}
ctx := context.Background()
// push a referrer manifest and ignore cleaning up error
err = repo.Push(ctx, referrerDescriptor, bytes.NewReader(referrerManifestContent))
if err != nil {
var re *remote.ReferrersError
if !errors.As(err, &re) || !re.IsReferrersIndexDelete() {
panic(err)
}
fmt.Println("ignoring error occurred during cleaning obsolete referrers index")
}
fmt.Println("Push finished")
// Output:
// ignoring error occurred during cleaning obsolete referrers index
// Push finished
}
oras-go-2.5.0/registry/remote/interface_test.go 0000664 0000000 0000000 00000002237 14576745303 0021600 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 remote_test
import (
"testing"
"oras.land/oras-go/v2"
"oras.land/oras-go/v2/internal/interfaces"
"oras.land/oras-go/v2/registry"
"oras.land/oras-go/v2/registry/remote"
)
func TestRepositoryInterface(t *testing.T) {
var repo interface{} = &remote.Repository{}
if _, ok := repo.(registry.Repository); !ok {
t.Error("&Repository{} does not conform registry.Repository")
}
if _, ok := repo.(oras.GraphTarget); !ok {
t.Error("&Repository{} does not conform oras.GraphTarget")
}
if _, ok := repo.(interfaces.ReferenceParser); !ok {
t.Error("&Repository{} does not conform interfaces.ReferenceParser")
}
}
oras-go-2.5.0/registry/remote/internal/ 0000775 0000000 0000000 00000000000 14576745303 0020062 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/registry/remote/internal/errutil/ 0000775 0000000 0000000 00000000000 14576745303 0021550 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/registry/remote/internal/errutil/errutil.go 0000664 0000000 0000000 00000003117 14576745303 0023567 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 errutil
import (
"encoding/json"
"errors"
"io"
"net/http"
"oras.land/oras-go/v2/registry/remote/errcode"
)
// maxErrorBytes specifies the default limit on how many response bytes are
// allowed in the server's error response.
// A typical error message is around 200 bytes. Hence, 8 KiB should be
// sufficient.
const maxErrorBytes int64 = 8 * 1024 // 8 KiB
// ParseErrorResponse parses the error returned by the remote registry.
func ParseErrorResponse(resp *http.Response) error {
resultErr := &errcode.ErrorResponse{
Method: resp.Request.Method,
URL: resp.Request.URL,
StatusCode: resp.StatusCode,
}
var body struct {
Errors errcode.Errors `json:"errors"`
}
lr := io.LimitReader(resp.Body, maxErrorBytes)
if err := json.NewDecoder(lr).Decode(&body); err == nil {
resultErr.Errors = body.Errors
}
return resultErr
}
// IsErrorCode returns true if err is an Error and its Code equals to code.
func IsErrorCode(err error, code string) bool {
var ec errcode.Error
return errors.As(err, &ec) && ec.Code == code
}
oras-go-2.5.0/registry/remote/internal/errutil/errutil_test.go 0000664 0000000 0000000 00000015132 14576745303 0024626 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 errutil
import (
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"oras.land/oras-go/v2/registry/remote/errcode"
)
func Test_ParseErrorResponse(t *testing.T) {
path := "/test"
expectedErrs := errcode.Errors{
{
Code: "UNAUTHORIZED",
Message: "authentication required",
},
{
Code: "NAME_UNKNOWN",
Message: "repository name not known to registry",
},
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case path:
msg := `{ "errors": [ { "code": "UNAUTHORIZED", "message": "authentication required", "detail": [ { "Type": "repository", "Class": "", "Name": "library/hello-world", "Action": "pull" } ] }, { "code": "NAME_UNKNOWN", "message": "repository name not known to registry" } ] }`
w.WriteHeader(http.StatusUnauthorized)
if _, err := w.Write([]byte(msg)); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
resp, err := http.Get(ts.URL + path)
if err != nil {
t.Fatalf("failed to do request: %v", err)
}
err = ParseErrorResponse(resp)
if err == nil {
t.Errorf("ParseErrorResponse() error = %v, wantErr %v", err, true)
}
var errResp *errcode.ErrorResponse
if ok := errors.As(err, &errResp); !ok {
t.Errorf("errors.As(err, &UnexpectedStatusCodeError) = %v, want %v", ok, true)
}
if want := http.MethodGet; errResp.Method != want {
t.Errorf("ParseErrorResponse() Method = %v, want Method %v", errResp.Method, want)
}
if want := http.StatusUnauthorized; errResp.StatusCode != want {
t.Errorf("ParseErrorResponse() StatusCode = %v, want StatusCode %v", errResp.StatusCode, want)
}
if want := path; errResp.URL.Path != want {
t.Errorf("ParseErrorResponse() URL = %v, want URL %v", errResp.URL.Path, want)
}
for i, e := range errResp.Errors {
if want := expectedErrs[i].Code; e.Code != expectedErrs[i].Code {
t.Errorf("ParseErrorResponse() Code = %v, want Code %v", e.Code, want)
}
if want := expectedErrs[i].Message; e.Message != want {
t.Errorf("ParseErrorResponse() Message = %v, want Code %v", e.Code, want)
}
}
errmsg := err.Error()
if want := "401"; !strings.Contains(errmsg, want) {
t.Errorf("ParseErrorResponse() error = %v, want err message %v", err, want)
}
// first error
if want := "unauthorized"; !strings.Contains(errmsg, want) {
t.Errorf("ParseErrorResponse() error = %v, want err message %v", err, want)
}
if want := "authentication required"; !strings.Contains(errmsg, want) {
t.Errorf("ParseErrorResponse() error = %v, want err message %v", err, want)
}
// second error
if want := "name unknown"; !strings.Contains(errmsg, want) {
t.Errorf("ParseErrorResponse() error = %v, want err message %v", err, want)
}
if want := "repository name not known to registry"; !strings.Contains(errmsg, want) {
t.Errorf("ParseErrorResponse() error = %v, want err message %v", err, want)
}
}
func Test_ParseErrorResponse_plain(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
}))
defer ts.Close()
resp, err := http.Get(ts.URL)
if err != nil {
t.Fatalf("failed to do request: %v", err)
}
err = ParseErrorResponse(resp)
if err == nil {
t.Errorf("ParseErrorResponse() error = %v, wantErr %v", err, true)
}
errmsg := err.Error()
if want := "401"; !strings.Contains(errmsg, want) {
t.Errorf("ParseErrorResponse() error = %v, want err message %v", err, want)
}
if want := http.StatusText(http.StatusUnauthorized); !strings.Contains(errmsg, want) {
t.Errorf("ParseErrorResponse() error = %v, want err message %v", err, want)
}
}
func TestIsErrorCode(t *testing.T) {
tests := []struct {
name string
err error
code string
want bool
}{
{
name: "test errcode.Error, same code",
err: errcode.Error{
Code: errcode.ErrorCodeNameUnknown,
},
code: errcode.ErrorCodeNameUnknown,
want: true,
},
{
name: "test errcode.Error, different code",
err: errcode.Error{
Code: errcode.ErrorCodeUnauthorized,
},
code: errcode.ErrorCodeNameUnknown,
want: false,
},
{
name: "test errcode.Errors containing single error, same code",
err: errcode.Errors{
{
Code: errcode.ErrorCodeNameUnknown,
},
},
code: errcode.ErrorCodeNameUnknown,
want: true,
},
{
name: "test errcode.Errors containing single error, different code",
err: errcode.Errors{
{
Code: errcode.ErrorCodeNameUnknown,
},
},
code: errcode.ErrorCodeNameUnknown,
want: true,
},
{
name: "test errcode.Errors containing multiple errors, same code",
err: errcode.Errors{
{
Code: errcode.ErrorCodeNameUnknown,
},
{
Code: errcode.ErrorCodeUnauthorized,
},
},
code: errcode.ErrorCodeNameUnknown,
want: false,
},
{
name: "test errcode.ErrorResponse containing single error, same code",
err: &errcode.ErrorResponse{
Errors: errcode.Errors{
{
Code: errcode.ErrorCodeNameUnknown,
},
},
},
code: errcode.ErrorCodeNameUnknown,
want: true,
},
{
name: "test errcode.ErrorResponse containing single error, different code",
err: &errcode.ErrorResponse{
Errors: errcode.Errors{
{
Code: errcode.ErrorCodeUnauthorized,
},
},
},
code: errcode.ErrorCodeNameUnknown,
want: false,
},
{
name: "test errcode.ErrorResponse containing multiple errors, same code",
err: &errcode.ErrorResponse{
Errors: errcode.Errors{
{
Code: errcode.ErrorCodeNameUnknown,
},
{
Code: errcode.ErrorCodeUnauthorized,
},
},
},
code: errcode.ErrorCodeNameUnknown,
want: false,
},
{
name: "test unstructured error",
err: errors.New(errcode.ErrorCodeNameUnknown),
code: errcode.ErrorCodeNameUnknown,
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsErrorCode(tt.err, tt.code); got != tt.want {
t.Errorf("IsErrorCode() = %v, want %v", got, tt.want)
}
})
}
}
oras-go-2.5.0/registry/remote/manifest.go 0000664 0000000 0000000 00000003474 14576745303 0020413 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 remote
import (
"strings"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/internal/docker"
"oras.land/oras-go/v2/internal/spec"
)
// defaultManifestMediaTypes contains the default set of manifests media types.
var defaultManifestMediaTypes = []string{
docker.MediaTypeManifest,
docker.MediaTypeManifestList,
ocispec.MediaTypeImageManifest,
ocispec.MediaTypeImageIndex,
spec.MediaTypeArtifactManifest,
}
// defaultManifestAcceptHeader is the default set in the `Accept` header for
// resolving manifests from tags.
var defaultManifestAcceptHeader = strings.Join(defaultManifestMediaTypes, ", ")
// isManifest determines if the given descriptor points to a manifest.
func isManifest(manifestMediaTypes []string, desc ocispec.Descriptor) bool {
if len(manifestMediaTypes) == 0 {
manifestMediaTypes = defaultManifestMediaTypes
}
for _, mediaType := range manifestMediaTypes {
if desc.MediaType == mediaType {
return true
}
}
return false
}
// manifestAcceptHeader generates the set in the `Accept` header for resolving
// manifests from tags.
func manifestAcceptHeader(manifestMediaTypes []string) string {
if len(manifestMediaTypes) == 0 {
return defaultManifestAcceptHeader
}
return strings.Join(manifestMediaTypes, ", ")
}
oras-go-2.5.0/registry/remote/referrers.go 0000664 0000000 0000000 00000014742 14576745303 0020604 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 remote
import (
"errors"
"strings"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/internal/descriptor"
)
// zeroDigest represents a digest that consists of zeros. zeroDigest is used
// for pinging Referrers API.
const zeroDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
// referrersState represents the state of Referrers API.
type referrersState = int32
const (
// referrersStateUnknown represents an unknown state of Referrers API.
referrersStateUnknown referrersState = iota
// referrersStateSupported represents that the repository is known to
// support Referrers API.
referrersStateSupported
// referrersStateUnsupported represents that the repository is known to
// not support Referrers API.
referrersStateUnsupported
)
// referrerOperation represents an operation on a referrer.
type referrerOperation = int32
const (
// referrerOperationAdd represents an addition operation on a referrer.
referrerOperationAdd referrerOperation = iota
// referrerOperationRemove represents a removal operation on a referrer.
referrerOperationRemove
)
// referrerChange represents a change on a referrer.
type referrerChange struct {
referrer ocispec.Descriptor
operation referrerOperation
}
var (
// ErrReferrersCapabilityAlreadySet is returned by SetReferrersCapability()
// when the Referrers API capability has been already set.
ErrReferrersCapabilityAlreadySet = errors.New("referrers capability cannot be changed once set")
// errNoReferrerUpdate is returned by applyReferrerChanges() when there
// is no any referrer update.
errNoReferrerUpdate = errors.New("no referrer update")
)
const (
// opDeleteReferrersIndex represents the operation for deleting a
// referrers index.
opDeleteReferrersIndex = "DeleteReferrersIndex"
)
// ReferrersError records an error and the operation and the subject descriptor.
type ReferrersError struct {
// Op represents the failing operation.
Op string
// Subject is the descriptor of referenced artifact.
Subject ocispec.Descriptor
// Err is the entity of referrers error.
Err error
}
// Error returns error msg of IgnorableError.
func (e *ReferrersError) Error() string {
return e.Err.Error()
}
// Unwrap returns the inner error of IgnorableError.
func (e *ReferrersError) Unwrap() error {
return errors.Unwrap(e.Err)
}
// IsIndexDelete tells if e is kind of error related to referrers
// index deletion.
func (e *ReferrersError) IsReferrersIndexDelete() bool {
return e.Op == opDeleteReferrersIndex
}
// buildReferrersTag builds the referrers tag for the given manifest descriptor.
// Format: -
// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#unavailable-referrers-api
func buildReferrersTag(desc ocispec.Descriptor) string {
alg := desc.Digest.Algorithm().String()
encoded := desc.Digest.Encoded()
return alg + "-" + encoded
}
// isReferrersFilterApplied checks if requsted is in the applied filter list.
func isReferrersFilterApplied(applied, requested string) bool {
if applied == "" || requested == "" {
return false
}
filters := strings.Split(applied, ",")
for _, f := range filters {
if f == requested {
return true
}
}
return false
}
// filterReferrers filters a slice of referrers by artifactType in place.
// The returned slice contains matching referrers.
func filterReferrers(refs []ocispec.Descriptor, artifactType string) []ocispec.Descriptor {
if artifactType == "" {
return refs
}
var j int
for i, ref := range refs {
if ref.ArtifactType == artifactType {
if i != j {
refs[j] = ref
}
j++
}
}
return refs[:j]
}
// applyReferrerChanges applies referrerChanges on referrers and returns the
// updated referrers.
// Returns errNoReferrerUpdate if there is no any referrers updates.
func applyReferrerChanges(referrers []ocispec.Descriptor, referrerChanges []referrerChange) ([]ocispec.Descriptor, error) {
referrersMap := make(map[descriptor.Descriptor]int, len(referrers)+len(referrerChanges))
updatedReferrers := make([]ocispec.Descriptor, 0, len(referrers)+len(referrerChanges))
var updateRequired bool
for _, r := range referrers {
if content.Equal(r, ocispec.Descriptor{}) {
// skip bad entry
updateRequired = true
continue
}
key := descriptor.FromOCI(r)
if _, ok := referrersMap[key]; ok {
// skip duplicates
updateRequired = true
continue
}
updatedReferrers = append(updatedReferrers, r)
referrersMap[key] = len(updatedReferrers) - 1
}
// apply changes
for _, change := range referrerChanges {
key := descriptor.FromOCI(change.referrer)
switch change.operation {
case referrerOperationAdd:
if _, ok := referrersMap[key]; !ok {
// add distinct referrers
updatedReferrers = append(updatedReferrers, change.referrer)
referrersMap[key] = len(updatedReferrers) - 1
}
case referrerOperationRemove:
if pos, ok := referrersMap[key]; ok {
// remove referrers that are already in the map
updatedReferrers[pos] = ocispec.Descriptor{}
delete(referrersMap, key)
}
}
}
// skip unnecessary update
if !updateRequired && len(referrersMap) == len(referrers) {
// if the result referrer map contains the same content as the
// original referrers, consider that there is no update on the
// referrers.
for _, r := range referrers {
key := descriptor.FromOCI(r)
if _, ok := referrersMap[key]; !ok {
updateRequired = true
}
}
if !updateRequired {
return nil, errNoReferrerUpdate
}
}
return removeEmptyDescriptors(updatedReferrers, len(referrersMap)), nil
}
// removeEmptyDescriptors in-place removes empty items from descs, given a hint
// of the number of non-empty descriptors.
func removeEmptyDescriptors(descs []ocispec.Descriptor, hint int) []ocispec.Descriptor {
j := 0
for i, r := range descs {
if !content.Equal(r, ocispec.Descriptor{}) {
if i > j {
descs[j] = r
}
j++
}
if j == hint {
break
}
}
return descs[:j]
}
oras-go-2.5.0/registry/remote/referrers_test.go 0000664 0000000 0000000 00000035567 14576745303 0021653 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 remote
import (
"reflect"
"testing"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/internal/spec"
)
func Test_buildReferrersTag(t *testing.T) {
tests := []struct {
name string
desc ocispec.Descriptor
want string
}{
{
name: "zero digest",
desc: ocispec.Descriptor{
Digest: "sha256:0000000000000000000000000000000000000000000000000000000000000000",
},
want: "sha256-0000000000000000000000000000000000000000000000000000000000000000",
},
{
name: "sha256",
desc: ocispec.Descriptor{
Digest: "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
},
want: "sha256-9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
},
{
name: "sha512",
desc: ocispec.Descriptor{
Digest: "sha512:ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff",
},
want: "sha512-ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := buildReferrersTag(tt.desc); got != tt.want {
t.Errorf("getReferrersTag() = %v, want %v", got, tt.want)
}
})
}
}
func Test_isReferrersFilterApplied(t *testing.T) {
tests := []struct {
name string
applied string
requested string
want bool
}{
{
name: "single filter applied, specified filter matches",
applied: "artifactType",
requested: "artifactType",
want: true,
},
{
name: "single filter applied, specified filter does not match",
applied: "foo",
requested: "artifactType",
want: false,
},
{
name: "multiple filters applied, specified filter matches",
applied: "foo,artifactType",
requested: "artifactType",
want: true,
},
{
name: "multiple filters applied, specified filter does not match",
applied: "foo,bar",
requested: "artifactType",
want: false,
},
{
name: "single filter applied, no specified filter",
applied: "foo",
requested: "",
want: false,
},
{
name: "no filter applied, specified filter does not match",
applied: "",
requested: "artifactType",
want: false,
},
{
name: "no filter applied, no specified filter",
applied: "",
requested: "",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isReferrersFilterApplied(tt.applied, tt.requested); got != tt.want {
t.Errorf("isReferrersFilterApplied() = %v, want %v", got, tt.want)
}
})
}
}
func Test_filterReferrers(t *testing.T) {
refs := []ocispec.Descriptor{
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 1,
Digest: digest.FromString("1"),
ArtifactType: "application/vnd.test",
},
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 2,
Digest: digest.FromString("2"),
ArtifactType: "application/vnd.foo",
},
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 3,
Digest: digest.FromString("3"),
ArtifactType: "application/vnd.bar",
},
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 4,
Digest: digest.FromString("4"),
ArtifactType: "application/vnd.test",
},
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 5,
Digest: digest.FromString("5"),
ArtifactType: "application/vnd.baz",
},
}
got := filterReferrers(refs, "application/vnd.test")
want := []ocispec.Descriptor{
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 1,
Digest: digest.FromString("1"),
ArtifactType: "application/vnd.test",
},
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 4,
Digest: digest.FromString("4"),
ArtifactType: "application/vnd.test",
},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("filterReferrers() = %v, want %v", got, want)
}
}
func Test_filterReferrers_allMatch(t *testing.T) {
refs := []ocispec.Descriptor{
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 1,
Digest: digest.FromString("1"),
ArtifactType: "application/vnd.test",
},
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 4,
Digest: digest.FromString("2"),
ArtifactType: "application/vnd.test",
},
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 5,
Digest: digest.FromString("3"),
ArtifactType: "application/vnd.test",
},
}
got := filterReferrers(refs, "application/vnd.test")
if !reflect.DeepEqual(got, refs) {
t.Errorf("filterReferrers() = %v, want %v", got, refs)
}
}
func Test_applyReferrerChanges(t *testing.T) {
descs := []ocispec.Descriptor{
{
MediaType: ocispec.MediaTypeDescriptor,
Digest: "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
Size: 3,
ArtifactType: "foo",
Annotations: map[string]string{"name": "foo"},
},
{
MediaType: ocispec.MediaTypeDescriptor,
Digest: "sha256:fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9",
Size: 3,
ArtifactType: "bar",
Annotations: map[string]string{"name": "bar"},
},
{
MediaType: ocispec.MediaTypeDescriptor,
Digest: "sha256:baa5a0964d3320fbc0c6a922140453c8513ea24ab8fd0577034804a967248096",
Size: 3,
ArtifactType: "baz",
Annotations: map[string]string{"name": "baz"},
},
{
MediaType: ocispec.MediaTypeDescriptor,
Digest: "sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
Size: 5,
ArtifactType: "hello",
Annotations: map[string]string{"name": "hello"},
},
{
MediaType: ocispec.MediaTypeDescriptor,
Digest: "sha256:82e35a63ceba37e9646434c5dd412ea577147f1e4a41ccde1614253187e3dbf9",
Size: 7,
ArtifactType: "goodbye",
Annotations: map[string]string{"name": "goodbye"},
},
}
tests := []struct {
name string
referrers []ocispec.Descriptor
referrerChanges []referrerChange
want []ocispec.Descriptor
wantErr error
}{
{
name: "add to an empty list",
referrers: []ocispec.Descriptor{},
referrerChanges: []referrerChange{
{descs[0], referrerOperationAdd}, // add new
{descs[1], referrerOperationAdd}, // add new
{descs[2], referrerOperationAdd}, // add new
},
want: []ocispec.Descriptor{
descs[0],
descs[1],
descs[2],
},
wantErr: nil,
},
{
name: "add to a non-empty list",
referrers: []ocispec.Descriptor{
descs[0],
descs[1],
},
referrerChanges: []referrerChange{
{descs[2], referrerOperationAdd}, // add new
{descs[1], referrerOperationAdd}, // add existing
{descs[1], referrerOperationAdd}, // add duplicate existing
{descs[3], referrerOperationAdd}, // add new
{descs[2], referrerOperationAdd}, // add duplicate new
},
want: []ocispec.Descriptor{
descs[0],
descs[1],
descs[2],
descs[3],
},
wantErr: nil,
},
{
name: "partially remove",
referrers: []ocispec.Descriptor{
descs[0],
descs[1],
descs[2],
},
referrerChanges: []referrerChange{
{descs[2], referrerOperationRemove}, // remove existing
{descs[1], referrerOperationRemove}, // remove existing
{descs[3], referrerOperationRemove}, // remove non-existing
{descs[2], referrerOperationRemove}, // remove duplicate existing
{descs[4], referrerOperationRemove}, // remove non-existing
},
want: []ocispec.Descriptor{
descs[0],
},
wantErr: nil,
},
{
name: "remove all",
referrers: []ocispec.Descriptor{
descs[0],
descs[1],
descs[2],
},
referrerChanges: []referrerChange{
{descs[2], referrerOperationRemove}, // remove existing
{descs[0], referrerOperationRemove}, // remove existing
{descs[1], referrerOperationRemove}, // remove existing
},
want: []ocispec.Descriptor{},
wantErr: nil,
},
{
name: "add a new one and remove it",
referrers: []ocispec.Descriptor{
descs[0],
descs[1],
descs[2],
},
referrerChanges: []referrerChange{
{descs[1], referrerOperationAdd}, // add existing
{descs[3], referrerOperationAdd}, // add new
{descs[3], referrerOperationAdd}, // add duplicate new
{descs[3], referrerOperationRemove}, // remove new
{descs[4], referrerOperationAdd}, // add new
},
want: []ocispec.Descriptor{
descs[0],
descs[1],
descs[2],
descs[4],
},
wantErr: nil,
},
{
name: "remove a new one and add it back",
referrers: []ocispec.Descriptor{
descs[0],
descs[1],
descs[2],
},
referrerChanges: []referrerChange{
{descs[1], referrerOperationAdd}, // add existing
{descs[3], referrerOperationAdd}, // add new
{descs[3], referrerOperationRemove}, // remove new,
{descs[3], referrerOperationAdd}, // add new back
{descs[4], referrerOperationAdd}, // add new
},
want: []ocispec.Descriptor{
descs[0],
descs[1],
descs[2],
descs[3],
descs[4],
},
wantErr: nil,
},
{
name: "remove an existing one and add it back",
referrers: []ocispec.Descriptor{
descs[0],
descs[1],
descs[2],
},
referrerChanges: []referrerChange{
{descs[2], referrerOperationRemove}, // remove existing
{descs[3], referrerOperationAdd}, // add new
{descs[2], referrerOperationAdd}, // add existing back
},
want: []ocispec.Descriptor{
descs[0],
descs[1],
descs[3],
descs[2],
},
wantErr: nil,
},
{
name: "list containing duplicate entries",
referrers: []ocispec.Descriptor{
descs[0],
descs[1],
descs[0], // duplicate
descs[2],
descs[3],
descs[1], // duplicate
},
referrerChanges: []referrerChange{
{descs[2], referrerOperationAdd}, // add new
{descs[2], referrerOperationAdd}, // add duplicate new
{descs[3], referrerOperationRemove}, // remove existing
},
want: []ocispec.Descriptor{
descs[0],
descs[1],
descs[2],
},
wantErr: nil,
},
{
name: "list containing bad entries",
referrers: []ocispec.Descriptor{
descs[0],
{},
descs[1],
},
referrerChanges: []referrerChange{
{descs[2], referrerOperationAdd}, // add new
{descs[1], referrerOperationRemove}, // remove existing
},
want: []ocispec.Descriptor{
descs[0],
descs[2],
},
wantErr: nil,
},
{
name: "no update: same order",
referrers: []ocispec.Descriptor{
descs[0],
descs[1],
descs[2],
},
referrerChanges: []referrerChange{
{descs[3], referrerOperationAdd}, // add new
{descs[2], referrerOperationRemove}, // remove existing
{descs[4], referrerOperationAdd}, // add new
{descs[4], referrerOperationRemove}, // remove new
{descs[2], referrerOperationAdd}, // add existing back
{descs[3], referrerOperationRemove}, // remove new
},
want: nil,
wantErr: errNoReferrerUpdate,
},
{
name: "no update: different order",
referrers: []ocispec.Descriptor{
descs[0],
descs[1],
descs[2],
},
referrerChanges: []referrerChange{
{descs[2], referrerOperationRemove}, // remove existing
{descs[0], referrerOperationRemove}, // remove existing
{descs[0], referrerOperationAdd}, // add existing back
{descs[2], referrerOperationAdd}, // add existing back
},
want: nil,
wantErr: errNoReferrerUpdate, // internal result: 2, 1, 0
},
{
name: "no update: list containing duplicate entries",
referrers: []ocispec.Descriptor{
descs[0],
descs[1],
descs[0], // duplicate
descs[2],
descs[1], // duplicate
},
referrerChanges: []referrerChange{
{descs[2], referrerOperationRemove}, // remove existing
{descs[0], referrerOperationRemove}, // remove existing
{descs[0], referrerOperationAdd}, // add existing back
{descs[2], referrerOperationAdd}, // add existing back
},
want: []ocispec.Descriptor{
descs[1],
descs[0],
descs[2],
},
wantErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := applyReferrerChanges(tt.referrers, tt.referrerChanges)
if err != tt.wantErr {
t.Errorf("applyReferrerChanges() error = %v, wantErr %v", err, tt.wantErr)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("applyReferrerChanges() = %v, want %v", got, tt.want)
}
})
}
}
func Test_removeEmptyDescriptors(t *testing.T) {
descs := []ocispec.Descriptor{
{
MediaType: ocispec.MediaTypeDescriptor,
Digest: "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
Size: 3,
ArtifactType: "foo",
Annotations: map[string]string{"name": "foo"},
},
{
MediaType: ocispec.MediaTypeDescriptor,
Digest: "sha256:fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9",
Size: 3,
ArtifactType: "bar",
Annotations: map[string]string{"name": "bar"},
},
{
MediaType: ocispec.MediaTypeDescriptor,
Digest: "sha256:baa5a0964d3320fbc0c6a922140453c8513ea24ab8fd0577034804a967248096",
Size: 3,
ArtifactType: "baz",
Annotations: map[string]string{"name": "baz"},
},
}
tests := []struct {
name string
descs []ocispec.Descriptor
hint int
want []ocispec.Descriptor
}{
{
name: "empty list",
descs: []ocispec.Descriptor{},
hint: 0,
want: []ocispec.Descriptor{},
},
{
name: "all non-empty",
descs: descs,
hint: len(descs),
want: descs,
},
{
name: "all empty",
descs: []ocispec.Descriptor{
{},
{},
{},
},
hint: 0,
want: []ocispec.Descriptor{},
},
{
name: "empty rear",
descs: []ocispec.Descriptor{
descs[0],
{},
descs[2],
{},
{},
},
hint: 2,
want: []ocispec.Descriptor{
descs[0],
descs[2],
},
},
{
name: "empty head",
descs: []ocispec.Descriptor{
{},
descs[0],
descs[1],
{},
{},
descs[2],
},
hint: 3,
want: []ocispec.Descriptor{
descs[0],
descs[1],
descs[2],
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := removeEmptyDescriptors(tt.descs, tt.hint); !reflect.DeepEqual(got, tt.want) {
t.Errorf("removeEmptyDescriptors() = %v, want %v", got, tt.want)
}
})
}
}
oras-go-2.5.0/registry/remote/registry.go 0000664 0000000 0000000 00000012776 14576745303 0020462 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 remote provides a client to the remote registry.
// Reference: https://github.com/distribution/distribution
package remote
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/registry"
"oras.land/oras-go/v2/registry/remote/auth"
"oras.land/oras-go/v2/registry/remote/internal/errutil"
)
// RepositoryOptions is an alias of Repository to avoid name conflicts.
// It also hides all methods associated with Repository.
type RepositoryOptions Repository
// Registry is an HTTP client to a remote registry.
type Registry struct {
// RepositoryOptions contains common options for Registry and Repository.
// It is also used as a template for derived repositories.
RepositoryOptions
// RepositoryListPageSize specifies the page size when invoking the catalog
// API.
// If zero, the page size is determined by the remote registry.
// Reference: https://docs.docker.com/registry/spec/api/#catalog
RepositoryListPageSize int
}
// NewRegistry creates a client to the remote registry with the specified domain
// name.
// Example: localhost:5000
func NewRegistry(name string) (*Registry, error) {
ref := registry.Reference{
Registry: name,
}
if err := ref.ValidateRegistry(); err != nil {
return nil, err
}
return &Registry{
RepositoryOptions: RepositoryOptions{
Reference: ref,
},
}, nil
}
// client returns an HTTP client used to access the remote registry.
// A default HTTP client is return if the client is not configured.
func (r *Registry) client() Client {
if r.Client == nil {
return auth.DefaultClient
}
return r.Client
}
// do sends an HTTP request and returns an HTTP response using the HTTP client
// returned by r.client().
func (r *Registry) do(req *http.Request) (*http.Response, error) {
if r.HandleWarning == nil {
return r.client().Do(req)
}
resp, err := r.client().Do(req)
if err != nil {
return nil, err
}
handleWarningHeaders(resp.Header.Values(headerWarning), r.HandleWarning)
return resp, nil
}
// Ping checks whether or not the registry implement Docker Registry API V2 or
// OCI Distribution Specification.
// Ping can be used to check authentication when an auth client is configured.
//
// References:
// - https://docs.docker.com/registry/spec/api/#base
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#api
func (r *Registry) Ping(ctx context.Context) error {
url := buildRegistryBaseURL(r.PlainHTTP, r.Reference)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
resp, err := r.do(req)
if err != nil {
return err
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
return nil
case http.StatusNotFound:
return errdef.ErrNotFound
default:
return errutil.ParseErrorResponse(resp)
}
}
// Repositories lists the name of repositories available in the registry.
// See also `RepositoryListPageSize`.
//
// If `last` is NOT empty, the entries in the response start after the
// repo specified by `last`. Otherwise, the response starts from the top
// of the Repositories list.
//
// Reference: https://docs.docker.com/registry/spec/api/#catalog
func (r *Registry) Repositories(ctx context.Context, last string, fn func(repos []string) error) error {
ctx = auth.AppendScopesForHost(ctx, r.Reference.Host(), auth.ScopeRegistryCatalog)
url := buildRegistryCatalogURL(r.PlainHTTP, r.Reference)
var err error
for err == nil {
url, err = r.repositories(ctx, last, fn, url)
// clear `last` for subsequent pages
last = ""
}
if err != errNoLink {
return err
}
return nil
}
// repositories returns a single page of repository list with the next link.
func (r *Registry) repositories(ctx context.Context, last string, fn func(repos []string) error, url string) (string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return "", err
}
if r.RepositoryListPageSize > 0 || last != "" {
q := req.URL.Query()
if r.RepositoryListPageSize > 0 {
q.Set("n", strconv.Itoa(r.RepositoryListPageSize))
}
if last != "" {
q.Set("last", last)
}
req.URL.RawQuery = q.Encode()
}
resp, err := r.do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", errutil.ParseErrorResponse(resp)
}
var page struct {
Repositories []string `json:"repositories"`
}
lr := limitReader(resp.Body, r.MaxMetadataBytes)
if err := json.NewDecoder(lr).Decode(&page); err != nil {
return "", fmt.Errorf("%s %q: failed to decode response: %w", resp.Request.Method, resp.Request.URL, err)
}
if err := fn(page.Repositories); err != nil {
return "", err
}
return parseLink(resp)
}
// Repository returns a repository reference by the given name.
func (r *Registry) Repository(ctx context.Context, name string) (registry.Repository, error) {
ref := registry.Reference{
Registry: r.Reference.Registry,
Repository: name,
}
return newRepositoryWithOptions(ref, &r.RepositoryOptions)
}
oras-go-2.5.0/registry/remote/registry_test.go 0000664 0000000 0000000 00000024505 14576745303 0021512 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 remote
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strconv"
"strings"
"testing"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/registry"
)
func TestRegistryInterface(t *testing.T) {
var reg interface{} = &Registry{}
if _, ok := reg.(registry.Registry); !ok {
t.Error("&Registry{} does not conform registry.Registry")
}
}
func TestRegistry_TLS(t *testing.T) {
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || r.URL.Path != "/v2/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
reg, err := NewRegistry(uri.Host)
if err != nil {
t.Fatalf("NewRegistry() error = %v", err)
}
reg.Client = ts.Client()
ctx := context.Background()
if err := reg.Ping(ctx); err != nil {
t.Errorf("Registry.Ping() error = %v", err)
}
}
func TestRegistry_Ping(t *testing.T) {
v2Implemented := true
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || r.URL.Path != "/v2/" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
if v2Implemented {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
reg, err := NewRegistry(uri.Host)
if err != nil {
t.Fatalf("NewRegistry() error = %v", err)
}
reg.PlainHTTP = true
ctx := context.Background()
if err := reg.Ping(ctx); err != nil {
t.Errorf("Registry.Ping() error = %v", err)
}
v2Implemented = false
if err := reg.Ping(ctx); err == nil {
t.Errorf("Registry.Ping() error = %v, wantErr %v", err, errdef.ErrNotFound)
}
}
func TestRegistry_Repositories(t *testing.T) {
repoSet := [][]string{
{"the", "quick", "brown", "fox"},
{"jumps", "over", "the", "lazy"},
{"dog"},
}
var ts *httptest.Server
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || r.URL.Path != "/v2/_catalog" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
q := r.URL.Query()
n, err := strconv.Atoi(q.Get("n"))
if err != nil || n != 4 {
t.Errorf("bad page size: %s", q.Get("n"))
w.WriteHeader(http.StatusBadRequest)
return
}
var repos []string
switch q.Get("test") {
case "foo":
repos = repoSet[1]
w.Header().Set("Link", fmt.Sprintf(`<%s/v2/_catalog?n=4&test=bar>; rel="next"`, ts.URL))
case "bar":
repos = repoSet[2]
default:
repos = repoSet[0]
w.Header().Set("Link", `; rel="next"`)
}
result := struct {
Repositories []string `json:"repositories"`
}{
Repositories: repos,
}
if err := json.NewEncoder(w).Encode(result); err != nil {
t.Errorf("failed to write response: %v", err)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
reg, err := NewRegistry(uri.Host)
if err != nil {
t.Fatalf("NewRegistry() error = %v", err)
}
reg.PlainHTTP = true
reg.RepositoryListPageSize = 4
ctx := context.Background()
index := 0
if err := reg.Repositories(ctx, "", func(got []string) error {
if index > 2 {
t.Fatalf("out of index bound: %d", index)
}
repos := repoSet[index]
index++
if !reflect.DeepEqual(got, repos) {
t.Errorf("Registry.Repositories() = %v, want %v", got, repos)
}
return nil
}); err != nil {
t.Fatalf("Registry.Repositories() error = %v", err)
}
}
func TestRegistry_Repository(t *testing.T) {
reg, err := NewRegistry("localhost:5000")
if err != nil {
t.Fatalf("NewRegistry() error = %v", err)
}
reg.PlainHTTP = true
reg.SkipReferrersGC = true
reg.RepositoryListPageSize = 50
reg.TagListPageSize = 100
reg.ReferrerListPageSize = 10
reg.MaxMetadataBytes = 8 * 1024 * 1024
ctx := context.Background()
got, err := reg.Repository(ctx, "hello-world")
if err != nil {
t.Fatalf("Registry.Repository() error = %v", err)
}
reg.Reference.Repository = "hello-world"
want := (*Repository)(®.RepositoryOptions)
if !reflect.DeepEqual(got, want) {
t.Errorf("Registry.Repository() = %v, want %v", got, want)
}
}
// Testing `last` parameter for Repositories list
func TestRegistry_Repositories_WithLastParam(t *testing.T) {
repoSet := strings.Split("abcdefghijklmnopqrstuvwxyz", "")
var offset int
var ts *httptest.Server
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || r.URL.Path != "/v2/_catalog" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
q := r.URL.Query()
n, err := strconv.Atoi(q.Get("n"))
if err != nil || n != 4 {
t.Errorf("bad page size: %s", q.Get("n"))
w.WriteHeader(http.StatusBadRequest)
return
}
last := q.Get("last")
if last != "" {
offset = indexOf(last, repoSet) + 1
}
var repos []string
switch q.Get("test") {
case "foo":
repos = repoSet[offset : offset+n]
w.Header().Set("Link", fmt.Sprintf(`<%s/v2/_catalog?n=4&last=v&test=bar>; rel="next"`, ts.URL))
case "bar":
repos = repoSet[offset : offset+n]
default:
repos = repoSet[offset : offset+n]
w.Header().Set("Link", fmt.Sprintf(`<%s/v2/_catalog?n=4&last=r&test=foo>; rel="next"`, ts.URL))
}
result := struct {
Repositories []string `json:"repositories"`
}{
Repositories: repos,
}
if err := json.NewEncoder(w).Encode(result); err != nil {
t.Errorf("failed to write response: %v", err)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
reg, err := NewRegistry(uri.Host)
if err != nil {
t.Fatalf("NewRegistry() error = %v", err)
}
reg.PlainHTTP = true
reg.RepositoryListPageSize = 4
last := "n"
startInd := indexOf(last, repoSet) + 1
ctx := context.Background()
if err := reg.Repositories(ctx, last, func(got []string) error {
want := repoSet[startInd : startInd+reg.RepositoryListPageSize]
startInd += reg.RepositoryListPageSize
if !reflect.DeepEqual(got, want) {
t.Errorf("Registry.Repositories() = %v, want %v", got, want)
}
return nil
}); err != nil {
t.Fatalf("Registry.Repositories() error = %v", err)
}
}
func TestRegistry_do(t *testing.T) {
data := []byte(`hello world!`)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || r.URL.Path != "/test" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
w.Header().Add("Warning", `299 - "Test 1: Good warning."`)
w.Header().Add("Warning", `199 - "Test 2: Warning with a non-299 code."`)
w.Header().Add("Warning", `299 - "Test 3: Good warning."`)
w.Header().Add("Warning", `299 myregistry.example.com "Test 4: Warning with a non-unknown agent"`)
w.Header().Add("Warning", `299 - "Test 5: Warning with a date." "Sat, 25 Aug 2012 23:34:45 GMT"`)
w.Header().Add("wArnIng", `299 - "Test 6: Good warning."`)
w.Write(data)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
testURL := ts.URL + "/test"
// test do() without HandleWarning
reg, err := NewRegistry(uri.Host)
if err != nil {
t.Fatal("NewRegistry() error =", err)
}
req, err := http.NewRequest(http.MethodGet, testURL, nil)
if err != nil {
t.Fatal("failed to create test request:", err)
}
resp, err := reg.do(req)
if err != nil {
t.Fatal("Registry.do() error =", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Registry.do() status code = %v, want %v", resp.StatusCode, http.StatusOK)
}
if got := len(resp.Header["Warning"]); got != 6 {
t.Errorf("Registry.do() warning header len = %v, want %v", got, 6)
}
got, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal("io.ReadAll() error =", err)
}
resp.Body.Close()
if !bytes.Equal(got, data) {
t.Errorf("Registry.do() = %v, want %v", got, data)
}
// test do() with HandleWarning
reg, err = NewRegistry(uri.Host)
if err != nil {
t.Fatal("NewRegistry() error =", err)
}
var gotWarnings []Warning
reg.HandleWarning = func(warning Warning) {
gotWarnings = append(gotWarnings, warning)
}
req, err = http.NewRequest(http.MethodGet, testURL, nil)
if err != nil {
t.Fatal("failed to create test request:", err)
}
resp, err = reg.do(req)
if err != nil {
t.Fatal("Registry.do() error =", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Registry.do() status code = %v, want %v", resp.StatusCode, http.StatusOK)
}
if got := len(resp.Header["Warning"]); got != 6 {
t.Errorf("Registry.do() warning header len = %v, want %v", got, 6)
}
got, err = io.ReadAll(resp.Body)
if err != nil {
t.Errorf("Registry.do() = %v, want %v", got, data)
}
resp.Body.Close()
if !bytes.Equal(got, data) {
t.Errorf("Registry.do() = %v, want %v", got, data)
}
wantWarnings := []Warning{
{
WarningValue: WarningValue{
Code: 299,
Agent: "-",
Text: "Test 1: Good warning.",
},
},
{
WarningValue: WarningValue{
Code: 299,
Agent: "-",
Text: "Test 3: Good warning.",
},
},
{
WarningValue: WarningValue{
Code: 299,
Agent: "-",
Text: "Test 6: Good warning.",
},
},
}
if !reflect.DeepEqual(gotWarnings, wantWarnings) {
t.Errorf("Registry.do() = %v, want %v", gotWarnings, wantWarnings)
}
}
// indexOf returns the index of an element within a slice
func indexOf(element string, data []string) int {
for ind, val := range data {
if element == val {
return ind
}
}
return -1 //not found.
}
oras-go-2.5.0/registry/remote/repository.go 0000664 0000000 0000000 00000163240 14576745303 0021022 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 remote
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"mime"
"net/http"
"slices"
"strconv"
"strings"
"sync"
"sync/atomic"
"github.com/opencontainers/go-digest"
specs "github.com/opencontainers/image-spec/specs-go"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/internal/cas"
"oras.land/oras-go/v2/internal/httputil"
"oras.land/oras-go/v2/internal/ioutil"
"oras.land/oras-go/v2/internal/spec"
"oras.land/oras-go/v2/internal/syncutil"
"oras.land/oras-go/v2/registry"
"oras.land/oras-go/v2/registry/remote/auth"
"oras.land/oras-go/v2/registry/remote/errcode"
"oras.land/oras-go/v2/registry/remote/internal/errutil"
)
const (
// headerDockerContentDigest is the "Docker-Content-Digest" header.
// If present on the response, it contains the canonical digest of the
// uploaded blob.
//
// References:
// - https://docs.docker.com/registry/spec/api/#digest-header
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#pull
headerDockerContentDigest = "Docker-Content-Digest"
// headerOCIFiltersApplied is the "OCI-Filters-Applied" header.
// If present on the response, it contains a comma-separated list of the
// applied filters.
//
// Reference:
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers
headerOCIFiltersApplied = "OCI-Filters-Applied"
// headerOCISubject is the "OCI-Subject" header.
// If present on the response, it contains the digest of the subject,
// indicating that Referrers API is supported by the registry.
headerOCISubject = "OCI-Subject"
)
// filterTypeArtifactType is the "artifactType" filter applied on the list of
// referrers.
//
// References:
// - Latest spec: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers
// - Compatible spec: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#listing-referrers
const filterTypeArtifactType = "artifactType"
// Client is an interface for a HTTP client.
type Client interface {
// Do sends an HTTP request and returns an HTTP response.
//
// Unlike http.RoundTripper, Client can attempt to interpret the response
// and handle higher-level protocol details such as redirects and
// authentication.
//
// Like http.RoundTripper, Client should not modify the request, and must
// always close the request body.
Do(*http.Request) (*http.Response, error)
}
// Repository is an HTTP client to a remote repository.
type Repository struct {
// Client is the underlying HTTP client used to access the remote registry.
// If nil, auth.DefaultClient is used.
Client Client
// Reference references the remote repository.
Reference registry.Reference
// PlainHTTP signals the transport to access the remote repository via HTTP
// instead of HTTPS.
PlainHTTP bool
// ManifestMediaTypes is used in `Accept` header for resolving manifests
// from references. It is also used in identifying manifests and blobs from
// descriptors. If an empty list is present, default manifest media types
// are used.
ManifestMediaTypes []string
// TagListPageSize specifies the page size when invoking the tag list API.
// If zero, the page size is determined by the remote registry.
// Reference: https://docs.docker.com/registry/spec/api/#tags
TagListPageSize int
// ReferrerListPageSize specifies the page size when invoking the Referrers
// API.
// If zero, the page size is determined by the remote registry.
// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers
ReferrerListPageSize int
// MaxMetadataBytes specifies a limit on how many response bytes are allowed
// in the server's response to the metadata APIs, such as catalog list, tag
// list, and referrers list.
// If less than or equal to zero, a default (currently 4MiB) is used.
MaxMetadataBytes int64
// SkipReferrersGC specifies whether to delete the dangling referrers
// index when referrers tag schema is utilized.
// - If false, the old referrers index will be deleted after the new one
// is successfully uploaded.
// - If true, the old referrers index is kept.
// By default, it is disabled (set to false). See also:
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#referrers-tag-schema
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#pushing-manifests-with-subject
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#deleting-manifests
SkipReferrersGC bool
// HandleWarning handles the warning returned by the remote server.
// Callers SHOULD deduplicate warnings from multiple associated responses.
//
// References:
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#warnings
// - https://www.rfc-editor.org/rfc/rfc7234#section-5.5
HandleWarning func(warning Warning)
// NOTE: Must keep fields in sync with clone().
// referrersState represents that if the repository supports Referrers API.
// default: referrersStateUnknown
referrersState referrersState
// referrersPingLock locks the pingReferrers() method and allows only
// one go-routine to send the request.
referrersPingLock sync.Mutex
// referrersMergePool provides a way to manage concurrent updates to a
// referrers index tagged by referrers tag schema.
referrersMergePool syncutil.Pool[syncutil.Merge[referrerChange]]
}
// NewRepository creates a client to the remote repository identified by a
// reference.
// Example: localhost:5000/hello-world
func NewRepository(reference string) (*Repository, error) {
ref, err := registry.ParseReference(reference)
if err != nil {
return nil, err
}
return &Repository{
Reference: ref,
}, nil
}
// newRepositoryWithOptions returns a Repository with the given Reference and
// RepositoryOptions.
//
// RepositoryOptions are part of the Registry struct and set its defaults.
// RepositoryOptions shares the same struct definition as Repository, which
// contains unexported state that must not be copied to multiple Repositories.
// To handle this we explicitly copy only the fields that we want to reproduce.
func newRepositoryWithOptions(ref registry.Reference, opts *RepositoryOptions) (*Repository, error) {
if err := ref.ValidateRepository(); err != nil {
return nil, err
}
repo := (*Repository)(opts).clone()
repo.Reference = ref
return repo, nil
}
// clone makes a copy of the Repository being careful not to copy non-copyable fields (sync.Mutex and syncutil.Pool types)
func (r *Repository) clone() *Repository {
return &Repository{
Client: r.Client,
Reference: r.Reference,
PlainHTTP: r.PlainHTTP,
ManifestMediaTypes: slices.Clone(r.ManifestMediaTypes),
TagListPageSize: r.TagListPageSize,
ReferrerListPageSize: r.ReferrerListPageSize,
MaxMetadataBytes: r.MaxMetadataBytes,
SkipReferrersGC: r.SkipReferrersGC,
HandleWarning: r.HandleWarning,
}
}
// SetReferrersCapability indicates the Referrers API capability of the remote
// repository. true: capable; false: not capable.
//
// SetReferrersCapability is valid only when it is called for the first time.
// SetReferrersCapability returns ErrReferrersCapabilityAlreadySet if the
// Referrers API capability has been already set.
// - When the capability is set to true, the Referrers() function will always
// request the Referrers API. Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers
// - When the capability is set to false, the Referrers() function will always
// request the Referrers Tag. Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#referrers-tag-schema
// - When the capability is not set, the Referrers() function will automatically
// determine which API to use.
func (r *Repository) SetReferrersCapability(capable bool) error {
var state referrersState
if capable {
state = referrersStateSupported
} else {
state = referrersStateUnsupported
}
if swapped := atomic.CompareAndSwapInt32(&r.referrersState, referrersStateUnknown, state); !swapped {
if fact := r.loadReferrersState(); fact != state {
return fmt.Errorf("%w: current capability = %v, new capability = %v",
ErrReferrersCapabilityAlreadySet,
fact == referrersStateSupported,
capable)
}
}
return nil
}
// setReferrersState atomically loads r.referrersState.
func (r *Repository) loadReferrersState() referrersState {
return atomic.LoadInt32(&r.referrersState)
}
// client returns an HTTP client used to access the remote repository.
// A default HTTP client is return if the client is not configured.
func (r *Repository) client() Client {
if r.Client == nil {
return auth.DefaultClient
}
return r.Client
}
// do sends an HTTP request and returns an HTTP response using the HTTP client
// returned by r.client().
func (r *Repository) do(req *http.Request) (*http.Response, error) {
if r.HandleWarning == nil {
return r.client().Do(req)
}
resp, err := r.client().Do(req)
if err != nil {
return nil, err
}
handleWarningHeaders(resp.Header.Values(headerWarning), r.HandleWarning)
return resp, nil
}
// blobStore detects the blob store for the given descriptor.
func (r *Repository) blobStore(desc ocispec.Descriptor) registry.BlobStore {
if isManifest(r.ManifestMediaTypes, desc) {
return r.Manifests()
}
return r.Blobs()
}
// Fetch fetches the content identified by the descriptor.
func (r *Repository) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
return r.blobStore(target).Fetch(ctx, target)
}
// Push pushes the content, matching the expected descriptor.
func (r *Repository) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error {
return r.blobStore(expected).Push(ctx, expected, content)
}
// Mount makes the blob with the given digest in fromRepo
// available in the repository signified by the receiver.
//
// This avoids the need to pull content down from fromRepo only to push it to r.
//
// If the registry does not implement mounting, getContent will be used to get the
// content to push. If getContent is nil, the content will be pulled from the source
// repository. If getContent returns an error, it will be wrapped inside the error
// returned from Mount.
func (r *Repository) Mount(ctx context.Context, desc ocispec.Descriptor, fromRepo string, getContent func() (io.ReadCloser, error)) error {
return r.Blobs().(registry.Mounter).Mount(ctx, desc, fromRepo, getContent)
}
// Exists returns true if the described content exists.
func (r *Repository) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) {
return r.blobStore(target).Exists(ctx, target)
}
// Delete removes the content identified by the descriptor.
func (r *Repository) Delete(ctx context.Context, target ocispec.Descriptor) error {
return r.blobStore(target).Delete(ctx, target)
}
// Blobs provides access to the blob CAS only, which contains config blobs,
// layers, and other generic blobs.
func (r *Repository) Blobs() registry.BlobStore {
return &blobStore{repo: r}
}
// Manifests provides access to the manifest CAS only.
func (r *Repository) Manifests() registry.ManifestStore {
return &manifestStore{repo: r}
}
// Resolve resolves a reference to a manifest descriptor.
// See also `ManifestMediaTypes`.
func (r *Repository) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) {
return r.Manifests().Resolve(ctx, reference)
}
// Tag tags a manifest descriptor with a reference string.
func (r *Repository) Tag(ctx context.Context, desc ocispec.Descriptor, reference string) error {
return r.Manifests().Tag(ctx, desc, reference)
}
// PushReference pushes the manifest with a reference tag.
func (r *Repository) PushReference(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error {
return r.Manifests().PushReference(ctx, expected, content, reference)
}
// FetchReference fetches the manifest identified by the reference.
// The reference can be a tag or digest.
func (r *Repository) FetchReference(ctx context.Context, reference string) (ocispec.Descriptor, io.ReadCloser, error) {
return r.Manifests().FetchReference(ctx, reference)
}
// ParseReference resolves a tag or a digest reference to a fully qualified
// reference from a base reference r.Reference.
// Tag, digest, or fully qualified references are accepted as input.
//
// If reference is a fully qualified reference, then ParseReference parses it
// and returns the parsed reference. If the parsed reference does not share
// the same base reference with the Repository r, ParseReference returns a
// wrapped error ErrInvalidReference.
func (r *Repository) ParseReference(reference string) (registry.Reference, error) {
ref, err := registry.ParseReference(reference)
if err != nil {
ref = registry.Reference{
Registry: r.Reference.Registry,
Repository: r.Reference.Repository,
Reference: reference,
}
// reference is not a FQDN
if index := strings.IndexByte(reference, '@'); index != -1 {
// `@` implies *digest*, so drop the *tag* (irrespective of what it is).
ref.Reference = reference[index+1:]
err = ref.ValidateReferenceAsDigest()
} else {
err = ref.ValidateReference()
}
if err != nil {
return registry.Reference{}, err
}
} else if ref.Registry != r.Reference.Registry || ref.Repository != r.Reference.Repository {
return registry.Reference{}, fmt.Errorf(
"%w: mismatch between received %q and expected %q",
errdef.ErrInvalidReference, ref, r.Reference,
)
}
if len(ref.Reference) == 0 {
return registry.Reference{}, errdef.ErrInvalidReference
}
return ref, nil
}
// Tags lists the tags available in the repository.
// See also `TagListPageSize`.
// If `last` is NOT empty, the entries in the response start after the
// tag specified by `last`. Otherwise, the response starts from the top
// of the Tags list.
//
// References:
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#content-discovery
// - https://docs.docker.com/registry/spec/api/#tags
func (r *Repository) Tags(ctx context.Context, last string, fn func(tags []string) error) error {
ctx = auth.AppendRepositoryScope(ctx, r.Reference, auth.ActionPull)
url := buildRepositoryTagListURL(r.PlainHTTP, r.Reference)
var err error
for err == nil {
url, err = r.tags(ctx, last, fn, url)
// clear `last` for subsequent pages
last = ""
}
if err != errNoLink {
return err
}
return nil
}
// tags returns a single page of tag list with the next link.
func (r *Repository) tags(ctx context.Context, last string, fn func(tags []string) error, url string) (string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return "", err
}
if r.TagListPageSize > 0 || last != "" {
q := req.URL.Query()
if r.TagListPageSize > 0 {
q.Set("n", strconv.Itoa(r.TagListPageSize))
}
if last != "" {
q.Set("last", last)
}
req.URL.RawQuery = q.Encode()
}
resp, err := r.do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", errutil.ParseErrorResponse(resp)
}
var page struct {
Tags []string `json:"tags"`
}
lr := limitReader(resp.Body, r.MaxMetadataBytes)
if err := json.NewDecoder(lr).Decode(&page); err != nil {
return "", fmt.Errorf("%s %q: failed to decode response: %w", resp.Request.Method, resp.Request.URL, err)
}
if err := fn(page.Tags); err != nil {
return "", err
}
return parseLink(resp)
}
// Predecessors returns the descriptors of image or artifact manifests directly
// referencing the given manifest descriptor.
// Predecessors internally leverages Referrers.
// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers
func (r *Repository) Predecessors(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
var res []ocispec.Descriptor
if err := r.Referrers(ctx, desc, "", func(referrers []ocispec.Descriptor) error {
res = append(res, referrers...)
return nil
}); err != nil {
return nil, err
}
return res, nil
}
// Referrers lists the descriptors of image or artifact manifests directly
// referencing the given manifest descriptor.
//
// fn is called for each page of the referrers result.
// If artifactType is not empty, only referrers of the same artifact type are
// fed to fn.
//
// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers
func (r *Repository) Referrers(ctx context.Context, desc ocispec.Descriptor, artifactType string, fn func(referrers []ocispec.Descriptor) error) error {
state := r.loadReferrersState()
if state == referrersStateUnsupported {
// The repository is known to not support Referrers API, fallback to
// referrers tag schema.
return r.referrersByTagSchema(ctx, desc, artifactType, fn)
}
err := r.referrersByAPI(ctx, desc, artifactType, fn)
if state == referrersStateSupported {
// The repository is known to support Referrers API, no fallback.
return err
}
// The referrers state is unknown.
if err != nil {
if errors.Is(err, errdef.ErrUnsupported) {
// Referrers API is not supported, fallback to referrers tag schema.
r.SetReferrersCapability(false)
return r.referrersByTagSchema(ctx, desc, artifactType, fn)
}
return err
}
r.SetReferrersCapability(true)
return nil
}
// referrersByAPI lists the descriptors of manifests directly referencing
// the given manifest descriptor by requesting Referrers API.
// fn is called for the referrers result. If artifactType is not empty,
// only referrers of the same artifact type are fed to fn.
func (r *Repository) referrersByAPI(ctx context.Context, desc ocispec.Descriptor, artifactType string, fn func(referrers []ocispec.Descriptor) error) error {
ref := r.Reference
ref.Reference = desc.Digest.String()
ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull)
url := buildReferrersURL(r.PlainHTTP, ref, artifactType)
var err error
for err == nil {
url, err = r.referrersPageByAPI(ctx, artifactType, fn, url)
}
if err == errNoLink {
return nil
}
return err
}
// referrersPageByAPI lists a single page of the descriptors of manifests
// directly referencing the given manifest descriptor. fn is called for
// a page of referrersPageByAPI result.
// If artifactType is not empty, only referrersPageByAPI of the same
// artifact type are fed to fn.
// referrersPageByAPI returns the link url for the next page.
func (r *Repository) referrersPageByAPI(ctx context.Context, artifactType string, fn func(referrers []ocispec.Descriptor) error, url string) (string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return "", err
}
if r.ReferrerListPageSize > 0 {
q := req.URL.Query()
q.Set("n", strconv.Itoa(r.ReferrerListPageSize))
req.URL.RawQuery = q.Encode()
}
resp, err := r.do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
case http.StatusNotFound:
if errResp := errutil.ParseErrorResponse(resp); errutil.IsErrorCode(errResp, errcode.ErrorCodeNameUnknown) {
// The repository is not found, Referrers API status is unknown
return "", errResp
}
// Referrers API is not supported.
return "", fmt.Errorf("failed to query referrers API: %w", errdef.ErrUnsupported)
default:
return "", errutil.ParseErrorResponse(resp)
}
// also check the content type
if ct := resp.Header.Get("Content-Type"); ct != ocispec.MediaTypeImageIndex {
return "", fmt.Errorf("unknown content returned (%s), expecting image index: %w", ct, errdef.ErrUnsupported)
}
var index ocispec.Index
lr := limitReader(resp.Body, r.MaxMetadataBytes)
if err := json.NewDecoder(lr).Decode(&index); err != nil {
return "", fmt.Errorf("%s %q: failed to decode response: %w", resp.Request.Method, resp.Request.URL, err)
}
referrers := index.Manifests
if artifactType != "" {
// check both filters header and filters annotations for compatibility
// latest spec for filters header: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers
// older spec for filters annotations: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#listing-referrers
filtersHeader := resp.Header.Get(headerOCIFiltersApplied)
filtersAnnotation := index.Annotations[spec.AnnotationReferrersFiltersApplied]
if !isReferrersFilterApplied(filtersHeader, filterTypeArtifactType) &&
!isReferrersFilterApplied(filtersAnnotation, filterTypeArtifactType) {
// perform client side filtering if the filter is not applied on the server side
referrers = filterReferrers(referrers, artifactType)
}
}
if len(referrers) > 0 {
if err := fn(referrers); err != nil {
return "", err
}
}
return parseLink(resp)
}
// referrersByTagSchema lists the descriptors of manifests directly
// referencing the given manifest descriptor by requesting referrers tag.
// fn is called for the referrers result. If artifactType is not empty,
// only referrers of the same artifact type are fed to fn.
// reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#backwards-compatibility
func (r *Repository) referrersByTagSchema(ctx context.Context, desc ocispec.Descriptor, artifactType string, fn func(referrers []ocispec.Descriptor) error) error {
referrersTag := buildReferrersTag(desc)
_, referrers, err := r.referrersFromIndex(ctx, referrersTag)
if err != nil {
if errors.Is(err, errdef.ErrNotFound) {
// no referrers to the manifest
return nil
}
return err
}
filtered := filterReferrers(referrers, artifactType)
if len(filtered) == 0 {
return nil
}
return fn(filtered)
}
// referrersFromIndex queries the referrers index using the the given referrers
// tag. If Succeeded, returns the descriptor of referrers index and the
// referrers list.
func (r *Repository) referrersFromIndex(ctx context.Context, referrersTag string) (ocispec.Descriptor, []ocispec.Descriptor, error) {
desc, rc, err := r.FetchReference(ctx, referrersTag)
if err != nil {
return ocispec.Descriptor{}, nil, err
}
defer rc.Close()
if err := limitSize(desc, r.MaxMetadataBytes); err != nil {
return ocispec.Descriptor{}, nil, fmt.Errorf("failed to read referrers index from referrers tag %s: %w", referrersTag, err)
}
var index ocispec.Index
if err := decodeJSON(rc, desc, &index); err != nil {
return ocispec.Descriptor{}, nil, fmt.Errorf("failed to decode referrers index from referrers tag %s: %w", referrersTag, err)
}
return desc, index.Manifests, nil
}
// pingReferrers returns true if the Referrers API is available for r.
func (r *Repository) pingReferrers(ctx context.Context) (bool, error) {
switch r.loadReferrersState() {
case referrersStateSupported:
return true, nil
case referrersStateUnsupported:
return false, nil
}
// referrers state is unknown
// limit the rate of pinging referrers API
r.referrersPingLock.Lock()
defer r.referrersPingLock.Unlock()
switch r.loadReferrersState() {
case referrersStateSupported:
return true, nil
case referrersStateUnsupported:
return false, nil
}
ref := r.Reference
ref.Reference = zeroDigest
ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull)
url := buildReferrersURL(r.PlainHTTP, ref, "")
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return false, err
}
resp, err := r.do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
supported := resp.Header.Get("Content-Type") == ocispec.MediaTypeImageIndex
r.SetReferrersCapability(supported)
return supported, nil
case http.StatusNotFound:
if err := errutil.ParseErrorResponse(resp); errutil.IsErrorCode(err, errcode.ErrorCodeNameUnknown) {
// repository not found
return false, err
}
r.SetReferrersCapability(false)
return false, nil
default:
return false, errutil.ParseErrorResponse(resp)
}
}
// delete removes the content identified by the descriptor in the entity "blobs"
// or "manifests".
func (r *Repository) delete(ctx context.Context, target ocispec.Descriptor, isManifest bool) error {
ref := r.Reference
ref.Reference = target.Digest.String()
ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionDelete)
buildURL := buildRepositoryBlobURL
if isManifest {
buildURL = buildRepositoryManifestURL
}
url := buildURL(r.PlainHTTP, ref)
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil)
if err != nil {
return err
}
resp, err := r.do(req)
if err != nil {
return err
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusAccepted:
return verifyContentDigest(resp, target.Digest)
case http.StatusNotFound:
return fmt.Errorf("%s: %w", target.Digest, errdef.ErrNotFound)
default:
return errutil.ParseErrorResponse(resp)
}
}
// blobStore accesses the blob part of the repository.
type blobStore struct {
repo *Repository
}
// Fetch fetches the content identified by the descriptor.
func (s *blobStore) Fetch(ctx context.Context, target ocispec.Descriptor) (rc io.ReadCloser, err error) {
ref := s.repo.Reference
ref.Reference = target.Digest.String()
ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull)
url := buildRepositoryBlobURL(s.repo.PlainHTTP, ref)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := s.repo.do(req)
if err != nil {
return nil, err
}
defer func() {
if err != nil {
resp.Body.Close()
}
}()
switch resp.StatusCode {
case http.StatusOK: // server does not support seek as `Range` was ignored.
if size := resp.ContentLength; size != -1 && size != target.Size {
return nil, fmt.Errorf("%s %q: mismatch Content-Length", resp.Request.Method, resp.Request.URL)
}
// check server range request capability.
// Docker spec allows range header form of "Range: bytes=-".
// However, the remote server may still not RFC 7233 compliant.
// Reference: https://docs.docker.com/registry/spec/api/#blob
if rangeUnit := resp.Header.Get("Accept-Ranges"); rangeUnit == "bytes" {
return httputil.NewReadSeekCloser(s.repo.client(), req, resp.Body, target.Size), nil
}
return resp.Body, nil
case http.StatusNotFound:
return nil, fmt.Errorf("%s: %w", target.Digest, errdef.ErrNotFound)
default:
return nil, errutil.ParseErrorResponse(resp)
}
}
// Mount mounts the given descriptor from fromRepo into s.
func (s *blobStore) Mount(ctx context.Context, desc ocispec.Descriptor, fromRepo string, getContent func() (io.ReadCloser, error)) error {
// pushing usually requires both pull and push actions.
// Reference: https://github.com/distribution/distribution/blob/v2.7.1/registry/handlers/app.go#L921-L930
ctx = auth.AppendRepositoryScope(ctx, s.repo.Reference, auth.ActionPull, auth.ActionPush)
// We also need pull access to the source repo.
fromRef := s.repo.Reference
fromRef.Repository = fromRepo
ctx = auth.AppendRepositoryScope(ctx, fromRef, auth.ActionPull)
url := buildRepositoryBlobMountURL(s.repo.PlainHTTP, s.repo.Reference, desc.Digest, fromRepo)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil)
if err != nil {
return err
}
resp, err := s.repo.do(req)
if err != nil {
return err
}
if resp.StatusCode == http.StatusCreated {
defer resp.Body.Close()
// Check the server seems to be behaving.
return verifyContentDigest(resp, desc.Digest)
}
if resp.StatusCode != http.StatusAccepted {
defer resp.Body.Close()
return errutil.ParseErrorResponse(resp)
}
resp.Body.Close()
// From the [spec]:
//
// "If a registry does not support cross-repository mounting
// or is unable to mount the requested blob,
// it SHOULD return a 202.
// This indicates that the upload session has begun
// and that the client MAY proceed with the upload."
//
// So we need to get the content from somewhere in order to
// push it. If the caller has provided a getContent function, we
// can use that, otherwise pull the content from the source repository.
//
// [spec]: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#mounting-a-blob-from-another-repository
var r io.ReadCloser
if getContent != nil {
r, err = getContent()
} else {
r, err = s.sibling(fromRepo).Fetch(ctx, desc)
}
if err != nil {
return fmt.Errorf("cannot read source blob: %w", err)
}
defer r.Close()
return s.completePushAfterInitialPost(ctx, req, resp, desc, r)
}
// sibling returns a blob store for another repository in the same
// registry.
func (s *blobStore) sibling(otherRepoName string) *blobStore {
otherRepo := s.repo.clone()
otherRepo.Reference.Repository = otherRepoName
return &blobStore{
repo: otherRepo,
}
}
// Push pushes the content, matching the expected descriptor.
// Existing content is not checked by Push() to minimize the number of out-going
// requests.
// Push is done by conventional 2-step monolithic upload instead of a single
// `POST` request for better overall performance. It also allows early fail on
// authentication errors.
//
// References:
// - https://docs.docker.com/registry/spec/api/#pushing-an-image
// - https://docs.docker.com/registry/spec/api/#initiate-blob-upload
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#pushing-a-blob-monolithically
func (s *blobStore) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error {
// start an upload
// pushing usually requires both pull and push actions.
// Reference: https://github.com/distribution/distribution/blob/v2.7.1/registry/handlers/app.go#L921-L930
ctx = auth.AppendRepositoryScope(ctx, s.repo.Reference, auth.ActionPull, auth.ActionPush)
url := buildRepositoryBlobUploadURL(s.repo.PlainHTTP, s.repo.Reference)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil)
if err != nil {
return err
}
resp, err := s.repo.do(req)
if err != nil {
return err
}
if resp.StatusCode != http.StatusAccepted {
defer resp.Body.Close()
return errutil.ParseErrorResponse(resp)
}
resp.Body.Close()
return s.completePushAfterInitialPost(ctx, req, resp, expected, content)
}
// completePushAfterInitialPost implements step 2 of the push protocol. This can be invoked either by
// Push or by Mount when the receiving repository does not implement the
// mount endpoint.
func (s *blobStore) completePushAfterInitialPost(ctx context.Context, req *http.Request, resp *http.Response, expected ocispec.Descriptor, content io.Reader) error {
reqHostname := req.URL.Hostname()
reqPort := req.URL.Port()
// monolithic upload
location, err := resp.Location()
if err != nil {
return err
}
// work-around solution for https://github.com/oras-project/oras-go/issues/177
// For some registries, if the port 443 is explicitly set to the hostname
// like registry.wabbit-networks.io:443/myrepo, blob push will fail since
// the hostname of the Location header in the response is set to
// registry.wabbit-networks.io instead of registry.wabbit-networks.io:443.
locationHostname := location.Hostname()
locationPort := location.Port()
// if location port 443 is missing, add it back
if reqPort == "443" && locationHostname == reqHostname && locationPort == "" {
location.Host = locationHostname + ":" + reqPort
}
url := location.String()
req, err = http.NewRequestWithContext(ctx, http.MethodPut, url, content)
if err != nil {
return err
}
if req.GetBody != nil && req.ContentLength != expected.Size {
// short circuit a size mismatch for built-in types.
return fmt.Errorf("mismatch content length %d: expect %d", req.ContentLength, expected.Size)
}
req.ContentLength = expected.Size
// the expected media type is ignored as in the API doc.
req.Header.Set("Content-Type", "application/octet-stream")
q := req.URL.Query()
q.Set("digest", expected.Digest.String())
req.URL.RawQuery = q.Encode()
// reuse credential from previous POST request
if auth := resp.Request.Header.Get("Authorization"); auth != "" {
req.Header.Set("Authorization", auth)
}
resp, err = s.repo.do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
return errutil.ParseErrorResponse(resp)
}
return nil
}
// Exists returns true if the described content exists.
func (s *blobStore) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) {
_, err := s.Resolve(ctx, target.Digest.String())
if err == nil {
return true, nil
}
if errors.Is(err, errdef.ErrNotFound) {
return false, nil
}
return false, err
}
// Delete removes the content identified by the descriptor.
func (s *blobStore) Delete(ctx context.Context, target ocispec.Descriptor) error {
return s.repo.delete(ctx, target, false)
}
// Resolve resolves a reference to a descriptor.
func (s *blobStore) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) {
ref, err := s.repo.ParseReference(reference)
if err != nil {
return ocispec.Descriptor{}, err
}
refDigest, err := ref.Digest()
if err != nil {
return ocispec.Descriptor{}, err
}
ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull)
url := buildRepositoryBlobURL(s.repo.PlainHTTP, ref)
req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil)
if err != nil {
return ocispec.Descriptor{}, err
}
resp, err := s.repo.do(req)
if err != nil {
return ocispec.Descriptor{}, err
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
return generateBlobDescriptor(resp, refDigest)
case http.StatusNotFound:
return ocispec.Descriptor{}, fmt.Errorf("%s: %w", ref, errdef.ErrNotFound)
default:
return ocispec.Descriptor{}, errutil.ParseErrorResponse(resp)
}
}
// FetchReference fetches the blob identified by the reference.
// The reference must be a digest.
func (s *blobStore) FetchReference(ctx context.Context, reference string) (desc ocispec.Descriptor, rc io.ReadCloser, err error) {
ref, err := s.repo.ParseReference(reference)
if err != nil {
return ocispec.Descriptor{}, nil, err
}
refDigest, err := ref.Digest()
if err != nil {
return ocispec.Descriptor{}, nil, err
}
ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull)
url := buildRepositoryBlobURL(s.repo.PlainHTTP, ref)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return ocispec.Descriptor{}, nil, err
}
resp, err := s.repo.do(req)
if err != nil {
return ocispec.Descriptor{}, nil, err
}
defer func() {
if err != nil {
resp.Body.Close()
}
}()
switch resp.StatusCode {
case http.StatusOK: // server does not support seek as `Range` was ignored.
if resp.ContentLength == -1 {
desc, err = s.Resolve(ctx, reference)
} else {
desc, err = generateBlobDescriptor(resp, refDigest)
}
if err != nil {
return ocispec.Descriptor{}, nil, err
}
// check server range request capability.
// Docker spec allows range header form of "Range: bytes=-".
// However, the remote server may still not RFC 7233 compliant.
// Reference: https://docs.docker.com/registry/spec/api/#blob
if rangeUnit := resp.Header.Get("Accept-Ranges"); rangeUnit == "bytes" {
return desc, httputil.NewReadSeekCloser(s.repo.client(), req, resp.Body, desc.Size), nil
}
return desc, resp.Body, nil
case http.StatusNotFound:
return ocispec.Descriptor{}, nil, fmt.Errorf("%s: %w", ref, errdef.ErrNotFound)
default:
return ocispec.Descriptor{}, nil, errutil.ParseErrorResponse(resp)
}
}
// generateBlobDescriptor returns a descriptor generated from the response.
func generateBlobDescriptor(resp *http.Response, refDigest digest.Digest) (ocispec.Descriptor, error) {
mediaType, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if mediaType == "" {
mediaType = "application/octet-stream"
}
size := resp.ContentLength
if size == -1 {
return ocispec.Descriptor{}, fmt.Errorf("%s %q: unknown response Content-Length", resp.Request.Method, resp.Request.URL)
}
if err := verifyContentDigest(resp, refDigest); err != nil {
return ocispec.Descriptor{}, err
}
return ocispec.Descriptor{
MediaType: mediaType,
Digest: refDigest,
Size: size,
}, nil
}
// manifestStore accesses the manifest part of the repository.
type manifestStore struct {
repo *Repository
}
// Fetch fetches the content identified by the descriptor.
func (s *manifestStore) Fetch(ctx context.Context, target ocispec.Descriptor) (rc io.ReadCloser, err error) {
ref := s.repo.Reference
ref.Reference = target.Digest.String()
ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull)
url := buildRepositoryManifestURL(s.repo.PlainHTTP, ref)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", target.MediaType)
resp, err := s.repo.do(req)
if err != nil {
return nil, err
}
defer func() {
if err != nil {
resp.Body.Close()
}
}()
switch resp.StatusCode {
case http.StatusOK:
// no-op
case http.StatusNotFound:
return nil, fmt.Errorf("%s: %w", target.Digest, errdef.ErrNotFound)
default:
return nil, errutil.ParseErrorResponse(resp)
}
mediaType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return nil, fmt.Errorf("%s %q: invalid response Content-Type: %w", resp.Request.Method, resp.Request.URL, err)
}
if mediaType != target.MediaType {
return nil, fmt.Errorf("%s %q: mismatch response Content-Type %q: expect %q", resp.Request.Method, resp.Request.URL, mediaType, target.MediaType)
}
if size := resp.ContentLength; size != -1 && size != target.Size {
return nil, fmt.Errorf("%s %q: mismatch Content-Length", resp.Request.Method, resp.Request.URL)
}
if err := verifyContentDigest(resp, target.Digest); err != nil {
return nil, err
}
return resp.Body, nil
}
// Push pushes the content, matching the expected descriptor.
func (s *manifestStore) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error {
return s.pushWithIndexing(ctx, expected, content, expected.Digest.String())
}
// Exists returns true if the described content exists.
func (s *manifestStore) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) {
_, err := s.Resolve(ctx, target.Digest.String())
if err == nil {
return true, nil
}
if errors.Is(err, errdef.ErrNotFound) {
return false, nil
}
return false, err
}
// Delete removes the manifest content identified by the descriptor.
func (s *manifestStore) Delete(ctx context.Context, target ocispec.Descriptor) error {
return s.deleteWithIndexing(ctx, target)
}
// deleteWithIndexing removes the manifest content identified by the descriptor,
// and indexes referrers for the manifest when needed.
func (s *manifestStore) deleteWithIndexing(ctx context.Context, target ocispec.Descriptor) error {
switch target.MediaType {
case spec.MediaTypeArtifactManifest, ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex:
if state := s.repo.loadReferrersState(); state == referrersStateSupported {
// referrers API is available, no client-side indexing needed
return s.repo.delete(ctx, target, true)
}
if err := limitSize(target, s.repo.MaxMetadataBytes); err != nil {
return err
}
ctx = auth.AppendRepositoryScope(ctx, s.repo.Reference, auth.ActionPull, auth.ActionDelete)
manifestJSON, err := content.FetchAll(ctx, s, target)
if err != nil {
return err
}
if err := s.indexReferrersForDelete(ctx, target, manifestJSON); err != nil {
return err
}
}
return s.repo.delete(ctx, target, true)
}
// indexReferrersForDelete indexes referrers for manifests with a subject field
// on manifest delete.
//
// References:
// - Latest spec: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#deleting-manifests
// - Compatible spec: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#deleting-manifests
func (s *manifestStore) indexReferrersForDelete(ctx context.Context, desc ocispec.Descriptor, manifestJSON []byte) error {
var manifest struct {
Subject *ocispec.Descriptor `json:"subject"`
}
if err := json.Unmarshal(manifestJSON, &manifest); err != nil {
return fmt.Errorf("failed to decode manifest: %s: %s: %w", desc.Digest, desc.MediaType, err)
}
if manifest.Subject == nil {
// no subject, no indexing needed
return nil
}
subject := *manifest.Subject
ok, err := s.repo.pingReferrers(ctx)
if err != nil {
return err
}
if ok {
// referrers API is available, no client-side indexing needed
return nil
}
return s.updateReferrersIndex(ctx, subject, referrerChange{desc, referrerOperationRemove})
}
// Resolve resolves a reference to a descriptor.
// See also `ManifestMediaTypes`.
func (s *manifestStore) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) {
ref, err := s.repo.ParseReference(reference)
if err != nil {
return ocispec.Descriptor{}, err
}
ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull)
url := buildRepositoryManifestURL(s.repo.PlainHTTP, ref)
req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil)
if err != nil {
return ocispec.Descriptor{}, err
}
req.Header.Set("Accept", manifestAcceptHeader(s.repo.ManifestMediaTypes))
resp, err := s.repo.do(req)
if err != nil {
return ocispec.Descriptor{}, err
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
return s.generateDescriptor(resp, ref, req.Method)
case http.StatusNotFound:
return ocispec.Descriptor{}, fmt.Errorf("%s: %w", ref, errdef.ErrNotFound)
default:
return ocispec.Descriptor{}, errutil.ParseErrorResponse(resp)
}
}
// FetchReference fetches the manifest identified by the reference.
// The reference can be a tag or digest.
func (s *manifestStore) FetchReference(ctx context.Context, reference string) (desc ocispec.Descriptor, rc io.ReadCloser, err error) {
ref, err := s.repo.ParseReference(reference)
if err != nil {
return ocispec.Descriptor{}, nil, err
}
ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull)
url := buildRepositoryManifestURL(s.repo.PlainHTTP, ref)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return ocispec.Descriptor{}, nil, err
}
req.Header.Set("Accept", manifestAcceptHeader(s.repo.ManifestMediaTypes))
resp, err := s.repo.do(req)
if err != nil {
return ocispec.Descriptor{}, nil, err
}
defer func() {
if err != nil {
resp.Body.Close()
}
}()
switch resp.StatusCode {
case http.StatusOK:
if resp.ContentLength == -1 {
desc, err = s.Resolve(ctx, reference)
} else {
desc, err = s.generateDescriptor(resp, ref, req.Method)
}
if err != nil {
return ocispec.Descriptor{}, nil, err
}
return desc, resp.Body, nil
case http.StatusNotFound:
return ocispec.Descriptor{}, nil, fmt.Errorf("%s: %w", ref, errdef.ErrNotFound)
default:
return ocispec.Descriptor{}, nil, errutil.ParseErrorResponse(resp)
}
}
// Tag tags a manifest descriptor with a reference string.
func (s *manifestStore) Tag(ctx context.Context, desc ocispec.Descriptor, reference string) error {
ref, err := s.repo.ParseReference(reference)
if err != nil {
return err
}
ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull, auth.ActionPush)
rc, err := s.Fetch(ctx, desc)
if err != nil {
return err
}
defer rc.Close()
return s.push(ctx, desc, rc, ref.Reference)
}
// PushReference pushes the manifest with a reference tag.
func (s *manifestStore) PushReference(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error {
ref, err := s.repo.ParseReference(reference)
if err != nil {
return err
}
return s.pushWithIndexing(ctx, expected, content, ref.Reference)
}
// push pushes the manifest content, matching the expected descriptor.
func (s *manifestStore) push(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error {
ref := s.repo.Reference
ref.Reference = reference
// pushing usually requires both pull and push actions.
// Reference: https://github.com/distribution/distribution/blob/v2.7.1/registry/handlers/app.go#L921-L930
ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull, auth.ActionPush)
url := buildRepositoryManifestURL(s.repo.PlainHTTP, ref)
// unwrap the content for optimizations of built-in types.
body := ioutil.UnwrapNopCloser(content)
if _, ok := body.(io.ReadCloser); ok {
// undo unwrap if the nopCloser is intended.
body = content
}
req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, body)
if err != nil {
return err
}
if req.GetBody != nil && req.ContentLength != expected.Size {
// short circuit a size mismatch for built-in types.
return fmt.Errorf("mismatch content length %d: expect %d", req.ContentLength, expected.Size)
}
req.ContentLength = expected.Size
req.Header.Set("Content-Type", expected.MediaType)
// if the underlying client is an auth client, the content might be read
// more than once for obtaining the auth challenge and the actual request.
// To prevent double reading, the manifest is read and stored in the memory,
// and serve from the memory.
client := s.repo.client()
if _, ok := client.(*auth.Client); ok && req.GetBody == nil {
store := cas.NewMemory()
err := store.Push(ctx, expected, content)
if err != nil {
return err
}
req.GetBody = func() (io.ReadCloser, error) {
return store.Fetch(ctx, expected)
}
req.Body, err = req.GetBody()
if err != nil {
return err
}
}
resp, err := s.repo.do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
return errutil.ParseErrorResponse(resp)
}
s.checkOCISubjectHeader(resp)
return verifyContentDigest(resp, expected.Digest)
}
// checkOCISubjectHeader checks the "OCI-Subject" header in the response and
// sets referrers capability accordingly.
// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#pushing-manifests-with-subject
func (s *manifestStore) checkOCISubjectHeader(resp *http.Response) {
// If the "OCI-Subject" header is set, it indicates that the registry
// supports the Referrers API and has processed the subject of the manifest.
if subjectHeader := resp.Header.Get(headerOCISubject); subjectHeader != "" {
s.repo.SetReferrersCapability(true)
}
// If the "OCI-Subject" header is NOT set, it means that either the manifest
// has no subject OR the referrers API is NOT supported by the registry.
//
// Since we don't know whether the pushed manifest has a subject or not,
// we do not set the referrers capability to false at here.
}
// pushWithIndexing pushes the manifest content matching the expected descriptor,
// and indexes referrers for the manifest when needed.
func (s *manifestStore) pushWithIndexing(ctx context.Context, expected ocispec.Descriptor, r io.Reader, reference string) error {
switch expected.MediaType {
case spec.MediaTypeArtifactManifest, ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex:
if state := s.repo.loadReferrersState(); state == referrersStateSupported {
// referrers API is available, no client-side indexing needed
return s.push(ctx, expected, r, reference)
}
if err := limitSize(expected, s.repo.MaxMetadataBytes); err != nil {
return err
}
manifestJSON, err := content.ReadAll(r, expected)
if err != nil {
return err
}
if err := s.push(ctx, expected, bytes.NewReader(manifestJSON), reference); err != nil {
return err
}
// check referrers API availability again after push
if state := s.repo.loadReferrersState(); state == referrersStateSupported {
// the subject has been processed the registry, no client-side
// indexing needed
return nil
}
return s.indexReferrersForPush(ctx, expected, manifestJSON)
default:
return s.push(ctx, expected, r, reference)
}
}
// indexReferrersForPush indexes referrers for manifests with a subject field
// on manifest push.
//
// References:
// - Latest spec: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#pushing-manifests-with-subject
// - Compatible spec: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#pushing-manifests-with-subject
func (s *manifestStore) indexReferrersForPush(ctx context.Context, desc ocispec.Descriptor, manifestJSON []byte) error {
var subject ocispec.Descriptor
switch desc.MediaType {
case spec.MediaTypeArtifactManifest:
var manifest spec.Artifact
if err := json.Unmarshal(manifestJSON, &manifest); err != nil {
return fmt.Errorf("failed to decode manifest: %s: %s: %w", desc.Digest, desc.MediaType, err)
}
if manifest.Subject == nil {
// no subject, no indexing needed
return nil
}
subject = *manifest.Subject
desc.ArtifactType = manifest.ArtifactType
desc.Annotations = manifest.Annotations
case ocispec.MediaTypeImageManifest:
var manifest ocispec.Manifest
if err := json.Unmarshal(manifestJSON, &manifest); err != nil {
return fmt.Errorf("failed to decode manifest: %s: %s: %w", desc.Digest, desc.MediaType, err)
}
if manifest.Subject == nil {
// no subject, no indexing needed
return nil
}
subject = *manifest.Subject
desc.ArtifactType = manifest.ArtifactType
if desc.ArtifactType == "" {
desc.ArtifactType = manifest.Config.MediaType
}
desc.Annotations = manifest.Annotations
case ocispec.MediaTypeImageIndex:
var manifest ocispec.Index
if err := json.Unmarshal(manifestJSON, &manifest); err != nil {
return fmt.Errorf("failed to decode manifest: %s: %s: %w", desc.Digest, desc.MediaType, err)
}
if manifest.Subject == nil {
// no subject, no indexing needed
return nil
}
subject = *manifest.Subject
desc.ArtifactType = manifest.ArtifactType
desc.Annotations = manifest.Annotations
default:
return nil
}
// if the manifest has a subject but the remote registry does not process it,
// it means that the Referrers API is not supported by the registry.
s.repo.SetReferrersCapability(false)
return s.updateReferrersIndex(ctx, subject, referrerChange{desc, referrerOperationAdd})
}
// updateReferrersIndex updates the referrers index for desc referencing subject
// on manifest push and manifest delete.
// References:
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#pushing-manifests-with-subject
// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#deleting-manifests
func (s *manifestStore) updateReferrersIndex(ctx context.Context, subject ocispec.Descriptor, change referrerChange) (err error) {
referrersTag := buildReferrersTag(subject)
var oldIndexDesc *ocispec.Descriptor
var oldReferrers []ocispec.Descriptor
prepare := func() error {
// 1. pull the original referrers list using the referrers tag schema
indexDesc, referrers, err := s.repo.referrersFromIndex(ctx, referrersTag)
if err != nil {
if errors.Is(err, errdef.ErrNotFound) {
// valid case: no old referrers index
return nil
}
return err
}
oldIndexDesc = &indexDesc
oldReferrers = referrers
return nil
}
update := func(referrerChanges []referrerChange) error {
// 2. apply the referrer changes on the referrers list
updatedReferrers, err := applyReferrerChanges(oldReferrers, referrerChanges)
if err != nil {
if err == errNoReferrerUpdate {
return nil
}
return err
}
// 3. push the updated referrers list using referrers tag schema
if len(updatedReferrers) > 0 || s.repo.SkipReferrersGC {
// push a new index in either case:
// 1. the referrers list has been updated with a non-zero size
// 2. OR the updated referrers list is empty but referrers GC
// is skipped, in this case an empty index should still be pushed
// as the old index won't get deleted
newIndexDesc, newIndex, err := generateIndex(updatedReferrers)
if err != nil {
return fmt.Errorf("failed to generate referrers index for referrers tag %s: %w", referrersTag, err)
}
if err := s.push(ctx, newIndexDesc, bytes.NewReader(newIndex), referrersTag); err != nil {
return fmt.Errorf("failed to push referrers index tagged by %s: %w", referrersTag, err)
}
}
// 4. delete the dangling original referrers index, if applicable
if s.repo.SkipReferrersGC || oldIndexDesc == nil {
return nil
}
if err := s.repo.delete(ctx, *oldIndexDesc, true); err != nil {
return &ReferrersError{
Op: opDeleteReferrersIndex,
Err: fmt.Errorf("failed to delete dangling referrers index %s for referrers tag %s: %w", oldIndexDesc.Digest.String(), referrersTag, err),
Subject: subject,
}
}
return nil
}
merge, done := s.repo.referrersMergePool.Get(referrersTag)
defer done()
return merge.Do(change, prepare, update)
}
// ParseReference parses a reference to a fully qualified reference.
func (s *manifestStore) ParseReference(reference string) (registry.Reference, error) {
return s.repo.ParseReference(reference)
}
// generateDescriptor returns a descriptor generated from the response.
// See the truth table at the top of `repository_test.go`
func (s *manifestStore) generateDescriptor(resp *http.Response, ref registry.Reference, httpMethod string) (ocispec.Descriptor, error) {
// 1. Validate Content-Type
mediaType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return ocispec.Descriptor{}, fmt.Errorf(
"%s %q: invalid response `Content-Type` header; %w",
resp.Request.Method,
resp.Request.URL,
err,
)
}
// 2. Validate Size
if resp.ContentLength == -1 {
return ocispec.Descriptor{}, fmt.Errorf(
"%s %q: unknown response Content-Length",
resp.Request.Method,
resp.Request.URL,
)
}
// 3. Validate Client Reference
var refDigest digest.Digest
if d, err := ref.Digest(); err == nil {
refDigest = d
}
// 4. Validate Server Digest (if present)
var serverHeaderDigest digest.Digest
if serverHeaderDigestStr := resp.Header.Get(headerDockerContentDigest); serverHeaderDigestStr != "" {
if serverHeaderDigest, err = digest.Parse(serverHeaderDigestStr); err != nil {
return ocispec.Descriptor{}, fmt.Errorf(
"%s %q: invalid response header value: `%s: %s`; %w",
resp.Request.Method,
resp.Request.URL,
headerDockerContentDigest,
serverHeaderDigestStr,
err,
)
}
}
/* 5. Now, look for specific error conditions; see truth table in method docstring */
var contentDigest digest.Digest
if len(serverHeaderDigest) == 0 {
if httpMethod == http.MethodHead {
if len(refDigest) == 0 {
// HEAD without server `Docker-Content-Digest` header is an
// immediate fail
return ocispec.Descriptor{}, fmt.Errorf(
"HTTP %s request missing required header %q",
httpMethod, headerDockerContentDigest,
)
}
// Otherwise, just trust the client-supplied digest
contentDigest = refDigest
} else {
// GET without server `Docker-Content-Digest` header forces the
// expensive calculation
var calculatedDigest digest.Digest
if calculatedDigest, err = calculateDigestFromResponse(resp, s.repo.MaxMetadataBytes); err != nil {
return ocispec.Descriptor{}, fmt.Errorf("failed to calculate digest on response body; %w", err)
}
contentDigest = calculatedDigest
}
} else {
contentDigest = serverHeaderDigest
}
if len(refDigest) > 0 && refDigest != contentDigest {
return ocispec.Descriptor{}, fmt.Errorf(
"%s %q: invalid response; digest mismatch in %s: received %q when expecting %q",
resp.Request.Method, resp.Request.URL,
headerDockerContentDigest, contentDigest,
refDigest,
)
}
// 6. Finally, if we made it this far, then all is good; return.
return ocispec.Descriptor{
MediaType: mediaType,
Digest: contentDigest,
Size: resp.ContentLength,
}, nil
}
// calculateDigestFromResponse calculates the actual digest of the response body
// taking care not to destroy it in the process.
func calculateDigestFromResponse(resp *http.Response, maxMetadataBytes int64) (digest.Digest, error) {
defer resp.Body.Close()
body := limitReader(resp.Body, maxMetadataBytes)
content, err := io.ReadAll(body)
if err != nil {
return "", fmt.Errorf("%s %q: failed to read response body: %w", resp.Request.Method, resp.Request.URL, err)
}
resp.Body = io.NopCloser(bytes.NewReader(content))
return digest.FromBytes(content), nil
}
// verifyContentDigest verifies "Docker-Content-Digest" header if present.
// OCI distribution-spec states the Docker-Content-Digest header is optional.
// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.0.1/spec.md#legacy-docker-support-http-headers
func verifyContentDigest(resp *http.Response, expected digest.Digest) error {
digestStr := resp.Header.Get(headerDockerContentDigest)
if len(digestStr) == 0 {
return nil
}
contentDigest, err := digest.Parse(digestStr)
if err != nil {
return fmt.Errorf(
"%s %q: invalid response header: `%s: %s`",
resp.Request.Method, resp.Request.URL,
headerDockerContentDigest, digestStr,
)
}
if contentDigest != expected {
return fmt.Errorf(
"%s %q: invalid response; digest mismatch in %s: received %q when expecting %q",
resp.Request.Method, resp.Request.URL,
headerDockerContentDigest, contentDigest,
expected,
)
}
return nil
}
// generateIndex generates an image index containing the given manifests list.
func generateIndex(manifests []ocispec.Descriptor) (ocispec.Descriptor, []byte, error) {
if manifests == nil {
manifests = []ocispec.Descriptor{} // make it an empty array to prevent potential server-side bugs
}
index := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: manifests,
}
indexJSON, err := json.Marshal(index)
if err != nil {
return ocispec.Descriptor{}, nil, err
}
indexDesc := content.NewDescriptorFromBytes(index.MediaType, indexJSON)
return indexDesc, indexJSON, nil
}
oras-go-2.5.0/registry/remote/repository_test.go 0000664 0000000 0000000 00000731333 14576745303 0022065 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 remote
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strconv"
"strings"
"sync/atomic"
"testing"
"github.com/opencontainers/go-digest"
specs "github.com/opencontainers/image-spec/specs-go"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"golang.org/x/sync/errgroup"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/internal/interfaces"
"oras.land/oras-go/v2/internal/spec"
"oras.land/oras-go/v2/registry"
"oras.land/oras-go/v2/registry/remote/auth"
"oras.land/oras-go/v2/registry/remote/errcode"
)
type testIOStruct struct {
isTag bool
clientSuppliedReference string
serverCalculatedDigest digest.Digest // for non-HEAD (body-containing) requests only
errExpectedOnHEAD bool
errExpectedOnGET bool
}
const theAmazingBanClan = "Ban Gu, Ban Chao, Ban Zhao"
const theAmazingBanDigest = "b526a4f2be963a2f9b0990c001255669eab8a254ab1a6e3f84f1820212ac7078"
// The following truth table aims to cover the expected GET/HEAD request outcome
// for all possible permutations of the client/server "containing a digest", for
// both Manifests and Blobs. Where the results between the two differ, the index
// of the first column has an exclamation mark.
//
// The client is said to "contain a digest" if the user-supplied reference string
// is of the form that contains a digest rather than a tag. The server, on the
// other hand, is said to "contain a digest" if the server responded with the
// special header `Docker-Content-Digest`.
//
// In this table, anything denoted with an asterisk indicates that the true
// response should actually be the opposite of what's expected; for example,
// `*PASS` means we will get a `PASS`, even though the true answer would be its
// diametric opposite--a `FAIL`. This may seem odd, and deserves an explanation.
// This function has blind-spots, and while it can expend power to gain sight,
// i.e., perform the expensive validation, we chose not to. The reason is two-
// fold: a) we "know" that even if we say "!PASS", it will eventually fail later
// when checks are performed, and with that assumption, we have the luxury for
// the second point, which is b) performance.
//
// _______________________________________________________________________________________________________________
// | ID | CLIENT | SERVER | Manifest.GET | Blob.GET | Manifest.HEAD | Blob.HEAD |
// |----+-----------------+------------------+-----------------------+-----------+---------------------+-----------+
// | 1 | tag | missing | CALCULATE,PASS | n/a | FAIL | n/a |
// | 2 | tag | presentCorrect | TRUST,PASS | n/a | TRUST,PASS | n/a |
// | 3 | tag | presentIncorrect | TRUST,*PASS | n/a | TRUST,*PASS | n/a |
// | 4 | correctDigest | missing | TRUST,PASS | PASS | TRUST,PASS | PASS |
// | 5 | correctDigest | presentCorrect | TRUST,COMPARE,PASS | PASS | TRUST,COMPARE,PASS | PASS |
// | 6 | correctDigest | presentIncorrect | TRUST,COMPARE,FAIL | FAIL | TRUST,COMPARE,FAIL | FAIL |
// ---------------------------------------------------------------------------------------------------------------
func getTestIOStructMapForGetDescriptorClass() map[string]testIOStruct {
correctDigest := fmt.Sprintf("sha256:%v", theAmazingBanDigest)
incorrectDigest := fmt.Sprintf("sha256:%v", "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
return map[string]testIOStruct{
"1. Client:Tag & Server:DigestMissing": {
isTag: true,
errExpectedOnHEAD: true,
},
"2. Client:Tag & Server:DigestValid": {
isTag: true,
serverCalculatedDigest: digest.Digest(correctDigest),
},
"3. Client:Tag & Server:DigestWrongButSyntacticallyValid": {
isTag: true,
serverCalculatedDigest: digest.Digest(incorrectDigest),
},
"4. Client:DigestValid & Server:DigestMissing": {
clientSuppliedReference: correctDigest,
},
"5. Client:DigestValid & Server:DigestValid": {
clientSuppliedReference: correctDigest,
serverCalculatedDigest: digest.Digest(correctDigest),
},
"6. Client:DigestValid & Server:DigestWrongButSyntacticallyValid": {
clientSuppliedReference: correctDigest,
serverCalculatedDigest: digest.Digest(incorrectDigest),
errExpectedOnHEAD: true,
errExpectedOnGET: true,
},
}
}
func TestRepository_Fetch(t *testing.T) {
blob := []byte("hello world")
blobDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
index := []byte(`{"manifests":[]}`)
indexDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageIndex,
Digest: digest.FromBytes(index),
Size: int64(len(index)),
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
switch r.URL.Path {
case "/v2/test/blobs/" + blobDesc.Digest.String():
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String())
if _, err := w.Write(blob); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
case "/v2/test/manifests/" + indexDesc.Digest.String():
if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", indexDesc.MediaType)
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
if _, err := w.Write(index); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
ctx := context.Background()
rc, err := repo.Fetch(ctx, blobDesc)
if err != nil {
t.Fatalf("Repository.Fetch() error = %v", err)
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(rc); err != nil {
t.Errorf("fail to read: %v", err)
}
if err := rc.Close(); err != nil {
t.Errorf("fail to close: %v", err)
}
if got := buf.Bytes(); !bytes.Equal(got, blob) {
t.Errorf("Repository.Fetch() = %v, want %v", got, blob)
}
rc, err = repo.Fetch(ctx, indexDesc)
if err != nil {
t.Fatalf("Repository.Fetch() error = %v", err)
}
buf.Reset()
if _, err := buf.ReadFrom(rc); err != nil {
t.Errorf("fail to read: %v", err)
}
if err := rc.Close(); err != nil {
t.Errorf("fail to close: %v", err)
}
if got := buf.Bytes(); !bytes.Equal(got, index) {
t.Errorf("Repository.Fetch() = %v, want %v", got, index)
}
}
func TestRepository_Push(t *testing.T) {
blob := []byte("hello world")
blobDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
var gotBlob []byte
index := []byte(`{"manifests":[]}`)
indexDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageIndex,
Digest: digest.FromBytes(index),
Size: int64(len(index)),
}
var gotIndex []byte
uuid := "4fd53bc9-565d-4527-ab80-3e051ac4880c"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPost && r.URL.Path == "/v2/test/blobs/uploads/":
w.Header().Set("Location", "/v2/test/blobs/uploads/"+uuid)
w.WriteHeader(http.StatusAccepted)
return
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/blobs/uploads/"+uuid:
if contentType := r.Header.Get("Content-Type"); contentType != "application/octet-stream" {
w.WriteHeader(http.StatusBadRequest)
break
}
if contentDigest := r.URL.Query().Get("digest"); contentDigest != blobDesc.Digest.String() {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotBlob = buf.Bytes()
w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
return
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String():
if contentType := r.Header.Get("Content-Type"); contentType != indexDesc.MediaType {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotIndex = buf.Bytes()
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
return
default:
w.WriteHeader(http.StatusForbidden)
}
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
ctx := context.Background()
err = repo.Push(ctx, blobDesc, bytes.NewReader(blob))
if err != nil {
t.Fatalf("Repository.Push() error = %v", err)
}
if !bytes.Equal(gotBlob, blob) {
t.Errorf("Repository.Push() = %v, want %v", gotBlob, blob)
}
err = repo.Push(ctx, indexDesc, bytes.NewReader(index))
if err != nil {
t.Fatalf("Repository.Push() error = %v", err)
}
if !bytes.Equal(gotIndex, index) {
t.Errorf("Repository.Push() = %v, want %v", gotIndex, index)
}
}
func TestRepository_Mount(t *testing.T) {
blob := []byte("hello world")
blobDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
gotMount := 0
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got, want := r.Method, "POST"; got != want {
t.Errorf("unexpected HTTP method; got %q want %q", got, want)
w.WriteHeader(http.StatusInternalServerError)
return
}
if err := r.ParseForm(); err != nil {
t.Errorf("invalid form in HTTP request: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
switch r.URL.Path {
case "/v2/test2/blobs/uploads/":
if got, want := r.Form.Get("mount"), blobDesc.Digest; digest.Digest(got) != want {
t.Errorf("unexpected value for 'mount' parameter; got %q want %q", got, want)
}
if got, want := r.Form.Get("from"), "test"; got != want {
t.Errorf("unexpected value for 'from' parameter; got %q want %q", got, want)
}
gotMount++
w.Header().Set(headerDockerContentDigest, blobDesc.Digest.String())
w.WriteHeader(201)
return
default:
t.Errorf("unexpected URL for mount request %q", r.URL)
w.WriteHeader(http.StatusInternalServerError)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test2")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
ctx := context.Background()
err = repo.Mount(ctx, blobDesc, "test", nil)
if err != nil {
t.Fatalf("Repository.Push() error = %v", err)
}
if gotMount != 1 {
t.Errorf("did not get expected mount request")
}
}
func TestRepository_Mount_Fallback(t *testing.T) {
// This test checks the case where the server does not know
// about the mount query parameters, so the call falls back to
// the regular push flow. This test is thus very similar to TestPush,
// except that it doesn't push a manifest because mounts aren't
// documented to be supported for manifests.
blob := []byte("hello world")
blobDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
var sequence string
var gotBlob []byte
uuid := "4fd53bc9-565d-4527-ab80-3e051ac4880c"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPost && r.URL.Path == "/v2/test2/blobs/uploads/":
w.Header().Set("Location", "/v2/test2/blobs/uploads/"+uuid)
w.WriteHeader(http.StatusAccepted)
sequence += "post "
return
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/blobs/"+blobDesc.Digest.String():
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String())
if _, err := w.Write(blob); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
sequence += "get "
return
case r.Method == http.MethodPut && r.URL.Path == "/v2/test2/blobs/uploads/"+uuid:
if got, want := r.Header.Get("Content-Type"), "application/octet-stream"; got != want {
t.Errorf("unexpected content type; got %q want %q", got, want)
w.WriteHeader(http.StatusBadRequest)
return
}
if got, want := r.URL.Query().Get("digest"), blobDesc.Digest.String(); got != want {
t.Errorf("unexpected content digest; got %q want %q", got, want)
w.WriteHeader(http.StatusBadRequest)
return
}
data, err := io.ReadAll(r.Body)
if err != nil {
t.Errorf("error reading body: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
gotBlob = data
w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
sequence += "put "
return
default:
w.WriteHeader(http.StatusForbidden)
}
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test2")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
ctx := context.Background()
t.Run("getContent is nil", func(t *testing.T) {
sequence = ""
err = repo.Mount(ctx, blobDesc, "test", nil)
if err != nil {
t.Fatalf("Repository.Push() error = %v", err)
}
if !bytes.Equal(gotBlob, blob) {
t.Errorf("Repository.Mount() = %v, want %v", gotBlob, blob)
}
if got, want := sequence, "post get put "; got != want {
t.Errorf("unexpected request sequence; got %q want %q", got, want)
}
})
t.Run("getContent is non nil", func(t *testing.T) {
sequence = ""
err = repo.Mount(ctx, blobDesc, "test", func() (io.ReadCloser, error) {
return io.NopCloser(bytes.NewReader(blob)), nil
})
if err != nil {
t.Fatalf("Repository.Push() error = %v", err)
}
if !bytes.Equal(gotBlob, blob) {
t.Errorf("Repository.Mount() = %v, want %v", gotBlob, blob)
}
if got, want := sequence, "post put "; got != want {
t.Errorf("unexpected request sequence; got %q want %q", got, want)
}
})
}
func TestRepository_Mount_Error(t *testing.T) {
blob := []byte("hello world")
blobDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got, want := r.Method, "POST"; got != want {
t.Errorf("unexpected HTTP method; got %q want %q", got, want)
w.WriteHeader(http.StatusInternalServerError)
return
}
if err := r.ParseForm(); err != nil {
t.Errorf("invalid form in HTTP request: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
switch r.URL.Path {
case "/v2/test/blobs/uploads/":
w.WriteHeader(400)
w.Write([]byte(`{ "errors": [ { "code": "NAME_UNKNOWN", "message": "some error" } ] }`))
default:
t.Errorf("unexpected URL for mount request %q", r.URL)
w.WriteHeader(http.StatusInternalServerError)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
err = repo.Mount(context.Background(), blobDesc, "foo", nil)
if err == nil {
t.Fatalf("expected error but got success instead")
}
var errResp *errcode.ErrorResponse
if !errors.As(err, &errResp) {
t.Fatalf("unexpected error type %#v", err)
}
if !reflect.DeepEqual(errResp.Errors, errcode.Errors{{
Code: "NAME_UNKNOWN",
Message: "some error",
}}) {
t.Errorf("unexpected errors %#v", errResp.Errors)
}
}
func TestRepository_Mount_Fallback_GetContent(t *testing.T) {
// This test checks the case where the server does not know
// about the mount query parameters, so the call falls back to
// the regular push flow, but using the getContent function
// parameter to get the content to push.
blob := []byte("hello world")
blobDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
var sequence string
var gotBlob []byte
uuid := "4fd53bc9-565d-4527-ab80-3e051ac4880c"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPost && r.URL.Path == "/v2/test2/blobs/uploads/":
w.Header().Set("Location", "/v2/test2/blobs/uploads/"+uuid)
w.WriteHeader(http.StatusAccepted)
sequence += "post "
return
case r.Method == http.MethodPut && r.URL.Path == "/v2/test2/blobs/uploads/"+uuid:
if got, want := r.Header.Get("Content-Type"), "application/octet-stream"; got != want {
t.Errorf("unexpected content type; got %q want %q", got, want)
w.WriteHeader(http.StatusBadRequest)
return
}
if got, want := r.URL.Query().Get("digest"), blobDesc.Digest.String(); got != want {
t.Errorf("unexpected content digest; got %q want %q", got, want)
w.WriteHeader(http.StatusBadRequest)
return
}
data, err := io.ReadAll(r.Body)
if err != nil {
t.Errorf("error reading body: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
gotBlob = data
w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
sequence += "put "
return
default:
w.WriteHeader(http.StatusForbidden)
}
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test2")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
ctx := context.Background()
err = repo.Mount(ctx, blobDesc, "test", func() (io.ReadCloser, error) {
return io.NopCloser(bytes.NewReader(blob)), nil
})
if err != nil {
t.Fatalf("Repository.Push() error = %v", err)
}
if !bytes.Equal(gotBlob, blob) {
t.Errorf("Repository.Mount() = %v, want %v", gotBlob, blob)
}
if got, want := sequence, "post put "; got != want {
t.Errorf("unexpected request sequence; got %q want %q", got, want)
}
}
func TestRepository_Mount_Fallback_GetContentError(t *testing.T) {
// This test checks the case where the server does not know
// about the mount query parameters, so the call falls back to
// the regular push flow, but it's possible the caller wants to
// avoid the pull/push pattern so returns an error from getContent
// and checks it to find out that's happened.
blob := []byte("hello world")
blobDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
var sequence string
uuid := "4fd53bc9-565d-4527-ab80-3e051ac4880c"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPost && r.URL.Path == "/v2/test2/blobs/uploads/":
w.Header().Set("Location", "/v2/test2/blobs/uploads/"+uuid)
w.WriteHeader(http.StatusAccepted)
sequence += "post "
return
default:
w.WriteHeader(http.StatusForbidden)
}
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test2")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
ctx := context.Background()
testErr := errors.New("test error")
err = repo.Mount(ctx, blobDesc, "test", func() (io.ReadCloser, error) {
return nil, testErr
})
if err == nil {
t.Fatalf("expected error but found no error")
}
if !errors.Is(err, testErr) {
t.Fatalf("expected getContent error to be wrapped")
}
if got, want := sequence, "post "; got != want {
t.Errorf("unexpected request sequence; got %q want %q", got, want)
}
}
func TestRepository_Exists(t *testing.T) {
blob := []byte("hello world")
blobDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
index := []byte(`{"manifests":[]}`)
indexDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageIndex,
Digest: digest.FromBytes(index),
Size: int64(len(index)),
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodHead {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
switch r.URL.Path {
case "/v2/test/blobs/" + blobDesc.Digest.String():
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(int(blobDesc.Size)))
case "/v2/test/manifests/" + indexDesc.Digest.String():
if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", indexDesc.MediaType)
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(int(indexDesc.Size)))
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
ctx := context.Background()
exists, err := repo.Exists(ctx, blobDesc)
if err != nil {
t.Fatalf("Repository.Exists() error = %v", err)
}
if !exists {
t.Errorf("Repository.Exists() = %v, want %v", exists, true)
}
exists, err = repo.Exists(ctx, indexDesc)
if err != nil {
t.Fatalf("Repository.Exists() error = %v", err)
}
if !exists {
t.Errorf("Repository.Exists() = %v, want %v", exists, true)
}
}
func TestRepository_Delete(t *testing.T) {
blob := []byte("hello world")
blobDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
index := []byte(`{"manifests":[]}`)
indexDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageIndex,
Digest: digest.FromBytes(index),
Size: int64(len(index)),
}
var blobDeleted bool
var indexDeleted bool
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/blobs/"+blobDesc.Digest.String():
blobDeleted = true
w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String())
w.WriteHeader(http.StatusAccepted)
case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String():
indexDeleted = true
// no "Docker-Content-Digest" header for manifest deletion
w.WriteHeader(http.StatusAccepted)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String():
if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", indexDesc.MediaType)
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
if _, err := w.Write(index); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
ctx := context.Background()
err = repo.Delete(ctx, blobDesc)
if err != nil {
t.Fatalf("Repository.Delete() error = %v", err)
}
if !blobDeleted {
t.Errorf("Repository.Delete() = %v, want %v", blobDeleted, true)
}
err = repo.Delete(ctx, indexDesc)
if err != nil {
t.Fatalf("Repository.Delete() error = %v", err)
}
if !indexDeleted {
t.Errorf("Repository.Delete() = %v, want %v", indexDeleted, true)
}
}
func TestRepository_Resolve(t *testing.T) {
blob := []byte("hello world")
blobDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
index := []byte(`{"manifests":[]}`)
indexDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageIndex,
Digest: digest.FromBytes(index),
Size: int64(len(index)),
}
ref := "foobar"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodHead {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
switch r.URL.Path {
case "/v2/test/manifests/" + blobDesc.Digest.String():
w.WriteHeader(http.StatusNotFound)
case "/v2/test/manifests/" + indexDesc.Digest.String(),
"/v2/test/manifests/" + ref:
if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", indexDesc.MediaType)
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(int(indexDesc.Size)))
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repoName := uri.Host + "/test"
repo, err := NewRepository(repoName)
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
ctx := context.Background()
_, err = repo.Resolve(ctx, blobDesc.Digest.String())
if !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("Repository.Resolve() error = %v, wantErr %v", err, errdef.ErrNotFound)
}
got, err := repo.Resolve(ctx, indexDesc.Digest.String())
if err != nil {
t.Fatalf("Repository.Resolve() error = %v", err)
}
if !reflect.DeepEqual(got, indexDesc) {
t.Errorf("Repository.Resolve() = %v, want %v", got, indexDesc)
}
got, err = repo.Resolve(ctx, ref)
if err != nil {
t.Fatalf("Repository.Resolve() error = %v", err)
}
if !reflect.DeepEqual(got, indexDesc) {
t.Errorf("Repository.Resolve() = %v, want %v", got, indexDesc)
}
tagDigestRef := "whatever" + "@" + indexDesc.Digest.String()
got, err = repo.Resolve(ctx, tagDigestRef)
if err != nil {
t.Fatalf("Repository.Resolve() error = %v", err)
}
if !reflect.DeepEqual(got, indexDesc) {
t.Errorf("Repository.Resolve() = %v, want %v", got, indexDesc)
}
fqdnRef := repoName + ":" + tagDigestRef
got, err = repo.Resolve(ctx, fqdnRef)
if err != nil {
t.Fatalf("Repository.Resolve() error = %v", err)
}
if !reflect.DeepEqual(got, indexDesc) {
t.Errorf("Repository.Resolve() = %v, want %v", got, indexDesc)
}
}
func TestRepository_Tag(t *testing.T) {
blob := []byte("hello world")
blobDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
index := []byte(`{"manifests":[]}`)
indexDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageIndex,
Digest: digest.FromBytes(index),
Size: int64(len(index)),
}
var gotIndex []byte
ref := "foobar"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+blobDesc.Digest.String():
w.WriteHeader(http.StatusNotFound)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String():
if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", indexDesc.MediaType)
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
if _, err := w.Write(index); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
case r.Method == http.MethodPut &&
r.URL.Path == "/v2/test/manifests/"+ref || r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String():
if contentType := r.Header.Get("Content-Type"); contentType != indexDesc.MediaType {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotIndex = buf.Bytes()
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
return
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusForbidden)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
ctx := context.Background()
err = repo.Tag(ctx, blobDesc, ref)
if err == nil {
t.Errorf("Repository.Tag() error = %v, wantErr %v", err, true)
}
err = repo.Tag(ctx, indexDesc, ref)
if err != nil {
t.Fatalf("Repository.Tag() error = %v", err)
}
if !bytes.Equal(gotIndex, index) {
t.Errorf("Repository.Tag() = %v, want %v", gotIndex, index)
}
gotIndex = nil
err = repo.Tag(ctx, indexDesc, indexDesc.Digest.String())
if err != nil {
t.Fatalf("Repository.Tag() error = %v", err)
}
if !bytes.Equal(gotIndex, index) {
t.Errorf("Repository.Tag() = %v, want %v", gotIndex, index)
}
}
func TestRepository_PushReference(t *testing.T) {
index := []byte(`{"manifests":[]}`)
indexDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageIndex,
Digest: digest.FromBytes(index),
Size: int64(len(index)),
}
var gotIndex []byte
ref := "foobar"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+ref:
if contentType := r.Header.Get("Content-Type"); contentType != indexDesc.MediaType {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotIndex = buf.Bytes()
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
return
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusForbidden)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
ctx := context.Background()
err = repo.PushReference(ctx, indexDesc, bytes.NewReader(index), ref)
if err != nil {
t.Fatalf("Repository.PushReference() error = %v", err)
}
if !bytes.Equal(gotIndex, index) {
t.Errorf("Repository.PushReference() = %v, want %v", gotIndex, index)
}
}
func TestRepository_FetchReference(t *testing.T) {
blob := []byte("hello world")
blobDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
index := []byte(`{"manifests":[]}`)
indexDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageIndex,
Digest: digest.FromBytes(index),
Size: int64(len(index)),
}
ref := "foobar"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
switch r.URL.Path {
case "/v2/test/manifests/" + blobDesc.Digest.String():
w.WriteHeader(http.StatusNotFound)
case "/v2/test/manifests/" + indexDesc.Digest.String(),
"/v2/test/manifests/" + ref:
if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", indexDesc.MediaType)
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
if _, err := w.Write(index); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repoName := uri.Host + "/test"
repo, err := NewRepository(repoName)
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
ctx := context.Background()
// test with blob digest
_, _, err = repo.FetchReference(ctx, blobDesc.Digest.String())
if !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("Repository.FetchReference() error = %v, wantErr %v", err, errdef.ErrNotFound)
}
// test with manifest digest
gotDesc, rc, err := repo.FetchReference(ctx, indexDesc.Digest.String())
if err != nil {
t.Fatalf("Repository.FetchReference() error = %v", err)
}
if !reflect.DeepEqual(gotDesc, indexDesc) {
t.Errorf("Repository.FetchReference() = %v, want %v", gotDesc, indexDesc)
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(rc); err != nil {
t.Errorf("fail to read: %v", err)
}
if err := rc.Close(); err != nil {
t.Errorf("fail to close: %v", err)
}
if got := buf.Bytes(); !bytes.Equal(got, index) {
t.Errorf("Repository.FetchReference() = %v, want %v", got, index)
}
// test with manifest tag
gotDesc, rc, err = repo.FetchReference(ctx, ref)
if err != nil {
t.Fatalf("Repository.FetchReference() error = %v", err)
}
if !reflect.DeepEqual(gotDesc, indexDesc) {
t.Errorf("Repository.FetchReference() = %v, want %v", gotDesc, indexDesc)
}
buf.Reset()
if _, err := buf.ReadFrom(rc); err != nil {
t.Errorf("fail to read: %v", err)
}
if err := rc.Close(); err != nil {
t.Errorf("fail to close: %v", err)
}
if got := buf.Bytes(); !bytes.Equal(got, index) {
t.Errorf("Repository.FetchReference() = %v, want %v", got, index)
}
// test with manifest tag@digest
tagDigestRef := "whatever" + "@" + indexDesc.Digest.String()
gotDesc, rc, err = repo.FetchReference(ctx, tagDigestRef)
if err != nil {
t.Fatalf("Repository.FetchReference() error = %v", err)
}
if !reflect.DeepEqual(gotDesc, indexDesc) {
t.Errorf("Repository.FetchReference() = %v, want %v", gotDesc, indexDesc)
}
buf.Reset()
if _, err := buf.ReadFrom(rc); err != nil {
t.Errorf("fail to read: %v", err)
}
if err := rc.Close(); err != nil {
t.Errorf("fail to close: %v", err)
}
if got := buf.Bytes(); !bytes.Equal(got, index) {
t.Errorf("Repository.FetchReference() = %v, want %v", got, index)
}
// test with manifest FQDN
fqdnRef := repoName + ":" + tagDigestRef
gotDesc, rc, err = repo.FetchReference(ctx, fqdnRef)
if err != nil {
t.Fatalf("Repository.FetchReference() error = %v", err)
}
if !reflect.DeepEqual(gotDesc, indexDesc) {
t.Errorf("Repository.FetchReference() = %v, want %v", gotDesc, indexDesc)
}
buf.Reset()
if _, err := buf.ReadFrom(rc); err != nil {
t.Errorf("fail to read: %v", err)
}
if err := rc.Close(); err != nil {
t.Errorf("fail to close: %v", err)
}
if got := buf.Bytes(); !bytes.Equal(got, index) {
t.Errorf("Repository.FetchReference() = %v, want %v", got, index)
}
}
func TestRepository_Tags(t *testing.T) {
tagSet := [][]string{
{"the", "quick", "brown", "fox"},
{"jumps", "over", "the", "lazy"},
{"dog"},
}
var ts *httptest.Server
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || r.URL.Path != "/v2/test/tags/list" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
q := r.URL.Query()
n, err := strconv.Atoi(q.Get("n"))
if err != nil || n != 4 {
t.Errorf("bad page size: %s", q.Get("n"))
w.WriteHeader(http.StatusBadRequest)
return
}
var tags []string
switch q.Get("test") {
case "foo":
tags = tagSet[1]
w.Header().Set("Link", fmt.Sprintf(`<%s/v2/test/tags/list?n=4&test=bar>; rel="next"`, ts.URL))
case "bar":
tags = tagSet[2]
default:
tags = tagSet[0]
w.Header().Set("Link", `; rel="next"`)
}
result := struct {
Tags []string `json:"tags"`
}{
Tags: tags,
}
if err := json.NewEncoder(w).Encode(result); err != nil {
t.Errorf("failed to write response: %v", err)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
repo.TagListPageSize = 4
ctx := context.Background()
index := 0
if err := repo.Tags(ctx, "", func(got []string) error {
if index > 2 {
t.Fatalf("out of index bound: %d", index)
}
tags := tagSet[index]
index++
if !reflect.DeepEqual(got, tags) {
t.Errorf("Repository.Tags() = %v, want %v", got, tags)
}
return nil
}); err != nil {
t.Errorf("Repository.Tags() error = %v", err)
}
}
func TestRepository_Predecessors(t *testing.T) {
manifest := []byte(`{"layers":[]}`)
manifestDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(manifest),
Size: int64(len(manifest)),
}
referrerSet := [][]ocispec.Descriptor{
{
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 1,
Digest: digest.FromString("1"),
ArtifactType: "application/vnd.test",
},
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 2,
Digest: digest.FromString("2"),
ArtifactType: "application/vnd.test",
},
},
{
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 3,
Digest: digest.FromString("3"),
ArtifactType: "application/vnd.test",
},
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 4,
Digest: digest.FromString("4"),
ArtifactType: "application/vnd.test",
},
},
{
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 5,
Digest: digest.FromString("5"),
ArtifactType: "application/vnd.test",
},
},
}
var ts *httptest.Server
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := "/v2/test/referrers/" + manifestDesc.Digest.String()
if r.Method != http.MethodGet || r.URL.Path != path {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
q := r.URL.Query()
n, err := strconv.Atoi(q.Get("n"))
if err != nil || n != 2 {
t.Errorf("bad page size: %s", q.Get("n"))
w.WriteHeader(http.StatusBadRequest)
return
}
var referrers []ocispec.Descriptor
switch q.Get("test") {
case "foo":
referrers = referrerSet[1]
w.Header().Set("Link", fmt.Sprintf(`<%s%s?n=2&test=bar>; rel="next"`, ts.URL, path))
case "bar":
referrers = referrerSet[2]
default:
referrers = referrerSet[0]
w.Header().Set("Link", fmt.Sprintf(`<%s?n=2&test=foo>; rel="next"`, path))
}
result := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: referrers,
}
w.Header().Set("Content-Type", ocispec.MediaTypeImageIndex)
if err := json.NewEncoder(w).Encode(result); err != nil {
t.Errorf("failed to write response: %v", err)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
repo.ReferrerListPageSize = 2
ctx := context.Background()
got, err := repo.Predecessors(ctx, manifestDesc)
if err != nil {
t.Fatalf("Repository.Predecessors() error = %v", err)
}
var want []ocispec.Descriptor
for _, referrers := range referrerSet {
want = append(want, referrers...)
}
if !reflect.DeepEqual(got, want) {
t.Errorf("Repository.Predecessors() = %v, want %v", got, want)
}
}
func TestRepository_Referrers(t *testing.T) {
manifest := []byte(`{"layers":[]}`)
manifestDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(manifest),
Size: int64(len(manifest)),
}
referrerSet := [][]ocispec.Descriptor{
{
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 1,
Digest: digest.FromString("1"),
ArtifactType: "application/vnd.test",
},
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 2,
Digest: digest.FromString("2"),
ArtifactType: "application/vnd.test",
},
},
{
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 3,
Digest: digest.FromString("3"),
ArtifactType: "application/vnd.test",
},
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 4,
Digest: digest.FromString("4"),
ArtifactType: "application/vnd.test",
},
},
{
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 5,
Digest: digest.FromString("5"),
ArtifactType: "application/vnd.test",
},
},
}
var ts *httptest.Server
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := "/v2/test/referrers/" + manifestDesc.Digest.String()
if r.Method != http.MethodGet || r.URL.Path != path {
referrersTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1)
if r.URL.Path != "/v2/test/manifests/"+referrersTag {
t.Errorf("unexpected access: %s %q", r.Method, r.URL)
}
w.WriteHeader(http.StatusNotFound)
return
}
q := r.URL.Query()
n, err := strconv.Atoi(q.Get("n"))
if err != nil || n != 2 {
t.Errorf("bad page size: %s", q.Get("n"))
w.WriteHeader(http.StatusBadRequest)
return
}
var referrers []ocispec.Descriptor
switch q.Get("test") {
case "foo":
referrers = referrerSet[1]
w.Header().Set("Link", fmt.Sprintf(`<%s%s?n=2&test=bar>; rel="next"`, ts.URL, path))
case "bar":
referrers = referrerSet[2]
default:
referrers = referrerSet[0]
w.Header().Set("Link", fmt.Sprintf(`<%s?n=2&test=foo>; rel="next"`, path))
}
result := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: referrers,
}
w.Header().Set("Content-Type", ocispec.MediaTypeImageIndex)
if err := json.NewEncoder(w).Encode(result); err != nil {
t.Errorf("failed to write response: %v", err)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
ctx := context.Background()
// test auto detect
// remote server supports Referrers, should be no error
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
repo.ReferrerListPageSize = 2
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
index := 0
if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error {
if index >= len(referrerSet) {
t.Fatalf("out of index bound: %d", index)
}
referrers := referrerSet[index]
index++
if !reflect.DeepEqual(got, referrers) {
t.Errorf("Repository.Referrers() = %v, want %v", got, referrers)
}
return nil
}); err != nil {
t.Errorf("Repository.Referrers() error = %v", err)
}
if state := repo.loadReferrersState(); state != referrersStateSupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported)
}
// test force attempt Referrers
// remote server supports Referrers, should be no error
repo, err = NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
repo.ReferrerListPageSize = 2
repo.SetReferrersCapability(true)
if state := repo.loadReferrersState(); state != referrersStateSupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported)
}
index = 0
if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error {
if index >= len(referrerSet) {
t.Fatalf("out of index bound: %d", index)
}
referrers := referrerSet[index]
index++
if !reflect.DeepEqual(got, referrers) {
t.Errorf("Repository.Referrers() = %v, want %v", got, referrers)
}
return nil
}); err != nil {
t.Errorf("Repository.Referrers() error = %v", err)
}
if state := repo.loadReferrersState(); state != referrersStateSupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported)
}
// test force attempt tag schema
// request tag schema but got 404, should be no error
repo, err = NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
repo.ReferrerListPageSize = 2
repo.SetReferrersCapability(false)
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error {
return nil
}); err != nil {
t.Errorf("Repository.Referrers() error = %v", err)
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
}
func TestRepository_Referrers_TagSchemaFallback(t *testing.T) {
manifest := []byte(`{"layers":[]}`)
manifestDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(manifest),
Size: int64(len(manifest)),
}
referrers := []ocispec.Descriptor{
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 1,
Digest: digest.FromString("1"),
ArtifactType: "application/vnd.test",
},
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 2,
Digest: digest.FromString("2"),
ArtifactType: "application/vnd.test",
},
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 3,
Digest: digest.FromString("3"),
ArtifactType: "application/vnd.test",
},
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 4,
Digest: digest.FromString("4"),
ArtifactType: "application/vnd.test",
},
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 5,
Digest: digest.FromString("5"),
ArtifactType: "application/vnd.test",
},
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
referrersTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1)
path := "/v2/test/manifests/" + referrersTag
if r.Method != http.MethodGet || r.URL.Path != path {
if r.URL.Path != "/v2/test/referrers/"+manifestDesc.Digest.String() {
t.Errorf("unexpected access: %s %q", r.Method, r.URL)
}
w.WriteHeader(http.StatusNotFound)
return
}
result := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: referrers,
}
if err := json.NewEncoder(w).Encode(result); err != nil {
t.Errorf("failed to write response: %v", err)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
ctx := context.Background()
// test auto detect
// remote server does not support Referrers, should fallback to tag schema
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error {
if !reflect.DeepEqual(got, referrers) {
t.Errorf("Repository.Referrers() = %v, want %v", got, referrers)
}
return nil
}); err != nil {
t.Errorf("Repository.Referrers() error = %v", err)
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
// test force attempt Referrers
// remote server does not support Referrers, should return error
repo, err = NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
repo.SetReferrersCapability(true)
if state := repo.loadReferrersState(); state != referrersStateSupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported)
}
if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error {
return nil
}); err == nil {
t.Errorf("Repository.Referrers() error = %v, wantErr %v", err, true)
}
if state := repo.loadReferrersState(); state != referrersStateSupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported)
}
// test force attempt tag schema
// should request tag schema
repo, err = NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
repo.SetReferrersCapability(false)
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error {
if !reflect.DeepEqual(got, referrers) {
t.Errorf("Repository.Referrers() = %v, want %v", got, referrers)
}
return nil
}); err != nil {
t.Errorf("Repository.Referrers() error = %v", err)
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
}
func TestRepository_Referrers_TagSchemaFallback_NotFound(t *testing.T) {
manifest := []byte(`{"layers":[]}`)
manifestDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(manifest),
Size: int64(len(manifest)),
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
referrersUrl := "/v2/test/referrers/" + manifestDesc.Digest.String()
referrersTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1)
tagSchemaUrl := "/v2/test/manifests/" + referrersTag
if r.Method == http.MethodGet ||
r.URL.Path == referrersUrl ||
r.URL.Path == tagSchemaUrl {
w.WriteHeader(http.StatusNotFound)
return
}
t.Errorf("unexpected access: %s %q", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
ctx := context.Background()
// test auto detect
// tag schema referrers not found, should be no error
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error {
return nil
}); err != nil {
t.Errorf("Repository.Referrers() error = %v", err)
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
// test force attempt tag schema
// tag schema referrers not found, should be no error
repo, err = NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
repo.SetReferrersCapability(false)
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error {
return nil
}); err != nil {
t.Errorf("Repository.Referrers() error = %v", err)
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
}
func TestRepository_Referrers_TagSchemaFallback_ContentType(t *testing.T) {
manifest := []byte(`{"layers":[]}`)
manifestDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(manifest),
Size: int64(len(manifest)),
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
referrersUrl := "/v2/test/referrers/" + manifestDesc.Digest.String()
referrersTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1)
tagSchemaUrl := "/v2/test/manifests/" + referrersTag
if r.URL.Path == referrersUrl {
w.Header().Set("Content-Type", "application/json") // not an OCI image index
w.WriteHeader(http.StatusOK)
return
}
if r.Method == http.MethodGet ||
r.URL.Path == tagSchemaUrl {
w.WriteHeader(http.StatusNotFound)
return
}
t.Errorf("unexpected access: %s %q", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
ctx := context.Background()
// test auto detect
// tag schema referrers not found, should be no error
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error {
return nil
}); err != nil {
t.Errorf("Repository.Referrers() error = %v", err)
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
}
func TestRepository_Referrers_BadRequest(t *testing.T) {
manifest := []byte(`{"layers":[]}`)
manifestDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(manifest),
Size: int64(len(manifest)),
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
referrersUrl := "/v2/test/referrers/" + manifestDesc.Digest.String()
referrersTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1)
tagSchemaUrl := "/v2/test/manifests/" + referrersTag
if r.Method == http.MethodGet ||
r.URL.Path == referrersUrl ||
r.URL.Path == tagSchemaUrl {
w.WriteHeader(http.StatusBadRequest)
return
}
t.Errorf("unexpected access: %s %q", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
ctx := context.Background()
// test auto detect
// Referrers returns error
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error {
return nil
}); err == nil {
t.Errorf("Repository.Referrers() error = nil, wantErr %v", true)
}
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
// test force attempt Referrers
// Referrers returns error
repo, err = NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
repo.SetReferrersCapability(true)
if state := repo.loadReferrersState(); state != referrersStateSupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported)
}
if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error {
return nil
}); err == nil {
t.Errorf("Repository.Referrers() error = nil, wantErr %v", true)
}
if state := repo.loadReferrersState(); state != referrersStateSupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported)
}
// test force attempt tag schema
// Referrers returns error
repo, err = NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
repo.SetReferrersCapability(false)
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error {
return nil
}); err == nil {
t.Errorf("Repository.Referrers() error = nil, wantErr %v", true)
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
}
func TestRepository_Referrers_RepositoryNotFound(t *testing.T) {
manifest := []byte(`{"layers":[]}`)
manifestDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(manifest),
Size: int64(len(manifest)),
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
referrersUrl := "/v2/test/referrers/" + manifestDesc.Digest.String()
referrersTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1)
tagSchemaUrl := "/v2/test/manifests/" + referrersTag
if r.Method == http.MethodGet &&
(r.URL.Path == referrersUrl || r.URL.Path == tagSchemaUrl) {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`{ "errors": [ { "code": "NAME_UNKNOWN", "message": "repository name not known to registry" } ] }`))
return
}
t.Errorf("unexpected access: %s %q", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
ctx := context.Background()
// test auto detect
// repository not found, should return error
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error {
return nil
}); err == nil {
t.Errorf("Repository.Referrers() error = %v, wantErr %v", err, true)
}
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
// test force attempt Referrers
// repository not found, should return error
repo, err = NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
repo.SetReferrersCapability(true)
if state := repo.loadReferrersState(); state != referrersStateSupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported)
}
if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error {
return nil
}); err == nil {
t.Errorf("Repository.Referrers() error = %v, wantErr %v", err, true)
}
if state := repo.loadReferrersState(); state != referrersStateSupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported)
}
// test force attempt tag schema
// repository not found, but should not return error
repo, err = NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
repo.SetReferrersCapability(false)
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error {
return nil
}); err != nil {
t.Errorf("Repository.Referrers() error = %v, wantErr %v", err, nil)
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
}
func TestRepository_Referrers_ServerFiltering(t *testing.T) {
manifest := []byte(`{"layers":[]}`)
manifestDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(manifest),
Size: int64(len(manifest)),
}
referrerSet := [][]ocispec.Descriptor{
{
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 1,
Digest: digest.FromString("1"),
ArtifactType: "application/vnd.test",
},
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 2,
Digest: digest.FromString("2"),
ArtifactType: "application/vnd.test",
},
},
{
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 3,
Digest: digest.FromString("3"),
ArtifactType: "application/vnd.test",
},
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 4,
Digest: digest.FromString("4"),
ArtifactType: "application/vnd.test",
},
},
{
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 5,
Digest: digest.FromString("5"),
ArtifactType: "application/vnd.test",
},
},
}
// Test with filter annotations only
var ts *httptest.Server
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := "/v2/test/referrers/" + manifestDesc.Digest.String()
queryParams, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
t.Fatal("failed to parse url query")
}
if r.Method != http.MethodGet ||
r.URL.Path != path ||
reflect.DeepEqual(queryParams["artifactType"], []string{"application%2Fvnd.test"}) {
t.Errorf("unexpected access: %s %q", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
q := r.URL.Query()
n, err := strconv.Atoi(q.Get("n"))
if err != nil || n != 2 {
t.Errorf("bad page size: %s", q.Get("n"))
w.WriteHeader(http.StatusBadRequest)
return
}
var referrers []ocispec.Descriptor
switch q.Get("test") {
case "foo":
referrers = referrerSet[1]
w.Header().Set("Link", fmt.Sprintf(`<%s%s?n=2&test=bar>; rel="next"`, ts.URL, path))
case "bar":
referrers = referrerSet[2]
default:
referrers = referrerSet[0]
w.Header().Set("Link", fmt.Sprintf(`<%s?n=2&test=foo>; rel="next"`, path))
}
result := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: referrers,
// set filter annotations
Annotations: map[string]string{
spec.AnnotationReferrersFiltersApplied: "artifactType",
},
}
w.Header().Set("Content-Type", ocispec.MediaTypeImageIndex)
if err := json.NewEncoder(w).Encode(result); err != nil {
t.Errorf("failed to write response: %v", err)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
repo.ReferrerListPageSize = 2
ctx := context.Background()
index := 0
if err := repo.Referrers(ctx, manifestDesc, "application/vnd.test", func(got []ocispec.Descriptor) error {
if index >= len(referrerSet) {
t.Fatalf("out of index bound: %d", index)
}
referrers := referrerSet[index]
index++
if !reflect.DeepEqual(got, referrers) {
t.Errorf("Repository.Referrers() = %v, want %v", got, referrers)
}
return nil
}); err != nil {
t.Errorf("Repository.Referrers() error = %v", err)
}
if index != len(referrerSet) {
t.Errorf("fn invoked %d time(s), want %d", index, len(referrerSet))
}
// Test with filter header only
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := "/v2/test/referrers/" + manifestDesc.Digest.String()
queryParams, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
t.Fatal("failed to parse url query")
}
if r.Method != http.MethodGet ||
r.URL.Path != path ||
reflect.DeepEqual(queryParams["artifactType"], []string{"application%2Fvnd.test"}) {
t.Errorf("unexpected access: %s %q", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
q := r.URL.Query()
n, err := strconv.Atoi(q.Get("n"))
if err != nil || n != 2 {
t.Errorf("bad page size: %s", q.Get("n"))
w.WriteHeader(http.StatusBadRequest)
return
}
var referrers []ocispec.Descriptor
switch q.Get("test") {
case "foo":
referrers = referrerSet[1]
w.Header().Set("Link", fmt.Sprintf(`<%s%s?n=2&test=bar>; rel="next"`, ts.URL, path))
case "bar":
referrers = referrerSet[2]
default:
referrers = referrerSet[0]
w.Header().Set("Link", fmt.Sprintf(`<%s?n=2&test=foo>; rel="next"`, path))
}
result := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: referrers,
}
w.Header().Set("Content-Type", ocispec.MediaTypeImageIndex)
// set filter header
w.Header().Set("OCI-Filters-Applied", "artifactType")
if err := json.NewEncoder(w).Encode(result); err != nil {
t.Errorf("failed to write response: %v", err)
}
}))
defer ts.Close()
uri, err = url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err = NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
repo.ReferrerListPageSize = 2
ctx = context.Background()
index = 0
if err := repo.Referrers(ctx, manifestDesc, "application/vnd.test", func(got []ocispec.Descriptor) error {
if index >= len(referrerSet) {
t.Fatalf("out of index bound: %d", index)
}
referrers := referrerSet[index]
index++
if !reflect.DeepEqual(got, referrers) {
t.Errorf("Repository.Referrers() = %v, want %v", got, referrers)
}
return nil
}); err != nil {
t.Errorf("Repository.Referrers() error = %v", err)
}
if index != len(referrerSet) {
t.Errorf("fn invoked %d time(s), want %d", index, len(referrerSet))
}
// Test with both filter annotation and filter header
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := "/v2/test/referrers/" + manifestDesc.Digest.String()
queryParams, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
t.Fatal("failed to parse url query")
}
if r.Method != http.MethodGet ||
r.URL.Path != path ||
reflect.DeepEqual(queryParams["artifactType"], []string{"application%2Fvnd.test"}) {
t.Errorf("unexpected access: %s %q", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
q := r.URL.Query()
n, err := strconv.Atoi(q.Get("n"))
if err != nil || n != 2 {
t.Errorf("bad page size: %s", q.Get("n"))
w.WriteHeader(http.StatusBadRequest)
return
}
var referrers []ocispec.Descriptor
switch q.Get("test") {
case "foo":
referrers = referrerSet[1]
w.Header().Set("Link", fmt.Sprintf(`<%s%s?n=2&test=bar>; rel="next"`, ts.URL, path))
case "bar":
referrers = referrerSet[2]
default:
referrers = referrerSet[0]
w.Header().Set("Link", fmt.Sprintf(`<%s?n=2&test=foo>; rel="next"`, path))
}
result := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: referrers,
// set filter annotation
Annotations: map[string]string{
spec.AnnotationReferrersFiltersApplied: "artifactType",
},
}
w.Header().Set("Content-Type", ocispec.MediaTypeImageIndex)
// set filter header
w.Header().Set("OCI-Filters-Applied", "artifactType")
if err := json.NewEncoder(w).Encode(result); err != nil {
t.Errorf("failed to write response: %v", err)
}
}))
defer ts.Close()
uri, err = url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err = NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
repo.ReferrerListPageSize = 2
ctx = context.Background()
index = 0
if err := repo.Referrers(ctx, manifestDesc, "application/vnd.test", func(got []ocispec.Descriptor) error {
if index >= len(referrerSet) {
t.Fatalf("out of index bound: %d", index)
}
referrers := referrerSet[index]
index++
if !reflect.DeepEqual(got, referrers) {
t.Errorf("Repository.Referrers() = %v, want %v", got, referrers)
}
return nil
}); err != nil {
t.Errorf("Repository.Referrers() error = %v", err)
}
if index != len(referrerSet) {
t.Errorf("fn invoked %d time(s), want %d", index, len(referrerSet))
}
}
func TestRepository_Referrers_ClientFiltering(t *testing.T) {
manifest := []byte(`{"layers":[]}`)
manifestDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(manifest),
Size: int64(len(manifest)),
}
referrerSet := [][]ocispec.Descriptor{
{
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 1,
Digest: digest.FromString("1"),
ArtifactType: "application/vnd.test",
},
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 2,
Digest: digest.FromString("2"),
ArtifactType: "application/vnd.foo",
},
},
{
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 3,
Digest: digest.FromString("3"),
ArtifactType: "application/vnd.test",
},
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 4,
Digest: digest.FromString("4"),
ArtifactType: "application/vnd.bar",
},
},
{
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 5,
Digest: digest.FromString("5"),
ArtifactType: "application/vnd.baz",
},
},
}
filteredReferrerSet := [][]ocispec.Descriptor{
{
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 1,
Digest: digest.FromString("1"),
ArtifactType: "application/vnd.test",
},
},
{
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 3,
Digest: digest.FromString("3"),
ArtifactType: "application/vnd.test",
},
},
}
var ts *httptest.Server
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := "/v2/test/referrers/" + manifestDesc.Digest.String()
queryParams, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
t.Fatal("failed to parse url query")
}
if r.Method != http.MethodGet ||
r.URL.Path != path ||
reflect.DeepEqual(queryParams["artifactType"], []string{"application%2Fvnd.test"}) {
t.Errorf("unexpected access: %s %q", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
q := r.URL.Query()
n, err := strconv.Atoi(q.Get("n"))
if err != nil || n != 2 {
t.Errorf("bad page size: %s", q.Get("n"))
w.WriteHeader(http.StatusBadRequest)
return
}
var referrers []ocispec.Descriptor
switch q.Get("test") {
case "foo":
referrers = referrerSet[1]
w.Header().Set("Link", fmt.Sprintf(`<%s%s?n=2&test=bar>; rel="next"`, ts.URL, path))
case "bar":
referrers = referrerSet[2]
default:
referrers = referrerSet[0]
w.Header().Set("Link", fmt.Sprintf(`<%s?n=2&test=foo>; rel="next"`, path))
}
result := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: referrers,
}
w.Header().Set("Content-Type", ocispec.MediaTypeImageIndex)
if err := json.NewEncoder(w).Encode(result); err != nil {
t.Errorf("failed to write response: %v", err)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
repo.ReferrerListPageSize = 2
ctx := context.Background()
index := 0
if err := repo.Referrers(ctx, manifestDesc, "application/vnd.test", func(got []ocispec.Descriptor) error {
if index >= len(filteredReferrerSet) {
t.Fatalf("out of index bound: %d", index)
}
referrers := filteredReferrerSet[index]
index++
if !reflect.DeepEqual(got, referrers) {
t.Errorf("Repository.Referrers() = %v, want %v", got, referrers)
}
return nil
}); err != nil {
t.Errorf("Repository.Referrers() error = %v", err)
}
if index != len(filteredReferrerSet) {
t.Errorf("fn invoked %d time(s), want %d", index, len(referrerSet))
}
}
func TestRepository_Referrers_TagSchemaFallback_ClientFiltering(t *testing.T) {
manifest := []byte(`{"layers":[]}`)
manifestDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(manifest),
Size: int64(len(manifest)),
}
referrers := []ocispec.Descriptor{
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 1,
Digest: digest.FromString("1"),
ArtifactType: "application/vnd.test",
},
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 2,
Digest: digest.FromString("2"),
ArtifactType: "application/vnd.foo",
},
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 3,
Digest: digest.FromString("3"),
ArtifactType: "application/vnd.test",
},
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 4,
Digest: digest.FromString("4"),
ArtifactType: "application/vnd.bar",
},
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 5,
Digest: digest.FromString("5"),
ArtifactType: "application/vnd.baz",
},
}
filteredReferrers := []ocispec.Descriptor{
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 1,
Digest: digest.FromString("1"),
ArtifactType: "application/vnd.test",
},
{
MediaType: spec.MediaTypeArtifactManifest,
Size: 3,
Digest: digest.FromString("3"),
ArtifactType: "application/vnd.test",
},
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
referrersTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1)
path := "/v2/test/manifests/" + referrersTag
if r.Method != http.MethodGet || r.URL.Path != path {
if r.URL.Path != "/v2/test/referrers/"+manifestDesc.Digest.String() {
t.Errorf("unexpected access: %s %q", r.Method, r.URL)
}
w.WriteHeader(http.StatusNotFound)
return
}
result := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: referrers,
}
if err := json.NewEncoder(w).Encode(result); err != nil {
t.Errorf("failed to write response: %v", err)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
ctx := context.Background()
if err := repo.Referrers(ctx, manifestDesc, "application/vnd.test", func(got []ocispec.Descriptor) error {
if !reflect.DeepEqual(got, filteredReferrers) {
t.Errorf("Repository.Referrers() = %v, want %v", got, filteredReferrers)
}
return nil
}); err != nil {
t.Errorf("Repository.Referrers() error = %v", err)
}
}
func Test_BlobStore_Fetch(t *testing.T) {
blob := []byte("hello world")
blobDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
switch r.URL.Path {
case "/v2/test/blobs/" + blobDesc.Digest.String():
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String())
if _, err := w.Write(blob); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
store := repo.Blobs()
ctx := context.Background()
rc, err := store.Fetch(ctx, blobDesc)
if err != nil {
t.Fatalf("Blobs.Fetch() error = %v", err)
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(rc); err != nil {
t.Errorf("fail to read: %v", err)
}
if err := rc.Close(); err != nil {
t.Errorf("fail to close: %v", err)
}
if got := buf.Bytes(); !bytes.Equal(got, blob) {
t.Errorf("Blobs.Fetch() = %v, want %v", got, blob)
}
content := []byte("foobar")
contentDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
_, err = store.Fetch(ctx, contentDesc)
if !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("Blobs.Fetch() error = %v, wantErr %v", err, errdef.ErrNotFound)
}
}
func Test_BlobStore_Fetch_Seek(t *testing.T) {
blob := []byte("hello world")
blobDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
seekable := false
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
switch r.URL.Path {
case "/v2/test/blobs/" + blobDesc.Digest.String():
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String())
if seekable {
w.Header().Set("Accept-Ranges", "bytes")
}
rangeHeader := r.Header.Get("Range")
if !seekable || rangeHeader == "" {
w.WriteHeader(http.StatusOK)
if _, err := w.Write(blob); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
return
}
var start, end int
_, err := fmt.Sscanf(rangeHeader, "bytes=%d-%d", &start, &end)
if err != nil {
t.Errorf("invalid range header: %s", rangeHeader)
w.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
return
}
if start < 0 || start > end || start >= int(blobDesc.Size) {
t.Errorf("invalid range: %s", rangeHeader)
w.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
return
}
end++
if end > int(blobDesc.Size) {
end = int(blobDesc.Size)
}
w.WriteHeader(http.StatusPartialContent)
if _, err := w.Write(blob[start:end]); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
store := repo.Blobs()
ctx := context.Background()
rc, err := store.Fetch(ctx, blobDesc)
if err != nil {
t.Fatalf("Blobs.Fetch() error = %v", err)
}
if _, ok := rc.(io.Seeker); ok {
t.Errorf("Blobs.Fetch() returns io.Seeker on non-seekable content")
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(rc); err != nil {
t.Errorf("fail to read: %v", err)
}
if err := rc.Close(); err != nil {
t.Errorf("fail to close: %v", err)
}
if got := buf.Bytes(); !bytes.Equal(got, blob) {
t.Errorf("Blobs.Fetch() = %v, want %v", got, blob)
}
seekable = true
rc, err = store.Fetch(ctx, blobDesc)
if err != nil {
t.Fatalf("Blobs.Fetch() error = %v", err)
}
s, ok := rc.(io.Seeker)
if !ok {
t.Fatalf("Blobs.Fetch() = %v, want io.Seeker", rc)
}
buf.Reset()
if _, err := buf.ReadFrom(rc); err != nil {
t.Errorf("fail to read: %v", err)
}
if got := buf.Bytes(); !bytes.Equal(got, blob) {
t.Errorf("Blobs.Fetch() = %v, want %v", got, blob)
}
_, err = s.Seek(3, io.SeekStart)
if err != nil {
t.Errorf("fail to seek: %v", err)
}
buf.Reset()
if _, err := buf.ReadFrom(rc); err != nil {
t.Errorf("fail to read: %v", err)
}
if got := buf.Bytes(); !bytes.Equal(got, blob[3:]) {
t.Errorf("Blobs.Fetch() = %v, want %v", got, blob[3:])
}
if err := rc.Close(); err != nil {
t.Errorf("fail to close: %v", err)
}
}
func Test_BlobStore_Fetch_ZeroSizedBlob(t *testing.T) {
blob := []byte("")
blobDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
switch r.URL.Path {
case "/v2/test/blobs/" + blobDesc.Digest.String():
if rangeHeader := r.Header.Get("Range"); rangeHeader != "" {
t.Errorf("unexpected range header")
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String())
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
store := repo.Blobs()
ctx := context.Background()
rc, err := store.Fetch(ctx, blobDesc)
if err != nil {
t.Fatalf("Blobs.Fetch() error = %v", err)
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(rc); err != nil {
t.Errorf("fail to read: %v", err)
}
if err := rc.Close(); err != nil {
t.Errorf("fail to close: %v", err)
}
if got := buf.Bytes(); !bytes.Equal(got, blob) {
t.Errorf("Blobs.Fetch() = %v, want %v", got, blob)
}
}
func Test_BlobStore_Push(t *testing.T) {
blob := []byte("hello world")
blobDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
var gotBlob []byte
uuid := "4fd53bc9-565d-4527-ab80-3e051ac4880c"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPost && r.URL.Path == "/v2/test/blobs/uploads/":
w.Header().Set("Location", "/v2/test/blobs/uploads/"+uuid)
w.WriteHeader(http.StatusAccepted)
return
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/blobs/uploads/"+uuid:
if contentType := r.Header.Get("Content-Type"); contentType != "application/octet-stream" {
w.WriteHeader(http.StatusBadRequest)
break
}
if contentDigest := r.URL.Query().Get("digest"); contentDigest != blobDesc.Digest.String() {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotBlob = buf.Bytes()
w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
return
default:
w.WriteHeader(http.StatusForbidden)
}
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
store := repo.Blobs()
ctx := context.Background()
err = store.Push(ctx, blobDesc, bytes.NewReader(blob))
if err != nil {
t.Fatalf("Blobs.Push() error = %v", err)
}
if !bytes.Equal(gotBlob, blob) {
t.Errorf("Blobs.Push() = %v, want %v", gotBlob, blob)
}
}
func Test_BlobStore_Exists(t *testing.T) {
blob := []byte("hello world")
blobDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodHead {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
switch r.URL.Path {
case "/v2/test/blobs/" + blobDesc.Digest.String():
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(int(blobDesc.Size)))
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
store := repo.Blobs()
ctx := context.Background()
exists, err := store.Exists(ctx, blobDesc)
if err != nil {
t.Fatalf("Blobs.Exists() error = %v", err)
}
if !exists {
t.Errorf("Blobs.Exists() = %v, want %v", exists, true)
}
content := []byte("foobar")
contentDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
exists, err = store.Exists(ctx, contentDesc)
if err != nil {
t.Fatalf("Blobs.Exists() error = %v", err)
}
if exists {
t.Errorf("Blobs.Exists() = %v, want %v", exists, false)
}
}
func Test_BlobStore_Delete(t *testing.T) {
blob := []byte("hello world")
blobDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
blobDeleted := false
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
switch r.URL.Path {
case "/v2/test/blobs/" + blobDesc.Digest.String():
blobDeleted = true
w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String())
w.WriteHeader(http.StatusAccepted)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
store := repo.Blobs()
ctx := context.Background()
err = store.Delete(ctx, blobDesc)
if err != nil {
t.Fatalf("Blobs.Delete() error = %v", err)
}
if !blobDeleted {
t.Errorf("Blobs.Delete() = %v, want %v", blobDeleted, true)
}
content := []byte("foobar")
contentDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
err = store.Delete(ctx, contentDesc)
if !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("Blobs.Delete() error = %v, wantErr %v", err, errdef.ErrNotFound)
}
}
func Test_BlobStore_Resolve(t *testing.T) {
blob := []byte("hello world")
blobDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
ref := "foobar"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodHead {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
switch r.URL.Path {
case "/v2/test/blobs/" + blobDesc.Digest.String():
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(int(blobDesc.Size)))
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repoName := uri.Host + "/test"
repo, err := NewRepository(repoName)
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
store := repo.Blobs()
ctx := context.Background()
got, err := store.Resolve(ctx, blobDesc.Digest.String())
if err != nil {
t.Fatalf("Blobs.Resolve() error = %v", err)
}
if got.Digest != blobDesc.Digest || got.Size != blobDesc.Size {
t.Errorf("Blobs.Resolve() = %v, want %v", got, blobDesc)
}
_, err = store.Resolve(ctx, ref)
if !errors.Is(err, digest.ErrDigestInvalidFormat) {
t.Errorf("Blobs.Resolve() error = %v, wantErr %v", err, digest.ErrDigestInvalidFormat)
}
fqdnRef := repoName + "@" + blobDesc.Digest.String()
got, err = store.Resolve(ctx, fqdnRef)
if err != nil {
t.Fatalf("Blobs.Resolve() error = %v", err)
}
if got.Digest != blobDesc.Digest || got.Size != blobDesc.Size {
t.Errorf("Blobs.Resolve() = %v, want %v", got, blobDesc)
}
content := []byte("foobar")
contentDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
_, err = store.Resolve(ctx, contentDesc.Digest.String())
if !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("Blobs.Resolve() error = %v, wantErr %v", err, errdef.ErrNotFound)
}
}
func Test_BlobStore_FetchReference(t *testing.T) {
blob := []byte("hello world")
blobDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
ref := "foobar"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
switch r.URL.Path {
case "/v2/test/blobs/" + blobDesc.Digest.String():
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String())
if _, err := w.Write(blob); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repoName := uri.Host + "/test"
repo, err := NewRepository(repoName)
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
store := repo.Blobs()
ctx := context.Background()
// test with digest
gotDesc, rc, err := store.FetchReference(ctx, blobDesc.Digest.String())
if err != nil {
t.Fatalf("Blobs.FetchReference() error = %v", err)
}
if gotDesc.Digest != blobDesc.Digest || gotDesc.Size != blobDesc.Size {
t.Errorf("Blobs.FetchReference() = %v, want %v", gotDesc, blobDesc)
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(rc); err != nil {
t.Errorf("fail to read: %v", err)
}
if err := rc.Close(); err != nil {
t.Errorf("fail to close: %v", err)
}
if got := buf.Bytes(); !bytes.Equal(got, blob) {
t.Errorf("Blobs.FetchReference() = %v, want %v", got, blob)
}
// test with non-digest reference
_, _, err = store.FetchReference(ctx, ref)
if !errors.Is(err, digest.ErrDigestInvalidFormat) {
t.Errorf("Blobs.FetchReference() error = %v, wantErr %v", err, digest.ErrDigestInvalidFormat)
}
// test with FQDN reference
fqdnRef := repoName + "@" + blobDesc.Digest.String()
gotDesc, rc, err = store.FetchReference(ctx, fqdnRef)
if err != nil {
t.Fatalf("Blobs.FetchReference() error = %v", err)
}
if gotDesc.Digest != blobDesc.Digest || gotDesc.Size != blobDesc.Size {
t.Errorf("Blobs.FetchReference() = %v, want %v", gotDesc, blobDesc)
}
buf.Reset()
if _, err := buf.ReadFrom(rc); err != nil {
t.Errorf("fail to read: %v", err)
}
if err := rc.Close(); err != nil {
t.Errorf("fail to close: %v", err)
}
if got := buf.Bytes(); !bytes.Equal(got, blob) {
t.Errorf("Blobs.FetchReference() = %v, want %v", got, blob)
}
content := []byte("foobar")
contentDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
// test with other digest
_, _, err = store.FetchReference(ctx, contentDesc.Digest.String())
if !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("Blobs.FetchReference() error = %v, wantErr %v", err, errdef.ErrNotFound)
}
}
func Test_BlobStore_FetchReference_Seek(t *testing.T) {
blob := []byte("hello world")
blobDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
seekable := false
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
switch r.URL.Path {
case "/v2/test/blobs/" + blobDesc.Digest.String():
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String())
if seekable {
w.Header().Set("Accept-Ranges", "bytes")
}
rangeHeader := r.Header.Get("Range")
if !seekable || rangeHeader == "" {
w.WriteHeader(http.StatusOK)
if _, err := w.Write(blob); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
return
}
var start int
_, err := fmt.Sscanf(rangeHeader, "bytes=%d-", &start)
if err != nil {
t.Errorf("invalid range header: %s", rangeHeader)
w.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
return
}
if start < 0 || start >= int(blobDesc.Size) {
t.Errorf("invalid range: %s", rangeHeader)
w.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
return
}
w.WriteHeader(http.StatusPartialContent)
if _, err := w.Write(blob[start:]); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
store := repo.Blobs()
ctx := context.Background()
// test non-seekable content
gotDesc, rc, err := store.FetchReference(ctx, blobDesc.Digest.String())
if err != nil {
t.Fatalf("Blobs.FetchReference() error = %v", err)
}
if gotDesc.Digest != blobDesc.Digest || gotDesc.Size != blobDesc.Size {
t.Errorf("Blobs.FetchReference() = %v, want %v", gotDesc, blobDesc)
}
if _, ok := rc.(io.Seeker); ok {
t.Errorf("Blobs.FetchReference() returns io.Seeker on non-seekable content")
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(rc); err != nil {
t.Errorf("fail to read: %v", err)
}
if err := rc.Close(); err != nil {
t.Errorf("fail to close: %v", err)
}
if got := buf.Bytes(); !bytes.Equal(got, blob) {
t.Errorf("Blobs.FetchReference() = %v, want %v", got, blob)
}
// test seekable content
seekable = true
gotDesc, rc, err = store.FetchReference(ctx, blobDesc.Digest.String())
if err != nil {
t.Fatalf("Blobs.FetchReference() error = %v", err)
}
if gotDesc.Digest != blobDesc.Digest || gotDesc.Size != blobDesc.Size {
t.Errorf("Blobs.FetchReference() = %v, want %v", gotDesc, blobDesc)
}
s, ok := rc.(io.Seeker)
if !ok {
t.Fatalf("Blobs.FetchReference() = %v, want io.Seeker", rc)
}
buf.Reset()
if _, err := buf.ReadFrom(rc); err != nil {
t.Errorf("fail to read: %v", err)
}
if got := buf.Bytes(); !bytes.Equal(got, blob) {
t.Errorf("Blobs.FetchReference() = %v, want %v", got, blob)
}
_, err = s.Seek(3, io.SeekStart)
if err != nil {
t.Errorf("fail to seek: %v", err)
}
buf.Reset()
if _, err := buf.ReadFrom(rc); err != nil {
t.Errorf("fail to read: %v", err)
}
if got := buf.Bytes(); !bytes.Equal(got, blob[3:]) {
t.Errorf("Blobs.FetchReference() = %v, want %v", got, blob[3:])
}
if err := rc.Close(); err != nil {
t.Errorf("fail to close: %v", err)
}
}
func Test_generateBlobDescriptorWithVariousDockerContentDigestHeaders(t *testing.T) {
reference := registry.Reference{
Registry: "eastern.haan.com",
Reference: "",
Repository: "from25to220ce",
}
tests := getTestIOStructMapForGetDescriptorClass()
for testName, dcdIOStruct := range tests {
if dcdIOStruct.isTag {
continue
}
for i, method := range []string{http.MethodGet, http.MethodHead} {
reference.Reference = dcdIOStruct.clientSuppliedReference
resp := http.Response{
Header: http.Header{
"Content-Type": []string{"application/vnd.docker.distribution.manifest.v2+json"},
headerDockerContentDigest: []string{dcdIOStruct.serverCalculatedDigest.String()},
},
}
if method == http.MethodGet {
resp.Body = io.NopCloser(bytes.NewBufferString(theAmazingBanClan))
}
resp.Request = &http.Request{
Method: method,
}
var err error
var d digest.Digest
if d, err = reference.Digest(); err != nil {
t.Errorf(
"[Blob.%v] %v; got digest from a tag reference unexpectedly",
method, testName,
)
}
errExpected := []bool{dcdIOStruct.errExpectedOnGET, dcdIOStruct.errExpectedOnHEAD}[i]
if len(d) == 0 {
// To avoid an otherwise impossible scenario in the tested code
// path, we set d so that verifyContentDigest does not break.
d = dcdIOStruct.serverCalculatedDigest
}
_, err = generateBlobDescriptor(&resp, d)
if !errExpected && err != nil {
t.Errorf(
"[Blob.%v] %v; expected no error for request, but got err: %v",
method, testName, err,
)
} else if errExpected && err == nil {
t.Errorf(
"[Blob.%v] %v; expected an error for request, but got none",
method, testName,
)
}
}
}
}
func TestManifestStoreInterface(t *testing.T) {
var ms interface{} = &manifestStore{}
if _, ok := ms.(interfaces.ReferenceParser); !ok {
t.Error("&manifestStore{} does not conform interfaces.ReferenceParser")
}
}
func TestRepositoryMounterInterface(t *testing.T) {
var r interface{} = &Repository{}
if _, ok := r.(registry.Mounter); !ok {
t.Error("&Repository{} does not conform to registry.Mounter")
}
}
func Test_ManifestStore_Fetch(t *testing.T) {
manifest := []byte(`{"layers":[]}`)
manifestDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(manifest),
Size: int64(len(manifest)),
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
switch r.URL.Path {
case "/v2/test/manifests/" + manifestDesc.Digest.String():
if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", manifestDesc.MediaType)
w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String())
if _, err := w.Write(manifest); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
store := repo.Manifests()
ctx := context.Background()
rc, err := store.Fetch(ctx, manifestDesc)
if err != nil {
t.Fatalf("Manifests.Fetch() error = %v", err)
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(rc); err != nil {
t.Errorf("fail to read: %v", err)
}
if err := rc.Close(); err != nil {
t.Errorf("fail to close: %v", err)
}
if got := buf.Bytes(); !bytes.Equal(got, manifest) {
t.Errorf("Manifests.Fetch() = %v, want %v", got, manifest)
}
content := []byte(`{"manifests":[]}`)
contentDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageIndex,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
_, err = store.Fetch(ctx, contentDesc)
if !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("Manifests.Fetch() error = %v, wantErr %v", err, errdef.ErrNotFound)
}
}
func Test_ManifestStore_Push(t *testing.T) {
manifest := []byte(`{"layers":[]}`)
manifestDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(manifest),
Size: int64(len(manifest)),
}
var gotManifest []byte
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String():
if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotManifest = buf.Bytes()
w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
return
default:
w.WriteHeader(http.StatusForbidden)
}
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
store := repo.Manifests()
ctx := context.Background()
err = store.Push(ctx, manifestDesc, bytes.NewReader(manifest))
if err != nil {
t.Fatalf("Manifests.Push() error = %v", err)
}
if !bytes.Equal(gotManifest, manifest) {
t.Errorf("Manifests.Push() = %v, want %v", gotManifest, manifest)
}
}
func Test_ManifestStore_Push_ReferrersAPIAvailable(t *testing.T) {
// generate test content
subject := []byte(`{"layers":[]}`)
subjectDesc := content.NewDescriptorFromBytes(spec.MediaTypeArtifactManifest, subject)
artifact := spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
Subject: &subjectDesc,
}
artifactJSON, err := json.Marshal(artifact)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
artifactDesc := content.NewDescriptorFromBytes(artifact.MediaType, artifactJSON)
manifest := ocispec.Manifest{
MediaType: ocispec.MediaTypeImageManifest,
Subject: &subjectDesc,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
manifestDesc := content.NewDescriptorFromBytes(manifest.MediaType, manifestJSON)
index := ocispec.Index{
MediaType: ocispec.MediaTypeImageIndex,
Subject: &subjectDesc,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
indexDesc := content.NewDescriptorFromBytes(manifest.MediaType, indexJSON)
var gotManifest []byte
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String():
if contentType := r.Header.Get("Content-Type"); contentType != artifactDesc.MediaType {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotManifest = buf.Bytes()
w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String())
w.Header().Set("OCI-Subject", subjectDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String():
if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotManifest = buf.Bytes()
w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String())
w.Header().Set("OCI-Subject", subjectDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String():
if contentType := r.Header.Get("Content-Type"); contentType != indexDesc.MediaType {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotManifest = buf.Bytes()
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
w.Header().Set("OCI-Subject", subjectDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
ctx := context.Background()
// test pushing artifact with subject
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
err = repo.Push(ctx, artifactDesc, bytes.NewReader(artifactJSON))
if err != nil {
t.Fatalf("Manifests.Push() error = %v", err)
}
if !bytes.Equal(gotManifest, artifactJSON) {
t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(artifactJSON))
}
if state := repo.loadReferrersState(); state != referrersStateSupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported)
}
// test pushing image manifest with subject
repo, err = NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
err = repo.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON))
if err != nil {
t.Fatalf("Manifests.Push() error = %v", err)
}
if !bytes.Equal(gotManifest, manifestJSON) {
t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(manifestJSON))
}
if state := repo.loadReferrersState(); state != referrersStateSupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported)
}
// test pushing image index with subject
err = repo.Push(ctx, indexDesc, bytes.NewReader(indexJSON))
if err != nil {
t.Fatalf("Manifests.Push() error = %v", err)
}
if !bytes.Equal(gotManifest, indexJSON) {
t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(indexJSON))
}
if state := repo.loadReferrersState(); state != referrersStateSupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported)
}
}
func Test_ManifestStore_Push_ReferrersAPIUnavailable(t *testing.T) {
// generate test content
subject := []byte(`{"layers":[]}`)
subjectDesc := content.NewDescriptorFromBytes(spec.MediaTypeArtifactManifest, subject)
referrersTag := strings.Replace(subjectDesc.Digest.String(), ":", "-", 1)
artifact := spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
Subject: &subjectDesc,
ArtifactType: "application/vnd.test",
Annotations: map[string]string{"foo": "bar"},
}
artifactJSON, err := json.Marshal(artifact)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
artifactDesc := content.NewDescriptorFromBytes(artifact.MediaType, artifactJSON)
artifactDesc.ArtifactType = artifact.ArtifactType
artifactDesc.Annotations = artifact.Annotations
// test pushing artifact with subject, a referrer list should be created
index_1 := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: []ocispec.Descriptor{
artifactDesc,
},
}
indexJSON_1, err := json.Marshal(index_1)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
indexDesc_1 := content.NewDescriptorFromBytes(index_1.MediaType, indexJSON_1)
var gotManifest []byte
var gotReferrerIndex []byte
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String():
if contentType := r.Header.Get("Content-Type"); contentType != artifactDesc.MediaType {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotManifest = buf.Bytes()
w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag:
w.WriteHeader(http.StatusNotFound)
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag:
if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotReferrerIndex = buf.Bytes()
w.Header().Set("Docker-Content-Digest", indexDesc_1.Digest.String())
w.WriteHeader(http.StatusCreated)
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
ctx := context.Background()
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
err = repo.Push(ctx, artifactDesc, bytes.NewReader(artifactJSON))
if err != nil {
t.Fatalf("Manifests.Push() error = %v", err)
}
if !bytes.Equal(gotManifest, artifactJSON) {
t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(artifactJSON))
}
if !bytes.Equal(gotReferrerIndex, indexJSON_1) {
t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_1))
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
// test pushing artifact with subject when an old empty referrer list exists,
// the referrer list should be updated
emptyIndex := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
}
emptyIndexJSON, err := json.Marshal(emptyIndex)
if err != nil {
t.Error("failed to marshal index", err)
}
emptyIndexDesc := content.NewDescriptorFromBytes(emptyIndex.MediaType, emptyIndexJSON)
var indexDeleted bool
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String():
if contentType := r.Header.Get("Content-Type"); contentType != artifactDesc.MediaType {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotManifest = buf.Bytes()
w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag:
w.Write(emptyIndexJSON)
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag:
if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotReferrerIndex = buf.Bytes()
w.Header().Set("Docker-Content-Digest", indexDesc_1.Digest.String())
w.WriteHeader(http.StatusCreated)
case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+emptyIndexDesc.Digest.String():
indexDeleted = true
// no "Docker-Content-Digest" header for manifest deletion
w.WriteHeader(http.StatusAccepted)
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err = url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
ctx = context.Background()
repo, err = NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
err = repo.Push(ctx, artifactDesc, bytes.NewReader(artifactJSON))
if err != nil {
t.Fatalf("Manifests.Push() error = %v", err)
}
if !bytes.Equal(gotManifest, artifactJSON) {
t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(artifactJSON))
}
if !bytes.Equal(gotReferrerIndex, indexJSON_1) {
t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_1))
}
if !indexDeleted {
t.Errorf("indexDeleted = %v, want %v", indexDeleted, true)
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
// test pushing image manifest with subject, referrer list should be updated
manifest := ocispec.Manifest{
MediaType: ocispec.MediaTypeImageManifest,
Config: ocispec.Descriptor{
MediaType: "testconfig",
},
Subject: &subjectDesc,
Annotations: map[string]string{"foo": "bar"},
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
manifestDesc := content.NewDescriptorFromBytes(manifest.MediaType, manifestJSON)
manifestDesc.ArtifactType = manifest.Config.MediaType
manifestDesc.Annotations = manifest.Annotations
index_2 := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: []ocispec.Descriptor{
artifactDesc,
manifestDesc,
},
}
indexJSON_2, err := json.Marshal(index_2)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
indexDesc_2 := content.NewDescriptorFromBytes(index_2.MediaType, indexJSON_2)
indexDeleted = false
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String():
if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotManifest = buf.Bytes()
w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag:
w.Write(indexJSON_1)
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag:
if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotReferrerIndex = buf.Bytes()
w.Header().Set("Docker-Content-Digest", indexDesc_2.Digest.String())
w.WriteHeader(http.StatusCreated)
case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+indexDesc_1.Digest.String():
indexDeleted = true
// no "Docker-Content-Digest" header for manifest deletion
w.WriteHeader(http.StatusAccepted)
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err = url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
ctx = context.Background()
repo, err = NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
err = repo.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON))
if err != nil {
t.Fatalf("Manifests.Push() error = %v", err)
}
if !bytes.Equal(gotManifest, manifestJSON) {
t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(manifestJSON))
}
if !bytes.Equal(gotReferrerIndex, indexJSON_2) {
t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_2))
}
if !indexDeleted {
t.Errorf("indexDeleted = %v, want %v", indexDeleted, true)
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
// test pushing image manifest with subject again, referrers list should not be changed
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String():
if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotManifest = buf.Bytes()
w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag:
w.Write(indexJSON_2)
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err = url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
ctx = context.Background()
repo, err = NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
err = repo.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON))
if err != nil {
t.Fatalf("Manifests.Push() error = %v", err)
}
if !bytes.Equal(gotManifest, manifestJSON) {
t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(manifestJSON))
}
// referrers list should not be changed
if !bytes.Equal(gotReferrerIndex, indexJSON_2) {
t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_2))
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
// push image index with subject, referrer list should be updated
indexManifest := ocispec.Index{
MediaType: ocispec.MediaTypeImageIndex,
Subject: &subjectDesc,
ArtifactType: "test/index",
Annotations: map[string]string{"foo": "bar"},
}
indexManifestJSON, err := json.Marshal(indexManifest)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
indexManifestDesc := content.NewDescriptorFromBytes(indexManifest.MediaType, indexManifestJSON)
indexManifestDesc.ArtifactType = indexManifest.ArtifactType
indexManifestDesc.Annotations = indexManifest.Annotations
index_3 := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: []ocispec.Descriptor{
artifactDesc,
manifestDesc,
indexManifestDesc,
},
}
indexJSON_3, err := json.Marshal(index_3)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
indexDesc_3 := content.NewDescriptorFromBytes(index_3.MediaType, indexJSON_3)
indexDeleted = false
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+indexManifestDesc.Digest.String():
if contentType := r.Header.Get("Content-Type"); contentType != indexManifestDesc.MediaType {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotManifest = buf.Bytes()
w.Header().Set("Docker-Content-Digest", indexManifestDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag:
w.Write(indexJSON_2)
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag:
if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotReferrerIndex = buf.Bytes()
w.Header().Set("Docker-Content-Digest", indexDesc_3.Digest.String())
w.WriteHeader(http.StatusCreated)
case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+indexDesc_2.Digest.String():
indexDeleted = true
// no "Docker-Content-Digest" header for manifest deletion
w.WriteHeader(http.StatusAccepted)
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err = url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
ctx = context.Background()
repo, err = NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
err = repo.Push(ctx, indexManifestDesc, bytes.NewReader(indexManifestJSON))
if err != nil {
t.Fatalf("Manifests.Push() error = %v", err)
}
if !bytes.Equal(gotManifest, indexManifestJSON) {
t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(indexManifestJSON))
}
if !bytes.Equal(gotReferrerIndex, indexJSON_3) {
t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_3))
}
if !indexDeleted {
t.Errorf("indexDeleted = %v, want %v", indexDeleted, true)
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
}
func Test_ManifestStore_Push_ReferrersAPIUnavailable_SkipReferrersGC(t *testing.T) {
// generate test content
subject := []byte(`{"layers":[]}`)
subjectDesc := content.NewDescriptorFromBytes(spec.MediaTypeArtifactManifest, subject)
referrersTag := strings.Replace(subjectDesc.Digest.String(), ":", "-", 1)
manifest := ocispec.Manifest{
MediaType: ocispec.MediaTypeImageManifest,
Config: ocispec.Descriptor{
MediaType: "testconfig",
},
Subject: &subjectDesc,
Annotations: map[string]string{"foo": "bar"},
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
manifestDesc := content.NewDescriptorFromBytes(manifest.MediaType, manifestJSON)
manifestDesc.ArtifactType = manifest.Config.MediaType
manifestDesc.Annotations = manifest.Annotations
index_1 := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: []ocispec.Descriptor{
manifestDesc,
},
}
// test pushing image manifest with subject, a referrers list should be created
indexJSON_1, err := json.Marshal(index_1)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
indexDesc_1 := content.NewDescriptorFromBytes(index_1.MediaType, indexJSON_1)
var gotManifest []byte
var gotReferrerIndex []byte
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String():
if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotManifest = buf.Bytes()
w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag:
w.WriteHeader(http.StatusNotFound)
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag:
if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotReferrerIndex = buf.Bytes()
w.Header().Set("Docker-Content-Digest", indexDesc_1.Digest.String())
w.WriteHeader(http.StatusCreated)
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
ctx := context.Background()
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
repo.SkipReferrersGC = true
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
err = repo.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON))
if err != nil {
t.Fatalf("Manifests.Push() error = %v", err)
}
if !bytes.Equal(gotManifest, manifestJSON) {
t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(manifestJSON))
}
if !bytes.Equal(gotReferrerIndex, indexJSON_1) {
t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_1))
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
// test pushing image manifest with subject when an old empty referrer list exists,
// the referrer list should be updated
emptyIndex := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
}
emptyIndexJSON, err := json.Marshal(emptyIndex)
if err != nil {
t.Error("failed to marshal index", err)
}
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String():
if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotManifest = buf.Bytes()
w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag:
w.Write(emptyIndexJSON)
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag:
if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotReferrerIndex = buf.Bytes()
w.Header().Set("Docker-Content-Digest", indexDesc_1.Digest.String())
w.WriteHeader(http.StatusCreated)
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err = url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
ctx = context.Background()
repo, err = NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
repo.SkipReferrersGC = true
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
err = repo.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON))
if err != nil {
t.Fatalf("Manifests.Push() error = %v", err)
}
if !bytes.Equal(gotManifest, manifestJSON) {
t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(manifestJSON))
}
if !bytes.Equal(gotReferrerIndex, indexJSON_1) {
t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_1))
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
// push image index with subject, referrer list should be updated, the old
// one should not be deleted
indexManifest := ocispec.Index{
MediaType: ocispec.MediaTypeImageIndex,
Subject: &subjectDesc,
ArtifactType: "test/index",
Annotations: map[string]string{"foo": "bar"},
}
indexManifestJSON, err := json.Marshal(indexManifest)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
indexManifestDesc := content.NewDescriptorFromBytes(indexManifest.MediaType, indexManifestJSON)
indexManifestDesc.ArtifactType = indexManifest.ArtifactType
indexManifestDesc.Annotations = indexManifest.Annotations
index_2 := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: []ocispec.Descriptor{
manifestDesc,
indexManifestDesc,
},
}
indexJSON_2, err := json.Marshal(index_2)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
indexDesc_2 := content.NewDescriptorFromBytes(index_2.MediaType, indexJSON_2)
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+indexManifestDesc.Digest.String():
if contentType := r.Header.Get("Content-Type"); contentType != indexManifestDesc.MediaType {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotManifest = buf.Bytes()
w.Header().Set("Docker-Content-Digest", indexManifestDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag:
w.Write(indexJSON_1)
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag:
if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotReferrerIndex = buf.Bytes()
w.Header().Set("Docker-Content-Digest", indexDesc_2.Digest.String())
w.WriteHeader(http.StatusCreated)
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err = url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
ctx = context.Background()
repo, err = NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
repo.SkipReferrersGC = true
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
err = repo.Push(ctx, indexManifestDesc, bytes.NewReader(indexManifestJSON))
if err != nil {
t.Fatalf("Manifests.Push() error = %v", err)
}
if !bytes.Equal(gotManifest, indexManifestJSON) {
t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(indexManifestJSON))
}
if !bytes.Equal(gotReferrerIndex, indexJSON_2) {
t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_2))
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
}
func Test_ManifestStore_Exists(t *testing.T) {
manifest := []byte(`{"layers":[]}`)
manifestDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(manifest),
Size: int64(len(manifest)),
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodHead {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
switch r.URL.Path {
case "/v2/test/manifests/" + manifestDesc.Digest.String():
if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", manifestDesc.MediaType)
w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(int(manifestDesc.Size)))
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
store := repo.Manifests()
ctx := context.Background()
exists, err := store.Exists(ctx, manifestDesc)
if err != nil {
t.Fatalf("Manifests.Exists() error = %v", err)
}
if !exists {
t.Errorf("Manifests.Exists() = %v, want %v", exists, true)
}
content := []byte(`{"manifests":[]}`)
contentDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageIndex,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
exists, err = store.Exists(ctx, contentDesc)
if err != nil {
t.Fatalf("Manifests.Exists() error = %v", err)
}
if exists {
t.Errorf("Manifests.Exists() = %v, want %v", exists, false)
}
}
func Test_ManifestStore_Delete(t *testing.T) {
manifest := []byte(`{"layers":[]}`)
manifestDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(manifest),
Size: int64(len(manifest)),
}
manifestDeleted := false
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete && r.Method != http.MethodGet {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusMethodNotAllowed)
}
switch {
case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String():
manifestDeleted = true
// no "Docker-Content-Digest" header for manifest deletion
w.WriteHeader(http.StatusAccepted)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String():
if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", manifestDesc.MediaType)
w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String())
if _, err := w.Write(manifest); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
store := repo.Manifests()
ctx := context.Background()
// test deleting manifest without subject
err = store.Delete(ctx, manifestDesc)
if err != nil {
t.Fatalf("Manifests.Delete() error = %v", err)
}
if !manifestDeleted {
t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true)
}
// test deleting content that does not exist
content := []byte(`{"manifests":[]}`)
contentDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageIndex,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
err = store.Delete(ctx, contentDesc)
if !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("Manifests.Delete() error = %v, wantErr %v", err, errdef.ErrNotFound)
}
}
func Test_ManifestStore_Delete_ReferrersAPIAvailable(t *testing.T) {
// generate test content
subject := []byte(`{"layers":[]}`)
subjectDesc := content.NewDescriptorFromBytes(spec.MediaTypeArtifactManifest, subject)
artifact := spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
Subject: &subjectDesc,
}
artifactJSON, err := json.Marshal(artifact)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
artifactDesc := content.NewDescriptorFromBytes(artifact.MediaType, artifactJSON)
manifest := ocispec.Manifest{
MediaType: ocispec.MediaTypeImageManifest,
Subject: &subjectDesc,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
manifestDesc := content.NewDescriptorFromBytes(manifest.MediaType, manifestJSON)
index := ocispec.Index{
MediaType: ocispec.MediaTypeImageIndex,
Subject: &subjectDesc,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
indexDesc := content.NewDescriptorFromBytes(index.MediaType, indexJSON)
var manifestDeleted bool
var artifactDeleted bool
var indexDeleted bool
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete && r.Method != http.MethodGet {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusMethodNotAllowed)
}
switch {
case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String():
artifactDeleted = true
// no "Docker-Content-Digest" header for manifest deletion
w.WriteHeader(http.StatusAccepted)
case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String():
manifestDeleted = true
// no "Docker-Content-Digest" header for manifest deletion
w.WriteHeader(http.StatusAccepted)
case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String():
indexDeleted = true
// no "Docker-Content-Digest" header for manifest deletion
w.WriteHeader(http.StatusAccepted)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String():
if accept := r.Header.Get("Accept"); !strings.Contains(accept, artifactDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", artifactDesc.MediaType)
w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String())
if _, err := w.Write(artifactJSON); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest:
result := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: []ocispec.Descriptor{},
}
w.Header().Set("Content-Type", ocispec.MediaTypeImageIndex)
if err := json.NewEncoder(w).Encode(result); err != nil {
t.Errorf("failed to write response: %v", err)
}
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
store := repo.Manifests()
ctx := context.Background()
// test deleting artifact with subject
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
err = store.Delete(ctx, artifactDesc)
if err != nil {
t.Fatalf("Manifests.Delete() error = %v", err)
}
if !artifactDeleted {
t.Errorf("Manifests.Delete() = %v, want %v", artifactDeleted, true)
}
// test deleting manifest with subject
if state := repo.loadReferrersState(); state != referrersStateSupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported)
}
err = store.Delete(ctx, manifestDesc)
if err != nil {
t.Fatalf("Manifests.Delete() error = %v", err)
}
if !manifestDeleted {
t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true)
}
// test deleting index with subject
if state := repo.loadReferrersState(); state != referrersStateSupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported)
}
err = store.Delete(ctx, indexDesc)
if err != nil {
t.Fatalf("Manifests.Delete() error = %v", err)
}
if !indexDeleted {
t.Errorf("Manifests.Delete() = %v, want %v", indexDeleted, true)
}
// test deleting content that does not exist
content := []byte("whatever")
contentDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
ctx = context.Background()
err = store.Delete(ctx, contentDesc)
if !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("Manifests.Delete() error = %v, wantErr %v", err, errdef.ErrNotFound)
}
}
func Test_ManifestStore_Delete_ReferrersAPIUnavailable(t *testing.T) {
// generate test content
subject := []byte(`{"layers":[]}`)
subjectDesc := content.NewDescriptorFromBytes(spec.MediaTypeArtifactManifest, subject)
referrersTag := strings.Replace(subjectDesc.Digest.String(), ":", "-", 1)
artifact := spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
Subject: &subjectDesc,
}
artifactJSON, err := json.Marshal(artifact)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
artifactDesc := content.NewDescriptorFromBytes(artifact.MediaType, artifactJSON)
manifest := ocispec.Manifest{
MediaType: ocispec.MediaTypeImageManifest,
Subject: &subjectDesc,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
manifestDesc := content.NewDescriptorFromBytes(manifest.MediaType, manifestJSON)
indexManifest := ocispec.Index{
MediaType: ocispec.MediaTypeImageIndex,
Subject: &subjectDesc,
}
indexManifestJSON, err := json.Marshal(indexManifest)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
indexManifestDesc := content.NewDescriptorFromBytes(indexManifest.MediaType, indexManifestJSON)
index_1 := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: []ocispec.Descriptor{
artifactDesc,
manifestDesc,
indexManifestDesc,
},
}
indexJSON_1, err := json.Marshal(index_1)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
indexDesc_1 := content.NewDescriptorFromBytes(index_1.MediaType, indexJSON_1)
index_2 := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: []ocispec.Descriptor{
manifestDesc,
indexManifestDesc,
},
}
indexJSON_2, err := json.Marshal(index_2)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
indexDesc_2 := content.NewDescriptorFromBytes(index_2.MediaType, indexJSON_2)
index_3 := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: []ocispec.Descriptor{
indexManifestDesc,
},
}
indexJSON_3, err := json.Marshal(index_3)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
indexDesc_3 := content.NewDescriptorFromBytes(index_3.MediaType, indexJSON_3)
// test deleting artifact with subject, referrers list should be updated
manifestDeleted := false
indexDeleted := false
var gotReferrerIndex []byte
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String():
manifestDeleted = true
// no "Docker-Content-Digest" header for manifest deletion
w.WriteHeader(http.StatusAccepted)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String():
if accept := r.Header.Get("Accept"); !strings.Contains(accept, artifactDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", artifactDesc.MediaType)
w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String())
if _, err := w.Write(artifactJSON); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest:
w.WriteHeader(http.StatusNotFound)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag:
w.Write(indexJSON_1)
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag:
if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotReferrerIndex = buf.Bytes()
w.Header().Set("Docker-Content-Digest", indexDesc_2.Digest.String())
w.WriteHeader(http.StatusCreated)
case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+indexDesc_1.Digest.String():
indexDeleted = true
// no "Docker-Content-Digest" header for manifest deletion
w.WriteHeader(http.StatusAccepted)
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
store := repo.Manifests()
ctx := context.Background()
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
err = store.Delete(ctx, artifactDesc)
if err != nil {
t.Fatalf("Manifests.Delete() error = %v", err)
}
if !manifestDeleted {
t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true)
}
if !bytes.Equal(gotReferrerIndex, indexJSON_2) {
t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_2))
}
if !indexDeleted {
t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true)
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
// test deleting manifest with subject, referrers list should be updated
manifestDeleted = false
indexDeleted = false
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String():
manifestDeleted = true
// no "Docker-Content-Digest" header for manifest deletion
w.WriteHeader(http.StatusAccepted)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String():
if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", manifestDesc.MediaType)
w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String())
if _, err := w.Write(manifestJSON); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest:
w.WriteHeader(http.StatusNotFound)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag:
w.Write(indexJSON_2)
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag:
if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotReferrerIndex = buf.Bytes()
w.Header().Set("Docker-Content-Digest", indexDesc_3.Digest.String())
w.WriteHeader(http.StatusCreated)
case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+indexDesc_2.Digest.String():
indexDeleted = true
// no "Docker-Content-Digest" header for manifest deletion
w.WriteHeader(http.StatusAccepted)
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err = url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err = NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
store = repo.Manifests()
ctx = context.Background()
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
err = store.Delete(ctx, manifestDesc)
if err != nil {
t.Fatalf("Manifests.Delete() error = %v", err)
}
if !manifestDeleted {
t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true)
}
if !indexDeleted {
t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true)
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
// test deleting index with a subject, referrers list should be updated
manifestDeleted = false
indexDeleted = false
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+indexManifestDesc.Digest.String():
manifestDeleted = true
// no "Docker-Content-Digest" header for manifest deletion
w.WriteHeader(http.StatusAccepted)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+indexManifestDesc.Digest.String():
if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexManifestDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", indexManifestDesc.MediaType)
w.Header().Set("Docker-Content-Digest", indexManifestDesc.Digest.String())
if _, err := w.Write(indexManifestJSON); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest:
w.WriteHeader(http.StatusNotFound)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag:
w.Write(indexJSON_3)
case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+indexDesc_3.Digest.String():
indexDeleted = true
// no "Docker-Content-Digest" header for manifest deletion
w.WriteHeader(http.StatusAccepted)
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err = url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err = NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
store = repo.Manifests()
ctx = context.Background()
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
err = store.Delete(ctx, indexManifestDesc)
if err != nil {
t.Fatalf("Manifests.Delete() error = %v", err)
}
if !manifestDeleted {
t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true)
}
if !indexDeleted {
t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true)
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
}
func Test_ManifestStore_Delete_ReferrersAPIUnavailable_SkipReferrersGC(t *testing.T) {
// generate test content
subject := []byte(`{"layers":[]}`)
subjectDesc := content.NewDescriptorFromBytes(spec.MediaTypeArtifactManifest, subject)
referrersTag := strings.Replace(subjectDesc.Digest.String(), ":", "-", 1)
manifest := ocispec.Manifest{
MediaType: ocispec.MediaTypeImageManifest,
Subject: &subjectDesc,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
manifestDesc := content.NewDescriptorFromBytes(manifest.MediaType, manifestJSON)
indexManifest := ocispec.Index{
MediaType: ocispec.MediaTypeImageIndex,
Subject: &subjectDesc,
}
indexManifestJSON, err := json.Marshal(indexManifest)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
indexManifestDesc := content.NewDescriptorFromBytes(indexManifest.MediaType, indexManifestJSON)
index_1 := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: []ocispec.Descriptor{
manifestDesc,
indexManifestDesc,
},
}
indexJSON_1, err := json.Marshal(index_1)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
index_2 := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: []ocispec.Descriptor{
indexManifestDesc,
},
}
indexJSON_2, err := json.Marshal(index_2)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
indexDesc_2 := content.NewDescriptorFromBytes(index_2.MediaType, indexJSON_2)
index_3 := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: []ocispec.Descriptor{},
}
indexJSON_3, err := json.Marshal(index_3)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
indexDesc_3 := content.NewDescriptorFromBytes(index_3.MediaType, indexJSON_3)
// test deleting image manifest with subject, referrers list should be updated,
// the old one should not be deleted
manifestDeleted := false
var gotReferrerIndex []byte
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String():
manifestDeleted = true
// no "Docker-Content-Digest" header for manifest deletion
w.WriteHeader(http.StatusAccepted)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String():
if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", manifestDesc.MediaType)
w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String())
if _, err := w.Write(manifestJSON); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest:
w.WriteHeader(http.StatusNotFound)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag:
w.Write(indexJSON_1)
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag:
if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotReferrerIndex = buf.Bytes()
w.Header().Set("Docker-Content-Digest", indexDesc_2.Digest.String())
w.WriteHeader(http.StatusCreated)
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
repo.SkipReferrersGC = true
store := repo.Manifests()
ctx := context.Background()
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
err = store.Delete(ctx, manifestDesc)
if err != nil {
t.Fatalf("Manifests.Delete() error = %v", err)
}
if !manifestDeleted {
t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true)
}
if !bytes.Equal(gotReferrerIndex, indexJSON_2) {
t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_2))
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
// test deleting index with a subject, referrers list should be updated,
// the old one should not be deleted, an empty one should be pushed
manifestDeleted = false
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+indexManifestDesc.Digest.String():
manifestDeleted = true
// no "Docker-Content-Digest" header for manifest deletion
w.WriteHeader(http.StatusAccepted)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+indexManifestDesc.Digest.String():
if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexManifestDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", indexManifestDesc.MediaType)
w.Header().Set("Docker-Content-Digest", indexManifestDesc.Digest.String())
if _, err := w.Write(indexManifestJSON); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest:
w.WriteHeader(http.StatusNotFound)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag:
w.Write(indexJSON_2)
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag:
if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotReferrerIndex = buf.Bytes()
w.Header().Set("Docker-Content-Digest", indexDesc_3.Digest.String())
w.WriteHeader(http.StatusCreated)
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err = url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err = NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
repo.SkipReferrersGC = true
store = repo.Manifests()
ctx = context.Background()
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
err = store.Delete(ctx, indexManifestDesc)
if err != nil {
t.Fatalf("Manifests.Delete() error = %v", err)
}
if !manifestDeleted {
t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true)
}
if !bytes.Equal(gotReferrerIndex, indexJSON_3) {
t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_3))
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
}
func Test_ManifestStore_Delete_ReferrersAPIUnavailable_InconsistentIndex(t *testing.T) {
// generate test content
subject := []byte(`{"layers":[]}`)
subjectDesc := content.NewDescriptorFromBytes(spec.MediaTypeArtifactManifest, subject)
referrersTag := strings.Replace(subjectDesc.Digest.String(), ":", "-", 1)
artifact := spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
Subject: &subjectDesc,
}
artifactJSON, err := json.Marshal(artifact)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
artifactDesc := content.NewDescriptorFromBytes(artifact.MediaType, artifactJSON)
// test inconsistent state: index not found
manifestDeleted := true
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String():
manifestDeleted = true
// no "Docker-Content-Digest" header for manifest deletion
w.WriteHeader(http.StatusAccepted)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String():
if accept := r.Header.Get("Accept"); !strings.Contains(accept, artifactDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", artifactDesc.MediaType)
w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String())
if _, err := w.Write(artifactJSON); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest:
w.WriteHeader(http.StatusNotFound)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag:
w.WriteHeader(http.StatusNotFound)
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
store := repo.Manifests()
ctx := context.Background()
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
err = store.Delete(ctx, artifactDesc)
if err != nil {
t.Fatalf("Manifests.Delete() error = %v", err)
}
if !manifestDeleted {
t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true)
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
// test inconsistent state: empty referrers list
manifestDeleted = true
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String():
manifestDeleted = true
// no "Docker-Content-Digest" header for manifest deletion
w.WriteHeader(http.StatusAccepted)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String():
if accept := r.Header.Get("Accept"); !strings.Contains(accept, artifactDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", artifactDesc.MediaType)
w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String())
if _, err := w.Write(artifactJSON); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest:
w.WriteHeader(http.StatusNotFound)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag:
result := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: []ocispec.Descriptor{},
}
if err := json.NewEncoder(w).Encode(result); err != nil {
t.Errorf("failed to write response: %v", err)
}
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err = url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err = NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
store = repo.Manifests()
ctx = context.Background()
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
err = store.Delete(ctx, artifactDesc)
if err != nil {
t.Fatalf("Manifests.Delete() error = %v", err)
}
if !manifestDeleted {
t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true)
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
// test inconsistent state: current referrer is not in referrers list
manifestDeleted = true
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String():
manifestDeleted = true
// no "Docker-Content-Digest" header for manifest deletion
w.WriteHeader(http.StatusAccepted)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String():
if accept := r.Header.Get("Accept"); !strings.Contains(accept, artifactDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", artifactDesc.MediaType)
w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String())
if _, err := w.Write(artifactJSON); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest:
w.WriteHeader(http.StatusNotFound)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag:
result := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: []ocispec.Descriptor{
content.NewDescriptorFromBytes(spec.MediaTypeArtifactManifest, []byte("whaterver")),
},
}
if err := json.NewEncoder(w).Encode(result); err != nil {
t.Errorf("failed to write response: %v", err)
}
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err = url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err = NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
store = repo.Manifests()
ctx = context.Background()
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
err = store.Delete(ctx, artifactDesc)
if err != nil {
t.Fatalf("Manifests.Delete() error = %v", err)
}
if !manifestDeleted {
t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true)
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
}
func Test_ManifestStore_Resolve(t *testing.T) {
manifest := []byte(`{"layers":[]}`)
manifestDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageIndex,
Digest: digest.FromBytes(manifest),
Size: int64(len(manifest)),
}
ref := "foobar"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodHead {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
switch r.URL.Path {
case "/v2/test/manifests/" + manifestDesc.Digest.String(),
"/v2/test/manifests/" + ref:
if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", manifestDesc.MediaType)
w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String())
w.Header().Set("Content-Length", strconv.Itoa(int(manifestDesc.Size)))
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repoName := uri.Host + "/test"
repo, err := NewRepository(repoName)
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
store := repo.Manifests()
ctx := context.Background()
got, err := store.Resolve(ctx, manifestDesc.Digest.String())
if err != nil {
t.Fatalf("Manifests.Resolve() error = %v", err)
}
if !reflect.DeepEqual(got, manifestDesc) {
t.Errorf("Manifests.Resolve() = %v, want %v", got, manifestDesc)
}
got, err = store.Resolve(ctx, ref)
if err != nil {
t.Fatalf("Manifests.Resolve() error = %v", err)
}
if !reflect.DeepEqual(got, manifestDesc) {
t.Errorf("Manifests.Resolve() = %v, want %v", got, manifestDesc)
}
tagDigestRef := "whatever" + "@" + manifestDesc.Digest.String()
got, err = repo.Resolve(ctx, tagDigestRef)
if err != nil {
t.Fatalf("Manifests.Resolve() error = %v", err)
}
if !reflect.DeepEqual(got, manifestDesc) {
t.Errorf("Manifests.Resolve() = %v, want %v", got, manifestDesc)
}
fqdnRef := repoName + ":" + tagDigestRef
got, err = repo.Resolve(ctx, fqdnRef)
if err != nil {
t.Fatalf("Manifests.Resolve() error = %v", err)
}
if !reflect.DeepEqual(got, manifestDesc) {
t.Errorf("Manifests.Resolve() = %v, want %v", got, manifestDesc)
}
content := []byte(`{"manifests":[]}`)
contentDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageIndex,
Digest: digest.FromBytes(content),
Size: int64(len(content)),
}
_, err = store.Resolve(ctx, contentDesc.Digest.String())
if !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("Manifests.Resolve() error = %v, wantErr %v", err, errdef.ErrNotFound)
}
}
func Test_ManifestStore_FetchReference(t *testing.T) {
manifest := []byte(`{"layers":[]}`)
manifestDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageIndex,
Digest: digest.FromBytes(manifest),
Size: int64(len(manifest)),
}
ref := "foobar"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
switch r.URL.Path {
case "/v2/test/manifests/" + manifestDesc.Digest.String(),
"/v2/test/manifests/" + ref:
if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", manifestDesc.MediaType)
w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String())
if _, err := w.Write(manifest); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repoName := uri.Host + "/test"
repo, err := NewRepository(repoName)
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
store := repo.Manifests()
ctx := context.Background()
// test with tag
gotDesc, rc, err := store.FetchReference(ctx, ref)
if err != nil {
t.Fatalf("Manifests.FetchReference() error = %v", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("Manifests.FetchReference() = %v, want %v", gotDesc, manifestDesc)
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(rc); err != nil {
t.Errorf("fail to read: %v", err)
}
if err := rc.Close(); err != nil {
t.Errorf("fail to close: %v", err)
}
if got := buf.Bytes(); !bytes.Equal(got, manifest) {
t.Errorf("Manifests.FetchReference() = %v, want %v", got, manifest)
}
// test with other tag
randomRef := "whatever"
_, _, err = store.FetchReference(ctx, randomRef)
if !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("Manifests.FetchReference() error = %v, wantErr %v", err, errdef.ErrNotFound)
}
// test with digest
gotDesc, rc, err = store.FetchReference(ctx, manifestDesc.Digest.String())
if err != nil {
t.Fatalf("Manifests.FetchReference() error = %v", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("Manifests.FetchReference() = %v, want %v", gotDesc, manifestDesc)
}
buf.Reset()
if _, err := buf.ReadFrom(rc); err != nil {
t.Errorf("fail to read: %v", err)
}
if err := rc.Close(); err != nil {
t.Errorf("fail to close: %v", err)
}
if got := buf.Bytes(); !bytes.Equal(got, manifest) {
t.Errorf("Manifests.FetchReference() = %v, want %v", got, manifest)
}
// test with other digest
randomContent := []byte("whatever")
randomContentDigest := digest.FromBytes(randomContent)
_, _, err = store.FetchReference(ctx, randomContentDigest.String())
if !errors.Is(err, errdef.ErrNotFound) {
t.Errorf("Manifests.FetchReference() error = %v, wantErr %v", err, errdef.ErrNotFound)
}
// test with tag@digest
tagDigestRef := randomRef + "@" + manifestDesc.Digest.String()
gotDesc, rc, err = store.FetchReference(ctx, tagDigestRef)
if err != nil {
t.Fatalf("Manifests.FetchReference() error = %v", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("Manifests.FetchReference() = %v, want %v", gotDesc, manifestDesc)
}
buf.Reset()
if _, err := buf.ReadFrom(rc); err != nil {
t.Errorf("fail to read: %v", err)
}
if err := rc.Close(); err != nil {
t.Errorf("fail to close: %v", err)
}
if got := buf.Bytes(); !bytes.Equal(got, manifest) {
t.Errorf("Manifests.FetchReference() = %v, want %v", got, manifest)
}
// test with FQDN
fqdnRef := repoName + ":" + tagDigestRef
gotDesc, rc, err = store.FetchReference(ctx, fqdnRef)
if err != nil {
t.Fatalf("Manifests.FetchReference() error = %v", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("Manifests.FetchReference() = %v, want %v", gotDesc, manifestDesc)
}
buf.Reset()
if _, err := buf.ReadFrom(rc); err != nil {
t.Errorf("fail to read: %v", err)
}
if err := rc.Close(); err != nil {
t.Errorf("fail to close: %v", err)
}
if got := buf.Bytes(); !bytes.Equal(got, manifest) {
t.Errorf("Manifests.FetchReference() = %v, want %v", got, manifest)
}
}
func Test_ManifestStore_Tag(t *testing.T) {
blob := []byte("hello world")
blobDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
index := []byte(`{"manifests":[]}`)
indexDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageIndex,
Digest: digest.FromBytes(index),
Size: int64(len(index)),
}
var gotIndex []byte
ref := "foobar"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+blobDesc.Digest.String():
w.WriteHeader(http.StatusNotFound)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String():
if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", indexDesc.MediaType)
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
if _, err := w.Write(index); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
case r.Method == http.MethodPut &&
r.URL.Path == "/v2/test/manifests/"+ref || r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String():
if contentType := r.Header.Get("Content-Type"); contentType != indexDesc.MediaType {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotIndex = buf.Bytes()
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
return
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusForbidden)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
store := repo.Manifests()
repo.PlainHTTP = true
ctx := context.Background()
err = store.Tag(ctx, blobDesc, ref)
if err == nil {
t.Errorf("Repository.Tag() error = %v, wantErr %v", err, true)
}
err = store.Tag(ctx, indexDesc, ref)
if err != nil {
t.Fatalf("Repository.Tag() error = %v", err)
}
if !bytes.Equal(gotIndex, index) {
t.Errorf("Repository.Tag() = %v, want %v", gotIndex, index)
}
gotIndex = nil
err = store.Tag(ctx, indexDesc, indexDesc.Digest.String())
if err != nil {
t.Fatalf("Repository.Tag() error = %v", err)
}
if !bytes.Equal(gotIndex, index) {
t.Errorf("Repository.Tag() = %v, want %v", gotIndex, index)
}
}
func Test_ManifestStore_PushReference(t *testing.T) {
index := []byte(`{"manifests":[]}`)
indexDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageIndex,
Digest: digest.FromBytes(index),
Size: int64(len(index)),
}
var gotIndex []byte
ref := "foobar"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+ref:
if contentType := r.Header.Get("Content-Type"); contentType != indexDesc.MediaType {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotIndex = buf.Bytes()
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
return
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusForbidden)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
store := repo.Manifests()
repo.PlainHTTP = true
ctx := context.Background()
err = store.PushReference(ctx, indexDesc, bytes.NewReader(index), ref)
if err != nil {
t.Fatalf("Repository.PushReference() error = %v", err)
}
if !bytes.Equal(gotIndex, index) {
t.Errorf("Repository.PushReference() = %v, want %v", gotIndex, index)
}
}
func Test_ManifestStore_PushReference_ReferrersAPIAvailable(t *testing.T) {
// generate test content
subject := []byte(`{"layers":[]}`)
subjectDesc := content.NewDescriptorFromBytes(spec.MediaTypeArtifactManifest, subject)
artifact := spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
Subject: &subjectDesc,
}
artifactJSON, err := json.Marshal(artifact)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
artifactDesc := content.NewDescriptorFromBytes(artifact.MediaType, artifactJSON)
artifactRef := "foo"
manifest := ocispec.Manifest{
MediaType: ocispec.MediaTypeImageManifest,
Subject: &subjectDesc,
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
manifestDesc := content.NewDescriptorFromBytes(manifest.MediaType, manifestJSON)
manifestRef := "bar"
index := ocispec.Index{
MediaType: ocispec.MediaTypeImageIndex,
Subject: &subjectDesc,
}
indexJSON, err := json.Marshal(index)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
indexDesc := content.NewDescriptorFromBytes(index.MediaType, indexJSON)
indexRef := "baz"
var gotManifest []byte
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+artifactRef:
if contentType := r.Header.Get("Content-Type"); contentType != artifactDesc.MediaType {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotManifest = buf.Bytes()
w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String())
w.Header().Set("OCI-Subject", subjectDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestRef:
if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotManifest = buf.Bytes()
w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String())
w.Header().Set("OCI-Subject", subjectDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+indexRef:
if contentType := r.Header.Get("Content-Type"); contentType != indexDesc.MediaType {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotManifest = buf.Bytes()
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
w.Header().Set("OCI-Subject", subjectDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest:
result := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: []ocispec.Descriptor{},
}
if err := json.NewEncoder(w).Encode(result); err != nil {
t.Errorf("failed to write response: %v", err)
}
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
ctx := context.Background()
// test pushing artifact with subject
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
err = repo.PushReference(ctx, artifactDesc, bytes.NewReader(artifactJSON), artifactRef)
if err != nil {
t.Fatalf("Manifests.Push() error = %v", err)
}
if !bytes.Equal(gotManifest, artifactJSON) {
t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(artifactJSON))
}
if state := repo.loadReferrersState(); state != referrersStateSupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported)
}
// test pushing image manifest with subject
repo, err = NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
err = repo.PushReference(ctx, manifestDesc, bytes.NewReader(manifestJSON), manifestRef)
if err != nil {
t.Fatalf("Manifests.Push() error = %v", err)
}
if !bytes.Equal(gotManifest, manifestJSON) {
t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(manifestJSON))
}
if state := repo.loadReferrersState(); state != referrersStateSupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported)
}
// test pushing image index with subject
err = repo.PushReference(ctx, indexDesc, bytes.NewReader(indexJSON), indexRef)
if err != nil {
t.Fatalf("Manifests.Push() error = %v", err)
}
if !bytes.Equal(gotManifest, indexJSON) {
t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(indexJSON))
}
if state := repo.loadReferrersState(); state != referrersStateSupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported)
}
}
func Test_ManifestStore_PushReference_ReferrersAPIUnavailable(t *testing.T) {
// generate test content
subject := []byte(`{"layers":[]}`)
subjectDesc := content.NewDescriptorFromBytes(spec.MediaTypeArtifactManifest, subject)
referrersTag := strings.Replace(subjectDesc.Digest.String(), ":", "-", 1)
artifact := spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
Subject: &subjectDesc,
ArtifactType: "application/vnd.test",
Annotations: map[string]string{"foo": "bar"},
}
artifactJSON, err := json.Marshal(artifact)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
artifactDesc := content.NewDescriptorFromBytes(artifact.MediaType, artifactJSON)
artifactDesc.ArtifactType = artifact.ArtifactType
artifactDesc.Annotations = artifact.Annotations
artifactRef := "foo"
// test pushing artifact with subject
index_1 := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: []ocispec.Descriptor{
artifactDesc,
},
}
indexJSON_1, err := json.Marshal(index_1)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
indexDesc_1 := content.NewDescriptorFromBytes(index_1.MediaType, indexJSON_1)
var gotManifest []byte
var gotReferrerIndex []byte
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+artifactRef:
if contentType := r.Header.Get("Content-Type"); contentType != artifactDesc.MediaType {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotManifest = buf.Bytes()
w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag:
w.WriteHeader(http.StatusNotFound)
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag:
if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotReferrerIndex = buf.Bytes()
w.Header().Set("Docker-Content-Digest", indexDesc_1.Digest.String())
w.WriteHeader(http.StatusCreated)
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
ctx := context.Background()
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
err = repo.PushReference(ctx, artifactDesc, bytes.NewReader(artifactJSON), artifactRef)
if err != nil {
t.Fatalf("Manifests.Push() error = %v", err)
}
if !bytes.Equal(gotManifest, artifactJSON) {
t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(artifactJSON))
}
if !bytes.Equal(gotReferrerIndex, indexJSON_1) {
t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_1))
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
// test pushing image manifest with subject, referrers list should be updated
manifest := ocispec.Manifest{
MediaType: ocispec.MediaTypeImageManifest,
Config: ocispec.Descriptor{
MediaType: "testconfig",
},
Subject: &subjectDesc,
Annotations: map[string]string{"foo": "bar"},
}
manifestJSON, err := json.Marshal(manifest)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
manifestDesc := content.NewDescriptorFromBytes(manifest.MediaType, manifestJSON)
manifestDesc.ArtifactType = manifest.Config.MediaType
manifestDesc.Annotations = manifest.Annotations
manifestRef := "bar"
index_2 := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: []ocispec.Descriptor{
artifactDesc,
manifestDesc,
},
}
indexJSON_2, err := json.Marshal(index_2)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
indexDesc_2 := content.NewDescriptorFromBytes(index_2.MediaType, indexJSON_2)
var manifestDeleted bool
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestRef:
if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotManifest = buf.Bytes()
w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag:
w.Write(indexJSON_1)
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag:
if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotReferrerIndex = buf.Bytes()
w.Header().Set("Docker-Content-Digest", indexDesc_2.Digest.String())
w.WriteHeader(http.StatusCreated)
case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+indexDesc_1.Digest.String():
manifestDeleted = true
// no "Docker-Content-Digest" header for manifest deletion
w.WriteHeader(http.StatusAccepted)
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err = url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
ctx = context.Background()
repo, err = NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
err = repo.PushReference(ctx, manifestDesc, bytes.NewReader(manifestJSON), manifestRef)
if err != nil {
t.Fatalf("Manifests.PushReference() error = %v", err)
}
if !bytes.Equal(gotManifest, manifestJSON) {
t.Errorf("Manifests.PushReference() = %v, want %v", string(gotManifest), string(manifestJSON))
}
if !bytes.Equal(gotReferrerIndex, indexJSON_2) {
t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_2))
}
if !manifestDeleted {
t.Errorf("manifestDeleted = %v, want %v", manifestDeleted, true)
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
// test pushing image manifest with subject again, referrers list should not be changed
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestRef:
if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotManifest = buf.Bytes()
w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag:
w.Write(indexJSON_2)
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err = url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
ctx = context.Background()
repo, err = NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
err = repo.PushReference(ctx, manifestDesc, bytes.NewReader(manifestJSON), manifestRef)
if err != nil {
t.Fatalf("Manifests.PushReference() error = %v", err)
}
if !bytes.Equal(gotManifest, manifestJSON) {
t.Errorf("Manifests.PushReference() = %v, want %v", string(gotManifest), string(manifestJSON))
}
// referrers list should not be changed
if !bytes.Equal(gotReferrerIndex, indexJSON_2) {
t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_2))
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
// push image index with subject, referrer list should be updated
indexManifest := ocispec.Index{
MediaType: ocispec.MediaTypeImageIndex,
Subject: &subjectDesc,
ArtifactType: "test/index",
Annotations: map[string]string{"foo": "bar"},
}
indexManifestJSON, err := json.Marshal(indexManifest)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
indexManifestDesc := content.NewDescriptorFromBytes(indexManifest.MediaType, indexManifestJSON)
indexManifestDesc.ArtifactType = indexManifest.ArtifactType
indexManifestDesc.Annotations = indexManifest.Annotations
indexManifestRef := "baz"
index_3 := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: []ocispec.Descriptor{
artifactDesc,
manifestDesc,
indexManifestDesc,
},
}
indexJSON_3, err := json.Marshal(index_3)
if err != nil {
t.Fatalf("failed to marshal manifest: %v", err)
}
indexDesc_3 := content.NewDescriptorFromBytes(index_3.MediaType, indexJSON_3)
manifestDeleted = false
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+indexManifestRef:
if contentType := r.Header.Get("Content-Type"); contentType != indexManifestDesc.MediaType {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotManifest = buf.Bytes()
w.Header().Set("Docker-Content-Digest", indexManifestDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag:
w.Write(indexJSON_2)
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag:
if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
gotReferrerIndex = buf.Bytes()
w.Header().Set("Docker-Content-Digest", indexDesc_3.Digest.String())
w.WriteHeader(http.StatusCreated)
case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+indexDesc_2.Digest.String():
manifestDeleted = true
// no "Docker-Content-Digest" header for manifest deletion
w.WriteHeader(http.StatusAccepted)
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err = url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
ctx = context.Background()
repo, err = NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
err = repo.PushReference(ctx, indexManifestDesc, bytes.NewReader(indexManifestJSON), indexManifestRef)
if err != nil {
t.Fatalf("Manifests.PushReference() error = %v", err)
}
if !bytes.Equal(gotManifest, indexManifestJSON) {
t.Errorf("Manifests.PushReference() = %v, want %v", string(gotManifest), string(indexManifestJSON))
}
if !bytes.Equal(gotReferrerIndex, indexJSON_3) {
t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_3))
}
if !manifestDeleted {
t.Errorf("manifestDeleted = %v, want %v", manifestDeleted, true)
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
}
func Test_ManifestStore_generateDescriptorWithVariousDockerContentDigestHeaders(t *testing.T) {
reference := registry.Reference{
Registry: "eastern.haan.com",
Reference: "",
Repository: "from25to220ce",
}
tests := getTestIOStructMapForGetDescriptorClass()
for testName, dcdIOStruct := range tests {
repo, err := NewRepository(fmt.Sprintf("%s/%s", reference.Repository, reference.Repository))
if err != nil {
t.Fatalf("failed to initialize repository")
}
s := manifestStore{repo: repo}
for i, method := range []string{http.MethodGet, http.MethodHead} {
reference.Reference = dcdIOStruct.clientSuppliedReference
resp := http.Response{
Header: http.Header{
"Content-Type": []string{"application/vnd.docker.distribution.manifest.v2+json"},
headerDockerContentDigest: []string{dcdIOStruct.serverCalculatedDigest.String()},
},
}
if method == http.MethodGet {
resp.Body = io.NopCloser(bytes.NewBufferString(theAmazingBanClan))
}
resp.Request = &http.Request{
Method: method,
}
errExpected := []bool{dcdIOStruct.errExpectedOnGET, dcdIOStruct.errExpectedOnHEAD}[i]
_, err = s.generateDescriptor(&resp, reference, method)
if !errExpected && err != nil {
t.Errorf(
"[Manifest.%v] %v; expected no error for request, but got err: %v",
method, testName, err,
)
} else if errExpected && err == nil {
t.Errorf(
"[Manifest.%v] %v; expected an error for request, but got none",
method, testName,
)
}
}
}
}
type testTransport struct {
proxyHost string
underlyingTransport http.RoundTripper
mockHost string
}
func (t *testTransport) RoundTrip(originalReq *http.Request) (*http.Response, error) {
req := originalReq.Clone(originalReq.Context())
mockHostName, mockPort, err := net.SplitHostPort(t.mockHost)
// when t.mockHost is as form host:port
if err == nil && (req.URL.Hostname() != mockHostName || req.URL.Port() != mockPort) {
return nil, errors.New("bad request")
}
// when t.mockHost does not have specified port, in this case,
// err is not nil
if err != nil && req.URL.Hostname() != t.mockHost {
return nil, errors.New("bad request")
}
req.Host = t.proxyHost
req.URL.Host = t.proxyHost
resp, err := t.underlyingTransport.RoundTrip(req)
if err != nil {
return nil, err
}
resp.Request.Host = t.mockHost
resp.Request.URL.Host = t.mockHost
return resp, nil
}
// Helper function to create a registry.BlobStore for
// Test_BlobStore_Push_Port443
func blobStore_Push_Port443_create_store(uri *url.URL, testRegistry string) (registry.BlobStore, error) {
repo, err := NewRepository(testRegistry + "/test")
repo.Client = &auth.Client{
Client: &http.Client{
Transport: &testTransport{
proxyHost: uri.Host,
underlyingTransport: http.DefaultTransport,
mockHost: testRegistry,
},
},
Cache: auth.NewCache(),
}
repo.PlainHTTP = true
store := repo.Blobs()
return store, err
}
func Test_BlobStore_Push_Port443(t *testing.T) {
blob := []byte("hello world")
blobDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
uuid := "4fd53bc9-565d-4527-ab80-3e051ac4880c"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPost && r.URL.Path == "/v2/test/blobs/uploads/":
w.Header().Set("Location", "http://registry.wabbit-networks.io/v2/test/blobs/uploads/"+uuid)
w.WriteHeader(http.StatusAccepted)
return
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/blobs/uploads/"+uuid:
if contentType := r.Header.Get("Content-Type"); contentType != "application/octet-stream" {
w.WriteHeader(http.StatusBadRequest)
break
}
if contentDigest := r.URL.Query().Get("digest"); contentDigest != blobDesc.Digest.String() {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
return
default:
w.WriteHeader(http.StatusForbidden)
}
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
// Test case with Host: "registry.wabbit-networks.io:443",
// Location: "registry.wabbit-networks.io"
testRegistry := "registry.wabbit-networks.io:443"
store, err := blobStore_Push_Port443_create_store(uri, testRegistry)
if err != nil {
t.Fatalf("blobStore_Push_Port443_create_store() error = %v", err)
}
ctx := context.Background()
err = store.Push(ctx, blobDesc, bytes.NewReader(blob))
if err != nil {
t.Fatalf("Blobs.Push() error = %v", err)
}
// Test case with Host: "registry.wabbit-networks.io",
// Location: "registry.wabbit-networks.io"
testRegistry = "registry.wabbit-networks.io"
store, err = blobStore_Push_Port443_create_store(uri, testRegistry)
if err != nil {
t.Fatalf("blobStore_Push_Port443_create_store() error = %v", err)
}
err = store.Push(ctx, blobDesc, bytes.NewReader(blob))
if err != nil {
t.Fatalf("Blobs.Push() error = %v", err)
}
}
// Helper function to create a registry.BlobStore for
// Test_BlobStore_Push_Port443_HTTPS
func blobStore_Push_Port443_HTTPS_create_store(uri *url.URL, testRegistry string) (registry.BlobStore, error) {
repo, err := NewRepository(testRegistry + "/test")
tlsConfig := &tls.Config{
InsecureSkipVerify: true,
}
transport := &http.Transport{
TLSClientConfig: tlsConfig,
}
repo.Client = &auth.Client{
Client: &http.Client{
Transport: &testTransport{
proxyHost: uri.Host,
underlyingTransport: transport,
mockHost: testRegistry,
},
},
Cache: auth.NewCache(),
}
repo.PlainHTTP = false
store := repo.Blobs()
return store, err
}
func Test_BlobStore_Push_Port443_HTTPS(t *testing.T) {
blob := []byte("hello world")
blobDesc := ocispec.Descriptor{
MediaType: "test",
Digest: digest.FromBytes(blob),
Size: int64(len(blob)),
}
uuid := "4fd53bc9-565d-4527-ab80-3e051ac4880c"
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPost && r.URL.Path == "/v2/test/blobs/uploads/":
w.Header().Set("Location", "https://registry.wabbit-networks.io/v2/test/blobs/uploads/"+uuid)
w.WriteHeader(http.StatusAccepted)
return
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/blobs/uploads/"+uuid:
if contentType := r.Header.Get("Content-Type"); contentType != "application/octet-stream" {
w.WriteHeader(http.StatusBadRequest)
break
}
if contentDigest := r.URL.Query().Get("digest"); contentDigest != blobDesc.Digest.String() {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
return
default:
w.WriteHeader(http.StatusForbidden)
}
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test https server: %v", err)
}
ctx := context.Background()
// Test case with Host: "registry.wabbit-networks.io:443",
// Location: "registry.wabbit-networks.io"
testRegistry := "registry.wabbit-networks.io:443"
store, err := blobStore_Push_Port443_HTTPS_create_store(uri, testRegistry)
if err != nil {
t.Fatalf("blobStore_Push_Port443_HTTPS_create_store() error = %v", err)
}
err = store.Push(ctx, blobDesc, bytes.NewReader(blob))
if err != nil {
t.Fatalf("Blobs.Push() error = %v", err)
}
// Test case with Host: "registry.wabbit-networks.io",
// Location: "registry.wabbit-networks.io"
testRegistry = "registry.wabbit-networks.io"
store, err = blobStore_Push_Port443_HTTPS_create_store(uri, testRegistry)
if err != nil {
t.Fatalf("blobStore_Push_Port443_HTTPS_create_store() error = %v", err)
}
err = store.Push(ctx, blobDesc, bytes.NewReader(blob))
if err != nil {
t.Fatalf("Blobs.Push() error = %v", err)
}
ts = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodPost && r.URL.Path == "/v2/test/blobs/uploads/":
w.Header().Set("Location", "https://registry.wabbit-networks.io:443/v2/test/blobs/uploads/"+uuid)
w.WriteHeader(http.StatusAccepted)
return
case r.Method == http.MethodPut && r.URL.Path == "/v2/test/blobs/uploads/"+uuid:
if contentType := r.Header.Get("Content-Type"); contentType != "application/octet-stream" {
w.WriteHeader(http.StatusBadRequest)
break
}
if contentDigest := r.URL.Query().Get("digest"); contentDigest != blobDesc.Digest.String() {
w.WriteHeader(http.StatusBadRequest)
break
}
buf := bytes.NewBuffer(nil)
if _, err := buf.ReadFrom(r.Body); err != nil {
t.Errorf("fail to read: %v", err)
}
w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String())
w.WriteHeader(http.StatusCreated)
return
default:
w.WriteHeader(http.StatusForbidden)
}
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
}))
defer ts.Close()
uri, err = url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test https server: %v", err)
}
// Test case with Host: "registry.wabbit-networks.io:443",
// Location: "registry.wabbit-networks.io:443"
testRegistry = "registry.wabbit-networks.io:443"
store, err = blobStore_Push_Port443_HTTPS_create_store(uri, testRegistry)
if err != nil {
t.Fatalf("blobStore_Push_Port443_HTTPS_create_store() error = %v", err)
}
err = store.Push(ctx, blobDesc, bytes.NewReader(blob))
if err != nil {
t.Fatalf("Blobs.Push() error = %v", err)
}
// Test case with Host: "registry.wabbit-networks.io",
// Location: "registry.wabbit-networks.io:443"
testRegistry = "registry.wabbit-networks.io"
store, err = blobStore_Push_Port443_HTTPS_create_store(uri, testRegistry)
if err != nil {
t.Fatalf("blobStore_Push_Port443_HTTPS_create_store() error = %v", err)
}
err = store.Push(ctx, blobDesc, bytes.NewReader(blob))
if err != nil {
t.Fatalf("Blobs.Push() error = %v", err)
}
}
// Testing `last` parameter for Tags list
func TestRepository_Tags_WithLastParam(t *testing.T) {
tagSet := strings.Split("abcdefghijklmnopqrstuvwxyz", "")
var offset int
var ts *httptest.Server
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || r.URL.Path != "/v2/test/tags/list" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
q := r.URL.Query()
n, err := strconv.Atoi(q.Get("n"))
if err != nil || n != 4 {
t.Errorf("bad page size: %s", q.Get("n"))
w.WriteHeader(http.StatusBadRequest)
return
}
last := q.Get("last")
if last != "" {
offset = indexOf(last, tagSet) + 1
}
var tags []string
switch q.Get("test") {
case "foo":
tags = tagSet[offset : offset+n]
offset += n
w.Header().Set("Link", fmt.Sprintf(`<%s/v2/test/tags/list?n=4&last=v&test=bar>; rel="next"`, ts.URL))
case "bar":
tags = tagSet[offset : offset+n]
default:
tags = tagSet[offset : offset+n]
offset += n
w.Header().Set("Link", fmt.Sprintf(`<%s/v2/test/tags/list?n=4&last=r&test=foo>; rel="next"`, ts.URL))
}
result := struct {
Tags []string `json:"tags"`
}{
Tags: tags,
}
if err := json.NewEncoder(w).Encode(result); err != nil {
t.Errorf("failed to write response: %v", err)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
repo.TagListPageSize = 4
last := "n"
startInd := indexOf(last, tagSet) + 1
ctx := context.Background()
if err := repo.Tags(ctx, last, func(got []string) error {
want := tagSet[startInd : startInd+repo.TagListPageSize]
startInd += repo.TagListPageSize
if !reflect.DeepEqual(got, want) {
t.Errorf("Registry.Repositories() = %v, want %v", got, want)
}
return nil
}); err != nil {
t.Errorf("Repository.Tags() error = %v", err)
}
}
func TestRepository_ParseReference(t *testing.T) {
type args struct {
reference string
}
tests := []struct {
name string
repoRef registry.Reference
args args
want registry.Reference
wantErr error
}{
{
name: "parse tag",
repoRef: registry.Reference{
Registry: "registry.example.com",
Repository: "hello-world",
},
args: args{
reference: "foobar",
},
want: registry.Reference{
Registry: "registry.example.com",
Repository: "hello-world",
Reference: "foobar",
},
wantErr: nil,
},
{
name: "parse digest",
repoRef: registry.Reference{
Registry: "registry.example.com",
Repository: "hello-world",
},
args: args{
reference: "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
},
want: registry.Reference{
Registry: "registry.example.com",
Repository: "hello-world",
Reference: "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
},
wantErr: nil,
},
{
name: "parse tag@digest",
repoRef: registry.Reference{
Registry: "registry.example.com",
Repository: "hello-world",
},
args: args{
reference: "foobar@sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
},
want: registry.Reference{
Registry: "registry.example.com",
Repository: "hello-world",
Reference: "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
},
wantErr: nil,
},
{
name: "parse FQDN tag",
repoRef: registry.Reference{
Registry: "registry.example.com",
Repository: "hello-world",
},
args: args{
reference: "registry.example.com/hello-world:foobar",
},
want: registry.Reference{
Registry: "registry.example.com",
Repository: "hello-world",
Reference: "foobar",
},
wantErr: nil,
},
{
name: "parse FQDN digest",
repoRef: registry.Reference{
Registry: "registry.example.com",
Repository: "hello-world",
},
args: args{
reference: "registry.example.com/hello-world@sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
},
want: registry.Reference{
Registry: "registry.example.com",
Repository: "hello-world",
Reference: "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
},
wantErr: nil,
},
{
name: "parse FQDN tag@digest",
repoRef: registry.Reference{
Registry: "registry.example.com",
Repository: "hello-world",
},
args: args{
reference: "registry.example.com/hello-world:foobar@sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
},
want: registry.Reference{
Registry: "registry.example.com",
Repository: "hello-world",
Reference: "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
},
wantErr: nil,
},
{
name: "empty reference",
repoRef: registry.Reference{
Registry: "registry.example.com",
Repository: "hello-world",
},
args: args{
reference: "",
},
want: registry.Reference{},
wantErr: errdef.ErrInvalidReference,
},
{
name: "missing repository",
repoRef: registry.Reference{
Registry: "registry.example.com",
Repository: "hello-world",
},
args: args{
reference: "myregistry.example.com:hello-world",
},
want: registry.Reference{},
wantErr: errdef.ErrInvalidReference,
},
{
name: "missing reference",
repoRef: registry.Reference{
Registry: "registry.example.com",
Repository: "hello-world",
},
args: args{
reference: "registry.example.com/hello-world",
},
want: registry.Reference{},
wantErr: errdef.ErrInvalidReference,
},
{
name: "registry mismatch",
repoRef: registry.Reference{
Registry: "registry.example.com",
Repository: "hello-world",
},
args: args{
reference: "myregistry.example.com/hello-world:foobar@sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
},
want: registry.Reference{},
wantErr: errdef.ErrInvalidReference,
},
{
name: "repository mismatch",
repoRef: registry.Reference{
Registry: "registry.example.com",
Repository: "hello-world",
},
args: args{
reference: "registry.example.com/goodbye-world:foobar@sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
},
want: registry.Reference{},
wantErr: errdef.ErrInvalidReference,
},
{
name: "digest posing as a tag",
repoRef: registry.Reference{
Registry: "registry.example.com",
Repository: "hello-world",
},
args: args{
reference: "registry.example.com:5000/hello-world:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
},
want: registry.Reference{},
wantErr: errdef.ErrInvalidReference,
},
{
name: "missing reference after the at sign",
repoRef: registry.Reference{
Registry: "registry.example.com",
Repository: "hello-world",
},
args: args{
reference: "registry.example.com/hello-world@",
},
want: registry.Reference{},
wantErr: errdef.ErrInvalidReference,
},
{
name: "missing reference after the colon",
repoRef: registry.Reference{
Registry: "localhost",
},
args: args{
reference: "localhost:5000/hello:",
},
want: registry.Reference{},
wantErr: errdef.ErrInvalidReference,
},
{
name: "zero-size tag, zero-size digest",
repoRef: registry.Reference{},
args: args{
reference: "localhost:5000/hello:@",
},
want: registry.Reference{},
wantErr: errdef.ErrInvalidReference,
},
{
name: "zero-size tag with valid digest",
repoRef: registry.Reference{},
args: args{
reference: "localhost:5000/hello:@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
},
want: registry.Reference{},
wantErr: errdef.ErrInvalidReference,
},
{
name: "valid tag with zero-size digest",
repoRef: registry.Reference{},
args: args{
reference: "localhost:5000/hello:foobar@",
},
want: registry.Reference{},
wantErr: errdef.ErrInvalidReference,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Repository{
Reference: tt.repoRef,
}
got, err := r.ParseReference(tt.args.reference)
if !errors.Is(err, tt.wantErr) {
t.Errorf("Repository.ParseReference() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Repository.ParseReference() = %v, want %v", got, tt.want)
}
})
}
}
func TestRepository_SetReferrersCapability(t *testing.T) {
repo, err := NewRepository("registry.example.com/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
// initial state
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
// valid first time set
if err := repo.SetReferrersCapability(true); err != nil {
t.Errorf("Repository.SetReferrersCapability() error = %v", err)
}
if state := repo.loadReferrersState(); state != referrersStateSupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported)
}
// invalid second time set, state should be no changed
if err := repo.SetReferrersCapability(false); !errors.Is(err, ErrReferrersCapabilityAlreadySet) {
t.Errorf("Repository.SetReferrersCapability() error = %v, wantErr %v", err, ErrReferrersCapabilityAlreadySet)
}
if state := repo.loadReferrersState(); state != referrersStateSupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported)
}
}
func Test_generateIndex(t *testing.T) {
referrer_1 := spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
ArtifactType: "foo",
}
referrerJSON_1, err := json.Marshal(referrer_1)
if err != nil {
t.Fatal("failed to marshal manifest:", err)
}
referrer_2 := spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
ArtifactType: "bar",
}
referrerJSON_2, err := json.Marshal(referrer_2)
if err != nil {
t.Fatal("failed to marshal manifest:", err)
}
referrers := []ocispec.Descriptor{
content.NewDescriptorFromBytes(referrer_1.MediaType, referrerJSON_1),
content.NewDescriptorFromBytes(referrer_2.MediaType, referrerJSON_2),
}
wantIndex := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: referrers,
}
wantIndexJSON, err := json.Marshal(wantIndex)
if err != nil {
t.Fatal("failed to marshal index:", err)
}
wantIndexDesc := content.NewDescriptorFromBytes(wantIndex.MediaType, wantIndexJSON)
wantEmptyIndex := ocispec.Index{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
MediaType: ocispec.MediaTypeImageIndex,
Manifests: []ocispec.Descriptor{},
}
wantEmptyIndexJSON, err := json.Marshal(wantEmptyIndex)
if err != nil {
t.Fatal("failed to marshal index:", err)
}
wantEmptyIndexDesc := content.NewDescriptorFromBytes(wantEmptyIndex.MediaType, wantEmptyIndexJSON)
tests := []struct {
name string
manifests []ocispec.Descriptor
wantDesc ocispec.Descriptor
wantBytes []byte
wantErr bool
}{
{
name: "non-empty referrers list",
manifests: referrers,
wantDesc: wantIndexDesc,
wantBytes: wantIndexJSON,
wantErr: false,
},
{
name: "nil referrers list",
manifests: nil,
wantDesc: wantEmptyIndexDesc,
wantBytes: wantEmptyIndexJSON,
wantErr: false,
},
{
name: "empty referrers list",
manifests: nil,
wantDesc: wantEmptyIndexDesc,
wantBytes: wantEmptyIndexJSON,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1, err := generateIndex(tt.manifests)
if (err != nil) != tt.wantErr {
t.Errorf("generateReferrersIndex() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.wantDesc) {
t.Errorf("generateReferrersIndex() got = %v, want %v", got, tt.wantDesc)
}
if !reflect.DeepEqual(got1, tt.wantBytes) {
t.Errorf("generateReferrersIndex() got1 = %v, want %v", got1, tt.wantBytes)
}
})
}
}
func TestRepository_pingReferrers(t *testing.T) {
t.Run("referrers available", func(t *testing.T) {
count := 0
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest:
count++
w.Header().Set("Content-Type", ocispec.MediaTypeImageIndex)
w.WriteHeader(http.StatusOK)
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
ctx := context.Background()
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
// 1st call
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
got, err := repo.pingReferrers(ctx)
if err != nil {
t.Errorf("Repository.pingReferrers() error = %v, wantErr %v", err, nil)
}
if got != true {
t.Errorf("Repository.pingReferrers() = %v, want %v", got, true)
}
if state := repo.loadReferrersState(); state != referrersStateSupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported)
}
if count != 1 {
t.Errorf("count(Repository.pingReferrers()) = %v, want %v", count, 1)
}
// 2nd call
if state := repo.loadReferrersState(); state != referrersStateSupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported)
}
got, err = repo.pingReferrers(ctx)
if err != nil {
t.Errorf("Repository.pingReferrers() error = %v, wantErr %v", err, nil)
}
if got != true {
t.Errorf("Repository.pingReferrers() = %v, want %v", got, true)
}
if state := repo.loadReferrersState(); state != referrersStateSupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported)
}
if count != 1 {
t.Errorf("count(Repository.pingReferrers()) = %v, want %v", count, 1)
}
})
t.Run("referrers unavailable", func(t *testing.T) {
count := 0
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest:
count++
w.WriteHeader(http.StatusNotFound)
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
ctx := context.Background()
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
// 1st call
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
got, err := repo.pingReferrers(ctx)
if err != nil {
t.Errorf("Repository.pingReferrers() error = %v, wantErr %v", err, nil)
}
if got != false {
t.Errorf("Repository.pingReferrers() = %v, want %v", got, false)
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
if count != 1 {
t.Errorf("count(Repository.pingReferrers()) = %v, want %v", count, 1)
}
// 2nd call
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
got, err = repo.pingReferrers(ctx)
if err != nil {
t.Errorf("Repository.pingReferrers() error = %v, wantErr %v", err, nil)
}
if got != false {
t.Errorf("Repository.pingReferrers() = %v, want %v", got, false)
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
if count != 1 {
t.Errorf("count(Repository.pingReferrers()) = %v, want %v", count, 1)
}
})
t.Run("referrers unavailable incorrect content type", func(t *testing.T) {
count := 0
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest:
count++
w.Header().Set("Content-Type", "text/html") // can be anything except an OCI image index
w.WriteHeader(http.StatusOK)
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
ctx := context.Background()
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
got, err := repo.pingReferrers(ctx)
if err != nil {
t.Errorf("Repository.pingReferrers() error = %v, wantErr %v", err, nil)
}
if got != false {
t.Errorf("Repository.pingReferrers() = %v, want %v", got, false)
}
if count != 1 {
t.Errorf("count(Repository.pingReferrers()) = %v, want %v", count, 1)
}
})
}
func TestRepository_pingReferrers_RepositoryNotFound(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`{ "errors": [ { "code": "NAME_UNKNOWN", "message": "repository name not known to registry" } ] }`))
return
}
t.Errorf("unexpected access: %s %q", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
ctx := context.Background()
// test referrers state unknown
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
if _, err = repo.pingReferrers(ctx); err == nil {
t.Fatalf("Repository.pingReferrers() error = %v, wantErr %v", err, true)
}
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
// test referrers state supported
repo, err = NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
repo.SetReferrersCapability(true)
if state := repo.loadReferrersState(); state != referrersStateSupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported)
}
got, err := repo.pingReferrers(ctx)
if err != nil {
t.Errorf("Repository.pingReferrers() error = %v, wantErr %v", err, nil)
}
if got != true {
t.Errorf("Repository.pingReferrers() = %v, want %v", got, true)
}
if state := repo.loadReferrersState(); state != referrersStateSupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported)
}
// test referrers state unsupported
repo, err = NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
repo.SetReferrersCapability(false)
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
got, err = repo.pingReferrers(ctx)
if err != nil {
t.Errorf("Repository.pingReferrers() error = %v, wantErr %v", err, nil)
}
if got != false {
t.Errorf("Repository.pingReferrers() = %v, want %v", got, false)
}
if state := repo.loadReferrersState(); state != referrersStateUnsupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported)
}
}
func TestRepository_pingReferrers_Concurrent(t *testing.T) {
// referrers available
var count int32
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest:
atomic.AddInt32(&count, 1)
w.Header().Set("Content-Type", ocispec.MediaTypeImageIndex)
w.WriteHeader(http.StatusOK)
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
ctx := context.Background()
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
concurrency := 64
eg, egCtx := errgroup.WithContext(ctx)
if state := repo.loadReferrersState(); state != referrersStateUnknown {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown)
}
for i := 0; i < concurrency; i++ {
eg.Go(func() func() error {
return func() error {
got, err := repo.pingReferrers(egCtx)
if err != nil {
t.Fatalf("Repository.pingReferrers() error = %v, wantErr %v", err, nil)
}
if got != true {
t.Errorf("Repository.pingReferrers() = %v, want %v", got, true)
}
return nil
}
}())
}
if err := eg.Wait(); err != nil {
t.Fatal(err)
}
if got := atomic.LoadInt32(&count); got != 1 {
t.Errorf("count(Repository.pingReferrers()) = %v, want %v", count, 1)
}
if state := repo.loadReferrersState(); state != referrersStateSupported {
t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported)
}
}
func TestRepository_do(t *testing.T) {
data := []byte(`hello world!`)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || r.URL.Path != "/test" {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
w.Header().Add("Warning", `299 - "Test 1: Good warning."`)
w.Header().Add("Warning", `199 - "Test 2: Warning with a non-299 code."`)
w.Header().Add("Warning", `299 - "Test 3: Good warning."`)
w.Header().Add("Warning", `299 myregistry.example.com "Test 4: Warning with a non-unknown agent"`)
w.Header().Add("Warning", `299 - "Test 5: Warning with a date." "Sat, 25 Aug 2012 23:34:45 GMT"`)
w.Header().Add("wArnIng", `299 - "Test 6: Good warning."`)
w.Write(data)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
testURL := ts.URL + "/test"
// test do() without HandleWarning
repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatal("NewRepository() error =", err)
}
req, err := http.NewRequest(http.MethodGet, testURL, nil)
if err != nil {
t.Fatal("failed to create test request:", err)
}
resp, err := repo.do(req)
if err != nil {
t.Fatal("Repository.do() error =", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Repository.do() status code = %v, want %v", resp.StatusCode, http.StatusOK)
}
if got := len(resp.Header["Warning"]); got != 6 {
t.Errorf("Repository.do() warning header len = %v, want %v", got, 6)
}
got, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal("io.ReadAll() error =", err)
}
resp.Body.Close()
if !bytes.Equal(got, data) {
t.Errorf("Repository.do() = %v, want %v", got, data)
}
// test do() with HandleWarning
repo, err = NewRepository(uri.Host + "/test")
if err != nil {
t.Fatal("NewRepository() error =", err)
}
var gotWarnings []Warning
repo.HandleWarning = func(warning Warning) {
gotWarnings = append(gotWarnings, warning)
}
req, err = http.NewRequest(http.MethodGet, testURL, nil)
if err != nil {
t.Fatal("failed to create test request:", err)
}
resp, err = repo.do(req)
if err != nil {
t.Fatal("Repository.do() error =", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Repository.do() status code = %v, want %v", resp.StatusCode, http.StatusOK)
}
if got := len(resp.Header["Warning"]); got != 6 {
t.Errorf("Repository.do() warning header len = %v, want %v", got, 6)
}
got, err = io.ReadAll(resp.Body)
if err != nil {
t.Errorf("Repository.do() = %v, want %v", got, data)
}
resp.Body.Close()
if !bytes.Equal(got, data) {
t.Errorf("Repository.do() = %v, want %v", got, data)
}
wantWarnings := []Warning{
{
WarningValue: WarningValue{
Code: 299,
Agent: "-",
Text: "Test 1: Good warning.",
},
},
{
WarningValue: WarningValue{
Code: 299,
Agent: "-",
Text: "Test 3: Good warning.",
},
},
{
WarningValue: WarningValue{
Code: 299,
Agent: "-",
Text: "Test 6: Good warning.",
},
},
}
if !reflect.DeepEqual(gotWarnings, wantWarnings) {
t.Errorf("Repository.do() = %v, want %v", gotWarnings, wantWarnings)
}
}
func TestRepository_clone(t *testing.T) {
repo, err := NewRepository("localhost:1234/repo/image")
if err != nil {
t.Fatalf("invalid repository: %v", err)
}
crepo := repo.clone()
if repo.Reference != crepo.Reference {
t.Fatal("references should be the same")
}
if !reflect.DeepEqual(&repo.referrersPingLock, &crepo.referrersPingLock) {
t.Fatal("referrersPingLock should be different")
}
if !reflect.DeepEqual(&repo.referrersMergePool, &crepo.referrersMergePool) {
t.Fatal("referrersMergePool should be different")
}
}
oras-go-2.5.0/registry/remote/retry/ 0000775 0000000 0000000 00000000000 14576745303 0017413 5 ustar 00root root 0000000 0000000 oras-go-2.5.0/registry/remote/retry/client.go 0000664 0000000 0000000 00000005366 14576745303 0021232 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 retry
import (
"net/http"
"time"
)
// DefaultClient is a client with the default retry policy.
var DefaultClient = NewClient()
// NewClient creates an HTTP client with the default retry policy.
func NewClient() *http.Client {
return &http.Client{
Transport: NewTransport(nil),
}
}
// Transport is an HTTP transport with retry policy.
type Transport struct {
// Base is the underlying HTTP transport to use.
// If nil, http.DefaultTransport is used for round trips.
Base http.RoundTripper
// Policy returns a retry Policy to use for the request.
// If nil, DefaultPolicy is used to determine if the request should be retried.
Policy func() Policy
}
// NewTransport creates an HTTP Transport with the default retry policy.
func NewTransport(base http.RoundTripper) *Transport {
return &Transport{
Base: base,
}
}
// RoundTrip executes a single HTTP transaction, returning a Response for the
// provided Request.
// It relies on the configured Policy to determine if the request should be
// retried and to backoff.
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
ctx := req.Context()
policy := t.policy()
attempt := 0
for {
resp, respErr := t.roundTrip(req)
duration, err := policy.Retry(attempt, resp, respErr)
if err != nil {
if respErr == nil {
resp.Body.Close()
}
return nil, err
}
if duration < 0 {
return resp, respErr
}
// rewind the body if possible
if req.Body != nil {
if req.GetBody == nil {
// body can't be rewound, so we can't retry
return resp, respErr
}
body, err := req.GetBody()
if err != nil {
// failed to rewind the body, so we can't retry
return resp, respErr
}
req.Body = body
}
// close the response body if needed
if respErr == nil {
resp.Body.Close()
}
timer := time.NewTimer(duration)
select {
case <-ctx.Done():
timer.Stop()
return nil, ctx.Err()
case <-timer.C:
}
attempt++
}
}
func (t *Transport) roundTrip(req *http.Request) (*http.Response, error) {
if t.Base == nil {
return http.DefaultTransport.RoundTrip(req)
}
return t.Base.RoundTrip(req)
}
func (t *Transport) policy() Policy {
if t.Policy == nil {
return DefaultPolicy
}
return t.Policy()
}
oras-go-2.5.0/registry/remote/retry/client_test.go 0000664 0000000 0000000 00000005476 14576745303 0022273 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 retry
import (
"bytes"
"net/http"
"net/http/httptest"
"testing"
)
func Test_Client(t *testing.T) {
testCases := []struct {
name string
attempts int
retryAfter bool
StatusCode int
expectedErr bool
}{
{
name: "successful request with 0 retry",
attempts: 1, retryAfter: false, StatusCode: http.StatusOK, expectedErr: false,
},
{
name: "successful request with 1 retry caused by rate limit",
// 1 request + 1 retry = 2 attempts
attempts: 2, retryAfter: true, StatusCode: http.StatusTooManyRequests, expectedErr: false,
},
{
name: "successful request with 1 retry caused by 408",
// 1 request + 1 retry = 2 attempts
attempts: 2, retryAfter: false, StatusCode: http.StatusRequestTimeout, expectedErr: false,
},
{
name: "successful request with 2 retries caused by 429",
// 1 request + 2 retries = 3 attempts
attempts: 3, retryAfter: false, StatusCode: http.StatusTooManyRequests, expectedErr: false,
},
{
name: "unsuccessful request with 6 retries caused by too many retries",
// 1 request + 6 retries = 7 attempts
attempts: 7, retryAfter: false, StatusCode: http.StatusServiceUnavailable, expectedErr: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
count := 0
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
count++
if count < tc.attempts {
if tc.retryAfter {
w.Header().Set("Retry-After", "1")
}
http.Error(w, "error", tc.StatusCode)
return
}
w.WriteHeader(http.StatusOK)
}))
defer ts.Close()
req, err := http.NewRequest(http.MethodPost, ts.URL, bytes.NewReader([]byte("test")))
if err != nil {
t.Fatalf("failed to create test request: %v", err)
}
resp, err := DefaultClient.Do(req)
if err != nil {
t.Fatalf("failed to do test request: %v", err)
}
if tc.expectedErr {
if count != (tc.attempts - 1) {
t.Errorf("expected attempts %d, got %d", tc.attempts, count)
}
if resp.StatusCode != http.StatusServiceUnavailable {
t.Errorf("expected status code %d, got %d", http.StatusServiceUnavailable, resp.StatusCode)
}
return
}
if tc.attempts != count {
t.Errorf("expected attempts %d, got %d", tc.attempts, count)
}
})
}
}
oras-go-2.5.0/registry/remote/retry/policy.go 0000664 0000000 0000000 00000011475 14576745303 0021251 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 retry
import (
"hash/maphash"
"math"
"math/rand"
"net"
"net/http"
"strconv"
"time"
)
// headerRetryAfter is the header key for Retry-After.
const headerRetryAfter = "Retry-After"
// DefaultPolicy is a policy with fine-tuned retry parameters.
// It uses an exponential backoff with jitter.
var DefaultPolicy Policy = &GenericPolicy{
Retryable: DefaultPredicate,
Backoff: DefaultBackoff,
MinWait: 200 * time.Millisecond,
MaxWait: 3 * time.Second,
MaxRetry: 5,
}
// DefaultPredicate is a predicate that retries on 5xx errors, 429 Too Many
// Requests, 408 Request Timeout and on network dial timeout.
var DefaultPredicate Predicate = func(resp *http.Response, err error) (bool, error) {
if err != nil {
// retry on Dial timeout
if err, ok := err.(net.Error); ok && err.Timeout() {
return true, nil
}
return false, err
}
if resp.StatusCode == http.StatusRequestTimeout || resp.StatusCode == http.StatusTooManyRequests {
return true, nil
}
if resp.StatusCode == 0 || resp.StatusCode >= 500 {
return true, nil
}
return false, nil
}
// DefaultBackoff is a backoff that uses an exponential backoff with jitter.
// It uses a base of 250ms, a factor of 2 and a jitter of 10%.
var DefaultBackoff Backoff = ExponentialBackoff(250*time.Millisecond, 2, 0.1)
// Policy is a retry policy.
type Policy interface {
// Retry returns the duration to wait before retrying the request.
// It returns a negative value if the request should not be retried.
// The attempt is used to:
// - calculate the backoff duration, the default backoff is an exponential backoff.
// - determine if the request should be retried.
// The attempt starts at 0 and should be less than MaxRetry for the request to
// be retried.
Retry(attempt int, resp *http.Response, err error) (time.Duration, error)
}
// Predicate is a function that returns true if the request should be retried.
type Predicate func(resp *http.Response, err error) (bool, error)
// Backoff is a function that returns the duration to wait before retrying the
// request. The attempt, is the next attempt number. The response is the
// response from the previous request.
type Backoff func(attempt int, resp *http.Response) time.Duration
// ExponentialBackoff returns a Backoff that uses an exponential backoff with
// jitter. The backoff is calculated as:
//
// temp = backoff * factor ^ attempt
// interval = temp * (1 - jitter) + rand.Int63n(2 * jitter * temp)
//
// The HTTP response is checked for a Retry-After header. If it is present, the
// value is used as the backoff duration.
func ExponentialBackoff(backoff time.Duration, factor, jitter float64) Backoff {
return func(attempt int, resp *http.Response) time.Duration {
var h maphash.Hash
h.SetSeed(maphash.MakeSeed())
rand := rand.New(rand.NewSource(int64(h.Sum64())))
// check Retry-After
if resp != nil && resp.StatusCode == http.StatusTooManyRequests {
if v := resp.Header.Get(headerRetryAfter); v != "" {
if retryAfter, _ := strconv.ParseInt(v, 10, 64); retryAfter > 0 {
return time.Duration(retryAfter) * time.Second
}
}
}
// do exponential backoff with jitter
temp := float64(backoff) * math.Pow(factor, float64(attempt))
return time.Duration(temp*(1-jitter)) + time.Duration(rand.Int63n(int64(2*jitter*temp)))
}
}
// GenericPolicy is a generic retry policy.
type GenericPolicy struct {
// Retryable is a predicate that returns true if the request should be
// retried.
Retryable Predicate
// Backoff is a function that returns the duration to wait before retrying.
Backoff Backoff
// MinWait is the minimum duration to wait before retrying.
MinWait time.Duration
// MaxWait is the maximum duration to wait before retrying.
MaxWait time.Duration
// MaxRetry is the maximum number of retries.
MaxRetry int
}
// Retry returns the duration to wait before retrying the request.
// It returns -1 if the request should not be retried.
func (p *GenericPolicy) Retry(attempt int, resp *http.Response, err error) (time.Duration, error) {
if attempt >= p.MaxRetry {
return -1, nil
}
if ok, err := p.Retryable(resp, err); err != nil {
return -1, err
} else if !ok {
return -1, nil
}
backoff := p.Backoff(attempt, resp)
if backoff < p.MinWait {
backoff = p.MinWait
}
if backoff > p.MaxWait {
backoff = p.MaxWait
}
return backoff, nil
}
oras-go-2.5.0/registry/remote/retry/policy_test.go 0000664 0000000 0000000 00000003421 14576745303 0022300 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 retry
import (
"testing"
"time"
)
func Test_ExponentialBackoff(t *testing.T) {
testCases := []struct {
name string
attempt int
expectedBackoff time.Duration
}{
{
name: "attempt 0 should have a backoff of 0,25s ± 10%",
attempt: 0, expectedBackoff: 250 * time.Millisecond,
},
{
name: "attempt 1 should have a backoff of 0,5s ± 10%",
attempt: 1, expectedBackoff: 500 * time.Millisecond,
},
{
name: "attempt 2 should have a backoff of 1s ± 10%",
attempt: 2, expectedBackoff: 1 * time.Second,
},
{
name: "attempt 3 should have a backoff of 2s ± 10%",
attempt: 3, expectedBackoff: 2 * time.Second,
},
{
name: "attempt 4 should have a backoff of 4s ± 10%",
attempt: 4, expectedBackoff: 4 * time.Second,
},
{
name: "attempt 5 should have a backoff of 8s ± 10%",
attempt: 5, expectedBackoff: 8 * time.Second,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
b := DefaultBackoff(tc.attempt, nil)
base := float64(tc.expectedBackoff)
if !(b >= time.Duration(base*0.9) && b <= time.Duration(base+base*0.1)) {
t.Errorf("expected backoff to be %s + jitter, got %s", time.Duration(base), b)
}
})
}
}
oras-go-2.5.0/registry/remote/url.go 0000664 0000000 0000000 00000010447 14576745303 0017405 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 remote
import (
"fmt"
"net/url"
"strings"
"github.com/opencontainers/go-digest"
"oras.land/oras-go/v2/registry"
)
// buildScheme returns HTTP scheme used to access the remote registry.
func buildScheme(plainHTTP bool) string {
if plainHTTP {
return "http"
}
return "https"
}
// buildRegistryBaseURL builds the URL for accessing the base API.
// Format: :///v2/
// Reference: https://docs.docker.com/registry/spec/api/#base
func buildRegistryBaseURL(plainHTTP bool, ref registry.Reference) string {
return fmt.Sprintf("%s://%s/v2/", buildScheme(plainHTTP), ref.Host())
}
// buildRegistryCatalogURL builds the URL for accessing the catalog API.
// Format: :///v2/_catalog
// Reference: https://docs.docker.com/registry/spec/api/#catalog
func buildRegistryCatalogURL(plainHTTP bool, ref registry.Reference) string {
return fmt.Sprintf("%s://%s/v2/_catalog", buildScheme(plainHTTP), ref.Host())
}
// buildRepositoryBaseURL builds the base endpoint of the remote repository.
// Format: :///v2/
func buildRepositoryBaseURL(plainHTTP bool, ref registry.Reference) string {
return fmt.Sprintf("%s://%s/v2/%s", buildScheme(plainHTTP), ref.Host(), ref.Repository)
}
// buildRepositoryTagListURL builds the URL for accessing the tag list API.
// Format: :///v2//tags/list
// Reference: https://docs.docker.com/registry/spec/api/#tags
func buildRepositoryTagListURL(plainHTTP bool, ref registry.Reference) string {
return buildRepositoryBaseURL(plainHTTP, ref) + "/tags/list"
}
// buildRepositoryManifestURL builds the URL for accessing the manifest API.
// Format: :///v2//manifests/
// Reference: https://docs.docker.com/registry/spec/api/#manifest
func buildRepositoryManifestURL(plainHTTP bool, ref registry.Reference) string {
return strings.Join([]string{
buildRepositoryBaseURL(plainHTTP, ref),
"manifests",
ref.Reference,
}, "/")
}
// buildRepositoryBlobURL builds the URL for accessing the blob API.
// Format: :///v2//blobs/
// Reference: https://docs.docker.com/registry/spec/api/#blob
func buildRepositoryBlobURL(plainHTTP bool, ref registry.Reference) string {
return strings.Join([]string{
buildRepositoryBaseURL(plainHTTP, ref),
"blobs",
ref.Reference,
}, "/")
}
// buildRepositoryBlobUploadURL builds the URL for blob uploading.
// Format: :///v2//blobs/uploads/
// Reference: https://docs.docker.com/registry/spec/api/#initiate-blob-upload
func buildRepositoryBlobUploadURL(plainHTTP bool, ref registry.Reference) string {
return buildRepositoryBaseURL(plainHTTP, ref) + "/blobs/uploads/"
}
// buildRepositoryBlobMountURLbuilds the URL for cross-repository mounting.
// Format: :///v2//blobs/uploads/?mount=&from=
// Reference: https://docs.docker.com/registry/spec/api/#blob
func buildRepositoryBlobMountURL(plainHTTP bool, ref registry.Reference, d digest.Digest, fromRepo string) string {
return fmt.Sprintf("%s?mount=%s&from=%s",
buildRepositoryBlobUploadURL(plainHTTP, ref),
d,
fromRepo,
)
}
// buildReferrersURL builds the URL for querying the Referrers API.
// Format: :///v2//referrers/?artifactType=
// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers
func buildReferrersURL(plainHTTP bool, ref registry.Reference, artifactType string) string {
var query string
if artifactType != "" {
v := url.Values{}
v.Set("artifactType", artifactType)
query = "?" + v.Encode()
}
return fmt.Sprintf(
"%s/referrers/%s%s",
buildRepositoryBaseURL(plainHTTP, ref),
ref.Reference,
query,
)
}
oras-go-2.5.0/registry/remote/url_test.go 0000664 0000000 0000000 00000005322 14576745303 0020440 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 remote
import (
"net/url"
"reflect"
"testing"
"oras.land/oras-go/v2/registry"
)
func Test_buildReferrersURL(t *testing.T) {
ref := registry.Reference{
Registry: "localhost",
Repository: "hello-world",
Reference: "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
}
params := []struct {
name string
plainHttp bool
artifactType string
want string
}{
{
name: "plain http, no filter",
plainHttp: true,
artifactType: "",
want: "http://localhost/v2/hello-world/referrers/sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
},
{
name: "https, no filter",
plainHttp: false,
artifactType: "",
want: "https://localhost/v2/hello-world/referrers/sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
},
{
name: "plain http, filter",
plainHttp: true,
artifactType: "signature/example",
want: "http://localhost/v2/hello-world/referrers/sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9?artifactType=signature%2Fexample",
},
{
name: "https, filter",
plainHttp: false,
artifactType: "signature/example",
want: "https://localhost/v2/hello-world/referrers/sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9?artifactType=signature%2Fexample",
},
}
for _, tt := range params {
t.Run(tt.name, func(t *testing.T) {
got := buildReferrersURL(tt.plainHttp, ref, tt.artifactType)
if !compareUrl(got, tt.want) {
t.Errorf("buildReferrersURL() = %s, want %s", got, tt.want)
}
})
}
}
// compareUrl compares two urls, regardless of query order and encoding
func compareUrl(s1, s2 string) bool {
u1, err := url.Parse(s1)
if err != nil {
return false
}
u2, err := url.Parse(s2)
if err != nil {
return false
}
q1, err := url.ParseQuery(u1.RawQuery)
if err != nil {
return false
}
q2, err := url.ParseQuery(u2.RawQuery)
if err != nil {
return false
}
return u1.Scheme == u2.Scheme &&
reflect.DeepEqual(u1.User, u1.User) &&
u1.Host == u2.Host &&
u1.Path == u2.Path &&
reflect.DeepEqual(q1, q2)
}
oras-go-2.5.0/registry/remote/utils.go 0000664 0000000 0000000 00000005177 14576745303 0017747 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 remote
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/errdef"
)
// defaultMaxMetadataBytes specifies the default limit on how many response
// bytes are allowed in the server's response to the metadata APIs.
// See also: Repository.MaxMetadataBytes
var defaultMaxMetadataBytes int64 = 4 * 1024 * 1024 // 4 MiB
// errNoLink is returned by parseLink() when no Link header is present.
var errNoLink = errors.New("no Link header in response")
// parseLink returns the URL of the response's "Link" header, if present.
func parseLink(resp *http.Response) (string, error) {
link := resp.Header.Get("Link")
if link == "" {
return "", errNoLink
}
if link[0] != '<' {
return "", fmt.Errorf("invalid next link %q: missing '<'", link)
}
if i := strings.IndexByte(link, '>'); i == -1 {
return "", fmt.Errorf("invalid next link %q: missing '>'", link)
} else {
link = link[1:i]
}
linkURL, err := resp.Request.URL.Parse(link)
if err != nil {
return "", err
}
return linkURL.String(), nil
}
// limitReader returns a Reader that reads from r but stops with EOF after n
// bytes. If n is less than or equal to zero, defaultMaxMetadataBytes is used.
func limitReader(r io.Reader, n int64) io.Reader {
if n <= 0 {
n = defaultMaxMetadataBytes
}
return io.LimitReader(r, n)
}
// limitSize returns ErrSizeExceedsLimit if the size of desc exceeds the limit n.
// If n is less than or equal to zero, defaultMaxMetadataBytes is used.
func limitSize(desc ocispec.Descriptor, n int64) error {
if n <= 0 {
n = defaultMaxMetadataBytes
}
if desc.Size > n {
return fmt.Errorf(
"content size %v exceeds MaxMetadataBytes %v: %w",
desc.Size,
n,
errdef.ErrSizeExceedsLimit)
}
return nil
}
// decodeJSON safely reads the JSON content described by desc, and
// decodes it into v.
func decodeJSON(r io.Reader, desc ocispec.Descriptor, v any) error {
jsonBytes, err := content.ReadAll(r, desc)
if err != nil {
return err
}
return json.Unmarshal(jsonBytes, v)
}
oras-go-2.5.0/registry/remote/utils_test.go 0000664 0000000 0000000 00000006660 14576745303 0021004 0 ustar 00root root 0000000 0000000 /*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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 remote
import (
"errors"
"net/http"
"net/url"
"testing"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/errdef"
)
func Test_parseLink(t *testing.T) {
tests := []struct {
name string
url string
header string
want string
wantErr bool
}{
{
name: "catalog",
url: "https://localhost:5000/v2/_catalog",
header: `; rel="next"`,
want: "https://localhost:5000/v2/_catalog?last=alpine&n=1",
},
{
name: "list tag",
url: "https://localhost:5000/v2/hello-world/tags/list",
header: `; rel="next"`,
want: "https://localhost:5000/v2/hello-world/tags/list?last=latest&n=1",
},
{
name: "other domain",
url: "https://localhost:5000/v2/_catalog",
header: `; rel="next"`,
want: "https://localhost:5001/v2/_catalog?last=alpine&n=1",
},
{
name: "invalid header",
url: "https://localhost:5000/v2/_catalog",
header: `