diff --git a/cmd/buildah/manifest.go b/cmd/buildah/manifest.go index 233ccf9b5b5..f7c6dbfcf8c 100644 --- a/cmd/buildah/manifest.go +++ b/cmd/buildah/manifest.go @@ -221,6 +221,7 @@ func init() { flags.StringVar(&manifestPushOpts.creds, "creds", "", "use `[username[:password]]` for accessing the registry") flags.StringVar(&manifestPushOpts.digestfile, "digestfile", "", "after copying the image, write the digest of the resulting digest to the file") flags.StringVarP(&manifestPushOpts.format, "format", "f", "", "manifest type (oci or v2s2) to attempt to use when pushing the manifest list (default is manifest type of source)") + flags.StringSliceVar(&manifestPushOpts.replicateWithCompression, "replicate", nil, "replicate instances with selected compression while pushing") flags.BoolVarP(&manifestPushOpts.removeSignatures, "remove-signatures", "", false, "don't copy signatures when pushing images") flags.StringVar(&manifestPushOpts.signBy, "sign-by", "", "sign the image using a GPG key with the specified `FINGERPRINT`") flags.StringVar(&manifestPushOpts.signaturePolicy, "signature-policy", "", "`pathname` of signature policy file (not usually used)") @@ -897,6 +898,7 @@ func manifestPush(systemContext *types.SystemContext, store storage.Store, listI RemoveSignatures: opts.removeSignatures, SignBy: opts.signBy, ManifestType: manifestType, + ReplicateWithCompression: opts.replicateWithCompression, } if opts.all { options.ImageListSelection = cp.CopyAllImages diff --git a/cmd/buildah/push.go b/cmd/buildah/push.go index cc59c7976ca..b2c37e604ed 100644 --- a/cmd/buildah/push.go +++ b/cmd/buildah/push.go @@ -46,6 +46,7 @@ type pushOptions struct { encryptionKeys []string encryptLayers []int insecure bool + replicateWithCompression []string } func init() { diff --git a/vendor/github.com/containers/common/libimage/manifests/manifests.go b/vendor/github.com/containers/common/libimage/manifests/manifests.go index 7a51b84237b..55c873c7005 100644 --- a/vendor/github.com/containers/common/libimage/manifests/manifests.go +++ b/vendor/github.com/containers/common/libimage/manifests/manifests.go @@ -11,6 +11,7 @@ import ( "github.com/containers/common/pkg/manifests" "github.com/containers/common/pkg/supplemented" cp "github.com/containers/image/v5/copy" + "github.com/containers/image/v5/pkg/compression" "github.com/containers/image/v5/docker/reference" "github.com/containers/image/v5/image" "github.com/containers/image/v5/manifest" @@ -70,6 +71,7 @@ type PushOptions struct { RemoveSignatures bool // true to discard signatures in images ManifestType string // the format to use when saving the list - possible options are oci, v2s1, and v2s2 SourceFilter LookupReferenceFunc // filter the list source + ReplicateWithCompression []string } // Create creates a new list containing information about the specified image, @@ -239,6 +241,16 @@ func (l *list) Push(ctx context.Context, dest types.ImageReference, options Push return nil, "", err } } + + compressionVariants := []cp.CopyOptionCompressionVariant{} + //options.EnsureCompressionVariantExists = []CopyOptionCompressionVariant{CopyOptionCompressionVariant{Algorithm: &compression.Zstd, Level:20}} + for _, compressionVariant := range options.ReplicateWithCompression { + if compressionVariant == "zstd" { + variant := cp.CopyOptionCompressionVariant{Algorithm: &compression.Zstd, Level:20} + compressionVariants = append(compressionVariants, variant) + } + } + copyOptions := &cp.Options{ ImageListSelection: options.ImageListSelection, Instances: options.Instances, @@ -252,6 +264,7 @@ func (l *list) Push(ctx context.Context, dest types.ImageReference, options Push SignBySigstorePrivateKeyFile: options.SignBySigstorePrivateKeyFile, SignSigstorePrivateKeyPassphrase: options.SignSigstorePrivateKeyPassphrase, ForceManifestMIMEType: singleImageManifestType, + EnsureCompressionVariantExists: compressionVariants, } // Copy whatever we were asked to copy. diff --git a/vendor/github.com/containers/image/v5/CODE-OF-CONDUCT.md b/vendor/github.com/containers/image/v5/CODE-OF-CONDUCT.md new file mode 100644 index 00000000000..88b27b7d8f0 --- /dev/null +++ b/vendor/github.com/containers/image/v5/CODE-OF-CONDUCT.md @@ -0,0 +1,3 @@ +## The image Project Community Code of Conduct + +The image project follows the [Containers Community Code of Conduct](https://github.com/containers/common/blob/main/CODE-OF-CONDUCT.md). diff --git a/vendor/github.com/containers/image/v5/CONTRIBUTING.md b/vendor/github.com/containers/image/v5/CONTRIBUTING.md new file mode 100644 index 00000000000..1ba458d96b0 --- /dev/null +++ b/vendor/github.com/containers/image/v5/CONTRIBUTING.md @@ -0,0 +1,144 @@ +# Contributing to Containers/Image + +We'd love to have you join the community! Below summarizes the processes +that we follow. + +## Topics + +* [Reporting Issues](#reporting-issues) +* [Submitting Pull Requests](#submitting-pull-requests) +* [Communications](#communications) + + +## Reporting Issues + +Before reporting an issue, check our backlog of +[open issues](https://github.com/containers/image/issues) +to see if someone else has already reported it. If so, feel free to add +your scenario, or additional information, to the discussion. Or simply +"subscribe" to it to be notified when it is updated. + +If you find a new issue with the project we'd love to hear about it! The most +important aspect of a bug report is that it includes enough information for +us to reproduce it. So, please include as much detail as possible and try +to remove the extra stuff that doesn't really relate to the issue itself. +The easier it is for us to reproduce it, the faster it'll be fixed! + +Please don't include any private/sensitive information in your issue! + +## Submitting Pull Requests + +No Pull Request (PR) is too small! Typos, additional comments in the code, +new testcases, bug fixes, new features, more documentation, ... it's all +welcome! + +While bug fixes can first be identified via an "issue", that is not required. +It's ok to just open up a PR with the fix, but make sure you include the same +information you would have included in an issue - like how to reproduce it. + +PRs for new features should include some background on what use cases the +new code is trying to address. When possible and when it makes sense, try to break-up +larger PRs into smaller ones - it's easier to review smaller +code changes. But only if those smaller ones make sense as stand-alone PRs. + +Regardless of the type of PR, all PRs should include: +* well documented code changes +* additional testcases. Ideally, they should fail w/o your code change applied +* documentation changes + +Squash your commits into logical pieces of work that might want to be reviewed +separate from the rest of the PRs. Ideally, each commit should implement a single +idea, and the PR branch should pass the tests at every commit. GitHub makes it easy +to review the cumulative effect of many commits; so, when in doubt, use smaller commits. + +PRs that fix issues should include a reference like `Closes #XXXX` in the +commit message so that github will automatically close the referenced issue +when the PR is merged. + + + +### Sign your PRs + +The sign-off is a line at the end of the explanation for the patch. Your +signature certifies that you wrote the patch or otherwise have the right to pass +it on as an open-source patch. The rules are simple: if you can certify +the below (from [developercertificate.org](https://developercertificate.org/)): + +``` +Developer Certificate of Origin +Version 1.1 + +Copyright (C) 2004, 2006 The Linux Foundation and its contributors. +660 York Street, Suite 102, +San Francisco, CA 94110 USA + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. +``` + +Then you just add a line to every git commit message: + + Signed-off-by: Joe Smith + +Use your real name (sorry, no pseudonyms or anonymous contributions.) + +If you set your `user.name` and `user.email` git configs, you can sign your +commit automatically with `git commit -s`. + +## Communications + +For general questions, or discussions, please use the +IRC group on `irc.freenode.net` called `container-projects` +that has been setup. + +For discussions around issues/bugs and features, you can use the github +[issues](https://github.com/containers/image/issues) +and +[PRs](https://github.com/containers/image/pulls) +tracking system. + + diff --git a/vendor/github.com/containers/image/v5/MAINTAINERS b/vendor/github.com/containers/image/v5/MAINTAINERS new file mode 100644 index 00000000000..a5fdce84f56 --- /dev/null +++ b/vendor/github.com/containers/image/v5/MAINTAINERS @@ -0,0 +1,6 @@ +Antonio Murdaca (@runcom) +Brandon Philips (@philips) +Miloslav Trmac (@mtrmac) +Dan Walsh (@dwalsh) +Nalin Dahyabhai (@nalind) +Valentin Rothberg (@vrothberg) diff --git a/vendor/github.com/containers/image/v5/Makefile b/vendor/github.com/containers/image/v5/Makefile new file mode 100644 index 00000000000..fe88f1d108f --- /dev/null +++ b/vendor/github.com/containers/image/v5/Makefile @@ -0,0 +1,93 @@ +.PHONY: all tools test validate lint .gitvalidation fmt + +export GOPROXY=https://proxy.golang.org + + +GOBIN := $(shell go env GOBIN) +ifeq ($(GOBIN),) +GOBIN := $(shell go env GOPATH)/bin +endif + +# when cross compiling _for_ a Darwin or windows host, then we must use openpgp +BUILD_TAGS_WINDOWS_CROSS = containers_image_openpgp +BUILD_TAGS_DARWIN_CROSS = containers_image_openpgp + +BUILDTAGS = btrfs_noversion libdm_no_deferred_remove +BUILDFLAGS := -tags "$(BUILDTAGS)" + +PACKAGES := $(shell GO111MODULE=on go list $(BUILDFLAGS) ./...) +SOURCE_DIRS = $(shell echo $(PACKAGES) | awk 'BEGIN{FS="/"; RS=" "}{print $$4}' | uniq) + +PREFIX ?= ${DESTDIR}/usr +MANINSTALLDIR=${PREFIX}/share/man +GOMD2MAN ?= $(shell command -v go-md2man || echo '$(GOBIN)/go-md2man') +MANPAGES_MD = $(wildcard docs/*.5.md) +MANPAGES ?= $(MANPAGES_MD:%.md=%) + +export PATH := $(PATH):${GOBIN} + +all: tools test validate .gitvalidation + +build: + GO111MODULE="on" go build $(BUILDFLAGS) ./... + +$(MANPAGES): %: %.md + $(GOMD2MAN) -in $< -out $@ + +docs: $(MANPAGES) + +install-docs: docs + install -d -m 755 ${MANINSTALLDIR}/man5 + install -m 644 docs/*.5 ${MANINSTALLDIR}/man5/ + +install: install-docs + +cross: + GOOS=windows $(MAKE) build BUILDTAGS="$(BUILDTAGS) $(BUILD_TAGS_WINDOWS_CROSS)" + GOOS=darwin $(MAKE) build BUILDTAGS="$(BUILDTAGS) $(BUILD_TAGS_DARWIN_CROSS)" + +tools: .install.gitvalidation .install.golangci-lint .install.golint + +.install.gitvalidation: + if [ ! -x "$(GOBIN)/git-validation" ]; then \ + GO111MODULE="off" go get $(BUILDFLAGS) github.com/vbatts/git-validation; \ + fi + +.install.golangci-lint: + if [ ! -x "$(GOBIN)/golangci-lint" ]; then \ + curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(GOBIN) v1.51.0; \ + fi + +.install.golint: + # Note, golint is only needed for Skopeo's tests. + if [ ! -x "$(GOBIN)/golint" ]; then \ + GO111MODULE="off" go get -u $(BUILDFLAGS) golang.org/x/lint/golint; \ + fi + +clean: + rm -rf $(MANPAGES) + +test: + @GO111MODULE="on" go test $(BUILDFLAGS) -cover ./... + +fmt: + @gofmt -l -s -w $(SOURCE_DIRS) + +validate: lint + @BUILDTAGS="$(BUILDTAGS)" hack/validate.sh + +lint: + $(GOBIN)/golangci-lint run --build-tags "$(BUILDTAGS)" + +# When this is running in CI, it will only check the CI commit range +.gitvalidation: + @which $(GOBIN)/git-validation > /dev/null 2>/dev/null || (echo "ERROR: git-validation not found. Consider 'make clean && make tools'" && false) + git fetch -q "https://github.com/containers/image.git" "refs/heads/main" + upstream="$$(git rev-parse --verify FETCH_HEAD)" ; \ + $(GOBIN)/git-validation -q -run DCO,short-subject,dangling-whitespace -range $$upstream..HEAD + +vendor-in-container: + podman run --privileged --rm --env HOME=/root -v `pwd`:/src -w /src golang go mod tidy + +codespell: + codespell -S Makefile,build,buildah,buildah.spec,imgtype,copy,AUTHORS,bin,vendor,.git,go.sum,CHANGELOG.md,changelog.txt,seccomp.json,.cirrus.yml,"*.xz,*.gz,*.tar,*.tgz,*ico,*.png,*.1,*.5,*.orig,*.rej" -L keypair,flate,uint,iff,od,ERRO -w diff --git a/vendor/github.com/containers/image/v5/README.md b/vendor/github.com/containers/image/v5/README.md new file mode 100644 index 00000000000..034665bf141 --- /dev/null +++ b/vendor/github.com/containers/image/v5/README.md @@ -0,0 +1,82 @@ +[![GoDoc](https://godoc.org/github.com/containers/image?status.svg)](https://godoc.org/github.com/containers/image) [![Build Status](https://api.cirrus-ci.com/github/containers/image.svg)](https://cirrus-ci.com/github/containers/image) += + +`image` is a set of Go libraries aimed at working in various way with +containers' images and container image registries. + +The containers/image library allows application to pull and push images from +container image registries, like the docker.io and quay.io registries. It also +implements "simple image signing". + +The containers/image library also allows you to inspect a repository on a +container registry without pulling down the image. This means it fetches the +repository's manifest and it is able to show you a `docker inspect`-like json +output about a whole repository or a tag. This library, in contrast to `docker +inspect`, helps you gather useful information about a repository or a tag +without requiring you to run `docker pull`. + +The containers/image library also allows you to translate from one image format +to another, for example docker container images to OCI images. It also allows +you to copy container images between various registries, possibly converting +them as necessary, and to sign and verify images. + +## Command-line usage + +The containers/image project is only a library with no user interface; +you can either incorporate it into your Go programs, or use the `skopeo` tool: + +The [skopeo](https://github.com/containers/skopeo) tool uses the +containers/image library and takes advantage of many of its features, +e.g. `skopeo copy` exposes the `containers/image/copy.Image` functionality. + +## Dependencies + +This library ships as a [Go module]. + +## Building + +If you want to see what the library can do, or an example of how it is called, +consider starting with the [skopeo](https://github.com/containers/skopeo) tool +instead. + +To integrate this library into your project, include it as a [Go module], +put it into `$GOPATH` or use your preferred vendoring tool to include a copy +in your project. Ensure that the dependencies documented [in go.mod][go.mod] +are also available (using those exact versions or different versions of +your choosing). + +This library also depends on some C libraries. Either install them: +```sh +Fedora$ dnf install gpgme-devel libassuan-devel # potentially also ostree-devel +macOS$ brew install gpgme +``` +or use the build tags described below to avoid the dependencies (e.g. using `go build -tags …`) + +[Go module]: https://github.com/golang/go/wiki/Modules +[go.mod]: https://github.com/containers/image/blob/master/go.mod + +### Supported build tags + +- `containers_image_docker_daemon_stub`: Don’t import the `docker-daemon:` transport in `github.com/containers/image/transports/alltransports`, to decrease the amount of required dependencies. Use a stub which reports that the transport is not supported instead. +- `containers_image_openpgp`: Use a Golang-only OpenPGP implementation for signature verification instead of the default cgo/gpgme-based implementation; +the primary downside is that creating new signatures with the Golang-only implementation is not supported. +- `containers_image_ostree`: Import `ostree:` transport in `github.com/containers/image/transports/alltransports`. This builds the library requiring the `libostree` development libraries. Otherwise a stub which reports that the transport is not supported gets used. The `github.com/containers/image/ostree` package is completely disabled +and impossible to import when this build tag is not in use. +- `containers_image_storage_stub`: Don’t import the `containers-storage:` transport in `github.com/containers/image/transports/alltransports`, to decrease the amount of required dependencies. Use a stub which reports that the transport is not supported instead. + +## [Contributing](CONTRIBUTING.md) + +Information about contributing to this project. + +When developing this library, please use `make` (or `make … BUILDTAGS=…`) to take advantage of the tests and validation. + +## License + +Apache License 2.0 + +SPDX-License-Identifier: Apache-2.0 + +## Contact + +- Mailing list: [containers-dev](https://groups.google.com/forum/?hl=en#!forum/containers-dev) +- IRC: #[container-projects](irc://irc.freenode.net:6667/#container-projects) on freenode.net diff --git a/vendor/github.com/containers/image/v5/SECURITY.md b/vendor/github.com/containers/image/v5/SECURITY.md new file mode 100644 index 00000000000..5a20c0e87b9 --- /dev/null +++ b/vendor/github.com/containers/image/v5/SECURITY.md @@ -0,0 +1,3 @@ +## Security and Disclosure Information Policy for the image Project + +The image Project follows the [Security and Disclosure Information Policy](https://github.com/containers/common/blob/main/SECURITY.md) for the Containers Projects. diff --git a/vendor/github.com/containers/image/v5/contrib/cirrus/runner.sh b/vendor/github.com/containers/image/v5/contrib/cirrus/runner.sh new file mode 100755 index 00000000000..dadaa5db162 --- /dev/null +++ b/vendor/github.com/containers/image/v5/contrib/cirrus/runner.sh @@ -0,0 +1,98 @@ +#!/bin/bash + +# This script is intended to be executed by automation or humans +# under a hack/get_ci_vm.sh context. Use under any other circumstances +# is unlikely to function. + +set -e + +if [[ -r "/etc/automation_environment" ]]; then + source /etc/automation_environment + source $AUTOMATION_LIB_PATH/common_lib.sh +else + ( + echo "WARNING: It does not appear that containers/automation was installed." + echo " Functionality of most of ${BASH_SOURCE[0]} will be negatively" + echo " impacted." + ) > /dev/stderr +fi + +export "PATH=$PATH:$GOPATH/bin" + +_run_setup() { + req_env_vars SKOPEO_PATH SKOPEO_CI_TAG GOSRC BUILDTAGS + + project_module=$(go list .) + + make tools + + rm -rf "${SKOPEO_PATH}" + git clone -b ${SKOPEO_CI_TAG} \ + https://github.com/containers/skopeo.git ${SKOPEO_PATH} + + cd "${SKOPEO_PATH}" + if [[ -n "$SKOPEO_PR" ]] && [[ $SKOPEO_PR -gt 1000 ]]; then + warn "Fetching and checking out code from skopeo pull-request #$SKOPEO_PR" + git fetch origin "+refs/pull/$SKOPEO_PR/head" + git checkout FETCH_HEAD + fi + + msg "Replacing upstream skopeo $SKOPEO_CI_TAG branch $project_module module" + go mod edit -replace ${project_module}=$GOSRC + + "${SKOPEO_PATH}/${SCRIPT_BASE}/runner.sh" setup +} + +_run_image_tests() { + req_env_vars GOPATH GOSRC + + # Tests in this repo. are intended to run as a regular user. + ROOTLESS_USER="testuser$RANDOM" + msg "Setting up rootless user '$ROOTLESS_USER'" + cd $GOSRC || exit 1 + # Guarantee independence from specific values + rootless_uid=$((RANDOM+1000)) + rootless_gid=$((RANDOM+1000)) + msg "Creating $rootless_uid:$rootless_gid $ROOTLESS_USER user" + groupadd -g $rootless_gid $ROOTLESS_USER + useradd -g $rootless_gid -u $rootless_uid --no-user-group --create-home $ROOTLESS_USER + + msg "Setting ownership of $GOPATH and $GOSRC" + chown -R $ROOTLESS_USER:$ROOTLESS_USER "$GOPATH" "$GOSRC" + + msg "Creating ssh key pairs" + mkdir -p "/root/.ssh" "/home/$ROOTLESS_USER/.ssh" + ssh-keygen -t ed25519 -P "" -f "/root/.ssh/id_ed25519" + + msg "Setup authorized_keys" + cat /root/.ssh/*.pub >> /home/$ROOTLESS_USER/.ssh/authorized_keys + + msg "Configure ssh file permissions" + chmod -R 700 "/root/.ssh" + chmod -R 700 "/home/$ROOTLESS_USER/.ssh" + chown -R $ROOTLESS_USER:$ROOTLESS_USER "/home/$ROOTLESS_USER/.ssh" + + msg "Ensure the ssh daemon is up and running within 5 minutes" + systemctl is-active sshd || \ + systemctl start sshd + + msg "Setup known_hosts for root" + ssh-keyscan localhost > /root/.ssh/known_hosts \ + + msg "Executing tests as $ROOTLESS_USER" + showrun ssh $ROOTLESS_USER@localhost make -C $GOSRC test "BUILDTAGS='$BUILDTAGS'" +} + +req_env_vars GOSRC + +handler="_run_${1}" +if [ "$(type -t $handler)" != "function" ]; then + die "Unknown/Unsupported command-line argument '$1'" +fi + +msg "************************************************************" +msg "Runner executing $1 on $OS_REL_VER" +msg "************************************************************" + +cd "$GOSRC" +$handler diff --git a/vendor/github.com/containers/image/v5/copy/blob_test.go b/vendor/github.com/containers/image/v5/copy/blob_test.go new file mode 100644 index 00000000000..61f1e360ab7 --- /dev/null +++ b/vendor/github.com/containers/image/v5/copy/blob_test.go @@ -0,0 +1,76 @@ +package copy + +import ( + "fmt" + "testing" + + "github.com/containers/image/v5/internal/private" + "github.com/containers/image/v5/pkg/compression" + "github.com/containers/image/v5/types" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/assert" +) + +func TestUpdatedBlobInfoFromUpload(t *testing.T) { + for _, c := range []struct { + srcInfo types.BlobInfo + uploaded private.UploadedBlob + expected types.BlobInfo + }{ + { // A straightforward upload with a known size + srcInfo: types.BlobInfo{ + Digest: "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb", + Size: 51354364, + URLs: []string{"https://layer.url"}, + Annotations: map[string]string{"test-annotation-2": "two"}, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + CompressionOperation: types.Compress, // Might be set by blobCacheSource.LayerInfosForCopy + CompressionAlgorithm: &compression.Gzip, // Set e.g. in copyLayer + // CryptoOperation is not set by LayerInfos() + }, + uploaded: private.UploadedBlob{ + Digest: "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb", + Size: 51354364, + }, + expected: types.BlobInfo{ + Digest: "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb", + Size: 51354364, + URLs: nil, + Annotations: map[string]string{"test-annotation-2": "two"}, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + CompressionOperation: types.Compress, // Might be set by blobCacheSource.LayerInfosForCopy + CompressionAlgorithm: &compression.Gzip, // Set e.g. in copyLayer + // CryptoOperation is set to the zero value + }, + }, + { // Upload determining the digest/size + srcInfo: types.BlobInfo{ + Digest: "", + Size: -1, + URLs: []string{"https://layer.url"}, + Annotations: map[string]string{"test-annotation-2": "two"}, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + CompressionOperation: types.Compress, // Might be set by blobCacheSource.LayerInfosForCopy + CompressionAlgorithm: &compression.Gzip, // Set e.g. in copyLayer + // CryptoOperation is not set by LayerInfos() + }, + uploaded: private.UploadedBlob{ + Digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + Size: 513543640, + }, + expected: types.BlobInfo{ + Digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + Size: 513543640, + URLs: nil, + Annotations: map[string]string{"test-annotation-2": "two"}, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + CompressionOperation: types.Compress, // Might be set by blobCacheSource.LayerInfosForCopy + CompressionAlgorithm: &compression.Gzip, // Set e.g. in copyLayer + // CryptoOperation is set to the zero value + }, + }, + } { + res := updatedBlobInfoFromUpload(c.srcInfo, c.uploaded) + assert.Equal(t, c.expected, res, fmt.Sprintf("%#v", c.uploaded)) + } +} diff --git a/vendor/github.com/containers/image/v5/copy/copy.go b/vendor/github.com/containers/image/v5/copy/copy.go index bf8f4015b68..f5c76e4dc94 100644 --- a/vendor/github.com/containers/image/v5/copy/copy.go +++ b/vendor/github.com/containers/image/v5/copy/copy.go @@ -17,6 +17,7 @@ import ( "github.com/containers/image/v5/internal/private" "github.com/containers/image/v5/manifest" "github.com/containers/image/v5/pkg/blobinfocache" + compression "github.com/containers/image/v5/pkg/compression/types" "github.com/containers/image/v5/signature" "github.com/containers/image/v5/signature/signer" "github.com/containers/image/v5/transports" @@ -38,6 +39,11 @@ var ( maxParallelDownloads = uint(6) ) +type CopyOptionCompressionVariant struct { + Algorithm *compression.Algorithm + Level int +} + const ( // CopySystemImage is the default value which, when set in // Options.ImageListSelection, indicates that the caller expects only one @@ -126,6 +132,10 @@ type Options struct { // Download layer contents with "nondistributable" media types ("foreign" layers) and translate the layer media type // to not indicate "nondistributable". DownloadForeignLayers bool + + // Contains a map of compression algorithm and compression level, where c/image will ensure that selected + // images in the manifest list will be replicated with provided compression algorithms. + EnsureCompressionVariantExists []CopyOptionCompressionVariant } // copier allows us to keep track of diffID values for blobs, and other @@ -257,7 +267,7 @@ func Image(ctx context.Context, policyContext *signature.PolicyContext, destRef, if !multiImage { // The simple case: just copy a single image. - if copiedManifest, _, _, err = c.copySingleImage(ctx, policyContext, options, unparsedToplevel, unparsedToplevel, nil); err != nil { + if copiedManifest, _, _, err = c.copySingleImage(ctx, policyContext, options, unparsedToplevel, unparsedToplevel, nil, nil); err != nil { return nil, err } } else if options.ImageListSelection == CopySystemImage { @@ -278,7 +288,7 @@ func Image(ctx context.Context, policyContext *signature.PolicyContext, destRef, logrus.Debugf("Source is a manifest list; copying (only) instance %s for current system", instanceDigest) unparsedInstance := image.UnparsedInstance(rawSource, &instanceDigest) - if copiedManifest, _, _, err = c.copySingleImage(ctx, policyContext, options, unparsedToplevel, unparsedInstance, nil); err != nil { + if copiedManifest, _, _, err = c.copySingleImage(ctx, policyContext, options, unparsedToplevel, unparsedInstance, nil, nil); err != nil { return nil, fmt.Errorf("copying system image from manifest list: %w", err) } } else { /* options.ImageListSelection == CopyAllImages or options.ImageListSelection == CopySpecificImages, */ diff --git a/vendor/github.com/containers/image/v5/copy/digesting_reader_test.go b/vendor/github.com/containers/image/v5/copy/digesting_reader_test.go new file mode 100644 index 00000000000..2e17437ae33 --- /dev/null +++ b/vendor/github.com/containers/image/v5/copy/digesting_reader_test.go @@ -0,0 +1,77 @@ +package copy + +import ( + "bytes" + "io" + "testing" + + digest "github.com/opencontainers/go-digest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewDigestingReader(t *testing.T) { + // Only the failure cases, success is tested in TestDigestingReaderRead below. + source := bytes.NewReader([]byte("abc")) + for _, input := range []digest.Digest{ + "abc", // Not algo:hexvalue + "crc32:", // Unknown algorithm, empty value + "crc32:012345678", // Unknown algorithm + "sha256:", // Empty value + "sha256:0", // Invalid hex value + "sha256:01", // Invalid length of hex value + } { + _, err := newDigestingReader(source, input) + assert.Error(t, err, input.String()) + } +} + +func TestDigestingReaderRead(t *testing.T) { + cases := []struct { + input []byte + digest digest.Digest + }{ + {[]byte(""), "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}, + {[]byte("abc"), "sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"}, + {make([]byte, 65537), "sha256:3266304f31be278d06c3bd3eb9aa3e00c59bedec0a890de466568b0b90b0e01f"}, + } + // Valid input + for _, c := range cases { + source := bytes.NewReader(c.input) + reader, err := newDigestingReader(source, c.digest) + require.NoError(t, err, c.digest.String()) + dest := bytes.Buffer{} + n, err := io.Copy(&dest, reader) + assert.NoError(t, err, c.digest.String()) + assert.Equal(t, int64(len(c.input)), n, c.digest.String()) + assert.Equal(t, c.input, dest.Bytes(), c.digest.String()) + assert.False(t, reader.validationFailed, c.digest.String()) + assert.True(t, reader.validationSucceeded, c.digest.String()) + } + // Modified input + for _, c := range cases { + source := bytes.NewReader(bytes.Join([][]byte{c.input, []byte("x")}, nil)) + reader, err := newDigestingReader(source, c.digest) + require.NoError(t, err, c.digest.String()) + dest := bytes.Buffer{} + _, err = io.Copy(&dest, reader) + assert.Error(t, err, c.digest.String()) + assert.True(t, reader.validationFailed, c.digest.String()) + assert.False(t, reader.validationSucceeded, c.digest.String()) + } + // Truncated input + for _, c := range cases { + source := bytes.NewReader(c.input) + reader, err := newDigestingReader(source, c.digest) + require.NoError(t, err, c.digest.String()) + if len(c.input) != 0 { + dest := bytes.Buffer{} + truncatedLen := int64(len(c.input) - 1) + n, err := io.CopyN(&dest, reader, truncatedLen) + assert.NoError(t, err, c.digest.String()) + assert.Equal(t, truncatedLen, n, c.digest.String()) + } + assert.False(t, reader.validationFailed, c.digest.String()) + assert.False(t, reader.validationSucceeded, c.digest.String()) + } +} diff --git a/vendor/github.com/containers/image/v5/copy/fixtures/Hello.bz2 b/vendor/github.com/containers/image/v5/copy/fixtures/Hello.bz2 new file mode 120000 index 00000000000..fc28d6c9ac2 --- /dev/null +++ b/vendor/github.com/containers/image/v5/copy/fixtures/Hello.bz2 @@ -0,0 +1 @@ +../../pkg/compression/fixtures/Hello.bz2 \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/copy/fixtures/Hello.gz b/vendor/github.com/containers/image/v5/copy/fixtures/Hello.gz new file mode 120000 index 00000000000..08aa805fcc1 --- /dev/null +++ b/vendor/github.com/containers/image/v5/copy/fixtures/Hello.gz @@ -0,0 +1 @@ +../../pkg/compression/fixtures/Hello.gz \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/copy/fixtures/Hello.std b/vendor/github.com/containers/image/v5/copy/fixtures/Hello.std new file mode 100644 index 00000000000..02770a67dae Binary files /dev/null and b/vendor/github.com/containers/image/v5/copy/fixtures/Hello.std differ diff --git a/vendor/github.com/containers/image/v5/copy/fixtures/Hello.uncompressed b/vendor/github.com/containers/image/v5/copy/fixtures/Hello.uncompressed new file mode 120000 index 00000000000..49b46625d8c --- /dev/null +++ b/vendor/github.com/containers/image/v5/copy/fixtures/Hello.uncompressed @@ -0,0 +1 @@ +../../pkg/compression/fixtures/Hello.uncompressed \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/copy/fixtures/Hello.xz b/vendor/github.com/containers/image/v5/copy/fixtures/Hello.xz new file mode 120000 index 00000000000..77bcd85587a --- /dev/null +++ b/vendor/github.com/containers/image/v5/copy/fixtures/Hello.xz @@ -0,0 +1 @@ +../../pkg/compression/fixtures/Hello.xz \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/copy/fixtures/Hello.zst b/vendor/github.com/containers/image/v5/copy/fixtures/Hello.zst new file mode 100644 index 00000000000..02770a67dae Binary files /dev/null and b/vendor/github.com/containers/image/v5/copy/fixtures/Hello.zst differ diff --git a/vendor/github.com/containers/image/v5/copy/manifest_test.go b/vendor/github.com/containers/image/v5/copy/manifest_test.go new file mode 100644 index 00000000000..0a7d6540af5 --- /dev/null +++ b/vendor/github.com/containers/image/v5/copy/manifest_test.go @@ -0,0 +1,484 @@ +package copy + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/containers/image/v5/internal/testing/mocks" + "github.com/containers/image/v5/manifest" + v1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestOrderedSet(t *testing.T) { + for _, c := range []struct{ input, expected []string }{ + {[]string{}, []string{}}, + {[]string{"a", "b", "c"}, []string{"a", "b", "c"}}, + {[]string{"a", "b", "a", "c"}, []string{"a", "b", "c"}}, + } { + os := newOrderedSet() + for _, s := range c.input { + os.append(s) + } + assert.Equal(t, c.expected, os.list, fmt.Sprintf("%#v", c.input)) + } +} + +func TestDetermineManifestConversion(t *testing.T) { + supportS1S2OCI := []string{ + v1.MediaTypeImageManifest, + manifest.DockerV2Schema2MediaType, + manifest.DockerV2Schema1SignedMediaType, + manifest.DockerV2Schema1MediaType, + } + supportS1OCI := []string{ + v1.MediaTypeImageManifest, + manifest.DockerV2Schema1SignedMediaType, + manifest.DockerV2Schema1MediaType, + } + supportS1S2 := []string{ + manifest.DockerV2Schema2MediaType, + manifest.DockerV2Schema1SignedMediaType, + manifest.DockerV2Schema1MediaType, + } + supportOnlyS1 := []string{ + manifest.DockerV2Schema1SignedMediaType, + manifest.DockerV2Schema1MediaType, + } + + cases := []struct { + description string + sourceType string + destTypes []string + expected manifestConversionPlan + }{ + // Destination accepts anything — no conversion necessary + { + "s1→anything", manifest.DockerV2Schema1SignedMediaType, nil, + manifestConversionPlan{ + preferredMIMEType: manifest.DockerV2Schema1SignedMediaType, + preferredMIMETypeNeedsConversion: false, + otherMIMETypeCandidates: []string{}, + }, + }, + { + "s2→anything", manifest.DockerV2Schema2MediaType, nil, + manifestConversionPlan{ + preferredMIMEType: manifest.DockerV2Schema2MediaType, + preferredMIMETypeNeedsConversion: false, + otherMIMETypeCandidates: []string{}, + }, + }, + // Destination accepts the unmodified original + { + "s1→s1s2", manifest.DockerV2Schema1SignedMediaType, supportS1S2, + manifestConversionPlan{ + preferredMIMEType: manifest.DockerV2Schema1SignedMediaType, + preferredMIMETypeNeedsConversion: false, + otherMIMETypeCandidates: []string{manifest.DockerV2Schema2MediaType, manifest.DockerV2Schema1MediaType}, + }, + }, + { + "s2→s1s2", manifest.DockerV2Schema2MediaType, supportS1S2, + manifestConversionPlan{ + preferredMIMEType: manifest.DockerV2Schema2MediaType, + preferredMIMETypeNeedsConversion: false, + otherMIMETypeCandidates: supportOnlyS1, + }, + }, + { + "s1→s1", manifest.DockerV2Schema1SignedMediaType, supportOnlyS1, + manifestConversionPlan{ + preferredMIMEType: manifest.DockerV2Schema1SignedMediaType, + preferredMIMETypeNeedsConversion: false, + otherMIMETypeCandidates: []string{manifest.DockerV2Schema1MediaType}, + }, + }, + // text/plain is normalized to s1, and if the destination accepts s1, no conversion happens. + { + "text→s1s2", "text/plain", supportS1S2, + manifestConversionPlan{ + preferredMIMEType: manifest.DockerV2Schema1SignedMediaType, + preferredMIMETypeNeedsConversion: false, + otherMIMETypeCandidates: []string{manifest.DockerV2Schema2MediaType, manifest.DockerV2Schema1MediaType}, + }, + }, + { + "text→s1", "text/plain", supportOnlyS1, + manifestConversionPlan{ + preferredMIMEType: manifest.DockerV2Schema1SignedMediaType, + preferredMIMETypeNeedsConversion: false, + otherMIMETypeCandidates: []string{manifest.DockerV2Schema1MediaType}, + }, + }, + // Conversion necessary, a preferred format is acceptable + { + "s2→s1", manifest.DockerV2Schema2MediaType, supportOnlyS1, + manifestConversionPlan{ + preferredMIMEType: manifest.DockerV2Schema1SignedMediaType, + preferredMIMETypeNeedsConversion: true, + otherMIMETypeCandidates: []string{manifest.DockerV2Schema1MediaType}, + }, + }, + // Conversion necessary, a preferred format is not acceptable + { + "s2→OCI", manifest.DockerV2Schema2MediaType, []string{v1.MediaTypeImageManifest}, + manifestConversionPlan{ + preferredMIMEType: v1.MediaTypeImageManifest, + preferredMIMETypeNeedsConversion: true, + otherMIMETypeCandidates: []string{}, + }, + }, + // text/plain is converted if the destination does not accept s1 + { + "text→s2", "text/plain", []string{manifest.DockerV2Schema2MediaType}, + manifestConversionPlan{ + preferredMIMEType: manifest.DockerV2Schema2MediaType, + preferredMIMETypeNeedsConversion: true, + otherMIMETypeCandidates: []string{}, + }, + }, + // Conversion necessary, try the preferred formats in order. + // We abuse manifest.DockerV2ListMediaType here as a MIME type which is not in supportS1S2OCI, + // but is still recognized by manifest.NormalizedMIMEType and not normalized to s1 + { + "special→s2", manifest.DockerV2ListMediaType, supportS1S2OCI, + manifestConversionPlan{ + preferredMIMEType: manifest.DockerV2Schema2MediaType, + preferredMIMETypeNeedsConversion: true, + otherMIMETypeCandidates: []string{manifest.DockerV2Schema1SignedMediaType, v1.MediaTypeImageManifest, manifest.DockerV2Schema1MediaType}, + }, + }, + { + "special→s1", manifest.DockerV2ListMediaType, supportS1OCI, + manifestConversionPlan{ + preferredMIMEType: manifest.DockerV2Schema1SignedMediaType, + preferredMIMETypeNeedsConversion: true, + otherMIMETypeCandidates: []string{v1.MediaTypeImageManifest, manifest.DockerV2Schema1MediaType}, + }, + }, + { + "special→OCI", manifest.DockerV2ListMediaType, []string{v1.MediaTypeImageManifest, "other options", "with lower priority"}, + manifestConversionPlan{ + preferredMIMEType: v1.MediaTypeImageManifest, + preferredMIMETypeNeedsConversion: true, + otherMIMETypeCandidates: []string{"other options", "with lower priority"}, + }, + }, + } + + for _, c := range cases { + res, err := determineManifestConversion(determineManifestConversionInputs{ + srcMIMEType: c.sourceType, + destSupportedManifestMIMETypes: c.destTypes, + forceManifestMIMEType: "", + requiresOCIEncryption: false, + cannotModifyManifestReason: "", + }) + require.NoError(t, err, c.description) + assert.Equal(t, c.expected, res, c.description) + } + + // Whatever the input is, with cannotModifyManifestReason we return "keep the original as is" + for _, c := range cases { + res, err := determineManifestConversion(determineManifestConversionInputs{ + srcMIMEType: c.sourceType, + destSupportedManifestMIMETypes: c.destTypes, + forceManifestMIMEType: "", + requiresOCIEncryption: false, + cannotModifyManifestReason: "Preserving digests", + }) + require.NoError(t, err, c.description) + assert.Equal(t, manifestConversionPlan{ + preferredMIMEType: manifest.NormalizedMIMEType(c.sourceType), + preferredMIMETypeNeedsConversion: false, + otherMIMETypeCandidates: []string{}, + }, res, c.description) + } + + // With forceManifestMIMEType, the output is always the forced manifest type (in this case oci manifest) + for _, c := range cases { + res, err := determineManifestConversion(determineManifestConversionInputs{ + srcMIMEType: c.sourceType, + destSupportedManifestMIMETypes: c.destTypes, + forceManifestMIMEType: v1.MediaTypeImageManifest, + requiresOCIEncryption: false, + cannotModifyManifestReason: "", + }) + require.NoError(t, err, c.description) + assert.Equal(t, manifestConversionPlan{ + preferredMIMEType: v1.MediaTypeImageManifest, + preferredMIMETypeNeedsConversion: true, + otherMIMETypeCandidates: []string{}, + }, res, c.description) + } + + // When encryption is required: + for _, c := range []struct { + description string + in determineManifestConversionInputs // with requiresOCIEncryption implied + expected manifestConversionPlan // Or {} to expect a failure + }{ + { // Destination accepts anything - no conversion necessary + "OCI→anything", + determineManifestConversionInputs{ + srcMIMEType: v1.MediaTypeImageManifest, + destSupportedManifestMIMETypes: nil, + }, + manifestConversionPlan{ + preferredMIMEType: v1.MediaTypeImageManifest, + preferredMIMETypeNeedsConversion: false, + otherMIMETypeCandidates: []string{}, + }, + }, + { // Destination accepts anything - need to convert for encryption + "s2→anything", + determineManifestConversionInputs{ + srcMIMEType: manifest.DockerV2Schema2MediaType, + destSupportedManifestMIMETypes: nil, + }, + manifestConversionPlan{ + preferredMIMEType: v1.MediaTypeImageManifest, + preferredMIMETypeNeedsConversion: true, + otherMIMETypeCandidates: []string{}, + }, + }, + // Destination accepts an encrypted format + { + "OCI→OCI", + determineManifestConversionInputs{ + srcMIMEType: v1.MediaTypeImageManifest, + destSupportedManifestMIMETypes: supportS1S2OCI, + }, + manifestConversionPlan{ + preferredMIMEType: v1.MediaTypeImageManifest, + preferredMIMETypeNeedsConversion: false, + otherMIMETypeCandidates: []string{}, + }, + }, + { + "s2→OCI", + determineManifestConversionInputs{ + srcMIMEType: manifest.DockerV2Schema2MediaType, + destSupportedManifestMIMETypes: supportS1S2OCI, + }, + manifestConversionPlan{ + preferredMIMEType: v1.MediaTypeImageManifest, + preferredMIMETypeNeedsConversion: true, + otherMIMETypeCandidates: []string{}, + }, + }, + // Destination does not accept an encrypted format + { + "OCI→s2", + determineManifestConversionInputs{ + srcMIMEType: v1.MediaTypeImageManifest, + destSupportedManifestMIMETypes: supportS1S2, + }, + manifestConversionPlan{}, + }, + { + "s2→s2", + determineManifestConversionInputs{ + srcMIMEType: manifest.DockerV2Schema2MediaType, + destSupportedManifestMIMETypes: supportS1S2, + }, + manifestConversionPlan{}, + }, + // Whatever the input is, with cannotModifyManifestReason we return "keep the original as is". + // Still, encryption is necessarily going to fail… + { + "OCI→OCI cannotModifyManifestReason", + determineManifestConversionInputs{ + srcMIMEType: v1.MediaTypeImageManifest, + destSupportedManifestMIMETypes: supportS1S2OCI, + cannotModifyManifestReason: "Preserving digests", + }, + manifestConversionPlan{ + preferredMIMEType: v1.MediaTypeImageManifest, + preferredMIMETypeNeedsConversion: false, + otherMIMETypeCandidates: []string{}, + }, + }, + { + "s2→OCI cannotModifyManifestReason", + determineManifestConversionInputs{ + srcMIMEType: manifest.DockerV2Schema2MediaType, + destSupportedManifestMIMETypes: supportS1S2OCI, + cannotModifyManifestReason: "Preserving digests", + }, + manifestConversionPlan{ + preferredMIMEType: manifest.DockerV2Schema2MediaType, + preferredMIMETypeNeedsConversion: false, + otherMIMETypeCandidates: []string{}, + }, + }, + // forceManifestMIMEType to a type that supports encryption + { + "OCI→OCI forced", + determineManifestConversionInputs{ + srcMIMEType: v1.MediaTypeImageManifest, + destSupportedManifestMIMETypes: supportS1S2OCI, + forceManifestMIMEType: v1.MediaTypeImageManifest, + }, + manifestConversionPlan{ + preferredMIMEType: v1.MediaTypeImageManifest, + preferredMIMETypeNeedsConversion: false, + otherMIMETypeCandidates: []string{}, + }, + }, + { + "s2→OCI forced", + determineManifestConversionInputs{ + srcMIMEType: manifest.DockerV2Schema2MediaType, + destSupportedManifestMIMETypes: supportS1S2OCI, + forceManifestMIMEType: v1.MediaTypeImageManifest, + }, + manifestConversionPlan{ + preferredMIMEType: v1.MediaTypeImageManifest, + preferredMIMETypeNeedsConversion: true, + otherMIMETypeCandidates: []string{}, + }, + }, + // forceManifestMIMEType to a type that does not support encryption + { + "OCI→s2 forced", + determineManifestConversionInputs{ + srcMIMEType: v1.MediaTypeImageManifest, + destSupportedManifestMIMETypes: supportS1S2OCI, + forceManifestMIMEType: manifest.DockerV2Schema2MediaType, + }, + manifestConversionPlan{}, + }, + { + "s2→s2 forced", + determineManifestConversionInputs{ + srcMIMEType: manifest.DockerV2Schema2MediaType, + destSupportedManifestMIMETypes: supportS1S2OCI, + forceManifestMIMEType: manifest.DockerV2Schema2MediaType, + }, + manifestConversionPlan{}, + }, + } { + in := c.in + in.requiresOCIEncryption = true + res, err := determineManifestConversion(in) + if c.expected.preferredMIMEType != "" { + require.NoError(t, err, c.description) + assert.Equal(t, c.expected, res, c.description) + } else { + assert.Error(t, err, c.description) + } + } +} + +// fakeUnparsedImage is an implementation of types.UnparsedImage which only returns itself as a MIME type in Manifest, +// except that "" means “reading the manifest should fail” +type fakeUnparsedImage struct { + mocks.ForbiddenUnparsedImage + mt string +} + +func (f fakeUnparsedImage) Manifest(ctx context.Context) ([]byte, string, error) { + if f.mt == "" { + return nil, "", errors.New("Manifest() directed to fail") + } + return nil, f.mt, nil +} + +func TestIsMultiImage(t *testing.T) { + // MIME type is available; more or less a smoke test, other cases are handled in manifest.MIMETypeIsMultiImage + for _, c := range []struct { + mt string + expected bool + }{ + {manifest.DockerV2ListMediaType, true}, + {manifest.DockerV2Schema2MediaType, false}, + {v1.MediaTypeImageManifest, false}, + {v1.MediaTypeImageIndex, true}, + } { + src := fakeUnparsedImage{mocks.ForbiddenUnparsedImage{}, c.mt} + res, err := isMultiImage(context.Background(), src) + require.NoError(t, err) + assert.Equal(t, c.expected, res, c.mt) + } + + // Error getting manifest MIME type + src := fakeUnparsedImage{mocks.ForbiddenUnparsedImage{}, ""} + _, err := isMultiImage(context.Background(), src) + assert.Error(t, err) +} + +func TestDetermineManifestListConversion(t *testing.T) { + supportS1S2OCI := []string{ + v1.MediaTypeImageIndex, + v1.MediaTypeImageManifest, + manifest.DockerV2ListMediaType, + manifest.DockerV2Schema2MediaType, + manifest.DockerV2Schema1SignedMediaType, + manifest.DockerV2Schema1MediaType, + } + supportS1S2 := []string{ + manifest.DockerV2ListMediaType, + manifest.DockerV2Schema2MediaType, + manifest.DockerV2Schema1SignedMediaType, + manifest.DockerV2Schema1MediaType, + } + supportOnlyOCI := []string{ + v1.MediaTypeImageIndex, + v1.MediaTypeImageManifest, + } + supportOnlyS1 := []string{ + manifest.DockerV2Schema1SignedMediaType, + manifest.DockerV2Schema1MediaType, + } + + cases := []struct { + description string + sourceType string + destTypes []string + expectedUpdate string + expectedOtherCandidates []string + }{ + // Destination accepts anything — try all variants + {"s2→anything", manifest.DockerV2ListMediaType, nil, "", []string{v1.MediaTypeImageIndex}}, + {"OCI→anything", v1.MediaTypeImageIndex, nil, "", []string{manifest.DockerV2ListMediaType}}, + // Destination accepts the unmodified original + {"s2→s1s2OCI", manifest.DockerV2ListMediaType, supportS1S2OCI, "", []string{v1.MediaTypeImageIndex}}, + {"OCI→s1s2OCI", v1.MediaTypeImageIndex, supportS1S2OCI, "", []string{manifest.DockerV2ListMediaType}}, + {"s2→s1s2", manifest.DockerV2ListMediaType, supportS1S2, "", []string{}}, + {"OCI→OCI", v1.MediaTypeImageIndex, supportOnlyOCI, "", []string{}}, + // Conversion necessary, try the preferred formats in order. + {"special→OCI", "unrecognized", supportS1S2OCI, v1.MediaTypeImageIndex, []string{manifest.DockerV2ListMediaType}}, + {"special→s2", "unrecognized", supportS1S2, manifest.DockerV2ListMediaType, []string{}}, + } + + for _, c := range cases { + copier := &copier{} + preferredMIMEType, otherCandidates, err := copier.determineListConversion(c.sourceType, c.destTypes, "") + require.NoError(t, err, c.description) + if c.expectedUpdate == "" { + assert.Equal(t, manifest.NormalizedMIMEType(c.sourceType), preferredMIMEType, c.description) + } else { + assert.Equal(t, c.expectedUpdate, preferredMIMEType, c.description) + } + assert.Equal(t, c.expectedOtherCandidates, otherCandidates, c.description) + } + + // With forceManifestMIMEType, the output is always the forced manifest type (in this case OCI index) + for _, c := range cases { + copier := &copier{} + preferredMIMEType, otherCandidates, err := copier.determineListConversion(c.sourceType, c.destTypes, v1.MediaTypeImageIndex) + require.NoError(t, err, c.description) + assert.Equal(t, v1.MediaTypeImageIndex, preferredMIMEType, c.description) + assert.Equal(t, []string{}, otherCandidates, c.description) + } + + // The destination doesn’t support list formats at all + copier := &copier{} + _, _, err := copier.determineListConversion(v1.MediaTypeImageIndex, supportOnlyS1, "") + assert.Error(t, err) +} diff --git a/vendor/github.com/containers/image/v5/copy/multiple.go b/vendor/github.com/containers/image/v5/copy/multiple.go index 41ea1b11b07..842225f587e 100644 --- a/vendor/github.com/containers/image/v5/copy/multiple.go +++ b/vendor/github.com/containers/image/v5/copy/multiple.go @@ -11,6 +11,8 @@ import ( "github.com/containers/image/v5/internal/image" internalManifest "github.com/containers/image/v5/internal/manifest" "github.com/containers/image/v5/manifest" + "github.com/containers/image/v5/pkg/compression" + compressionTypes "github.com/containers/image/v5/pkg/compression/types" "github.com/containers/image/v5/signature" digest "github.com/opencontainers/go-digest" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" @@ -26,12 +28,13 @@ const ( ) type instanceCopy struct { - op instanceCopyKind - sourceDigest digest.Digest + op instanceCopyKind + sourceDigest digest.Digest + compressionVariant CopyOptionCompressionVariant } // prepareInstanceCopies prepares a list of instances which needs to copied to the manifest list. -func prepareInstanceCopies(instanceDigests []digest.Digest, options *Options) []instanceCopy { +func prepareInstanceCopies(instanceDigests []digest.Digest, options *Options) ([]instanceCopy, error) { res := []instanceCopy{} for i, instanceDigest := range instanceDigests { if options.ImageListSelection == CopySpecificImages && @@ -39,12 +42,28 @@ func prepareInstanceCopies(instanceDigests []digest.Digest, options *Options) [] logrus.Debugf("Skipping instance %s (%d/%d)", instanceDigest, i+1, len(instanceDigests)) continue } + for _, compressionVariant := range options.EnsureCompressionVariantExists { + switch compressionVariant.Algorithm.Name() { + case compressionTypes.ZstdAlgorithmName: + res = append(res, instanceCopy{ + op: instanceCopyClone, + sourceDigest: instanceDigest, + compressionVariant: compressionVariant, + }) + case compressionTypes.GzipAlgorithmName: + continue + default: + return res, fmt.Errorf("unsupported compression algorithm for instance %s: %s", instanceDigest, compressionVariant.Algorithm.Name()) + } + } + defaultCompressionVariant := CopyOptionCompressionVariant{Algorithm: &compression.Gzip, Level: -1} res = append(res, instanceCopy{ - op: instanceCopyCopy, - sourceDigest: instanceDigest, + op: instanceCopyCopy, + sourceDigest: instanceDigest, + compressionVariant: defaultCompressionVariant, }) } - return res + return res, nil } // copyMultipleImages copies some or all of an image list's instances, using @@ -119,7 +138,10 @@ func (c *copier) copyMultipleImages(ctx context.Context, policyContext *signatur // Copy each image, or just the ones we want to copy, in turn. instanceDigests := updatedList.Instances() instanceEdits := []internalManifest.ListEdit{} - instanceCopyList := prepareInstanceCopies(instanceDigests, options) + instanceCopyList, err := prepareInstanceCopies(instanceDigests, options) + if err != nil { + return nil, fmt.Errorf("while preparing instances for copy: %w", err) + } c.Printf("Copying %d of %d images in list\n", len(instanceCopyList), len(instanceDigests)) for i, instance := range instanceCopyList { // Update instances to be edited by their `ListOperation` and @@ -129,7 +151,7 @@ func (c *copier) copyMultipleImages(ctx context.Context, policyContext *signatur logrus.Debugf("Copying instance %s (%d/%d)", instance.sourceDigest, i+1, len(instanceCopyList)) c.Printf("Copying image %s (%d/%d)\n", instance.sourceDigest, i+1, len(instanceCopyList)) unparsedInstance := image.UnparsedInstance(c.rawSource, &instanceCopyList[i].sourceDigest) - updatedManifest, updatedManifestType, updatedManifestDigest, err := c.copySingleImage(ctx, policyContext, options, unparsedToplevel, unparsedInstance, &instanceCopyList[i].sourceDigest) + updatedManifest, updatedManifestType, updatedManifestDigest, err := c.copySingleImage(ctx, policyContext, options, unparsedToplevel, unparsedInstance, &instanceCopyList[i].sourceDigest, instance.compressionVariant.Algorithm) if err != nil { return nil, fmt.Errorf("copying image %d/%d from manifest list: %w", i+1, len(instanceCopyList), err) } @@ -140,8 +162,34 @@ func (c *copier) copyMultipleImages(ctx context.Context, policyContext *signatur UpdateDigest: updatedManifestDigest, UpdateSize: int64(len(updatedManifest)), UpdateMediaType: updatedManifestType}) - default: - return nil, fmt.Errorf("copying image: invalid copy operation %d", instance.op) + case instanceCopyClone: + c.Printf("Replicating image %s (%d/%d)\n", instance.sourceDigest, i+1, len(instanceCopyList)) + unparsedInstance := image.UnparsedInstance(c.rawSource, &instanceCopyList[i].sourceDigest) + instanceDetails, err := updatedList.Instance(instanceCopyList[i].sourceDigest) + if err != nil { + return nil, fmt.Errorf("Replicating image %d/%d from manifest list: %w", i+1, len(instanceCopyList), err) + } + addCompressionAlgorithms := []compressionTypes.Algorithm{} + optionsClone := options + if instance.compressionVariant.Algorithm != nil { + // copy options with new compression format + optionsClone.DestinationCtx.CompressionFormat = instance.compressionVariant.Algorithm + optionsClone.DestinationCtx.CompressionLevel = &instance.compressionVariant.Level + addCompressionAlgorithms = append(addCompressionAlgorithms, *instance.compressionVariant.Algorithm) + } + addManifest, addManifestType, addManifestDigest, err := c.copySingleImage(ctx, policyContext, optionsClone, unparsedToplevel, unparsedInstance, &instanceCopyList[i].sourceDigest, instance.compressionVariant.Algorithm) + if err != nil { + return nil, fmt.Errorf("Replicating image %d/%d from manifest list: %w", i+1, len(instanceCopyList), err) + } + // Record the result of a possible conversion here. + instanceEdits = append(instanceEdits, internalManifest.ListEdit{ + ListOperation: internalManifest.ListOpAdd, + AddDigest: addManifestDigest, + AddSize: int64(len(addManifest)), + AddMediaType: addManifestType, + AddPlatform: instanceDetails.Platform, + AddCompressionAlgorithms: addCompressionAlgorithms, + }) } } diff --git a/vendor/github.com/containers/image/v5/copy/multiple_test.go b/vendor/github.com/containers/image/v5/copy/multiple_test.go new file mode 100644 index 00000000000..2c3052b55f5 --- /dev/null +++ b/vendor/github.com/containers/image/v5/copy/multiple_test.go @@ -0,0 +1,38 @@ +package copy + +import ( + "testing" + + digest "github.com/opencontainers/go-digest" + "github.com/stretchr/testify/assert" +) + +func TestPrepareCopyInstances(t *testing.T) { + // Test CopyAllImages + sourceInstances := []digest.Digest{ + digest.Digest("sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270"), + digest.Digest("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + digest.Digest("sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"), + } + + instancesToCopy, _ := prepareInstanceCopies(sourceInstances, &Options{}) + compare := []instanceCopy{} + noCompressionVariant := CopyOptionCompressionVariant{Algorithm: nil, Level: -1} + for _, instance := range sourceInstances { + compare = append(compare, instanceCopy{op: instanceCopyCopy, + sourceDigest: instance, compressionVariant: noCompressionVariant}) + } + assert.Equal(t, instancesToCopy, compare) + + // Test with CopySpecific Images + copyOnly := []digest.Digest{ + digest.Digest("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + } + instancesToCopy, _ = prepareInstanceCopies(sourceInstances, &Options{ + Instances: copyOnly, + ImageListSelection: CopySpecificImages}) + assert.Equal(t, instancesToCopy, []instanceCopy{{ + op: instanceCopyCopy, + sourceDigest: copyOnly[0], + compressionVariant: noCompressionVariant}}) +} diff --git a/vendor/github.com/containers/image/v5/copy/progress_bars.go b/vendor/github.com/containers/image/v5/copy/progress_bars.go index 25f2463630e..ce078234cb6 100644 --- a/vendor/github.com/containers/image/v5/copy/progress_bars.go +++ b/vendor/github.com/containers/image/v5/copy/progress_bars.go @@ -84,6 +84,8 @@ func (c *copier) createProgressBar(pool *mpb.Progress, partial bool, info types. ), mpb.AppendDecorators( decor.OnComplete(decor.CountersKibiByte("%.1f / %.1f"), ""), + decor.Name(" | "), + decor.OnComplete(decor.EwmaSpeed(decor.SizeB1024(0), "% .1f", 30), ""), ), ) } @@ -94,6 +96,9 @@ func (c *copier) createProgressBar(pool *mpb.Progress, partial bool, info types. mpb.PrependDecorators( decor.OnComplete(decor.Name(prefix), onComplete), ), + mpb.AppendDecorators( + decor.OnComplete(decor.EwmaSpeed(decor.SizeB1024(0), "% .1f", 30), ""), + ), ) } return &progressBar{ diff --git a/vendor/github.com/containers/image/v5/copy/progress_channel_test.go b/vendor/github.com/containers/image/v5/copy/progress_channel_test.go new file mode 100644 index 00000000000..86f31c894f9 --- /dev/null +++ b/vendor/github.com/containers/image/v5/copy/progress_channel_test.go @@ -0,0 +1,80 @@ +package copy + +import ( + "bytes" + "io" + "testing" + "time" + + "github.com/containers/image/v5/types" + "github.com/stretchr/testify/assert" +) + +func newSUT( + t *testing.T, + reader io.Reader, + duration time.Duration, + channel chan types.ProgressProperties, +) *progressReader { + artifact := types.BlobInfo{Size: 10} + + go func() { + res := <-channel + assert.Equal(t, res.Event, types.ProgressEventNewArtifact) + assert.Equal(t, res.Artifact, artifact) + }() + res := newProgressReader(reader, channel, duration, artifact) + + return res +} + +func TestNewProgressReader(t *testing.T) { + // Given + channel := make(chan types.ProgressProperties) + sut := newSUT(t, nil, time.Second, channel) + assert.NotNil(t, sut) + + // When/Then + go func() { + res := <-channel + assert.Equal(t, res.Event, types.ProgressEventDone) + }() + sut.reportDone() +} + +func TestReadWithoutEvent(t *testing.T) { + // Given + channel := make(chan types.ProgressProperties) + reader := bytes.NewReader([]byte{0, 1, 2}) + sut := newSUT(t, reader, time.Second, channel) + assert.NotNil(t, sut) + + // When + b := []byte{0, 1, 2, 3, 4} + read, err := reader.Read(b) + + // Then + assert.Nil(t, err) + assert.Equal(t, read, 3) +} + +func TestReadWithEvent(t *testing.T) { + // Given + channel := make(chan types.ProgressProperties) + reader := bytes.NewReader([]byte{0, 1, 2, 3, 4, 5, 6}) + sut := newSUT(t, reader, time.Nanosecond, channel) + assert.NotNil(t, sut) + b := []byte{0, 1, 2, 3, 4} + + // When/Then + go func() { + res := <-channel + assert.Equal(t, res.Event, types.ProgressEventRead) + assert.Equal(t, res.Offset, uint64(5)) + assert.Equal(t, res.OffsetUpdate, uint64(5)) + }() + read, err := reader.Read(b) + assert.Equal(t, read, 5) + assert.Nil(t, err) + +} diff --git a/vendor/github.com/containers/image/v5/copy/sign_test.go b/vendor/github.com/containers/image/v5/copy/sign_test.go new file mode 100644 index 00000000000..c0733dee832 --- /dev/null +++ b/vendor/github.com/containers/image/v5/copy/sign_test.go @@ -0,0 +1,162 @@ +package copy + +import ( + "context" + "errors" + "io" + "testing" + + "github.com/containers/image/v5/directory" + "github.com/containers/image/v5/docker" + "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/internal/imagedestination" + internalsig "github.com/containers/image/v5/internal/signature" + internalSigner "github.com/containers/image/v5/internal/signer" + "github.com/containers/image/v5/signature/signer" + "github.com/containers/image/v5/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// stubSignerImpl is a signer.SigningImplementation that allows us to check the signed identity, without the overhead of actually signing. +// We abuse internalsig.Sigstore to store the signed manifest and identity in the payload and MIME type fields, respectively. +type stubSignerImpl struct { + signingFailure error // if set, SignImageManifest returns this +} + +func (s *stubSignerImpl) ProgressMessage() string { + return "Signing with stubSigner" +} + +func (s *stubSignerImpl) SignImageManifest(ctx context.Context, m []byte, dockerReference reference.Named) (internalsig.Signature, error) { + if s.signingFailure != nil { + return nil, s.signingFailure + } + return internalsig.SigstoreFromComponents(dockerReference.String(), m, nil), nil +} + +func (s *stubSignerImpl) Close() error { + return nil +} + +func TestCreateSignatures(t *testing.T) { + stubSigner := internalSigner.NewSigner(&stubSignerImpl{}) + defer stubSigner.Close() + + manifestBlob := []byte("Something") + // Set up dir: and docker: destinations + tempDir := t.TempDir() + dirRef, err := directory.NewReference(tempDir) + require.NoError(t, err) + dirDest, err := dirRef.NewImageDestination(context.Background(), nil) + require.NoError(t, err) + defer dirDest.Close() + dockerRef, err := docker.ParseReference("//busybox") + require.NoError(t, err) + dockerDest, err := dockerRef.NewImageDestination(context.Background(), + &types.SystemContext{RegistriesDirPath: "/this/does/not/exist", DockerPerHostCertDirPath: "/this/does/not/exist"}) + require.NoError(t, err) + defer dockerDest.Close() + + workingOptions := Options{Signers: []*signer.Signer{stubSigner}} + for _, cc := range []struct { + name string + dest types.ImageDestination + options *Options + identity string + successWithNoSigs bool + successfullySignedIdentity string // Set to expect a successful signing with workingOptions + }{ + { + name: "signing fails", + dest: dockerDest, + options: &Options{ + Signers: []*signer.Signer{ + internalSigner.NewSigner(&stubSignerImpl{signingFailure: errors.New("fails")}), + }, + }, + }, + { + name: "second signing fails", + dest: dockerDest, + options: &Options{ + Signers: []*signer.Signer{ + stubSigner, + internalSigner.NewSigner(&stubSignerImpl{signingFailure: errors.New("fails")}), + }, + }, + }, + { + name: "not a full reference", + dest: dockerDest, + identity: "myregistry.io/myrepo", + }, + { + name: "dir: with no identity specified, but no signing request", + dest: dirDest, + options: &Options{}, + successWithNoSigs: true, + }, + + { + name: "dir: with no identity specified", + dest: dirDest, + identity: "", + }, + { + name: "dir: with overridden identity", + dest: dirDest, + identity: "myregistry.io/myrepo:mytag", + successfullySignedIdentity: "myregistry.io/myrepo:mytag", + }, + { + name: "docker:// without overriding the identity", + dest: dockerDest, + identity: "", + successfullySignedIdentity: "docker.io/library/busybox:latest", + }, + { + name: "docker:// with overidden identity", + dest: dockerDest, + identity: "myregistry.io/myrepo:mytag", + successfullySignedIdentity: "myregistry.io/myrepo:mytag", + }, + } { + var identity reference.Named = nil + if cc.identity != "" { + i, err := reference.ParseNormalizedNamed(cc.identity) + require.NoError(t, err, cc.name) + identity = i + } + options := cc.options + if options == nil { + options = &workingOptions + } + + c := &copier{ + dest: imagedestination.FromPublic(cc.dest), + reportWriter: io.Discard, + } + defer c.close() + err := c.setupSigners(options) + require.NoError(t, err, cc.name) + sigs, err := c.createSignatures(context.Background(), manifestBlob, identity) + switch { + case cc.successfullySignedIdentity != "": + require.NoError(t, err, cc.name) + require.Len(t, sigs, 1, cc.name) + stubSig, ok := sigs[0].(internalsig.Sigstore) + require.True(t, ok, cc.name) + // Compare how stubSignerImpl.SignImageManifest stuffs the signing parameters into these fields. + assert.Equal(t, manifestBlob, stubSig.UntrustedPayload(), cc.name) + assert.Equal(t, cc.successfullySignedIdentity, stubSig.UntrustedMIMEType(), cc.name) + + case cc.successWithNoSigs: + require.NoError(t, err, cc.name) + require.Empty(t, sigs, cc.name) + + default: + assert.Error(t, err, cc.name) + } + } +} diff --git a/vendor/github.com/containers/image/v5/copy/single.go b/vendor/github.com/containers/image/v5/copy/single.go index b8569a70c06..caae05de155 100644 --- a/vendor/github.com/containers/image/v5/copy/single.go +++ b/vendor/github.com/containers/image/v5/copy/single.go @@ -43,7 +43,7 @@ type imageCopier struct { // copySingleImage copies a single (non-manifest-list) image unparsedImage, using policyContext to validate // source image admissibility. -func (c *copier) copySingleImage(ctx context.Context, policyContext *signature.PolicyContext, options *Options, unparsedToplevel, unparsedImage *image.UnparsedImage, targetInstance *digest.Digest) (retManifest []byte, retManifestType string, retManifestDigest digest.Digest, retErr error) { +func (c *copier) copySingleImage(ctx context.Context, policyContext *signature.PolicyContext, options *Options, unparsedToplevel, unparsedImage *image.UnparsedImage, targetInstance *digest.Digest, requiredCompression *compression.Algorithm) (retManifest []byte, retManifestType string, retManifestDigest digest.Digest, retErr error) { // The caller is handling manifest lists; this could happen only if a manifest list contains a manifest list. // Make sure we fail cleanly in such cases. multiImage, err := isMultiImage(ctx, unparsedImage) @@ -188,7 +188,7 @@ func (c *copier) copySingleImage(ctx context.Context, policyContext *signature.P } } - if err := ic.copyLayers(ctx); err != nil { + if err := ic.copyLayers(ctx, requiredCompression); err != nil { return nil, "", "", err } @@ -336,6 +336,7 @@ func compareImageDestinationManifestEqual(ctx context.Context, options *Options, logrus.Debugf("Unable to create destination image %s source: %v", dest.Reference(), err) return false, nil, "", "", nil } + defer destImageSource.Close() destManifest, destManifestType, err := destImageSource.GetManifest(ctx, targetInstance) if err != nil { @@ -358,7 +359,7 @@ func compareImageDestinationManifestEqual(ctx context.Context, options *Options, } // copyLayers copies layers from ic.src/ic.c.rawSource to dest, using and updating ic.manifestUpdates if necessary and ic.cannotModifyManifestReason == "". -func (ic *imageCopier) copyLayers(ctx context.Context) error { +func (ic *imageCopier) copyLayers(ctx context.Context, requiredCompression *compression.Algorithm) error { srcInfos := ic.src.LayerInfos() numLayers := len(srcInfos) updatedSrcInfos, err := ic.src.LayerInfosForCopy(ctx) @@ -407,7 +408,7 @@ func (ic *imageCopier) copyLayers(ctx context.Context) error { logrus.Debugf("Skipping foreign layer %q copy to %s", cld.destInfo.Digest, ic.c.dest.Reference().Transport().Name()) } } else { - cld.destInfo, cld.diffID, cld.err = ic.copyLayer(ctx, srcLayer, toEncrypt, pool, index, srcRef, manifestLayerInfos[index].EmptyLayer) + cld.destInfo, cld.diffID, cld.err = ic.copyLayer(ctx, srcLayer, toEncrypt, pool, index, srcRef, manifestLayerInfos[index].EmptyLayer, requiredCompression) } data[index] = cld } @@ -580,7 +581,7 @@ type diffIDResult struct { // copyLayer copies a layer with srcInfo (with known Digest and Annotations and possibly known Size) in src to dest, perhaps (de/re/)compressing it, // and returns a complete blobInfo of the copied layer, and a value for LayerDiffIDs if diffIDIsNeeded // srcRef can be used as an additional hint to the destination during checking whether a layer can be reused but srcRef can be nil. -func (ic *imageCopier) copyLayer(ctx context.Context, srcInfo types.BlobInfo, toEncrypt bool, pool *mpb.Progress, layerIndex int, srcRef reference.Named, emptyLayer bool) (types.BlobInfo, digest.Digest, error) { +func (ic *imageCopier) copyLayer(ctx context.Context, srcInfo types.BlobInfo, toEncrypt bool, pool *mpb.Progress, layerIndex int, srcRef reference.Named, emptyLayer bool, requiredCompression *compression.Algorithm) (types.BlobInfo, digest.Digest, error) { // If the srcInfo doesn't contain compression information, try to compute it from the // MediaType, which was either read from a manifest by way of LayerInfos() or constructed // by LayerInfosForCopy(), if it was supplied at all. If we succeed in copying the blob, @@ -624,11 +625,12 @@ func (ic *imageCopier) copyLayer(ctx context.Context, srcInfo types.BlobInfo, to // Fixing that will probably require passing more information to TryReusingBlob() than the current version of // the ImageDestination interface lets us pass in. reused, reusedBlob, err := ic.c.dest.TryReusingBlobWithOptions(ctx, srcInfo, private.TryReusingBlobOptions{ - Cache: ic.c.blobInfoCache, - CanSubstitute: canSubstitute, - EmptyLayer: emptyLayer, - LayerIndex: &layerIndex, - SrcRef: srcRef, + Cache: ic.c.blobInfoCache, + CanSubstitute: canSubstitute, + EmptyLayer: emptyLayer, + LayerIndex: &layerIndex, + SrcRef: srcRef, + RequiredCompression: requiredCompression, }) if err != nil { return types.BlobInfo{}, "", fmt.Errorf("trying to reuse blob %s at destination: %w", srcInfo.Digest, err) diff --git a/vendor/github.com/containers/image/v5/copy/single_test.go b/vendor/github.com/containers/image/v5/copy/single_test.go new file mode 100644 index 00000000000..144b5ed2aac --- /dev/null +++ b/vendor/github.com/containers/image/v5/copy/single_test.go @@ -0,0 +1,141 @@ +package copy + +import ( + "bytes" + "errors" + "fmt" + "io" + "os" + "testing" + "time" + + "github.com/containers/image/v5/internal/private" + "github.com/containers/image/v5/pkg/compression" + compressiontypes "github.com/containers/image/v5/pkg/compression/types" + "github.com/containers/image/v5/types" + digest "github.com/opencontainers/go-digest" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUpdatedBlobInfoFromReuse(t *testing.T) { + srcInfo := types.BlobInfo{ + Digest: "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb", + Size: 51354364, + URLs: []string{"https://layer.url"}, + Annotations: map[string]string{"test-annotation-2": "two"}, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + CompressionOperation: types.Compress, // Might be set by blobCacheSource.LayerInfosForCopy + CompressionAlgorithm: &compression.Gzip, // Set e.g. in copyLayer + // CryptoOperation is not set by LayerInfos() + } + + for _, c := range []struct { + reused private.ReusedBlob + expected types.BlobInfo + }{ + { // A straightforward reuse without substitution + reused: private.ReusedBlob{ + Digest: "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb", + Size: 51354364, + // CompressionOperation not set + // CompressionAlgorithm not set + }, + expected: types.BlobInfo{ + Digest: "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb", + Size: 51354364, + URLs: nil, + Annotations: map[string]string{"test-annotation-2": "two"}, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + CompressionOperation: types.Compress, // Might be set by blobCacheSource.LayerInfosForCopy + CompressionAlgorithm: &compression.Gzip, // Set e.g. in copyLayer + // CryptoOperation is set to the zero value + }, + }, + { // Reuse with substitution + reused: private.ReusedBlob{ + Digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + Size: 513543640, + CompressionOperation: types.Decompress, + CompressionAlgorithm: nil, + }, + expected: types.BlobInfo{ + Digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + Size: 513543640, + URLs: nil, + Annotations: map[string]string{"test-annotation-2": "two"}, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + CompressionOperation: types.Decompress, + CompressionAlgorithm: nil, + // CryptoOperation is set to the zero value + }, + }, + } { + res := updatedBlobInfoFromReuse(srcInfo, c.reused) + assert.Equal(t, c.expected, res, fmt.Sprintf("%#v", c.reused)) + } +} + +func goDiffIDComputationGoroutineWithTimeout(layerStream io.ReadCloser, decompressor compressiontypes.DecompressorFunc) *diffIDResult { + ch := make(chan diffIDResult) + go diffIDComputationGoroutine(ch, layerStream, decompressor) + timeout := time.After(time.Second) + select { + case res := <-ch: + return &res + case <-timeout: + return nil + } +} + +func TestDiffIDComputationGoroutine(t *testing.T) { + stream, err := os.Open("fixtures/Hello.uncompressed") + require.NoError(t, err) + res := goDiffIDComputationGoroutineWithTimeout(stream, nil) + require.NotNil(t, res) + assert.NoError(t, res.err) + assert.Equal(t, "sha256:185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969", res.digest.String()) + + // Error reading input + reader, writer := io.Pipe() + err = writer.CloseWithError(errors.New("Expected error reading input in diffIDComputationGoroutine")) + require.NoError(t, err) + res = goDiffIDComputationGoroutineWithTimeout(reader, nil) + require.NotNil(t, res) + assert.Error(t, res.err) +} + +func TestComputeDiffID(t *testing.T) { + for _, c := range []struct { + filename string + decompressor compressiontypes.DecompressorFunc + result digest.Digest + }{ + {"fixtures/Hello.uncompressed", nil, "sha256:185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969"}, + {"fixtures/Hello.gz", nil, "sha256:0bd4409dcd76476a263b8f3221b4ce04eb4686dec40bfdcc2e86a7403de13609"}, + {"fixtures/Hello.gz", compression.GzipDecompressor, "sha256:185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969"}, + {"fixtures/Hello.zst", nil, "sha256:361a8e0372ad438a0316eb39a290318364c10b60d0a7e55b40aa3eafafc55238"}, + {"fixtures/Hello.zst", compression.ZstdDecompressor, "sha256:185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969"}, + } { + stream, err := os.Open(c.filename) + require.NoError(t, err, c.filename) + defer stream.Close() + + diffID, err := computeDiffID(stream, c.decompressor) + require.NoError(t, err, c.filename) + assert.Equal(t, c.result, diffID) + } + + // Error initializing decompression + _, err := computeDiffID(bytes.NewReader([]byte{}), compression.GzipDecompressor) + assert.Error(t, err) + + // Error reading input + reader, writer := io.Pipe() + defer reader.Close() + err = writer.CloseWithError(errors.New("Expected error reading input in computeDiffID")) + require.NoError(t, err) + _, err = computeDiffID(reader, nil) + assert.Error(t, err) +} diff --git a/vendor/github.com/containers/image/v5/directory/directory_dest.go b/vendor/github.com/containers/image/v5/directory/directory_dest.go index 974d23d5faf..222723a8f53 100644 --- a/vendor/github.com/containers/image/v5/directory/directory_dest.go +++ b/vendor/github.com/containers/image/v5/directory/directory_dest.go @@ -190,6 +190,9 @@ func (d *dirImageDestination) PutBlobWithOptions(ctx context.Context, stream io. // If the blob has been successfully reused, returns (true, info, nil). // If the transport can not reuse the requested blob, TryReusingBlob returns (false, {}, nil); it returns a non-nil error only on an unexpected failure. func (d *dirImageDestination) TryReusingBlobWithOptions(ctx context.Context, info types.BlobInfo, options private.TryReusingBlobOptions) (bool, private.ReusedBlob, error) { + if !impl.OriginalBlobMatchesRequiredCompression(options) { + return false, private.ReusedBlob{}, nil + } if info.Digest == "" { return false, private.ReusedBlob{}, fmt.Errorf("Can not check for a blob with unknown digest") } diff --git a/vendor/github.com/containers/image/v5/directory/directory_test.go b/vendor/github.com/containers/image/v5/directory/directory_test.go new file mode 100644 index 00000000000..8c80262ad45 --- /dev/null +++ b/vendor/github.com/containers/image/v5/directory/directory_test.go @@ -0,0 +1,209 @@ +package directory + +import ( + "bytes" + "context" + "errors" + "io" + "os" + "testing" + + "github.com/containers/image/v5/internal/private" + "github.com/containers/image/v5/manifest" + "github.com/containers/image/v5/pkg/blobinfocache/memory" + "github.com/containers/image/v5/types" + "github.com/opencontainers/go-digest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var _ private.ImageSource = (*dirImageSource)(nil) +var _ private.ImageDestination = (*dirImageDestination)(nil) + +func TestDestinationReference(t *testing.T) { + ref, tmpDir := refToTempDir(t) + + dest, err := ref.NewImageDestination(context.Background(), nil) + require.NoError(t, err) + defer dest.Close() + ref2 := dest.Reference() + assert.Equal(t, tmpDir, ref2.StringWithinTransport()) +} + +func TestGetPutManifest(t *testing.T) { + ref, _ := refToTempDir(t) + + man := []byte("test-manifest") + list := []byte("test-manifest-list") + md, err := manifest.Digest(man) + require.NoError(t, err) + dest, err := ref.NewImageDestination(context.Background(), nil) + require.NoError(t, err) + defer dest.Close() + err = dest.PutManifest(context.Background(), man, &md) + assert.NoError(t, err) + err = dest.PutManifest(context.Background(), list, nil) + assert.NoError(t, err) + err = dest.Commit(context.Background(), nil) // nil unparsedToplevel is invalid, we don’t currently use the value + assert.NoError(t, err) + + src, err := ref.NewImageSource(context.Background(), nil) + require.NoError(t, err) + defer src.Close() + m, mt, err := src.GetManifest(context.Background(), nil) + assert.NoError(t, err) + assert.Equal(t, list, m) + assert.Equal(t, "", mt) + + m, mt, err = src.GetManifest(context.Background(), &md) + assert.NoError(t, err) + assert.Equal(t, man, m) + assert.Equal(t, "", mt) +} + +func TestGetPutBlob(t *testing.T) { + computedBlob := []byte("test-blob") + providedBlob := []byte("provided-blob") + providedDigest := digest.Digest("sha256:provided-test-digest") + + ref, _ := refToTempDir(t) + cache := memory.New() + + dest, err := ref.NewImageDestination(context.Background(), nil) + require.NoError(t, err) + defer dest.Close() + assert.Equal(t, types.PreserveOriginal, dest.DesiredLayerCompression()) + // PutBlob with caller-provided data + providedInfo, err := dest.PutBlob(context.Background(), bytes.NewReader(providedBlob), types.BlobInfo{Digest: providedDigest, Size: int64(len(providedBlob))}, cache, false) + assert.NoError(t, err) + assert.Equal(t, int64(len(providedBlob)), providedInfo.Size) + assert.Equal(t, providedDigest, providedInfo.Digest) + // PutBlob with unknown data + computedInfo, err := dest.PutBlob(context.Background(), bytes.NewReader(computedBlob), types.BlobInfo{Digest: "", Size: int64(-1)}, cache, false) + assert.NoError(t, err) + assert.Equal(t, int64(len(computedBlob)), computedInfo.Size) + assert.Equal(t, digest.FromBytes(computedBlob), computedInfo.Digest) + err = dest.Commit(context.Background(), nil) // nil unparsedToplevel is invalid, we don’t currently use the value + assert.NoError(t, err) + + src, err := ref.NewImageSource(context.Background(), nil) + require.NoError(t, err) + defer src.Close() + for digest, expectedBlob := range map[digest.Digest][]byte{ + providedInfo.Digest: providedBlob, + computedInfo.Digest: computedBlob, + } { + rc, size, err := src.GetBlob(context.Background(), types.BlobInfo{Digest: digest, Size: int64(len(expectedBlob))}, cache) + assert.NoError(t, err) + defer rc.Close() + b, err := io.ReadAll(rc) + assert.NoError(t, err) + assert.Equal(t, expectedBlob, b) + assert.Equal(t, int64(len(expectedBlob)), size) + } +} + +// readerFromFunc allows implementing Reader by any function, e.g. a closure. +type readerFromFunc func([]byte) (int, error) + +func (fn readerFromFunc) Read(p []byte) (int, error) { + return fn(p) +} + +// TestPutBlobDigestFailure simulates behavior on digest verification failure. +func TestPutBlobDigestFailure(t *testing.T) { + const digestErrorString = "Simulated digest error" + const blobDigest = digest.Digest("sha256:test-digest") + + ref, _ := refToTempDir(t) + dirRef, ok := ref.(dirReference) + require.True(t, ok) + blobPath := dirRef.layerPath(blobDigest) + cache := memory.New() + + firstRead := true + reader := readerFromFunc(func(p []byte) (int, error) { + _, err := os.Lstat(blobPath) + require.Error(t, err) + require.True(t, os.IsNotExist(err)) + if firstRead { + if len(p) > 0 { + firstRead = false + } + for i := 0; i < len(p); i++ { + p[i] = 0xAA + } + return len(p), nil + } + return 0, errors.New(digestErrorString) + }) + + dest, err := ref.NewImageDestination(context.Background(), nil) + require.NoError(t, err) + defer dest.Close() + _, err = dest.PutBlob(context.Background(), reader, types.BlobInfo{Digest: blobDigest, Size: -1}, cache, false) + assert.ErrorContains(t, err, digestErrorString) + err = dest.Commit(context.Background(), nil) // nil unparsedToplevel is invalid, we don’t currently use the value + assert.NoError(t, err) + + _, err = os.Lstat(blobPath) + require.Error(t, err) + require.True(t, os.IsNotExist(err)) +} + +func TestGetPutSignatures(t *testing.T) { + ref, _ := refToTempDir(t) + + man := []byte("test-manifest") + list := []byte("test-manifest-list") + md, err := manifest.Digest(man) + require.NoError(t, err) + // These signatures are completely invalid; start with 0xA3 just to be minimally plausible to signature.FromBlob. + signatures := [][]byte{ + []byte("\xA3sig1"), + []byte("\xA3sig2"), + } + listSignatures := [][]byte{ + []byte("\xA3sig3"), + []byte("\xA3sig4"), + } + + dest, err := ref.NewImageDestination(context.Background(), nil) + require.NoError(t, err) + defer dest.Close() + err = dest.SupportsSignatures(context.Background()) + assert.NoError(t, err) + + err = dest.PutManifest(context.Background(), man, &md) + require.NoError(t, err) + err = dest.PutManifest(context.Background(), list, nil) + require.NoError(t, err) + + err = dest.PutSignatures(context.Background(), signatures, &md) + assert.NoError(t, err) + err = dest.PutSignatures(context.Background(), listSignatures, nil) + assert.NoError(t, err) + err = dest.Commit(context.Background(), nil) // nil unparsedToplevel is invalid, we don’t currently use the value + assert.NoError(t, err) + + src, err := ref.NewImageSource(context.Background(), nil) + require.NoError(t, err) + defer src.Close() + sigs, err := src.GetSignatures(context.Background(), nil) + assert.NoError(t, err) + assert.Equal(t, listSignatures, sigs) + + sigs, err = src.GetSignatures(context.Background(), &md) + assert.NoError(t, err) + assert.Equal(t, signatures, sigs) +} + +func TestSourceReference(t *testing.T) { + ref, tmpDir := refToTempDir(t) + + src, err := ref.NewImageSource(context.Background(), nil) + require.NoError(t, err) + defer src.Close() + ref2 := src.Reference() + assert.Equal(t, tmpDir, ref2.StringWithinTransport()) +} diff --git a/vendor/github.com/containers/image/v5/directory/directory_transport_test.go b/vendor/github.com/containers/image/v5/directory/directory_transport_test.go new file mode 100644 index 00000000000..0ef96c4f683 --- /dev/null +++ b/vendor/github.com/containers/image/v5/directory/directory_transport_test.go @@ -0,0 +1,230 @@ +package directory + +import ( + "context" + "os" + "path/filepath" + "testing" + + _ "github.com/containers/image/v5/internal/testing/explicitfilepath-tmpdir" + "github.com/containers/image/v5/types" + digest "github.com/opencontainers/go-digest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTransportName(t *testing.T) { + assert.Equal(t, "dir", Transport.Name()) +} + +func TestTransportParseReference(t *testing.T) { + testNewReference(t, Transport.ParseReference) +} + +func TestTransportValidatePolicyConfigurationScope(t *testing.T) { + for _, scope := range []string{ + "/etc", + "/this/does/not/exist", + } { + err := Transport.ValidatePolicyConfigurationScope(scope) + assert.NoError(t, err, scope) + } + + for _, scope := range []string{ + "relative/path", + "/double//slashes", + "/has/./dot", + "/has/dot/../dot", + "/trailing/slash/", + "/", + } { + err := Transport.ValidatePolicyConfigurationScope(scope) + assert.Error(t, err, scope) + } +} + +func TestNewReference(t *testing.T) { + testNewReference(t, NewReference) +} + +// testNewReference is a test shared for Transport.ParseReference and NewReference. +func testNewReference(t *testing.T, fn func(string) (types.ImageReference, error)) { + tmpDir := t.TempDir() + + for _, path := range []string{ + "/", + "/etc", + tmpDir, + "relativepath", + tmpDir + "/thisdoesnotexist", + } { + ref, err := fn(path) + require.NoError(t, err, path) + dirRef, ok := ref.(dirReference) + require.True(t, ok) + assert.Equal(t, path, dirRef.path, path) + } + + _, err := fn(tmpDir + "/thisparentdoesnotexist/something") + assert.Error(t, err) +} + +// refToTempDir creates a temporary directory and returns a reference to it. +func refToTempDir(t *testing.T) (types.ImageReference, string) { + tmpDir := t.TempDir() + ref, err := NewReference(tmpDir) + require.NoError(t, err) + return ref, tmpDir +} + +func TestReferenceTransport(t *testing.T) { + ref, _ := refToTempDir(t) + assert.Equal(t, Transport, ref.Transport()) +} + +func TestReferenceStringWithinTransport(t *testing.T) { + ref, tmpDir := refToTempDir(t) + assert.Equal(t, tmpDir, ref.StringWithinTransport()) +} + +func TestReferenceDockerReference(t *testing.T) { + ref, _ := refToTempDir(t) + assert.Nil(t, ref.DockerReference()) +} + +func TestReferencePolicyConfigurationIdentity(t *testing.T) { + ref, tmpDir := refToTempDir(t) + + assert.Equal(t, tmpDir, ref.PolicyConfigurationIdentity()) + // A non-canonical path. Test just one, the various other cases are + // tested in explicitfilepath.ResolvePathToFullyExplicit. + ref, err := NewReference(tmpDir + "/.") + require.NoError(t, err) + assert.Equal(t, tmpDir, ref.PolicyConfigurationIdentity()) + + // "/" as a corner case. + ref, err = NewReference("/") + require.NoError(t, err) + assert.Equal(t, "/", ref.PolicyConfigurationIdentity()) +} + +func TestReferencePolicyConfigurationNamespaces(t *testing.T) { + ref, tmpDir := refToTempDir(t) + // We don't really know enough to make a full equality test here. + ns := ref.PolicyConfigurationNamespaces() + require.NotNil(t, ns) + assert.NotEmpty(t, ns) + assert.Equal(t, filepath.Dir(tmpDir), ns[0]) + + // Test with a known path which should exist. Test just one non-canonical + // path, the various other cases are tested in explicitfilepath.ResolvePathToFullyExplicit. + // + // It would be nice to test a deeper hierarchy, but it is not obvious what + // deeper path is always available in the various distros, AND is not likely + // to contains a symbolic link. + for _, path := range []string{"/usr/share", "/usr/share/./."} { + _, err := os.Lstat(path) + require.NoError(t, err) + ref, err := NewReference(path) + require.NoError(t, err) + ns := ref.PolicyConfigurationNamespaces() + require.NotNil(t, ns) + assert.Equal(t, []string{"/usr"}, ns) + } + + // "/" as a corner case. + ref, err := NewReference("/") + require.NoError(t, err) + assert.Equal(t, []string{}, ref.PolicyConfigurationNamespaces()) +} + +func TestReferenceNewImage(t *testing.T) { + ref, _ := refToTempDir(t) + + dest, err := ref.NewImageDestination(context.Background(), nil) + require.NoError(t, err) + defer dest.Close() + mFixture, err := os.ReadFile("../manifest/fixtures/v2s1.manifest.json") + require.NoError(t, err) + err = dest.PutManifest(context.Background(), mFixture, nil) + assert.NoError(t, err) + err = dest.Commit(context.Background(), nil) // nil unparsedToplevel is invalid, we don’t currently use the value + assert.NoError(t, err) + + img, err := ref.NewImage(context.Background(), nil) + assert.NoError(t, err) + defer img.Close() +} + +func TestReferenceNewImageNoValidManifest(t *testing.T) { + ref, _ := refToTempDir(t) + + dest, err := ref.NewImageDestination(context.Background(), nil) + require.NoError(t, err) + defer dest.Close() + err = dest.PutManifest(context.Background(), []byte(`{"schemaVersion":1}`), nil) + assert.NoError(t, err) + err = dest.Commit(context.Background(), nil) // nil unparsedToplevel is invalid, we don’t currently use the value + assert.NoError(t, err) + + _, err = ref.NewImage(context.Background(), nil) + assert.Error(t, err) +} + +func TestReferenceNewImageSource(t *testing.T) { + ref, _ := refToTempDir(t) + src, err := ref.NewImageSource(context.Background(), nil) + assert.NoError(t, err) + defer src.Close() +} + +func TestReferenceNewImageDestination(t *testing.T) { + ref, _ := refToTempDir(t) + dest, err := ref.NewImageDestination(context.Background(), nil) + assert.NoError(t, err) + defer dest.Close() +} + +func TestReferenceDeleteImage(t *testing.T) { + ref, _ := refToTempDir(t) + err := ref.DeleteImage(context.Background(), nil) + assert.Error(t, err) +} + +func TestReferenceManifestPath(t *testing.T) { + dhex := digest.Digest("sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") + + ref, tmpDir := refToTempDir(t) + dirRef, ok := ref.(dirReference) + require.True(t, ok) + assert.Equal(t, tmpDir+"/manifest.json", dirRef.manifestPath(nil)) + assert.Equal(t, tmpDir+"/"+dhex.Encoded()+".manifest.json", dirRef.manifestPath(&dhex)) +} + +func TestReferenceLayerPath(t *testing.T) { + const hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + ref, tmpDir := refToTempDir(t) + dirRef, ok := ref.(dirReference) + require.True(t, ok) + assert.Equal(t, tmpDir+"/"+hex, dirRef.layerPath("sha256:"+hex)) +} + +func TestReferenceSignaturePath(t *testing.T) { + dhex := digest.Digest("sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") + + ref, tmpDir := refToTempDir(t) + dirRef, ok := ref.(dirReference) + require.True(t, ok) + assert.Equal(t, tmpDir+"/signature-1", dirRef.signaturePath(0, nil)) + assert.Equal(t, tmpDir+"/signature-10", dirRef.signaturePath(9, nil)) + assert.Equal(t, tmpDir+"/"+dhex.Encoded()+".signature-1", dirRef.signaturePath(0, &dhex)) + assert.Equal(t, tmpDir+"/"+dhex.Encoded()+".signature-10", dirRef.signaturePath(9, &dhex)) +} + +func TestReferenceVersionPath(t *testing.T) { + ref, tmpDir := refToTempDir(t) + dirRef, ok := ref.(dirReference) + require.True(t, ok) + assert.Equal(t, tmpDir+"/version", dirRef.versionPath()) +} diff --git a/vendor/github.com/containers/image/v5/directory/explicitfilepath/path_test.go b/vendor/github.com/containers/image/v5/directory/explicitfilepath/path_test.go new file mode 100644 index 00000000000..7a4c24debaa --- /dev/null +++ b/vendor/github.com/containers/image/v5/directory/explicitfilepath/path_test.go @@ -0,0 +1,172 @@ +package explicitfilepath + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + _ "github.com/containers/image/v5/internal/testing/explicitfilepath-tmpdir" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type pathResolvingTestCase struct { + setup func(*testing.T, string) string + expected string +} + +var testCases = []pathResolvingTestCase{ + { // A straightforward subdirectory hierarchy + func(t *testing.T, top string) string { + err := os.MkdirAll(filepath.Join(top, "dir1/dir2/dir3"), 0755) + require.NoError(t, err) + return "dir1/dir2/dir3" + }, + "dir1/dir2/dir3", + }, + { // Missing component + func(t *testing.T, top string) string { + return "thisismissing/dir2" + }, + "", + }, + { // Symlink on the path + func(t *testing.T, top string) string { + err := os.MkdirAll(filepath.Join(top, "dir1/dir2"), 0755) + require.NoError(t, err) + err = os.Symlink("dir1", filepath.Join(top, "link1")) + require.NoError(t, err) + return "link1/dir2" + }, + "dir1/dir2", + }, + { // Trailing symlink + func(t *testing.T, top string) string { + err := os.MkdirAll(filepath.Join(top, "dir1/dir2"), 0755) + require.NoError(t, err) + err = os.Symlink("dir2", filepath.Join(top, "dir1/link2")) + require.NoError(t, err) + return "dir1/link2" + }, + "dir1/dir2", + }, + { // Symlink pointing nowhere, as a non-final component + func(t *testing.T, top string) string { + err := os.Symlink("thisismissing", filepath.Join(top, "link1")) + require.NoError(t, err) + return "link1/dir2" + }, + "", + }, + { // Trailing symlink pointing nowhere (but note that a missing non-symlink would be accepted) + func(t *testing.T, top string) string { + err := os.Symlink("thisismissing", filepath.Join(top, "link1")) + require.NoError(t, err) + return "link1" + }, + "", + }, + { // Relative components in a path + func(t *testing.T, top string) string { + err := os.MkdirAll(filepath.Join(top, "dir1/dir2/dir3"), 0755) + require.NoError(t, err) + return "dir1/./dir2/../dir2/dir3" + }, + "dir1/dir2/dir3", + }, + { // Trailing relative components + func(t *testing.T, top string) string { + err := os.MkdirAll(filepath.Join(top, "dir1/dir2"), 0755) + require.NoError(t, err) + return "dir1/dir2/.." + }, + "dir1", + }, + { // Relative components in symlink + func(t *testing.T, top string) string { + err := os.MkdirAll(filepath.Join(top, "dir1/dir2"), 0755) + require.NoError(t, err) + err = os.Symlink("../dir1/dir2", filepath.Join(top, "dir1/link2")) + require.NoError(t, err) + return "dir1/link2" + }, + "dir1/dir2", + }, + { // Relative component pointing "into" a symlink + func(t *testing.T, top string) string { + err := os.MkdirAll(filepath.Join(top, "dir1/dir2/dir3"), 0755) + require.NoError(t, err) + err = os.Symlink("dir3", filepath.Join(top, "dir1/dir2/link3")) + require.NoError(t, err) + return "dir1/dir2/link3/../.." + }, + "dir1", + }, + { // Unreadable directory + func(t *testing.T, top string) string { + err := os.MkdirAll(filepath.Join(top, "unreadable/dir2"), 0755) + require.NoError(t, err) + err = os.Chmod(filepath.Join(top, "unreadable"), 000) + require.NoError(t, err) + return "unreadable/dir2" + }, + "", + }, +} + +func testPathsAreSameFile(t *testing.T, path1, path2, description string) { + fi1, err := os.Stat(path1) + require.NoError(t, err) + fi2, err := os.Stat(path2) + require.NoError(t, err) + assert.True(t, os.SameFile(fi1, fi2), description) +} + +func runPathResolvingTestCase(t *testing.T, f func(string) (string, error), c pathResolvingTestCase, suffix string) { + topDir := t.TempDir() + defer func() { + // Clean up after the "Unreadable directory" case; os.RemoveAll just fails without this. + _ = os.Chmod(filepath.Join(topDir, "unreadable"), 0755) // Ignore errors, especially if this does not exist. + }() + + input := c.setup(t, topDir) + suffix // Do not call filepath.Join() on input, it calls filepath.Clean() internally! + description := fmt.Sprintf("%s vs. %s%s", input, c.expected, suffix) + + fullOutput, err := f(topDir + "/" + input) + if c.expected == "" { + assert.Error(t, err, description) + } else { + require.NoError(t, err, input) + fullExpected := topDir + "/" + c.expected + suffix + assert.Equal(t, fullExpected, fullOutput) + + // Either the two paths resolve to the same existing file, or to the same name in the same existing parent. + if _, err := os.Lstat(fullExpected); err == nil { + testPathsAreSameFile(t, fullOutput, fullExpected, description) + } else { + require.True(t, os.IsNotExist(err)) + _, err := os.Stat(fullOutput) + require.Error(t, err) + require.True(t, os.IsNotExist(err)) + + parentExpected, fileExpected := filepath.Split(fullExpected) + parentOutput, fileOutput := filepath.Split(fullOutput) + assert.Equal(t, fileExpected, fileOutput) + testPathsAreSameFile(t, parentOutput, parentExpected, description) + } + } +} + +func TestResolvePathToFullyExplicit(t *testing.T) { + for _, c := range testCases { + runPathResolvingTestCase(t, ResolvePathToFullyExplicit, c, "") + runPathResolvingTestCase(t, ResolvePathToFullyExplicit, c, "/trailing") + } +} + +func TestResolveExistingPathToFullyExplicit(t *testing.T) { + for _, c := range testCases { + runPathResolvingTestCase(t, resolveExistingPathToFullyExplicit, c, "") + } +} diff --git a/vendor/github.com/containers/image/v5/doc.go b/vendor/github.com/containers/image/v5/doc.go new file mode 100644 index 00000000000..7ed58be0d3d --- /dev/null +++ b/vendor/github.com/containers/image/v5/doc.go @@ -0,0 +1,70 @@ +// The package image provides libraries and commands to interact with container images. +// +// package main +// +// import ( +// "context" +// "fmt" +// +// "github.com/containers/image/v5/docker" +// ) +// +// func main() { +// ref, err := docker.ParseReference("//fedora") +// if err != nil { +// panic(err) +// } +// ctx := context.Background() +// img, err := ref.NewImage(ctx, nil) +// if err != nil { +// panic(err) +// } +// defer img.Close() +// b, _, err := img.Manifest(ctx) +// if err != nil { +// panic(err) +// } +// fmt.Printf("%s", string(b)) +// } +// +// ## Notes on running in rootless mode +// +// If your application needs to access a containers/storage store in rootless +// mode, then the following additional steps have to be performed at start-up of +// your application: +// +// package main +// +// import ( +// "github.com/containers/storage/pkg/reexec" +// "github.com/syndtr/gocapability/capability" +// "github.com/containers/storage/pkg/unshare" +// ) +// +// var neededCapabilities = []capability.Cap{ +// capability.CAP_CHOWN, +// capability.CAP_DAC_OVERRIDE, +// capability.CAP_FOWNER, +// capability.CAP_FSETID, +// capability.CAP_MKNOD, +// capability.CAP_SETFCAP, +// } +// +// func main() { +// reexec.Init() +// +// capabilities, err := capability.NewPid(0) +// if err != nil { +// panic(err) +// } +// for _, cap := range neededCapabilities { +// if !capabilities.Get(capability.EFFECTIVE, cap) { +// // We miss a capability we need, create a user namespaces +// unshare.MaybeReexecUsingUserNamespace(true) +// } +// } +// // rest of your code follows here +// } +// +// TODO(runcom) +package image diff --git a/vendor/github.com/containers/image/v5/docker/archive/dest_test.go b/vendor/github.com/containers/image/v5/docker/archive/dest_test.go new file mode 100644 index 00000000000..06b390931e8 --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/archive/dest_test.go @@ -0,0 +1,5 @@ +package archive + +import "github.com/containers/image/v5/internal/private" + +var _ private.ImageDestination = (*archiveImageDestination)(nil) diff --git a/vendor/github.com/containers/image/v5/docker/archive/fixtures/almostempty.tar b/vendor/github.com/containers/image/v5/docker/archive/fixtures/almostempty.tar new file mode 100644 index 00000000000..ac37c1e4d64 Binary files /dev/null and b/vendor/github.com/containers/image/v5/docker/archive/fixtures/almostempty.tar differ diff --git a/vendor/github.com/containers/image/v5/docker/archive/src_test.go b/vendor/github.com/containers/image/v5/docker/archive/src_test.go new file mode 100644 index 00000000000..b98abea7613 --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/archive/src_test.go @@ -0,0 +1,5 @@ +package archive + +import "github.com/containers/image/v5/internal/private" + +var _ private.ImageSource = (*archiveImageSource)(nil) diff --git a/vendor/github.com/containers/image/v5/docker/archive/transport_test.go b/vendor/github.com/containers/image/v5/docker/archive/transport_test.go new file mode 100644 index 00000000000..59936855619 --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/archive/transport_test.go @@ -0,0 +1,284 @@ +package archive + +import ( + "context" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + sha256digestHex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + sha256digest = "@sha256:" + sha256digestHex + tarFixture = "fixtures/almostempty.tar" +) + +func TestTransportName(t *testing.T) { + assert.Equal(t, "docker-archive", Transport.Name()) +} + +func TestTransportParseReference(t *testing.T) { + testParseReference(t, Transport.ParseReference) +} + +func TestTransportValidatePolicyConfigurationScope(t *testing.T) { + for _, scope := range []string{ // A semi-representative assortment of values; everything is rejected. + "docker.io/library/busybox:notlatest", + "docker.io/library/busybox", + "docker.io/library", + "docker.io", + "", + } { + err := Transport.ValidatePolicyConfigurationScope(scope) + assert.Error(t, err, scope) + } +} + +func TestParseReference(t *testing.T) { + testParseReference(t, ParseReference) +} + +// testParseReference is a test shared for Transport.ParseReference and ParseReference. +func testParseReference(t *testing.T, fn func(string) (types.ImageReference, error)) { + for _, c := range []struct { + input, expectedPath, expectedRef string + expectedSourceIndex int + }{ + {"", "", "", -1}, // Empty input is explicitly rejected + {"/path", "/path", "", -1}, + {"/path:busybox:notlatest", "/path", "docker.io/library/busybox:notlatest", -1}, // Explicit tag + {"/path:busybox" + sha256digest, "", "", -1}, // Digest references are forbidden + {"/path:busybox", "/path", "docker.io/library/busybox:latest", -1}, // Default tag + // A github.com/distribution/reference value can have a tag and a digest at the same time! + {"/path:busybox:latest" + sha256digest, "", "", -1}, // Both tag and digest is rejected + {"/path:docker.io/library/busybox:latest", "/path", "docker.io/library/busybox:latest", -1}, // All implied reference parts explicitly specified + {"/path:UPPERCASEISINVALID", "", "", -1}, // Invalid reference format + {"/path:@", "", "", -1}, // Missing source index + {"/path:@0", "/path", "", 0}, // Valid source index + {"/path:@999999", "/path", "", 999999}, // Valid source index + {"/path:@-2", "", "", -1}, // Negative source index + {"/path:@-1", "", "", -1}, // Negative source index, using the placeholder value + {"/path:busybox@0", "", "", -1}, // References and source indices can’t be combined. + {"/path:@0:busybox", "", "", -1}, // References and source indices can’t be combined. + } { + ref, err := fn(c.input) + if c.expectedPath == "" { + assert.Error(t, err, c.input) + } else { + require.NoError(t, err, c.input) + archiveRef, ok := ref.(archiveReference) + require.True(t, ok, c.input) + assert.Equal(t, c.expectedPath, archiveRef.path, c.input) + if c.expectedRef == "" { + assert.Nil(t, archiveRef.ref, c.input) + } else { + require.NotNil(t, archiveRef.ref, c.input) + assert.Equal(t, c.expectedRef, archiveRef.ref.String(), c.input) + } + assert.Equal(t, c.expectedSourceIndex, archiveRef.sourceIndex, c.input) + } + } +} + +// namedTaggedRef returns a reference.NamedTagged for input +func namedTaggedRef(t *testing.T, input string) reference.NamedTagged { + named, err := reference.ParseNormalizedNamed(input) + require.NoError(t, err, input) + nt, ok := named.(reference.NamedTagged) + require.True(t, ok, input) + return nt +} + +func TestNewReference(t *testing.T) { + for _, path := range []string{"relative", "/absolute"} { + for _, c := range []struct { + ref string + ok bool + }{ + {"busybox:notlatest", true}, + {"busybox:notlatest" + sha256digest, false}, + {"", true}, + } { + var ntRef reference.NamedTagged = nil + if c.ref != "" { + ntRef = namedTaggedRef(t, c.ref) + } + + res, err := NewReference(path, ntRef) + if !c.ok { + assert.Error(t, err, c.ref) + } else { + require.NoError(t, err, c.ref) + archiveRef, ok := res.(archiveReference) + require.True(t, ok, c.ref) + assert.Equal(t, path, archiveRef.path) + if c.ref == "" { + assert.Nil(t, archiveRef.ref, c.ref) + } else { + require.NotNil(t, archiveRef.ref, c.ref) + assert.Equal(t, ntRef.String(), archiveRef.ref.String(), c.ref) + } + assert.Equal(t, -1, archiveRef.sourceIndex, c.ref) + } + } + } + _, err := NewReference("with:colon", nil) + assert.Error(t, err) + + // Complete coverage testing of the private newReference here as well + ntRef := namedTaggedRef(t, "busybox:latest") + _, err = newReference("path", ntRef, 0, nil, nil) + assert.Error(t, err) +} + +func TestNewIndexReference(t *testing.T) { + for _, path := range []string{"relative", "/absolute"} { + for _, c := range []struct { + index int + ok bool + }{ + {0, true}, + {9999990, true}, + {-1, true}, + {-2, false}, + } { + res, err := NewIndexReference(path, c.index) + if !c.ok { + assert.Error(t, err, c.index) + } else { + require.NoError(t, err, c.index) + archiveRef, ok := res.(archiveReference) + require.True(t, ok, c.index) + assert.Equal(t, path, archiveRef.path) + assert.Nil(t, archiveRef.ref, c.index) + assert.Equal(t, c.index, archiveRef.sourceIndex) + } + } + } + _, err := NewReference("with:colon", nil) + assert.Error(t, err) +} + +// A common list of reference formats to test for the various ImageReference methods. +var validReferenceTestCases = []struct { + input, dockerRef string + sourceIndex int + stringWithinTransport string +}{ + {"/pathonly", "", -1, "/pathonly"}, + {"/path:busybox:notlatest", "docker.io/library/busybox:notlatest", -1, "/path:docker.io/library/busybox:notlatest"}, // Explicit tag + {"/path:docker.io/library/busybox:latest", "docker.io/library/busybox:latest", -1, "/path:docker.io/library/busybox:latest"}, // All implied reference part explicitly specified + {"/path:example.com/ns/foo:bar", "example.com/ns/foo:bar", -1, "/path:example.com/ns/foo:bar"}, // All values explicitly specified + {"/path:@0", "", 0, "/path:@0"}, + {"/path:@999999", "", 999999, "/path:@999999"}, +} + +func TestReferenceTransport(t *testing.T) { + ref, err := ParseReference("/tmp/archive.tar") + require.NoError(t, err) + assert.Equal(t, Transport, ref.Transport()) +} + +func TestReferenceStringWithinTransport(t *testing.T) { + for _, c := range validReferenceTestCases { + ref, err := ParseReference(c.input) + require.NoError(t, err, c.input) + stringRef := ref.StringWithinTransport() + assert.Equal(t, c.stringWithinTransport, stringRef, c.input) + // Do one more round to verify that the output can be parsed, to an equal value. + ref2, err := Transport.ParseReference(stringRef) + require.NoError(t, err, c.input) + stringRef2 := ref2.StringWithinTransport() + assert.Equal(t, stringRef, stringRef2, c.input) + } +} + +func TestReferenceDockerReference(t *testing.T) { + for _, c := range validReferenceTestCases { + ref, err := ParseReference(c.input) + require.NoError(t, err, c.input) + dockerRef := ref.DockerReference() + if c.dockerRef != "" { + require.NotNil(t, dockerRef, c.input) + assert.Equal(t, c.dockerRef, dockerRef.String(), c.input) + } else { + require.Nil(t, dockerRef, c.input) + } + } +} + +func TestReferencePolicyConfigurationIdentity(t *testing.T) { + for _, c := range validReferenceTestCases { + ref, err := ParseReference(c.input) + require.NoError(t, err, c.input) + assert.Equal(t, "", ref.PolicyConfigurationIdentity(), c.input) + } +} + +func TestReferencePolicyConfigurationNamespaces(t *testing.T) { + for _, c := range validReferenceTestCases { + ref, err := ParseReference(c.input) + require.NoError(t, err, c.input) + assert.Empty(t, "", ref.PolicyConfigurationNamespaces(), c.input) + } +} + +func TestReferenceNewImage(t *testing.T) { + for _, suffix := range []string{"", ":emptyimage:latest", ":@0"} { + ref, err := ParseReference(tarFixture + suffix) + require.NoError(t, err, suffix) + img, err := ref.NewImage(context.Background(), nil) + require.NoError(t, err, suffix) + defer img.Close() + } +} + +func TestReferenceNewImageSource(t *testing.T) { + for _, suffix := range []string{"", ":emptyimage:latest", ":@0"} { + ref, err := ParseReference(tarFixture + suffix) + require.NoError(t, err, suffix) + src, err := ref.NewImageSource(context.Background(), nil) + require.NoError(t, err, suffix) + defer src.Close() + } +} + +func TestReferenceNewImageDestination(t *testing.T) { + tmpDir := t.TempDir() + + ref, err := ParseReference(filepath.Join(tmpDir, "no-reference")) + require.NoError(t, err) + dest, err := ref.NewImageDestination(context.Background(), nil) + assert.NoError(t, err) + dest.Close() + + ref, err = ParseReference(filepath.Join(tmpDir, "with-reference") + "busybox:latest") + require.NoError(t, err) + dest, err = ref.NewImageDestination(context.Background(), nil) + assert.NoError(t, err) + defer dest.Close() +} + +func TestReferenceDeleteImage(t *testing.T) { + tmpDir := t.TempDir() + + for i, suffix := range []string{"", ":some-reference", ":@0"} { + testFile := filepath.Join(tmpDir, fmt.Sprintf("file%d.tar", i)) + err := os.WriteFile(testFile, []byte("nonempty"), 0644) + require.NoError(t, err, suffix) + + ref, err := ParseReference(testFile + suffix) + require.NoError(t, err, suffix) + err = ref.DeleteImage(context.Background(), nil) + assert.Error(t, err, suffix) + + _, err = os.Lstat(testFile) + assert.NoError(t, err, suffix) + } +} diff --git a/vendor/github.com/containers/image/v5/docker/body_reader_test.go b/vendor/github.com/containers/image/v5/docker/body_reader_test.go new file mode 100644 index 00000000000..0011582b7a9 --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/body_reader_test.go @@ -0,0 +1,196 @@ +package docker + +import ( + "errors" + "math" + "net/http" + "testing" + "time" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseDecimalInString(t *testing.T) { + for _, prefix := range []string{"", "text", "0"} { + for _, suffix := range []string{"", "text"} { + for _, c := range []struct { + s string + v int64 + }{ + {"0", 0}, + {"1", 1}, + {"0700", 700}, // not octal + } { + input := prefix + c.s + suffix + res, pos, err := parseDecimalInString(input, len(prefix)) + require.NoError(t, err, input) + assert.Equal(t, c.v, res, input) + assert.Equal(t, len(prefix)+len(c.s), pos, input) + } + for _, c := range []string{ + "-1", + "xA", + "&", + "", + "999999999999999999999999999999999999999999999999999999999999999999", + } { + input := prefix + c + suffix + _, _, err := parseDecimalInString(input, len(prefix)) + assert.Error(t, err, c) + } + } + } +} + +func TestParseExpectedChar(t *testing.T) { + for _, prefix := range []string{"", "text", "0"} { + for _, suffix := range []string{"", "text"} { + input := prefix + "+" + suffix + pos, err := parseExpectedChar(input, len(prefix), '+') + require.NoError(t, err, input) + assert.Equal(t, len(prefix)+1, pos, input) + + _, err = parseExpectedChar(input, len(prefix), '-') + assert.Error(t, err, input) + } + } +} + +func TestParseContentRange(t *testing.T) { + for _, c := range []struct { + in string + first, last, completeLength int64 + }{ + {"bytes 0-0/1", 0, 0, 1}, + {"bytes 010-020/030", 10, 20, 30}, + {"bytes 1000-1010/*", 1000, 1010, -1}, + } { + first, last, completeLength, err := parseContentRange(&http.Response{ + Header: http.Header{ + http.CanonicalHeaderKey("Content-Range"): []string{c.in}, + }, + }) + require.NoError(t, err, c.in) + assert.Equal(t, c.first, first, c.in) + assert.Equal(t, c.last, last, c.in) + assert.Equal(t, c.completeLength, completeLength, c.in) + } + + for _, hdr := range []http.Header{ + nil, + {http.CanonicalHeaderKey("Content-Range"): []string{}}, + {http.CanonicalHeaderKey("Content-Range"): []string{"bytes 1-2/3", "bytes 1-2/3"}}, + } { + _, _, _, err := parseContentRange(&http.Response{ + Header: hdr, + }) + assert.Error(t, err) + } + + for _, c := range []string{ + "", + "notbytes 1-2/3", + "bytes ", + "bytes x-2/3", + "bytes 1*2/3", + "bytes 1", + "bytes 1-", + "bytes 1-x/3", + "bytes 1-2", + "bytes 1-2@3", + "bytes 1-2/", + "bytes 1-2/*a", + "bytes 1-2/3a", + } { + _, _, _, err := parseContentRange(&http.Response{ + Header: http.Header{ + http.CanonicalHeaderKey("Content-Range"): []string{c}, + }, + }) + assert.Error(t, err, c, c) + } +} + +func TestMillisecondsSinceOptional(t *testing.T) { + current := time.Date(2023, 2, 9, 8, 7, 6, 5, time.UTC) + res := millisecondsSinceOptional(current, time.Time{}) + assert.True(t, math.IsNaN(res)) + tm := current.Add(-60 * time.Second) // 60 seconds _before_ current + res = millisecondsSinceOptional(current, tm) + assert.Equal(t, res, 60_000.0) +} + +func TestBodyReaderErrorIfNotReconnecting(t *testing.T) { + // Silence logrus.Info logs in the tested method + prevLevel := logrus.StandardLogger().Level + logrus.StandardLogger().SetLevel(logrus.WarnLevel) + t.Cleanup(func() { + logrus.StandardLogger().SetLevel(prevLevel) + }) + + for _, c := range []struct { + name string + previousRetry bool + currentOffset int64 + currentTime int // milliseconds + expectReconnect bool + }{ + { + name: "A lot of progress, after a long time, second retry", + previousRetry: true, + currentOffset: 2 * bodyReaderMinimumProgress, + currentTime: 2 * bodyReaderMSSinceLastRetry, + expectReconnect: true, + }, + { + name: "A lot of progress, after little time, second retry", + previousRetry: true, + currentOffset: 2 * bodyReaderMinimumProgress, + currentTime: 1, + expectReconnect: true, + }, + { + name: "Little progress, after a long time, second retry", + previousRetry: true, + currentOffset: 1, + currentTime: 2 * bodyReaderMSSinceLastRetry, + expectReconnect: true, + }, + { + name: "Little progress, after little time, second retry", + previousRetry: true, + currentOffset: 1, + currentTime: 1, + expectReconnect: false, + }, + { + name: "Little progress, after little time, first retry", + previousRetry: false, + currentOffset: 1, + currentTime: bodyReaderMSSinceLastRetry / 2, + expectReconnect: true, + }, + } { + tm := time.Now() + br := bodyReader{} + if c.previousRetry { + br.lastRetryOffset = 2 * bodyReaderMinimumProgress + br.offset = br.lastRetryOffset + c.currentOffset + br.firstConnectionTime = tm.Add(-time.Duration(c.currentTime+2*bodyReaderMSSinceLastRetry) * time.Millisecond) + br.lastRetryTime = tm.Add(-time.Duration(c.currentTime) * time.Millisecond) + } else { + br.lastRetryOffset = -1 + br.lastRetryTime = time.Time{} + br.offset = c.currentOffset + br.firstConnectionTime = tm.Add(-time.Duration(c.currentTime) * time.Millisecond) + } + err := br.errorIfNotReconnecting(errors.New("some error for error text only"), "URL for error text only") + if c.expectReconnect { + assert.NoError(t, err, c.name, br) + } else { + assert.Error(t, err, c.name, br) + } + } +} diff --git a/vendor/github.com/containers/image/v5/docker/daemon/client_test.go b/vendor/github.com/containers/image/v5/docker/daemon/client_test.go new file mode 100644 index 00000000000..161747f750c --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/daemon/client_test.go @@ -0,0 +1,110 @@ +package daemon + +import ( + "net/http" + "os" + "path/filepath" + "testing" + + "github.com/containers/image/v5/types" + dockerclient "github.com/docker/docker/client" + "github.com/stretchr/testify/assert" +) + +func TestDockerClientFromNilSystemContext(t *testing.T) { + client, err := newDockerClient(nil) + + assert.Nil(t, err, "There should be no error creating the Docker client") + assert.NotNil(t, client, "A Docker client reference should have been returned") + + assert.Equal(t, dockerclient.DefaultDockerHost, client.DaemonHost(), "The default docker host should have been used") + assert.Equal(t, defaultAPIVersion, client.ClientVersion(), "The default api version should have been used") + + assert.NoError(t, client.Close()) +} + +func TestDockerClientFromCertContext(t *testing.T) { + testDir := testDir(t) + + host := "tcp://127.0.0.1:2376" + systemCtx := &types.SystemContext{ + DockerDaemonCertPath: filepath.Join(testDir, "testdata", "certs"), + DockerDaemonHost: host, + DockerDaemonInsecureSkipTLSVerify: true, + } + + client, err := newDockerClient(systemCtx) + + assert.Nil(t, err, "There should be no error creating the Docker client") + assert.NotNil(t, client, "A Docker client reference should have been returned") + + assert.Equal(t, host, client.DaemonHost()) + assert.Equal(t, "1.22", client.ClientVersion()) + + assert.NoError(t, client.Close()) +} + +func TestTlsConfigFromInvalidCertPath(t *testing.T) { + ctx := &types.SystemContext{ + DockerDaemonCertPath: "/foo/bar", + } + + _, err := tlsConfig(ctx) + assert.ErrorContains(t, err, "could not read CA certificate") +} + +func TestTlsConfigFromCertPath(t *testing.T) { + testDir := testDir(t) + + ctx := &types.SystemContext{ + DockerDaemonCertPath: filepath.Join(testDir, "testdata", "certs"), + DockerDaemonInsecureSkipTLSVerify: true, + } + + httpClient, err := tlsConfig(ctx) + + assert.NoError(t, err, "There should be no error creating the HTTP client") + + tlsConfig := httpClient.Transport.(*http.Transport).TLSClientConfig + assert.True(t, tlsConfig.InsecureSkipVerify, "TLS verification should be skipped") + assert.Len(t, tlsConfig.Certificates, 1, "There should be one certificate") +} + +func TestSkipTLSVerifyOnly(t *testing.T) { + //testDir := testDir(t) + + ctx := &types.SystemContext{ + DockerDaemonInsecureSkipTLSVerify: true, + } + + httpClient, err := tlsConfig(ctx) + + assert.NoError(t, err, "There should be no error creating the HTTP client") + + tlsConfig := httpClient.Transport.(*http.Transport).TLSClientConfig + assert.True(t, tlsConfig.InsecureSkipVerify, "TLS verification should be skipped") + assert.Len(t, tlsConfig.Certificates, 0, "There should be no certificate") +} + +func TestSpecifyPlainHTTPViaHostScheme(t *testing.T) { + host := "http://127.0.0.1:2376" + ctx := &types.SystemContext{ + DockerDaemonHost: host, + } + + client, err := newDockerClient(ctx) + + assert.Nil(t, err, "There should be no error creating the Docker client") + assert.NotNil(t, client, "A Docker client reference should have been returned") + + assert.Equal(t, host, client.DaemonHost()) + assert.NoError(t, client.Close()) +} + +func testDir(t *testing.T) string { + testDir, err := os.Getwd() + if err != nil { + t.Fatal("Unable to determine the current test directory") + } + return testDir +} diff --git a/vendor/github.com/containers/image/v5/docker/daemon/daemon_dest_test.go b/vendor/github.com/containers/image/v5/docker/daemon/daemon_dest_test.go new file mode 100644 index 00000000000..51e5d09dafd --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/daemon/daemon_dest_test.go @@ -0,0 +1,5 @@ +package daemon + +import "github.com/containers/image/v5/internal/private" + +var _ private.ImageDestination = (*daemonImageDestination)(nil) diff --git a/vendor/github.com/containers/image/v5/docker/daemon/daemon_src_test.go b/vendor/github.com/containers/image/v5/docker/daemon/daemon_src_test.go new file mode 100644 index 00000000000..7214ce7013e --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/daemon/daemon_src_test.go @@ -0,0 +1,5 @@ +package daemon + +import "github.com/containers/image/v5/internal/private" + +var _ private.ImageSource = (*daemonImageSource)(nil) diff --git a/vendor/github.com/containers/image/v5/docker/daemon/daemon_transport_test.go b/vendor/github.com/containers/image/v5/docker/daemon/daemon_transport_test.go new file mode 100644 index 00000000000..6a08d54d7c5 --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/daemon/daemon_transport_test.go @@ -0,0 +1,242 @@ +package daemon + +import ( + "context" + "testing" + + "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/types" + "github.com/opencontainers/go-digest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + sha256digestHex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + sha256digest = "sha256:" + sha256digestHex +) + +func TestTransportName(t *testing.T) { + assert.Equal(t, "docker-daemon", Transport.Name()) +} + +func TestTransportParseReference(t *testing.T) { + testParseReference(t, Transport.ParseReference) +} + +func TestTransportValidatePolicyConfigurationScope(t *testing.T) { + // docker/policyconfiguration-accepted identities and scopes are accepted + for _, scope := range []string{ + "registry.example.com/ns/stream" + sha256digest, + "registry.example.com/ns/stream:notlatest", + "registry.example.com/ns/stream", + "registry.example.com/ns", + "registry.example.com", + "*.example.com", + "*.com", + sha256digestHex, // Accept also unqualified hexdigest values, they are in principle possible host names. + } { + err := Transport.ValidatePolicyConfigurationScope(scope) + assert.NoError(t, err, scope) + } + + // Hexadecimal IDs are rejected. algo:hexdigest is clearly an invalid host:port value. + err := Transport.ValidatePolicyConfigurationScope(sha256digest) + assert.Error(t, err) +} + +func TestParseReference(t *testing.T) { + testParseReference(t, ParseReference) +} + +// testParseReference is a test shared for Transport.ParseReference and ParseReference. +func testParseReference(t *testing.T, fn func(string) (types.ImageReference, error)) { + for _, c := range []struct{ input, expectedID, expectedRef string }{ + {sha256digest, sha256digest, ""}, // Valid digest format + {"sha512:" + sha256digestHex + sha256digestHex, "", ""}, // Non-digest.Canonical digest + {"sha256:ab", "", ""}, // Invalid digest value (too short) + {sha256digest + "ab", "", ""}, // Invalid digest value (too long) + {"sha256:XX23456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "", ""}, // Invalid digest value + {"UPPERCASEISINVALID", "", ""}, // Invalid reference input + {"busybox", "", ""}, // Missing tag or digest + {"busybox:latest", "", "docker.io/library/busybox:latest"}, // Explicit tag + {"busybox@" + sha256digest, "", "docker.io/library/busybox@" + sha256digest}, // Explicit digest + // A github.com/distribution/reference value can have a tag and a digest at the same time! + // Most versions of docker/reference do not handle that (ignoring the tag), so we reject such input. + {"busybox:latest@" + sha256digest, "", ""}, // Both tag and digest + {"docker.io/library/busybox:latest", "", "docker.io/library/busybox:latest"}, // All implied values explicitly specified + } { + ref, err := fn(c.input) + if c.expectedID == "" && c.expectedRef == "" { + assert.Error(t, err, c.input) + } else { + require.NoError(t, err, c.input) + daemonRef, ok := ref.(daemonReference) + require.True(t, ok, c.input) + // If we don't reject the input, the interpretation must be consistent with reference.ParseAnyReference + dockerRef, err := reference.ParseAnyReference(c.input) + require.NoError(t, err, c.input) + + if c.expectedRef == "" { + assert.Equal(t, c.expectedID, daemonRef.id.String(), c.input) + assert.Nil(t, daemonRef.ref, c.input) + + _, ok := dockerRef.(reference.Digested) + require.True(t, ok, c.input) + assert.Equal(t, c.expectedID, dockerRef.String(), c.input) + } else { + assert.Equal(t, "", daemonRef.id.String(), c.input) + require.NotNil(t, daemonRef.ref, c.input) + assert.Equal(t, c.expectedRef, daemonRef.ref.String(), c.input) + + _, ok := dockerRef.(reference.Named) + require.True(t, ok, c.input) + assert.Equal(t, c.expectedRef, dockerRef.String(), c.input) + } + } + } +} + +// A common list of reference formats to test for the various ImageReference methods. +// (For IDs it is much simpler, we simply use them unmodified) +var validNamedReferenceTestCases = []struct{ input, dockerRef, stringWithinTransport string }{ + {"busybox:notlatest", "docker.io/library/busybox:notlatest", "busybox:notlatest"}, // Explicit tag + {"busybox" + sha256digest, "docker.io/library/busybox" + sha256digest, "busybox" + sha256digest}, // Explicit digest + {"docker.io/library/busybox:latest", "docker.io/library/busybox:latest", "busybox:latest"}, // All implied values explicitly specified + {"example.com/ns/foo:bar", "example.com/ns/foo:bar", "example.com/ns/foo:bar"}, // All values explicitly specified +} + +func TestNewReference(t *testing.T) { + // An ID reference. + id, err := digest.Parse(sha256digest) + require.NoError(t, err) + ref, err := NewReference(id, nil) + require.NoError(t, err) + daemonRef, ok := ref.(daemonReference) + require.True(t, ok) + assert.Equal(t, id, daemonRef.id) + assert.Nil(t, daemonRef.ref) + + // Named references + for _, c := range validNamedReferenceTestCases { + parsed, err := reference.ParseNormalizedNamed(c.input) + require.NoError(t, err) + ref, err := NewReference("", parsed) + require.NoError(t, err, c.input) + daemonRef, ok := ref.(daemonReference) + require.True(t, ok, c.input) + assert.Equal(t, "", daemonRef.id.String()) + require.NotNil(t, daemonRef.ref) + assert.Equal(t, c.dockerRef, daemonRef.ref.String(), c.input) + } + + // Both an ID and a named reference provided + parsed, err := reference.ParseNormalizedNamed("busybox:latest") + require.NoError(t, err) + _, err = NewReference(id, parsed) + assert.Error(t, err) + + // A reference with neither a tag nor digest + parsed, err = reference.ParseNormalizedNamed("busybox") + require.NoError(t, err) + _, err = NewReference("", parsed) + assert.Error(t, err) + + // A github.com/distribution/reference value can have a tag and a digest at the same time! + parsed, err = reference.ParseNormalizedNamed("busybox:notlatest@" + sha256digest) + require.NoError(t, err) + _, ok = parsed.(reference.Canonical) + require.True(t, ok) + _, ok = parsed.(reference.NamedTagged) + require.True(t, ok) + _, err = NewReference("", parsed) + assert.Error(t, err) +} + +func TestReferenceTransport(t *testing.T) { + ref, err := ParseReference(sha256digest) + require.NoError(t, err) + assert.Equal(t, Transport, ref.Transport()) + + ref, err = ParseReference("busybox:latest") + require.NoError(t, err) + assert.Equal(t, Transport, ref.Transport()) +} + +func TestReferenceStringWithinTransport(t *testing.T) { + ref, err := ParseReference(sha256digest) + require.NoError(t, err) + assert.Equal(t, sha256digest, ref.StringWithinTransport()) + + for _, c := range validNamedReferenceTestCases { + ref, err := ParseReference(c.input) + require.NoError(t, err, c.input) + stringRef := ref.StringWithinTransport() + assert.Equal(t, c.stringWithinTransport, stringRef, c.input) + // Do one more round to verify that the output can be parsed, to an equal value. + ref2, err := Transport.ParseReference(stringRef) + require.NoError(t, err, c.input) + stringRef2 := ref2.StringWithinTransport() + assert.Equal(t, stringRef, stringRef2, c.input) + } +} + +func TestReferenceDockerReference(t *testing.T) { + ref, err := ParseReference(sha256digest) + require.NoError(t, err) + assert.Nil(t, ref.DockerReference()) + + for _, c := range validNamedReferenceTestCases { + ref, err := ParseReference(c.input) + require.NoError(t, err, c.input) + dockerRef := ref.DockerReference() + require.NotNil(t, dockerRef, c.input) + assert.Equal(t, c.dockerRef, dockerRef.String(), c.input) + } +} + +func TestReferencePolicyConfigurationIdentity(t *testing.T) { + // id-only references have no identity. + ref, err := ParseReference(sha256digest) + require.NoError(t, err) + assert.Equal(t, "", ref.PolicyConfigurationIdentity()) + + // Just a smoke test, the substance is tested in policyconfiguration.TestDockerReference. + ref, err = ParseReference("busybox:notlatest") + require.NoError(t, err) + assert.Equal(t, "docker.io/library/busybox:notlatest", ref.PolicyConfigurationIdentity()) +} + +func TestReferencePolicyConfigurationNamespaces(t *testing.T) { + // id-only references have no identity. + ref, err := ParseReference(sha256digest) + require.NoError(t, err) + assert.Empty(t, ref.PolicyConfigurationNamespaces()) + + // Just a smoke test, the substance is tested in policyconfiguration.TestDockerReference. + ref, err = ParseReference("busybox:notlatest") + require.NoError(t, err) + assert.Equal(t, []string{ + "docker.io/library/busybox", + "docker.io/library", + "docker.io", + "*.io", + }, ref.PolicyConfigurationNamespaces()) +} + +// daemonReference.NewImage, daemonReference.NewImageSource, openshiftReference.NewImageDestination +// untested because just creating the objects immediately connects to the daemon. + +func TestReferenceDeleteImage(t *testing.T) { + ref, err := ParseReference(sha256digest) + require.NoError(t, err) + err = ref.DeleteImage(context.Background(), nil) + assert.Error(t, err) + + for _, c := range validNamedReferenceTestCases { + ref, err := ParseReference(c.input) + require.NoError(t, err, c.input) + err = ref.DeleteImage(context.Background(), nil) + assert.Error(t, err, c.input) + } +} diff --git a/vendor/github.com/containers/image/v5/docker/daemon/testdata/certs/ca.pem b/vendor/github.com/containers/image/v5/docker/daemon/testdata/certs/ca.pem new file mode 100644 index 00000000000..3fc3820b64f --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/daemon/testdata/certs/ca.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIICzjCCAbagAwIBAgIRAIGgYBNZse0EqRVzxe7aQGIwDQYJKoZIhvcNAQELBQAw +EDEOMAwGA1UEChMFaGFyZHkwHhcNMTcxMDA0MDgzNDAwWhcNMjAwOTE4MDgzNDAw +WjAQMQ4wDAYDVQQKEwVoYXJkeTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAMlrdtoXWlZMPFwgeKZHrGxjVe4KXkQy5MFBUfO48htyIe2OlZAd3HGyap41 +7L4YciFhw0bp7wHnYtSTiCHQrnA4SLzNuaU2NM5nJw+E4c5kNrkvhLJqpTNCaYCy +Xbh3H8REW+5UJIgnyeKLx//kvlDm6p4O55+OLlGgzxNaTIgldKLPmx543VVt6VDT +qgFlaYsRz8hZ12+qAqu5am/Wpfal2+Df7Pmmn5M90UBTUwY8CLc/ZiWbv6hihDWV +I28JoM0onEqAx7phRd0SwwK4mYfEe/u614r3bZaI36e9ojU9/St4nbMoMeyZP96t +DOdX9A1SMbsqLOYKXBKM+jXPEaECAwEAAaMjMCEwDgYDVR0PAQH/BAQDAgKsMA8G +A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBALah7CjpwbEY6yjA2KDv +VaAHEgz4Xd8USW/L2292EiQLmFdIaEJiiWRjtKFiF427TXfAPXvxHA2q9OElDW4d +G6XAcBJg5mcBh8WRTHwfLQ8llfj7dH1/sfazSUZeat6lTIyhQfkF99LAJTqlfYAF +aNqIQio7FAjGyJqIPYLa1FKmfLdZr9azb9IjTZLhBGBWdLF0+JOn+JBsl7g9BvUp +ArCI0Wib/vsr368xkzWzKjij1exZdfw0TmsieNYvViFoFJGNCB5XLPo0bHrmMVVe +25EGam+xPkG/JQP5Eb3iikSEn8y5SIeJ0nS0EQE6uXPv+lQj1LmVv8OYzjXqpoJT +n6g= +-----END CERTIFICATE----- diff --git a/vendor/github.com/containers/image/v5/docker/daemon/testdata/certs/cert.pem b/vendor/github.com/containers/image/v5/docker/daemon/testdata/certs/cert.pem new file mode 100644 index 00000000000..f4d8edeb03a --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/daemon/testdata/certs/cert.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC6zCCAdOgAwIBAgIQEh1UsPL20u9KnyOByuhYWDANBgkqhkiG9w0BAQsFADAQ +MQ4wDAYDVQQKEwVoYXJkeTAeFw0xNzEwMDQwODM0MDBaFw0yMDA5MTgwODM0MDBa +MBwxGjAYBgNVBAoMEWhhcmR5Ljxib290c3RyYXA+MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAyJm29vB/urzreEwF012iAAWW3fgE1VEeNLTP/sZTYV3z +UNGKao5x7dUIiah8rptkK3+FN4TID8Z2c1DpKzMTisdpRF3UoRWmjm1UTbxEENhk +EptkFwGFM6BcZSyiLlyCBVM+wGsqzHAASe833S/yiu8miNc2S+jd0FIluKWe0yzG +u2oaJfA28dBfqWyn9hh6msqBVYK6sDle9t0ditNubCyD+vrnoK8825LOIPV6QafL +kVyW0/mj4GJutPOVop37HyQMcuQnDWBA+934l3tpeaJ93d3u8XjU7dXuOobKMohw ++33/pTALu9P0WtDbEeo/xcEICgimqpir92KMSXxUbwIDAQABozUwMzAOBgNVHQ8B +Af8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADANBgkq +hkiG9w0BAQsFAAOCAQEAnYffv9ipGQVW/t3sFxKu9LXQ7ZkhUSgoxPIA51goaYop +YM9QR3ZBf2tMJwjKXuOLEkxxweNjP3dMKh2gykFory+jv6OQYIiLf9M82ty8rOPi +mWLMDAIWWagkj5Yy6b+/aLkpXQ+lEsxLyi6po+D+I+AwRUYvfSc74a7XxkJk77JF +/0SVgNdDtL08zVNOGDgepP/95e1pKMKgsOiCDnFCOAY+l6HcvizwBH+EI+XtdLVb +qBmOAYiwYObBaRuyhVbbDKqKRGFUNkmmDv6vCQoTL1C9wrBnAiJe2khbLm1ix9Re +3MW15CLuipneSgRAWXSdMbDIv9+KQE8fo2TWqikrCw== +-----END CERTIFICATE----- diff --git a/vendor/github.com/containers/image/v5/docker/daemon/testdata/certs/key.pem b/vendor/github.com/containers/image/v5/docker/daemon/testdata/certs/key.pem new file mode 100644 index 00000000000..ede7d2e1ea9 --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/daemon/testdata/certs/key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAyJm29vB/urzreEwF012iAAWW3fgE1VEeNLTP/sZTYV3zUNGK +ao5x7dUIiah8rptkK3+FN4TID8Z2c1DpKzMTisdpRF3UoRWmjm1UTbxEENhkEptk +FwGFM6BcZSyiLlyCBVM+wGsqzHAASe833S/yiu8miNc2S+jd0FIluKWe0yzGu2oa +JfA28dBfqWyn9hh6msqBVYK6sDle9t0ditNubCyD+vrnoK8825LOIPV6QafLkVyW +0/mj4GJutPOVop37HyQMcuQnDWBA+934l3tpeaJ93d3u8XjU7dXuOobKMohw+33/ +pTALu9P0WtDbEeo/xcEICgimqpir92KMSXxUbwIDAQABAoIBAQCyuKjXR5w1Ll4I +FotWLmTH6jLo3jDIMPZddP6e+emNpRvD1HyixPhiMdvicXdsRUuwqXNx7F4mF+au +hNbIwz/U9CcoXwSy48w5ttRWUba+31wBa+p3yMX5IhVPmr1/2rGItwsAejpuXBcV +yAiYi0BnYfyODFf2t6jwElBDO2POtdEoYVYwgtMTMy5pmDA2QA3mKkjCcJviectZ +9yFb8DFiwIYkryErWrGWaKls/oYV2O0A0mCaIqgw3HfhIl6F1pk+9oYnmsq6IzF5 +wSIg2evd4GMm/L2sqlVFqb4Kj54fbyfdOFK0bQso6VQZvB5tZ6NLHfv2f3BBFHVu +jO+On/ixAoGBAOJkPHavnAb/lLDnMJjjXYNUQqyxxSlwOwNifG4evf/KAezIIalJ +kC7jZoFsUkARVbRKQag0T2Xvxw/dDqmNastR1NxsBkhOWjYiQbALYP3u2f06Nhf8 +YlX6hyEje/3bb838//sH5jnaN8GcZnDBrAoPzW+V87pQoCyVrjs2t8qXAoGBAOLV ++PviAUWFjUO//dYk9H9IWotr6rdkzmpLbrj+NoLNSGeoZbByPmT5BuNswXvNyk+9 +smOQ8yqBiMpjxKwR4WQnS6Ydh6HTT33IWLLVazDFMf7ACmXWoScFhCAW6qGfdrYQ +hkCSbwgun8jbL2D477jJl6ZyQG48lVnnZDjkFbfpAoGAUOqCsekSW23+Nzxqojqh +sc7sBc2EKstyTENnNfTG9CW/imH9pgQlBJ1Chf+xZjTL7SSdUwFfX4/UFldsZi2l +fgZBjocNt8pJdA/KaqGmiRxVzayAqRIME673nWCRcKp9y6Ih3Bd2sjbMtuavtp2C +YBZF1xxBgNZQaZ8WJxPnnQECgYEAzLgGJPWc5iyZCJsesQTbMICRTyEPTYKKFD6N +6CFt+vDgNsUxOWRx0Vk6kUhW+rAItZzjgZ6RBzyuwtH17sGYZHZefMZL4Y2/QSru +ej/IpNRjwaF6AN0KxhfhXcCw8zrivX/+WgqOcJj7lh/TC7a/S0uNNSgJ5DODKwd9 +WSboPvkCgYEAzqdWfetko7hEI4076pufJrHPnnCJSHkkQ1QnfVl71mq7UmKXLDxD +L5oWtU53+dswzvxGrzkOWsRJC5nN30BYJuYlwKzo3+MCKlUzJSuIMVTbTPlwKudh +AF19s4GFZVo29FlgIQhA5dfIkZgFXAlVxYcGTLUixEmPwrc6yguULPs= +-----END RSA PRIVATE KEY----- diff --git a/vendor/github.com/containers/image/v5/docker/distribution_error_test.go b/vendor/github.com/containers/image/v5/docker/distribution_error_test.go new file mode 100644 index 00000000000..c77dccf6bee --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/distribution_error_test.go @@ -0,0 +1,113 @@ +// Code below is taken from https://github.com/distribution/distribution/blob/a4d9db5a884b70be0c96dd6a7a9dbef4f2798c51/registry/client/errors.go +// Copyright 2022 github.com/distribution/distribution authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES 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 + +import ( + "bytes" + "io" + "net/http" + "strings" + "testing" +) + +func TestHandleErrorResponse401ValidBody(t *testing.T) { + json := []byte("{\"errors\":[{\"code\":\"UNAUTHORIZED\",\"message\":\"action requires authentication\"}]}") + response := &http.Response{ + Status: "401 Unauthorized", + StatusCode: 401, + Body: io.NopCloser(bytes.NewReader(json)), + } + err := handleErrorResponse(response) + + expectedMsg := "unauthorized: action requires authentication" + if !strings.Contains(err.Error(), expectedMsg) { + t.Errorf("Expected \"%s\", got: \"%s\"", expectedMsg, err.Error()) + } +} + +func TestHandleErrorResponse401WithInvalidBody(t *testing.T) { + json := []byte("{invalid json}") + response := &http.Response{ + Status: "401 Unauthorized", + StatusCode: 401, + Body: io.NopCloser(bytes.NewReader(json)), + } + err := handleErrorResponse(response) + + expectedMsg := "unauthorized: authentication required" + if !strings.Contains(err.Error(), expectedMsg) { + t.Errorf("Expected \"%s\", got: \"%s\"", expectedMsg, err.Error()) + } +} + +func TestHandleErrorResponseExpectedStatusCode400ValidBody(t *testing.T) { + json := []byte("{\"errors\":[{\"code\":\"DIGEST_INVALID\",\"message\":\"provided digest does not match\"}]}") + response := &http.Response{ + Status: "400 Bad Request", + StatusCode: 400, + Body: io.NopCloser(bytes.NewReader(json)), + } + err := handleErrorResponse(response) + + expectedMsg := "digest invalid: provided digest does not match" + if !strings.Contains(err.Error(), expectedMsg) { + t.Errorf("Expected \"%s\", got: \"%s\"", expectedMsg, err.Error()) + } +} + +func TestHandleErrorResponseExpectedStatusCode404EmptyErrorSlice(t *testing.T) { + json := []byte(`{"randomkey": "randomvalue"}`) + response := &http.Response{ + Status: "404 Not Found", + StatusCode: 404, + Body: io.NopCloser(bytes.NewReader(json)), + } + err := handleErrorResponse(response) + + expectedMsg := `error parsing HTTP 404 response body: no error details found in HTTP response body: "{\"randomkey\": \"randomvalue\"}"` + if !strings.Contains(err.Error(), expectedMsg) { + t.Errorf("Expected \"%s\", got: \"%s\"", expectedMsg, err.Error()) + } +} + +func TestHandleErrorResponseExpectedStatusCode404InvalidBody(t *testing.T) { + json := []byte("{invalid json}") + response := &http.Response{ + Status: "404 Not Found", + StatusCode: 404, + Body: io.NopCloser(bytes.NewReader(json)), + } + err := handleErrorResponse(response) + + expectedMsg := "error parsing HTTP 404 response body: invalid character 'i' looking for beginning of object key string: \"{invalid json}\"" + if !strings.Contains(err.Error(), expectedMsg) { + t.Errorf("Expected \"%s\", got: \"%s\"", expectedMsg, err.Error()) + } +} + +func TestHandleErrorResponseUnexpectedStatusCode501(t *testing.T) { + response := &http.Response{ + Status: "501 Not Implemented", + StatusCode: 501, + Body: io.NopCloser(bytes.NewReader([]byte("{\"Error Encountered\" : \"Function not implemented.\"}"))), + } + err := handleErrorResponse(response) + + expectedMsg := "received unexpected HTTP status: 501 Not Implemented" + if !strings.Contains(err.Error(), expectedMsg) { + t.Errorf("Expected \"%s\", got: \"%s\"", expectedMsg, err.Error()) + } +} diff --git a/vendor/github.com/containers/image/v5/docker/docker_client_test.go b/vendor/github.com/containers/image/v5/docker/docker_client_test.go new file mode 100644 index 00000000000..086bc132a6b --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/docker_client_test.go @@ -0,0 +1,390 @@ +package docker + +import ( + "bufio" + "bytes" + "context" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/containers/image/v5/internal/useragent" + "github.com/containers/image/v5/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDockerCertDir(t *testing.T) { + const nondefaultFullPath = "/this/is/not/the/default/full/path" + const nondefaultPerHostDir = "/this/is/not/the/default/certs.d" + const variableReference = "$HOME" + const rootPrefix = "/root/prefix" + const registryHostPort = "thishostdefinitelydoesnotexist:5000" + + systemPerHostResult := filepath.Join(perHostCertDirs[len(perHostCertDirs)-1].path, registryHostPort) + for _, c := range []struct { + sys *types.SystemContext + expected string + }{ + // The common case + {nil, systemPerHostResult}, + // There is a context, but it does not override the path. + {&types.SystemContext{}, systemPerHostResult}, + // Full path overridden + {&types.SystemContext{DockerCertPath: nondefaultFullPath}, nondefaultFullPath}, + // Per-host path overridden + { + &types.SystemContext{DockerPerHostCertDirPath: nondefaultPerHostDir}, + filepath.Join(nondefaultPerHostDir, registryHostPort), + }, + // Both overridden + { + &types.SystemContext{ + DockerCertPath: nondefaultFullPath, + DockerPerHostCertDirPath: nondefaultPerHostDir, + }, + nondefaultFullPath, + }, + // Root overridden + { + &types.SystemContext{RootForImplicitAbsolutePaths: rootPrefix}, + filepath.Join(rootPrefix, systemPerHostResult), + }, + // Root and path overrides present simultaneously, + { + &types.SystemContext{ + DockerCertPath: nondefaultFullPath, + RootForImplicitAbsolutePaths: rootPrefix, + }, + nondefaultFullPath, + }, + { + &types.SystemContext{ + DockerPerHostCertDirPath: nondefaultPerHostDir, + RootForImplicitAbsolutePaths: rootPrefix, + }, + filepath.Join(nondefaultPerHostDir, registryHostPort), + }, + // … and everything at once + { + &types.SystemContext{ + DockerCertPath: nondefaultFullPath, + DockerPerHostCertDirPath: nondefaultPerHostDir, + RootForImplicitAbsolutePaths: rootPrefix, + }, + nondefaultFullPath, + }, + // No environment expansion happens in the overridden paths + {&types.SystemContext{DockerCertPath: variableReference}, variableReference}, + { + &types.SystemContext{DockerPerHostCertDirPath: variableReference}, + filepath.Join(variableReference, registryHostPort), + }, + } { + path, err := dockerCertDir(c.sys, registryHostPort) + require.Equal(t, nil, err) + assert.Equal(t, c.expected, path) + } +} + +func TestNewBearerTokenFromJsonBlob(t *testing.T) { + expected := &bearerToken{Token: "IAmAToken", ExpiresIn: 100, IssuedAt: time.Unix(1514800802, 0)} + tokenBlob := []byte(`{"token":"IAmAToken","expires_in":100,"issued_at":"2018-01-01T10:00:02+00:00"}`) + token, err := newBearerTokenFromJSONBlob(tokenBlob) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + assertBearerTokensEqual(t, expected, token) +} + +func TestNewBearerAccessTokenFromJsonBlob(t *testing.T) { + expected := &bearerToken{Token: "IAmAToken", ExpiresIn: 100, IssuedAt: time.Unix(1514800802, 0)} + tokenBlob := []byte(`{"access_token":"IAmAToken","expires_in":100,"issued_at":"2018-01-01T10:00:02+00:00"}`) + token, err := newBearerTokenFromJSONBlob(tokenBlob) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + assertBearerTokensEqual(t, expected, token) +} + +func TestNewBearerTokenFromInvalidJsonBlob(t *testing.T) { + tokenBlob := []byte("IAmNotJson") + _, err := newBearerTokenFromJSONBlob(tokenBlob) + if err == nil { + t.Fatalf("unexpected an error unmarshaling JSON") + } +} + +func TestNewBearerTokenSmallExpiryFromJsonBlob(t *testing.T) { + expected := &bearerToken{Token: "IAmAToken", ExpiresIn: 60, IssuedAt: time.Unix(1514800802, 0)} + tokenBlob := []byte(`{"token":"IAmAToken","expires_in":1,"issued_at":"2018-01-01T10:00:02+00:00"}`) + token, err := newBearerTokenFromJSONBlob(tokenBlob) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + assertBearerTokensEqual(t, expected, token) +} + +func TestNewBearerTokenIssuedAtZeroFromJsonBlob(t *testing.T) { + zeroTime := time.Time{}.Format(time.RFC3339) + now := time.Now() + tokenBlob := []byte(fmt.Sprintf(`{"token":"IAmAToken","expires_in":100,"issued_at":"%s"}`, zeroTime)) + token, err := newBearerTokenFromJSONBlob(tokenBlob) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if token.IssuedAt.Before(now) { + t.Fatalf("expected [%s] not to be before [%s]", token.IssuedAt, now) + } + +} + +func assertBearerTokensEqual(t *testing.T, expected, subject *bearerToken) { + if expected.Token != subject.Token { + t.Fatalf("expected [%s] to equal [%s], it did not", subject.Token, expected.Token) + } + if expected.ExpiresIn != subject.ExpiresIn { + t.Fatalf("expected [%d] to equal [%d], it did not", subject.ExpiresIn, expected.ExpiresIn) + } + if !expected.IssuedAt.Equal(subject.IssuedAt) { + t.Fatalf("expected [%s] to equal [%s], it did not", subject.IssuedAt, expected.IssuedAt) + } +} + +func TestUserAgent(t *testing.T) { + const sentinelUA = "sentinel/1.0" + + var expectedUA string + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + got := r.Header.Get("User-Agent") + assert.Equal(t, expectedUA, got) + w.WriteHeader(http.StatusOK) + })) + defer s.Close() + + for _, tc := range []struct { + sys *types.SystemContext + expected string + }{ + // Can't both test nil and set DockerInsecureSkipTLSVerify :( + // {nil, defaultUA}, + {&types.SystemContext{}, useragent.DefaultUserAgent}, + {&types.SystemContext{DockerRegistryUserAgent: sentinelUA}, sentinelUA}, + } { + // For this test against localhost, we don't care. + tc.sys.DockerInsecureSkipTLSVerify = types.OptionalBoolTrue + + registry := strings.TrimPrefix(s.URL, "http://") + + expectedUA = tc.expected + if err := CheckAuth(context.Background(), tc.sys, "", "", registry); err != nil { + t.Fatalf("unexpected error: %v", err) + } + } +} + +func TestNeedsRetryOnError(t *testing.T) { + needsRetry, _ := needsRetryWithUpdatedScope(errors.New("generic"), nil) + if needsRetry { + t.Fatal("Got needRetry for a connection that included an error") + } +} + +var registrySuseComResp = http.Response{ + Status: "401 Unauthorized", + StatusCode: http.StatusUnauthorized, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: map[string][]string{ + "Content-Length": {"145"}, + "Content-Type": {"application/json"}, + "Date": {"Fri, 26 Aug 2022 08:03:13 GMT"}, + "Docker-Distribution-Api-Version": {"registry/2.0"}, + // "Www-Authenticate": {`Bearer realm="https://registry.suse.com/auth",service="SUSE Linux Docker Registry",scope="registry:catalog:*",error="insufficient_scope"`}, + "X-Content-Type-Options": {"nosniff"}, + }, + Request: nil, +} + +func TestNeedsRetryOnInsuficientScope(t *testing.T) { + resp := registrySuseComResp + resp.Header["Www-Authenticate"] = []string{ + `Bearer realm="https://registry.suse.com/auth",service="SUSE Linux Docker Registry",scope="registry:catalog:*",error="insufficient_scope"`, + } + expectedScope := authScope{ + resourceType: "registry", + remoteName: "catalog", + actions: "*", + } + + needsRetry, scope := needsRetryWithUpdatedScope(nil, &resp) + + if !needsRetry { + t.Fatal("Expected needing to retry") + } + + if expectedScope != *scope { + t.Fatalf("Got an invalid scope, expected '%q' but got '%q'", expectedScope, *scope) + } +} + +func TestNeedsRetryNoRetryWhenNoAuthHeader(t *testing.T) { + resp := registrySuseComResp + delete(resp.Header, "Www-Authenticate") + + needsRetry, _ := needsRetryWithUpdatedScope(nil, &resp) + + if needsRetry { + t.Fatal("Expected no need to retry, as no Authentication headers are present") + } +} + +func TestNeedsRetryNoRetryWhenNoBearerAuthHeader(t *testing.T) { + resp := registrySuseComResp + resp.Header["Www-Authenticate"] = []string{ + `OAuth2 realm="https://registry.suse.com/auth",service="SUSE Linux Docker Registry",scope="registry:catalog:*"`, + } + + needsRetry, _ := needsRetryWithUpdatedScope(nil, &resp) + + if needsRetry { + t.Fatal("Expected no need to retry, as no bearer authentication header is present") + } +} + +func TestNeedsRetryNoRetryWhenNoErrorInBearer(t *testing.T) { + resp := registrySuseComResp + resp.Header["Www-Authenticate"] = []string{ + `Bearer realm="https://registry.suse.com/auth",service="SUSE Linux Docker Registry",scope="registry:catalog:*"`, + } + + needsRetry, _ := needsRetryWithUpdatedScope(nil, &resp) + + if needsRetry { + t.Fatal("Expected no need to retry, as no insufficient error is present in the authentication header") + } +} + +func TestNeedsRetryNoRetryWhenInvalidErrorInBearer(t *testing.T) { + resp := registrySuseComResp + resp.Header["Www-Authenticate"] = []string{ + `Bearer realm="https://registry.suse.com/auth",service="SUSE Linux Docker Registry",scope="registry:catalog:*,error="random_error"`, + } + + needsRetry, _ := needsRetryWithUpdatedScope(nil, &resp) + + if needsRetry { + t.Fatal("Expected no need to retry, as no insufficient_error is present in the authentication header") + } +} + +func TestNeedsRetryNoRetryWhenInvalidScope(t *testing.T) { + resp := registrySuseComResp + resp.Header["Www-Authenticate"] = []string{ + `Bearer realm="https://registry.suse.com/auth",service="SUSE Linux Docker Registry",scope="foo:bar",error="insufficient_scope"`, + } + + needsRetry, _ := needsRetryWithUpdatedScope(nil, &resp) + + if needsRetry { + t.Fatal("Expected no need to retry, as no insufficient_error is present in the authentication header") + } +} + +func TestNeedsNoRetry(t *testing.T) { + resp := http.Response{ + Status: "200 OK", + StatusCode: http.StatusOK, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: map[string][]string{"Apptime": {"D=49722"}, + "Content-Length": {"1683"}, + "Content-Type": {"application/json; charset=utf-8"}, + "Date": {"Fri, 26 Aug 2022 09:00:21 GMT"}, + "Docker-Distribution-Api-Version": {"registry/2.0"}, + "Link": {`; rel="next"`}, + "Referrer-Policy": {"same-origin"}, + "Server": {"Apache"}, + "Strict-Transport-Security": {"max-age=31536000; includeSubDomains; preload"}, + "Vary": {"Accept"}, + "X-Content-Type-Options": {"nosniff"}, + "X-Fedora-Proxyserver": {"proxy10.iad2.fedoraproject.org"}, + "X-Fedora-Requestid": {"YwiLpHEhLsbSTugJblBF8QAAAEI"}, + "X-Frame-Options": {"SAMEORIGIN"}, + "X-Xss-Protection": {"1; mode=block"}, + }, + } + + needsRetry, _ := needsRetryWithUpdatedScope(nil, &resp) + if needsRetry { + t.Fatal("Got the need to retry, but none should be required") + } +} + +func TestIsManifestUnknownError(t *testing.T) { + // Mostly a smoke test; we can add more registries here if they need special handling. + + for _, c := range []struct{ name, response string }{ + { + name: "docker.io when a tag in an _existing repo_ is not found", + response: "HTTP/1.1 404 Not Found\r\n" + + "Connection: close\r\n" + + "Content-Length: 109\r\n" + + "Content-Type: application/json\r\n" + + "Date: Thu, 12 Aug 2021 20:51:32 GMT\r\n" + + "Docker-Distribution-Api-Version: registry/2.0\r\n" + + "Ratelimit-Limit: 100;w=21600\r\n" + + "Ratelimit-Remaining: 100;w=21600\r\n" + + "Strict-Transport-Security: max-age=31536000\r\n" + + "\r\n" + + "{\"errors\":[{\"code\":\"MANIFEST_UNKNOWN\",\"message\":\"manifest unknown\",\"detail\":{\"Tag\":\"this-does-not-exist\"}}]}\n", + }, + { + name: "registry.redhat.io/v2/this-does-not-exist/manifests/latest", + response: "HTTP/1.1 404 Not Found\r\n" + + "Connection: close\r\n" + + "Content-Length: 53\r\n" + + "Cache-Control: max-age=0, no-cache, no-store\r\n" + + "Content-Type: application/json\r\n" + + "Date: Thu, 13 Oct 2022 18:15:15 GMT\r\n" + + "Expires: Thu, 13 Oct 2022 18:15:15 GMT\r\n" + + "Pragma: no-cache\r\n" + + "Server: Apache\r\n" + + "Strict-Transport-Security: max-age=63072000; includeSubdomains; preload\r\n" + + "X-Hostname: crane-tbr06.cran-001.prod.iad2.dc.redhat.com\r\n" + + "\r\n" + + "{\"errors\": [{\"code\": \"404\", \"message\": \"Not Found\"}]}\r\n", + }, + { + name: "registry.redhat.io/v2/rhosp15-rhel8/openstack-cron/manifests/sha256-8df5e60c42668706ac108b59c559b9187fa2de7e4e262e2967e3e9da35d5a8d7.sig", + response: "HTTP/1.1 404 Not Found\r\n" + + "Connection: close\r\n" + + "Content-Length: 10\r\n" + + "Accept-Ranges: bytes\r\n" + + "Date: Thu, 13 Oct 2022 18:13:53 GMT\r\n" + + "Server: AkamaiNetStorage\r\n" + + "X-Docker-Size: -1\r\n" + + "\r\n" + + "Not found\r\n", + }, + } { + resp, err := http.ReadResponse(bufio.NewReader(bytes.NewReader([]byte(c.response))), nil) + require.NoError(t, err, c.name) + defer resp.Body.Close() + err = fmt.Errorf("wrapped: %w", registryHTTPResponseToError(resp)) + + res := isManifestUnknownError(err) + assert.True(t, res, "%#v", err, c.name) + } +} diff --git a/vendor/github.com/containers/image/v5/docker/docker_image_dest.go b/vendor/github.com/containers/image/v5/docker/docker_image_dest.go index 44e2aea23d9..63e372d6776 100644 --- a/vendor/github.com/containers/image/v5/docker/docker_image_dest.go +++ b/vendor/github.com/containers/image/v5/docker/docker_image_dest.go @@ -321,13 +321,21 @@ func (d *dockerImageDestination) TryReusingBlobWithOptions(ctx context.Context, return false, private.ReusedBlob{}, errors.New("Can not check for a blob with unknown digest") } - // First, check whether the blob happens to already exist at the destination. - haveBlob, reusedInfo, err := d.tryReusingExactBlob(ctx, info, options.Cache) - if err != nil { - return false, private.ReusedBlob{}, err - } - if haveBlob { - return true, reusedInfo, nil + if impl.OriginalBlobMatchesRequiredCompression(options) { + // First, check whether the blob happens to already exist at the destination. + haveBlob, reusedInfo, err := d.tryReusingExactBlob(ctx, info, options.Cache) + if err != nil { + return false, private.ReusedBlob{}, err + } + if haveBlob { + return true, reusedInfo, nil + } + } else { + requiredCompression := "nil" + if options.OriginalCompression != nil { + requiredCompression = options.OriginalCompression.Name() + } + logrus.Debugf("Ignoring exact blob match case due to compression mismatch ( %s vs %s )", options.RequiredCompression.Name(), requiredCompression) } // Then try reusing blobs from other locations. @@ -338,6 +346,19 @@ func (d *dockerImageDestination) TryReusingBlobWithOptions(ctx context.Context, logrus.Debugf("Error parsing BlobInfoCache location reference: %s", err) continue } + compressionOperation, compressionAlgorithm, err := blobinfocache.OperationAndAlgorithmForCompressor(candidate.CompressorName) + if err != nil { + logrus.Debugf("OperationAndAlgorithmForCompressor Failed: %v", err) + continue + } + if !impl.BlobMatchesRequiredCompression(options, compressionAlgorithm) { + requiredCompression := "nil" + if compressionAlgorithm != nil { + requiredCompression = compressionAlgorithm.Name() + } + logrus.Debugf("Ignoring candidate blob %s as reuse candidate due to compression mismatch ( %s vs %s ) in %s", candidate.Digest.String(), options.RequiredCompression.Name(), requiredCompression, candidateRepo.Name()) + continue + } if candidate.CompressorName != blobinfocache.Uncompressed { logrus.Debugf("Trying to reuse cached location %s compressed with %s in %s", candidate.Digest.String(), candidate.CompressorName, candidateRepo.Name()) } else { @@ -388,12 +409,6 @@ func (d *dockerImageDestination) TryReusingBlobWithOptions(ctx context.Context, options.Cache.RecordKnownLocation(d.ref.Transport(), bicTransportScope(d.ref), candidate.Digest, newBICLocationReference(d.ref)) - compressionOperation, compressionAlgorithm, err := blobinfocache.OperationAndAlgorithmForCompressor(candidate.CompressorName) - if err != nil { - logrus.Debugf("... Failed: %v", err) - continue - } - return true, private.ReusedBlob{ Digest: candidate.Digest, Size: size, diff --git a/vendor/github.com/containers/image/v5/docker/docker_image_dest_test.go b/vendor/github.com/containers/image/v5/docker/docker_image_dest_test.go new file mode 100644 index 00000000000..8c196e83abd --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/docker_image_dest_test.go @@ -0,0 +1,36 @@ +package docker + +import ( + "bufio" + "bytes" + "net/http" + "testing" + + "github.com/containers/image/v5/internal/private" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var _ private.ImageDestination = (*dockerImageDestination)(nil) + +func TestIsManifestInvalidError(t *testing.T) { + // Sadly only a smoke test; this really should record all known errors exactly as they happen. + + // docker/distribution 2.1.1 when uploading to a tag (because it can’t find a matching tag + // inside the manifest) + response := "HTTP/1.1 400 Bad Request\r\n" + + "Connection: close\r\n" + + "Content-Length: 79\r\n" + + "Content-Type: application/json; charset=utf-8\r\n" + + "Date: Sat, 14 Aug 2021 19:27:29 GMT\r\n" + + "Docker-Distribution-Api-Version: registry/2.0\r\n" + + "\r\n" + + "{\"errors\":[{\"code\":\"TAG_INVALID\",\"message\":\"manifest tag did not match URI\"}]}\n" + resp, err := http.ReadResponse(bufio.NewReader(bytes.NewReader([]byte(response))), nil) + require.NoError(t, err) + defer resp.Body.Close() + err = registryHTTPResponseToError(resp) + + res := isManifestInvalidError(err) + assert.True(t, res, "%#v", err) +} diff --git a/vendor/github.com/containers/image/v5/docker/docker_image_src_test.go b/vendor/github.com/containers/image/v5/docker/docker_image_src_test.go new file mode 100644 index 00000000000..585d3ddf88e --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/docker_image_src_test.go @@ -0,0 +1,213 @@ +package docker + +import ( + "bytes" + "context" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "regexp" + "strings" + "testing" + + "github.com/containers/image/v5/internal/private" + "github.com/containers/image/v5/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var _ private.ImageSource = (*dockerImageSource)(nil) + +func TestDockerImageSourceReference(t *testing.T) { + manifestPathRegex := regexp.MustCompile("^/v2/.*/manifests/latest$") + + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + switch { + case r.Method == http.MethodGet && r.URL.Path == "/v2/": + rw.WriteHeader(http.StatusOK) + case r.Method == http.MethodGet && manifestPathRegex.MatchString(r.URL.Path): + rw.WriteHeader(http.StatusOK) + // Empty body is good enough for this test + default: + require.FailNowf(t, "Unexpected request", "%v %v", r.Method, r.URL.Path) + } + })) + defer server.Close() + registryURL, err := url.Parse(server.URL) + require.NoError(t, err) + registry := registryURL.Host + + mirrorConfiguration := strings.ReplaceAll( + `[[registry]] +prefix = "primary-override.example.com" +location = "@REGISTRY@/primary-override" + +[[registry]] +location = "with-mirror.example.com" + +[[registry.mirror]] +location = "@REGISTRY@/with-mirror" +`, "@REGISTRY@", registry) + registriesConf, err := os.CreateTemp("", "docker-image-src") + require.NoError(t, err) + defer registriesConf.Close() + defer os.Remove(registriesConf.Name()) + err = os.WriteFile(registriesConf.Name(), []byte(mirrorConfiguration), 0600) + require.NoError(t, err) + + for _, c := range []struct{ input, physical string }{ + {registry + "/no-redirection/busybox:latest", registry + "/no-redirection/busybox:latest"}, + {"primary-override.example.com/busybox:latest", registry + "/primary-override/busybox:latest"}, + {"with-mirror.example.com/busybox:latest", registry + "/with-mirror/busybox:latest"}, + } { + ref, err := ParseReference("//" + c.input) + require.NoError(t, err, c.input) + src, err := ref.NewImageSource(context.Background(), &types.SystemContext{ + RegistriesDirPath: "/this/does/not/exist", + DockerPerHostCertDirPath: "/this/does/not/exist", + SystemRegistriesConfPath: registriesConf.Name(), + DockerInsecureSkipTLSVerify: types.OptionalBoolTrue, + }) + require.NoError(t, err, c.input) + defer src.Close() + + // The observable behavior + assert.Equal(t, "//"+c.input, src.Reference().StringWithinTransport(), c.input) + assert.Equal(t, ref.StringWithinTransport(), src.Reference().StringWithinTransport(), c.input) + // Also peek into internal state + src2, ok := src.(*dockerImageSource) + require.True(t, ok, c.input) + assert.Equal(t, "//"+c.input, src2.logicalRef.StringWithinTransport(), c.input) + assert.Equal(t, "//"+c.physical, src2.physicalRef.StringWithinTransport(), c.input) + } +} + +func TestSimplifyContentType(t *testing.T) { + for _, c := range []struct{ input, expected string }{ + {"", ""}, + {"application/json", "application/json"}, + {"application/json;charset=utf-8", "application/json"}, + {"application/json; charset=utf-8", "application/json"}, + {"application/json ; charset=utf-8", "application/json"}, + {"application/json\t;\tcharset=utf-8", "application/json"}, + {"application/json ;charset=utf-8", "application/json"}, + {`application/json; charset="utf-8"`, "application/json"}, + {"completely invalid", ""}, + } { + out := simplifyContentType(c.input) + assert.Equal(t, c.expected, out, c.input) + } +} + +func readNextStream(streams chan io.ReadCloser, errs chan error) ([]byte, error) { + select { + case r := <-streams: + if r == nil { + return nil, nil + } + defer r.Close() + return io.ReadAll(r) + case err := <-errs: + return nil, err + } +} + +type verifyGetBlobAtData struct { + expectedData []byte + expectedError error +} + +func verifyGetBlobAtOutput(t *testing.T, streams chan io.ReadCloser, errs chan error, expected []verifyGetBlobAtData) { + for _, c := range expected { + data, err := readNextStream(streams, errs) + assert.Equal(t, c.expectedData, data) + assert.Equal(t, c.expectedError, err) + } +} + +func TestSplitHTTP200ResponseToPartial(t *testing.T) { + body := io.NopCloser(bytes.NewReader([]byte("123456789"))) + defer body.Close() + streams := make(chan io.ReadCloser) + errs := make(chan error) + chunks := []private.ImageSourceChunk{ + {Offset: 1, Length: 2}, + {Offset: 4, Length: 1}, + } + go splitHTTP200ResponseToPartial(streams, errs, body, chunks) + + expected := []verifyGetBlobAtData{ + {[]byte("23"), nil}, + {[]byte("5"), nil}, + {[]byte(nil), nil}, + } + + verifyGetBlobAtOutput(t, streams, errs, expected) +} + +func TestHandle206Response(t *testing.T) { + body := io.NopCloser(bytes.NewReader([]byte("--AAA\r\n\r\n23\r\n--AAA\r\n\r\n5\r\n--AAA--"))) + defer body.Close() + streams := make(chan io.ReadCloser) + errs := make(chan error) + chunks := []private.ImageSourceChunk{ + {Offset: 1, Length: 2}, + {Offset: 4, Length: 1}, + } + mediaType := "multipart/form-data" + params := map[string]string{ + "boundary": "AAA", + } + go handle206Response(streams, errs, body, chunks, mediaType, params) + + expected := []verifyGetBlobAtData{ + {[]byte("23"), nil}, + {[]byte("5"), nil}, + {[]byte(nil), nil}, + } + verifyGetBlobAtOutput(t, streams, errs, expected) + + body = io.NopCloser(bytes.NewReader([]byte("HELLO"))) + defer body.Close() + streams = make(chan io.ReadCloser) + errs = make(chan error) + chunks = []private.ImageSourceChunk{{Offset: 100, Length: 5}} + mediaType = "text/plain" + params = map[string]string{} + go handle206Response(streams, errs, body, chunks, mediaType, params) + + expected = []verifyGetBlobAtData{ + {[]byte("HELLO"), nil}, + {[]byte(nil), nil}, + } + verifyGetBlobAtOutput(t, streams, errs, expected) +} + +func TestParseMediaType(t *testing.T) { + mediaType, params, err := parseMediaType("multipart/byteranges; boundary=CloudFront:3F750DE0752BEDE3882F7DBE80010D31") + require.NoError(t, err) + assert.Equal(t, mediaType, "multipart/byteranges") + assert.Equal(t, params["boundary"], "CloudFront:3F750DE0752BEDE3882F7DBE80010D31") + + mediaType, params, err = parseMediaType("multipart/byteranges; boundary=00000000000061573284") + require.NoError(t, err) + assert.Equal(t, mediaType, "multipart/byteranges") + assert.Equal(t, params["boundary"], "00000000000061573284") + + mediaType, params, err = parseMediaType("multipart/byteranges; foo=bar; bar=baz") + require.NoError(t, err) + assert.Equal(t, mediaType, "multipart/byteranges") + assert.Equal(t, params["foo"], "bar") + assert.Equal(t, params["bar"], "baz") + + // quoted symbols '@' + _, params, err = parseMediaType("multipart/byteranges; boundary=\"@:\"") + require.NoError(t, err) + assert.Equal(t, params["boundary"], "@:") + + // unquoted '@' + _, _, err = parseMediaType("multipart/byteranges; boundary=@") + require.Error(t, err) +} diff --git a/vendor/github.com/containers/image/v5/docker/docker_transport_test.go b/vendor/github.com/containers/image/v5/docker/docker_transport_test.go new file mode 100644 index 00000000000..1cb3bfcff1c --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/docker_transport_test.go @@ -0,0 +1,206 @@ +package docker + +import ( + "context" + "testing" + + "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + sha256digestHex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + sha256digest = "@sha256:" + sha256digestHex +) + +func TestTransportName(t *testing.T) { + assert.Equal(t, "docker", Transport.Name()) +} + +func TestTransportParseReference(t *testing.T) { + testParseReference(t, Transport.ParseReference) +} + +func TestTransportValidatePolicyConfigurationScope(t *testing.T) { + for _, scope := range []string{ + "docker.io/library/busybox" + sha256digest, + "docker.io/library/busybox:notlatest", + "docker.io/library/busybox", + "docker.io/library", + "docker.io", + "*.io", + } { + err := Transport.ValidatePolicyConfigurationScope(scope) + assert.NoError(t, err, scope) + } +} + +func TestParseReference(t *testing.T) { + testParseReference(t, ParseReference) +} + +// testParseReference is a test shared for Transport.ParseReference and ParseReference. +func testParseReference(t *testing.T, fn func(string) (types.ImageReference, error)) { + for _, c := range []struct{ input, expected string }{ + {"busybox", ""}, // Missing // prefix + {"//busybox:notlatest", "docker.io/library/busybox:notlatest"}, // Explicit tag + {"//busybox" + sha256digest, "docker.io/library/busybox" + sha256digest}, // Explicit digest + {"//busybox", "docker.io/library/busybox:latest"}, // Default tag + // A github.com/distribution/reference value can have a tag and a digest at the same time! + // The docker/distribution API does not really support that (we can’t ask for an image with a specific + // tag and digest), so fail. This MAY be accepted in the future. + {"//busybox:latest" + sha256digest, ""}, // Both tag and digest + {"//docker.io/library/busybox:latest", "docker.io/library/busybox:latest"}, // All implied values explicitly specified + {"//UPPERCASEISINVALID", ""}, // Invalid input + } { + ref, err := fn(c.input) + if c.expected == "" { + assert.Error(t, err, c.input) + } else { + require.NoError(t, err, c.input) + dockerRef, ok := ref.(dockerReference) + require.True(t, ok, c.input) + assert.Equal(t, c.expected, dockerRef.ref.String(), c.input) + } + } +} + +// A common list of reference formats to test for the various ImageReference methods. +var validReferenceTestCases = []struct{ input, dockerRef, stringWithinTransport string }{ + {"busybox:notlatest", "docker.io/library/busybox:notlatest", "//busybox:notlatest"}, // Explicit tag + {"busybox" + sha256digest, "docker.io/library/busybox" + sha256digest, "//busybox" + sha256digest}, // Explicit digest + {"docker.io/library/busybox:latest", "docker.io/library/busybox:latest", "//busybox:latest"}, // All implied values explicitly specified + {"example.com/ns/foo:bar", "example.com/ns/foo:bar", "//example.com/ns/foo:bar"}, // All values explicitly specified +} + +func TestNewReference(t *testing.T) { + for _, c := range validReferenceTestCases { + parsed, err := reference.ParseNormalizedNamed(c.input) + require.NoError(t, err) + ref, err := NewReference(parsed) + require.NoError(t, err, c.input) + dockerRef, ok := ref.(dockerReference) + require.True(t, ok, c.input) + assert.Equal(t, c.dockerRef, dockerRef.ref.String(), c.input) + } + + // Neither a tag nor digest + parsed, err := reference.ParseNormalizedNamed("busybox") + require.NoError(t, err) + _, err = NewReference(parsed) + assert.Error(t, err) + + // A github.com/distribution/reference value can have a tag and a digest at the same time! + parsed, err = reference.ParseNormalizedNamed("busybox:notlatest" + sha256digest) + require.NoError(t, err) + _, ok := parsed.(reference.Canonical) + require.True(t, ok) + _, ok = parsed.(reference.NamedTagged) + require.True(t, ok) + _, err = NewReference(parsed) + assert.Error(t, err) +} + +func TestReferenceTransport(t *testing.T) { + ref, err := ParseReference("//busybox") + require.NoError(t, err) + assert.Equal(t, Transport, ref.Transport()) +} + +func TestReferenceStringWithinTransport(t *testing.T) { + for _, c := range validReferenceTestCases { + ref, err := ParseReference("//" + c.input) + require.NoError(t, err, c.input) + stringRef := ref.StringWithinTransport() + assert.Equal(t, c.stringWithinTransport, stringRef, c.input) + // Do one more round to verify that the output can be parsed, to an equal value. + ref2, err := Transport.ParseReference(stringRef) + require.NoError(t, err, c.input) + stringRef2 := ref2.StringWithinTransport() + assert.Equal(t, stringRef, stringRef2, c.input) + } +} + +func TestReferenceDockerReference(t *testing.T) { + for _, c := range validReferenceTestCases { + ref, err := ParseReference("//" + c.input) + require.NoError(t, err, c.input) + dockerRef := ref.DockerReference() + require.NotNil(t, dockerRef, c.input) + assert.Equal(t, c.dockerRef, dockerRef.String(), c.input) + } +} + +func TestReferencePolicyConfigurationIdentity(t *testing.T) { + // Just a smoke test, the substance is tested in policyconfiguration.TestDockerReference. + ref, err := ParseReference("//busybox") + require.NoError(t, err) + assert.Equal(t, "docker.io/library/busybox:latest", ref.PolicyConfigurationIdentity()) +} + +func TestReferencePolicyConfigurationNamespaces(t *testing.T) { + // Just a smoke test, the substance is tested in policyconfiguration.TestDockerReference. + ref, err := ParseReference("//busybox") + require.NoError(t, err) + assert.Equal(t, []string{ + "docker.io/library/busybox", + "docker.io/library", + "docker.io", + "*.io", + }, ref.PolicyConfigurationNamespaces()) +} + +func TestReferenceNewImage(t *testing.T) { + ref, err := ParseReference("//quay.io/libpod/busybox") + require.NoError(t, err) + img, err := ref.NewImage(context.Background(), &types.SystemContext{ + RegistriesDirPath: "/this/does/not/exist", + DockerPerHostCertDirPath: "/this/does/not/exist", + ArchitectureChoice: "amd64", + OSChoice: "linux", + }) + require.NoError(t, err) + defer img.Close() +} + +func TestReferenceNewImageSource(t *testing.T) { + ref, err := ParseReference("//quay.io/libpod/busybox") + require.NoError(t, err) + src, err := ref.NewImageSource(context.Background(), + &types.SystemContext{RegistriesDirPath: "/this/does/not/exist", DockerPerHostCertDirPath: "/this/does/not/exist"}) + assert.NoError(t, err) + defer src.Close() +} + +func TestReferenceNewImageDestination(t *testing.T) { + ref, err := ParseReference("//quay.io/libpod/busybox") + require.NoError(t, err) + dest, err := ref.NewImageDestination(context.Background(), + &types.SystemContext{RegistriesDirPath: "/this/does/not/exist", DockerPerHostCertDirPath: "/this/does/not/exist"}) + require.NoError(t, err) + defer dest.Close() +} + +func TestReferenceTagOrDigest(t *testing.T) { + for input, expected := range map[string]string{ + "//busybox:notlatest": "notlatest", + "//busybox" + sha256digest: "sha256:" + sha256digestHex, + } { + ref, err := ParseReference(input) + require.NoError(t, err, input) + dockerRef, ok := ref.(dockerReference) + require.True(t, ok, input) + tod, err := dockerRef.tagOrDigest() + require.NoError(t, err, input) + assert.Equal(t, expected, tod, input) + } + + // Invalid input + ref, err := reference.ParseNormalizedNamed("busybox") + require.NoError(t, err) + dockerRef := dockerReference{ref: ref} + _, err = dockerRef.tagOrDigest() + assert.Error(t, err) +} diff --git a/vendor/github.com/containers/image/v5/docker/errors_test.go b/vendor/github.com/containers/image/v5/docker/errors_test.go new file mode 100644 index 00000000000..4149067fec0 --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/errors_test.go @@ -0,0 +1,200 @@ +package docker + +import ( + "bufio" + "bytes" + "errors" + "net/http" + "testing" + + "github.com/docker/distribution/registry/api/errcode" + v2 "github.com/docker/distribution/registry/api/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// NOTE: This test records expected text strings, but NEITHER the returned error types +// NOR the error texts are an API commitment subject to API stability expectations; +// they can change at any time for any reason. +func TestRegistryHTTPResponseToError(t *testing.T) { + var unwrappedUnexpectedHTTPResponseError *unexpectedHTTPResponseError + var unwrappedErrcodeError errcode.Error + for _, c := range []struct { + name string + response string + errorString string + errorType any // A value of the same type as the expected error, or nil + unwrappedErrorPtr any // A pointer to a value expected to be reachable using errors.As, or nil + errorCode *errcode.ErrorCode // A matching ErrorCode, or nil + fn func(t *testing.T, err error) // A more specialized test, or nil + }{ + { + name: "HTTP status out of registry error range", + response: "HTTP/1.1 333 HTTP status out of range\r\n" + + "Header1: Value1\r\n" + + "\r\n" + + "Body of the request\r\n", + errorString: "received unexpected HTTP status: 333 HTTP status out of range", + errorType: &unexpectedHTTPStatusError{}, + }, + { + name: "HTTP body not in expected format", + response: "HTTP/1.1 400 I don't like this request\r\n" + + "Header1: Value1\r\n" + + "\r\n" + + "JSON? What JSON?\r\n", + errorString: "StatusCode: 400, JSON? What JSON?\r\n", + errorType: nil, + unwrappedErrorPtr: &unwrappedUnexpectedHTTPResponseError, + }, + { + name: "401 body not in expected format", + response: "HTTP/1.1 401 I don't like this request\r\n" + + "Header1: Value1\r\n" + + "\r\n" + + "JSON? What JSON?\r\n", + errorString: "authentication required", + errorType: nil, + unwrappedErrorPtr: &unwrappedErrcodeError, + errorCode: &errcode.ErrorCodeUnauthorized, + }, + { // docker.io when an image is not found + name: "GET https://registry-1.docker.io/v2/library/this-does-not-exist/manifests/latest", + response: "HTTP/1.1 401 Unauthorized\r\n" + + "Connection: close\r\n" + + "Content-Length: 170\r\n" + + "Content-Type: application/json\r\n" + + "Date: Thu, 12 Aug 2021 20:11:01 GMT\r\n" + + "Docker-Distribution-Api-Version: registry/2.0\r\n" + + "Strict-Transport-Security: max-age=31536000\r\n" + + "Www-Authenticate: Bearer realm=\"https://auth.docker.io/token\",service=\"registry.docker.io\",scope=\"repository:library/this-does-not-exist:pull\",error=\"insufficient_scope\"\r\n" + + "\r\n" + + "{\"errors\":[{\"code\":\"UNAUTHORIZED\",\"message\":\"authentication required\",\"detail\":[{\"Type\":\"repository\",\"Class\":\"\",\"Name\":\"library/this-does-not-exist\",\"Action\":\"pull\"}]}]}\n", + errorString: "requested access to the resource is denied", + errorType: nil, + unwrappedErrorPtr: &unwrappedErrcodeError, + errorCode: &errcode.ErrorCodeDenied, + }, + { // docker.io when a tag is not found + name: "GET https://registry-1.docker.io/v2/library/busybox/manifests/this-does-not-exist", + response: "HTTP/1.1 404 Not Found\r\n" + + "Connection: close\r\n" + + "Content-Length: 109\r\n" + + "Content-Type: application/json\r\n" + + "Date: Thu, 12 Aug 2021 20:51:32 GMT\r\n" + + "Docker-Distribution-Api-Version: registry/2.0\r\n" + + "Ratelimit-Limit: 100;w=21600\r\n" + + "Ratelimit-Remaining: 100;w=21600\r\n" + + "Strict-Transport-Security: max-age=31536000\r\n" + + "\r\n" + + "{\"errors\":[{\"code\":\"MANIFEST_UNKNOWN\",\"message\":\"manifest unknown\",\"detail\":{\"Tag\":\"this-does-not-exist\"}}]}\n", + errorString: "manifest unknown", + errorType: nil, + unwrappedErrorPtr: &unwrappedErrcodeError, + errorCode: &v2.ErrorCodeManifestUnknown, + }, + { // public.ecr.aws does not implement tag list + name: "GET https://public.ecr.aws/v2/nginx/nginx/tags/list", + response: "HTTP/1.1 404 Not Found\r\n" + + "Connection: close\r\n" + + "Content-Length: 65\r\n" + + "Content-Type: application/json; charset=utf-8\r\n" + + "Date: Tue, 06 Sep 2022 21:19:02 GMT\r\n" + + "Docker-Distribution-Api-Version: registry/2.0\r\n" + + "\r\n" + + "{\"errors\":[{\"code\":\"NOT_FOUND\",\"message\":\"404 page not found\"}]}\r\n", + errorString: "unknown: 404 page not found", + errorType: nil, + unwrappedErrorPtr: &unwrappedErrcodeError, + errorCode: &errcode.ErrorCodeUnknown, + fn: func(t *testing.T, err error) { + var e errcode.Error + ok := errors.As(err, &e) + require.True(t, ok) + // Note: (skopeo inspect) is checking for this errcode.Error value + assert.Equal(t, errcode.Error{ + Code: errcode.ErrorCodeUnknown, // The NOT_FOUND value is not defined, and turns into Unknown + Message: "404 page not found", + Detail: nil, + }, e) + }, + }, + { // registry.redhat.io is not compliant, variant 1: invalid "code" value + name: "registry.redhat.io/v2/this-does-not-exist/manifests/latest", + response: "HTTP/1.1 404 Not Found\r\n" + + "Connection: close\r\n" + + "Content-Length: 53\r\n" + + "Cache-Control: max-age=0, no-cache, no-store\r\n" + + "Content-Type: application/json\r\n" + + "Date: Thu, 13 Oct 2022 18:15:15 GMT\r\n" + + "Expires: Thu, 13 Oct 2022 18:15:15 GMT\r\n" + + "Pragma: no-cache\r\n" + + "Server: Apache\r\n" + + "Strict-Transport-Security: max-age=63072000; includeSubdomains; preload\r\n" + + "X-Hostname: crane-tbr06.cran-001.prod.iad2.dc.redhat.com\r\n" + + "\r\n" + + "{\"errors\": [{\"code\": \"404\", \"message\": \"Not Found\"}]}\r\n", + errorString: "unknown: Not Found", + errorType: errcode.Error{}, + unwrappedErrorPtr: &unwrappedErrcodeError, + errorCode: &errcode.ErrorCodeUnknown, + fn: func(t *testing.T, err error) { + var e errcode.Error + ok := errors.As(err, &e) + require.True(t, ok) + // isManifestUnknownError is checking for this + assert.Equal(t, errcode.Error{ + Code: errcode.ErrorCodeUnknown, // The 404 value is not defined, and turns into Unknown + Message: "Not Found", + Detail: nil, + }, e) + }, + }, + { // registry.redhat.io is not compliant, variant 2: a completely out-of-protocol response + name: "registry.redhat.io/v2/rhosp15-rhel8/openstack-cron/manifests/sha256-8df5e60c42668706ac108b59c559b9187fa2de7e4e262e2967e3e9da35d5a8d7.sig", + response: "HTTP/1.1 404 Not Found\r\n" + + "Connection: close\r\n" + + "Content-Length: 10\r\n" + + "Accept-Ranges: bytes\r\n" + + "Date: Thu, 13 Oct 2022 18:13:53 GMT\r\n" + + "Server: AkamaiNetStorage\r\n" + + "X-Docker-Size: -1\r\n" + + "\r\n" + + "Not found\r\n", + errorString: "StatusCode: 404, Not found\r", + errorType: nil, + unwrappedErrorPtr: &unwrappedUnexpectedHTTPResponseError, + fn: func(t *testing.T, err error) { + var e *unexpectedHTTPResponseError + ok := errors.As(err, &e) + require.True(t, ok) + // isManifestUnknownError is checking for this + assert.Equal(t, 404, e.StatusCode) + assert.Equal(t, []byte("Not found\r"), e.Response) + }, + }, + } { + res, err := http.ReadResponse(bufio.NewReader(bytes.NewReader([]byte(c.response))), nil) + require.NoError(t, err, c.name) + defer res.Body.Close() + + err = registryHTTPResponseToError(res) + assert.Equal(t, c.errorString, err.Error(), c.name) + if c.errorType != nil { + assert.IsType(t, c.errorType, err, c.name) + } + if c.unwrappedErrorPtr != nil { + found := errors.As(err, c.unwrappedErrorPtr) + assert.True(t, found, c.name) + } + if c.errorCode != nil { + var ec errcode.ErrorCoder + ok := errors.As(err, &ec) + require.True(t, ok, c.name) + assert.Equal(t, *c.errorCode, ec.ErrorCode(), c.name) + } + if c.fn != nil { + c.fn(t, err) + } + } +} diff --git a/vendor/github.com/containers/image/v5/docker/fixtures/registries.d/emptyConfig.yaml b/vendor/github.com/containers/image/v5/docker/fixtures/registries.d/emptyConfig.yaml new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/fixtures/registries.d/emptyConfig.yaml @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/docker/fixtures/registries.d/internal-example.com.yaml b/vendor/github.com/containers/image/v5/docker/fixtures/registries.d/internal-example.com.yaml new file mode 100644 index 00000000000..aea25128310 --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/fixtures/registries.d/internal-example.com.yaml @@ -0,0 +1,18 @@ +docker: + example.com: + lookaside: https://lookaside.example.com + registry.test.example.com: + lookaside: http://registry.test.example.com/lookaside + registry.test.example.com:8888: + lookaside: http://registry.test.example.com:8889/lookaside + lookaside-staging: https://registry.test.example.com:8889/lookaside/specialAPIserverWhichDoesNotExist + localhost: + lookaside: file:///home/mitr/mydevelopment1 + localhost:8080: + lookaside: file:///home/mitr/mydevelopment2 + localhost/invalid/url/test: + lookaside: ":emptyscheme" + localhost/file/path/test: + lookaside: "/no/scheme/just/a/path" + localhost/relative/path/test: + lookaside: "no/scheme/relative/path" diff --git a/vendor/github.com/containers/image/v5/docker/fixtures/registries.d/internet-user.yaml b/vendor/github.com/containers/image/v5/docker/fixtures/registries.d/internet-user.yaml new file mode 100644 index 00000000000..89eec0ba879 --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/fixtures/registries.d/internet-user.yaml @@ -0,0 +1,12 @@ +default-docker: + lookaside: file:///mnt/companywide/signatures/for/other/repositories +docker: + docker.io/contoso: + lookaside: https://lookaside.contoso.com/fordocker + docker.io/centos: + lookaside: https://lookaside.centos.org/ + docker.io/centos/mybetaproduct: + lookaside: http://localhost:9999/mybetaWIP/lookaside + lookaside-staging: file:///srv/mybetaWIP/lookaside + docker.io/centos/mybetaproduct:latest: + lookaside: https://lookaside.centos.org/ diff --git a/vendor/github.com/containers/image/v5/docker/fixtures/registries.d/invalid-but.notyaml b/vendor/github.com/containers/image/v5/docker/fixtures/registries.d/invalid-but.notyaml new file mode 100644 index 00000000000..5c34318c214 --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/fixtures/registries.d/invalid-but.notyaml @@ -0,0 +1 @@ +} diff --git a/vendor/github.com/containers/image/v5/docker/internal/tarfile/dest.go b/vendor/github.com/containers/image/v5/docker/internal/tarfile/dest.go index 00e25748bd6..7507d855957 100644 --- a/vendor/github.com/containers/image/v5/docker/internal/tarfile/dest.go +++ b/vendor/github.com/containers/image/v5/docker/internal/tarfile/dest.go @@ -129,6 +129,9 @@ func (d *Destination) PutBlobWithOptions(ctx context.Context, stream io.Reader, // If the blob has been successfully reused, returns (true, info, nil). // If the transport can not reuse the requested blob, TryReusingBlob returns (false, {}, nil); it returns a non-nil error only on an unexpected failure. func (d *Destination) TryReusingBlobWithOptions(ctx context.Context, info types.BlobInfo, options private.TryReusingBlobOptions) (bool, private.ReusedBlob, error) { + if !impl.OriginalBlobMatchesRequiredCompression(options) { + return false, private.ReusedBlob{}, nil + } if err := d.archive.lock(); err != nil { return false, private.ReusedBlob{}, err } diff --git a/vendor/github.com/containers/image/v5/docker/internal/tarfile/src_test.go b/vendor/github.com/containers/image/v5/docker/internal/tarfile/src_test.go new file mode 100644 index 00000000000..1f8248ac567 --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/internal/tarfile/src_test.go @@ -0,0 +1,66 @@ +package tarfile + +import ( + "bytes" + "context" + "io" + "strings" + "testing" + + "github.com/containers/image/v5/manifest" + "github.com/containers/image/v5/pkg/blobinfocache/memory" + "github.com/containers/image/v5/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSourcePrepareLayerData(t *testing.T) { + // Just a smoke test to verify prepareLayerData does not crash on missing data + for _, c := range []struct { + config string + shouldFail bool + }{ + {`{}`, true}, // No RootFS entry: can fail, shouldn’t crash + {`{"rootfs":{}}`, false}, // Useless no-layer configuration + } { + cache := memory.New() + var tarfileBuffer bytes.Buffer + ctx := context.Background() + + writer := NewWriter(&tarfileBuffer) + dest := NewDestination(nil, writer, "transport name", nil) + // No layers + configInfo, err := dest.PutBlob(ctx, strings.NewReader(c.config), + types.BlobInfo{Size: -1}, cache, true) + require.NoError(t, err, c.config) + manifest, err := manifest.Schema2FromComponents( + manifest.Schema2Descriptor{ + MediaType: manifest.DockerV2Schema2ConfigMediaType, + Size: configInfo.Size, + Digest: configInfo.Digest, + }, []manifest.Schema2Descriptor{}).Serialize() + require.NoError(t, err, c.config) + err = dest.PutManifest(ctx, manifest, nil) + require.NoError(t, err, c.config) + err = writer.Close() + require.NoError(t, err, c.config) + + reader, err := NewReaderFromStream(nil, &tarfileBuffer) + require.NoError(t, err, c.config) + src := NewSource(reader, true, "transport name", nil, -1) + require.NoError(t, err, c.config) + defer src.Close() + configStream, _, err := src.GetBlob(ctx, types.BlobInfo{ + Digest: configInfo.Digest, + Size: -1, + }, cache) + if !c.shouldFail { + require.NoError(t, err, c.config) + config2, err := io.ReadAll(configStream) + require.NoError(t, err, c.config) + assert.Equal(t, []byte(c.config), config2, c.config) + } else { + assert.Error(t, err, c.config) + } + } +} diff --git a/vendor/github.com/containers/image/v5/docker/policyconfiguration/naming_test.go b/vendor/github.com/containers/image/v5/docker/policyconfiguration/naming_test.go new file mode 100644 index 00000000000..8f70c66ffd6 --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/policyconfiguration/naming_test.go @@ -0,0 +1,86 @@ +package policyconfiguration + +import ( + "fmt" + "strings" + "testing" + + "github.com/containers/image/v5/docker/reference" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestDockerReference tests DockerReferenceIdentity and DockerReferenceNamespaces simultaneously +// to ensure they are consistent. +func TestDockerReference(t *testing.T) { + sha256Digest := "@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + // Test both that DockerReferenceIdentity returns the expected value (fullName+suffix), + // and that DockerReferenceNamespaces starts with the expected value (fullName), i.e. that the two functions are + // consistent. + for inputName, expectedNS := range map[string][]string{ + "example.com/ns/repo": {"example.com/ns/repo", "example.com/ns", "example.com", "*.com"}, + "example.com/repo": {"example.com/repo", "example.com", "*.com"}, + "localhost/ns/repo": {"localhost/ns/repo", "localhost/ns", "localhost"}, + // Note that "localhost" is special here: notlocalhost/repo is parsed as docker.io/notlocalhost.repo: + "localhost/repo": {"localhost/repo", "localhost"}, + "notlocalhost/repo": {"docker.io/notlocalhost/repo", "docker.io/notlocalhost", "docker.io", "*.io"}, + "docker.io/ns/repo": {"docker.io/ns/repo", "docker.io/ns", "docker.io", "*.io"}, + "docker.io/library/repo": {"docker.io/library/repo", "docker.io/library", "docker.io", "*.io"}, + "docker.io/repo": {"docker.io/library/repo", "docker.io/library", "docker.io", "*.io"}, + "ns/repo": {"docker.io/ns/repo", "docker.io/ns", "docker.io", "*.io"}, + "library/repo": {"docker.io/library/repo", "docker.io/library", "docker.io", "*.io"}, + "repo": {"docker.io/library/repo", "docker.io/library", "docker.io", "*.io"}, + "yet.another.example.com:8443/ns/repo": {"yet.another.example.com:8443/ns/repo", "yet.another.example.com:8443/ns", "yet.another.example.com:8443", "*.another.example.com", "*.example.com", "*.com"}, + } { + for inputSuffix, mappedSuffix := range map[string]string{ + ":tag": ":tag", + sha256Digest: sha256Digest, + } { + fullInput := inputName + inputSuffix + ref, err := reference.ParseNormalizedNamed(fullInput) + require.NoError(t, err, fullInput) + + identity, err := DockerReferenceIdentity(ref) + require.NoError(t, err, fullInput) + assert.Equal(t, expectedNS[0]+mappedSuffix, identity, fullInput) + + ns := DockerReferenceNamespaces(ref) + require.NotNil(t, ns, fullInput) + require.Len(t, ns, len(expectedNS), fullInput) + moreSpecific := identity + for i := range expectedNS { + assert.Equal(t, ns[i], expectedNS[i], fmt.Sprintf("%s item %d", fullInput, i)) + // Verify that expectedNS is ordered from most specific to least specific + if strings.HasPrefix(ns[i], "*.") { + // Check for subdomain matches if wildcard present + assert.True(t, strings.Contains(moreSpecific, ns[i][1:])) + } else { + assert.True(t, strings.HasPrefix(moreSpecific, ns[i])) + } + moreSpecific = ns[i] + } + } + } +} + +func TestDockerReferenceIdentity(t *testing.T) { + // TestDockerReference above has tested the core of the functionality, this tests only the failure cases. + + // Neither a tag nor digest + parsed, err := reference.ParseNormalizedNamed("busybox") + require.NoError(t, err) + id, err := DockerReferenceIdentity(parsed) + assert.Equal(t, "", id) + assert.Error(t, err) + + // A github.com/distribution/reference value can have a tag and a digest at the same time! + parsed, err = reference.ParseNormalizedNamed("busybox:notlatest@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") + require.NoError(t, err) + _, ok := parsed.(reference.Canonical) + require.True(t, ok) + _, ok = parsed.(reference.NamedTagged) + require.True(t, ok) + id, err = DockerReferenceIdentity(parsed) + assert.Equal(t, "", id) + assert.Error(t, err) +} diff --git a/vendor/github.com/containers/image/v5/docker/reference/normalize_test.go b/vendor/github.com/containers/image/v5/docker/reference/normalize_test.go new file mode 100644 index 00000000000..a21c800e146 --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/reference/normalize_test.go @@ -0,0 +1,652 @@ +package reference + +import ( + "strconv" + "testing" + + "github.com/opencontainers/go-digest" +) + +func TestValidateReferenceName(t *testing.T) { + validRepoNames := []string{ + "docker/docker", + "library/debian", + "debian", + "docker.io/docker/docker", + "docker.io/library/debian", + "docker.io/debian", + "index.docker.io/docker/docker", + "index.docker.io/library/debian", + "index.docker.io/debian", + "127.0.0.1:5000/docker/docker", + "127.0.0.1:5000/library/debian", + "127.0.0.1:5000/debian", + "thisisthesongthatneverendsitgoesonandonandonthisisthesongthatnev", + + // This test case was moved from invalid to valid since it is valid input + // when specified with a hostname, it removes the ambiguity from about + // whether the value is an identifier or repository name + "docker.io/1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a", + } + invalidRepoNames := []string{ + "https://github.com/docker/docker", + "docker/Docker", + "-docker", + "-docker/docker", + "-docker.io/docker/docker", + "docker///docker", + "docker.io/docker/Docker", + "docker.io/docker///docker", + "1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a", + } + + for _, name := range invalidRepoNames { + _, err := ParseNormalizedNamed(name) + if err == nil { + t.Fatalf("Expected invalid repo name for %q", name) + } + } + + for _, name := range validRepoNames { + _, err := ParseNormalizedNamed(name) + if err != nil { + t.Fatalf("Error parsing repo name %s, got: %q", name, err) + } + } +} + +func TestValidateRemoteName(t *testing.T) { + validRepositoryNames := []string{ + // Sanity check. + "docker/docker", + + // Allow 64-character non-hexadecimal names (hexadecimal names are forbidden). + "thisisthesongthatneverendsitgoesonandonandonthisisthesongthatnev", + + // Allow embedded hyphens. + "docker-rules/docker", + + // Allow multiple hyphens as well. + "docker---rules/docker", + + // Username doc and image name docker being tested. + "doc/docker", + + // single character names are now allowed. + "d/docker", + "jess/t", + + // Consecutive underscores. + "dock__er/docker", + } + for _, repositoryName := range validRepositoryNames { + _, err := ParseNormalizedNamed(repositoryName) + if err != nil { + t.Errorf("Repository name should be valid: %v. Error: %v", repositoryName, err) + } + } + + invalidRepositoryNames := []string{ + // Disallow capital letters. + "docker/Docker", + + // Only allow one slash. + "docker///docker", + + // Disallow 64-character hexadecimal. + "1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a", + + // Disallow leading and trailing hyphens in namespace. + "-docker/docker", + "docker-/docker", + "-docker-/docker", + + // Don't allow underscores everywhere (as opposed to hyphens). + "____/____", + + "_docker/_docker", + + // Disallow consecutive periods. + "dock..er/docker", + "dock_.er/docker", + "dock-.er/docker", + + // No repository. + "docker/", + + // namespace too long + "this_is_not_a_valid_namespace_because_its_length_is_greater_than_255_this_is_not_a_valid_namespace_because_its_length_is_greater_than_255_this_is_not_a_valid_namespace_because_its_length_is_greater_than_255_this_is_not_a_valid_namespace_because_its_length_is_greater_than_255/docker", + } + for _, repositoryName := range invalidRepositoryNames { + if _, err := ParseNormalizedNamed(repositoryName); err == nil { + t.Errorf("Repository name should be invalid: %v", repositoryName) + } + } +} + +func TestParseRepositoryInfo(t *testing.T) { + type tcase struct { + RemoteName, FamiliarName, FullName, AmbiguousName, Domain string + } + + tcases := []tcase{ + { + RemoteName: "fooo/bar", + FamiliarName: "fooo/bar", + FullName: "docker.io/fooo/bar", + AmbiguousName: "index.docker.io/fooo/bar", + Domain: "docker.io", + }, + { + RemoteName: "library/ubuntu", + FamiliarName: "ubuntu", + FullName: "docker.io/library/ubuntu", + AmbiguousName: "library/ubuntu", + Domain: "docker.io", + }, + { + RemoteName: "nonlibrary/ubuntu", + FamiliarName: "nonlibrary/ubuntu", + FullName: "docker.io/nonlibrary/ubuntu", + AmbiguousName: "", + Domain: "docker.io", + }, + { + RemoteName: "other/library", + FamiliarName: "other/library", + FullName: "docker.io/other/library", + AmbiguousName: "", + Domain: "docker.io", + }, + { + RemoteName: "private/moonbase", + FamiliarName: "127.0.0.1:8000/private/moonbase", + FullName: "127.0.0.1:8000/private/moonbase", + AmbiguousName: "", + Domain: "127.0.0.1:8000", + }, + { + RemoteName: "privatebase", + FamiliarName: "127.0.0.1:8000/privatebase", + FullName: "127.0.0.1:8000/privatebase", + AmbiguousName: "", + Domain: "127.0.0.1:8000", + }, + { + RemoteName: "private/moonbase", + FamiliarName: "example.com/private/moonbase", + FullName: "example.com/private/moonbase", + AmbiguousName: "", + Domain: "example.com", + }, + { + RemoteName: "privatebase", + FamiliarName: "example.com/privatebase", + FullName: "example.com/privatebase", + AmbiguousName: "", + Domain: "example.com", + }, + { + RemoteName: "private/moonbase", + FamiliarName: "example.com:8000/private/moonbase", + FullName: "example.com:8000/private/moonbase", + AmbiguousName: "", + Domain: "example.com:8000", + }, + { + RemoteName: "privatebasee", + FamiliarName: "example.com:8000/privatebasee", + FullName: "example.com:8000/privatebasee", + AmbiguousName: "", + Domain: "example.com:8000", + }, + { + RemoteName: "library/ubuntu-12.04-base", + FamiliarName: "ubuntu-12.04-base", + FullName: "docker.io/library/ubuntu-12.04-base", + AmbiguousName: "index.docker.io/library/ubuntu-12.04-base", + Domain: "docker.io", + }, + { + RemoteName: "library/foo", + FamiliarName: "foo", + FullName: "docker.io/library/foo", + AmbiguousName: "docker.io/foo", + Domain: "docker.io", + }, + { + RemoteName: "library/foo/bar", + FamiliarName: "library/foo/bar", + FullName: "docker.io/library/foo/bar", + AmbiguousName: "", + Domain: "docker.io", + }, + { + RemoteName: "store/foo/bar", + FamiliarName: "store/foo/bar", + FullName: "docker.io/store/foo/bar", + AmbiguousName: "", + Domain: "docker.io", + }, + } + + for _, tcase := range tcases { + refStrings := []string{tcase.FamiliarName, tcase.FullName} + if tcase.AmbiguousName != "" { + refStrings = append(refStrings, tcase.AmbiguousName) + } + + var refs []Named + for _, r := range refStrings { + named, err := ParseNormalizedNamed(r) + if err != nil { + t.Fatal(err) + } + refs = append(refs, named) + } + + for _, r := range refs { + if expected, actual := tcase.FamiliarName, FamiliarName(r); expected != actual { + t.Fatalf("Invalid normalized reference for %q. Expected %q, got %q", r, expected, actual) + } + if expected, actual := tcase.FullName, r.String(); expected != actual { + t.Fatalf("Invalid canonical reference for %q. Expected %q, got %q", r, expected, actual) + } + if expected, actual := tcase.Domain, Domain(r); expected != actual { + t.Fatalf("Invalid domain for %q. Expected %q, got %q", r, expected, actual) + } + if expected, actual := tcase.RemoteName, Path(r); expected != actual { + t.Fatalf("Invalid remoteName for %q. Expected %q, got %q", r, expected, actual) + } + } + } +} + +func TestParseReferenceWithTagAndDigest(t *testing.T) { + shortRef := "busybox:latest@sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa" + ref, err := ParseNormalizedNamed(shortRef) + if err != nil { + t.Fatal(err) + } + if expected, actual := "docker.io/library/"+shortRef, ref.String(); actual != expected { + t.Fatalf("Invalid parsed reference for %q: expected %q, got %q", ref, expected, actual) + } + + if _, isTagged := ref.(NamedTagged); !isTagged { + t.Fatalf("Reference from %q should support tag", ref) + } + if _, isCanonical := ref.(Canonical); !isCanonical { + t.Fatalf("Reference from %q should support digest", ref) + } + if expected, actual := shortRef, FamiliarString(ref); actual != expected { + t.Fatalf("Invalid parsed reference for %q: expected %q, got %q", ref, expected, actual) + } +} + +func TestInvalidReferenceComponents(t *testing.T) { + if _, err := ParseNormalizedNamed("-foo"); err == nil { + t.Fatal("Expected WithName to detect invalid name") + } + ref, err := ParseNormalizedNamed("busybox") + if err != nil { + t.Fatal(err) + } + if _, err := WithTag(ref, "-foo"); err == nil { + t.Fatal("Expected WithName to detect invalid tag") + } + if _, err := WithDigest(ref, digest.Digest("foo")); err == nil { + t.Fatal("Expected WithDigest to detect invalid digest") + } +} + +func equalReference(r1, r2 Reference) bool { + switch v1 := r1.(type) { + case digestReference: + if v2, ok := r2.(digestReference); ok { + return v1 == v2 + } + case repository: + if v2, ok := r2.(repository); ok { + return v1 == v2 + } + case taggedReference: + if v2, ok := r2.(taggedReference); ok { + return v1 == v2 + } + case canonicalReference: + if v2, ok := r2.(canonicalReference); ok { + return v1 == v2 + } + case reference: + if v2, ok := r2.(reference); ok { + return v1 == v2 + } + } + return false +} + +func TestParseAnyReference(t *testing.T) { + tcases := []struct { + Reference string + Equivalent string + Expected Reference + }{ + { + Reference: "redis", + Equivalent: "docker.io/library/redis", + }, + { + Reference: "redis:latest", + Equivalent: "docker.io/library/redis:latest", + }, + { + Reference: "docker.io/library/redis:latest", + Equivalent: "docker.io/library/redis:latest", + }, + { + Reference: "redis@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", + Equivalent: "docker.io/library/redis@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", + }, + { + Reference: "docker.io/library/redis@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", + Equivalent: "docker.io/library/redis@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", + }, + { + Reference: "dmcgowan/myapp", + Equivalent: "docker.io/dmcgowan/myapp", + }, + { + Reference: "dmcgowan/myapp:latest", + Equivalent: "docker.io/dmcgowan/myapp:latest", + }, + { + Reference: "docker.io/mcgowan/myapp:latest", + Equivalent: "docker.io/mcgowan/myapp:latest", + }, + { + Reference: "dmcgowan/myapp@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", + Equivalent: "docker.io/dmcgowan/myapp@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", + }, + { + Reference: "docker.io/dmcgowan/myapp@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", + Equivalent: "docker.io/dmcgowan/myapp@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", + }, + { + Reference: "dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", + Expected: digestReference("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"), + Equivalent: "sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", + }, + { + Reference: "sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", + Expected: digestReference("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"), + Equivalent: "sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c", + }, + { + Reference: "dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9", + Equivalent: "docker.io/library/dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9", + }, + } + + for _, tcase := range tcases { + var ref Reference + var err error + ref, err = ParseAnyReference(tcase.Reference) + if err != nil { + t.Fatalf("Error parsing reference %s: %v", tcase.Reference, err) + } + if ref.String() != tcase.Equivalent { + t.Fatalf("Unexpected string: %s, expected %s", ref.String(), tcase.Equivalent) + } + + expected := tcase.Expected + if expected == nil { + expected, err = Parse(tcase.Equivalent) + if err != nil { + t.Fatalf("Error parsing reference %s: %v", tcase.Equivalent, err) + } + } + if !equalReference(ref, expected) { + t.Errorf("Unexpected reference %#v, expected %#v", ref, expected) + } + } +} + +func TestNormalizedSplitHostname(t *testing.T) { + testcases := []struct { + input string + domain string + name string + }{ + { + input: "test.com/foo", + domain: "test.com", + name: "foo", + }, + { + input: "test_com/foo", + domain: "docker.io", + name: "test_com/foo", + }, + { + input: "docker/migrator", + domain: "docker.io", + name: "docker/migrator", + }, + { + input: "test.com:8080/foo", + domain: "test.com:8080", + name: "foo", + }, + { + input: "test-com:8080/foo", + domain: "test-com:8080", + name: "foo", + }, + { + input: "foo", + domain: "docker.io", + name: "library/foo", + }, + { + input: "xn--n3h.com/foo", + domain: "xn--n3h.com", + name: "foo", + }, + { + input: "xn--n3h.com:18080/foo", + domain: "xn--n3h.com:18080", + name: "foo", + }, + { + input: "docker.io/foo", + domain: "docker.io", + name: "library/foo", + }, + { + input: "docker.io/library/foo", + domain: "docker.io", + name: "library/foo", + }, + { + input: "docker.io/library/foo/bar", + domain: "docker.io", + name: "library/foo/bar", + }, + } + for _, testcase := range testcases { + failf := func(format string, v ...interface{}) { + t.Logf(strconv.Quote(testcase.input)+": "+format, v...) + t.Fail() + } + + named, err := ParseNormalizedNamed(testcase.input) + if err != nil { + failf("error parsing name: %s", err) + } + domain, name := SplitHostname(named) + if domain != testcase.domain { + failf("unexpected domain: got %q, expected %q", domain, testcase.domain) + } + if name != testcase.name { + failf("unexpected name: got %q, expected %q", name, testcase.name) + } + } +} + +func TestMatchError(t *testing.T) { + named, err := ParseAnyReference("foo") + if err != nil { + t.Fatal(err) + } + _, err = FamiliarMatch("[-x]", named) + if err == nil { + t.Fatalf("expected an error, got nothing") + } +} + +func TestMatch(t *testing.T) { + matchCases := []struct { + reference string + pattern string + expected bool + }{ + { + reference: "foo", + pattern: "foo/**/ba[rz]", + expected: false, + }, + { + reference: "foo/any/bat", + pattern: "foo/**/ba[rz]", + expected: false, + }, + { + reference: "foo/a/bar", + pattern: "foo/**/ba[rz]", + expected: true, + }, + { + reference: "foo/b/baz", + pattern: "foo/**/ba[rz]", + expected: true, + }, + { + reference: "foo/c/baz:tag", + pattern: "foo/**/ba[rz]", + expected: true, + }, + { + reference: "foo/c/baz:tag", + pattern: "foo/*/baz:tag", + expected: true, + }, + { + reference: "foo/c/baz:tag", + pattern: "foo/c/baz:tag", + expected: true, + }, + { + reference: "example.com/foo/c/baz:tag", + pattern: "*/foo/c/baz", + expected: true, + }, + { + reference: "example.com/foo/c/baz:tag", + pattern: "example.com/foo/c/baz", + expected: true, + }, + } + for _, c := range matchCases { + named, err := ParseAnyReference(c.reference) + if err != nil { + t.Fatal(err) + } + actual, err := FamiliarMatch(c.pattern, named) + if err != nil { + t.Fatal(err) + } + if actual != c.expected { + t.Fatalf("expected %s match %s to be %v, was %v", c.reference, c.pattern, c.expected, actual) + } + } +} + +func TestParseDockerRef(t *testing.T) { + testcases := []struct { + name string + input string + expected string + }{ + { + name: "nothing", + input: "busybox", + expected: "docker.io/library/busybox:latest", + }, + { + name: "tag only", + input: "busybox:latest", + expected: "docker.io/library/busybox:latest", + }, + { + name: "digest only", + input: "busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", + expected: "docker.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", + }, + { + name: "path only", + input: "library/busybox", + expected: "docker.io/library/busybox:latest", + }, + { + name: "hostname only", + input: "docker.io/busybox", + expected: "docker.io/library/busybox:latest", + }, + { + name: "no tag", + input: "docker.io/library/busybox", + expected: "docker.io/library/busybox:latest", + }, + { + name: "no path", + input: "docker.io/busybox:latest", + expected: "docker.io/library/busybox:latest", + }, + { + name: "no hostname", + input: "library/busybox:latest", + expected: "docker.io/library/busybox:latest", + }, + { + name: "full reference with tag", + input: "docker.io/library/busybox:latest", + expected: "docker.io/library/busybox:latest", + }, + { + name: "gcr reference without tag", + input: "gcr.io/library/busybox", + expected: "gcr.io/library/busybox:latest", + }, + { + name: "both tag and digest", + input: "gcr.io/library/busybox:latest@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", + expected: "gcr.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", + }, + } + for _, test := range testcases { + t.Run(test.name, func(t *testing.T) { + normalized, err := ParseDockerRef(test.input) + if err != nil { + t.Fatal(err) + } + output := normalized.String() + if output != test.expected { + t.Fatalf("expected %q to be parsed as %v, got %v", test.input, test.expected, output) + } + _, err = Parse(output) + if err != nil { + t.Fatalf("%q should be a valid reference, but got an error: %v", output, err) + } + }) + } +} diff --git a/vendor/github.com/containers/image/v5/docker/reference/reference_test.go b/vendor/github.com/containers/image/v5/docker/reference/reference_test.go new file mode 100644 index 00000000000..ce1a11ddcbe --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/reference/reference_test.go @@ -0,0 +1,657 @@ +package reference + +import ( + _ "crypto/sha256" + _ "crypto/sha512" + "encoding/json" + "strconv" + "strings" + "testing" + + "github.com/opencontainers/go-digest" +) + +func TestReferenceParse(t *testing.T) { + // referenceTestcases is a unified set of testcases for + // testing the parsing of references + referenceTestcases := []struct { + // input is the repository name or name component testcase + input string + // err is the error expected from Parse, or nil + err error + // repository is the string representation for the reference + repository string + // domain is the domain expected in the reference + domain string + // tag is the tag for the reference + tag string + // digest is the digest for the reference (enforces digest reference) + digest string + }{ + { + input: "test_com", + repository: "test_com", + }, + { + input: "test.com:tag", + repository: "test.com", + tag: "tag", + }, + { + input: "test.com:5000", + repository: "test.com", + tag: "5000", + }, + { + input: "test.com/repo:tag", + domain: "test.com", + repository: "test.com/repo", + tag: "tag", + }, + { + input: "test:5000/repo", + domain: "test:5000", + repository: "test:5000/repo", + }, + { + input: "test:5000/repo:tag", + domain: "test:5000", + repository: "test:5000/repo", + tag: "tag", + }, + { + input: "test:5000/repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + domain: "test:5000", + repository: "test:5000/repo", + digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + }, + { + input: "test:5000/repo:tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + domain: "test:5000", + repository: "test:5000/repo", + tag: "tag", + digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + }, + { + input: "test:5000/repo", + domain: "test:5000", + repository: "test:5000/repo", + }, + { + input: "", + err: ErrNameEmpty, + }, + { + input: ":justtag", + err: ErrReferenceInvalidFormat, + }, + { + input: "@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + err: ErrReferenceInvalidFormat, + }, + { + input: "repo@sha256:ffffffffffffffffffffffffffffffffff", + err: digest.ErrDigestInvalidLength, + }, + { + input: "validname@invaliddigest:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + err: digest.ErrDigestUnsupported, + }, + { + input: "Uppercase:tag", + err: ErrNameContainsUppercase, + }, + // FIXME "Uppercase" is incorrectly handled as a domain-name here, therefore passes. + // See https://github.com/docker/distribution/pull/1778, and https://github.com/docker/docker/pull/20175 + // { + // input: "Uppercase/lowercase:tag", + // err: ErrNameContainsUppercase, + // }, + { + input: "test:5000/Uppercase/lowercase:tag", + err: ErrNameContainsUppercase, + }, + { + input: "lowercase:Uppercase", + repository: "lowercase", + tag: "Uppercase", + }, + { + input: strings.Repeat("a/", 128) + "a:tag", + err: ErrNameTooLong, + }, + { + input: strings.Repeat("a/", 127) + "a:tag-puts-this-over-max", + domain: "a", + repository: strings.Repeat("a/", 127) + "a", + tag: "tag-puts-this-over-max", + }, + { + input: "aa/asdf$$^/aa", + err: ErrReferenceInvalidFormat, + }, + { + input: "sub-dom1.foo.com/bar/baz/quux", + domain: "sub-dom1.foo.com", + repository: "sub-dom1.foo.com/bar/baz/quux", + }, + { + input: "sub-dom1.foo.com/bar/baz/quux:some-long-tag", + domain: "sub-dom1.foo.com", + repository: "sub-dom1.foo.com/bar/baz/quux", + tag: "some-long-tag", + }, + { + input: "b.gcr.io/test.example.com/my-app:test.example.com", + domain: "b.gcr.io", + repository: "b.gcr.io/test.example.com/my-app", + tag: "test.example.com", + }, + { + input: "xn--n3h.com/myimage:xn--n3h.com", // ☃.com in punycode + domain: "xn--n3h.com", + repository: "xn--n3h.com/myimage", + tag: "xn--n3h.com", + }, + { + input: "xn--7o8h.com/myimage:xn--7o8h.com@sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", // 🐳.com in punycode + domain: "xn--7o8h.com", + repository: "xn--7o8h.com/myimage", + tag: "xn--7o8h.com", + digest: "sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + }, + { + input: "foo_bar.com:8080", + repository: "foo_bar.com", + tag: "8080", + }, + { + input: "foo/foo_bar.com:8080", + domain: "foo", + repository: "foo/foo_bar.com", + tag: "8080", + }, + } + for _, testcase := range referenceTestcases { + failf := func(format string, v ...interface{}) { + t.Logf(strconv.Quote(testcase.input)+": "+format, v...) + t.Fail() + } + + repo, err := Parse(testcase.input) + if testcase.err != nil { + if err == nil { + failf("missing expected error: %v", testcase.err) + } else if testcase.err != err { + failf("mismatched error: got %v, expected %v", err, testcase.err) + } + continue + } else if err != nil { + failf("unexpected parse error: %v", err) + continue + } + if repo.String() != testcase.input { + failf("mismatched repo: got %q, expected %q", repo.String(), testcase.input) + } + + if named, ok := repo.(Named); ok { + if named.Name() != testcase.repository { + failf("unexpected repository: got %q, expected %q", named.Name(), testcase.repository) + } + domain, _ := SplitHostname(named) + if domain != testcase.domain { + failf("unexpected domain: got %q, expected %q", domain, testcase.domain) + } + } else if testcase.repository != "" || testcase.domain != "" { + failf("expected named type, got %T", repo) + } + + tagged, ok := repo.(Tagged) + if testcase.tag != "" { + if ok { + if tagged.Tag() != testcase.tag { + failf("unexpected tag: got %q, expected %q", tagged.Tag(), testcase.tag) + } + } else { + failf("expected tagged type, got %T", repo) + } + } else if ok { + failf("unexpected tagged type") + } + + digested, ok := repo.(Digested) + if testcase.digest != "" { + if ok { + if digested.Digest().String() != testcase.digest { + failf("unexpected digest: got %q, expected %q", digested.Digest().String(), testcase.digest) + } + } else { + failf("expected digested type, got %T", repo) + } + } else if ok { + failf("unexpected digested type") + } + } +} + +// TestWithNameFailure tests cases where WithName should fail. Cases where it +// should succeed are covered by TestSplitHostname, below. +func TestWithNameFailure(t *testing.T) { + testcases := []struct { + input string + err error + }{ + { + input: "", + err: ErrNameEmpty, + }, + { + input: ":justtag", + err: ErrReferenceInvalidFormat, + }, + { + input: "@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + err: ErrReferenceInvalidFormat, + }, + { + input: "validname@invaliddigest:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + err: ErrReferenceInvalidFormat, + }, + { + input: strings.Repeat("a/", 128) + "a:tag", + err: ErrNameTooLong, + }, + { + input: "aa/asdf$$^/aa", + err: ErrReferenceInvalidFormat, + }, + } + for _, testcase := range testcases { + failf := func(format string, v ...interface{}) { + t.Logf(strconv.Quote(testcase.input)+": "+format, v...) + t.Fail() + } + + _, err := WithName(testcase.input) + if err == nil { + failf("no error parsing name. expected: %s", testcase.err) + } + } +} + +func TestSplitHostname(t *testing.T) { + testcases := []struct { + input string + domain string + name string + }{ + { + input: "test.com/foo", + domain: "test.com", + name: "foo", + }, + { + input: "test_com/foo", + domain: "", + name: "test_com/foo", + }, + { + input: "test:8080/foo", + domain: "test:8080", + name: "foo", + }, + { + input: "test.com:8080/foo", + domain: "test.com:8080", + name: "foo", + }, + { + input: "test-com:8080/foo", + domain: "test-com:8080", + name: "foo", + }, + { + input: "xn--n3h.com:18080/foo", + domain: "xn--n3h.com:18080", + name: "foo", + }, + } + for _, testcase := range testcases { + failf := func(format string, v ...interface{}) { + t.Logf(strconv.Quote(testcase.input)+": "+format, v...) + t.Fail() + } + + named, err := WithName(testcase.input) + if err != nil { + failf("error parsing name: %s", err) + } + domain, name := SplitHostname(named) + if domain != testcase.domain { + failf("unexpected domain: got %q, expected %q", domain, testcase.domain) + } + if name != testcase.name { + failf("unexpected name: got %q, expected %q", name, testcase.name) + } + } +} + +type serializationType struct { + Description string + Field Field +} + +func TestSerialization(t *testing.T) { + testcases := []struct { + description string + input string + name string + tag string + digest string + err error + }{ + { + description: "empty value", + err: ErrNameEmpty, + }, + { + description: "just a name", + input: "example.com:8000/named", + name: "example.com:8000/named", + }, + { + description: "name with a tag", + input: "example.com:8000/named:tagged", + name: "example.com:8000/named", + tag: "tagged", + }, + { + description: "name with digest", + input: "other.com/named@sha256:1234567890098765432112345667890098765432112345667890098765432112", + name: "other.com/named", + digest: "sha256:1234567890098765432112345667890098765432112345667890098765432112", + }, + } + for _, testcase := range testcases { + failf := func(format string, v ...interface{}) { + t.Logf(strconv.Quote(testcase.input)+": "+format, v...) + t.Fail() + } + + m := map[string]string{ + "Description": testcase.description, + "Field": testcase.input, + } + b, err := json.Marshal(m) + if err != nil { + failf("error marshaling: %v", err) + } + t := serializationType{} + + if err := json.Unmarshal(b, &t); err != nil { + if testcase.err == nil { + failf("error unmarshaling: %v", err) + } + if err != testcase.err { + failf("wrong error, expected %v, got %v", testcase.err, err) + } + + continue + } else if testcase.err != nil { + failf("expected error unmarshaling: %v", testcase.err) + } + + if t.Description != testcase.description { + failf("wrong description, expected %q, got %q", testcase.description, t.Description) + } + + ref := t.Field.Reference() + + if named, ok := ref.(Named); ok { + if named.Name() != testcase.name { + failf("unexpected repository: got %q, expected %q", named.Name(), testcase.name) + } + } else if testcase.name != "" { + failf("expected named type, got %T", ref) + } + + tagged, ok := ref.(Tagged) + if testcase.tag != "" { + if ok { + if tagged.Tag() != testcase.tag { + failf("unexpected tag: got %q, expected %q", tagged.Tag(), testcase.tag) + } + } else { + failf("expected tagged type, got %T", ref) + } + } else if ok { + failf("unexpected tagged type") + } + + digested, ok := ref.(Digested) + if testcase.digest != "" { + if ok { + if digested.Digest().String() != testcase.digest { + failf("unexpected digest: got %q, expected %q", digested.Digest().String(), testcase.digest) + } + } else { + failf("expected digested type, got %T", ref) + } + } else if ok { + failf("unexpected digested type") + } + + t = serializationType{ + Description: testcase.description, + Field: AsField(ref), + } + + b2, err := json.Marshal(t) + if err != nil { + failf("error marshaling serialization type: %v", err) + } + + if string(b) != string(b2) { + failf("unexpected serialized value: expected %q, got %q", string(b), string(b2)) + } + + // Ensure t.Field is not implementing "Reference" directly, getting + // around the Reference type system + var fieldInterface interface{} = t.Field + if _, ok := fieldInterface.(Reference); ok { + failf("field should not implement Reference interface") + } + } +} + +func TestWithTag(t *testing.T) { + testcases := []struct { + name string + digest digest.Digest + tag string + combined string + }{ + { + name: "test.com/foo", + tag: "tag", + combined: "test.com/foo:tag", + }, + { + name: "foo", + tag: "tag2", + combined: "foo:tag2", + }, + { + name: "test.com:8000/foo", + tag: "tag4", + combined: "test.com:8000/foo:tag4", + }, + { + name: "test.com:8000/foo", + tag: "TAG5", + combined: "test.com:8000/foo:TAG5", + }, + { + name: "test.com:8000/foo", + digest: "sha256:1234567890098765432112345667890098765", + tag: "TAG5", + combined: "test.com:8000/foo:TAG5@sha256:1234567890098765432112345667890098765", + }, + } + for _, testcase := range testcases { + failf := func(format string, v ...interface{}) { + t.Logf(strconv.Quote(testcase.name)+": "+format, v...) + t.Fail() + } + + named, err := WithName(testcase.name) + if err != nil { + failf("error parsing name: %s", err) + } + if testcase.digest != "" { + canonical, err := WithDigest(named, testcase.digest) + if err != nil { + failf("error adding digest") + } + named = canonical + } + + tagged, err := WithTag(named, testcase.tag) + if err != nil { + failf("WithTag failed: %s", err) + } + if tagged.String() != testcase.combined { + failf("unexpected: got %q, expected %q", tagged.String(), testcase.combined) + } + } +} + +func TestWithDigest(t *testing.T) { + testcases := []struct { + name string + digest digest.Digest + tag string + combined string + }{ + { + name: "test.com/foo", + digest: "sha256:1234567890098765432112345667890098765", + combined: "test.com/foo@sha256:1234567890098765432112345667890098765", + }, + { + name: "foo", + digest: "sha256:1234567890098765432112345667890098765", + combined: "foo@sha256:1234567890098765432112345667890098765", + }, + { + name: "test.com:8000/foo", + digest: "sha256:1234567890098765432112345667890098765", + combined: "test.com:8000/foo@sha256:1234567890098765432112345667890098765", + }, + { + name: "test.com:8000/foo", + digest: "sha256:1234567890098765432112345667890098765", + tag: "latest", + combined: "test.com:8000/foo:latest@sha256:1234567890098765432112345667890098765", + }, + } + for _, testcase := range testcases { + failf := func(format string, v ...interface{}) { + t.Logf(strconv.Quote(testcase.name)+": "+format, v...) + t.Fail() + } + + named, err := WithName(testcase.name) + if err != nil { + failf("error parsing name: %s", err) + } + if testcase.tag != "" { + tagged, err := WithTag(named, testcase.tag) + if err != nil { + failf("error adding tag") + } + named = tagged + } + digested, err := WithDigest(named, testcase.digest) + if err != nil { + failf("WithDigest failed: %s", err) + } + if digested.String() != testcase.combined { + failf("unexpected: got %q, expected %q", digested.String(), testcase.combined) + } + } +} + +func TestParseNamed(t *testing.T) { + testcases := []struct { + input string + domain string + name string + err error + }{ + { + input: "test.com/foo", + domain: "test.com", + name: "foo", + }, + { + input: "test:8080/foo", + domain: "test:8080", + name: "foo", + }, + { + input: "test_com/foo", + err: ErrNameNotCanonical, + }, + { + input: "test.com", + err: ErrNameNotCanonical, + }, + { + input: "foo", + err: ErrNameNotCanonical, + }, + { + input: "library/foo", + err: ErrNameNotCanonical, + }, + { + input: "docker.io/library/foo", + domain: "docker.io", + name: "library/foo", + }, + // Ambiguous case, parser will add "library/" to foo + { + input: "docker.io/foo", + err: ErrNameNotCanonical, + }, + } + for _, testcase := range testcases { + failf := func(format string, v ...interface{}) { + t.Logf(strconv.Quote(testcase.input)+": "+format, v...) + t.Fail() + } + + named, err := ParseNamed(testcase.input) + if err != nil && testcase.err == nil { + failf("error parsing name: %s", err) + continue + } else if err == nil && testcase.err != nil { + failf("parsing succeeded: expected error %v", testcase.err) + continue + } else if err != testcase.err { + failf("unexpected error %v, expected %v", err, testcase.err) + continue + } else if err != nil { + continue + } + + domain, name := SplitHostname(named) + if domain != testcase.domain { + failf("unexpected domain: got %q, expected %q", domain, testcase.domain) + } + if name != testcase.name { + failf("unexpected name: got %q, expected %q", name, testcase.name) + } + } +} diff --git a/vendor/github.com/containers/image/v5/docker/reference/regexp_test.go b/vendor/github.com/containers/image/v5/docker/reference/regexp_test.go new file mode 100644 index 00000000000..289e559fd26 --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/reference/regexp_test.go @@ -0,0 +1,525 @@ +package reference + +import ( + "regexp" + "strings" + "testing" +) + +type regexpMatch struct { + input string + match bool + subs []string +} +type Regex interface { + FindStringSubmatch(s string) []string + NumSubexp() int +} + +func checkRegexp(t *testing.T, r Regex, m regexpMatch) { + matches := r.FindStringSubmatch(m.input) + if m.match && matches != nil { + if len(matches) != (r.NumSubexp()+1) || matches[0] != m.input { + t.Fatalf("Bad match result %#v for %q", matches, m.input) + } + if len(matches) < (len(m.subs) + 1) { + t.Errorf("Expected %d sub matches, only have %d for %q", len(m.subs), len(matches)-1, m.input) + } + for i := range m.subs { + if m.subs[i] != matches[i+1] { + t.Errorf("Unexpected submatch %d: %q, expected %q for %q", i+1, matches[i+1], m.subs[i], m.input) + } + } + } else if m.match { + t.Errorf("Expected match for %q", m.input) + } else if matches != nil { + t.Errorf("Unexpected match for %q", m.input) + } +} + +func TestDomainRegexp(t *testing.T) { + hostcases := []regexpMatch{ + { + input: "test.com", + match: true, + }, + { + input: "test.com:10304", + match: true, + }, + { + input: "test.com:http", + match: false, + }, + { + input: "localhost", + match: true, + }, + { + input: "localhost:8080", + match: true, + }, + { + input: "a", + match: true, + }, + { + input: "a.b", + match: true, + }, + { + input: "ab.cd.com", + match: true, + }, + { + input: "a-b.com", + match: true, + }, + { + input: "-ab.com", + match: false, + }, + { + input: "ab-.com", + match: false, + }, + { + input: "ab.c-om", + match: true, + }, + { + input: "ab.-com", + match: false, + }, + { + input: "ab.com-", + match: false, + }, + { + input: "0101.com", + match: true, // TODO(dmcgowan): valid if this should be allowed + }, + { + input: "001a.com", + match: true, + }, + { + input: "b.gbc.io:443", + match: true, + }, + { + input: "b.gbc.io", + match: true, + }, + { + input: "xn--n3h.com", // ☃.com in punycode + match: true, + }, + { + input: "Asdf.com", // uppercase character + match: true, + }, + } + r := regexp.MustCompile(`^` + DomainRegexp.String() + `$`) + for i := range hostcases { + checkRegexp(t, r, hostcases[i]) + } +} + +func TestFullNameRegexp(t *testing.T) { + if anchoredNameRegexp.NumSubexp() != 2 { + t.Fatalf("anchored name regexp should have two submatches: %v, %v != 2", + anchoredNameRegexp.String(), anchoredNameRegexp.NumSubexp()) + } + + testcases := []regexpMatch{ + { + input: "", + match: false, + }, + { + input: "short", + match: true, + subs: []string{"", "short"}, + }, + { + input: "simple/name", + match: true, + subs: []string{"simple", "name"}, + }, + { + input: "library/ubuntu", + match: true, + subs: []string{"library", "ubuntu"}, + }, + { + input: "docker/stevvooe/app", + match: true, + subs: []string{"docker", "stevvooe/app"}, + }, + { + input: "aa/aa/aa/aa/aa/aa/aa/aa/aa/bb/bb/bb/bb/bb/bb", + match: true, + subs: []string{"aa", "aa/aa/aa/aa/aa/aa/aa/aa/bb/bb/bb/bb/bb/bb"}, + }, + { + input: "aa/aa/bb/bb/bb", + match: true, + subs: []string{"aa", "aa/bb/bb/bb"}, + }, + { + input: "a/a/a/a", + match: true, + subs: []string{"a", "a/a/a"}, + }, + { + input: "a/a/a/a/", + match: false, + }, + { + input: "a//a/a", + match: false, + }, + { + input: "a", + match: true, + subs: []string{"", "a"}, + }, + { + input: "a/aa", + match: true, + subs: []string{"a", "aa"}, + }, + { + input: "a/aa/a", + match: true, + subs: []string{"a", "aa/a"}, + }, + { + input: "foo.com", + match: true, + subs: []string{"", "foo.com"}, + }, + { + input: "foo.com/", + match: false, + }, + { + input: "foo.com:8080/bar", + match: true, + subs: []string{"foo.com:8080", "bar"}, + }, + { + input: "foo.com:http/bar", + match: false, + }, + { + input: "foo.com/bar", + match: true, + subs: []string{"foo.com", "bar"}, + }, + { + input: "foo.com/bar/baz", + match: true, + subs: []string{"foo.com", "bar/baz"}, + }, + { + input: "localhost:8080/bar", + match: true, + subs: []string{"localhost:8080", "bar"}, + }, + { + input: "sub-dom1.foo.com/bar/baz/quux", + match: true, + subs: []string{"sub-dom1.foo.com", "bar/baz/quux"}, + }, + { + input: "blog.foo.com/bar/baz", + match: true, + subs: []string{"blog.foo.com", "bar/baz"}, + }, + { + input: "a^a", + match: false, + }, + { + input: "aa/asdf$$^/aa", + match: false, + }, + { + input: "asdf$$^/aa", + match: false, + }, + { + input: "aa-a/a", + match: true, + subs: []string{"aa-a", "a"}, + }, + { + input: strings.Repeat("a/", 128) + "a", + match: true, + subs: []string{"a", strings.Repeat("a/", 127) + "a"}, + }, + { + input: "a-/a/a/a", + match: false, + }, + { + input: "foo.com/a-/a/a", + match: false, + }, + { + input: "-foo/bar", + match: false, + }, + { + input: "foo/bar-", + match: false, + }, + { + input: "foo-/bar", + match: false, + }, + { + input: "foo/-bar", + match: false, + }, + { + input: "_foo/bar", + match: false, + }, + { + input: "foo_bar", + match: true, + subs: []string{"", "foo_bar"}, + }, + { + input: "foo_bar.com", + match: true, + subs: []string{"", "foo_bar.com"}, + }, + { + input: "foo_bar.com:8080", + match: false, + }, + { + input: "foo_bar.com:8080/app", + match: false, + }, + { + input: "foo.com/foo_bar", + match: true, + subs: []string{"foo.com", "foo_bar"}, + }, + { + input: "____/____", + match: false, + }, + { + input: "_docker/_docker", + match: false, + }, + { + input: "docker_/docker_", + match: false, + }, + { + input: "b.gcr.io/test.example.com/my-app", + match: true, + subs: []string{"b.gcr.io", "test.example.com/my-app"}, + }, + { + input: "xn--n3h.com/myimage", // ☃.com in punycode + match: true, + subs: []string{"xn--n3h.com", "myimage"}, + }, + { + input: "xn--7o8h.com/myimage", // 🐳.com in punycode + match: true, + subs: []string{"xn--7o8h.com", "myimage"}, + }, + { + input: "example.com/xn--7o8h.com/myimage", // 🐳.com in punycode + match: true, + subs: []string{"example.com", "xn--7o8h.com/myimage"}, + }, + { + input: "example.com/some_separator__underscore/myimage", + match: true, + subs: []string{"example.com", "some_separator__underscore/myimage"}, + }, + { + input: "example.com/__underscore/myimage", + match: false, + }, + { + input: "example.com/..dots/myimage", + match: false, + }, + { + input: "example.com/.dots/myimage", + match: false, + }, + { + input: "example.com/nodouble..dots/myimage", + match: false, + }, + { + input: "example.com/nodouble..dots/myimage", + match: false, + }, + { + input: "docker./docker", + match: false, + }, + { + input: ".docker/docker", + match: false, + }, + { + input: "docker-/docker", + match: false, + }, + { + input: "-docker/docker", + match: false, + }, + { + input: "do..cker/docker", + match: false, + }, + { + input: "do__cker:8080/docker", + match: false, + }, + { + input: "do__cker/docker", + match: true, + subs: []string{"", "do__cker/docker"}, + }, + { + input: "b.gcr.io/test.example.com/my-app", + match: true, + subs: []string{"b.gcr.io", "test.example.com/my-app"}, + }, + { + input: "registry.io/foo/project--id.module--name.ver---sion--name", + match: true, + subs: []string{"registry.io", "foo/project--id.module--name.ver---sion--name"}, + }, + { + input: "Asdf.com/foo/bar", // uppercase character in hostname + match: true, + }, + { + input: "Foo/FarB", // uppercase characters in remote name + match: false, + }, + } + for i := range testcases { + checkRegexp(t, anchoredNameRegexp, testcases[i]) + } +} + +func TestReferenceRegexp(t *testing.T) { + if ReferenceRegexp.NumSubexp() != 3 { + t.Fatalf("anchored name regexp should have three submatches: %v, %v != 3", + ReferenceRegexp, ReferenceRegexp.NumSubexp()) + } + + testcases := []regexpMatch{ + { + input: "registry.com:8080/myapp:tag", + match: true, + subs: []string{"registry.com:8080/myapp", "tag", ""}, + }, + { + input: "registry.com:8080/myapp@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", + match: true, + subs: []string{"registry.com:8080/myapp", "", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"}, + }, + { + input: "registry.com:8080/myapp:tag2@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", + match: true, + subs: []string{"registry.com:8080/myapp", "tag2", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"}, + }, + { + input: "registry.com:8080/myapp@sha256:badbadbadbad", + match: false, + }, + { + input: "registry.com:8080/myapp:invalid~tag", + match: false, + }, + { + input: "bad_hostname.com:8080/myapp:tag", + match: false, + }, + { + input:// localhost treated as name, missing tag with 8080 as tag + "localhost:8080@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", + match: true, + subs: []string{"localhost", "8080", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"}, + }, + { + input: "localhost:8080/name@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", + match: true, + subs: []string{"localhost:8080/name", "", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"}, + }, + { + input: "localhost:http/name@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", + match: false, + }, + { + // localhost will be treated as an image name without a host + input: "localhost@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", + match: true, + subs: []string{"localhost", "", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"}, + }, + { + input: "registry.com:8080/myapp@bad", + match: false, + }, + { + input: "registry.com:8080/myapp@2bad", + match: false, // TODO(dmcgowan): Support this as valid + }, + } + + for i := range testcases { + checkRegexp(t, ReferenceRegexp, testcases[i]) + } + +} + +func TestIdentifierRegexp(t *testing.T) { + fullCases := []regexpMatch{ + { + input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf9821", + match: true, + }, + { + input: "7EC43B381E5AEFE6E04EFB0B3F0693FF2A4A50652D64AEC573905F2DB5889A1C", + match: false, + }, + { + input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf", + match: false, + }, + { + input: "sha256:da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf9821", + match: false, + }, + { + input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf98218482", + match: false, + }, + } + + for i := range fullCases { + checkRegexp(t, anchoredIdentifierRegexp, fullCases[i]) + if IsFullIdentifier(fullCases[i].input) != fullCases[i].match { + t.Errorf("Expected match for %q to be %v", fullCases[i].input, fullCases[i].match) + } + } +} diff --git a/vendor/github.com/containers/image/v5/docker/registries_d_test.go b/vendor/github.com/containers/image/v5/docker/registries_d_test.go new file mode 100644 index 00000000000..8ff505898b2 --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/registries_d_test.go @@ -0,0 +1,336 @@ +package docker + +import ( + "fmt" + "net/url" + "os" + "path/filepath" + "testing" + + "github.com/containers/image/v5/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func dockerRefFromString(t *testing.T, s string) dockerReference { + ref, err := ParseReference(s) + require.NoError(t, err, s) + dockerRef, ok := ref.(dockerReference) + require.True(t, ok, s) + return dockerRef +} + +func TestSignatureStorageBaseURL(t *testing.T) { + emptyDir := t.TempDir() + for _, c := range []struct { + dir, ref string + expected string // Or "" to expect failure + }{ + { // Error reading configuration directory (/dev/null is not a directory) + "/dev/null", "//busybox", + "", + }, + { // No match found: expect default user storage base + emptyDir, "//this/is/not/in/the:configuration", + "file://" + filepath.Join(os.Getenv("HOME"), defaultUserDockerDir, "//this/is/not/in/the"), + }, + { // Invalid URL + "fixtures/registries.d", "//localhost/invalid/url/test", + "", + }, + // URLs without a scheme: This will be rejected by consumers, so we don't really care about + // the returned value, but it should not crash at the very least. + { // Absolute path + "fixtures/registries.d", "//localhost/file/path/test", + "/no/scheme/just/a/path/file/path/test", + }, + { // Relative path + "fixtures/registries.d", "//localhost/relative/path/test", + "no/scheme/relative/path/relative/path/test", + }, + { // Success + "fixtures/registries.d", "//example.com/my/project", + "https://lookaside.example.com/my/project", + }, + } { + base, err := SignatureStorageBaseURL(&types.SystemContext{RegistriesDirPath: c.dir}, + dockerRefFromString(t, c.ref), false) + if c.expected != "" { + require.NoError(t, err, c.ref) + require.NotNil(t, base, c.ref) + assert.Equal(t, c.expected, base.String(), c.ref) + } else { + assert.Error(t, err, c.ref) + } + } +} + +func TestRegistriesDirPath(t *testing.T) { + const nondefaultPath = "/this/is/not/the/default/registries.d" + const variableReference = "$HOME" + const rootPrefix = "/root/prefix" + tempHome := t.TempDir() + var userRegistriesDir = filepath.FromSlash(".config/containers/registries.d") + userRegistriesDirPath := filepath.Join(tempHome, userRegistriesDir) + for _, c := range []struct { + sys *types.SystemContext + userFilePresent bool + expected string + }{ + // The common case + {nil, false, systemRegistriesDirPath}, + // There is a context, but it does not override the path. + {&types.SystemContext{}, false, systemRegistriesDirPath}, + // Path overridden + {&types.SystemContext{RegistriesDirPath: nondefaultPath}, false, nondefaultPath}, + // Root overridden + { + &types.SystemContext{RootForImplicitAbsolutePaths: rootPrefix}, + false, + filepath.Join(rootPrefix, systemRegistriesDirPath), + }, + // Root and path overrides present simultaneously, + { + &types.SystemContext{ + RootForImplicitAbsolutePaths: rootPrefix, + RegistriesDirPath: nondefaultPath, + }, + false, + nondefaultPath, + }, + // User registries.d present, not overridden + {&types.SystemContext{}, true, userRegistriesDirPath}, + // Context and user User registries.d preset simultaneously + {&types.SystemContext{RegistriesDirPath: nondefaultPath}, true, nondefaultPath}, + // Root and user registries.d overrides present simultaneously, + { + &types.SystemContext{ + RootForImplicitAbsolutePaths: rootPrefix, + RegistriesDirPath: nondefaultPath, + }, + true, + nondefaultPath, + }, + // No environment expansion happens in the overridden paths + {&types.SystemContext{RegistriesDirPath: variableReference}, false, variableReference}, + } { + if c.userFilePresent { + err := os.MkdirAll(userRegistriesDirPath, 0700) + require.NoError(t, err) + } else { + err := os.RemoveAll(userRegistriesDirPath) + require.NoError(t, err) + } + path := registriesDirPathWithHomeDir(c.sys, tempHome) + assert.Equal(t, c.expected, path) + } +} + +func TestLoadAndMergeConfig(t *testing.T) { + tmpDir := t.TempDir() + + // No registries.d exists + config, err := loadAndMergeConfig(filepath.Join(tmpDir, "thisdoesnotexist")) + require.NoError(t, err) + assert.Equal(t, ®istryConfiguration{Docker: map[string]registryNamespace{}}, config) + + // Empty registries.d directory + emptyDir := filepath.Join(tmpDir, "empty") + err = os.Mkdir(emptyDir, 0755) + require.NoError(t, err) + config, err = loadAndMergeConfig(emptyDir) + require.NoError(t, err) + assert.Equal(t, ®istryConfiguration{Docker: map[string]registryNamespace{}}, config) + + // Unreadable registries.d directory + unreadableDir := filepath.Join(tmpDir, "unreadable") + err = os.Mkdir(unreadableDir, 0000) + require.NoError(t, err) + _, err = loadAndMergeConfig(unreadableDir) + assert.Error(t, err) + + // An unreadable file in a registries.d directory + unreadableFileDir := filepath.Join(tmpDir, "unreadableFile") + err = os.Mkdir(unreadableFileDir, 0755) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(unreadableFileDir, "0.yaml"), []byte("{}"), 0644) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(unreadableFileDir, "1.yaml"), nil, 0000) + require.NoError(t, err) + _, err = loadAndMergeConfig(unreadableFileDir) + assert.Error(t, err) + + // Invalid YAML + invalidYAMLDir := filepath.Join(tmpDir, "invalidYAML") + err = os.Mkdir(invalidYAMLDir, 0755) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(invalidYAMLDir, "0.yaml"), []byte("}"), 0644) + require.NoError(t, err) + _, err = loadAndMergeConfig(invalidYAMLDir) + assert.Error(t, err) + + // Duplicate DefaultDocker + duplicateDefault := filepath.Join(tmpDir, "duplicateDefault") + err = os.Mkdir(duplicateDefault, 0755) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(duplicateDefault, "0.yaml"), + []byte("default-docker:\n lookaside: file:////tmp/something"), 0644) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(duplicateDefault, "1.yaml"), + []byte("default-docker:\n lookaside: file:////tmp/different"), 0644) + require.NoError(t, err) + _, err = loadAndMergeConfig(duplicateDefault) + assert.ErrorContains(t, err, "0.yaml") + assert.ErrorContains(t, err, "1.yaml") + + // Duplicate DefaultDocker + duplicateNS := filepath.Join(tmpDir, "duplicateNS") + err = os.Mkdir(duplicateNS, 0755) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(duplicateNS, "0.yaml"), + []byte("docker:\n example.com:\n lookaside: file:////tmp/something"), 0644) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(duplicateNS, "1.yaml"), + []byte("docker:\n example.com:\n lookaside: file:////tmp/different"), 0644) + require.NoError(t, err) + _, err = loadAndMergeConfig(duplicateNS) + assert.ErrorContains(t, err, "0.yaml") + assert.ErrorContains(t, err, "1.yaml") + + // A fully worked example, including an empty-dictionary file and a non-.yaml file + config, err = loadAndMergeConfig("fixtures/registries.d") + require.NoError(t, err) + assert.Equal(t, ®istryConfiguration{ + DefaultDocker: ®istryNamespace{Lookaside: "file:///mnt/companywide/signatures/for/other/repositories"}, + Docker: map[string]registryNamespace{ + "example.com": {Lookaside: "https://lookaside.example.com"}, + "registry.test.example.com": {Lookaside: "http://registry.test.example.com/lookaside"}, + "registry.test.example.com:8888": {Lookaside: "http://registry.test.example.com:8889/lookaside", LookasideStaging: "https://registry.test.example.com:8889/lookaside/specialAPIserverWhichDoesNotExist"}, + "localhost": {Lookaside: "file:///home/mitr/mydevelopment1"}, + "localhost:8080": {Lookaside: "file:///home/mitr/mydevelopment2"}, + "localhost/invalid/url/test": {Lookaside: ":emptyscheme"}, + "localhost/file/path/test": {Lookaside: "/no/scheme/just/a/path"}, + "localhost/relative/path/test": {Lookaside: "no/scheme/relative/path"}, + "docker.io/contoso": {Lookaside: "https://lookaside.contoso.com/fordocker"}, + "docker.io/centos": {Lookaside: "https://lookaside.centos.org/"}, + "docker.io/centos/mybetaproduct": { + Lookaside: "http://localhost:9999/mybetaWIP/lookaside", + LookasideStaging: "file:///srv/mybetaWIP/lookaside", + }, + "docker.io/centos/mybetaproduct:latest": {Lookaside: "https://lookaside.centos.org/"}, + }, + }, config) +} + +func TestRegistryConfigurationSignatureTopLevel(t *testing.T) { + config := registryConfiguration{ + DefaultDocker: ®istryNamespace{Lookaside: "=default", LookasideStaging: "=default+w"}, + Docker: map[string]registryNamespace{}, + } + for _, ns := range []string{ + "localhost", + "localhost:5000", + "example.com", + "example.com/ns1", + "example.com/ns1/ns2", + "example.com/ns1/ns2/repo", + "example.com/ns1/ns2/repo:notlatest", + } { + config.Docker[ns] = registryNamespace{Lookaside: ns, LookasideStaging: ns + "+w"} + } + + for _, c := range []struct{ input, expected string }{ + {"example.com/ns1/ns2/repo:notlatest", "example.com/ns1/ns2/repo:notlatest"}, + {"example.com/ns1/ns2/repo:unmatched", "example.com/ns1/ns2/repo"}, + {"example.com/ns1/ns2/notrepo:notlatest", "example.com/ns1/ns2"}, + {"example.com/ns1/notns2/repo:notlatest", "example.com/ns1"}, + {"example.com/notns1/ns2/repo:notlatest", "example.com"}, + {"unknown.example.com/busybox", "=default"}, + {"localhost:5000/busybox", "localhost:5000"}, + {"localhost/busybox", "localhost"}, + {"localhost:9999/busybox", "=default"}, + } { + dr := dockerRefFromString(t, "//"+c.input) + + res := config.signatureTopLevel(dr, false) + assert.Equal(t, c.expected, res, c.input) + res = config.signatureTopLevel(dr, true) // test that forWriting is correctly propagated + assert.Equal(t, c.expected+"+w", res, c.input) + } + + config = registryConfiguration{ + Docker: map[string]registryNamespace{ + "unmatched": {Lookaside: "a", LookasideStaging: "b"}, + }, + } + dr := dockerRefFromString(t, "//thisisnotmatched") + res := config.signatureTopLevel(dr, false) + assert.Equal(t, "", res) + res = config.signatureTopLevel(dr, true) + assert.Equal(t, "", res) +} + +func TestRegistryNamespaceSignatureTopLevel(t *testing.T) { + for _, c := range []struct { + ns registryNamespace + forWriting bool + expected string + }{ + {registryNamespace{LookasideStaging: "a", Lookaside: "b"}, true, "a"}, + {registryNamespace{LookasideStaging: "a", Lookaside: "b"}, false, "b"}, + {registryNamespace{Lookaside: "b"}, true, "b"}, + {registryNamespace{Lookaside: "b"}, false, "b"}, + {registryNamespace{LookasideStaging: "a"}, true, "a"}, + {registryNamespace{LookasideStaging: "a"}, false, ""}, + {registryNamespace{}, true, ""}, + {registryNamespace{}, false, ""}, + + {registryNamespace{LookasideStaging: "a", Lookaside: "b", SigStoreStaging: "c", SigStore: "d"}, true, "a"}, + {registryNamespace{Lookaside: "b", SigStoreStaging: "c", SigStore: "d"}, true, "c"}, + {registryNamespace{Lookaside: "b", SigStore: "d"}, true, "b"}, + {registryNamespace{SigStore: "d"}, true, "d"}, + + {registryNamespace{LookasideStaging: "a", Lookaside: "b", SigStoreStaging: "c", SigStore: "d"}, false, "b"}, + {registryNamespace{Lookaside: "b", SigStoreStaging: "c", SigStore: "d"}, false, "b"}, + {registryNamespace{Lookaside: "b", SigStore: "d"}, false, "b"}, + {registryNamespace{SigStore: "d"}, false, "d"}, + } { + res := c.ns.signatureTopLevel(c.forWriting) + assert.Equal(t, c.expected, res, fmt.Sprintf("%#v %v", c.ns, c.forWriting)) + } +} + +func TestLookasideStorageURL(t *testing.T) { + const mdInput = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + const mdMapped = "sha256=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + for _, c := range []struct { + base string + index int + expected string + }{ + {"file:///tmp", 0, "file:///tmp@" + mdMapped + "/signature-1"}, + {"file:///tmp", 1, "file:///tmp@" + mdMapped + "/signature-2"}, + {"https://localhost:5555/root", 0, "https://localhost:5555/root@" + mdMapped + "/signature-1"}, + {"https://localhost:5555/root", 1, "https://localhost:5555/root@" + mdMapped + "/signature-2"}, + {"http://localhost:5555/root", 0, "http://localhost:5555/root@" + mdMapped + "/signature-1"}, + {"http://localhost:5555/root", 1, "http://localhost:5555/root@" + mdMapped + "/signature-2"}, + } { + baseURL, err := url.Parse(c.base) + require.NoError(t, err) + expectedURL, err := url.Parse(c.expected) + require.NoError(t, err) + res := lookasideStorageURL(baseURL, mdInput, c.index) + assert.Equal(t, expectedURL, res, c.expected) + } +} + +func TestBuiltinDefaultLookasideStorageDir(t *testing.T) { + base := builtinDefaultLookasideStorageDir(0) + assert.NotNil(t, base) + assert.Equal(t, "file://"+defaultDockerDir, base.String()) + + base = builtinDefaultLookasideStorageDir(1000) + assert.NotNil(t, base) + assert.Equal(t, "file://"+filepath.Join(os.Getenv("HOME"), defaultUserDockerDir), base.String()) +} diff --git a/vendor/github.com/containers/image/v5/docker/tarfile/dest.go b/vendor/github.com/containers/image/v5/docker/tarfile/dest.go new file mode 100644 index 00000000000..4547654396e --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/tarfile/dest.go @@ -0,0 +1,119 @@ +package tarfile + +import ( + "context" + "io" + + internal "github.com/containers/image/v5/docker/internal/tarfile" + "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/types" + "github.com/opencontainers/go-digest" +) + +// Destination is a partial implementation of types.ImageDestination for writing to an io.Writer. +type Destination struct { + internal *internal.Destination + archive *internal.Writer +} + +// NewDestination returns a tarfile.Destination for the specified io.Writer. +// Deprecated: please use NewDestinationWithContext instead +func NewDestination(dest io.Writer, ref reference.NamedTagged) *Destination { + return NewDestinationWithContext(nil, dest, ref) +} + +// NewDestinationWithContext returns a tarfile.Destination for the specified io.Writer. +func NewDestinationWithContext(sys *types.SystemContext, dest io.Writer, ref reference.NamedTagged) *Destination { + archive := internal.NewWriter(dest) + return &Destination{ + internal: internal.NewDestination(sys, archive, "[An external docker/tarfile caller]", ref), + archive: archive, + } +} + +// AddRepoTags adds the specified tags to the destination's repoTags. +func (d *Destination) AddRepoTags(tags []reference.NamedTagged) { + d.internal.AddRepoTags(tags) +} + +// SupportedManifestMIMETypes tells which manifest mime types the destination supports +// If an empty slice or nil it's returned, then any mime type can be tried to upload +func (d *Destination) SupportedManifestMIMETypes() []string { + return d.internal.SupportedManifestMIMETypes() +} + +// SupportsSignatures returns an error (to be displayed to the user) if the destination certainly can't store signatures. +// Note: It is still possible for PutSignatures to fail if SupportsSignatures returns nil. +func (d *Destination) SupportsSignatures(ctx context.Context) error { + return d.internal.SupportsSignatures(ctx) +} + +// AcceptsForeignLayerURLs returns false iff foreign layers in manifest should be actually +// uploaded to the image destination, true otherwise. +func (d *Destination) AcceptsForeignLayerURLs() bool { + return d.internal.AcceptsForeignLayerURLs() +} + +// MustMatchRuntimeOS returns true iff the destination can store only images targeted for the current runtime architecture and OS. False otherwise. +func (d *Destination) MustMatchRuntimeOS() bool { + return d.internal.MustMatchRuntimeOS() +} + +// IgnoresEmbeddedDockerReference returns true iff the destination does not care about Image.EmbeddedDockerReferenceConflicts(), +// and would prefer to receive an unmodified manifest instead of one modified for the destination. +// Does not make a difference if Reference().DockerReference() is nil. +func (d *Destination) IgnoresEmbeddedDockerReference() bool { + return d.internal.IgnoresEmbeddedDockerReference() +} + +// HasThreadSafePutBlob indicates whether PutBlob can be executed concurrently. +func (d *Destination) HasThreadSafePutBlob() bool { + return d.internal.HasThreadSafePutBlob() +} + +// PutBlob writes contents of stream and returns data representing the result (with all data filled in). +// inputInfo.Digest can be optionally provided if known; if provided, and stream is read to the end without error, the digest MUST match the stream contents. +// inputInfo.Size is the expected length of stream, if known. +// May update cache. +// WARNING: The contents of stream are being verified on the fly. Until stream.Read() returns io.EOF, the contents of the data SHOULD NOT be available +// to any other readers for download using the supplied digest. +// If stream.Read() at any time, ESPECIALLY at end of input, returns an error, PutBlob MUST 1) fail, and 2) delete any data stored so far. +func (d *Destination) PutBlob(ctx context.Context, stream io.Reader, inputInfo types.BlobInfo, cache types.BlobInfoCache, isConfig bool) (types.BlobInfo, error) { + return d.internal.PutBlob(ctx, stream, inputInfo, cache, isConfig) +} + +// TryReusingBlob checks whether the transport already contains, or can efficiently reuse, a blob, and if so, applies it to the current destination +// (e.g. if the blob is a filesystem layer, this signifies that the changes it describes need to be applied again when composing a filesystem tree). +// info.Digest must not be empty. +// If canSubstitute, TryReusingBlob can use an equivalent equivalent of the desired blob; in that case the returned info may not match the input. +// If the blob has been successfully reused, returns (true, info, nil); info must contain at least a digest and size, and may +// include CompressionOperation and CompressionAlgorithm fields to indicate that a change to the compression type should be +// reflected in the manifest that will be written. +// If the transport can not reuse the requested blob, TryReusingBlob returns (false, {}, nil); it returns a non-nil error only on an unexpected failure. +// May use and/or update cache. +func (d *Destination) TryReusingBlob(ctx context.Context, info types.BlobInfo, cache types.BlobInfoCache, canSubstitute bool) (bool, types.BlobInfo, error) { + return d.internal.TryReusingBlob(ctx, info, cache, canSubstitute) +} + +// PutManifest writes manifest to the destination. +// The instanceDigest value is expected to always be nil, because this transport does not support manifest lists, so +// there can be no secondary manifests. +// FIXME? This should also receive a MIME type if known, to differentiate between schema versions. +// If the destination is in principle available, refuses this manifest type (e.g. it does not recognize the schema), +// but may accept a different manifest type, the returned error must be an ManifestTypeRejectedError. +func (d *Destination) PutManifest(ctx context.Context, m []byte, instanceDigest *digest.Digest) error { + return d.internal.PutManifest(ctx, m, instanceDigest) +} + +// PutSignatures would add the given signatures to the docker tarfile (currently not supported). +// The instanceDigest value is expected to always be nil, because this transport does not support manifest lists, so +// there can be no secondary manifests. MUST be called after PutManifest (signatures reference manifest contents). +func (d *Destination) PutSignatures(ctx context.Context, signatures [][]byte, instanceDigest *digest.Digest) error { + return d.internal.PutSignatures(ctx, signatures, instanceDigest) +} + +// Commit finishes writing data to the underlying io.Writer. +// It is the caller's responsibility to close it, if necessary. +func (d *Destination) Commit(ctx context.Context) error { + return d.archive.Close() +} diff --git a/vendor/github.com/containers/image/v5/docker/tarfile/doc.go b/vendor/github.com/containers/image/v5/docker/tarfile/doc.go new file mode 100644 index 00000000000..4ea5369c05c --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/tarfile/doc.go @@ -0,0 +1,3 @@ +// Package tarfile is an internal implementation detail of some transports. +// Do not use outside of the github.com/containers/image repo! +package tarfile diff --git a/vendor/github.com/containers/image/v5/docker/tarfile/src.go b/vendor/github.com/containers/image/v5/docker/tarfile/src.go new file mode 100644 index 00000000000..067716ce5ae --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/tarfile/src.go @@ -0,0 +1,104 @@ +package tarfile + +import ( + "context" + "io" + + internal "github.com/containers/image/v5/docker/internal/tarfile" + "github.com/containers/image/v5/types" + digest "github.com/opencontainers/go-digest" +) + +// Source is a partial implementation of types.ImageSource for reading from tarPath. +// Most users should use this via implementations of ImageReference from docker/archive or docker/daemon. +type Source struct { + internal *internal.Source +} + +// NewSourceFromFile returns a tarfile.Source for the specified path. +// Deprecated: Please use NewSourceFromFileWithContext which will allows you to configure temp directory +// for big files through SystemContext.BigFilesTemporaryDir +func NewSourceFromFile(path string) (*Source, error) { + return NewSourceFromFileWithContext(nil, path) +} + +// NewSourceFromFileWithContext returns a tarfile.Source for the specified path. +func NewSourceFromFileWithContext(sys *types.SystemContext, path string) (*Source, error) { + archive, err := internal.NewReaderFromFile(sys, path) + if err != nil { + return nil, err + } + src := internal.NewSource(archive, true, "[An external docker/tarfile caller]", nil, -1) + return &Source{internal: src}, nil +} + +// NewSourceFromStream returns a tarfile.Source for the specified inputStream, +// which can be either compressed or uncompressed. The caller can close the +// inputStream immediately after NewSourceFromFile returns. +// Deprecated: Please use NewSourceFromStreamWithSystemContext which will allows you to configure +// temp directory for big files through SystemContext.BigFilesTemporaryDir +func NewSourceFromStream(inputStream io.Reader) (*Source, error) { + return NewSourceFromStreamWithSystemContext(nil, inputStream) +} + +// NewSourceFromStreamWithSystemContext returns a tarfile.Source for the specified inputStream, +// which can be either compressed or uncompressed. The caller can close the +// inputStream immediately after NewSourceFromFile returns. +func NewSourceFromStreamWithSystemContext(sys *types.SystemContext, inputStream io.Reader) (*Source, error) { + archive, err := internal.NewReaderFromStream(sys, inputStream) + if err != nil { + return nil, err + } + src := internal.NewSource(archive, true, "[An external docker/tarfile caller]", nil, -1) + return &Source{internal: src}, nil +} + +// Close removes resources associated with an initialized Source, if any. +func (s *Source) Close() error { + return s.internal.Close() +} + +// LoadTarManifest loads and decodes the manifest.json +func (s *Source) LoadTarManifest() ([]ManifestItem, error) { + return s.internal.TarManifest(), nil +} + +// GetManifest returns the image's manifest along with its MIME type (which may be empty when it can't be determined but the manifest is available). +// It may use a remote (= slow) service. +// If instanceDigest is not nil, it contains a digest of the specific manifest instance to retrieve (when the primary manifest is a manifest list); +// this never happens if the primary manifest is not a manifest list (e.g. if the source never returns manifest lists). +// This source implementation does not support manifest lists, so the passed-in instanceDigest should always be nil, +// as the primary manifest can not be a list, so there can be no secondary instances. +func (s *Source) GetManifest(ctx context.Context, instanceDigest *digest.Digest) ([]byte, string, error) { + return s.internal.GetManifest(ctx, instanceDigest) +} + +// HasThreadSafeGetBlob indicates whether GetBlob can be executed concurrently. +func (s *Source) HasThreadSafeGetBlob() bool { + return s.internal.HasThreadSafeGetBlob() +} + +// GetBlob returns a stream for the specified blob, and the blob’s size (or -1 if unknown). +// The Digest field in BlobInfo is guaranteed to be provided, Size may be -1 and MediaType may be optionally provided. +// May update BlobInfoCache, preferably after it knows for certain that a blob truly exists at a specific location. +func (s *Source) GetBlob(ctx context.Context, info types.BlobInfo, cache types.BlobInfoCache) (io.ReadCloser, int64, error) { + return s.internal.GetBlob(ctx, info, cache) +} + +// GetSignatures returns the image's signatures. It may use a remote (= slow) service. +// This source implementation does not support manifest lists, so the passed-in instanceDigest should always be nil, +// as there can be no secondary manifests. +func (s *Source) GetSignatures(ctx context.Context, instanceDigest *digest.Digest) ([][]byte, error) { + return s.internal.GetSignatures(ctx, instanceDigest) +} + +// LayerInfosForCopy returns either nil (meaning the values in the manifest are fine), or updated values for the layer +// blobsums that are listed in the image's manifest. If values are returned, they should be used when using GetBlob() +// to read the image's layers. +// This source implementation does not support manifest lists, so the passed-in instanceDigest should always be nil, +// as the primary manifest can not be a list, so there can be no secondary manifests. +// The Digest field is guaranteed to be provided; Size may be -1. +// WARNING: The list may contain duplicates, and they are semantically relevant. +func (s *Source) LayerInfosForCopy(ctx context.Context, instanceDigest *digest.Digest) ([]types.BlobInfo, error) { + return s.internal.LayerInfosForCopy(ctx, instanceDigest) +} diff --git a/vendor/github.com/containers/image/v5/docker/tarfile/types.go b/vendor/github.com/containers/image/v5/docker/tarfile/types.go new file mode 100644 index 00000000000..0f14389e6d0 --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/tarfile/types.go @@ -0,0 +1,8 @@ +package tarfile + +import ( + internal "github.com/containers/image/v5/docker/internal/tarfile" +) + +// ManifestItem is an element of the array stored in the top-level manifest.json file. +type ManifestItem = internal.ManifestItem // All public members from the internal package remain accessible. diff --git a/vendor/github.com/containers/image/v5/docker/wwwauthenticate_test.go b/vendor/github.com/containers/image/v5/docker/wwwauthenticate_test.go new file mode 100644 index 00000000000..d11f6fbc96e --- /dev/null +++ b/vendor/github.com/containers/image/v5/docker/wwwauthenticate_test.go @@ -0,0 +1,45 @@ +package docker + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// This is just a smoke test for the common expected header formats, +// by no means comprehensive. +func TestParseValueAndParams(t *testing.T) { + for _, c := range []struct { + input string + scope string + params map[string]string + }{ + { + `Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:library/busybox:pull"`, + "bearer", + map[string]string{ + "realm": "https://auth.docker.io/token", + "service": "registry.docker.io", + "scope": "repository:library/busybox:pull", + }, + }, + { + `Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:library/busybox:pull,push"`, + "bearer", + map[string]string{ + "realm": "https://auth.docker.io/token", + "service": "registry.docker.io", + "scope": "repository:library/busybox:pull,push", + }, + }, + { + `Bearer realm="http://127.0.0.1:5000/openshift/token"`, + "bearer", + map[string]string{"realm": "http://127.0.0.1:5000/openshift/token"}, + }, + } { + scope, params := parseValueAndParams(c.input) + assert.Equal(t, c.scope, scope, c.input) + assert.Equal(t, c.params, params, c.input) + } +} diff --git a/vendor/github.com/containers/image/v5/docs/atomic-signature-embedded-json.json b/vendor/github.com/containers/image/v5/docs/atomic-signature-embedded-json.json new file mode 100644 index 00000000000..5a7c8998c5a --- /dev/null +++ b/vendor/github.com/containers/image/v5/docs/atomic-signature-embedded-json.json @@ -0,0 +1,66 @@ +{ + "title": "JSON embedded in an atomic container signature", + "description": "This schema is a supplement to atomic-signature.md in this directory.\n\nConsumers of the JSON MUST use the processing rules documented in atomic-signature.md, especially the requirements for the 'critical' subobject.\n\nWhenever this schema and atomic-signature.md, or the github.com/containers/image/signature implementation, differ,\nit is the atomic-signature.md document, or the github.com/containers/image/signature implementation, which governs.\n\nUsers are STRONGLY RECOMMENDED to use the github.com/containers/image/signature implementation instead of writing\ntheir own, ESPECIALLY when consuming signatures, so that the policy.json format can be shared by all image consumers.\n", + "type": "object", + "required": [ + "critical", + "optional" + ], + "additionalProperties": false, + "properties": { + "critical": { + "type": "object", + "required": [ + "type", + "image", + "identity" + ], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": [ + "atomic container signature" + ] + }, + "image": { + "type": "object", + "required": [ + "docker-manifest-digest" + ], + "additionalProperties": false, + "properties": { + "docker-manifest-digest": { + "type": "string" + } + } + }, + "identity": { + "type": "object", + "required": [ + "docker-reference" + ], + "additionalProperties": false, + "properties": { + "docker-reference": { + "type": "string" + } + } + } + } + }, + "optional": { + "type": "object", + "description": "All members are optional, but if they are included, they must be valid.", + "additionalProperties": true, + "properties": { + "creator": { + "type": "string" + }, + "timestamp": { + "type": "integer" + } + } + } + } +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/docs/containers-auth.json.5.md b/vendor/github.com/containers/image/v5/docs/containers-auth.json.5.md new file mode 100644 index 00000000000..c5e22b087c7 --- /dev/null +++ b/vendor/github.com/containers/image/v5/docs/containers-auth.json.5.md @@ -0,0 +1,105 @@ +% containers-auth.json 5 + +# NAME +containers-auth.json - syntax for the registry authentication file + +# DESCRIPTION + +A file in JSON format controlling authentication against container image registries. +The primary (read/write) file is stored at `${XDG_RUNTIME_DIR}/containers/auth.json` on Linux; +on Windows and macOS, at `$HOME/.config/containers/auth.json`. + +When searching for the credential for a registry, the following files will be read in sequence until the valid credential is found: +first reading the primary (read/write) file, or the explicit override using an option of the calling application. +If credentials are not present there, +the search continues in `${XDG_CONFIG_HOME}/containers/auth.json` (usually `~/.config/containers/auth.json`), `$HOME/.docker/config.json`, `$HOME/.dockercfg`. + +Except for the primary (read/write) file, other files are read-only unless the user, using an option of the calling application, explicitly points at it as an override. + + +## FORMAT + +The auth.json file stores, or references, credentials that allow the user to authenticate +to container image registries. +It is primarily managed by a `login` command from a container tool such as `podman login`, +`buildah login`, or `skopeo login`. + +Each entry contains a single hostname (e.g., `docker.io`) or a namespace (e.g., `quay.io/user/image`) as a key, +and credentials in the form of a base64-encoded string as value of `auth`. The +base64-encoded string contains a concatenation of the username, a colon, and the +password. + +When checking for available credentials, the relevant repository is matched +against available keys in its hierarchical order, going from most-specific to least-specific. +For example, an image pull for `my-registry.local/namespace/user/image:latest` will +result in a lookup in `auth.json` in the following order: + +- `my-registry.local/namespace/user/image` +- `my-registry.local/namespace/user` +- `my-registry.local/namespace` +- `my-registry.local` + +This way it is possible to setup multiple credentials for a single registry +which can be distinguished by their path. + +The following example shows the values found in auth.json after the user logged in to +their accounts on quay.io and docker.io: + +``` +{ + "auths": { + "docker.io": { + "auth": "erfi7sYi89234xJUqaqxgmzcnQ2rRFWM5aJX0EC=" + }, + "quay.io": { + "auth": "juQAqGmz5eR1ipzx8Evn6KGdw8fEa1w5MWczmgY=" + } + } +} +``` + +This example demonstrates how to use multiple paths for a single registry, while +preserving a fallback for `my-registry.local`: + +``` +{ + "auths": { + "my-registry.local/foo/bar/image": { + "auth": "…" + }, + "my-registry.local/foo": { + "auth": "…" + }, + "my-registry.local": { + "auth": "…" + }, + } +} +``` + +An entry can be removed by using a `logout` command from a container +tool such as `podman logout` or `buildah logout`. + +In addition, credential helpers can be configured for specific registries, and the credentials-helper +software can be used to manage the credentials more securely than storing only base64-encoded credentials in `auth.json`. + +When the credential helper is in use on a Linux platform, the auth.json file would contain keys that specify the registry domain, and values that specify the suffix of the program to use (i.e. everything after docker-credential-). For example: + +``` +{ + "auths": { + "localhost:5001": {} + }, + "credHelpers": { + "registry.example.com": "secretservice" + } +} +``` + +For more information on credential helpers, please reference the [GitHub docker-credential-helpers project](https://github.com/docker/docker-credential-helpers/releases). + +# SEE ALSO + buildah-login(1), buildah-logout(1), podman-login(1), podman-logout(1), skopeo-login(1), skopeo-logout(1) + +# HISTORY +Feb 2020, Originally compiled by Tom Sweeney diff --git a/vendor/github.com/containers/image/v5/docs/containers-certs.d.5.md b/vendor/github.com/containers/image/v5/docs/containers-certs.d.5.md new file mode 100644 index 00000000000..828ff764518 --- /dev/null +++ b/vendor/github.com/containers/image/v5/docs/containers-certs.d.5.md @@ -0,0 +1,28 @@ +% containers-certs.d 5 Directory for storing custom container-registry TLS configurations + +# NAME +containers-certs.d - Directory for storing custom container-registry TLS configurations + +# DESCRIPTION +A custom TLS configuration for a container registry can be configured by creating a directory under `$HOME/.config/containers/certs.d` or `/etc/containers/certs.d`. +The name of the directory must correspond to the `host:port` of the registry (e.g., `my-registry.com:5000`). + +## Directory Structure +A certs directory can contain one or more files with the following extensions: + +* `*.crt` files with this extensions will be interpreted as CA certificates +* `*.cert` files with this extensions will be interpreted as client certificates +* `*.key` files with this extensions will be interpreted as client keys + +Note that the client certificate-key pair will be selected by the file name (e.g., `client.{cert,key}`). +An exemplary setup for a registry running at `my-registry.com:5000` may look as follows: +``` +/etc/containers/certs.d/ <- Certificate directory +└── my-registry.com:5000 <- Hostname:port + ├── client.cert <- Client certificate + ├── client.key <- Client key + └── ca.crt <- Certificate authority that signed the registry certificate +``` + +# HISTORY +Feb 2019, Originally compiled by Valentin Rothberg diff --git a/vendor/github.com/containers/image/v5/docs/containers-policy.json.5.md b/vendor/github.com/containers/image/v5/docs/containers-policy.json.5.md new file mode 100644 index 00000000000..6aa5f80536f --- /dev/null +++ b/vendor/github.com/containers/image/v5/docs/containers-policy.json.5.md @@ -0,0 +1,421 @@ +% CONTAINERS-POLICY.JSON 5 policy.json Man Page +% Miloslav Trmač +% September 2016 + +# NAME +containers-policy.json - syntax for the signature verification policy file + +## DESCRIPTION + +Signature verification policy files are used to specify policy, e.g. trusted keys, +applicable when deciding whether to accept an image, or individual signatures of that image, as valid. + +By default, the policy is read from `$HOME/.config/containers/policy.json`, if it exists, otherwise from `/etc/containers/policy.json`; applications performing verification may allow using a different policy instead. + +## FORMAT + +The signature verification policy file, usually called `policy.json`, +uses a JSON format. Unlike some other JSON files, its parsing is fairly strict: +unrecognized, duplicated or otherwise invalid fields cause the entire file, +and usually the entire operation, to be rejected. + +The purpose of the policy file is to define a set of *policy requirements* for a container image, +usually depending on its location (where it is being pulled from) or otherwise defined identity. + +Policy requirements can be defined for: + +- An individual *scope* in a *transport*. + The *transport* values are the same as the transport prefixes when pushing/pulling images (e.g. `docker:`, `atomic:`), + and *scope* values are defined by each transport; see below for more details. + + Usually, a scope can be defined to match a single image, and various prefixes of + such a most specific scope define namespaces of matching images. + +- A default policy for a single transport, expressed using an empty string as a scope + +- A global default policy. + +If multiple policy requirements match a given image, only the requirements from the most specific match apply, +the more general policy requirements definitions are ignored. + +This is expressed in JSON using the top-level syntax +```js +{ + "default": [/* policy requirements: global default */] + "transports": { + transport_name: { + "": [/* policy requirements: default for transport $transport_name */], + scope_1: [/* policy requirements: default for $scope_1 in $transport_name */], + scope_2: [/*…*/] + /*…*/ + }, + transport_name_2: {/*…*/} + /*…*/ + } +} +``` + +The global `default` set of policy requirements is mandatory; all of the other fields +(`transports` itself, any specific transport, the transport-specific default, etc.) are optional. + + +## Supported transports and their scopes + +### `atomic:` + +The `atomic:` transport refers to images in an Atomic Registry. + +Supported scopes use the form _hostname_[`:`_port_][`/`_namespace_[`/`_imagestream_ [`:`_tag_]]], +i.e. either specifying a complete name of a tagged image, or prefix denoting +a host/namespace/image stream or a wildcarded expression for matching all +subdomains. For wildcarded subdomain matching, `*.example.com` is a valid case, but `example*.*.com` is not. + +*Note:* The _hostname_ and _port_ refer to the container registry host and port (the one used +e.g. for `docker pull`), _not_ to the OpenShift API host and port. + +### `dir:` + +The `dir:` transport refers to images stored in local directories. + +Supported scopes are paths of directories (either containing a single image or +subdirectories possibly containing images). + +*Note:* The paths must be absolute and contain no symlinks. Paths violating these requirements may be silently ignored. + +The top-level scope `"/"` is forbidden; use the transport default scope `""`, +for consistency with other transports. + +### `docker:` + +The `docker:` transport refers to images in a registry implementing the "Docker Registry HTTP API V2". + +Scopes matching individual images are named Docker references *in the fully expanded form*, either +using a tag or digest. For example, `docker.io/library/busybox:latest` (*not* `busybox:latest`). + +More general scopes are prefixes of individual-image scopes, and specify a repository (by omitting the tag or digest), +a repository namespace, or a registry host (by only specifying the host name) +or a wildcarded expression for matching all subdomains. For wildcarded subdomain +matching, `*.example.com` is a valid case, but `example*.*.com` is not. + +### `oci:` + +The `oci:` transport refers to images in directories compliant with "Open Container Image Layout Specification". + +Supported scopes use the form _directory_`:`_tag_, and _directory_ referring to +a directory containing one or more tags, or any of the parent directories. + +*Note:* See `dir:` above for semantics and restrictions on the directory paths, they apply to `oci:` equivalently. + +### `tarball:` + +The `tarball:` transport refers to tarred up container root filesystems. + +Scopes are ignored. + +## Policy Requirements + +Using the mechanisms above, a set of policy requirements is looked up. The policy requirements +are represented as a JSON array of individual requirement objects. For an image to be accepted, +*all* of the requirements must be satisfied simultaneously. + +The policy requirements can also be used to decide whether an individual signature is accepted (= is signed by a recognized key of a known author); +in that case some requirements may apply only to some signatures, but each signature must be accepted by *at least one* requirement object. + +The following requirement objects are supported: + +### `insecureAcceptAnything` + +A simple requirement with the following syntax + +```json +{"type":"insecureAcceptAnything"} +``` + +This requirement accepts any image (but note that other requirements in the array still apply). + +When deciding to accept an individual signature, this requirement does not have any effect; it does *not* cause the signature to be accepted, though. + +This is useful primarily for policy scopes where no signature verification is required; +because the array of policy requirements must not be empty, this requirement is used +to represent the lack of requirements explicitly. + +### `reject` + +A simple requirement with the following syntax: + +```json +{"type":"reject"} +``` + +This requirement rejects every image, and every signature. + +### `signedBy` + +This requirement requires an image to be signed using “simple signing” with an expected identity, or accepts a signature if it is using an expected identity and key. + +```js +{ + "type": "signedBy", + "keyType": "GPGKeys", /* The only currently supported value */ + "keyPath": "/path/to/local/keyring/file", + "keyPaths": ["/path/to/local/keyring/file1","/path/to/local/keyring/file2"…], + "keyData": "base64-encoded-keyring-data", + "signedIdentity": identity_requirement +} +``` + + +Exactly one of `keyPath`, `keyPaths` and `keyData` must be present, containing a GPG keyring of one or more public keys. Only signatures made by these keys are accepted. + +The `signedIdentity` field, a JSON object, specifies what image identity the signature claims about the image. +One of the following alternatives are supported: + +- The identity in the signature must exactly match the image identity. Note that with this, referencing an image by digest (with a signature claiming a _repository_`:`_tag_ identity) will fail. + + ```json + {"type":"matchExact"} + ``` +- If the image identity carries a tag, the identity in the signature must exactly match; + if the image identity uses a digest reference, the identity in the signature must be in the same repository as the image identity (using any tag). + + (Note that with images identified using digest references, the digest from the reference is validated even before signature verification starts.) + + ```json + {"type":"matchRepoDigestOrExact"} + ``` +- The identity in the signature must be in the same repository as the image identity. This is useful e.g. to pull an image using the `:latest` tag when the image is signed with a tag specifying an exact image version. + + ```json + {"type":"matchRepository"} + ``` +- The identity in the signature must exactly match a specified identity. + This is useful e.g. when locally mirroring images signed using their public identity. + + ```js + { + "type": "exactReference", + "dockerReference": docker_reference_value + } + ``` +- The identity in the signature must be in the same repository as a specified identity. + This combines the properties of `matchRepository` and `exactReference`. + + ```js + { + "type": "exactRepository", + "dockerRepository": docker_repository_value + } + ``` +- Prefix remapping: + + If the image identity matches the specified prefix, that prefix is replaced by the specified “signed prefix” + (otherwise it is used as unchanged and no remapping takes place); + matching then follows the `matchRepoDigestOrExact` semantics documented above + (i.e. if the image identity carries a tag, the identity in the signature must exactly match, + if it uses a digest reference, the repository must match). + + The `prefix` and `signedPrefix` values can be either host[:port] values + (matching exactly the same host[:port], string), + repository namespaces, or repositories (i.e. they must not contain tags/digests), + and match as prefixes *of the fully expanded form*. + For example, `docker.io/library/busybox` (*not* `busybox`) to specify that single repository, + or `docker.io/library` (not an empty string) to specify the parent namespace of `docker.io/library/busybox`==`busybox`). + + The `prefix` value is usually the same as the scope containing the parent `signedBy` requirement. + + ```js + { + "type": "remapIdentity", + "prefix": prefix, + "signedPrefix": prefix, + } + ``` + +If the `signedIdentity` field is missing, it is treated as `matchRepoDigestOrExact`. + +*Note*: `matchExact`, `matchRepoDigestOrExact` and `matchRepository` can be only used if a Docker-like image identity is +provided by the transport. In particular, the `dir:` and `oci:` transports can be only +used with `exactReference` or `exactRepository`. + + + + +### `sigstoreSigned` + +This requirement requires an image to be signed using a sigstore signature with an expected identity and key. + +```js +{ + "type": "sigstoreSigned", + "keyPath": "/path/to/local/public/key/file", + "keyData": "base64-encoded-public-key-data", + "fulcio": { + "caPath": "/path/to/local/CA/file", + "caData": "base64-encoded-CA-data", + "oidcIssuer": "https://expected.OIDC.issuer/", + "subjectEmail", "expected-signing-user@example.com", + }, + "rekorPublicKeyPath": "/path/to/local/public/key/file", + "rekorPublicKeyData": "base64-encoded-public-key-data", + "signedIdentity": identity_requirement +} +``` +Exactly one of `keyPath`, `keyData` and `fulcio` must be present. + +If `keyPath` or `keyData` is present, it contains a sigstore public key. +Only signatures made by this key are accepted. + +If `fulcio` is present, the signature must be based on a Fulcio-issued certificate. +One of `caPath` and `caData` must be specified, containing the public key of the Fulcio instance. +Both `oidcIssuer` and `subjectEmail` are mandatory, +exactly specifying the expected identity provider, +and the identity of the user obtaining the Fulcio certificate. + +At most one of `rekorPublicKeyPath` and `rekorPublicKeyData` can be present; +it is mandatory if `fulcio` is specified. +If a Rekor public key is specified, +the signature must have been uploaded to a Rekor server +and the signature must contain an (offline-verifiable) “signed entry timestamp” +proving the existence of the Rekor log record, +signed by the provided public key. + +The `signedIdentity` field has the same semantics as in the `signedBy` requirement described above. +Note that `cosign`-created signatures only contain a repository, so only `matchRepository` and `exactRepository` can be used to accept them (and that does not protect against substitution of a signed image with an unexpected tag). + +To use this with images hosted on image registries, the `use-sigstore-attachments` option needs to be enabled for the relevant registry or repository in the client's containers-registries.d(5). + +## Examples + +It is *strongly* recommended to set the `default` policy to `reject`, and then +selectively allow individual transports and scopes as desired. + +### A reasonably locked-down system + +(Note that the `/*`…`*/` comments are not valid in JSON, and must not be used in real policies.) + +```js +{ + "default": [{"type": "reject"}], /* Reject anything not explicitly allowed */ + "transports": { + "docker": { + /* Allow installing images from a specific repository namespace, without cryptographic verification. + This namespace includes images like openshift/hello-openshift and openshift/origin. */ + "docker.io/openshift": [{"type": "insecureAcceptAnything"}], + /* Similarly, allow installing the “official” busybox images. Note how the fully expanded + form, with the explicit /library/, must be used. */ + "docker.io/library/busybox": [{"type": "insecureAcceptAnything"}], + /* Allow installing images from all subdomains */ + "*.temporary-project.example.com": [{"type": "insecureAcceptAnything"}], + /* A sigstore-signed repository */ + "hostname:5000/myns/sigstore-signed-with-full-references": [ + { + "type": "sigstoreSigned", + "keyPath": "/path/to/sigstore-pubkey.pub" + } + ], + /* A sigstore-signed repository using the community Fulcio+Rekor servers. + + The community servers’ public keys can be obtained from + https://github.com/sigstore/sigstore/tree/main/pkg/tuf/repository/targets . */ + "hostname:5000/myns/sigstore-signed-fulcio-rekor": [ + { + "type": "sigstoreSigned", + "fulcio": { + "caPath": "/path/to/fulcio_v1.crt.pem", + "oidcIssuer": "https://github.com/login/oauth", + "subjectEmail": "test-user@example.com" + }, + "rekorPublicKeyPath": "/path/to/rekor.pub", + } + ], + /* A sigstore-signed repository, accepts signatures by /usr/bin/cosign */ + "hostname:5000/myns/sigstore-signed-allows-malicious-tag-substitution": [ + { + "type": "sigstoreSigned", + "keyPath": "/path/to/sigstore-pubkey.pub", + "signedIdentity": {"type": "matchRepository"} + } + ], + /* A sigstore-signed repository using the community Fulcio+Rekor servers, + accepts signatures by /usr/bin/cosign. + + The community servers’ public keys can be obtained from + https://github.com/sigstore/sigstore/tree/main/pkg/tuf/repository/targets . */ + "hostname:5000/myns/sigstore-signed-fulcio-rekor- allows-malicious-tag-substitution": [ + { + "type": "sigstoreSigned", + "fulcio": { + "caPath": "/path/to/fulcio_v1.crt.pem", + "oidcIssuer": "https://github.com/login/oauth", + "subjectEmail": "test-user@example.com" + }, + "rekorPublicKeyPath": "/path/to/rekor.pub", + "signedIdentity": { "type": "matchRepository" } + } + ] + /* Other docker: images use the global default policy and are rejected */ + }, + "dir": { + "": [{"type": "insecureAcceptAnything"}] /* Allow any images originating in local directories */ + }, + "atomic": { + /* The common case: using a known key for a repository or set of repositories */ + "hostname:5000/myns/official": [ + { + "type": "signedBy", + "keyType": "GPGKeys", + "keyPath": "/path/to/official-pubkey.gpg" + } + ], + /* A more complex example, for a repository which contains a mirror of a third-party product, + which must be signed-off by local IT */ + "hostname:5000/vendor/product": [ + { /* Require the image to be signed by the original vendor, using the vendor's repository location. */ + "type": "signedBy", + "keyType": "GPGKeys", + "keyPath": "/path/to/vendor-pubkey.gpg", + "signedIdentity": { + "type": "exactRepository", + "dockerRepository": "vendor-hostname/product/repository" + } + }, + { /* Require the image to _also_ be signed by a local reviewer. */ + "type": "signedBy", + "keyType": "GPGKeys", + "keyPath": "/path/to/reviewer-pubkey.gpg" + } + ], + /* A way to mirror many repositories from a single vendor */ + "private-mirror:5000/vendor-mirror": [ + { /* Require the image to be signed by the original vendor, using the vendor's repository location. + For example, private-mirror:5000/vendor-mirror/productA/image1:latest needs to be signed as + vendor.example/productA/image1:latest . */ + "type": "signedBy", + "keyType": "GPGKeys", + "keyPath": "/path/to/vendor-pubkey.gpg", + "signedIdentity": { + "type": "remapIdentity", + "prefix": "private-mirror:5000/vendor-mirror", + "signedPrefix": "vendor.example.com" + } + } + ] + } + } +} +``` + +### Completely disable security, allow all images, do not trust any signatures + +```json +{ + "default": [{"type": "insecureAcceptAnything"}] +} +``` +## SEE ALSO + atomic(1) + +## HISTORY +August 2018, Rename to containers-policy.json(5) by Valentin Rothberg + +September 2016, Originally compiled by Miloslav Trmač diff --git a/vendor/github.com/containers/image/v5/docs/containers-registries.conf.5.md b/vendor/github.com/containers/image/v5/docs/containers-registries.conf.5.md new file mode 100644 index 00000000000..fdc2fa9c38e --- /dev/null +++ b/vendor/github.com/containers/image/v5/docs/containers-registries.conf.5.md @@ -0,0 +1,322 @@ +% CONTAINERS-REGISTRIES.CONF 5 System-wide registry configuration file +% Brent Baude +% Aug 2017 + +# NAME +containers-registries.conf - Syntax of System Registry Configuration File + +# DESCRIPTION +The CONTAINERS-REGISTRIES configuration file is a system-wide configuration +file for container image registries. The file format is TOML. + +Container engines will use the `$HOME/.config/containers/registries.conf` if it exists, otherwise they will use `/etc/containers/registries.conf` + +### GLOBAL SETTINGS + +`unqualified-search-registries` +: An array of _host_[`:`_port_] registries to try when pulling an unqualified image, in order. + +`credential-helpers` +: An array of default credential helpers used as external credential stores. Note that "containers-auth.json" is a reserved value to use auth files as specified in containers-auth.json(5). The credential helpers are set to `["containers-auth.json"]` if none are specified. + +### NAMESPACED `[[registry]]` SETTINGS + +The bulk of the configuration is represented as an array of `[[registry]]` +TOML tables; the settings may therefore differ among different registries +as well as among different namespaces/repositories within a registry. + +#### Choosing a `[[registry]]` TOML table + +Given an image name, a single `[[registry]]` TOML table is chosen based on its `prefix` field. + +`prefix`: A prefix of the user-specified image name, i.e. using one of the following formats: + - _host_[`:`_port_] + - _host_[`:`_port_]`/`_namespace_[`/`_namespace_…] + - _host_[`:`_port_]`/`_namespace_[`/`_namespace_…]`/`_repo_ + - _host_[`:`_port_]`/`_namespace_[`/`_namespace_…]`/`_repo_(`:`_tag|`@`_digest_) + - [`*.`]_host_ + +The user-specified image name must start with the specified `prefix` (and continue +with the appropriate separator) for a particular `[[registry]]` TOML table to be +considered; (only) the TOML table with the longest match is used. It can +also include wildcarded subdomains in the format `*.example.com`. +The wildcard should only be present at the beginning as shown in the formats +above. Other cases will not work. For example, `*.example.com` is valid but +`example.*.com`, `*.example.com/foo` and `*.example.com:5000/foo/bar:baz` are not. +Note that `*` matches an arbitrary number of subdomains. `*.example.com` will hence +match `bar.example.com`, `foo.bar.example.com` and so on. + +As a special case, the `prefix` field can be missing; if so, it defaults to the value +of the `location` field (described below). + +#### Per-namespace settings + +`insecure` +: `true` or `false`. +By default, container runtimes require TLS when retrieving images from a registry. +If `insecure` is set to `true`, unencrypted HTTP as well as TLS connections with untrusted +certificates are allowed. + +`blocked` +: `true` or `false`. +If `true`, pulling images with matching names is forbidden. + +#### Remapping and mirroring registries + +The user-specified image reference is, primarily, a "logical" image name, always used for naming +the image. By default, the image reference also directly specifies the registry and repository +to use, but the following options can be used to redirect the underlying accesses +to different registry servers or locations (e.g. to support configurations with no access to the +internet without having to change `Dockerfile`s, or to add redundancy). + +`location` +: Accepts the same format as the `prefix` field, and specifies the physical location +of the `prefix`-rooted namespace. + +By default, this equal to `prefix` (in which case `prefix` can be omitted and the +`[[registry]]` TOML table can only specify `location`). + +Example: Given +``` +prefix = "example.com/foo" +location = "internal-registry-for-example.net/bar" +``` +requests for the image `example.com/foo/myimage:latest` will actually work with the +`internal-registry-for-example.net/bar/myimage:latest` image. + +With a `prefix` containing a wildcard in the format: "*.example.com" for subdomain matching, +the location can be empty. In such a case, +prefix matching will occur, but no reference rewrite will occur. The +original requested image string will be used as-is. But other settings like +`insecure` / `blocked` / `mirrors` will be applied to matching images. + +Example: Given +``` +prefix = "*.example.com" +``` +requests for the image `blah.example.com/foo/myimage:latest` will be used +as-is. But other settings like insecure/blocked/mirrors will be applied to matching images + +`mirror` +: An array of TOML tables specifying (possibly-partial) mirrors for the +`prefix`-rooted namespace (i.e., the current `[[registry]]` TOML table). + +The mirrors are attempted in the specified order; the first one that can be +contacted and contains the image will be used (and if none of the mirrors contains the image, +the primary location specified by the `registry.location` field, or using the unmodified +user-specified reference, is tried last). + +Each TOML table in the `mirror` array can contain the following fields: +- `location`: same semantics +as specified in the `[[registry]]` TOML table +- `insecure`: same semantics +as specified in the `[[registry]]` TOML table +- `pull-from-mirror`: `all`, `digest-only` or `tag-only`. If "digest-only", mirrors will only be used for digest pulls. Pulling images by tag can potentially yield different images, depending on which endpoint we pull from. Restricting mirrors to pulls by digest avoids that issue. If "tag-only", mirrors will only be used for tag pulls. For a more up-to-date and expensive mirror that it is less likely to be out of sync if tags move, it should not be unnecessarily used for digest references. Default is "all" (or left empty), mirrors will be used for both digest pulls and tag pulls unless the mirror-by-digest-only is set for the primary registry. +Note that this per-mirror setting is allowed only when `mirror-by-digest-only` is not configured for the primary registry. + +`mirror-by-digest-only` +: `true` or `false`. +If `true`, mirrors will only be used during pulling if the image reference includes a digest. +Note that if all mirrors are configured to be digest-only, images referenced by a tag will only use the primary +registry. +If all mirrors are configured to be tag-only, images referenced by a digest will only use the primary +registry. + +Referencing an image by digest ensures that the same is always used +(whereas referencing an image by a tag may cause different registries to return +different images if the tag mapping is out of sync). + + +*Note*: Redirection and mirrors are currently processed only when reading images, not when pushing +to a registry; that may change in the future. + +#### Short-Name Aliasing +The use of unqualified-search registries entails an ambiguity as it is +unclear from which registry a given image, referenced by a short name, +may be pulled from. + +As mentioned in the note at the end of this man page, using short names is +subject to the risk of hitting squatted registry namespaces. If the +unqualified-search registries are set to `["registry1.com", "registry2.com"]` +an attacker may take over a namespace of registry1.com such that an image may +be pulled from registry1.com instead of the intended source registry2.com. + +While it is highly recommended to always use fully-qualified image references, +existing deployments using short names may not be easily changed. To +circumvent the aforementioned ambiguity, so called short-name aliases can be +configured that point to a fully-qualified image +reference. + +Short-name aliases can be configured in the `[aliases]` table in the form of +`"name"="value"` with the left-hand `name` being the short name (e.g., "image") +and the right-hand `value` being the fully-qualified image reference (e.g., +"registry.com/namespace/image"). Note that neither "name" nor "value" can +include a tag or digest. Moreover, "name" must be a short name and hence +cannot include a registry domain or refer to localhost. + +When pulling a short name, the configured aliases table will be used for +resolving the short name. If a matching alias is found, it will be used +without further consulting the unqualified-search registries list. If no +matching alias is found, the behavior can be controlled via the +`short-name-mode` option as described below. + +Note that tags and digests are stripped off a user-specified short name for +alias resolution. Hence, "image", "image:tag" and "image@digest" all resolve +to the same alias (i.e., "image"). Stripped off tags and digests are later +appended to the resolved alias. + +Further note that drop-in configuration files (see containers-registries.conf.d(5)) +can override aliases in the specific loading order of the files. If the "value" of +an alias is empty (i.e., ""), the alias will be erased. However, a given +"name" may only be specified once in a single config file. + + +#### Short-Name Aliasing: Modes + +The `short-name-mode` option supports three modes to control the behaviour of +short-name resolution. + +* `enforcing`: If only one unqualified-search registry is set, use it as there + is no ambiguity. If there is more than one registry and the user program is + running in a terminal (i.e., stdout & stdin are a TTY), prompt the user to + select one of the specified search registries. If the program is not running + in a terminal, the ambiguity cannot be resolved which will lead to an error. + +* `permissive`: Behaves as enforcing but does not lead to an error if the + program is not running in a terminal. Instead, fallback to using all + unqualified-search registries. + +* `disabled`: Use all unqualified-search registries without prompting. + +If `short-name-mode` is not specified at all or left empty, default to the +`permissive` mode. If the user-specified short name was not aliased already, +the `enforcing` and `permissive` mode if prompted, will record a new alias +after a successful pull. Note that the recorded alias will be written to +`/var/cache/containers/short-name-aliases.conf` for root to have a clear +separation between possibly human-edited registries.conf files and the +machine-generated `short-name-aliases-conf`. Note that `$HOME/.cache` is used +for rootless users. If an alias is specified in a +`registries.conf` file and also the machine-generated +`short-name-aliases.conf`, the `short-name-aliases.conf` file has precedence. + +#### Normalization of docker.io references + +The Docker Hub `docker.io` is handled in a special way: every push and pull +operation gets internally normalized with `/library` if no other specific +namespace is defined (for example on `docker.io/namespace/image`). + +(Note that the above-described normalization happens to match the behavior of +Docker.) + +This means that a pull of `docker.io/alpine` will be internally translated to +`docker.io/library/alpine`. A pull of `docker.io/user/alpine` will not be +rewritten because this is already the correct remote path. + +Therefore, to remap or mirror the `docker.io` images in the (implied) `/library` +namespace (or that whole namespace), the prefix and location fields in this +configuration file must explicitly include that `/library` namespace. For +example `prefix = "docker.io/library/alpine"` and not `prefix = +"docker.io/alpine"`. The latter would match the `docker.io/alpine/*` +repositories but not the `docker.io/[library/]alpine` image). + +### EXAMPLE + +``` +unqualified-search-registries = ["example.com"] + +[[registry]] +prefix = "example.com/foo" +insecure = false +blocked = false +location = "internal-registry-for-example.com/bar" + +[[registry.mirror]] +location = "example-mirror-0.local/mirror-for-foo" + +[[registry.mirror]] +location = "example-mirror-1.local/mirrors/foo" +insecure = true + +[[registry]] +location = "registry.com" + +[[registry.mirror]] +location = "mirror.registry.com" +``` +Given the above, a pull of `example.com/foo/image:latest` will try: + +1. `example-mirror-0.local/mirror-for-foo/image:latest` +2. `example-mirror-1.local/mirrors/foo/image:latest` +3. `internal-registry-for-example.net/bar/image:latest` + +in order, and use the first one that exists. + +Note that a mirror is associated only with the current `[[registry]]` TOML table. If using the example above, pulling the image `registry.com/image:latest` will hence only reach out to `mirror.registry.com`, and the mirrors associated with `example.com/foo` will not be considered. + +## VERSION 1 FORMAT - DEPRECATED +VERSION 1 format is still supported but it does not support +using registry mirrors, longest-prefix matches, or location rewriting. + +The TOML format is used to build a simple list of registries under three +categories: `registries.search`, `registries.insecure`, and `registries.block`. +You can list multiple registries using a comma separated list. + +Search registries are used when the caller of a container runtime does not fully specify the +container image that they want to execute. These registries are prepended onto the front +of the specified container image until the named image is found at a registry. + +Note that insecure registries can be used for any registry, not just the registries listed +under search. + +The `registries.insecure` and `registries.block` lists have the same meaning as the +`insecure` and `blocked` fields in the current version. + +### EXAMPLE +The following example configuration defines two searchable registries, one +insecure registry, and two blocked registries. + +``` +[registries.search] +registries = ['registry1.com', 'registry2.com'] + +[registries.insecure] +registries = ['registry3.com'] + +[registries.block] +registries = ['registry.untrusted.com', 'registry.unsafe.com'] +``` + +# NOTE: RISK OF USING UNQUALIFIED IMAGE NAMES +We recommend always using fully qualified image names including the registry +server (full dns name), namespace, image name, and tag +(e.g., registry.redhat.io/ubi8/ubi:latest). When using short names, there is +always an inherent risk that the image being pulled could be spoofed. For +example, a user wants to pull an image named `foobar` from a registry and +expects it to come from myregistry.com. If myregistry.com is not first in the +search list, an attacker could place a different `foobar` image at a registry +earlier in the search list. The user would accidentally pull and run the +attacker's image and code rather than the intended content. We recommend only +adding registries which are completely trusted, i.e. registries which don't +allow unknown or anonymous users to create accounts with arbitrary names. This +will prevent an image from being spoofed, squatted or otherwise made insecure. +If it is necessary to use one of these registries, it should be added at the +end of the list. + +It is recommended to use fully-qualified images for pulling as +the destination registry is unambiguous. Pulling by digest +(i.e., quay.io/repository/name@digest) further eliminates the ambiguity of +tags. + +# SEE ALSO + containers-auth.json(5) containers-certs.d(5) + +# HISTORY +Dec 2019, Warning added for unqualified image names by Tom Sweeney + +Mar 2019, Added additional configuration format by Sascha Grunert + +Aug 2018, Renamed to containers-registries.conf(5) by Valentin Rothberg + +Jun 2018, Updated by Tom Sweeney + +Aug 2017, Originally compiled by Brent Baude diff --git a/vendor/github.com/containers/image/v5/docs/containers-registries.conf.d.5.md b/vendor/github.com/containers/image/v5/docs/containers-registries.conf.d.5.md new file mode 100644 index 00000000000..563ccfd00cb --- /dev/null +++ b/vendor/github.com/containers/image/v5/docs/containers-registries.conf.d.5.md @@ -0,0 +1,37 @@ +% CONTAINERS-REGISTRIES.CONF.D 5 +% Valentin Rothberg +% Mar 2020 + +# NAME +containers-registries.conf.d - directory for drop-in registries.conf files + +# DESCRIPTION +CONTAINERS-REGISTRIES.CONF.D is a system-wide directory for drop-in +configuration files in the `containers-registries.conf(5)` format. + +By default, the directory is located at `/etc/containers/registries.conf.d`. + +# CONFIGURATION PRECEDENCE + +Once the main configuration at `/etc/containers/registries.conf` is loaded, the +files in `/etc/containers/registries.conf.d` are loaded in alpha-numerical +order. Then the conf files in `$HOME/.config/containers/registries.conf.d` are loaded in alpha-numerical order, if they exist. If the `$HOME/.config/containers/registries.conf` is loaded, only the conf files under `$HOME/.config/containers/registries.conf.d` are loaded in alpha-numerical order. +Specified fields in a conf file will overwrite any previous setting. Note +that only files with the `.conf` suffix are loaded, other files and +sub-directories are ignored. + +For instance, setting the `unqualified-search-registries` in +`/etc/containers/registries.conf.d/myregistries.conf` will overwrite previous +settings in `/etc/containers/registries.conf`. The `[[registry]]` tables merged +by overwriting existing items if the prefixes are identical while new ones are +added. + +All drop-in configuration files must be specified in the version 2 of the +`containers-registries.conf(5)` format. + +# SEE ALSO +`containers-registries.conf(5)` + +# HISTORY + +Mar 2020, Originally compiled by Valentin Rothberg diff --git a/vendor/github.com/containers/image/v5/docs/containers-registries.d.5.md b/vendor/github.com/containers/image/v5/docs/containers-registries.d.5.md new file mode 100644 index 00000000000..04434de4b6f --- /dev/null +++ b/vendor/github.com/containers/image/v5/docs/containers-registries.d.5.md @@ -0,0 +1,140 @@ +% containers-registries.d 5 Registries.d Man Page +% Miloslav Trmač +% August 2016 + +# NAME +containers-registries.d - Directory for various registries configurations + +# DESCRIPTION + +The registries configuration directory contains configuration for various registries +(servers storing remote container images), and for content stored in them, +so that the configuration does not have to be provided in command-line options over and over for every command, +and so that it can be shared by all users of containers/image. + +By default, the registries configuration directory is `$HOME/.config/containers/registries.d` if it exists, otherwise `/etc/containers/registries.d` (unless overridden at compile-time); +applications may allow using a different directory instead. + +## Directory Structure + +The directory may contain any number of files with the extension `.yaml`, +each using the YAML format. Other than the mandatory extension, names of the files +don’t matter. + +The contents of these files are merged together; to have a well-defined and easy to understand +behavior, there can be only one configuration section describing a single namespace within a registry +(in particular there can be at most one one `default-docker` section across all files, +and there can be at most one instance of any key under the `docker` section; +these sections are documented later). + +Thus, it is forbidden to have two conflicting configurations for a single registry or scope, +and it is also forbidden to split a configuration for a single registry or scope across +more than one file (even if they are not semantically in conflict). + +## Registries, Scopes and Search Order + +Each YAML file must contain a “YAML mapping” (key-value pairs). Two top-level keys are defined: + +- `default-docker` is the _configuration section_ (as documented below) + for registries implementing "Docker Registry HTTP API V2". + + This key is optional. + +- `docker` is a mapping, using individual registries implementing "Docker Registry HTTP API V2", + or namespaces and individual images within these registries, as keys; + the value assigned to any such key is a _configuration section_. + + This key is optional. + + Scopes matching individual images are named Docker references *in the fully expanded form*, either + using a tag or digest. For example, `docker.io/library/busybox:latest` (*not* `busybox:latest`). + + More general scopes are prefixes of individual-image scopes, and specify a repository (by omitting the tag or digest), + a repository namespace, or a registry host (and a port if it differs from the default). + + Note that if a registry is accessed using a hostname+port configuration, the port-less hostname + is _not_ used as parent scope. + +When searching for a configuration to apply for an individual container image, only +the configuration for the most-precisely matching scope is used; configuration using +more general scopes is ignored. For example, if _any_ configuration exists for +`docker.io/library/busybox`, the configuration for `docker.io` is ignored +(even if some element of the configuration is defined for `docker.io` and not for `docker.io/library/busybox`). + +### Built-in Defaults + +If no `docker` section can be found for the container image, and no `default-docker` section is configured: + +- The default directory, `/var/lib/containers/sigstore` for root and `$HOME/.local/share/containers/sigstore` for unprivileged user, will be used for reading and writing signatures. +- Sigstore attachments will not be read/written. + +## Individual Configuration Sections + +A single configuration section is selected for a container image using the process +described above. The configuration section is a YAML mapping, with the following keys: + + + +- `lookaside-staging` defines an URL of of the signature storage, used for editing it (adding or deleting signatures). + + This key is optional; if it is missing, `lookaside` below is used. + +- `lookaside` defines an URL of the signature storage. + This URL is used for reading existing signatures, + and if `lookaside-staging` does not exist, also for adding or removing them. + + This key is optional; if it is missing, no signature storage is defined (no signatures + are download along with images, adding new signatures is possible only if `lookaside-staging` is defined). + +- `use-sigstore-attachments` specifies whether sigstore image attachments (signatures, attestations and the like) are going to be read/written along with the image. + If disabled, the images are treated as if no attachments exist; attempts to write attachments fail. + +## Examples + +### Using Containers from Various Origins + +The following demonstrates how to to consume and run images from various registries and namespaces: + +```yaml +docker: + registry.database-supplier.com: + lookaside: https://lookaside.database-supplier.com + distribution.great-middleware.org: + lookaside: https://security-team.great-middleware.org/lookaside + docker.io/web-framework: + lookaside: https://lookaside.web-framework.io:8080 +``` + +### Developing and Signing Containers, Staging Signatures + +For developers in `example.com`: + +- Consume most container images using the public servers also used by clients. +- Use a separate signature storage for an container images in a namespace corresponding to the developers' department, with a staging storage used before publishing signatures. +- Craft an individual exception for a single branch a specific developer is working on locally. + +```yaml +docker: + registry.example.com: + lookaside: https://registry-lookaside.example.com + registry.example.com/mydepartment: + lookaside: https://lookaside.mydepartment.example.com + lookaside-staging: file:///mnt/mydepartment/lookaside-staging + registry.example.com/mydepartment/myproject:mybranch: + lookaside: http://localhost:4242/lookaside + lookaside-staging: file:///home/useraccount/webroot/lookaside +``` + +### A Global Default + +If a company publishes its products using a different domain, and different registry hostname for each of them, it is still possible to use a single signature storage server +without listing each domain individually. This is expected to rarely happen, usually only for staging new signatures. + +```yaml +default-docker: + lookaside-staging: file:///mnt/company/common-lookaside-staging +``` + +# AUTHORS + +Miloslav Trmač diff --git a/vendor/github.com/containers/image/v5/docs/containers-signature.5.md b/vendor/github.com/containers/image/v5/docs/containers-signature.5.md new file mode 100644 index 00000000000..cc6f9c36621 --- /dev/null +++ b/vendor/github.com/containers/image/v5/docs/containers-signature.5.md @@ -0,0 +1,246 @@ +% container-signature 5 Container signature format +% Miloslav Trmač +% March 2017 + +# NAME +container-signature - Container signature format + +# DESCRIPTION +This document describes the format of container signatures, +as implemented by the `github.com/containers/image/signature` package. + +Most users should be able to consume these signatures by using the `github.com/containers/image/signature` package +(preferably through the higher-level `signature.PolicyContext` interface) +without having to care about the details of the format described below. +This documentation exists primarily for maintainers of the package +and to allow independent reimplementations. + +## High-level overview + +The signature provides an end-to-end authenticated claim that a container image +has been approved by a specific party (e.g. the creator of the image as their work, +an automated build system as a result of an automated build, +a company IT department approving the image for production) under a specified _identity_ +(e.g. an OS base image / specific application, with a specific version). + +A container signature consists of a cryptographic signature which identifies +and authenticates who signed the image, and carries as a signed payload a JSON document. +The JSON document identifies the image being signed, claims a specific identity of the +image and if applicable, contains other information about the image. + +The signatures do not modify the container image (the layers, configuration, manifest, …); +e.g. their presence does not change the manifest digest used to identify the image in +docker/distribution servers; rather, the signatures are associated with an immutable image. +An image can have any number of signatures so signature distribution systems SHOULD support +associating more than one signature with an image. + +## The cryptographic signature + +As distributed, the container signature is a blob which contains a cryptographic signature +in an industry-standard format, carrying a signed JSON payload (i.e. the blob contains both the +JSON document and a signature of the JSON document; it is not a “detached signature” with +independent blobs containing the JSON document and a cryptographic signature). + +Currently the only defined cryptographic signature format is an OpenPGP signature (RFC 4880), +but others may be added in the future. (The blob does not contain metadata identifying the +cryptographic signature format. It is expected that most formats are sufficiently self-describing +that this is not necessary and the configured expected public key provides another indication +of the expected cryptographic signature format. Such metadata may be added in the future for +newly added cryptographic signature formats, if necessary.) + +Consumers of container signatures SHOULD verify the cryptographic signature +against one or more trusted public keys +(e.g. defined in a [policy.json signature verification policy file](containers-policy.json.5.md)) +before parsing or processing the JSON payload in _any_ way, +in particular they SHOULD stop processing the container signature +if the cryptographic signature verification fails, without even starting to process the JSON payload. + +(Consumers MAY extract identification of the signing key and other metadata from the cryptographic signature, +and the JSON payload, without verifying the signature, if the purpose is to allow managing the signature blobs, +e.g. to list the authors and image identities of signatures associated with a single container image; +if so, they SHOULD design the output of such processing to minimize the risk of users considering the output trusted +or in any way usable for making policy decisions about the image.) + +### OpenPGP signature verification + +When verifying a cryptographic signature in the OpenPGP format, +the consumer MUST verify at least the following aspects of the signature +(like the `github.com/containers/image/signature` package does): + +- The blob MUST be a “Signed Message” as defined RFC 4880 section 11.3. + (e.g. it MUST NOT be an unsigned “Literal Message”, + a “Cleartext Signature” as defined in RFC 4880 section 7, + or any other non-signature format). +- The signature MUST have been made by an expected key trusted for the purpose (and the specific container image). +- The signature MUST be correctly formed and pass the cryptographic validation. +- The signature MUST correctly authenticate the included JSON payload + (in particular, the parsing of the JSON payload MUST NOT start before the complete payload has been cryptographically authenticated). +- The signature MUST NOT be expired. + +The consumer SHOULD have tests for its verification code which verify that signatures failing any of the above are rejected. + +## JSON processing and forward compatibility + +The payload of the cryptographic signature is a JSON document (RFC 7159). +Consumers SHOULD parse it very strictly, +refusing any signature which violates the expected format (e.g. missing members, incorrect member types) +or can be interpreted ambiguously (e.g. a duplicated member in a JSON object). + +Any violations of the JSON format or of other requirements in this document MAY be accepted if the JSON document can be recognized +to have been created by a known-incorrect implementation (see [`optional.creator`](#optionalcreator) below) +and if the semantics of the invalid document, as created by such an implementation, is clear. + +The top-level value of the JSON document MUST be a JSON object with exactly two members, `critical` and `optional`, +each a JSON object. + +The `critical` object MUST contain a `type` member identifying the document as a container signature +(as defined [below](#criticaltype)) +and signature consumers MUST reject signatures which do not have this member or in which this member does not have the expected value. + +To ensure forward compatibility (allowing older signature consumers to correctly +accept or reject signatures created at a later date, with possible extensions to this format), +consumers MUST reject the signature if the `critical` object, or _any_ of its subobjects, +contain _any_ member or data value which is unrecognized, unsupported, invalid, or in any other way unexpected. +At a minimum, this includes unrecognized members in a JSON object, or incorrect types of expected members. + +For the same reason, consumers SHOULD accept any members with unrecognized names in the `optional` object, +and MAY accept signatures where the object member is recognized but unsupported, or the value of the member is unsupported. +Consumers still SHOULD reject signatures where a member of an `optional` object is supported but the value is recognized as invalid. + +## JSON data format + +An example of the full format follows, with detailed description below. +To reiterate, consumers of the signature SHOULD perform successful cryptographic verification, +and MUST reject unexpected data in the `critical` object, or in the top-level object, as described above. + +```json +{ + "critical": { + "type": "atomic container signature", + "image": { + "docker-manifest-digest": "sha256:817a12c32a39bbe394944ba49de563e085f1d3c5266eb8e9723256bc4448680e" + }, + "identity": { + "docker-reference": "docker.io/library/busybox:latest" + } + }, + "optional": { + "creator": "some software package v1.0.1-35", + "timestamp": 1483228800, + } +} +``` + +### `critical` + +This MUST be a JSON object which contains data critical to correctly evaluating the validity of a signature. + +Consumers MUST reject any signature where the `critical` object contains any unrecognized, unsupported, invalid or in any other way unexpected member or data. + +### `critical.type` + +This MUST be a string with a string value exactly equal to `atomic container signature` (three words, including the spaces). + +Signature consumers MUST reject signatures which do not have this member or this member does not have exactly the expected value. + +(The consumers MAY support signatures with a different value of the `type` member, if any is defined in the future; +if so, the rest of the JSON document is interpreted according to rules defining that value of `critical.type`, +not by this document.) + +### `critical.image` + +This MUST be a JSON object which identifies the container image this signature applies to. + +Consumers MUST reject any signature where the `critical.image` object contains any unrecognized, unsupported, invalid or in any other way unexpected member or data. + +(Currently only the `docker-manifest-digest` way of identifying a container image is defined; +alternatives to this may be defined in the future, +but existing consumers are required to reject signatures which use formats they do not support.) + +### `critical.image.docker-manifest-digest` + +This MUST be a JSON string, in the `github.com/opencontainers/go-digest.Digest` string format. + +The value of this member MUST match the manifest of the signed container image, as implemented in the docker/distribution manifest addressing system. + +The consumer of the signature SHOULD verify the manifest digest against a fully verified signature before processing the contents of the image manifest in any other way +(e.g. parsing the manifest further or downloading layers of the image). + +Implementation notes: +* A single container image manifest may have several valid manifest digest values, using different algorithms. +* For “signed” [docker/distribution schema 1](https://github.com/docker/distribution/blob/master/docs/spec/manifest-v2-1.md) manifests, +the manifest digest applies to the payload of the JSON web signature, not to the raw manifest blob. + +### `critical.identity` + +This MUST be a JSON object which identifies the claimed identity of the image (usually the purpose of the image, or the application, along with a version information), +as asserted by the author of the signature. + +Consumers MUST reject any signature where the `critical.identity` object contains any unrecognized, unsupported, invalid or in any other way unexpected member or data. + +(Currently only the `docker-reference` way of claiming an image identity/purpose is defined; +alternatives to this may be defined in the future, +but existing consumers are required to reject signatures which use formats they do not support.) + +### `critical.identity.docker-reference` + +This MUST be a JSON string, in the `github.com/docker/distribution/reference` string format, +and using the same normalization semantics (where e.g. `busybox:latest` is equivalent to `docker.io/library/busybox:latest`). +If the normalization semantics allows multiple string representations of the claimed identity with equivalent meaning, +the `critical.identity.docker-reference` member SHOULD use the fully explicit form (including the full host name and namespaces). + +The value of this member MUST match the image identity/purpose expected by the consumer of the image signature and the image +(again, accounting for the `docker/distribution/reference` normalization semantics). + +In the most common case, this means that the `critical.identity.docker-reference` value must be equal to the docker/distribution reference used to refer to or download the image. +However, depending on the specific application, users or system administrators may accept less specific matches +(e.g. ignoring the tag value in the signature when pulling the `:latest` tag or when referencing an image by digest), +or they may require `critical.identity.docker-reference` values with a completely different namespace to the reference used to refer to/download the image +(e.g. requiring a `critical.identity.docker-reference` value which identifies the image as coming from a supplier when fetching it from a company-internal mirror of approved images). +The software performing this verification SHOULD allow the users to define such a policy using the [policy.json signature verification policy file format](containers-policy.json.5.md). + +The `critical.identity.docker-reference` value SHOULD contain either a tag or digest; +in most cases, it SHOULD use a tag rather than a digest. (See also the default [`matchRepoDigestOrExact` matching semantics in `policy.json`](containers-policy.json.5.md#signedby).) + +### `optional` + +This MUST be a JSON object. + +Consumers SHOULD accept any members with unrecognized names in the `optional` object, +and MAY accept a signature where the object member is recognized but unsupported, or the value of the member is valid but unsupported. +Consumers still SHOULD reject any signature where a member of an `optional` object is supported but the value is recognized as invalid. + +### `optional.creator` + +If present, this MUST be a JSON string, identifying the name and version of the software which has created the signature +(identifying the low-level software implementation; not the top-level caller). + +The contents of this string is not defined in detail; however each implementation creating container signatures: + +- SHOULD define the contents to unambiguously define the software in practice (e.g. it SHOULD contain the name of the software, not only the version number) +- SHOULD use a build and versioning process which ensures that the contents of this string (e.g. an included version number) + changes whenever the format or semantics of the generated signature changes in any way; + it SHOULD not be possible for two implementations which use a different format or semantics to have the same `optional.creator` value +- SHOULD use a format which is reasonably easy to parse in software (perhaps using a regexp), + and which makes it easy enough to recognize a range of versions of a specific implementation + (e.g. the version of the implementation SHOULD NOT be only a git hash, because they don’t have an easily defined ordering; + the string should contain a version number, or at least a date of the commit). + +Consumers of container signatures MAY recognize specific values or sets of values of `optional.creator` +(perhaps augmented with `optional.timestamp`), +and MAY change their processing of the signature based on these values +(usually to accommodate violations of this specification in past versions of the signing software which cannot be fixed retroactively), +as long as the semantics of the invalid document, as created by such an implementation, is clear. + +If consumers of signatures do change their behavior based on the `optional.creator` value, +they SHOULD take care that the way they process the signatures is not inconsistent with +strictly validating signature consumers. +(I.e. it is acceptable for a consumer to accept a signature based on a specific `optional.creator` value +if other implementations would completely reject the signature, +but it would be very undesirable for the two kinds of implementations to accept the signature in different +and inconsistent situations.) + +### `optional.timestamp` + +If present, this MUST be a JSON number, which is representable as a 64-bit integer, and identifies the time when the signature was created +as the number of seconds since the UNIX epoch (Jan 1 1970 00:00 UTC). diff --git a/vendor/github.com/containers/image/v5/docs/containers-sigstore-signing-params.yaml.5.md b/vendor/github.com/containers/image/v5/docs/containers-sigstore-signing-params.yaml.5.md new file mode 100644 index 00000000000..f081cdb87d3 --- /dev/null +++ b/vendor/github.com/containers/image/v5/docs/containers-sigstore-signing-params.yaml.5.md @@ -0,0 +1,117 @@ +% CONTAINERS-SIGSTORE-SIGNING-PARAMS.YAML 5 sigstore signing parameters Man Page +% Miloslav Trmač +% January 2023 + +# NAME +containers-sigstore-signing-params.yaml - syntax for the sigstore signing parameter file + +# DESCRIPTION + +Sigstore signing parameter files are used to store options that may be required to create sigstore signatures. +There is no default location for these files; they are user-managed, and used as inputs to a container image signing operation, +e.g. `skopeo copy --sign-by-sigstore=`_param-file_`.yaml` or `podman push --sign-by-sigstore=`_param-file_`.yaml` . + +## FORMAT + +Sigstore signing parameter files use YAML. + +Many parameters are optional, but the file must specify enough to create a signature; +in particular either a private key, or Fulcio. + +### Signing with Private Keys + +- `privateKeyFile:` _path_ + + Create a signature using a private key at _path_. + Existence of this field triggers the use of a private key. + +- `privateKeyPassphraseFile:` _passphrasePath_ + + Read the passphrase required to use `privateKeyFile` from _passphrasePath_. + Optional: if this is not set, the user must provide the passphrase interactively. + +### Signing with Fulcio-generated Certificates + +Instead of a static private key, the signing process generates a short-lived key pair +and requests a Fulcio server to issue a certificate for that key pair, +based on the user authenticating to an OpenID Connect provider. + +To specify Fulcio, include a `fulcio` sub-object with one or more of the following keys. +In addition, a Rekor server must be specified as well. + +- `fulcioURL:` _URL_ + + Required. URL of the Fulcio server to use. + +- `oidcMode:` `interactive` | `deviceGrant` | `staticToken` + + Required. Specifies how to obtain the necessary OpenID Connect credential. + + `interactive` opens a web browser on the same machine, or if that is not possible, + asks the user to open a browser manually and to type in the provided code. + It requires the user to be able to directly interact with the signing process. + + `deviceGrant` uses a device authorization grant flow (RFC 8628). + It requires the user to be able to read text printed by the signing process, and to act on it reasonably promptly. + + `staticToken` provides a pre-existing OpenID Connect “ID token”, which must have been obtained separately. + +- `oidcIssuerURL:` _URL_ + + Required for `oidcMode:` `interactive` or `deviceGrant`. URL of an OpenID Connect issuer server to authenticate with. + +- `oidcClientID:` _client ID_ + + Used for `oidcMode:` `interactive` or `deviceGrant` to identify the client when contacting the issuer. + Optional but likely to be necessary in those cases. + +- `oidcClientSecret:` _client secret_ + + Used for `oidcMode:` `interactive` or `deviceGrant` to authenticate the client when contacting the issuer. + Optional. + +- `oidcIDToken:` _token_ + + Required for `oidcMode: staticToken`. + An OpenID Connect ID token that identifies the user (and authorizes certificate issuance). + +### Recording the Signature to a Rekor Transparency Server + +This can be combined with either a private key or Fulcio. +It is, practically speaking, required for Fulcio; it is optional when a static private key is used, but necessary for +interoperability with the default configuration of `cosign`. + +- `rekorURL`: _URL_ + + URL of the Rekor server to use. + +# EXAMPLES + +### Sign Using a Pre-existing Private Key + +Uses the ”community infrastructure” Rekor server. + +```yaml +privateKeyFile: "/home/user/sigstore/private-key.key" +privateKeyPassphraseFile: "/mnt/user/sigstore-private-key" +rekorURL: "https://rekor.sigstore.dev" +``` + +### Sign Using a Fulcio-Issued Certificate + +Uses the ”community infrastructure” Fulcio and Rekor server, +and the Dex OIDC issuer which delegates to other major issuers like Google and GitHub. + +Other configurations will very likely need to also provide an OIDC client secret. + +```yaml +fulcio: + fulcioURL: "https://fulcio.sigstore.dev" + oidcMode: "interactive" + oidcIssuerURL: "https://oauth2.sigstore.dev/auth" + oidcClientID: "sigstore" +rekorURL: "https://rekor.sigstore.dev" +``` + +# SEE ALSO + skopeo(1), podman(1) diff --git a/vendor/github.com/containers/image/v5/docs/containers-transports.5.md b/vendor/github.com/containers/image/v5/docs/containers-transports.5.md new file mode 100644 index 00000000000..0f4cacfec0d --- /dev/null +++ b/vendor/github.com/containers/image/v5/docs/containers-transports.5.md @@ -0,0 +1,123 @@ +% CONTAINERS-TRANSPORTS 5 Containers Transports Man Page +% Valentin Rothberg +% April 2019 + +## NAME + +containers-transports - description of supported transports for copying and storing container images + +## DESCRIPTION + +Tools which use the containers/image library, including skopeo(1), buildah(1), podman(1), all share a common syntax for referring to container images in various locations. +The general form of the syntax is _transport:details_, where details are dependent on the specified transport, which are documented below. + +The semantics of the image names ultimately depend on the environment where +they are evaluated. For example: if evaluated on a remote server, image names +might refer to paths on that server; relative paths are relative to the current +directory of the image consumer. + +### **containers-storage**:[**[**storage-specifier**]**]{image-id|docker-reference[@image-id]} + +An image located in a local containers storage. +The format of _docker-reference_ is described in detail in the **docker** transport. + +The _storage-specifier_ allows for referencing storage locations on the file system and has the format `[[driver@]root[+run-root][:options]]` where the optional `driver` refers to the storage driver (e.g., overlay or btrfs) and where `root` is an absolute path to the storage's root directory. +The optional `run-root` can be used to specify the run directory of the storage where all temporary writable content is stored. +The optional `options` are a comma-separated list of driver-specific options. +Please refer to containers-storage.conf(5) for further information on the drivers and supported options. + +### **dir:**_path_ + +An existing local directory _path_ storing the manifest, layer tarballs and signatures as individual files. +This is a non-standardized format, primarily useful for debugging or noninvasive container inspection. + +### **docker://**_docker-reference_ + +An image in a registry implementing the "Docker Registry HTTP API V2". +By default, uses the authorization state in `$XDG_RUNTIME_DIR/containers/auth.json`, which is set using podman-login(1). +If the authorization state is not found there, `$HOME/.docker/config.json` is checked, which is set using docker-login(1). +The containers-registries.conf(5) further allows for configuring various settings of a registry. + +Note that a _docker-reference_ has the following format: `name[:tag|@digest]`. +While the docker transport does not support both a tag and a digest at the same time some formats like containers-storage do. +Digests can also be used in an image destination as long as the manifest matches the provided digest. +The digest of images can be explored with skopeo-inspect(1). +If `name` does not contain a slash, it is treated as `docker.io/library/name`. +Otherwise, the component before the first slash is checked if it is recognized as a `hostname[:port]` (i.e., it contains either a . or a :, or the component is exactly localhost). +If the first component of name is not recognized as a `hostname[:port]`, `name` is treated as `docker.io/name`. + +### **docker-archive:**_path[:{docker-reference|@source-index}]_ + +An image is stored in the docker-save(1) formatted file. +_docker-reference_ must not contain a digest. +Alternatively, for reading archives, @_source-index_ is a zero-based index in archive manifest +(to access untagged images). +If neither _docker-reference_ nor @_source_index is specified when reading an archive, the archive must contain exactly one image. + +It is further possible to copy data to stdin by specifying `docker-archive:/dev/stdin` but note that the used file must be seekable. + +### **docker-daemon:**_docker-reference|algo:digest_ + +An image stored in the docker daemon's internal storage. +The image must be specified as a _docker-reference_ or in an alternative _algo:digest_ format when being used as an image source. +The _algo:digest_ refers to the image ID reported by docker-inspect(1). + +### **oci:**_path[:reference]_ + +An image in a directory structure compliant with the "Open Container Image Layout Specification" at _path_. + +_Path_ terminates at the first `:` character; any further `:` characters are not separators, but a part of _reference_. +Specify a _reference_ to allow storing multiple images within the same _path_. + +### **oci-archive:**_path[:reference]_ + +An image in a tar(1) archive with contents compliant with the "Open Container Image Layout Specification" at _path_. + +_Path_ terminates at the first `:` character; any further `:` characters are not separators, but a part of _reference_. +Specify a _reference_ to allow storing multiple images within the same _path_. + +### **ostree:**_docker-reference[@/absolute/repo/path]_ + +An image in the local ostree(1) repository. +_/absolute/repo/path_ defaults to _/ostree/repo_. + +## Examples + +The following examples demonstrate how some of the containers transports can be used. +The examples use skopeo-copy(1) for copying container images. + +**Copying an image from one registry to another**: +``` +$ skopeo copy docker://docker.io/library/alpine:latest docker://localhost:5000/alpine:latest +``` + +**Copying an image from a running Docker daemon to a directory in the OCI layout**: +``` +$ mkdir alpine-oci +$ skopeo copy docker-daemon:alpine:latest oci:alpine-oci +$ tree alpine-oci +test-oci/ +├── blobs +│   └── sha256 +│   ├── 83ef92b73cf4595aa7fe214ec6747228283d585f373d8f6bc08d66bebab531b7 +│   ├── 9a6259e911dcd0a53535a25a9760ad8f2eded3528e0ad5604c4488624795cecc +│   └── ff8df268d29ccbe81cdf0a173076dcfbbea4bb2b6df1dd26766a73cb7b4ae6f7 +├── index.json +└── oci-layout + +2 directories, 5 files +``` + +**Copying an image from a registry to the local storage**: +``` +$ skopeo copy docker://docker.io/library/alpine:latest containers-storage:alpine:latest +``` + +## SEE ALSO + +docker-login(1), docker-save(1), ostree(1), podman-login(1), skopeo-copy(1), skopeo-inspect(1), tar(1), container-registries.conf(5), containers-storage.conf(5) + +## AUTHORS + +Miloslav Trmač +Valentin Rothberg diff --git a/vendor/github.com/containers/image/v5/docs/signature-protocols.md b/vendor/github.com/containers/image/v5/docs/signature-protocols.md new file mode 100644 index 00000000000..334c589e997 --- /dev/null +++ b/vendor/github.com/containers/image/v5/docs/signature-protocols.md @@ -0,0 +1,136 @@ +# Signature access protocols + +The `github.com/containers/image` library supports signatures implemented as blobs “attached to” an image. +Some image transports (local storage formats and remote protocols) implement these signatures natively +or trivially; for others, the protocol extensions described below are necessary. + +## docker/distribution registries—separate storage + +### Usage + +Any existing docker/distribution registry, whether or not it natively supports signatures, +can be augmented with separate signature storage by configuring a signature storage URL in [`registries.d`](containers-registries.d.md). +`registries.d` can be configured to use one storage URL for a whole docker/distribution server, +or also separate URLs for smaller namespaces or individual repositories within the server +(which e.g. allows image authors to manage their own signature storage while publishing +the images on the public `docker.io` server). + +The signature storage URL defines a root of a path hierarchy. +It can be either a `file:///…` URL, pointing to a local directory structure, +or a `http`/`https` URL, pointing to a remote server. +`file:///` signature storage can be both read and written, `http`/`https` only supports reading. + +The same path hierarchy is used in both cases, so the HTTP/HTTPS server can be +a simple static web server serving a directory structure created by writing to a `file:///` signature storage. +(This of course does not prevent other server implementations, +e.g. a HTTP server reading signatures from a database.) + +The usual workflow for producing and distributing images using the separate storage mechanism +is to configure the repository in `registries.d` with `lookaside-staging` URL pointing to a private +`file:///` staging area, and a `lookaside` URL pointing to a public web server. +To publish an image, the image author would sign the image as necessary (e.g. using `skopeo copy`), +and then copy the created directory structure from the `file:///` staging area +to a subdirectory of a webroot of the public web server so that they are accessible using the public `lookaside` URL. +The author would also instruct consumers of the image to, or provide a `registries.d` configuration file to, +set up a `lookaside` URL pointing to the public web server. + +### Path structure + +Given a _base_ signature storage URL configured in `registries.d` as mentioned above, +and a container image stored in a docker/distribution registry using the _fully-expanded_ name +_hostname_`/`_namespaces_`/`_name_{`@`_digest_,`:`_tag_} (e.g. for `docker.io/library/busybox:latest`, +_namespaces_ is `library`, even if the user refers to the image using the shorter syntax as `busybox:latest`), +signatures are accessed using URLs of the form +> _base_`/`_namespaces_`/`_name_`@`_digest-algo_`=`_digest-value_`/signature-`_index_ + +where _digest-algo_`:`_digest-value_ is a manifest digest usable for referencing the relevant image manifest +(i.e. even if the user referenced the image using a tag, +the signature storage is always disambiguated using digest references). +Note that in the URLs used for signatures, +_digest-algo_ and _digest-value_ are separated using the `=` character, +not `:` like when accessing the manifest using the docker/distribution API. + +Within the URL, _index_ is a decimal integer (in the canonical form), starting with 1. +Signatures are stored at URLs with successive _index_ values; to read all of them, start with _index_=1, +and continue reading signatures and increasing _index_ as long as signatures with these _index_ values exist. +Similarly, to add one more signature to an image, find the first _index_ which does not exist, and +then store the new signature using that _index_ value. + +There is no way to list existing signatures other than iterating through the successive _index_ values, +and no way to download all of the signatures at once. + +### Examples + +For a docker/distribution image available as `busybox@sha256:817a12c32a39bbe394944ba49de563e085f1d3c5266eb8e9723256bc4448680e` +(or as `busybox:latest` if the `latest` tag points to to a manifest with the same digest), +and with a `registries.d` configuration specifying a `lookaside` URL `https://example.com/lookaside` for the same image, +the following URLs would be accessed to download all signatures: +> - `https://example.com/lookaside/library/busybox@sha256=817a12c32a39bbe394944ba49de563e085f1d3c5266eb8e9723256bc4448680e/signature-1` +> - `https://example.com/lookaside/library/busybox@sha256=817a12c32a39bbe394944ba49de563e085f1d3c5266eb8e9723256bc4448680e/signature-2` +> - … + +For a docker/distribution image available as `example.com/ns1/ns2/ns3/repo@somedigest:digestvalue` and the same +`lookaside` URL, the signatures would be available at +> `https://example.com/lookaside/ns1/ns2/ns3/repo@somedigest=digestvalue/signature-1` + +and so on. + +## (OpenShift) docker/distribution API extension + +As of https://github.com/openshift/origin/pull/12504/ , the OpenShift-embedded registry also provides +an extension of the docker/distribution API which allows simpler access to the signatures, +using only the docker/distribution API endpoint. + +This API is not inherently OpenShift-specific (e.g. the client does not need to know the OpenShift API endpoint, +and credentials sufficient to access the docker/distribution API server are sufficient to access signatures as well), +and it is the preferred way implement signature storage in registries. + +See https://github.com/openshift/openshift-docs/pull/3556 for the upstream documentation of the API. + +To read the signature, any user with access to an image can use the `/extensions/v2/…/signatures/…` +path to read an array of signatures. Use only the signature objects +which have `version` equal to `2`, `type` equal to `atomic`, and read the signature from `content`; +ignore the other fields of the signature object. + +To add a single signature, `PUT` a new object with `version` set to `2`, `type` set to `atomic`, +and `content` set to the signature. Also set `name` to an unique name with the form +_digest_`@`_per-image-name_, where _digest_ is an image manifest digest (also used in the URL), +and _per-image-name_ is any unique identifier. + +To add more than one signature, add them one at a time. This API does not allow deleting signatures. + +Note that because signatures are stored within the cluster-wide image objects, +i.e. different namespaces can not associate different sets of signatures to the same image, +updating signatures requires a cluster-wide access to the `imagesignatures` resource +(by default available to the `system:image-signer` role), + +## OpenShift-embedded registries + +The OpenShift-embedded registry implements the ordinary docker/distribution API, +and it also exposes images through the OpenShift REST API (available through the “API master” servers). + +Note: OpenShift versions 1.5 and later support the above-described [docker/distribution API extension](#openshift-dockerdistribution-api-extension), +which is easier to set up and should usually be preferred. +Continue reading for details on using older versions of OpenShift. + +As of https://github.com/openshift/origin/pull/9181, +signatures are exposed through the OpenShift API +(i.e. to access the complete image, it is necessary to use both APIs, +in particular to know the URLs for both the docker/distribution and the OpenShift API master endpoints). + +To read the signature, any user with access to an image can use the `imagestreamimages` namespaced +resource to read an `Image` object and its `Signatures` array. Use only the `ImageSignature` objects +which have `Type` equal to `atomic`, and read the signature from `Content`; ignore the other fields of +the `ImageSignature` object. + +To add or remove signatures, use the cluster-wide (non-namespaced) `imagesignatures` resource, +with `Type` set to `atomic` and `Content` set to the signature. Signature names must have the form +_digest_`@`_per-image-name_, where _digest_ is an image manifest digest (OpenShift “image name”), +and _per-image-name_ is any unique identifier. + +Note that because signatures are stored within the cluster-wide image objects, +i.e. different namespaces can not associate different sets of signatures to the same image, +updating signatures requires a cluster-wide access to the `imagesignatures` resource +(by default available to the `system:image-signer` role), +and deleting signatures is strongly discouraged +(it deletes the signature from all namespaces which contain the same image). diff --git a/vendor/github.com/containers/image/v5/go.mod b/vendor/github.com/containers/image/v5/go.mod new file mode 100644 index 00000000000..ebaa227c70f --- /dev/null +++ b/vendor/github.com/containers/image/v5/go.mod @@ -0,0 +1,130 @@ +module github.com/containers/image/v5 + +go 1.18 + +require ( + dario.cat/mergo v1.0.0 + github.com/BurntSushi/toml v1.3.2 + github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 + github.com/containers/ocicrypt v1.1.7 + github.com/containers/storage v1.48.0 + github.com/cyberphone/json-canonicalization v0.0.0-20230701045847-91eb5f1b7744 + github.com/docker/distribution v2.8.2+incompatible + github.com/docker/docker v24.0.4+incompatible + github.com/docker/docker-credential-helpers v0.7.0 + github.com/docker/go-connections v0.4.0 + github.com/go-openapi/strfmt v0.21.7 + github.com/go-openapi/swag v0.22.4 + github.com/hashicorp/go-multierror v1.1.1 + github.com/hashicorp/go-retryablehttp v0.7.4 + github.com/klauspost/compress v1.16.7 + github.com/klauspost/pgzip v1.2.6 + github.com/manifoldco/promptui v0.9.0 + github.com/opencontainers/go-digest v1.0.0 + github.com/opencontainers/image-spec v1.1.0-rc4 + github.com/opencontainers/selinux v1.11.0 + github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f + github.com/proglottis/gpgme v0.1.3 + github.com/sigstore/fulcio v1.3.4 + github.com/sigstore/rekor v1.2.2 + github.com/sigstore/sigstore v1.7.1 + github.com/sirupsen/logrus v1.9.3 + github.com/stretchr/testify v1.8.4 + github.com/sylabs/sif/v2 v2.11.5 + github.com/theupdateframework/go-tuf v0.5.2 + github.com/ulikunitz/xz v0.5.11 + github.com/vbatts/tar-split v0.11.3 + github.com/vbauerster/mpb/v8 v8.4.0 + github.com/xeipuuv/gojsonschema v1.2.0 + go.etcd.io/bbolt v1.3.7 + golang.org/x/crypto v0.11.0 + golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df + golang.org/x/oauth2 v0.10.0 + golang.org/x/sync v0.3.0 + golang.org/x/term v0.10.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/14rcole/gopopulate v0.0.0-20180821133914-b175b219e774 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/Microsoft/hcsshim v0.9.9 // indirect + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/chzyer/readline v1.5.1 // indirect + github.com/containerd/cgroups v1.0.4 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect + github.com/coreos/go-oidc/v3 v3.6.0 // indirect + github.com/cyphar/filepath-securejoin v0.2.3 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/go-jose/go-jose/v3 v3.0.0 // indirect + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/analysis v0.21.4 // indirect + github.com/go-openapi/errors v0.20.4 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/loads v0.21.2 // indirect + github.com/go-openapi/runtime v0.26.0 // indirect + github.com/go-openapi/spec v0.20.9 // indirect + github.com/go-openapi/validate v0.22.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/go-containerregistry v0.15.2 // indirect + github.com/google/go-intervals v0.0.2 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/gorilla/mux v1.8.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kr/pretty v0.3.0 // indirect + github.com/letsencrypt/boulder v0.0.0-20230213213521-fdfea0d469b6 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mattn/go-shellwords v1.0.12 // indirect + github.com/miekg/pkcs11 v1.1.1 // indirect + github.com/mistifyio/go-zfs/v3 v3.0.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/moby/sys/mountinfo v0.6.2 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/opencontainers/runc v1.1.7 // indirect + github.com/opencontainers/runtime-spec v1.1.0-rc.3 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/segmentio/ksuid v1.0.4 // indirect + github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect + github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980 // indirect + github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect + github.com/tchap/go-patricia/v2 v2.3.1 // indirect + github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + go.mongodb.org/mongo-driver v1.11.3 // indirect + go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/otel v1.16.0 // indirect + go.opentelemetry.io/otel/metric v1.16.0 // indirect + go.opentelemetry.io/otel/trace v1.16.0 // indirect + golang.org/x/mod v0.11.0 // indirect + golang.org/x/net v0.12.0 // indirect + golang.org/x/sys v0.10.0 // indirect + golang.org/x/text v0.11.0 // indirect + golang.org/x/tools v0.8.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230629202037-9506855d4529 // indirect + google.golang.org/grpc v1.56.2 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/go-jose/go-jose.v2 v2.6.1 // indirect + gopkg.in/square/go-jose.v2 v2.6.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gotest.tools/v3 v3.4.0 // indirect +) diff --git a/vendor/github.com/containers/image/v5/go.sum b/vendor/github.com/containers/image/v5/go.sum new file mode 100644 index 00000000000..98b86464772 --- /dev/null +++ b/vendor/github.com/containers/image/v5/go.sum @@ -0,0 +1,1387 @@ +bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/14rcole/gopopulate v0.0.0-20180821133914-b175b219e774 h1:SCbEWT58NSt7d2mcFdvxC9uyrdcTfvBbPLThhkDmXzg= +github.com/14rcole/gopopulate v0.0.0-20180821133914-b175b219e774/go.mod h1:6/0dYRLLXyJjbkIPeeGyoJ/eKOSI0eU6eTlCBYibgd0= +github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= +github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= +github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= +github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= +github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8= +github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= +github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00= +github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600= +github.com/Microsoft/hcsshim v0.8.21/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4= +github.com/Microsoft/hcsshim v0.9.9 h1:FYrTiCNOc8ZddNBVkJBxWZYm22rgxHFmxMoGK66sDF0= +github.com/Microsoft/hcsshim v0.9.9/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= +github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= +github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= +github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= +github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= +github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= +github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= +github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc= +github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= +github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= +github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE= +github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU= +github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= +github.com/containerd/aufs v1.0.0/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= +github.com/containerd/btrfs v0.0.0-20201111183144-404b9149801e/go.mod h1:jg2QkJcsabfHugurUvvPhS3E08Oxiuh5W/g1ybB4e0E= +github.com/containerd/btrfs v0.0.0-20210316141732-918d888fb676/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= +github.com/containerd/btrfs v1.0.0/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= +github.com/containerd/cgroups v0.0.0-20190717030353-c4b9ac5c7601/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI= +github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= +github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM= +github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= +github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= +github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= +github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU= +github.com/containerd/cgroups v1.0.4 h1:jN/mbWBEaz+T1pi5OFtnkQ+8qnmEbAr1Oo1FRm5B0dA= +github.com/containerd/cgroups v1.0.4/go.mod h1:nLNQtsF7Sl2HxNebu77i1R0oDlhiTG+kO4JTrUzo6IA= +github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= +github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= +github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE= +github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw= +github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= +github.com/containerd/containerd v1.2.10/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.1-0.20191213020239-082f7e3aed57/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7V960Tmcumvqn8Mc+pCYQ= +github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU= +github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI= +github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s= +github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g= +github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c= +github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo= +github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y= +github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ= +github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM= +github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= +github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= +github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= +github.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= +github.com/containerd/fifo v0.0.0-20210316144830-115abcc95a1d/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= +github.com/containerd/fifo v1.0.0/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= +github.com/containerd/go-cni v1.0.1/go.mod h1:+vUpYxKvAF72G9i1WoDOiPGRtQpqsNW/ZHtSlv++smU= +github.com/containerd/go-cni v1.0.2/go.mod h1:nrNABBHzu0ZwCug9Ije8hL2xBCYh/pjfMb1aZGrrohk= +github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= +github.com/containerd/go-runc v0.0.0-20190911050354-e029b79d8cda/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= +github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g= +github.com/containerd/go-runc v0.0.0-20201020171139-16b287bc67d0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= +github.com/containerd/go-runc v1.0.0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= +github.com/containerd/imgcrypt v1.0.1/go.mod h1:mdd8cEPW7TPgNG4FpuP3sGBiQ7Yi/zak9TYCG3juvb0= +github.com/containerd/imgcrypt v1.0.4-0.20210301171431-0ae5c75f59ba/go.mod h1:6TNsg0ctmizkrOgXRNQjAPFWpMYRWuiB6dSF4Pfa5SA= +github.com/containerd/imgcrypt v1.1.1-0.20210312161619-7ed62a527887/go.mod h1:5AZJNI6sLHJljKuI9IHnw1pWqo/F0nGDOuR9zgTs7ow= +github.com/containerd/imgcrypt v1.1.1/go.mod h1:xpLnwiQmEUJPvQoAapeb2SNCxz7Xr6PJrXQb0Dpc4ms= +github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c= +github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= +github.com/containerd/nri v0.1.0/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= +github.com/containerd/stargz-snapshotter/estargz v0.4.1/go.mod h1:x7Q9dg9QYb4+ELgxmo4gBUeJB0tl5dqH1Sdz0nJU1QM= +github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= +github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= +github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= +github.com/containerd/ttrpc v0.0.0-20190828172938-92c8520ef9f8/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= +github.com/containerd/ttrpc v0.0.0-20191028202541-4f1b8fe65a5c/go.mod h1:LPm1u0xBw8r8NOKoOdNMeVHSawSsltak+Ihv+etqsE8= +github.com/containerd/ttrpc v1.0.1/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= +github.com/containerd/ttrpc v1.0.2/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= +github.com/containerd/ttrpc v1.1.0/go.mod h1:XX4ZTnoOId4HklF4edwc4DcqskFZuvXB1Evzy5KFQpQ= +github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= +github.com/containerd/typeurl v0.0.0-20190911142611-5eb25027c9fd/go.mod h1:GeKYzf2pQcqv7tJ0AoCuuhtnqhva5LNU3U+OyKxxJpk= +github.com/containerd/typeurl v1.0.1/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg= +github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s= +github.com/containerd/zfs v0.0.0-20200918131355-0a33824f23a2/go.mod h1:8IgZOBdv8fAgXddBT4dBXJPtxyRsejFIpXoklgxgEjw= +github.com/containerd/zfs v0.0.0-20210301145711-11e8f1707f62/go.mod h1:A9zfAbMlQwE+/is6hi0Xw8ktpL+6glmqZYtevJgaB8Y= +github.com/containerd/zfs v0.0.0-20210315114300-dde8f0fda960/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= +github.com/containerd/zfs v0.0.0-20210324211415-d5c4544f0433/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= +github.com/containerd/zfs v1.0.0/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= +github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/containernetworking/cni v0.8.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM= +github.com/containernetworking/plugins v0.9.1/go.mod h1:xP/idU2ldlzN6m4p5LmGiwRDjeJr6FLK6vuiUwoH7P8= +github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 h1:Qzk5C6cYglewc+UyGf6lc8Mj2UaPTHy/iF2De0/77CA= +github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY= +github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc= +github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgUV4GP9qXPfu4= +github.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY= +github.com/containers/ocicrypt v1.1.7 h1:thhNr4fu2ltyGz8aMx8u48Ae0Pnbip3ePP9/mzkZ/3U= +github.com/containers/ocicrypt v1.1.7/go.mod h1:7CAhjcj2H8AYp5YvEie7oVSK2AhBY8NscCYRawuDNtw= +github.com/containers/storage v1.48.0 h1:wiPs8J2xiFoOEAhxHDRtP6A90Jzj57VqzLRXOqeizns= +github.com/containers/storage v1.48.0/go.mod h1:pRp3lkRo2qodb/ltpnudoXggrviRmaCmU5a5GhTBae0= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= +github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= +github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-oidc/v3 v3.6.0 h1:AKVxfYw1Gmkn/w96z0DbT/B/xFnzTd3MkZvWLjF4n/o= +github.com/coreos/go-oidc/v3 v3.6.0/go.mod h1:ZpHUsHBucTUj6WOkrP4E20UPynbLZzhTQ1XKCXkxyPc= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyberphone/json-canonicalization v0.0.0-20230701045847-91eb5f1b7744 h1:MqMnhqqfDsYF2bjxndKIqvISTIRBb1KCzrIwVzKJHe0= +github.com/cyberphone/json-canonicalization v0.0.0-20230701045847-91eb5f1b7744/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= +github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= +github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI= +github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= +github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= +github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8= +github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= +github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= +github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= +github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v24.0.4+incompatible h1:s/LVDftw9hjblvqIeTiGYXBCD95nOEEl7qRsRrIOuQI= +github.com/docker/docker v24.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= +github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= +github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= +github.com/facebookgo/limitgroup v0.0.0-20150612190941-6abd8d71ec01 h1:IeaD1VDVBPlx3viJT9Md8if8IxxJnO+x0JCGb054heg= +github.com/facebookgo/muster v0.0.0-20150708232844-fd3d7953fd52 h1:a4DFiKFJiDRGFD1qIcqGLX/WlUMD9dyLSLDt+9QZgt8= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= +github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= +github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/analysis v0.21.2/go.mod h1:HZwRk4RRisyG8vx2Oe6aqeSQcoxRp47Xkp3+K6q+LdY= +github.com/go-openapi/analysis v0.21.4 h1:ZDFLvSNxpDaomuCueM0BlSXxpANBlFYiBvr+GXrvIHc= +github.com/go-openapi/analysis v0.21.4/go.mod h1:4zQ35W4neeZTqh3ol0rv/O8JBbka9QyAgQRPp9y3pfo= +github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.19.9/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.20.4 h1:unTcVm6PispJsMECE3zWgvG4xTiKda1LIR5rCRWLG6M= +github.com/go-openapi/errors v0.20.4/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuAOhlsB1FSgk= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/loads v0.21.1/go.mod h1:/DtAMXXneXFjbQMGEtbamCZb+4x7eGwkvZCvBmwUG+g= +github.com/go-openapi/loads v0.21.2 h1:r2a/xFIYeZ4Qd2TnGpWDIQNcP80dIaZgf704za8enro= +github.com/go-openapi/loads v0.21.2/go.mod h1:Jq58Os6SSGz0rzh62ptiu8Z31I+OTHqmULx5e/gJbNw= +github.com/go-openapi/runtime v0.26.0 h1:HYOFtG00FM1UvqrcxbEJg/SwvDRvYLQKGhw2zaQjTcc= +github.com/go-openapi/runtime v0.26.0/go.mod h1:QgRGeZwrUcSHdeh4Ka9Glvo0ug1LC5WyE+EV88plZrQ= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8= +github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/strfmt v0.21.0/go.mod h1:ZRQ409bWMj+SOgXofQAGTIo2Ebu72Gs+WaRADcS5iNg= +github.com/go-openapi/strfmt v0.21.1/go.mod h1:I/XVKeLc5+MM5oPNN7P6urMOpuLXEcNrCX/rPGuWb0k= +github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= +github.com/go-openapi/strfmt v0.21.7 h1:rspiXgNWgeUzhjo1YU01do6qsahtJNByjLVbPLNHb8k= +github.com/go-openapi/strfmt v0.21.7/go.mod h1:adeGTkxE44sPyLk0JV235VQAO/ZXUr8KAzYjclFs3ew= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/validate v0.22.1 h1:G+c2ub6q47kfX1sOBLwIQwzBVt8qmOAARyo/9Fqs9NU= +github.com/go-openapi/validate v0.22.1/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= +github.com/go-rod/rod v0.113.3 h1:oLiKZW721CCMwA5g7977cWfcAKQ+FuosP47Zf1QiDrA= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= +github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= +github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= +github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= +github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= +github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= +github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= +github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= +github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= +github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= +github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= +github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= +github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= +github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= +github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= +github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= +github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= +github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU= +github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0= +github.com/google/go-containerregistry v0.15.2 h1:MMkSh+tjSdnmJZO7ljvEqV1DjfekB6VUEAZgy3a+TQE= +github.com/google/go-containerregistry v0.15.2/go.mod h1:wWK+LnOv4jXMM23IT/F1wdYftGWGr47Is8CG+pmHK1Q= +github.com/google/go-intervals v0.0.2 h1:FGrVEiUnTRKR8yE04qzXYaJMtnIYqobR5QbblK3ixcM= +github.com/google/go-intervals v0.0.2/go.mod h1:MkaR3LNRfeKLPmqgJYs4E66z5InYjmCjbbr4TQlcT6Y= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA= +github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/honeycombio/beeline-go v1.10.0 h1:cUDe555oqvw8oD76BQJ8alk7FP0JZ/M/zXpNvOEDLDc= +github.com/honeycombio/libhoney-go v1.16.0 h1:kPpqoz6vbOzgp7jC6SR7SkNj7rua7rgxvznI6M3KdHc= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= +github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548 h1:dYTbLf4m0a5u0KLmPfB6mgxbcV7588bOCx79hxa5Sr4= +github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= +github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= +github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/letsencrypt/boulder v0.0.0-20230213213521-fdfea0d469b6 h1:unJdfS94Y3k85TKy+mvKzjW5R9rIC+Lv4KGbE7uNu0I= +github.com/letsencrypt/boulder v0.0.0-20230213213521-fdfea0d469b6/go.mod h1:PUgW5vI9ANEaV6qv9a6EKu8gAySgwf0xrzG9xIB/CK0= +github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= +github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= +github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= +github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= +github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= +github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= +github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY= +github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= +github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= +github.com/mistifyio/go-zfs/v3 v3.0.1 h1:YaoXgBePoMA12+S1u/ddkv+QqxcfiZK4prI6HPnkFiU= +github.com/mistifyio/go-zfs/v3 v3.0.1/go.mod h1:CzVgeB0RvF2EGzQnytKVvVSDwmKJXxkOTUGbNrTja/k= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= +github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= +github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= +github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= +github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= +github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= +github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= +github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +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.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.1.0-rc4 h1:oOxKUJWnFC4YGHCCMNql1x4YaDfYBTS5Y4x/Cgeo1E0= +github.com/opencontainers/image-spec v1.1.0-rc4/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= +github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0= +github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0= +github.com/opencontainers/runc v1.1.7 h1:y2EZDS8sNng4Ksf0GUYNhKbTShZJPJg1FiXJNH/uoCk= +github.com/opencontainers/runc v1.1.7/go.mod h1:CbUumNnWCuTGFukNXahoo/RFBZvDAgRh/smNYNOhA50= +github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.3-0.20200929063507-e6143ca7d51d/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.1.0-rc.3 h1:l04uafi6kxByhbxev7OWiuUv0LZxEsYUfDWZ6bztAuU= +github.com/opencontainers/runtime-spec v1.1.0-rc.3/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= +github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE= +github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo= +github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8= +github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU= +github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f h1:/UDgs8FGMqwnHagNDPGOlts35QkhAZ8by3DR7nMih7M= +github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f/go.mod h1:J6OG6YJVEWopen4avK3VNQSnALmmjvniMmni/YFYAwc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= +github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/proglottis/gpgme v0.1.3 h1:Crxx0oz4LKB3QXc5Ea0J19K/3ICfy3ftr5exgUK1AU0= +github.com/proglottis/gpgme v0.1.3/go.mod h1:fPbW/EZ0LvwQtH8Hy7eixhp1eF3G39dtx7GUN+0Gmy0= +github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= +github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= +github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= +github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y= +github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= +github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= +github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sigstore/fulcio v1.3.2 h1:92ubG3JPgpjeKKFWROoWj095SvVKBdbolq22t95qBUg= +github.com/sigstore/fulcio v1.3.2/go.mod h1:RCktAN81mgf6fz7ydiT6X2mSjhMGxaRXfCCa7srztTo= +github.com/sigstore/fulcio v1.3.4 h1:ZpR0eK8CdxYvIiXYR/mbcpB4GCPjVuE44vQsf65dfH8= +github.com/sigstore/fulcio v1.3.4/go.mod h1:7QQMi1lDVhxIYR8IE7buT4FE+WR0lRPTEl19CcmjvXk= +github.com/sigstore/rekor v1.2.2 h1:5JK/zKZvcQpL/jBmHvmFj3YbpDMBQnJQ6ygp8xdF3bY= +github.com/sigstore/rekor v1.2.2/go.mod h1:FGnWBGWzeNceJnp0x9eDFd41mI8aQqCjj+Zp0IEs0Qg= +github.com/sigstore/sigstore v1.7.1 h1:fCATemikcBK0cG4+NcM940MfoIgmioY1vC6E66hXxks= +github.com/sigstore/sigstore v1.7.1/go.mod h1:0PmMzfJP2Y9+lugD0wer4e7TihR5tM7NcIs3bQNk5xg= +github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980 h1:lIOOHPEbXzO3vnmx2gok1Tfs31Q8GQqKLc8vVqyQq/I= +github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= +github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/sylabs/sif/v2 v2.11.5 h1:7ssPH3epSonsTrzbS1YxeJ9KuqAN7ISlSM61a7j/mQM= +github.com/sylabs/sif/v2 v2.11.5/go.mod h1:GBoZs9LU3e4yJH1dcZ3Akf/jsqYgy5SeguJQC+zd75Y= +github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= +github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes= +github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= +github.com/theupdateframework/go-tuf v0.5.2 h1:habfDzTmpbzBLIFGWa2ZpVhYvFBoK0C1onC3a4zuPRA= +github.com/theupdateframework/go-tuf v0.5.2/go.mod h1:SyMV5kg5n4uEclsyxXJZI2UxPFJNDc4Y+r7wv+MlvTA= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= +github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= +github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= +github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck= +github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY= +github.com/vbauerster/mpb/v8 v8.4.0 h1:Jq2iNA7T6SydpMVOwaT+2OBWlXS9Th8KEvBqeu5eeTo= +github.com/vbauerster/mpb/v8 v8.4.0/go.mod h1:vjp3hSTuCtR+x98/+2vW3eZ8XzxvGoP8CPseHMhiPyc= +github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= +github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ= +github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= +github.com/ysmood/got v0.34.1 h1:IrV2uWLs45VXNvZqhJ6g2nIhY+pgIG1CUoOcqfXFl1s= +github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE= +github.com/ysmood/leakless v0.8.0 h1:BzLrVoiwxikpgEQR0Lk8NyBN5Cit2b1z+u0mgL4ZJak= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= +github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= +github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= +go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= +go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg= +go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= +go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= +go.mongodb.org/mongo-driver v1.11.3 h1:Ql6K6qYHEzB6xvu4+AU0BoRoqf9vFPcc4o7MUIdPW8Y= +go.mongodb.org/mongo-driver v1.11.3/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= +go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= +go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak= +go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s= +go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4= +go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo= +go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4= +go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE= +go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs= +go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= +golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= +golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= +golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc h1:XSJ8Vk1SWuNr8S18z1NZSziL0CPIXLCCMDOEFtHBOFc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230629202037-9506855d4529 h1:DEH99RbiLZhMxrpEJCZ0A+wdTe0EOgou/poSLx9vWf4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230629202037-9506855d4529/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= +google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.56.1 h1:z0dNfjIl0VpaZ9iSVjA6daGatAYwPGstTjt5vkRMFkQ= +google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/grpc v1.56.2 h1:fVRFRnXvU+x6C4IlHZewvJOVHoOv1TUuQyoRsYnB4bI= +google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/alexcesaro/statsd.v2 v2.0.0 h1:FXkZSCZIH17vLCO5sO2UucTHsH9pc+17F6pl3JVCwMc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/go-jose/go-jose.v2 v2.6.1 h1:qEzJlIDmG9q5VO0M/o8tGS65QMHMS1w01TQJB1VPJ4U= +gopkg.in/go-jose/go-jose.v2 v2.6.1/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= +gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= +gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo= +k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ= +k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8= +k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= +k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= +k8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc= +k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU= +k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM= +k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q= +k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y= +k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k= +k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0= +k8s.io/code-generator v0.19.7/go.mod h1:lwEq3YnLYb/7uVXLorOJfxg+cUu2oihFhHZ0n9NIla0= +k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk= +k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI= +k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM= +k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM= +k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= +k8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= +k8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc= +k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= +k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= +k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= +k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/vendor/github.com/containers/image/v5/hack/get_ci_vm.sh b/vendor/github.com/containers/image/v5/hack/get_ci_vm.sh new file mode 100755 index 00000000000..f046d28c12c --- /dev/null +++ b/vendor/github.com/containers/image/v5/hack/get_ci_vm.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash + +# +# For help and usage information, simply execute the script w/o any arguments. +# +# This script is intended to be run by Red Hat image developers who need +# to debug problems specifically related to Cirrus-CI automated testing. +# It requires that you have been granted prior access to create VMs in +# google-cloud. For non-Red Hat contributors, VMs are available as-needed, +# with supervision upon request. + +set -e + +SCRIPT_FILEPATH=$(realpath "${BASH_SOURCE[0]}") +SCRIPT_DIRPATH=$(dirname "$SCRIPT_FILEPATH") +REPO_DIRPATH=$(realpath "$SCRIPT_DIRPATH/../") + +# Help detect if we were called by get_ci_vm container +GET_CI_VM="${GET_CI_VM:-0}" +in_get_ci_vm() { + if ((GET_CI_VM==0)); then + echo "Error: $1 is not intended for use in this context" + exit 2 + fi +} + +# get_ci_vm APIv1 container entrypoint calls into this script +# to obtain required repo. specific configuration options. +if [[ "$1" == "--config" ]]; then + in_get_ci_vm "$1" + cat < /dev/stderr + ${GOSRC}/${SCRIPT_BASE}/runner.sh setup +else + # Create and access VM for specified Cirrus-CI task + mkdir -p $HOME/.config/gcloud/ssh + podman run -it --rm \ + --tz=local \ + -e NAME="$USER" \ + -e SRCDIR=/src \ + -e GCLOUD_ZONE="$GCLOUD_ZONE" \ + -e DEBUG="${DEBUG:-0}" \ + -v $REPO_DIRPATH:/src:O \ + -v $HOME/.config/gcloud:/root/.config/gcloud:z \ + -v $HOME/.config/gcloud/ssh:/root/.ssh:z \ + quay.io/libpod/get_ci_vm:latest "$@" +fi diff --git a/vendor/github.com/containers/image/v5/hack/validate.sh b/vendor/github.com/containers/image/v5/hack/validate.sh new file mode 100755 index 00000000000..078d34fff2c --- /dev/null +++ b/vendor/github.com/containers/image/v5/hack/validate.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +set -eo pipefail + +eval $(go env) +PATH="$GOPATH/bin:$PATH" + +die() { echo "Error: ${1:-No message provided}" > /dev/stderr; exit 1; } + +# Always run from the repository root +cd $(dirname "${BASH_SOURCE[0]}")/../ + +if [[ -z $(type -P gofmt) ]]; then + die "Unable to find 'gofmt' binary in \$PATH: $PATH" +fi + +echo "Executing go vet" +GO111MODULE="on" go vet -tags="$BUILDTAGS" ./... + +echo "Executing gofmt" +OUTPUT=$(gofmt -s -l . | sed -e '/^vendor/d') +if [[ ! -z "$OUTPUT" ]]; then + die "Please fix the formatting of the following files: +$OUTPUT" +fi diff --git a/vendor/github.com/containers/image/v5/internal/image/common_test.go b/vendor/github.com/containers/image/v5/internal/image/common_test.go new file mode 100644 index 00000000000..d903cfc4691 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/image/common_test.go @@ -0,0 +1,31 @@ +package image + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// assertJSONEqualsFixture tests that jsonBytes is structurally equal to fixture, +// possibly ignoring ignoreFields +func assertJSONEqualsFixture(t *testing.T, jsonBytes []byte, fixture string, ignoreFields ...string) { + var contents map[string]any + err := json.Unmarshal(jsonBytes, &contents) + require.NoError(t, err) + + fixtureBytes, err := os.ReadFile(filepath.Join("fixtures", fixture)) + require.NoError(t, err) + var fixtureContents map[string]any + + err = json.Unmarshal(fixtureBytes, &fixtureContents) + require.NoError(t, err) + for _, f := range ignoreFields { + delete(contents, f) + delete(fixtureContents, f) + } + assert.Equal(t, fixtureContents, contents) +} diff --git a/vendor/github.com/containers/image/v5/internal/image/docker_schema1_test.go b/vendor/github.com/containers/image/v5/internal/image/docker_schema1_test.go new file mode 100644 index 00000000000..e536d567bce --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/image/docker_schema1_test.go @@ -0,0 +1,664 @@ +package image + +import ( + "context" + "os" + "path/filepath" + "testing" + "time" + + "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/manifest" + "github.com/containers/image/v5/types" + digest "github.com/opencontainers/go-digest" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var schema1FixtureLayerInfos = []types.BlobInfo{ + { + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Size: 74876245, + Digest: "sha256:9cadd93b16ff2a0c51ac967ea2abfadfac50cfa3af8b5bf983d89b8f8647f3e4", + }, + { + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Size: 1239, + Digest: "sha256:4aa565ad8b7a87248163ce7dba1dd3894821aac97e846b932ff6b8ef9a8a508a", + }, + { + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Size: 78339724, + Digest: "sha256:f576d102e09b9eef0e305aaef705d2d43a11bebc3fd5810a761624bd5e11997e", + }, + { + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Size: 76857203, + Digest: "sha256:9e92df2aea7dc0baf5f1f8d509678d6a6306de27ad06513f8e218371938c07a6", + }, + { + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Size: 25923380, + Digest: "sha256:62e48e39dc5b30b75a97f05bccc66efbae6058b860ee20a5c9a184b9d5e25788", + }, + { + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Size: 23511300, + Digest: "sha256:e623934bca8d1a74f51014256445937714481e49343a31bda2bc5f534748184d", + }, +} + +var schema1FixtureLayerDiffIDs = []digest.Digest{ + "sha256:e1d829eddb62dc49f1c56dbf8acd0c71299b3996115399de853a9d66d81b822f", + "sha256:02404b4d7e5d89b1383ca346b4462b199128aa4b238c5a2b2c186004ac148ba8", + "sha256:45fad80a4b1cec165c421eb570dec312d825bd8fac362e255028fa3f2169148d", + "sha256:7ddef8efd44586e54880ec4797458eac87b368544c438d7e7c63fbc0d9a7ae97", + "sha256:b56b16b6407ba1b86252e7e50f98f142cf6844fab42e4495d56ebb7ce559e2af", + "sha256:9bd63850e406167b4751f5050f6dc0ebd789bb5ef5e5c6c31ed062bda8c063e8", +} + +var schema1WithThrowawaysFixtureLayerInfos = []types.BlobInfo{ + {Digest: "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb", Size: 51354364}, + {Digest: GzippedEmptyLayerDigest, Size: int64(len(GzippedEmptyLayer))}, + {Digest: GzippedEmptyLayerDigest, Size: int64(len(GzippedEmptyLayer))}, + {Digest: GzippedEmptyLayerDigest, Size: int64(len(GzippedEmptyLayer))}, + {Digest: "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c", Size: 150}, + {Digest: GzippedEmptyLayerDigest, Size: int64(len(GzippedEmptyLayer))}, + {Digest: "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9", Size: 11739507}, + {Digest: GzippedEmptyLayerDigest, Size: int64(len(GzippedEmptyLayer))}, + {Digest: GzippedEmptyLayerDigest, Size: int64(len(GzippedEmptyLayer))}, + {Digest: GzippedEmptyLayerDigest, Size: int64(len(GzippedEmptyLayer))}, + {Digest: GzippedEmptyLayerDigest, Size: int64(len(GzippedEmptyLayer))}, + {Digest: "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909", Size: 8841833}, + {Digest: "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa", Size: 291}, + {Digest: GzippedEmptyLayerDigest, Size: int64(len(GzippedEmptyLayer))}, + {Digest: GzippedEmptyLayerDigest, Size: int64(len(GzippedEmptyLayer))}, +} + +var schema1WithThrowawaysFixtureLayerDiffIDs = []digest.Digest{ + "sha256:142a601d97936307e75220c35dde0348971a9584c21e7cb42e1f7004005432ab", + GzippedEmptyLayerDigest, + GzippedEmptyLayerDigest, + GzippedEmptyLayerDigest, + "sha256:90fcc66ad3be9f1757f954b750deb37032f208428aa12599fcb02182b9065a9c", + GzippedEmptyLayerDigest, + "sha256:5a8624bb7e76d1e6829f9c64c43185e02bc07f97a2189eb048609a8914e72c56", + GzippedEmptyLayerDigest, + GzippedEmptyLayerDigest, + GzippedEmptyLayerDigest, + GzippedEmptyLayerDigest, + "sha256:d349ff6b3afc6a2800054768c82bfbf4289c9aa5da55c1290f802943dcd4d1e9", + "sha256:8c064bb1f60e84fa8cc6079b6d2e76e0423389fd6aeb7e497dfdae5e05b2b25b", + GzippedEmptyLayerDigest, + GzippedEmptyLayerDigest, +} + +func manifestSchema1FromFixture(t *testing.T, fixture string) genericManifest { + manifest, err := os.ReadFile(filepath.Join("fixtures", fixture)) + require.NoError(t, err) + + m, err := manifestSchema1FromManifest(manifest) + require.NoError(t, err) + return m +} + +func manifestSchema1FromComponentsLikeFixture(t *testing.T) genericManifest { + ref, err := reference.ParseNormalizedNamed("rhosp12/openstack-nova-api:latest") + require.NoError(t, err) + m, err := manifestSchema1FromComponents(ref, []manifest.Schema1FSLayers{ + {BlobSum: "sha256:e623934bca8d1a74f51014256445937714481e49343a31bda2bc5f534748184d"}, + {BlobSum: "sha256:62e48e39dc5b30b75a97f05bccc66efbae6058b860ee20a5c9a184b9d5e25788"}, + {BlobSum: "sha256:9e92df2aea7dc0baf5f1f8d509678d6a6306de27ad06513f8e218371938c07a6"}, + {BlobSum: "sha256:f576d102e09b9eef0e305aaef705d2d43a11bebc3fd5810a761624bd5e11997e"}, + {BlobSum: "sha256:4aa565ad8b7a87248163ce7dba1dd3894821aac97e846b932ff6b8ef9a8a508a"}, + {BlobSum: "sha256:9cadd93b16ff2a0c51ac967ea2abfadfac50cfa3af8b5bf983d89b8f8647f3e4"}, + }, []manifest.Schema1History{ + {V1Compatibility: "{\"architecture\":\"amd64\",\"config\":{\"Hostname\":\"9428cdea83ba\",\"Domainname\":\"\",\"User\":\"nova\",\"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\",\"container=oci\",\"KOLLA_BASE_DISTRO=rhel\",\"KOLLA_INSTALL_TYPE=binary\",\"KOLLA_INSTALL_METATYPE=rhos\",\"PS1=$(tput bold)($(printenv KOLLA_SERVICE_NAME))$(tput sgr0)[$(id -un)@$(hostname -s) $(pwd)]$ \"],\"Cmd\":[\"kolla_start\"],\"Healthcheck\":{\"Test\":[\"CMD-SHELL\",\"/openstack/healthcheck\"]},\"ArgsEscaped\":true,\"Image\":\"3bf9afe371220b1eb1c57bec39b5a99ba976c36c92d964a1c014584f95f51e33\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":[],\"Labels\":{\"Kolla-SHA\":\"5.0.0-39-g6f1b947b\",\"architecture\":\"x86_64\",\"authoritative-source-url\":\"registry.access.redhat.com\",\"build-date\":\"2018-01-25T00:32:27.807261\",\"com.redhat.build-host\":\"ip-10-29-120-186.ec2.internal\",\"com.redhat.component\":\"openstack-nova-api-docker\",\"description\":\"Red Hat OpenStack Platform 12.0 nova-api\",\"distribution-scope\":\"public\",\"io.k8s.description\":\"Red Hat OpenStack Platform 12.0 nova-api\",\"io.k8s.display-name\":\"Red Hat OpenStack Platform 12.0 nova-api\",\"io.openshift.tags\":\"rhosp osp openstack osp-12.0\",\"kolla_version\":\"stable/pike\",\"name\":\"rhosp12/openstack-nova-api\",\"release\":\"20180124.1\",\"summary\":\"Red Hat OpenStack Platform 12.0 nova-api\",\"tripleo-common_version\":\"7.6.3-23-g4891cfe\",\"url\":\"https://access.redhat.com/containers/#/registry.access.redhat.com/rhosp12/openstack-nova-api/images/12.0-20180124.1\",\"vcs-ref\":\"9b31243b7b448eb2fc3b6e2c96935b948f806e98\",\"vcs-type\":\"git\",\"vendor\":\"Red Hat, Inc.\",\"version\":\"12.0\",\"version-release\":\"12.0-20180124.1\"}},\"container_config\":{\"Hostname\":\"9428cdea83ba\",\"Domainname\":\"\",\"User\":\"nova\",\"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\",\"container=oci\",\"KOLLA_BASE_DISTRO=rhel\",\"KOLLA_INSTALL_TYPE=binary\",\"KOLLA_INSTALL_METATYPE=rhos\",\"PS1=$(tput bold)($(printenv KOLLA_SERVICE_NAME))$(tput sgr0)[$(id -un)@$(hostname -s) $(pwd)]$ \"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) \",\"USER [nova]\"],\"Healthcheck\":{\"Test\":[\"CMD-SHELL\",\"/openstack/healthcheck\"]},\"ArgsEscaped\":true,\"Image\":\"sha256:274ce4dcbeb09fa173a5d50203ae5cec28f456d1b8b59477b47a42bd74d068bf\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":[],\"Labels\":{\"Kolla-SHA\":\"5.0.0-39-g6f1b947b\",\"architecture\":\"x86_64\",\"authoritative-source-url\":\"registry.access.redhat.com\",\"build-date\":\"2018-01-25T00:32:27.807261\",\"com.redhat.build-host\":\"ip-10-29-120-186.ec2.internal\",\"com.redhat.component\":\"openstack-nova-api-docker\",\"description\":\"Red Hat OpenStack Platform 12.0 nova-api\",\"distribution-scope\":\"public\",\"io.k8s.description\":\"Red Hat OpenStack Platform 12.0 nova-api\",\"io.k8s.display-name\":\"Red Hat OpenStack Platform 12.0 nova-api\",\"io.openshift.tags\":\"rhosp osp openstack osp-12.0\",\"kolla_version\":\"stable/pike\",\"name\":\"rhosp12/openstack-nova-api\",\"release\":\"20180124.1\",\"summary\":\"Red Hat OpenStack Platform 12.0 nova-api\",\"tripleo-common_version\":\"7.6.3-23-g4891cfe\",\"url\":\"https://access.redhat.com/containers/#/registry.access.redhat.com/rhosp12/openstack-nova-api/images/12.0-20180124.1\",\"vcs-ref\":\"9b31243b7b448eb2fc3b6e2c96935b948f806e98\",\"vcs-type\":\"git\",\"vendor\":\"Red Hat, Inc.\",\"version\":\"12.0\",\"version-release\":\"12.0-20180124.1\"}},\"created\":\"2018-01-25T00:37:48.268558Z\",\"docker_version\":\"1.12.6\",\"id\":\"486cbbaf6c6f7d890f9368c86eda3f4ebe3ae982b75098037eb3c3cc6f0e0cdf\",\"os\":\"linux\",\"parent\":\"20d0c9c79f9fee83c4094993335b9b321112f13eef60ed9ec1599c7593dccf20\"}"}, + {V1Compatibility: "{\"id\":\"20d0c9c79f9fee83c4094993335b9b321112f13eef60ed9ec1599c7593dccf20\",\"parent\":\"47a1014db2116c312736e11adcc236fb77d0ad32457f959cbaec0c3fc9ab1caa\",\"created\":\"2018-01-24T23:08:25.300741Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c rm -f '/etc/yum.repos.d/rhel-7.4.repo' '/etc/yum.repos.d/rhos-optools-12.0.repo' '/etc/yum.repos.d/rhos-12.0-container-yum-need_images.repo'\"]}}"}, + {V1Compatibility: "{\"id\":\"47a1014db2116c312736e11adcc236fb77d0ad32457f959cbaec0c3fc9ab1caa\",\"parent\":\"cec66cab6c92a5f7b50ef407b80b83840a0d089b9896257609fd01de3a595824\",\"created\":\"2018-01-24T22:00:57.807862Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c rm -f '/etc/yum.repos.d/rhel-7.4.repo' '/etc/yum.repos.d/rhos-optools-12.0.repo' '/etc/yum.repos.d/rhos-12.0-container-yum-need_images.repo'\"]}}"}, + {V1Compatibility: "{\"id\":\"cec66cab6c92a5f7b50ef407b80b83840a0d089b9896257609fd01de3a595824\",\"parent\":\"0e7730eccb3d014b33147b745d771bc0e38a967fd932133a6f5325a3c84282e2\",\"created\":\"2018-01-24T21:40:32.494686Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c rm -f '/etc/yum.repos.d/rhel-7.4.repo' '/etc/yum.repos.d/rhos-optools-12.0.repo' '/etc/yum.repos.d/rhos-12.0-container-yum-need_images.repo'\"]}}"}, + {V1Compatibility: "{\"id\":\"0e7730eccb3d014b33147b745d771bc0e38a967fd932133a6f5325a3c84282e2\",\"parent\":\"3e49094c0233214ab73f8e5c204af8a14cfc6f0403384553c17fbac2e9d38345\",\"created\":\"2017-11-21T16:49:37.292899Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c rm -f '/etc/yum.repos.d/compose-rpms-1.repo'\"]},\"author\":\"Red Hat, Inc.\"}"}, + {V1Compatibility: "{\"id\":\"3e49094c0233214ab73f8e5c204af8a14cfc6f0403384553c17fbac2e9d38345\",\"comment\":\"Imported from -\",\"created\":\"2017-11-21T16:47:27.755341705Z\",\"container_config\":{\"Cmd\":[\"\"]}}"}, + }, "amd64") + require.NoError(t, err) + return m +} + +func TestManifestSchema1FromManifest(t *testing.T) { + // This just tests that the JSON can be loaded; we test that the parsed + // values are correctly returned in tests for the individual getter methods. + _ = manifestSchema1FromFixture(t, "schema1.json") + + // FIXME: Detailed coverage of manifest.Schema1FromManifest failures + _, err := manifestSchema1FromManifest([]byte{}) + assert.Error(t, err) +} + +func TestManifestSchema1FromComponents(t *testing.T) { + // This just smoke-tests that the manifest can be created; we test that the parsed + // values are correctly returned in tests for the individual getter methods. + _ = manifestSchema1FromComponentsLikeFixture(t) + + // Error on invalid input + _, err := manifestSchema1FromComponents(nil, []manifest.Schema1FSLayers{}, []manifest.Schema1History{}, "amd64") + assert.Error(t, err) +} + +func TestManifestSchema1Serialize(t *testing.T) { + for _, m := range []genericManifest{ + manifestSchema1FromFixture(t, "schema1.json"), + manifestSchema1FromComponentsLikeFixture(t), + } { + serialized, err := m.serialize() + require.NoError(t, err) + // Drop "signatures" which is generated by AddDummyV2S1Signature + // We would ideally like to compare “serialized” with some transformation of + // the original fixture, but the ordering of fields in JSON maps is undefined, so this is + // easier. + assertJSONEqualsFixture(t, serialized, "schema1.json", "signatures") + } +} + +func TestManifestSchema1ManifestMIMEType(t *testing.T) { + for _, m := range []genericManifest{ + manifestSchema1FromFixture(t, "schema1.json"), + manifestSchema1FromComponentsLikeFixture(t), + } { + assert.Equal(t, manifest.DockerV2Schema1SignedMediaType, m.manifestMIMEType()) + } +} + +func TestManifestSchema1ConfigInfo(t *testing.T) { + for _, m := range []genericManifest{ + manifestSchema1FromFixture(t, "schema1.json"), + manifestSchema1FromComponentsLikeFixture(t), + } { + assert.Equal(t, types.BlobInfo{Digest: ""}, m.ConfigInfo()) + } +} + +func TestManifestSchema1ConfigBlob(t *testing.T) { + for _, m := range []genericManifest{ + manifestSchema1FromFixture(t, "schema1.json"), + manifestSchema1FromComponentsLikeFixture(t), + } { + blob, err := m.ConfigBlob(context.Background()) + require.NoError(t, err) + assert.Nil(t, blob) + } +} + +func TestManifestSchema1OCIConfig(t *testing.T) { + m := manifestSchema1FromFixture(t, "schema1-for-oci-config.json") + configOCI, err := m.OCIConfig(context.Background()) + require.NoError(t, err) + // FIXME: A more comprehensive test? + assert.Equal(t, "/pause", configOCI.Config.Entrypoint[0]) +} + +func TestManifestSchema1LayerInfo(t *testing.T) { + for _, m := range []genericManifest{ + manifestSchema1FromFixture(t, "schema1.json"), + manifestSchema1FromComponentsLikeFixture(t), + } { + assert.Equal(t, []types.BlobInfo{ + { + Digest: "sha256:9cadd93b16ff2a0c51ac967ea2abfadfac50cfa3af8b5bf983d89b8f8647f3e4", + Size: -1, + }, + { + Digest: "sha256:4aa565ad8b7a87248163ce7dba1dd3894821aac97e846b932ff6b8ef9a8a508a", + Size: -1, + }, + { + Digest: "sha256:f576d102e09b9eef0e305aaef705d2d43a11bebc3fd5810a761624bd5e11997e", + Size: -1, + }, + { + Digest: "sha256:9e92df2aea7dc0baf5f1f8d509678d6a6306de27ad06513f8e218371938c07a6", + Size: -1, + }, + { + Digest: "sha256:62e48e39dc5b30b75a97f05bccc66efbae6058b860ee20a5c9a184b9d5e25788", + Size: -1, + }, + { + Digest: "sha256:e623934bca8d1a74f51014256445937714481e49343a31bda2bc5f534748184d", + Size: -1, + }, + }, m.LayerInfos()) + } +} + +func TestManifestSchema1EmbeddedDockerReferenceConflicts(t *testing.T) { + for _, m := range []genericManifest{ + manifestSchema1FromFixture(t, "schema1.json"), + manifestSchema1FromComponentsLikeFixture(t), + } { + for name, expected := range map[string]bool{ + "rhosp12/openstack-nova-api:latest": false, // Exactly the embedded reference + "example.com/rhosp12/openstack-nova-api:latest": false, // A different host name, but path and tag match + "docker.io:3333/rhosp12/openstack-nova-api:latest": false, // A different port, but path and tag match + "busybox": true, // Entirely different, minimal + "example.com:5555/ns/repo:tag": true, // Entirely different, maximal + "rhosp12/openstack-nova-api": true, // Missing tag + "rhosp12/openstack-nova-api:notlatest": true, // Different tag + "notrhosp12/openstack-nova-api:latest": true, // Different namespace + "rhosp12/notopenstack-nova-api:latest": true, // Different repo + } { + ref, err := reference.ParseNormalizedNamed(name) + require.NoError(t, err, name) + conflicts := m.EmbeddedDockerReferenceConflicts(ref) + assert.Equal(t, expected, conflicts, name) + } + } +} + +func TestManifestSchema1Inspect(t *testing.T) { + for _, m := range []genericManifest{ + manifestSchema1FromFixture(t, "schema1.json"), + manifestSchema1FromComponentsLikeFixture(t), + } { + ii, err := m.Inspect(context.Background()) + require.NoError(t, err) + created := time.Date(2018, 1, 25, 0, 37, 48, 268558000, time.UTC) + var emptyAnnotations map[string]string + assert.Equal(t, types.ImageInspectInfo{ + Tag: "latest", + Created: &created, + DockerVersion: "1.12.6", + Labels: map[string]string{ + "Kolla-SHA": "5.0.0-39-g6f1b947b", + "architecture": "x86_64", + "authoritative-source-url": "registry.access.redhat.com", + "build-date": "2018-01-25T00:32:27.807261", + "com.redhat.build-host": "ip-10-29-120-186.ec2.internal", + "com.redhat.component": "openstack-nova-api-docker", + "description": "Red Hat OpenStack Platform 12.0 nova-api", + "distribution-scope": "public", + "io.k8s.description": "Red Hat OpenStack Platform 12.0 nova-api", + "io.k8s.display-name": "Red Hat OpenStack Platform 12.0 nova-api", + "io.openshift.tags": "rhosp osp openstack osp-12.0", + "kolla_version": "stable/pike", + "name": "rhosp12/openstack-nova-api", + "release": "20180124.1", + "summary": "Red Hat OpenStack Platform 12.0 nova-api", + "tripleo-common_version": "7.6.3-23-g4891cfe", + "url": "https://access.redhat.com/containers/#/registry.access.redhat.com/rhosp12/openstack-nova-api/images/12.0-20180124.1", + "vcs-ref": "9b31243b7b448eb2fc3b6e2c96935b948f806e98", + "vcs-type": "git", + "vendor": "Red Hat, Inc.", + "version": "12.0", + "version-release": "12.0-20180124.1", + }, + Architecture: "amd64", + Os: "linux", + Layers: []string{ + "sha256:9cadd93b16ff2a0c51ac967ea2abfadfac50cfa3af8b5bf983d89b8f8647f3e4", + "sha256:4aa565ad8b7a87248163ce7dba1dd3894821aac97e846b932ff6b8ef9a8a508a", + "sha256:f576d102e09b9eef0e305aaef705d2d43a11bebc3fd5810a761624bd5e11997e", + "sha256:9e92df2aea7dc0baf5f1f8d509678d6a6306de27ad06513f8e218371938c07a6", + "sha256:62e48e39dc5b30b75a97f05bccc66efbae6058b860ee20a5c9a184b9d5e25788", + "sha256:e623934bca8d1a74f51014256445937714481e49343a31bda2bc5f534748184d", + }, + LayersData: []types.ImageInspectLayer{{ + MIMEType: "", + Digest: "sha256:9cadd93b16ff2a0c51ac967ea2abfadfac50cfa3af8b5bf983d89b8f8647f3e4", + Size: -1, + Annotations: emptyAnnotations, + }, { + MIMEType: "", + Digest: "sha256:4aa565ad8b7a87248163ce7dba1dd3894821aac97e846b932ff6b8ef9a8a508a", + Size: -1, + Annotations: emptyAnnotations, + }, { + MIMEType: "", + Digest: "sha256:f576d102e09b9eef0e305aaef705d2d43a11bebc3fd5810a761624bd5e11997e", + Size: -1, + Annotations: emptyAnnotations, + }, { + MIMEType: "", + Digest: "sha256:9e92df2aea7dc0baf5f1f8d509678d6a6306de27ad06513f8e218371938c07a6", + Size: -1, + Annotations: emptyAnnotations, + }, { + MIMEType: "", + Digest: "sha256:62e48e39dc5b30b75a97f05bccc66efbae6058b860ee20a5c9a184b9d5e25788", + Size: -1, + Annotations: emptyAnnotations, + }, + { + MIMEType: "", + Digest: "sha256:e623934bca8d1a74f51014256445937714481e49343a31bda2bc5f534748184d", + Size: -1, + Annotations: emptyAnnotations, + }, + }, + Author: "", + Env: []string{ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "container=oci", + "KOLLA_BASE_DISTRO=rhel", + "KOLLA_INSTALL_TYPE=binary", + "KOLLA_INSTALL_METATYPE=rhos", + "PS1=$(tput bold)($(printenv KOLLA_SERVICE_NAME))$(tput sgr0)[$(id -un)@$(hostname -s) $(pwd)]$ ", + }, + }, *ii) + } +} + +func TestManifestSchema1UpdatedImageNeedsLayerDiffIDs(t *testing.T) { + for _, m := range []genericManifest{ + manifestSchema1FromFixture(t, "schema1.json"), + manifestSchema1FromComponentsLikeFixture(t), + } { + for mt, expected := range map[string]bool{ + "": false, + manifest.DockerV2Schema1MediaType: false, + manifest.DockerV2Schema1SignedMediaType: false, + manifest.DockerV2Schema2MediaType: true, + imgspecv1.MediaTypeImageManifest: true, + } { + needsDiffIDs := m.UpdatedImageNeedsLayerDiffIDs(types.ManifestUpdateOptions{ + ManifestMIMEType: mt, + }) + assert.Equal(t, expected, needsDiffIDs, mt) + } + } +} + +func TestManifestSchema1UpdatedImage(t *testing.T) { + original := manifestSchema1FromFixture(t, "schema1.json") + + // LayerInfos: + layerInfos := append(original.LayerInfos()[1:], original.LayerInfos()[0]) + res, err := original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + LayerInfos: layerInfos, + }) + require.NoError(t, err) + assert.Equal(t, layerInfos, res.LayerInfos()) + _, err = original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + LayerInfos: append(layerInfos, layerInfos[0]), + }) + assert.Error(t, err) + + // EmbeddedDockerReference: + for _, refName := range []string{ + "busybox", + "busybox:notlatest", + "rhosp12/openstack-nova-api:latest", + } { + embeddedRef, err := reference.ParseNormalizedNamed(refName) + require.NoError(t, err) + res, err = original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + EmbeddedDockerReference: embeddedRef, + }) + require.NoError(t, err) + // The previous embedded docker reference now does not match. + nonEmbeddedRef, err := reference.ParseNormalizedNamed("rhosp12/openstack-nova-api:latest") + require.NoError(t, err) + conflicts := res.EmbeddedDockerReferenceConflicts(nonEmbeddedRef) + assert.Equal(t, refName != "rhosp12/openstack-nova-api:latest", conflicts) + } + + // ManifestMIMEType: + // Only smoke-test the valid conversions, detailed tests are below. (This also verifies that “original” is not affected.) + for _, mime := range []string{ + manifest.DockerV2Schema2MediaType, + imgspecv1.MediaTypeImageManifest, + } { + _, err = original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + ManifestMIMEType: mime, + InformationOnly: types.ManifestUpdateInformation{ + LayerInfos: schema1FixtureLayerInfos, + LayerDiffIDs: schema1FixtureLayerDiffIDs, + }, + }) + assert.NoError(t, err, mime) + } + for _, mime := range []string{ + "this is invalid", + } { + _, err = original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + ManifestMIMEType: mime, + }) + assert.Error(t, err, mime) + } + + // m hasn’t been changed: + m2 := manifestSchema1FromFixture(t, "schema1.json") + typedOriginal, ok := original.(*manifestSchema1) + require.True(t, ok) + typedM2, ok := m2.(*manifestSchema1) + require.True(t, ok) + assert.Equal(t, *typedM2, *typedOriginal) +} + +func TestManifestSchema1ConvertToSchema2(t *testing.T) { + original := manifestSchema1FromFixture(t, "schema1.json") + res, err := original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + ManifestMIMEType: manifest.DockerV2Schema2MediaType, + InformationOnly: types.ManifestUpdateInformation{ + LayerInfos: schema1FixtureLayerInfos, + LayerDiffIDs: schema1FixtureLayerDiffIDs, + }, + }) + require.NoError(t, err) + + convertedJSON, mt, err := res.Manifest(context.Background()) + require.NoError(t, err) + assert.Equal(t, manifest.DockerV2Schema2MediaType, mt) + // Ignore "config": we don’t want to hard-code a specific digest and size of the marshaled config here. + assertJSONEqualsFixture(t, convertedJSON, "schema1-to-schema2.json", "config") + + convertedConfig, err := res.ConfigBlob(context.Background()) + require.NoError(t, err) + assertJSONEqualsFixture(t, convertedConfig, "schema1-to-schema2-config.json") + + // Conversion to schema2 together with changing LayerInfos works as expected (which requires + // handling schema1 throwaway layers): + // Use the recorded result of converting the schema2 fixture to schema1, because that one + // (unlike schem1.json) contains throwaway layers. + original = manifestSchema1FromFixture(t, "schema2-to-schema1-by-docker.json") + updatedLayers, updatedLayersCopy := modifiedLayerInfos(t, schema1WithThrowawaysFixtureLayerInfos) + res, err = original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + LayerInfos: updatedLayers, + ManifestMIMEType: manifest.DockerV2Schema2MediaType, + InformationOnly: types.ManifestUpdateInformation{ + LayerInfos: updatedLayers, + LayerDiffIDs: schema1WithThrowawaysFixtureLayerDiffIDs, + }, + }) + require.NoError(t, err) + assert.Equal(t, updatedLayersCopy, updatedLayers) // updatedLayers have not been modified in place + convertedJSON, mt, err = res.Manifest(context.Background()) + require.NoError(t, err) + assert.Equal(t, manifest.DockerV2Schema2MediaType, mt) + // Layers have been updated as expected + originalSrc := newSchema2ImageSource(t, "httpd:latest") + s2Manifest, err := manifestSchema2FromManifest(originalSrc, convertedJSON) + require.NoError(t, err) + assert.Equal(t, []types.BlobInfo{ + { + Digest: "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5ba", + Size: 51354365, + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + }, + { + Digest: "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680d", + Size: 151, + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + }, + { + Digest: "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a8", + Size: 11739506, + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + }, + { + Digest: "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25908", + Size: 8841832, + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + }, + { + Digest: "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fb", + Size: 290, + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + }, + }, s2Manifest.LayerInfos()) + + // FIXME? Test also the various failure cases, if only to see that we don't crash? +} + +func TestManifestSchema1ConvertToManifestOCI1(t *testing.T) { + original := manifestSchema1FromFixture(t, "schema1.json") + res, err := original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + ManifestMIMEType: imgspecv1.MediaTypeImageManifest, + InformationOnly: types.ManifestUpdateInformation{ + LayerInfos: schema1FixtureLayerInfos, + LayerDiffIDs: schema1FixtureLayerDiffIDs, + }, + }) + require.NoError(t, err) + + convertedJSON, mt, err := res.Manifest(context.Background()) + require.NoError(t, err) + assert.Equal(t, imgspecv1.MediaTypeImageManifest, mt) + // Ignore "config": we don’t want to hard-code a specific digest and size of the marshaled config here. + assertJSONEqualsFixture(t, convertedJSON, "schema1-to-oci1.json", "config") + + convertedConfig, err := res.ConfigBlob(context.Background()) + require.NoError(t, err) + assertJSONEqualsFixture(t, convertedConfig, "schema1-to-oci1-config.json") + + // Conversion to OCI together with changing LayerInfos works as expected (which requires + // handling schema1 throwaway layers): + // Use the recorded result of converting the schema2 fixture to schema1, because that one + // (unlike schem1.json) contains throwaway layers. + original = manifestSchema1FromFixture(t, "schema2-to-schema1-by-docker.json") + updatedLayers, updatedLayersCopy := modifiedLayerInfos(t, schema1WithThrowawaysFixtureLayerInfos) + res, err = original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + LayerInfos: updatedLayers, + ManifestMIMEType: imgspecv1.MediaTypeImageManifest, + InformationOnly: types.ManifestUpdateInformation{ // FIXME: deduplicate this data + LayerInfos: updatedLayers, + LayerDiffIDs: schema1WithThrowawaysFixtureLayerDiffIDs, + }, + }) + require.NoError(t, err) + assert.Equal(t, updatedLayersCopy, updatedLayers) // updatedLayers have not been modified in place + convertedJSON, mt, err = res.Manifest(context.Background()) + require.NoError(t, err) + assert.Equal(t, imgspecv1.MediaTypeImageManifest, mt) + // Layers have been updated as expected + originalSrc := newSchema2ImageSource(t, "httpd:latest") + ociManifest, err := manifestOCI1FromManifest(originalSrc, convertedJSON) + require.NoError(t, err) + assert.Equal(t, []types.BlobInfo{ + { + Digest: "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5ba", + Size: 51354365, + MediaType: "application/vnd.oci.image.layer.v1.tar+gzip", + }, + { + Digest: "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680d", + Size: 151, + MediaType: "application/vnd.oci.image.layer.v1.tar+gzip", + }, + { + Digest: "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a8", + Size: 11739506, + MediaType: "application/vnd.oci.image.layer.v1.tar+gzip", + }, + { + Digest: "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25908", + Size: 8841832, + MediaType: "application/vnd.oci.image.layer.v1.tar+gzip", + }, + { + Digest: "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fb", + Size: 290, + MediaType: "application/vnd.oci.image.layer.v1.tar+gzip", + }, + }, ociManifest.LayerInfos()) + + // FIXME? Test also the various failure cases, if only to see that we don't crash? +} + +func TestConvertSchema1ToManifestOCIWithAnnotations(t *testing.T) { + // Test when converting an image from schema 1 (which doesn't support certain fields like + // URLs, annotations, etc.) to an OCI image (which supports those fields), + // that UpdatedImage propagates the features to the converted manifest. + + original := manifestSchema1FromFixture(t, "schema1.json") + layerInfoOverwrites := []types.BlobInfo{ + { + Digest: "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb", + Size: 51354364, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + }, + { + Digest: "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c", + Size: 150, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + }, + { + Digest: "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9", + Size: 11739507, + URLs: []string{ + "https://layer.url", + }, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + }, + { + Digest: "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909", + Size: 8841833, + Annotations: map[string]string{ + "test-annotation-2": "two", + }, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + }, + { + Digest: "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa", + Size: 291, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + }, + { + Digest: "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa", + Size: 291, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + }, + } + res, err := original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + ManifestMIMEType: imgspecv1.MediaTypeImageManifest, + LayerInfos: layerInfoOverwrites, + InformationOnly: types.ManifestUpdateInformation{ + LayerInfos: schema1FixtureLayerInfos, + LayerDiffIDs: schema1FixtureLayerDiffIDs, + }, + }) + require.NoError(t, err) + assert.Equal(t, res.LayerInfos(), layerInfoOverwrites) + + // Doing this with schema2 should fail + original = manifestSchema1FromFixture(t, "schema1.json") + res, err = original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + ManifestMIMEType: manifest.DockerV2Schema2MediaType, + LayerInfos: layerInfoOverwrites, + InformationOnly: types.ManifestUpdateInformation{ + LayerInfos: schema1FixtureLayerInfos, + LayerDiffIDs: schema1FixtureLayerDiffIDs, + }, + }) + require.NoError(t, err) + assert.NotEqual(t, res.LayerInfos(), layerInfoOverwrites) +} + +func TestManifestSchema1CanChangeLayerCompression(t *testing.T) { + for _, m := range []genericManifest{ + manifestSchema1FromFixture(t, "schema1.json"), + manifestSchema1FromComponentsLikeFixture(t), + } { + assert.True(t, m.CanChangeLayerCompression("")) + } +} diff --git a/vendor/github.com/containers/image/v5/internal/image/docker_schema2_test.go b/vendor/github.com/containers/image/v5/internal/image/docker_schema2_test.go new file mode 100644 index 00000000000..f2f8a3f1d0f --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/image/docker_schema2_test.go @@ -0,0 +1,669 @@ +package image + +import ( + "bytes" + "context" + "encoding/hex" + "errors" + "io" + "os" + "path/filepath" + "testing" + "time" + + "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/internal/testing/mocks" + "github.com/containers/image/v5/manifest" + "github.com/containers/image/v5/types" + "github.com/opencontainers/go-digest" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/exp/slices" +) + +func manifestSchema2FromFixture(t *testing.T, src types.ImageSource, fixture string, mustFail bool) genericManifest { + manifest, err := os.ReadFile(filepath.Join("fixtures", fixture)) + require.NoError(t, err) + + m, err := manifestSchema2FromManifest(src, manifest) + if mustFail { + require.Error(t, err) + } else { + require.NoError(t, err) + } + return m +} + +func manifestSchema2FromComponentsLikeFixture(configBlob []byte) genericManifest { + return manifestSchema2FromComponents(manifest.Schema2Descriptor{ + MediaType: "application/octet-stream", + Size: 5940, + Digest: "sha256:9ca4bda0a6b3727a6ffcc43e981cad0f24e2ec79d338f6ba325b4dfd0756fb8f", + }, nil, configBlob, []manifest.Schema2Descriptor{ + { + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Digest: "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb", + Size: 51354364, + }, + { + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Digest: "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c", + Size: 150, + }, + { + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Digest: "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9", + Size: 11739507, + }, + { + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Digest: "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909", + Size: 8841833, + }, + { + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Digest: "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa", + Size: 291, + }, + }) +} + +func TestManifestSchema2FromManifest(t *testing.T) { + // This just tests that the JSON can be loaded; we test that the parsed + // values are correctly returned in tests for the individual getter methods. + _ = manifestSchema2FromFixture(t, mocks.ForbiddenImageSource{}, "schema2.json", false) + + _, err := manifestSchema2FromManifest(nil, []byte{}) + assert.Error(t, err) +} + +func TestManifestSchema2FromComponents(t *testing.T) { + // This just smoke-tests that the manifest can be created; we test that the parsed + // values are correctly returned in tests for the individual getter methods. + _ = manifestSchema2FromComponentsLikeFixture(nil) +} + +func TestManifestSchema2Serialize(t *testing.T) { + for _, m := range []genericManifest{ + manifestSchema2FromFixture(t, mocks.ForbiddenImageSource{}, "schema2.json", false), + manifestSchema2FromComponentsLikeFixture(nil), + } { + serialized, err := m.serialize() + require.NoError(t, err) + // We would ideally like to compare “serialized” with some transformation of + // the original fixture, but the ordering of fields in JSON maps is undefined, so this is + // easier. + assertJSONEqualsFixture(t, serialized, "schema2.json") + } +} + +func TestManifestSchema2ManifestMIMEType(t *testing.T) { + for _, m := range []genericManifest{ + manifestSchema2FromFixture(t, mocks.ForbiddenImageSource{}, "schema2.json", false), + manifestSchema2FromComponentsLikeFixture(nil), + } { + assert.Equal(t, manifest.DockerV2Schema2MediaType, m.manifestMIMEType()) + } +} + +func TestManifestSchema2ConfigInfo(t *testing.T) { + for _, m := range []genericManifest{ + manifestSchema2FromFixture(t, mocks.ForbiddenImageSource{}, "schema2.json", false), + manifestSchema2FromComponentsLikeFixture(nil), + } { + assert.Equal(t, types.BlobInfo{ + Size: 5940, + Digest: "sha256:9ca4bda0a6b3727a6ffcc43e981cad0f24e2ec79d338f6ba325b4dfd0756fb8f", + MediaType: "application/octet-stream", + }, m.ConfigInfo()) + } +} + +// configBlobImageSource allows testing various GetBlob behaviors in .ConfigBlob() +type configBlobImageSource struct { + mocks.ForbiddenImageSource // We inherit almost all of the methods, which just panic() + f func() (io.ReadCloser, int64, error) +} + +func (f configBlobImageSource) GetBlob(ctx context.Context, info types.BlobInfo, _ types.BlobInfoCache) (io.ReadCloser, int64, error) { + if info.Digest.String() != "sha256:9ca4bda0a6b3727a6ffcc43e981cad0f24e2ec79d338f6ba325b4dfd0756fb8f" { + panic("Unexpected digest in GetBlob") + } + return f.f() +} + +func TestManifestSchema2ConfigBlob(t *testing.T) { + realConfigJSON, err := os.ReadFile("fixtures/schema2-config.json") + require.NoError(t, err) + + for _, c := range []struct { + cbISfn func() (io.ReadCloser, int64, error) + blob []byte + }{ + // Success + {func() (io.ReadCloser, int64, error) { + return io.NopCloser(bytes.NewReader(realConfigJSON)), int64(len(realConfigJSON)), nil + }, realConfigJSON}, + // Various kinds of failures + {nil, nil}, + {func() (io.ReadCloser, int64, error) { + return nil, -1, errors.New("Error returned from GetBlob") + }, nil}, + {func() (io.ReadCloser, int64, error) { + reader, writer := io.Pipe() + err = writer.CloseWithError(errors.New("Expected error reading input in ConfigBlob")) + assert.NoError(t, err) + return reader, 1, nil + }, nil}, + {func() (io.ReadCloser, int64, error) { + nonmatchingJSON := []byte("This does not match ConfigDescriptor.Digest") + return io.NopCloser(bytes.NewReader(nonmatchingJSON)), int64(len(nonmatchingJSON)), nil + }, nil}, + } { + var src types.ImageSource + if c.cbISfn != nil { + src = configBlobImageSource{f: c.cbISfn} + } else { + src = nil + } + m := manifestSchema2FromFixture(t, src, "schema2.json", false) + blob, err := m.ConfigBlob(context.Background()) + if c.blob != nil { + assert.NoError(t, err) + assert.Equal(t, c.blob, blob) + } else { + assert.Error(t, err) + } + } + + // Generally configBlob should match ConfigInfo; we don’t quite need it to, and this will + // guarantee that the returned object is returning the original contents instead + // of reading an object from elsewhere. + configBlob := []byte("config blob which does not match ConfigInfo") + // This just tests that the manifest can be created; we test that the parsed + // values are correctly returned in tests for the individual getter methods. + m := manifestSchema2FromComponentsLikeFixture(configBlob) + cb, err := m.ConfigBlob(context.Background()) + require.NoError(t, err) + assert.Equal(t, configBlob, cb) +} + +func TestManifestSchema2LayerInfo(t *testing.T) { + for _, m := range []genericManifest{ + manifestSchema2FromFixture(t, mocks.ForbiddenImageSource{}, "schema2.json", false), + manifestSchema2FromComponentsLikeFixture(nil), + } { + assert.Equal(t, []types.BlobInfo{ + { + Digest: "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb", + Size: 51354364, + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + }, + { + Digest: "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c", + Size: 150, + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + }, + { + Digest: "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9", + Size: 11739507, + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + }, + { + Digest: "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909", + Size: 8841833, + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + }, + { + Digest: "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa", + Size: 291, + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + }, + }, m.LayerInfos()) + } +} + +func TestManifestSchema2EmbeddedDockerReferenceConflicts(t *testing.T) { + for _, m := range []genericManifest{ + manifestSchema2FromFixture(t, mocks.ForbiddenImageSource{}, "schema2.json", false), + manifestSchema2FromComponentsLikeFixture(nil), + } { + for _, name := range []string{"busybox", "example.com:5555/ns/repo:tag"} { + ref, err := reference.ParseNormalizedNamed(name) + require.NoError(t, err) + conflicts := m.EmbeddedDockerReferenceConflicts(ref) + assert.False(t, conflicts) + } + } +} + +func TestManifestSchema2Inspect(t *testing.T) { + configJSON, err := os.ReadFile("fixtures/schema2-config.json") + require.NoError(t, err) + + m := manifestSchema2FromComponentsLikeFixture(configJSON) + ii, err := m.Inspect(context.Background()) + require.NoError(t, err) + created := time.Date(2016, 9, 23, 23, 20, 45, 789764590, time.UTC) + + var emptyAnnotations map[string]string + assert.Equal(t, types.ImageInspectInfo{ + Tag: "", + Created: &created, + DockerVersion: "1.12.1", + Labels: map[string]string{}, + Architecture: "amd64", + Os: "linux", + Layers: []string{ + "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb", + "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c", + "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9", + "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909", + "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa", + }, + LayersData: []types.ImageInspectLayer{{ + MIMEType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Digest: "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb", + Size: 51354364, + Annotations: emptyAnnotations, + }, { + MIMEType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Digest: "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c", + Size: 150, + Annotations: emptyAnnotations, + }, { + MIMEType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Digest: "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9", + Size: 11739507, + Annotations: emptyAnnotations, + }, { + MIMEType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Digest: "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909", + Size: 8841833, + Annotations: emptyAnnotations, + }, { + MIMEType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Digest: "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa", + Size: 291, + Annotations: emptyAnnotations, + }, + }, + Author: "", + Env: []string{ + "PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "HTTPD_PREFIX=/usr/local/apache2", + "HTTPD_VERSION=2.4.23", + "HTTPD_SHA1=5101be34ac4a509b245adb70a56690a84fcc4e7f", + "HTTPD_BZ2_URL=https://www.apache.org/dyn/closer.cgi?action=download&filename=httpd/httpd-2.4.23.tar.bz2", + "HTTPD_ASC_URL=https://www.apache.org/dist/httpd/httpd-2.4.23.tar.bz2.asc", + }, + }, *ii) + + // nil configBlob will trigger an error in m.ConfigBlob() + m = manifestSchema2FromComponentsLikeFixture(nil) + _, err = m.Inspect(context.Background()) + assert.Error(t, err) + + m = manifestSchema2FromComponentsLikeFixture([]byte("invalid JSON")) + _, err = m.Inspect(context.Background()) + assert.Error(t, err) +} + +func TestManifestSchema2UpdatedImageNeedsLayerDiffIDs(t *testing.T) { + for _, m := range []genericManifest{ + manifestSchema2FromFixture(t, mocks.ForbiddenImageSource{}, "schema2.json", false), + manifestSchema2FromComponentsLikeFixture(nil), + } { + assert.False(t, m.UpdatedImageNeedsLayerDiffIDs(types.ManifestUpdateOptions{ + ManifestMIMEType: manifest.DockerV2Schema1SignedMediaType, + })) + } +} + +// schema2ImageSource is plausible enough for schema conversions in manifestSchema2.UpdatedImage() to work. +type schema2ImageSource struct { + configBlobImageSource + ref reference.Named +} + +func (s2is *schema2ImageSource) Reference() types.ImageReference { + return refImageReferenceMock{ref: s2is.ref} +} + +// refImageReferenceMock is a mock of types.ImageReference which returns itself in DockerReference. +type refImageReferenceMock struct { + mocks.ForbiddenImageReference // We inherit almost all of the methods, which just panic() + ref reference.Named +} + +func (ref refImageReferenceMock) DockerReference() reference.Named { + return ref.ref +} + +func newSchema2ImageSource(t *testing.T, dockerRef string) *schema2ImageSource { + realConfigJSON, err := os.ReadFile("fixtures/schema2-config.json") + require.NoError(t, err) + + ref, err := reference.ParseNormalizedNamed(dockerRef) + require.NoError(t, err) + + return &schema2ImageSource{ + configBlobImageSource: configBlobImageSource{ + f: func() (io.ReadCloser, int64, error) { + return io.NopCloser(bytes.NewReader(realConfigJSON)), int64(len(realConfigJSON)), nil + }, + }, + ref: ref, + } +} + +type memoryImageDest struct { + ref reference.Named + storedBlobs map[digest.Digest][]byte +} + +func (d *memoryImageDest) Reference() types.ImageReference { + return refImageReferenceMock{ref: d.ref} +} +func (d *memoryImageDest) Close() error { + panic("Unexpected call to a mock function") +} +func (d *memoryImageDest) SupportedManifestMIMETypes() []string { + panic("Unexpected call to a mock function") +} +func (d *memoryImageDest) SupportsSignatures(ctx context.Context) error { + panic("Unexpected call to a mock function") +} +func (d *memoryImageDest) DesiredLayerCompression() types.LayerCompression { + panic("Unexpected call to a mock function") +} +func (d *memoryImageDest) AcceptsForeignLayerURLs() bool { + panic("Unexpected call to a mock function") +} +func (d *memoryImageDest) MustMatchRuntimeOS() bool { + panic("Unexpected call to a mock function") +} +func (d *memoryImageDest) IgnoresEmbeddedDockerReference() bool { + panic("Unexpected call to a mock function") +} +func (d *memoryImageDest) HasThreadSafePutBlob() bool { + panic("Unexpected call to a mock function") +} +func (d *memoryImageDest) PutBlob(ctx context.Context, stream io.Reader, inputInfo types.BlobInfo, cache types.BlobInfoCache, isConfig bool) (types.BlobInfo, error) { + if d.storedBlobs == nil { + d.storedBlobs = make(map[digest.Digest][]byte) + } + if inputInfo.Digest == "" { + panic("inputInfo.Digest unexpectedly empty") + } + contents, err := io.ReadAll(stream) + if err != nil { + return types.BlobInfo{}, err + } + d.storedBlobs[inputInfo.Digest] = contents + return types.BlobInfo{Digest: inputInfo.Digest, Size: int64(len(contents))}, nil +} +func (d *memoryImageDest) TryReusingBlob(context.Context, types.BlobInfo, types.BlobInfoCache, bool) (bool, types.BlobInfo, error) { + panic("Unexpected call to a mock function") +} +func (d *memoryImageDest) PutManifest(context.Context, []byte, *digest.Digest) error { + panic("Unexpected call to a mock function") +} +func (d *memoryImageDest) PutSignatures(ctx context.Context, signatures [][]byte, instanceDigest *digest.Digest) error { + panic("Unexpected call to a mock function") +} +func (d *memoryImageDest) Commit(context.Context, types.UnparsedImage) error { + panic("Unexpected call to a mock function") +} + +// modifiedLayerInfos returns two identical (but separately allocated) copies of +// layers from input, where the size and digest of each item is predictably modified from the original in input. +// (This is used to test ManifestUpdateOptions.LayerInfos handling.) +func modifiedLayerInfos(t *testing.T, input []types.BlobInfo) ([]types.BlobInfo, []types.BlobInfo) { + modified := []types.BlobInfo{} + for _, blob := range input { + b2 := blob + oldDigest, err := hex.DecodeString(b2.Digest.Encoded()) + require.NoError(t, err) + oldDigest[len(oldDigest)-1] ^= 1 + b2.Digest = digest.NewDigestFromEncoded(b2.Digest.Algorithm(), hex.EncodeToString(oldDigest)) + b2.Size ^= 1 + modified = append(modified, b2) + } + + copy := slices.Clone(modified) + return modified, copy +} + +func TestManifestSchema2UpdatedImage(t *testing.T) { + originalSrc := newSchema2ImageSource(t, "httpd:latest") + original := manifestSchema2FromFixture(t, originalSrc, "schema2.json", false) + + // LayerInfos: + layerInfos := append(original.LayerInfos()[1:], original.LayerInfos()[0]) + res, err := original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + LayerInfos: layerInfos, + }) + require.NoError(t, err) + assert.Equal(t, layerInfos, res.LayerInfos()) + _, err = original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + LayerInfos: append(layerInfos, layerInfos[0]), + }) + assert.Error(t, err) + + // EmbeddedDockerReference: + // … is ignored + embeddedRef, err := reference.ParseNormalizedNamed("busybox") + require.NoError(t, err) + res, err = original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + EmbeddedDockerReference: embeddedRef, + }) + require.NoError(t, err) + nonEmbeddedRef, err := reference.ParseNormalizedNamed("notbusybox:notlatest") + require.NoError(t, err) + conflicts := res.EmbeddedDockerReferenceConflicts(nonEmbeddedRef) + assert.False(t, conflicts) + + // ManifestMIMEType: + // Only smoke-test the valid conversions, detailed tests are below. (This also verifies that “original” is not affected.) + for _, mime := range []string{ + manifest.DockerV2Schema1MediaType, + manifest.DockerV2Schema1SignedMediaType, + } { + _, err = original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + ManifestMIMEType: mime, + InformationOnly: types.ManifestUpdateInformation{ + Destination: &memoryImageDest{ref: originalSrc.ref}, + }, + }) + assert.NoError(t, err, mime) + } + for _, mime := range []string{ + manifest.DockerV2Schema2MediaType, // This indicates a confused caller, not a no-op + "this is invalid", + } { + _, err = original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + ManifestMIMEType: mime, + }) + assert.Error(t, err, mime) + } + + // m hasn’t been changed: + m2 := manifestSchema2FromFixture(t, originalSrc, "schema2.json", false) + typedOriginal, ok := original.(*manifestSchema2) + require.True(t, ok) + typedM2, ok := m2.(*manifestSchema2) + require.True(t, ok) + assert.Equal(t, *typedM2, *typedOriginal) +} + +func TestConvertToManifestOCI(t *testing.T) { + originalSrc := newSchema2ImageSource(t, "httpd-copy:latest") + original := manifestSchema2FromFixture(t, originalSrc, "schema2.json", false) + res, err := original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + ManifestMIMEType: imgspecv1.MediaTypeImageManifest, + }) + require.NoError(t, err) + + convertedJSON, mt, err := res.Manifest(context.Background()) + require.NoError(t, err) + assert.Equal(t, imgspecv1.MediaTypeImageManifest, mt) + assertJSONEqualsFixture(t, convertedJSON, "schema2-to-oci1.json") + + convertedConfig, err := res.ConfigBlob(context.Background()) + require.NoError(t, err) + assertJSONEqualsFixture(t, convertedConfig, "schema2-to-oci1-config.json") +} + +func TestConvertToManifestOCIAllMediaTypes(t *testing.T) { + originalSrc := newSchema2ImageSource(t, "httpd-copy:latest") + original := manifestSchema2FromFixture(t, originalSrc, "schema2-all-media-types.json", false) + res, err := original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + ManifestMIMEType: imgspecv1.MediaTypeImageManifest, + }) + require.NoError(t, err) + convertedJSON, mt, err := res.Manifest(context.Background()) + require.NoError(t, err) + assert.Equal(t, imgspecv1.MediaTypeImageManifest, mt) + assertJSONEqualsFixture(t, convertedJSON, "schema2-all-media-types-to-oci1.json") + + convertedConfig, err := res.ConfigBlob(context.Background()) + require.NoError(t, err) + assertJSONEqualsFixture(t, convertedConfig, "schema2-to-oci1-config.json") +} + +func TestConvertToOCIWithInvalidMIMEType(t *testing.T) { + originalSrc := newSchema2ImageSource(t, "httpd-copy:latest") + manifestSchema2FromFixture(t, originalSrc, "schema2-invalid-media-type.json", true) +} + +func TestConvertToManifestSchema1(t *testing.T) { + originalSrc := newSchema2ImageSource(t, "httpd-copy:latest") + original := manifestSchema2FromFixture(t, originalSrc, "schema2.json", false) + memoryDest := &memoryImageDest{ref: originalSrc.ref} + res, err := original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + ManifestMIMEType: manifest.DockerV2Schema1SignedMediaType, + InformationOnly: types.ManifestUpdateInformation{ + Destination: memoryDest, + }, + }) + require.NoError(t, err) + + convertedJSON, mt, err := res.Manifest(context.Background()) + require.NoError(t, err) + assert.Equal(t, manifest.DockerV2Schema1SignedMediaType, mt) + + // schema2-to-schema1-by-docker.json is the result of asking the Docker Hub for a schema1 manifest, + // except that we have replaced "name" to verify that the ref from + // memoryDest, not from originalSrc, is used. + assertJSONEqualsFixture(t, convertedJSON, "schema2-to-schema1-by-docker.json", "signatures") + + assert.Equal(t, GzippedEmptyLayer, memoryDest.storedBlobs[GzippedEmptyLayerDigest]) + + // Conversion to schema1 together with changing LayerInfos works as expected (which requires + // handling schema1 empty layers): + updatedLayers, updatedLayersCopy := modifiedLayerInfos(t, original.LayerInfos()) + res, err = original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + LayerInfos: updatedLayers, + ManifestMIMEType: manifest.DockerV2Schema1SignedMediaType, + InformationOnly: types.ManifestUpdateInformation{ + Destination: memoryDest, + }, + }) + require.NoError(t, err) + assert.Equal(t, updatedLayersCopy, updatedLayers) // updatedLayers have not been modified in place + convertedJSON, mt, err = res.Manifest(context.Background()) + require.NoError(t, err) + assert.Equal(t, manifest.DockerV2Schema1SignedMediaType, mt) + // Layers have been updated as expected + s1Manifest, err := manifestSchema1FromManifest(convertedJSON) + require.NoError(t, err) + assert.Equal(t, []types.BlobInfo{ + {Digest: "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5ba", Size: -1}, + {Digest: GzippedEmptyLayerDigest, Size: -1}, + {Digest: GzippedEmptyLayerDigest, Size: -1}, + {Digest: GzippedEmptyLayerDigest, Size: -1}, + {Digest: "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680d", Size: -1}, + {Digest: GzippedEmptyLayerDigest, Size: -1}, + {Digest: "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a8", Size: -1}, + {Digest: GzippedEmptyLayerDigest, Size: -1}, + {Digest: GzippedEmptyLayerDigest, Size: -1}, + {Digest: GzippedEmptyLayerDigest, Size: -1}, + {Digest: GzippedEmptyLayerDigest, Size: -1}, + {Digest: "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25908", Size: -1}, + {Digest: "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fb", Size: -1}, + {Digest: GzippedEmptyLayerDigest, Size: -1}, + {Digest: GzippedEmptyLayerDigest, Size: -1}, + }, s1Manifest.LayerInfos()) + + // FIXME? Test also the various failure cases, if only to see that we don't crash? +} + +func TestConvertSchema2ToManifestOCIWithAnnotations(t *testing.T) { + // Test when converting an image from schema 2 (which doesn't support certain fields like + // URLs, annotations, etc.) to an OCI image (which supports those fields), + // that UpdatedImage propagates the features to the converted manifest. + originalSrc := newSchema2ImageSource(t, "httpd-copy:latest") + original := manifestSchema2FromFixture(t, originalSrc, "schema2.json", false) + layerInfoOverwrites := []types.BlobInfo{ + { + Digest: "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb", + Size: 51354364, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + }, + { + Digest: "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c", + Size: 150, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + }, + { + Digest: "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9", + Size: 11739507, + URLs: []string{ + "https://layer.url", + }, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + }, + { + Digest: "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909", + Size: 8841833, + Annotations: map[string]string{ + "test-annotation-2": "two", + }, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + }, + { + Digest: "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa", + Size: 291, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + }, + } + res, err := original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + ManifestMIMEType: imgspecv1.MediaTypeImageManifest, + LayerInfos: layerInfoOverwrites, + }) + require.NoError(t, err) + assert.Equal(t, res.LayerInfos(), layerInfoOverwrites) + + // Doing this with schema2 should fail + originalSrc = newSchema2ImageSource(t, "httpd-copy:latest") + original = manifestSchema2FromFixture(t, originalSrc, "schema2.json", false) + res, err = original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + ManifestMIMEType: "", + LayerInfos: layerInfoOverwrites, + }) + require.NoError(t, err) + assert.NotEqual(t, res.LayerInfos(), layerInfoOverwrites) +} + +func TestManifestSchema2CanChangeLayerCompression(t *testing.T) { + for _, m := range []genericManifest{ + manifestSchema2FromFixture(t, mocks.ForbiddenImageSource{}, "schema2.json", false), + manifestSchema2FromComponentsLikeFixture(nil), + } { + assert.True(t, m.CanChangeLayerCompression(manifest.DockerV2Schema2LayerMediaType)) + // Some projects like to use squashfs and other unspecified formats for layers; don’t touch those. + assert.False(t, m.CanChangeLayerCompression("a completely unknown and quite possibly invalid MIME type")) + } +} diff --git a/vendor/github.com/containers/image/v5/internal/image/fixtures/oci1-all-media-types-to-schema2.json b/vendor/github.com/containers/image/v5/internal/image/fixtures/oci1-all-media-types-to-schema2.json new file mode 100644 index 00000000000..702addfb0e2 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/image/fixtures/oci1-all-media-types-to-schema2.json @@ -0,0 +1,41 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 4651, + "digest": "sha256:a13a0762ab7bed51a1b49adec0a702b1cd99294fd460a025b465bcfb7b152745" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar", + "size": 51354364, + "digest": "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.zstd", + "size": 150, + "digest": "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 152, + "digest": "sha256:2bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.foreign.diff.tar", + "size": 11739507, + "digest": "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip", + "size": 8841833, + "digest": "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip", + "size": 291, + "digest": "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa" + } + ] +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/internal/image/fixtures/oci1-all-media-types.json b/vendor/github.com/containers/image/v5/internal/image/fixtures/oci1-all-media-types.json new file mode 100644 index 00000000000..96553928287 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/image/fixtures/oci1-all-media-types.json @@ -0,0 +1,41 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 4651, + "digest": "sha256:a13a0762ab7bed51a1b49adec0a702b1cd99294fd460a025b465bcfb7b152745" + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar", + "size": 51354364, + "digest": "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+zstd", + "size": 150, + "digest": "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 152, + "digest": "sha256:2bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c" + }, + { + "mediaType": "application/vnd.oci.image.layer.nondistributable.v1.tar", + "size": 11739507, + "digest": "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9" + }, + { + "mediaType": "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip", + "size": 8841833, + "digest": "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909" + }, + { + "mediaType": "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip", + "size": 291, + "digest": "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa" + } + ] +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/internal/image/fixtures/oci1-artifact.json b/vendor/github.com/containers/image/v5/internal/image/fixtures/oci1-artifact.json new file mode 100644 index 00000000000..9e54409a31d --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/image/fixtures/oci1-artifact.json @@ -0,0 +1,43 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.custom.artifact.config.v1+json", + "size": 5940, + "digest": "sha256:9ca4bda0a6b3727a6ffcc43e981cad0f24e2ec79d338f6ba325b4dfd0756fb8f", + "annotations": { + "test-annotation-1": "one" + } + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 51354364, + "digest": "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 150, + "digest": "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 11739507, + "digest": "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9", + "urls": ["https://layer.url"] + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 8841833, + "digest": "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909", + "annotations": { + "test-annotation-2": "two" + } + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 291, + "digest": "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa" + } + ] +} diff --git a/vendor/github.com/containers/image/v5/internal/image/fixtures/oci1-config.json b/vendor/github.com/containers/image/v5/internal/image/fixtures/oci1-config.json new file mode 100644 index 00000000000..f49230ea77d --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/image/fixtures/oci1-config.json @@ -0,0 +1 @@ +{"architecture":"amd64","config":{"Hostname":"383850eeb47b","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"ExposedPorts":{"80/tcp":{}},"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","HTTPD_PREFIX=/usr/local/apache2","HTTPD_VERSION=2.4.23","HTTPD_SHA1=5101be34ac4a509b245adb70a56690a84fcc4e7f","HTTPD_BZ2_URL=https://www.apache.org/dyn/closer.cgi?action=download\u0026filename=httpd/httpd-2.4.23.tar.bz2","HTTPD_ASC_URL=https://www.apache.org/dist/httpd/httpd-2.4.23.tar.bz2.asc"],"Cmd":["httpd-foreground"],"ArgsEscaped":true,"Image":"sha256:4f83530449c67c1ed8fca72583c5b92fdf446010990028c362a381e55dd84afd","Volumes":null,"WorkingDir":"/usr/local/apache2","Entrypoint":null,"OnBuild":[],"Labels":{}},"container":"8825acde1b009729807e4b70a65a89399dd8da8e53be9216b9aaabaff4339f69","container_config":{"Hostname":"383850eeb47b","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"ExposedPorts":{"80/tcp":{}},"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","HTTPD_PREFIX=/usr/local/apache2","HTTPD_VERSION=2.4.23","HTTPD_SHA1=5101be34ac4a509b245adb70a56690a84fcc4e7f","HTTPD_BZ2_URL=https://www.apache.org/dyn/closer.cgi?action=download\u0026filename=httpd/httpd-2.4.23.tar.bz2","HTTPD_ASC_URL=https://www.apache.org/dist/httpd/httpd-2.4.23.tar.bz2.asc"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\"httpd-foreground\"]"],"ArgsEscaped":true,"Image":"sha256:4f83530449c67c1ed8fca72583c5b92fdf446010990028c362a381e55dd84afd","Volumes":null,"WorkingDir":"/usr/local/apache2","Entrypoint":null,"OnBuild":[],"Labels":{}},"created":"2016-09-23T23:20:45.78976459Z","docker_version":"1.12.1","history":[{"created":"2016-09-23T18:08:50.537223822Z","created_by":"/bin/sh -c #(nop) ADD file:c6c23585ab140b0b320d4e99bc1b0eb544c9e96c24d90fec5e069a6d57d335ca in / "},{"created":"2016-09-23T18:08:51.133779867Z","created_by":"/bin/sh -c #(nop) CMD [\"/bin/bash\"]","empty_layer":true},{"created":"2016-09-23T19:16:40.725768956Z","created_by":"/bin/sh -c #(nop) ENV HTTPD_PREFIX=/usr/local/apache2","empty_layer":true},{"created":"2016-09-23T19:16:41.037788416Z","created_by":"/bin/sh -c #(nop) ENV PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","empty_layer":true},{"created":"2016-09-23T19:16:41.990121202Z","created_by":"/bin/sh -c mkdir -p \"$HTTPD_PREFIX\" \t\u0026\u0026 chown www-data:www-data \"$HTTPD_PREFIX\""},{"created":"2016-09-23T19:16:42.339911155Z","created_by":"/bin/sh -c #(nop) WORKDIR /usr/local/apache2","empty_layer":true},{"created":"2016-09-23T19:16:54.948461741Z","created_by":"/bin/sh -c apt-get update \t\u0026\u0026 apt-get install -y --no-install-recommends \t\tlibapr1 \t\tlibaprutil1 \t\tlibaprutil1-ldap \t\tlibapr1-dev \t\tlibaprutil1-dev \t\tlibpcre++0 \t\tlibssl1.0.0 \t\u0026\u0026 rm -r /var/lib/apt/lists/*"},{"created":"2016-09-23T19:16:55.321573403Z","created_by":"/bin/sh -c #(nop) ENV HTTPD_VERSION=2.4.23","empty_layer":true},{"created":"2016-09-23T19:16:55.629947307Z","created_by":"/bin/sh -c #(nop) ENV HTTPD_SHA1=5101be34ac4a509b245adb70a56690a84fcc4e7f","empty_layer":true},{"created":"2016-09-23T23:19:03.705796801Z","created_by":"/bin/sh -c #(nop) ENV HTTPD_BZ2_URL=https://www.apache.org/dyn/closer.cgi?action=download\u0026filename=httpd/httpd-2.4.23.tar.bz2","empty_layer":true},{"created":"2016-09-23T23:19:04.009782822Z","created_by":"/bin/sh -c #(nop) ENV HTTPD_ASC_URL=https://www.apache.org/dist/httpd/httpd-2.4.23.tar.bz2.asc","empty_layer":true},{"created":"2016-09-23T23:20:44.585743332Z","created_by":"/bin/sh -c set -x \t\u0026\u0026 buildDeps=' \t\tbzip2 \t\tca-certificates \t\tgcc \t\tlibpcre++-dev \t\tlibssl-dev \t\tmake \t\twget \t' \t\u0026\u0026 apt-get update \t\u0026\u0026 apt-get install -y --no-install-recommends $buildDeps \t\u0026\u0026 rm -r /var/lib/apt/lists/* \t\t\u0026\u0026 wget -O httpd.tar.bz2 \"$HTTPD_BZ2_URL\" \t\u0026\u0026 echo \"$HTTPD_SHA1 *httpd.tar.bz2\" | sha1sum -c - \t\u0026\u0026 wget -O httpd.tar.bz2.asc \"$HTTPD_ASC_URL\" \t\u0026\u0026 export GNUPGHOME=\"$(mktemp -d)\" \t\u0026\u0026 gpg --keyserver ha.pool.sks-keyservers.net --recv-keys A93D62ECC3C8EA12DB220EC934EA76E6791485A8 \t\u0026\u0026 gpg --batch --verify httpd.tar.bz2.asc httpd.tar.bz2 \t\u0026\u0026 rm -r \"$GNUPGHOME\" httpd.tar.bz2.asc \t\t\u0026\u0026 mkdir -p src \t\u0026\u0026 tar -xvf httpd.tar.bz2 -C src --strip-components=1 \t\u0026\u0026 rm httpd.tar.bz2 \t\u0026\u0026 cd src \t\t\u0026\u0026 ./configure \t\t--prefix=\"$HTTPD_PREFIX\" \t\t--enable-mods-shared=reallyall \t\u0026\u0026 make -j\"$(nproc)\" \t\u0026\u0026 make install \t\t\u0026\u0026 cd .. \t\u0026\u0026 rm -r src \t\t\u0026\u0026 sed -ri \t\t-e 's!^(\\s*CustomLog)\\s+\\S+!\\1 /proc/self/fd/1!g' \t\t-e 's!^(\\s*ErrorLog)\\s+\\S+!\\1 /proc/self/fd/2!g' \t\t\"$HTTPD_PREFIX/conf/httpd.conf\" \t\t\u0026\u0026 apt-get purge -y --auto-remove $buildDeps"},{"created":"2016-09-23T23:20:45.127455562Z","created_by":"/bin/sh -c #(nop) COPY file:761e313354b918b6cd7ea99975a4f6b53ff5381ba689bab2984aec4dab597215 in /usr/local/bin/ "},{"created":"2016-09-23T23:20:45.453934921Z","created_by":"/bin/sh -c #(nop) EXPOSE 80/tcp","empty_layer":true},{"created":"2016-09-23T23:20:45.78976459Z","created_by":"/bin/sh -c #(nop) CMD [\"httpd-foreground\"]","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:142a601d97936307e75220c35dde0348971a9584c21e7cb42e1f7004005432ab","sha256:90fcc66ad3be9f1757f954b750deb37032f208428aa12599fcb02182b9065a9c","sha256:5a8624bb7e76d1e6829f9c64c43185e02bc07f97a2189eb048609a8914e72c56","sha256:d349ff6b3afc6a2800054768c82bfbf4289c9aa5da55c1290f802943dcd4d1e9","sha256:8c064bb1f60e84fa8cc6079b6d2e76e0423389fd6aeb7e497dfdae5e05b2b25b"]}} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/internal/image/fixtures/oci1-invalid-media-type.json b/vendor/github.com/containers/image/v5/internal/image/fixtures/oci1-invalid-media-type.json new file mode 100644 index 00000000000..7b7d06ee747 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/image/fixtures/oci1-invalid-media-type.json @@ -0,0 +1,15 @@ +{ + "schemaVersion": 2, + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 5940, + "digest": "sha256:9ca4bda0a6b3727a6ffcc43e981cad0f24e2ec79d338f6ba325b4dfd0756fb8f" + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+invalid-suffix", + "size": 51354364, + "digest": "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb" + } + ] +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/internal/image/fixtures/oci1-to-schema1.json b/vendor/github.com/containers/image/v5/internal/image/fixtures/oci1-to-schema1.json new file mode 100644 index 00000000000..a85b3ff9f0e --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/image/fixtures/oci1-to-schema1.json @@ -0,0 +1 @@ +{"name":"library/httpd-copy","tag":"latest","architecture":"amd64","fsLayers":[{"blobSum":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"},{"blobSum":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"},{"blobSum":"sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa"},{"blobSum":"sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909"},{"blobSum":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"},{"blobSum":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"},{"blobSum":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"},{"blobSum":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"},{"blobSum":"sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9"},{"blobSum":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"},{"blobSum":"sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c"},{"blobSum":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"},{"blobSum":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"},{"blobSum":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"},{"blobSum":"sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb"}],"history":[{"v1Compatibility":"{\"architecture\":\"amd64\",\"config\":{\"Hostname\":\"383850eeb47b\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":{\"80/tcp\":{}},\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"HTTPD_PREFIX=/usr/local/apache2\",\"HTTPD_VERSION=2.4.23\",\"HTTPD_SHA1=5101be34ac4a509b245adb70a56690a84fcc4e7f\",\"HTTPD_BZ2_URL=https://www.apache.org/dyn/closer.cgi?action=download\\u0026filename=httpd/httpd-2.4.23.tar.bz2\",\"HTTPD_ASC_URL=https://www.apache.org/dist/httpd/httpd-2.4.23.tar.bz2.asc\"],\"Cmd\":[\"httpd-foreground\"],\"ArgsEscaped\":true,\"Image\":\"sha256:4f83530449c67c1ed8fca72583c5b92fdf446010990028c362a381e55dd84afd\",\"Volumes\":null,\"WorkingDir\":\"/usr/local/apache2\",\"Entrypoint\":null,\"OnBuild\":[],\"Labels\":{}},\"container\":\"8825acde1b009729807e4b70a65a89399dd8da8e53be9216b9aaabaff4339f69\",\"container_config\":{\"Hostname\":\"383850eeb47b\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":{\"80/tcp\":{}},\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"HTTPD_PREFIX=/usr/local/apache2\",\"HTTPD_VERSION=2.4.23\",\"HTTPD_SHA1=5101be34ac4a509b245adb70a56690a84fcc4e7f\",\"HTTPD_BZ2_URL=https://www.apache.org/dyn/closer.cgi?action=download\\u0026filename=httpd/httpd-2.4.23.tar.bz2\",\"HTTPD_ASC_URL=https://www.apache.org/dist/httpd/httpd-2.4.23.tar.bz2.asc\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) \",\"CMD [\\\"httpd-foreground\\\"]\"],\"ArgsEscaped\":true,\"Image\":\"sha256:4f83530449c67c1ed8fca72583c5b92fdf446010990028c362a381e55dd84afd\",\"Volumes\":null,\"WorkingDir\":\"/usr/local/apache2\",\"Entrypoint\":null,\"OnBuild\":[],\"Labels\":{}},\"created\":\"2016-09-23T23:20:45.78976459Z\",\"docker_version\":\"1.12.1\",\"id\":\"dca7323f9c839837493199d63263083d94f5eb1796d7bd04ca8374c4e9d3749a\",\"os\":\"linux\",\"parent\":\"1b750729af47c9a802c8d14b0d327d3ad5ecdce5ae773ac728a0263315b914f4\",\"throwaway\":true}"},{"v1Compatibility":"{\"id\":\"1b750729af47c9a802c8d14b0d327d3ad5ecdce5ae773ac728a0263315b914f4\",\"parent\":\"3ef2f186f8b0a2fd2d95f5a1f1cd213f5fb0a6e51b0a8dfbe2ec7003a788ff9a\",\"created\":\"2016-09-23T23:20:45.453934921Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) EXPOSE 80/tcp\"]},\"throwaway\":true}"},{"v1Compatibility":"{\"id\":\"3ef2f186f8b0a2fd2d95f5a1f1cd213f5fb0a6e51b0a8dfbe2ec7003a788ff9a\",\"parent\":\"dbbb5c772ba968f675ebdb1968a2fbcf3cf53c0c85ff4e3329619e3735c811e6\",\"created\":\"2016-09-23T23:20:45.127455562Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) COPY file:761e313354b918b6cd7ea99975a4f6b53ff5381ba689bab2984aec4dab597215 in /usr/local/bin/ \"]}}"},{"v1Compatibility":"{\"id\":\"dbbb5c772ba968f675ebdb1968a2fbcf3cf53c0c85ff4e3329619e3735c811e6\",\"parent\":\"d264ded964bb52f78c8905c9e6c5f2b8526ef33f371981f0651f3fb0164ad4a7\",\"created\":\"2016-09-23T23:20:44.585743332Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c set -x \\t\\u0026\\u0026 buildDeps=' \\t\\tbzip2 \\t\\tca-certificates \\t\\tgcc \\t\\tlibpcre++-dev \\t\\tlibssl-dev \\t\\tmake \\t\\twget \\t' \\t\\u0026\\u0026 apt-get update \\t\\u0026\\u0026 apt-get install -y --no-install-recommends $buildDeps \\t\\u0026\\u0026 rm -r /var/lib/apt/lists/* \\t\\t\\u0026\\u0026 wget -O httpd.tar.bz2 \\\"$HTTPD_BZ2_URL\\\" \\t\\u0026\\u0026 echo \\\"$HTTPD_SHA1 *httpd.tar.bz2\\\" | sha1sum -c - \\t\\u0026\\u0026 wget -O httpd.tar.bz2.asc \\\"$HTTPD_ASC_URL\\\" \\t\\u0026\\u0026 export GNUPGHOME=\\\"$(mktemp -d)\\\" \\t\\u0026\\u0026 gpg --keyserver ha.pool.sks-keyservers.net --recv-keys A93D62ECC3C8EA12DB220EC934EA76E6791485A8 \\t\\u0026\\u0026 gpg --batch --verify httpd.tar.bz2.asc httpd.tar.bz2 \\t\\u0026\\u0026 rm -r \\\"$GNUPGHOME\\\" httpd.tar.bz2.asc \\t\\t\\u0026\\u0026 mkdir -p src \\t\\u0026\\u0026 tar -xvf httpd.tar.bz2 -C src --strip-components=1 \\t\\u0026\\u0026 rm httpd.tar.bz2 \\t\\u0026\\u0026 cd src \\t\\t\\u0026\\u0026 ./configure \\t\\t--prefix=\\\"$HTTPD_PREFIX\\\" \\t\\t--enable-mods-shared=reallyall \\t\\u0026\\u0026 make -j\\\"$(nproc)\\\" \\t\\u0026\\u0026 make install \\t\\t\\u0026\\u0026 cd .. \\t\\u0026\\u0026 rm -r src \\t\\t\\u0026\\u0026 sed -ri \\t\\t-e 's!^(\\\\s*CustomLog)\\\\s+\\\\S+!\\\\1 /proc/self/fd/1!g' \\t\\t-e 's!^(\\\\s*ErrorLog)\\\\s+\\\\S+!\\\\1 /proc/self/fd/2!g' \\t\\t\\\"$HTTPD_PREFIX/conf/httpd.conf\\\" \\t\\t\\u0026\\u0026 apt-get purge -y --auto-remove $buildDeps\"]}}"},{"v1Compatibility":"{\"id\":\"d264ded964bb52f78c8905c9e6c5f2b8526ef33f371981f0651f3fb0164ad4a7\",\"parent\":\"fd6f8d569a8a6d2a95f797494ab3cee7a47693dde647210b236a141f76b5c5fd\",\"created\":\"2016-09-23T23:19:04.009782822Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ENV HTTPD_ASC_URL=https://www.apache.org/dist/httpd/httpd-2.4.23.tar.bz2.asc\"]},\"throwaway\":true}"},{"v1Compatibility":"{\"id\":\"fd6f8d569a8a6d2a95f797494ab3cee7a47693dde647210b236a141f76b5c5fd\",\"parent\":\"5e2578d171daa47c0eeb55e592b4e3bd28a0946a75baed58e4d4dd315c5d5780\",\"created\":\"2016-09-23T23:19:03.705796801Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ENV HTTPD_BZ2_URL=https://www.apache.org/dyn/closer.cgi?action=download\\u0026filename=httpd/httpd-2.4.23.tar.bz2\"]},\"throwaway\":true}"},{"v1Compatibility":"{\"id\":\"5e2578d171daa47c0eeb55e592b4e3bd28a0946a75baed58e4d4dd315c5d5780\",\"parent\":\"1912159ee5bea8d7fde49b85012f90c47bceb3f09e4082b112b1f06a3f339c53\",\"created\":\"2016-09-23T19:16:55.629947307Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ENV HTTPD_SHA1=5101be34ac4a509b245adb70a56690a84fcc4e7f\"]},\"throwaway\":true}"},{"v1Compatibility":"{\"id\":\"1912159ee5bea8d7fde49b85012f90c47bceb3f09e4082b112b1f06a3f339c53\",\"parent\":\"3bfb089ca9d4bb73a9016e44a2c6f908b701f97704433305c419f75e8559d8a2\",\"created\":\"2016-09-23T19:16:55.321573403Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ENV HTTPD_VERSION=2.4.23\"]},\"throwaway\":true}"},{"v1Compatibility":"{\"id\":\"3bfb089ca9d4bb73a9016e44a2c6f908b701f97704433305c419f75e8559d8a2\",\"parent\":\"ae1ece73de4d0365c8b8ab45ba0bf6b1efa4213c16a4903b89341b704d101c3c\",\"created\":\"2016-09-23T19:16:54.948461741Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c apt-get update \\t\\u0026\\u0026 apt-get install -y --no-install-recommends \\t\\tlibapr1 \\t\\tlibaprutil1 \\t\\tlibaprutil1-ldap \\t\\tlibapr1-dev \\t\\tlibaprutil1-dev \\t\\tlibpcre++0 \\t\\tlibssl1.0.0 \\t\\u0026\\u0026 rm -r /var/lib/apt/lists/*\"]}}"},{"v1Compatibility":"{\"id\":\"ae1ece73de4d0365c8b8ab45ba0bf6b1efa4213c16a4903b89341b704d101c3c\",\"parent\":\"bffbcb416f40e0bd3ebae202403587bfd41829cd1e0d538b66f29adce40c6408\",\"created\":\"2016-09-23T19:16:42.339911155Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) WORKDIR /usr/local/apache2\"]},\"throwaway\":true}"},{"v1Compatibility":"{\"id\":\"bffbcb416f40e0bd3ebae202403587bfd41829cd1e0d538b66f29adce40c6408\",\"parent\":\"7b27731a3363efcb6b0520962d544471745aae15664920dffe690b4fdb410d80\",\"created\":\"2016-09-23T19:16:41.990121202Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c mkdir -p \\\"$HTTPD_PREFIX\\\" \\t\\u0026\\u0026 chown www-data:www-data \\\"$HTTPD_PREFIX\\\"\"]}}"},{"v1Compatibility":"{\"id\":\"7b27731a3363efcb6b0520962d544471745aae15664920dffe690b4fdb410d80\",\"parent\":\"57a0a421f1acbc1fe6b88b32d3d1c3c0388ff1958b97f95dd0e3a599b810499b\",\"created\":\"2016-09-23T19:16:41.037788416Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ENV PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"]},\"throwaway\":true}"},{"v1Compatibility":"{\"id\":\"57a0a421f1acbc1fe6b88b32d3d1c3c0388ff1958b97f95dd0e3a599b810499b\",\"parent\":\"faeaf6fdfdcbb18d68c12db9683a02428bab83962a493de88b4c7b1ec941db8f\",\"created\":\"2016-09-23T19:16:40.725768956Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ENV HTTPD_PREFIX=/usr/local/apache2\"]},\"throwaway\":true}"},{"v1Compatibility":"{\"id\":\"faeaf6fdfdcbb18d68c12db9683a02428bab83962a493de88b4c7b1ec941db8f\",\"parent\":\"d0c4f1eb7dc8f4dae2b45fe5c0cf4cfc70e5be85d933f5f5f4deb59f134fb520\",\"created\":\"2016-09-23T18:08:51.133779867Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) CMD [\\\"/bin/bash\\\"]\"]},\"throwaway\":true}"},{"v1Compatibility":"{\"id\":\"d0c4f1eb7dc8f4dae2b45fe5c0cf4cfc70e5be85d933f5f5f4deb59f134fb520\",\"created\":\"2016-09-23T18:08:50.537223822Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ADD file:c6c23585ab140b0b320d4e99bc1b0eb544c9e96c24d90fec5e069a6d57d335ca in / \"]}}"}],"schemaVersion":1,"signatures":[{"header":{"jwk":{"crv":"P-256","kid":"Q3ZE:RPLV:YRWL:CJGY:3YUV:CVRB:KOZN:DPRO:QZKD:B7KB:4FJ5:XUDM","kty":"EC","x":"iIJKPTtzobd73WmVmIoRSGkHWQB86bL5BBw9-YWcVjA","y":"3fiBd8u2fXRc5DZG20gQWQ8LUvTuPRqkU0e672ymn-8"},"alg":"ES256"},"signature":"I1uNEFT2P64rwc7dajzBOCD9o4DB4W7xbWQRxOgWm43Py1_N3omkvqUStMeUQsQVjNqje6NQyVzQzOACDHsPYg","protected":"eyJmb3JtYXRMZW5ndGgiOjEwMTEyLCJmb3JtYXRUYWlsIjoiZlEiLCJ0aW1lIjoiMjAyMC0wMy0yNVQyMzo0NzowOVoifQ"}]} diff --git a/vendor/github.com/containers/image/v5/internal/image/fixtures/oci1-to-schema2-config.json b/vendor/github.com/containers/image/v5/internal/image/fixtures/oci1-to-schema2-config.json new file mode 100644 index 00000000000..f49230ea77d --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/image/fixtures/oci1-to-schema2-config.json @@ -0,0 +1 @@ +{"architecture":"amd64","config":{"Hostname":"383850eeb47b","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"ExposedPorts":{"80/tcp":{}},"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","HTTPD_PREFIX=/usr/local/apache2","HTTPD_VERSION=2.4.23","HTTPD_SHA1=5101be34ac4a509b245adb70a56690a84fcc4e7f","HTTPD_BZ2_URL=https://www.apache.org/dyn/closer.cgi?action=download\u0026filename=httpd/httpd-2.4.23.tar.bz2","HTTPD_ASC_URL=https://www.apache.org/dist/httpd/httpd-2.4.23.tar.bz2.asc"],"Cmd":["httpd-foreground"],"ArgsEscaped":true,"Image":"sha256:4f83530449c67c1ed8fca72583c5b92fdf446010990028c362a381e55dd84afd","Volumes":null,"WorkingDir":"/usr/local/apache2","Entrypoint":null,"OnBuild":[],"Labels":{}},"container":"8825acde1b009729807e4b70a65a89399dd8da8e53be9216b9aaabaff4339f69","container_config":{"Hostname":"383850eeb47b","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"ExposedPorts":{"80/tcp":{}},"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","HTTPD_PREFIX=/usr/local/apache2","HTTPD_VERSION=2.4.23","HTTPD_SHA1=5101be34ac4a509b245adb70a56690a84fcc4e7f","HTTPD_BZ2_URL=https://www.apache.org/dyn/closer.cgi?action=download\u0026filename=httpd/httpd-2.4.23.tar.bz2","HTTPD_ASC_URL=https://www.apache.org/dist/httpd/httpd-2.4.23.tar.bz2.asc"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\"httpd-foreground\"]"],"ArgsEscaped":true,"Image":"sha256:4f83530449c67c1ed8fca72583c5b92fdf446010990028c362a381e55dd84afd","Volumes":null,"WorkingDir":"/usr/local/apache2","Entrypoint":null,"OnBuild":[],"Labels":{}},"created":"2016-09-23T23:20:45.78976459Z","docker_version":"1.12.1","history":[{"created":"2016-09-23T18:08:50.537223822Z","created_by":"/bin/sh -c #(nop) ADD file:c6c23585ab140b0b320d4e99bc1b0eb544c9e96c24d90fec5e069a6d57d335ca in / "},{"created":"2016-09-23T18:08:51.133779867Z","created_by":"/bin/sh -c #(nop) CMD [\"/bin/bash\"]","empty_layer":true},{"created":"2016-09-23T19:16:40.725768956Z","created_by":"/bin/sh -c #(nop) ENV HTTPD_PREFIX=/usr/local/apache2","empty_layer":true},{"created":"2016-09-23T19:16:41.037788416Z","created_by":"/bin/sh -c #(nop) ENV PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","empty_layer":true},{"created":"2016-09-23T19:16:41.990121202Z","created_by":"/bin/sh -c mkdir -p \"$HTTPD_PREFIX\" \t\u0026\u0026 chown www-data:www-data \"$HTTPD_PREFIX\""},{"created":"2016-09-23T19:16:42.339911155Z","created_by":"/bin/sh -c #(nop) WORKDIR /usr/local/apache2","empty_layer":true},{"created":"2016-09-23T19:16:54.948461741Z","created_by":"/bin/sh -c apt-get update \t\u0026\u0026 apt-get install -y --no-install-recommends \t\tlibapr1 \t\tlibaprutil1 \t\tlibaprutil1-ldap \t\tlibapr1-dev \t\tlibaprutil1-dev \t\tlibpcre++0 \t\tlibssl1.0.0 \t\u0026\u0026 rm -r /var/lib/apt/lists/*"},{"created":"2016-09-23T19:16:55.321573403Z","created_by":"/bin/sh -c #(nop) ENV HTTPD_VERSION=2.4.23","empty_layer":true},{"created":"2016-09-23T19:16:55.629947307Z","created_by":"/bin/sh -c #(nop) ENV HTTPD_SHA1=5101be34ac4a509b245adb70a56690a84fcc4e7f","empty_layer":true},{"created":"2016-09-23T23:19:03.705796801Z","created_by":"/bin/sh -c #(nop) ENV HTTPD_BZ2_URL=https://www.apache.org/dyn/closer.cgi?action=download\u0026filename=httpd/httpd-2.4.23.tar.bz2","empty_layer":true},{"created":"2016-09-23T23:19:04.009782822Z","created_by":"/bin/sh -c #(nop) ENV HTTPD_ASC_URL=https://www.apache.org/dist/httpd/httpd-2.4.23.tar.bz2.asc","empty_layer":true},{"created":"2016-09-23T23:20:44.585743332Z","created_by":"/bin/sh -c set -x \t\u0026\u0026 buildDeps=' \t\tbzip2 \t\tca-certificates \t\tgcc \t\tlibpcre++-dev \t\tlibssl-dev \t\tmake \t\twget \t' \t\u0026\u0026 apt-get update \t\u0026\u0026 apt-get install -y --no-install-recommends $buildDeps \t\u0026\u0026 rm -r /var/lib/apt/lists/* \t\t\u0026\u0026 wget -O httpd.tar.bz2 \"$HTTPD_BZ2_URL\" \t\u0026\u0026 echo \"$HTTPD_SHA1 *httpd.tar.bz2\" | sha1sum -c - \t\u0026\u0026 wget -O httpd.tar.bz2.asc \"$HTTPD_ASC_URL\" \t\u0026\u0026 export GNUPGHOME=\"$(mktemp -d)\" \t\u0026\u0026 gpg --keyserver ha.pool.sks-keyservers.net --recv-keys A93D62ECC3C8EA12DB220EC934EA76E6791485A8 \t\u0026\u0026 gpg --batch --verify httpd.tar.bz2.asc httpd.tar.bz2 \t\u0026\u0026 rm -r \"$GNUPGHOME\" httpd.tar.bz2.asc \t\t\u0026\u0026 mkdir -p src \t\u0026\u0026 tar -xvf httpd.tar.bz2 -C src --strip-components=1 \t\u0026\u0026 rm httpd.tar.bz2 \t\u0026\u0026 cd src \t\t\u0026\u0026 ./configure \t\t--prefix=\"$HTTPD_PREFIX\" \t\t--enable-mods-shared=reallyall \t\u0026\u0026 make -j\"$(nproc)\" \t\u0026\u0026 make install \t\t\u0026\u0026 cd .. \t\u0026\u0026 rm -r src \t\t\u0026\u0026 sed -ri \t\t-e 's!^(\\s*CustomLog)\\s+\\S+!\\1 /proc/self/fd/1!g' \t\t-e 's!^(\\s*ErrorLog)\\s+\\S+!\\1 /proc/self/fd/2!g' \t\t\"$HTTPD_PREFIX/conf/httpd.conf\" \t\t\u0026\u0026 apt-get purge -y --auto-remove $buildDeps"},{"created":"2016-09-23T23:20:45.127455562Z","created_by":"/bin/sh -c #(nop) COPY file:761e313354b918b6cd7ea99975a4f6b53ff5381ba689bab2984aec4dab597215 in /usr/local/bin/ "},{"created":"2016-09-23T23:20:45.453934921Z","created_by":"/bin/sh -c #(nop) EXPOSE 80/tcp","empty_layer":true},{"created":"2016-09-23T23:20:45.78976459Z","created_by":"/bin/sh -c #(nop) CMD [\"httpd-foreground\"]","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:142a601d97936307e75220c35dde0348971a9584c21e7cb42e1f7004005432ab","sha256:90fcc66ad3be9f1757f954b750deb37032f208428aa12599fcb02182b9065a9c","sha256:5a8624bb7e76d1e6829f9c64c43185e02bc07f97a2189eb048609a8914e72c56","sha256:d349ff6b3afc6a2800054768c82bfbf4289c9aa5da55c1290f802943dcd4d1e9","sha256:8c064bb1f60e84fa8cc6079b6d2e76e0423389fd6aeb7e497dfdae5e05b2b25b"]}} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/internal/image/fixtures/oci1-to-schema2.json b/vendor/github.com/containers/image/v5/internal/image/fixtures/oci1-to-schema2.json new file mode 100644 index 00000000000..50aa6dc06c4 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/image/fixtures/oci1-to-schema2.json @@ -0,0 +1,37 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 5940, + "digest": "sha256:9ca4bda0a6b3727a6ffcc43e981cad0f24e2ec79d338f6ba325b4dfd0756fb8f" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 51354364, + "digest": "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 150, + "digest": "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 11739507, + "digest": "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9", + "urls": ["https://layer.url"] + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 8841833, + "digest": "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 291, + "digest": "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa" + } + ] +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/internal/image/fixtures/oci1.json b/vendor/github.com/containers/image/v5/internal/image/fixtures/oci1.json new file mode 100644 index 00000000000..26efc23db78 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/image/fixtures/oci1.json @@ -0,0 +1,43 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 5940, + "digest": "sha256:9ca4bda0a6b3727a6ffcc43e981cad0f24e2ec79d338f6ba325b4dfd0756fb8f", + "annotations": { + "test-annotation-1": "one" + } + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 51354364, + "digest": "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 150, + "digest": "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 11739507, + "digest": "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9", + "urls": ["https://layer.url"] + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 8841833, + "digest": "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909", + "annotations": { + "test-annotation-2": "two" + } + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 291, + "digest": "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa" + } + ] +} diff --git a/vendor/github.com/containers/image/v5/internal/image/fixtures/schema1-for-oci-config.json b/vendor/github.com/containers/image/v5/internal/image/fixtures/schema1-for-oci-config.json new file mode 100644 index 00000000000..ee5825794ae --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/image/fixtures/schema1-for-oci-config.json @@ -0,0 +1,29 @@ +{ + "schemaVersion": 1, + "name": "google_containers/pause-amd64", + "tag": "3.0", + "architecture": "amd64", + "fsLayers": [ + { + "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" + }, + { + "blobSum": "sha256:f112334343777b75be77ec1f835e3bbbe7d7bd46e27b6a2ae35c6b3cfea0987c" + }, + { + "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" + } + ], + "history": [ + { + "v1Compatibility": "{\"id\":\"bb497e16a2d55195649174d1fadac52b00fa2c14124d73009712606909286bc5\",\"parent\":\"f8e2eec424cf985b4e41d6423991433fb7a93c90f9acc73a5e7bee213b789c52\",\"created\":\"2016-05-04T06:26:41.522308365Z\",\"container\":\"a9873535145fe72b464d3055efbac36aab70d059914e221cbbd7fe3cac53ef6b\",\"container_config\":{\"Hostname\":\"95722352e41d\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ENTRYPOINT \\u0026{[\\\"/pause\\\"]}\"],\"Image\":\"f8e2eec424cf985b4e41d6423991433fb7a93c90f9acc73a5e7bee213b789c52\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":[\"/pause\"],\"OnBuild\":null,\"Labels\":{}},\"docker_version\":\"1.9.1\",\"config\":{\"Hostname\":\"95722352e41d\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"f8e2eec424cf985b4e41d6423991433fb7a93c90f9acc73a5e7bee213b789c52\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":[\"/pause\"],\"OnBuild\":null,\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\"}" + }, + { + "v1Compatibility": "{\"id\":\"f8e2eec424cf985b4e41d6423991433fb7a93c90f9acc73a5e7bee213b789c52\",\"parent\":\"bdb43c586e887b513a056722b50553727b255e3a3d9166f318632d4209963464\",\"created\":\"2016-05-04T06:26:41.091672218Z\",\"container\":\"e1b38778b023f25642273ed9e7f4846b4bf38b22a8b55755880b2e6ab6019811\",\"container_config\":{\"Hostname\":\"95722352e41d\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ADD file:b7eb6a5df9d5fbe509cac16ed89f8d6513a4362017184b14c6a5fae151eee5c5 in /pause\"],\"Image\":\"bdb43c586e887b513a056722b50553727b255e3a3d9166f318632d4209963464\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"docker_version\":\"1.9.1\",\"config\":{\"Hostname\":\"95722352e41d\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"bdb43c586e887b513a056722b50553727b255e3a3d9166f318632d4209963464\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":746888}" + }, + { + "v1Compatibility": "{\"id\":\"bdb43c586e887b513a056722b50553727b255e3a3d9166f318632d4209963464\",\"created\":\"2016-05-04T06:26:40.628395649Z\",\"container\":\"95722352e41d57660259fbede4413d06889a28eb07a7302d2a7b3f9c71ceaa46\",\"container_config\":{\"Hostname\":\"95722352e41d\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) ARG ARCH\"],\"Image\":\"\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.9.1\",\"config\":{\"Hostname\":\"95722352e41d\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\"}" + } + ],"signatures":[{"header":{"alg":"ES256","jwk":{"crv":"P-256","kid":"ORN4:M47W:3KP3:TZRZ:C3UF:5MFQ:INZV:TCMY:LHNV:EYQU:IRGJ:IJLJ","kty":"EC","x":"yJ0ZQ19NBZUQn8LV60sFEabhlgky9svozfK0VGVou7Y","y":"gOJScOkkLVY1f8aAx-6XXpVM5rJaDYLkCNJ1dvcQGMs"}},"protected":"eyJmb3JtYXRMZW5ndGgiOjQxMzMsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNi0wNS0wNFQwNjoyODo1MVoifQ","signature":"77_7DVx1IZ3PiKNnO7QnvoF7Sgik4GI4bnlVJdtQW461dSyYzd-nSdBmky8Jew3InEW8Cuv_t5w4GmOSwXvL7g"}] + +} diff --git a/vendor/github.com/containers/image/v5/internal/image/fixtures/schema1-to-oci1-config.json b/vendor/github.com/containers/image/v5/internal/image/fixtures/schema1-to-oci1-config.json new file mode 100644 index 00000000000..950e2252cd9 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/image/fixtures/schema1-to-oci1-config.json @@ -0,0 +1,82 @@ +{ + "architecture": "amd64", + "config": { + "User": "nova", + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "container=oci", + "KOLLA_BASE_DISTRO=rhel", + "KOLLA_INSTALL_TYPE=binary", + "KOLLA_INSTALL_METATYPE=rhos", + "PS1=$(tput bold)($(printenv KOLLA_SERVICE_NAME))$(tput sgr0)[$(id -un)@$(hostname -s) $(pwd)]$ " + ], + "Cmd": [ + "kolla_start" + ], + "Labels": { + "Kolla-SHA": "5.0.0-39-g6f1b947b", + "architecture": "x86_64", + "authoritative-source-url": "registry.access.redhat.com", + "build-date": "2018-01-25T00:32:27.807261", + "com.redhat.build-host": "ip-10-29-120-186.ec2.internal", + "com.redhat.component": "openstack-nova-api-docker", + "description": "Red Hat OpenStack Platform 12.0 nova-api", + "distribution-scope": "public", + "io.k8s.description": "Red Hat OpenStack Platform 12.0 nova-api", + "io.k8s.display-name": "Red Hat OpenStack Platform 12.0 nova-api", + "io.openshift.tags": "rhosp osp openstack osp-12.0", + "kolla_version": "stable/pike", + "name": "rhosp12/openstack-nova-api", + "release": "20180124.1", + "summary": "Red Hat OpenStack Platform 12.0 nova-api", + "tripleo-common_version": "7.6.3-23-g4891cfe", + "url": "https://access.redhat.com/containers/#/registry.access.redhat.com/rhosp12/openstack-nova-api/images/12.0-20180124.1", + "vcs-ref": "9b31243b7b448eb2fc3b6e2c96935b948f806e98", + "vcs-type": "git", + "vendor": "Red Hat, Inc.", + "version": "12.0", + "version-release": "12.0-20180124.1" + }, + "ArgsEscaped": true + }, + "created": "2018-01-25T00:37:48.268558Z", + "os": "linux", + "history": [ + { + "comment": "Imported from -", + "created": "2017-11-21T16:47:27.755341705Z" + }, + { + "author": "Red Hat, Inc.", + "created": "2017-11-21T16:49:37.292899Z", + "created_by": "/bin/sh -c rm -f '/etc/yum.repos.d/compose-rpms-1.repo'" + }, + { + "created": "2018-01-24T21:40:32.494686Z", + "created_by": "/bin/sh -c rm -f '/etc/yum.repos.d/rhel-7.4.repo' '/etc/yum.repos.d/rhos-optools-12.0.repo' '/etc/yum.repos.d/rhos-12.0-container-yum-need_images.repo'" + }, + { + "created": "2018-01-24T22:00:57.807862Z", + "created_by": "/bin/sh -c rm -f '/etc/yum.repos.d/rhel-7.4.repo' '/etc/yum.repos.d/rhos-optools-12.0.repo' '/etc/yum.repos.d/rhos-12.0-container-yum-need_images.repo'" + }, + { + "created": "2018-01-24T23:08:25.300741Z", + "created_by": "/bin/sh -c rm -f '/etc/yum.repos.d/rhel-7.4.repo' '/etc/yum.repos.d/rhos-optools-12.0.repo' '/etc/yum.repos.d/rhos-12.0-container-yum-need_images.repo'" + }, + { + "created": "2018-01-25T00:37:48.268558Z", + "created_by": "/bin/sh -c #(nop) USER [nova]" + } + ], + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:e1d829eddb62dc49f1c56dbf8acd0c71299b3996115399de853a9d66d81b822f", + "sha256:02404b4d7e5d89b1383ca346b4462b199128aa4b238c5a2b2c186004ac148ba8", + "sha256:45fad80a4b1cec165c421eb570dec312d825bd8fac362e255028fa3f2169148d", + "sha256:7ddef8efd44586e54880ec4797458eac87b368544c438d7e7c63fbc0d9a7ae97", + "sha256:b56b16b6407ba1b86252e7e50f98f142cf6844fab42e4495d56ebb7ce559e2af", + "sha256:9bd63850e406167b4751f5050f6dc0ebd789bb5ef5e5c6c31ed062bda8c063e8" + ] + } +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/internal/image/fixtures/schema1-to-oci1.json b/vendor/github.com/containers/image/v5/internal/image/fixtures/schema1-to-oci1.json new file mode 100644 index 00000000000..0af1fbecf4f --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/image/fixtures/schema1-to-oci1.json @@ -0,0 +1,41 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": -1, + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 74876245, + "digest": "sha256:9cadd93b16ff2a0c51ac967ea2abfadfac50cfa3af8b5bf983d89b8f8647f3e4" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 1239, + "digest": "sha256:4aa565ad8b7a87248163ce7dba1dd3894821aac97e846b932ff6b8ef9a8a508a" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 78339724, + "digest": "sha256:f576d102e09b9eef0e305aaef705d2d43a11bebc3fd5810a761624bd5e11997e" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 76857203, + "digest": "sha256:9e92df2aea7dc0baf5f1f8d509678d6a6306de27ad06513f8e218371938c07a6" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 25923380, + "digest": "sha256:62e48e39dc5b30b75a97f05bccc66efbae6058b860ee20a5c9a184b9d5e25788" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 23511300, + "digest": "sha256:e623934bca8d1a74f51014256445937714481e49343a31bda2bc5f534748184d" + } + ] +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/internal/image/fixtures/schema1-to-schema2-config.json b/vendor/github.com/containers/image/v5/internal/image/fixtures/schema1-to-schema2-config.json new file mode 100644 index 00000000000..c182ded35b2 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/image/fixtures/schema1-to-schema2-config.json @@ -0,0 +1,163 @@ +{ + "architecture": "amd64", + "config": { + "Hostname": "9428cdea83ba", + "Domainname": "", + "User": "nova", + "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", + "container=oci", + "KOLLA_BASE_DISTRO=rhel", + "KOLLA_INSTALL_TYPE=binary", + "KOLLA_INSTALL_METATYPE=rhos", + "PS1=$(tput bold)($(printenv KOLLA_SERVICE_NAME))$(tput sgr0)[$(id -un)@$(hostname -s) $(pwd)]$ " + ], + "Cmd": [ + "kolla_start" + ], + "Healthcheck": { + "Test": [ + "CMD-SHELL", + "/openstack/healthcheck" + ] + }, + "ArgsEscaped": true, + "Image": "3bf9afe371220b1eb1c57bec39b5a99ba976c36c92d964a1c014584f95f51e33", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": [], + "Labels": { + "Kolla-SHA": "5.0.0-39-g6f1b947b", + "architecture": "x86_64", + "authoritative-source-url": "registry.access.redhat.com", + "build-date": "2018-01-25T00:32:27.807261", + "com.redhat.build-host": "ip-10-29-120-186.ec2.internal", + "com.redhat.component": "openstack-nova-api-docker", + "description": "Red Hat OpenStack Platform 12.0 nova-api", + "distribution-scope": "public", + "io.k8s.description": "Red Hat OpenStack Platform 12.0 nova-api", + "io.k8s.display-name": "Red Hat OpenStack Platform 12.0 nova-api", + "io.openshift.tags": "rhosp osp openstack osp-12.0", + "kolla_version": "stable/pike", + "name": "rhosp12/openstack-nova-api", + "release": "20180124.1", + "summary": "Red Hat OpenStack Platform 12.0 nova-api", + "tripleo-common_version": "7.6.3-23-g4891cfe", + "url": "https://access.redhat.com/containers/#/registry.access.redhat.com/rhosp12/openstack-nova-api/images/12.0-20180124.1", + "vcs-ref": "9b31243b7b448eb2fc3b6e2c96935b948f806e98", + "vcs-type": "git", + "vendor": "Red Hat, Inc.", + "version": "12.0", + "version-release": "12.0-20180124.1" + } + }, + "container_config": { + "Hostname": "9428cdea83ba", + "Domainname": "", + "User": "nova", + "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", + "container=oci", + "KOLLA_BASE_DISTRO=rhel", + "KOLLA_INSTALL_TYPE=binary", + "KOLLA_INSTALL_METATYPE=rhos", + "PS1=$(tput bold)($(printenv KOLLA_SERVICE_NAME))$(tput sgr0)[$(id -un)@$(hostname -s) $(pwd)]$ " + ], + "Cmd": [ + "/bin/sh", + "-c", + "#(nop) ", + "USER [nova]" + ], + "Healthcheck": { + "Test": [ + "CMD-SHELL", + "/openstack/healthcheck" + ] + }, + "ArgsEscaped": true, + "Image": "sha256:274ce4dcbeb09fa173a5d50203ae5cec28f456d1b8b59477b47a42bd74d068bf", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": [], + "Labels": { + "Kolla-SHA": "5.0.0-39-g6f1b947b", + "architecture": "x86_64", + "authoritative-source-url": "registry.access.redhat.com", + "build-date": "2018-01-25T00:32:27.807261", + "com.redhat.build-host": "ip-10-29-120-186.ec2.internal", + "com.redhat.component": "openstack-nova-api-docker", + "description": "Red Hat OpenStack Platform 12.0 nova-api", + "distribution-scope": "public", + "io.k8s.description": "Red Hat OpenStack Platform 12.0 nova-api", + "io.k8s.display-name": "Red Hat OpenStack Platform 12.0 nova-api", + "io.openshift.tags": "rhosp osp openstack osp-12.0", + "kolla_version": "stable/pike", + "name": "rhosp12/openstack-nova-api", + "release": "20180124.1", + "summary": "Red Hat OpenStack Platform 12.0 nova-api", + "tripleo-common_version": "7.6.3-23-g4891cfe", + "url": "https://access.redhat.com/containers/#/registry.access.redhat.com/rhosp12/openstack-nova-api/images/12.0-20180124.1", + "vcs-ref": "9b31243b7b448eb2fc3b6e2c96935b948f806e98", + "vcs-type": "git", + "vendor": "Red Hat, Inc.", + "version": "12.0", + "version-release": "12.0-20180124.1" + } + }, + "created": "2018-01-25T00:37:48.268558Z", + "docker_version": "1.12.6", + "os": "linux", + "history": [ + { + "comment": "Imported from -", + "created": "2017-11-21T16:47:27.755341705Z" + }, + { + "author": "Red Hat, Inc.", + "created": "2017-11-21T16:49:37.292899Z", + "created_by": "/bin/sh -c rm -f '/etc/yum.repos.d/compose-rpms-1.repo'" + }, + { + "created": "2018-01-24T21:40:32.494686Z", + "created_by": "/bin/sh -c rm -f '/etc/yum.repos.d/rhel-7.4.repo' '/etc/yum.repos.d/rhos-optools-12.0.repo' '/etc/yum.repos.d/rhos-12.0-container-yum-need_images.repo'" + }, + { + "created": "2018-01-24T22:00:57.807862Z", + "created_by": "/bin/sh -c rm -f '/etc/yum.repos.d/rhel-7.4.repo' '/etc/yum.repos.d/rhos-optools-12.0.repo' '/etc/yum.repos.d/rhos-12.0-container-yum-need_images.repo'" + }, + { + "created": "2018-01-24T23:08:25.300741Z", + "created_by": "/bin/sh -c rm -f '/etc/yum.repos.d/rhel-7.4.repo' '/etc/yum.repos.d/rhos-optools-12.0.repo' '/etc/yum.repos.d/rhos-12.0-container-yum-need_images.repo'" + }, + { + "created": "2018-01-25T00:37:48.268558Z", + "created_by": "/bin/sh -c #(nop) USER [nova]" + } + ], + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:e1d829eddb62dc49f1c56dbf8acd0c71299b3996115399de853a9d66d81b822f", + "sha256:02404b4d7e5d89b1383ca346b4462b199128aa4b238c5a2b2c186004ac148ba8", + "sha256:45fad80a4b1cec165c421eb570dec312d825bd8fac362e255028fa3f2169148d", + "sha256:7ddef8efd44586e54880ec4797458eac87b368544c438d7e7c63fbc0d9a7ae97", + "sha256:b56b16b6407ba1b86252e7e50f98f142cf6844fab42e4495d56ebb7ce559e2af", + "sha256:9bd63850e406167b4751f5050f6dc0ebd789bb5ef5e5c6c31ed062bda8c063e8" + ] + } +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/internal/image/fixtures/schema1-to-schema2.json b/vendor/github.com/containers/image/v5/internal/image/fixtures/schema1-to-schema2.json new file mode 100644 index 00000000000..9d6feee2389 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/image/fixtures/schema1-to-schema2.json @@ -0,0 +1,41 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": -1, + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 74876245, + "digest": "sha256:9cadd93b16ff2a0c51ac967ea2abfadfac50cfa3af8b5bf983d89b8f8647f3e4" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 1239, + "digest": "sha256:4aa565ad8b7a87248163ce7dba1dd3894821aac97e846b932ff6b8ef9a8a508a" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 78339724, + "digest": "sha256:f576d102e09b9eef0e305aaef705d2d43a11bebc3fd5810a761624bd5e11997e" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 76857203, + "digest": "sha256:9e92df2aea7dc0baf5f1f8d509678d6a6306de27ad06513f8e218371938c07a6" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 25923380, + "digest": "sha256:62e48e39dc5b30b75a97f05bccc66efbae6058b860ee20a5c9a184b9d5e25788" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 23511300, + "digest": "sha256:e623934bca8d1a74f51014256445937714481e49343a31bda2bc5f534748184d" + } + ] +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/internal/image/fixtures/schema1.json b/vendor/github.com/containers/image/v5/internal/image/fixtures/schema1.json new file mode 100644 index 00000000000..d74114906a9 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/image/fixtures/schema1.json @@ -0,0 +1,62 @@ +{ + "schemaVersion": 1, + "name": "rhosp12/openstack-nova-api", + "tag": "latest", + "architecture": "amd64", + "fsLayers": [ + { + "blobSum": "sha256:e623934bca8d1a74f51014256445937714481e49343a31bda2bc5f534748184d" + }, + { + "blobSum": "sha256:62e48e39dc5b30b75a97f05bccc66efbae6058b860ee20a5c9a184b9d5e25788" + }, + { + "blobSum": "sha256:9e92df2aea7dc0baf5f1f8d509678d6a6306de27ad06513f8e218371938c07a6" + }, + { + "blobSum": "sha256:f576d102e09b9eef0e305aaef705d2d43a11bebc3fd5810a761624bd5e11997e" + }, + { + "blobSum": "sha256:4aa565ad8b7a87248163ce7dba1dd3894821aac97e846b932ff6b8ef9a8a508a" + }, + { + "blobSum": "sha256:9cadd93b16ff2a0c51ac967ea2abfadfac50cfa3af8b5bf983d89b8f8647f3e4" + } + ], + "history": [ + { + "v1Compatibility": "{\"architecture\":\"amd64\",\"config\":{\"Hostname\":\"9428cdea83ba\",\"Domainname\":\"\",\"User\":\"nova\",\"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\",\"container=oci\",\"KOLLA_BASE_DISTRO=rhel\",\"KOLLA_INSTALL_TYPE=binary\",\"KOLLA_INSTALL_METATYPE=rhos\",\"PS1=$(tput bold)($(printenv KOLLA_SERVICE_NAME))$(tput sgr0)[$(id -un)@$(hostname -s) $(pwd)]$ \"],\"Cmd\":[\"kolla_start\"],\"Healthcheck\":{\"Test\":[\"CMD-SHELL\",\"/openstack/healthcheck\"]},\"ArgsEscaped\":true,\"Image\":\"3bf9afe371220b1eb1c57bec39b5a99ba976c36c92d964a1c014584f95f51e33\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":[],\"Labels\":{\"Kolla-SHA\":\"5.0.0-39-g6f1b947b\",\"architecture\":\"x86_64\",\"authoritative-source-url\":\"registry.access.redhat.com\",\"build-date\":\"2018-01-25T00:32:27.807261\",\"com.redhat.build-host\":\"ip-10-29-120-186.ec2.internal\",\"com.redhat.component\":\"openstack-nova-api-docker\",\"description\":\"Red Hat OpenStack Platform 12.0 nova-api\",\"distribution-scope\":\"public\",\"io.k8s.description\":\"Red Hat OpenStack Platform 12.0 nova-api\",\"io.k8s.display-name\":\"Red Hat OpenStack Platform 12.0 nova-api\",\"io.openshift.tags\":\"rhosp osp openstack osp-12.0\",\"kolla_version\":\"stable/pike\",\"name\":\"rhosp12/openstack-nova-api\",\"release\":\"20180124.1\",\"summary\":\"Red Hat OpenStack Platform 12.0 nova-api\",\"tripleo-common_version\":\"7.6.3-23-g4891cfe\",\"url\":\"https://access.redhat.com/containers/#/registry.access.redhat.com/rhosp12/openstack-nova-api/images/12.0-20180124.1\",\"vcs-ref\":\"9b31243b7b448eb2fc3b6e2c96935b948f806e98\",\"vcs-type\":\"git\",\"vendor\":\"Red Hat, Inc.\",\"version\":\"12.0\",\"version-release\":\"12.0-20180124.1\"}},\"container_config\":{\"Hostname\":\"9428cdea83ba\",\"Domainname\":\"\",\"User\":\"nova\",\"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\",\"container=oci\",\"KOLLA_BASE_DISTRO=rhel\",\"KOLLA_INSTALL_TYPE=binary\",\"KOLLA_INSTALL_METATYPE=rhos\",\"PS1=$(tput bold)($(printenv KOLLA_SERVICE_NAME))$(tput sgr0)[$(id -un)@$(hostname -s) $(pwd)]$ \"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) \",\"USER [nova]\"],\"Healthcheck\":{\"Test\":[\"CMD-SHELL\",\"/openstack/healthcheck\"]},\"ArgsEscaped\":true,\"Image\":\"sha256:274ce4dcbeb09fa173a5d50203ae5cec28f456d1b8b59477b47a42bd74d068bf\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":[],\"Labels\":{\"Kolla-SHA\":\"5.0.0-39-g6f1b947b\",\"architecture\":\"x86_64\",\"authoritative-source-url\":\"registry.access.redhat.com\",\"build-date\":\"2018-01-25T00:32:27.807261\",\"com.redhat.build-host\":\"ip-10-29-120-186.ec2.internal\",\"com.redhat.component\":\"openstack-nova-api-docker\",\"description\":\"Red Hat OpenStack Platform 12.0 nova-api\",\"distribution-scope\":\"public\",\"io.k8s.description\":\"Red Hat OpenStack Platform 12.0 nova-api\",\"io.k8s.display-name\":\"Red Hat OpenStack Platform 12.0 nova-api\",\"io.openshift.tags\":\"rhosp osp openstack osp-12.0\",\"kolla_version\":\"stable/pike\",\"name\":\"rhosp12/openstack-nova-api\",\"release\":\"20180124.1\",\"summary\":\"Red Hat OpenStack Platform 12.0 nova-api\",\"tripleo-common_version\":\"7.6.3-23-g4891cfe\",\"url\":\"https://access.redhat.com/containers/#/registry.access.redhat.com/rhosp12/openstack-nova-api/images/12.0-20180124.1\",\"vcs-ref\":\"9b31243b7b448eb2fc3b6e2c96935b948f806e98\",\"vcs-type\":\"git\",\"vendor\":\"Red Hat, Inc.\",\"version\":\"12.0\",\"version-release\":\"12.0-20180124.1\"}},\"created\":\"2018-01-25T00:37:48.268558Z\",\"docker_version\":\"1.12.6\",\"id\":\"486cbbaf6c6f7d890f9368c86eda3f4ebe3ae982b75098037eb3c3cc6f0e0cdf\",\"os\":\"linux\",\"parent\":\"20d0c9c79f9fee83c4094993335b9b321112f13eef60ed9ec1599c7593dccf20\"}" + }, + { + "v1Compatibility": "{\"id\":\"20d0c9c79f9fee83c4094993335b9b321112f13eef60ed9ec1599c7593dccf20\",\"parent\":\"47a1014db2116c312736e11adcc236fb77d0ad32457f959cbaec0c3fc9ab1caa\",\"created\":\"2018-01-24T23:08:25.300741Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c rm -f '/etc/yum.repos.d/rhel-7.4.repo' '/etc/yum.repos.d/rhos-optools-12.0.repo' '/etc/yum.repos.d/rhos-12.0-container-yum-need_images.repo'\"]}}" + }, + { + "v1Compatibility": "{\"id\":\"47a1014db2116c312736e11adcc236fb77d0ad32457f959cbaec0c3fc9ab1caa\",\"parent\":\"cec66cab6c92a5f7b50ef407b80b83840a0d089b9896257609fd01de3a595824\",\"created\":\"2018-01-24T22:00:57.807862Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c rm -f '/etc/yum.repos.d/rhel-7.4.repo' '/etc/yum.repos.d/rhos-optools-12.0.repo' '/etc/yum.repos.d/rhos-12.0-container-yum-need_images.repo'\"]}}" + }, + { + "v1Compatibility": "{\"id\":\"cec66cab6c92a5f7b50ef407b80b83840a0d089b9896257609fd01de3a595824\",\"parent\":\"0e7730eccb3d014b33147b745d771bc0e38a967fd932133a6f5325a3c84282e2\",\"created\":\"2018-01-24T21:40:32.494686Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c rm -f '/etc/yum.repos.d/rhel-7.4.repo' '/etc/yum.repos.d/rhos-optools-12.0.repo' '/etc/yum.repos.d/rhos-12.0-container-yum-need_images.repo'\"]}}" + }, + { + "v1Compatibility": "{\"id\":\"0e7730eccb3d014b33147b745d771bc0e38a967fd932133a6f5325a3c84282e2\",\"parent\":\"3e49094c0233214ab73f8e5c204af8a14cfc6f0403384553c17fbac2e9d38345\",\"created\":\"2017-11-21T16:49:37.292899Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c rm -f '/etc/yum.repos.d/compose-rpms-1.repo'\"]},\"author\":\"Red Hat, Inc.\"}" + }, + { + "v1Compatibility": "{\"id\":\"3e49094c0233214ab73f8e5c204af8a14cfc6f0403384553c17fbac2e9d38345\",\"comment\":\"Imported from -\",\"created\":\"2017-11-21T16:47:27.755341705Z\",\"container_config\":{\"Cmd\":[\"\"]}}" + } + ], + "signatures": [ + { + "header": { + "jwk": { + "crv": "P-256", + "kid": "DB2X:GSG2:72H3:AE3R:KCMI:Y77E:W7TF:ERHK:V5HR:JJ2Y:YMS6:HFGJ", + "kty": "EC", + "x": "jyr9-xZBorSC9fhqNsmfU_Ud31wbaZ-bVGz0HmySvbQ", + "y": "vkE6qZCCvYRWjSUwgAOvibQx_s8FipYkAiHS0VnAFNs" + }, + "alg": "ES256" + }, + "signature": "jBBsnocfxw77LzmM_VeN6Nb031BtqPgx-DbppYOEnhZfGLRcyYwGUPW--3JrkeEX6AlEGzPI57R0tlu5bZvrnQ", + "protected": "eyJmb3JtYXRMZW5ndGgiOjY4MTMsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxOC0wMS0zMFQxOToyNToxMloifQ" + } + ] +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/internal/image/fixtures/schema2-all-media-types-to-oci1.json b/vendor/github.com/containers/image/v5/internal/image/fixtures/schema2-all-media-types-to-oci1.json new file mode 100644 index 00000000000..65fff4a8229 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/image/fixtures/schema2-all-media-types-to-oci1.json @@ -0,0 +1,36 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 4670, + "digest": "sha256:f15ba60ec257ee2cf4fddfb9451bb86ba2668450e88d402f5ecc7ea6ce1b661a" + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar", + "size": 51354364, + "digest": "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 152, + "digest": "sha256:2bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c" + }, + { + "mediaType": "application/vnd.oci.image.layer.nondistributable.v1.tar", + "size": 11739507, + "digest": "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9" + }, + { + "mediaType": "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip", + "size": 8841833, + "digest": "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909" + }, + { + "mediaType": "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip", + "size": 291, + "digest": "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa" + } + ] +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/internal/image/fixtures/schema2-all-media-types.json b/vendor/github.com/containers/image/v5/internal/image/fixtures/schema2-all-media-types.json new file mode 100644 index 00000000000..2c3d8c7da90 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/image/fixtures/schema2-all-media-types.json @@ -0,0 +1,36 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 4651, + "digest": "sha256:9ca4bda0a6b3727a6ffcc43e981cad0f24e2ec79d338f6ba325b4dfd0756fb8f" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar", + "size": 51354364, + "digest": "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 152, + "digest": "sha256:2bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.foreign.diff.tar", + "size": 11739507, + "digest": "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip", + "size": 8841833, + "digest": "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip", + "size": 291, + "digest": "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa" + } + ] +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/internal/image/fixtures/schema2-config.json b/vendor/github.com/containers/image/v5/internal/image/fixtures/schema2-config.json new file mode 100644 index 00000000000..f49230ea77d --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/image/fixtures/schema2-config.json @@ -0,0 +1 @@ +{"architecture":"amd64","config":{"Hostname":"383850eeb47b","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"ExposedPorts":{"80/tcp":{}},"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","HTTPD_PREFIX=/usr/local/apache2","HTTPD_VERSION=2.4.23","HTTPD_SHA1=5101be34ac4a509b245adb70a56690a84fcc4e7f","HTTPD_BZ2_URL=https://www.apache.org/dyn/closer.cgi?action=download\u0026filename=httpd/httpd-2.4.23.tar.bz2","HTTPD_ASC_URL=https://www.apache.org/dist/httpd/httpd-2.4.23.tar.bz2.asc"],"Cmd":["httpd-foreground"],"ArgsEscaped":true,"Image":"sha256:4f83530449c67c1ed8fca72583c5b92fdf446010990028c362a381e55dd84afd","Volumes":null,"WorkingDir":"/usr/local/apache2","Entrypoint":null,"OnBuild":[],"Labels":{}},"container":"8825acde1b009729807e4b70a65a89399dd8da8e53be9216b9aaabaff4339f69","container_config":{"Hostname":"383850eeb47b","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"ExposedPorts":{"80/tcp":{}},"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","HTTPD_PREFIX=/usr/local/apache2","HTTPD_VERSION=2.4.23","HTTPD_SHA1=5101be34ac4a509b245adb70a56690a84fcc4e7f","HTTPD_BZ2_URL=https://www.apache.org/dyn/closer.cgi?action=download\u0026filename=httpd/httpd-2.4.23.tar.bz2","HTTPD_ASC_URL=https://www.apache.org/dist/httpd/httpd-2.4.23.tar.bz2.asc"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\"httpd-foreground\"]"],"ArgsEscaped":true,"Image":"sha256:4f83530449c67c1ed8fca72583c5b92fdf446010990028c362a381e55dd84afd","Volumes":null,"WorkingDir":"/usr/local/apache2","Entrypoint":null,"OnBuild":[],"Labels":{}},"created":"2016-09-23T23:20:45.78976459Z","docker_version":"1.12.1","history":[{"created":"2016-09-23T18:08:50.537223822Z","created_by":"/bin/sh -c #(nop) ADD file:c6c23585ab140b0b320d4e99bc1b0eb544c9e96c24d90fec5e069a6d57d335ca in / "},{"created":"2016-09-23T18:08:51.133779867Z","created_by":"/bin/sh -c #(nop) CMD [\"/bin/bash\"]","empty_layer":true},{"created":"2016-09-23T19:16:40.725768956Z","created_by":"/bin/sh -c #(nop) ENV HTTPD_PREFIX=/usr/local/apache2","empty_layer":true},{"created":"2016-09-23T19:16:41.037788416Z","created_by":"/bin/sh -c #(nop) ENV PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","empty_layer":true},{"created":"2016-09-23T19:16:41.990121202Z","created_by":"/bin/sh -c mkdir -p \"$HTTPD_PREFIX\" \t\u0026\u0026 chown www-data:www-data \"$HTTPD_PREFIX\""},{"created":"2016-09-23T19:16:42.339911155Z","created_by":"/bin/sh -c #(nop) WORKDIR /usr/local/apache2","empty_layer":true},{"created":"2016-09-23T19:16:54.948461741Z","created_by":"/bin/sh -c apt-get update \t\u0026\u0026 apt-get install -y --no-install-recommends \t\tlibapr1 \t\tlibaprutil1 \t\tlibaprutil1-ldap \t\tlibapr1-dev \t\tlibaprutil1-dev \t\tlibpcre++0 \t\tlibssl1.0.0 \t\u0026\u0026 rm -r /var/lib/apt/lists/*"},{"created":"2016-09-23T19:16:55.321573403Z","created_by":"/bin/sh -c #(nop) ENV HTTPD_VERSION=2.4.23","empty_layer":true},{"created":"2016-09-23T19:16:55.629947307Z","created_by":"/bin/sh -c #(nop) ENV HTTPD_SHA1=5101be34ac4a509b245adb70a56690a84fcc4e7f","empty_layer":true},{"created":"2016-09-23T23:19:03.705796801Z","created_by":"/bin/sh -c #(nop) ENV HTTPD_BZ2_URL=https://www.apache.org/dyn/closer.cgi?action=download\u0026filename=httpd/httpd-2.4.23.tar.bz2","empty_layer":true},{"created":"2016-09-23T23:19:04.009782822Z","created_by":"/bin/sh -c #(nop) ENV HTTPD_ASC_URL=https://www.apache.org/dist/httpd/httpd-2.4.23.tar.bz2.asc","empty_layer":true},{"created":"2016-09-23T23:20:44.585743332Z","created_by":"/bin/sh -c set -x \t\u0026\u0026 buildDeps=' \t\tbzip2 \t\tca-certificates \t\tgcc \t\tlibpcre++-dev \t\tlibssl-dev \t\tmake \t\twget \t' \t\u0026\u0026 apt-get update \t\u0026\u0026 apt-get install -y --no-install-recommends $buildDeps \t\u0026\u0026 rm -r /var/lib/apt/lists/* \t\t\u0026\u0026 wget -O httpd.tar.bz2 \"$HTTPD_BZ2_URL\" \t\u0026\u0026 echo \"$HTTPD_SHA1 *httpd.tar.bz2\" | sha1sum -c - \t\u0026\u0026 wget -O httpd.tar.bz2.asc \"$HTTPD_ASC_URL\" \t\u0026\u0026 export GNUPGHOME=\"$(mktemp -d)\" \t\u0026\u0026 gpg --keyserver ha.pool.sks-keyservers.net --recv-keys A93D62ECC3C8EA12DB220EC934EA76E6791485A8 \t\u0026\u0026 gpg --batch --verify httpd.tar.bz2.asc httpd.tar.bz2 \t\u0026\u0026 rm -r \"$GNUPGHOME\" httpd.tar.bz2.asc \t\t\u0026\u0026 mkdir -p src \t\u0026\u0026 tar -xvf httpd.tar.bz2 -C src --strip-components=1 \t\u0026\u0026 rm httpd.tar.bz2 \t\u0026\u0026 cd src \t\t\u0026\u0026 ./configure \t\t--prefix=\"$HTTPD_PREFIX\" \t\t--enable-mods-shared=reallyall \t\u0026\u0026 make -j\"$(nproc)\" \t\u0026\u0026 make install \t\t\u0026\u0026 cd .. \t\u0026\u0026 rm -r src \t\t\u0026\u0026 sed -ri \t\t-e 's!^(\\s*CustomLog)\\s+\\S+!\\1 /proc/self/fd/1!g' \t\t-e 's!^(\\s*ErrorLog)\\s+\\S+!\\1 /proc/self/fd/2!g' \t\t\"$HTTPD_PREFIX/conf/httpd.conf\" \t\t\u0026\u0026 apt-get purge -y --auto-remove $buildDeps"},{"created":"2016-09-23T23:20:45.127455562Z","created_by":"/bin/sh -c #(nop) COPY file:761e313354b918b6cd7ea99975a4f6b53ff5381ba689bab2984aec4dab597215 in /usr/local/bin/ "},{"created":"2016-09-23T23:20:45.453934921Z","created_by":"/bin/sh -c #(nop) EXPOSE 80/tcp","empty_layer":true},{"created":"2016-09-23T23:20:45.78976459Z","created_by":"/bin/sh -c #(nop) CMD [\"httpd-foreground\"]","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:142a601d97936307e75220c35dde0348971a9584c21e7cb42e1f7004005432ab","sha256:90fcc66ad3be9f1757f954b750deb37032f208428aa12599fcb02182b9065a9c","sha256:5a8624bb7e76d1e6829f9c64c43185e02bc07f97a2189eb048609a8914e72c56","sha256:d349ff6b3afc6a2800054768c82bfbf4289c9aa5da55c1290f802943dcd4d1e9","sha256:8c064bb1f60e84fa8cc6079b6d2e76e0423389fd6aeb7e497dfdae5e05b2b25b"]}} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/internal/image/fixtures/schema2-invalid-media-type.json b/vendor/github.com/containers/image/v5/internal/image/fixtures/schema2-invalid-media-type.json new file mode 100644 index 00000000000..d6b0691b296 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/image/fixtures/schema2-invalid-media-type.json @@ -0,0 +1,36 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/octet-stream", + "size": 5940, + "digest": "sha256:9ca4bda0a6b3727a6ffcc43e981cad0f24e2ec79d338f6ba325b4dfd0756fb8f" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.zstd", + "size": 51354364, + "digest": "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 150, + "digest": "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 11739507, + "digest": "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 8841833, + "digest": "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 291, + "digest": "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa" + } + ] + } \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/internal/image/fixtures/schema2-to-oci1-config.json b/vendor/github.com/containers/image/v5/internal/image/fixtures/schema2-to-oci1-config.json new file mode 100644 index 00000000000..eb43d8789a9 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/image/fixtures/schema2-to-oci1-config.json @@ -0,0 +1,105 @@ +{ + "architecture": "amd64", + "config": { + "ExposedPorts": { + "80/tcp": {} + }, + "Env": [ + "PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "HTTPD_PREFIX=/usr/local/apache2", + "HTTPD_VERSION=2.4.23", + "HTTPD_SHA1=5101be34ac4a509b245adb70a56690a84fcc4e7f", + "HTTPD_BZ2_URL=https://www.apache.org/dyn/closer.cgi?action=download\u0026filename=httpd/httpd-2.4.23.tar.bz2", + "HTTPD_ASC_URL=https://www.apache.org/dist/httpd/httpd-2.4.23.tar.bz2.asc" + ], + "Cmd": [ + "httpd-foreground" + ], + "ArgsEscaped": true, + "WorkingDir": "/usr/local/apache2" + }, + "created": "2016-09-23T23:20:45.78976459Z", + "history": [ + { + "created": "2016-09-23T18:08:50.537223822Z", + "created_by": "/bin/sh -c #(nop) ADD file:c6c23585ab140b0b320d4e99bc1b0eb544c9e96c24d90fec5e069a6d57d335ca in / " + }, + { + "created": "2016-09-23T18:08:51.133779867Z", + "created_by": "/bin/sh -c #(nop) CMD [\"/bin/bash\"]", + "empty_layer": true + }, + { + "created": "2016-09-23T19:16:40.725768956Z", + "created_by": "/bin/sh -c #(nop) ENV HTTPD_PREFIX=/usr/local/apache2", + "empty_layer": true + }, + { + "created": "2016-09-23T19:16:41.037788416Z", + "created_by": "/bin/sh -c #(nop) ENV PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "empty_layer": true + }, + { + "created": "2016-09-23T19:16:41.990121202Z", + "created_by": "/bin/sh -c mkdir -p \"$HTTPD_PREFIX\" \t\u0026\u0026 chown www-data:www-data \"$HTTPD_PREFIX\"" + }, + { + "created": "2016-09-23T19:16:42.339911155Z", + "created_by": "/bin/sh -c #(nop) WORKDIR /usr/local/apache2", + "empty_layer": true + }, + { + "created": "2016-09-23T19:16:54.948461741Z", + "created_by": "/bin/sh -c apt-get update \t\u0026\u0026 apt-get install -y --no-install-recommends \t\tlibapr1 \t\tlibaprutil1 \t\tlibaprutil1-ldap \t\tlibapr1-dev \t\tlibaprutil1-dev \t\tlibpcre++0 \t\tlibssl1.0.0 \t\u0026\u0026 rm -r /var/lib/apt/lists/*" + }, + { + "created": "2016-09-23T19:16:55.321573403Z", + "created_by": "/bin/sh -c #(nop) ENV HTTPD_VERSION=2.4.23", + "empty_layer": true + }, + { + "created": "2016-09-23T19:16:55.629947307Z", + "created_by": "/bin/sh -c #(nop) ENV HTTPD_SHA1=5101be34ac4a509b245adb70a56690a84fcc4e7f", + "empty_layer": true + }, + { + "created": "2016-09-23T23:19:03.705796801Z", + "created_by": "/bin/sh -c #(nop) ENV HTTPD_BZ2_URL=https://www.apache.org/dyn/closer.cgi?action=download\u0026filename=httpd/httpd-2.4.23.tar.bz2", + "empty_layer": true + }, + { + "created": "2016-09-23T23:19:04.009782822Z", + "created_by": "/bin/sh -c #(nop) ENV HTTPD_ASC_URL=https://www.apache.org/dist/httpd/httpd-2.4.23.tar.bz2.asc", + "empty_layer": true + }, + { + "created": "2016-09-23T23:20:44.585743332Z", + "created_by": "/bin/sh -c set -x \t\u0026\u0026 buildDeps=' \t\tbzip2 \t\tca-certificates \t\tgcc \t\tlibpcre++-dev \t\tlibssl-dev \t\tmake \t\twget \t' \t\u0026\u0026 apt-get update \t\u0026\u0026 apt-get install -y --no-install-recommends $buildDeps \t\u0026\u0026 rm -r /var/lib/apt/lists/* \t\t\u0026\u0026 wget -O httpd.tar.bz2 \"$HTTPD_BZ2_URL\" \t\u0026\u0026 echo \"$HTTPD_SHA1 *httpd.tar.bz2\" | sha1sum -c - \t\u0026\u0026 wget -O httpd.tar.bz2.asc \"$HTTPD_ASC_URL\" \t\u0026\u0026 export GNUPGHOME=\"$(mktemp -d)\" \t\u0026\u0026 gpg --keyserver ha.pool.sks-keyservers.net --recv-keys A93D62ECC3C8EA12DB220EC934EA76E6791485A8 \t\u0026\u0026 gpg --batch --verify httpd.tar.bz2.asc httpd.tar.bz2 \t\u0026\u0026 rm -r \"$GNUPGHOME\" httpd.tar.bz2.asc \t\t\u0026\u0026 mkdir -p src \t\u0026\u0026 tar -xvf httpd.tar.bz2 -C src --strip-components=1 \t\u0026\u0026 rm httpd.tar.bz2 \t\u0026\u0026 cd src \t\t\u0026\u0026 ./configure \t\t--prefix=\"$HTTPD_PREFIX\" \t\t--enable-mods-shared=reallyall \t\u0026\u0026 make -j\"$(nproc)\" \t\u0026\u0026 make install \t\t\u0026\u0026 cd .. \t\u0026\u0026 rm -r src \t\t\u0026\u0026 sed -ri \t\t-e 's!^(\\s*CustomLog)\\s+\\S+!\\1 /proc/self/fd/1!g' \t\t-e 's!^(\\s*ErrorLog)\\s+\\S+!\\1 /proc/self/fd/2!g' \t\t\"$HTTPD_PREFIX/conf/httpd.conf\" \t\t\u0026\u0026 apt-get purge -y --auto-remove $buildDeps" + }, + { + "created": "2016-09-23T23:20:45.127455562Z", + "created_by": "/bin/sh -c #(nop) COPY file:761e313354b918b6cd7ea99975a4f6b53ff5381ba689bab2984aec4dab597215 in /usr/local/bin/ " + }, + { + "created": "2016-09-23T23:20:45.453934921Z", + "created_by": "/bin/sh -c #(nop) EXPOSE 80/tcp", + "empty_layer": true + }, + { + "created": "2016-09-23T23:20:45.78976459Z", + "created_by": "/bin/sh -c #(nop) CMD [\"httpd-foreground\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:142a601d97936307e75220c35dde0348971a9584c21e7cb42e1f7004005432ab", + "sha256:90fcc66ad3be9f1757f954b750deb37032f208428aa12599fcb02182b9065a9c", + "sha256:5a8624bb7e76d1e6829f9c64c43185e02bc07f97a2189eb048609a8914e72c56", + "sha256:d349ff6b3afc6a2800054768c82bfbf4289c9aa5da55c1290f802943dcd4d1e9", + "sha256:8c064bb1f60e84fa8cc6079b6d2e76e0423389fd6aeb7e497dfdae5e05b2b25b" + ] + } +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/internal/image/fixtures/schema2-to-oci1.json b/vendor/github.com/containers/image/v5/internal/image/fixtures/schema2-to-oci1.json new file mode 100644 index 00000000000..251e4e579c1 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/image/fixtures/schema2-to-oci1.json @@ -0,0 +1,30 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 4670, + "digest": "sha256:f15ba60ec257ee2cf4fddfb9451bb86ba2668450e88d402f5ecc7ea6ce1b661a" + }, + "layers": [{ + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 51354364, + "digest": "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb" + }, { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 150, + "digest": "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c" + }, { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 11739507, + "digest": "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9" + }, { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 8841833, + "digest": "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909" + }, { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 291, + "digest": "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa" + }] +} diff --git a/vendor/github.com/containers/image/v5/internal/image/fixtures/schema2-to-schema1-by-docker.json b/vendor/github.com/containers/image/v5/internal/image/fixtures/schema2-to-schema1-by-docker.json new file mode 100644 index 00000000000..494450d9fdf --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/image/fixtures/schema2-to-schema1-by-docker.json @@ -0,0 +1,116 @@ +{ + "schemaVersion": 1, + "name": "library/httpd-copy", + "tag": "latest", + "architecture": "amd64", + "fsLayers": [ + { + "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" + }, + { + "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" + }, + { + "blobSum": "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa" + }, + { + "blobSum": "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909" + }, + { + "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" + }, + { + "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" + }, + { + "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" + }, + { + "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" + }, + { + "blobSum": "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9" + }, + { + "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" + }, + { + "blobSum": "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c" + }, + { + "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" + }, + { + "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" + }, + { + "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" + }, + { + "blobSum": "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb" + } + ], + "history": [ + { + "v1Compatibility": "{\"architecture\":\"amd64\",\"config\":{\"Hostname\":\"383850eeb47b\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":{\"80/tcp\":{}},\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"HTTPD_PREFIX=/usr/local/apache2\",\"HTTPD_VERSION=2.4.23\",\"HTTPD_SHA1=5101be34ac4a509b245adb70a56690a84fcc4e7f\",\"HTTPD_BZ2_URL=https://www.apache.org/dyn/closer.cgi?action=download\\u0026filename=httpd/httpd-2.4.23.tar.bz2\",\"HTTPD_ASC_URL=https://www.apache.org/dist/httpd/httpd-2.4.23.tar.bz2.asc\"],\"Cmd\":[\"httpd-foreground\"],\"ArgsEscaped\":true,\"Image\":\"sha256:4f83530449c67c1ed8fca72583c5b92fdf446010990028c362a381e55dd84afd\",\"Volumes\":null,\"WorkingDir\":\"/usr/local/apache2\",\"Entrypoint\":null,\"OnBuild\":[],\"Labels\":{}},\"container\":\"8825acde1b009729807e4b70a65a89399dd8da8e53be9216b9aaabaff4339f69\",\"container_config\":{\"Hostname\":\"383850eeb47b\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":{\"80/tcp\":{}},\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",\"HTTPD_PREFIX=/usr/local/apache2\",\"HTTPD_VERSION=2.4.23\",\"HTTPD_SHA1=5101be34ac4a509b245adb70a56690a84fcc4e7f\",\"HTTPD_BZ2_URL=https://www.apache.org/dyn/closer.cgi?action=download\\u0026filename=httpd/httpd-2.4.23.tar.bz2\",\"HTTPD_ASC_URL=https://www.apache.org/dist/httpd/httpd-2.4.23.tar.bz2.asc\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) \",\"CMD [\\\"httpd-foreground\\\"]\"],\"ArgsEscaped\":true,\"Image\":\"sha256:4f83530449c67c1ed8fca72583c5b92fdf446010990028c362a381e55dd84afd\",\"Volumes\":null,\"WorkingDir\":\"/usr/local/apache2\",\"Entrypoint\":null,\"OnBuild\":[],\"Labels\":{}},\"created\":\"2016-09-23T23:20:45.78976459Z\",\"docker_version\":\"1.12.1\",\"id\":\"dca7323f9c839837493199d63263083d94f5eb1796d7bd04ca8374c4e9d3749a\",\"os\":\"linux\",\"parent\":\"1b750729af47c9a802c8d14b0d327d3ad5ecdce5ae773ac728a0263315b914f4\",\"throwaway\":true}" + }, + { + "v1Compatibility": "{\"id\":\"1b750729af47c9a802c8d14b0d327d3ad5ecdce5ae773ac728a0263315b914f4\",\"parent\":\"3ef2f186f8b0a2fd2d95f5a1f1cd213f5fb0a6e51b0a8dfbe2ec7003a788ff9a\",\"created\":\"2016-09-23T23:20:45.453934921Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) EXPOSE 80/tcp\"]},\"throwaway\":true}" + }, + { + "v1Compatibility": "{\"id\":\"3ef2f186f8b0a2fd2d95f5a1f1cd213f5fb0a6e51b0a8dfbe2ec7003a788ff9a\",\"parent\":\"dbbb5c772ba968f675ebdb1968a2fbcf3cf53c0c85ff4e3329619e3735c811e6\",\"created\":\"2016-09-23T23:20:45.127455562Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) COPY file:761e313354b918b6cd7ea99975a4f6b53ff5381ba689bab2984aec4dab597215 in /usr/local/bin/ \"]}}" + }, + { + "v1Compatibility": "{\"id\":\"dbbb5c772ba968f675ebdb1968a2fbcf3cf53c0c85ff4e3329619e3735c811e6\",\"parent\":\"d264ded964bb52f78c8905c9e6c5f2b8526ef33f371981f0651f3fb0164ad4a7\",\"created\":\"2016-09-23T23:20:44.585743332Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c set -x \\t\\u0026\\u0026 buildDeps=' \\t\\tbzip2 \\t\\tca-certificates \\t\\tgcc \\t\\tlibpcre++-dev \\t\\tlibssl-dev \\t\\tmake \\t\\twget \\t' \\t\\u0026\\u0026 apt-get update \\t\\u0026\\u0026 apt-get install -y --no-install-recommends $buildDeps \\t\\u0026\\u0026 rm -r /var/lib/apt/lists/* \\t\\t\\u0026\\u0026 wget -O httpd.tar.bz2 \\\"$HTTPD_BZ2_URL\\\" \\t\\u0026\\u0026 echo \\\"$HTTPD_SHA1 *httpd.tar.bz2\\\" | sha1sum -c - \\t\\u0026\\u0026 wget -O httpd.tar.bz2.asc \\\"$HTTPD_ASC_URL\\\" \\t\\u0026\\u0026 export GNUPGHOME=\\\"$(mktemp -d)\\\" \\t\\u0026\\u0026 gpg --keyserver ha.pool.sks-keyservers.net --recv-keys A93D62ECC3C8EA12DB220EC934EA76E6791485A8 \\t\\u0026\\u0026 gpg --batch --verify httpd.tar.bz2.asc httpd.tar.bz2 \\t\\u0026\\u0026 rm -r \\\"$GNUPGHOME\\\" httpd.tar.bz2.asc \\t\\t\\u0026\\u0026 mkdir -p src \\t\\u0026\\u0026 tar -xvf httpd.tar.bz2 -C src --strip-components=1 \\t\\u0026\\u0026 rm httpd.tar.bz2 \\t\\u0026\\u0026 cd src \\t\\t\\u0026\\u0026 ./configure \\t\\t--prefix=\\\"$HTTPD_PREFIX\\\" \\t\\t--enable-mods-shared=reallyall \\t\\u0026\\u0026 make -j\\\"$(nproc)\\\" \\t\\u0026\\u0026 make install \\t\\t\\u0026\\u0026 cd .. \\t\\u0026\\u0026 rm -r src \\t\\t\\u0026\\u0026 sed -ri \\t\\t-e 's!^(\\\\s*CustomLog)\\\\s+\\\\S+!\\\\1 /proc/self/fd/1!g' \\t\\t-e 's!^(\\\\s*ErrorLog)\\\\s+\\\\S+!\\\\1 /proc/self/fd/2!g' \\t\\t\\\"$HTTPD_PREFIX/conf/httpd.conf\\\" \\t\\t\\u0026\\u0026 apt-get purge -y --auto-remove $buildDeps\"]}}" + }, + { + "v1Compatibility": "{\"id\":\"d264ded964bb52f78c8905c9e6c5f2b8526ef33f371981f0651f3fb0164ad4a7\",\"parent\":\"fd6f8d569a8a6d2a95f797494ab3cee7a47693dde647210b236a141f76b5c5fd\",\"created\":\"2016-09-23T23:19:04.009782822Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ENV HTTPD_ASC_URL=https://www.apache.org/dist/httpd/httpd-2.4.23.tar.bz2.asc\"]},\"throwaway\":true}" + }, + { + "v1Compatibility": "{\"id\":\"fd6f8d569a8a6d2a95f797494ab3cee7a47693dde647210b236a141f76b5c5fd\",\"parent\":\"5e2578d171daa47c0eeb55e592b4e3bd28a0946a75baed58e4d4dd315c5d5780\",\"created\":\"2016-09-23T23:19:03.705796801Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ENV HTTPD_BZ2_URL=https://www.apache.org/dyn/closer.cgi?action=download\\u0026filename=httpd/httpd-2.4.23.tar.bz2\"]},\"throwaway\":true}" + }, + { + "v1Compatibility": "{\"id\":\"5e2578d171daa47c0eeb55e592b4e3bd28a0946a75baed58e4d4dd315c5d5780\",\"parent\":\"1912159ee5bea8d7fde49b85012f90c47bceb3f09e4082b112b1f06a3f339c53\",\"created\":\"2016-09-23T19:16:55.629947307Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ENV HTTPD_SHA1=5101be34ac4a509b245adb70a56690a84fcc4e7f\"]},\"throwaway\":true}" + }, + { + "v1Compatibility": "{\"id\":\"1912159ee5bea8d7fde49b85012f90c47bceb3f09e4082b112b1f06a3f339c53\",\"parent\":\"3bfb089ca9d4bb73a9016e44a2c6f908b701f97704433305c419f75e8559d8a2\",\"created\":\"2016-09-23T19:16:55.321573403Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ENV HTTPD_VERSION=2.4.23\"]},\"throwaway\":true}" + }, + { + "v1Compatibility": "{\"id\":\"3bfb089ca9d4bb73a9016e44a2c6f908b701f97704433305c419f75e8559d8a2\",\"parent\":\"ae1ece73de4d0365c8b8ab45ba0bf6b1efa4213c16a4903b89341b704d101c3c\",\"created\":\"2016-09-23T19:16:54.948461741Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c apt-get update \\t\\u0026\\u0026 apt-get install -y --no-install-recommends \\t\\tlibapr1 \\t\\tlibaprutil1 \\t\\tlibaprutil1-ldap \\t\\tlibapr1-dev \\t\\tlibaprutil1-dev \\t\\tlibpcre++0 \\t\\tlibssl1.0.0 \\t\\u0026\\u0026 rm -r /var/lib/apt/lists/*\"]}}" + }, + { + "v1Compatibility": "{\"id\":\"ae1ece73de4d0365c8b8ab45ba0bf6b1efa4213c16a4903b89341b704d101c3c\",\"parent\":\"bffbcb416f40e0bd3ebae202403587bfd41829cd1e0d538b66f29adce40c6408\",\"created\":\"2016-09-23T19:16:42.339911155Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) WORKDIR /usr/local/apache2\"]},\"throwaway\":true}" + }, + { + "v1Compatibility": "{\"id\":\"bffbcb416f40e0bd3ebae202403587bfd41829cd1e0d538b66f29adce40c6408\",\"parent\":\"7b27731a3363efcb6b0520962d544471745aae15664920dffe690b4fdb410d80\",\"created\":\"2016-09-23T19:16:41.990121202Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c mkdir -p \\\"$HTTPD_PREFIX\\\" \\t\\u0026\\u0026 chown www-data:www-data \\\"$HTTPD_PREFIX\\\"\"]}}" + }, + { + "v1Compatibility": "{\"id\":\"7b27731a3363efcb6b0520962d544471745aae15664920dffe690b4fdb410d80\",\"parent\":\"57a0a421f1acbc1fe6b88b32d3d1c3c0388ff1958b97f95dd0e3a599b810499b\",\"created\":\"2016-09-23T19:16:41.037788416Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ENV PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"]},\"throwaway\":true}" + }, + { + "v1Compatibility": "{\"id\":\"57a0a421f1acbc1fe6b88b32d3d1c3c0388ff1958b97f95dd0e3a599b810499b\",\"parent\":\"faeaf6fdfdcbb18d68c12db9683a02428bab83962a493de88b4c7b1ec941db8f\",\"created\":\"2016-09-23T19:16:40.725768956Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ENV HTTPD_PREFIX=/usr/local/apache2\"]},\"throwaway\":true}" + }, + { + "v1Compatibility": "{\"id\":\"faeaf6fdfdcbb18d68c12db9683a02428bab83962a493de88b4c7b1ec941db8f\",\"parent\":\"d0c4f1eb7dc8f4dae2b45fe5c0cf4cfc70e5be85d933f5f5f4deb59f134fb520\",\"created\":\"2016-09-23T18:08:51.133779867Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) CMD [\\\"/bin/bash\\\"]\"]},\"throwaway\":true}" + }, + { + "v1Compatibility": "{\"id\":\"d0c4f1eb7dc8f4dae2b45fe5c0cf4cfc70e5be85d933f5f5f4deb59f134fb520\",\"created\":\"2016-09-23T18:08:50.537223822Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ADD file:c6c23585ab140b0b320d4e99bc1b0eb544c9e96c24d90fec5e069a6d57d335ca in / \"]}}" + } + ], + "signatures": [ + { + "header": { + "jwk": { + "crv": "P-256", + "kid": "6QVR:5NTY:VIHC:W6IU:XYIN:CTKT:OG5R:XEEG:Z6XJ:2623:YCBP:36MA", + "kty": "EC", + "x": "NAGHj6-IdNonuFoxlqJnNMjcrCCE1CBoq2r_1NDci68", + "y": "Kocqgj_Ey5J-wLXTjkuqLC-HjciAnWxsBEziAOTvSPc" + }, + "alg": "ES256" + }, + "signature": "2MN5k06i8xkJhD5ay4yxAFK7tsZk58UznAZONxDplvQ5lZwbRS162OeBDjCb0Hk0IDyrLXtAfBDlY2Gzf6jrpw", + "protected": "eyJmb3JtYXRMZW5ndGgiOjEwODk1LCJmb3JtYXRUYWlsIjoiQ24wIiwidGltZSI6IjIwMTYtMTAtMTRUMTY6MTI6MDlaIn0" + } + ] +} diff --git a/vendor/github.com/containers/image/v5/internal/image/fixtures/schema2.json b/vendor/github.com/containers/image/v5/internal/image/fixtures/schema2.json new file mode 100644 index 00000000000..8df4c0daf8f --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/image/fixtures/schema2.json @@ -0,0 +1,36 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/octet-stream", + "size": 5940, + "digest": "sha256:9ca4bda0a6b3727a6ffcc43e981cad0f24e2ec79d338f6ba325b4dfd0756fb8f" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 51354364, + "digest": "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 150, + "digest": "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 11739507, + "digest": "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 8841833, + "digest": "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 291, + "digest": "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa" + } + ] +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/internal/image/manifest_test.go b/vendor/github.com/containers/image/v5/internal/image/manifest_test.go new file mode 100644 index 00000000000..64ad1302dd1 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/image/manifest_test.go @@ -0,0 +1,71 @@ +package image + +import ( + "testing" + + "github.com/containers/image/v5/manifest" + "github.com/containers/image/v5/types" + "github.com/stretchr/testify/assert" +) + +func TestManifestLayerInfosToBlobInfos(t *testing.T) { + blobs := manifestLayerInfosToBlobInfos([]manifest.LayerInfo{}) + assert.Equal(t, []types.BlobInfo{}, blobs) + + blobs = manifestLayerInfosToBlobInfos([]manifest.LayerInfo{ + { + BlobInfo: types.BlobInfo{ + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Digest: "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", + Size: 32, + }, + EmptyLayer: true, + }, + { + BlobInfo: types.BlobInfo{ + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Digest: "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909", + Size: 8841833, + }, + EmptyLayer: false, + }, + { + BlobInfo: types.BlobInfo{ + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Digest: "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa", + Size: 291, + }, + EmptyLayer: false, + }, + { + BlobInfo: types.BlobInfo{ + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Digest: "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", + Size: 32, + }, + EmptyLayer: true, + }, + }) + assert.Equal(t, []types.BlobInfo{ + { + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Digest: "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", + Size: 32, + }, + { + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Digest: "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909", + Size: 8841833, + }, + { + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Digest: "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa", + Size: 291, + }, + { + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Digest: "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", + Size: 32, + }, + }, blobs) +} diff --git a/vendor/github.com/containers/image/v5/internal/image/oci_test.go b/vendor/github.com/containers/image/v5/internal/image/oci_test.go new file mode 100644 index 00000000000..84398dcf347 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/image/oci_test.go @@ -0,0 +1,561 @@ +package image + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "io" + "os" + "path/filepath" + "testing" + "time" + + "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/internal/testing/mocks" + "github.com/containers/image/v5/manifest" + "github.com/containers/image/v5/types" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func manifestOCI1FromFixture(t *testing.T, src types.ImageSource, fixture string) genericManifest { + manifest, err := os.ReadFile(filepath.Join("fixtures", fixture)) + require.NoError(t, err) + + m, err := manifestOCI1FromManifest(src, manifest) + require.NoError(t, err) + return m +} + +func manifestOCI1FromComponentsLikeFixture(configBlob []byte) genericManifest { + return manifestOCI1FromComponents(imgspecv1.Descriptor{ + MediaType: imgspecv1.MediaTypeImageConfig, + Size: 5940, + Digest: "sha256:9ca4bda0a6b3727a6ffcc43e981cad0f24e2ec79d338f6ba325b4dfd0756fb8f", + Annotations: map[string]string{ + "test-annotation-1": "one", + }, + }, nil, configBlob, []imgspecv1.Descriptor{ + { + MediaType: imgspecv1.MediaTypeImageLayerGzip, + Digest: "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb", + Size: 51354364, + }, + { + MediaType: imgspecv1.MediaTypeImageLayerGzip, + Digest: "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c", + Size: 150, + }, + { + MediaType: imgspecv1.MediaTypeImageLayerGzip, + Digest: "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9", + Size: 11739507, + URLs: []string{ + "https://layer.url", + }, + }, + { + MediaType: imgspecv1.MediaTypeImageLayerGzip, + Digest: "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909", + Size: 8841833, + Annotations: map[string]string{ + "test-annotation-2": "two", + }, + }, + { + MediaType: imgspecv1.MediaTypeImageLayerGzip, + Digest: "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa", + Size: 291, + }, + }) +} + +func TestManifestOCI1FromManifest(t *testing.T) { + // This just tests that the JSON can be loaded; we test that the parsed + // values are correctly returned in tests for the individual getter methods. + _ = manifestOCI1FromFixture(t, mocks.ForbiddenImageSource{}, "oci1.json") + + _, err := manifestOCI1FromManifest(nil, []byte{}) + assert.Error(t, err) +} + +func TestManifestOCI1FromComponents(t *testing.T) { + // This just smoke-tests that the manifest can be created; we test that the parsed + // values are correctly returned in tests for the individual getter methods. + _ = manifestOCI1FromComponentsLikeFixture(nil) +} + +func TestManifestOCI1Serialize(t *testing.T) { + for _, m := range []genericManifest{ + manifestOCI1FromFixture(t, mocks.ForbiddenImageSource{}, "oci1.json"), + manifestOCI1FromComponentsLikeFixture(nil), + } { + serialized, err := m.serialize() + require.NoError(t, err) + // We would ideally like to compare “serialized” with some transformation of + // the original fixture, but the ordering of fields in JSON maps is undefined, so this is + // easier. + assertJSONEqualsFixture(t, serialized, "oci1.json") + } +} + +func TestManifestOCI1ManifestMIMEType(t *testing.T) { + for _, m := range []genericManifest{ + manifestOCI1FromFixture(t, mocks.ForbiddenImageSource{}, "oci1.json"), + manifestOCI1FromComponentsLikeFixture(nil), + } { + assert.Equal(t, imgspecv1.MediaTypeImageManifest, m.manifestMIMEType()) + } +} + +func TestManifestOCI1ConfigInfo(t *testing.T) { + for _, m := range []genericManifest{ + manifestOCI1FromFixture(t, mocks.ForbiddenImageSource{}, "oci1.json"), + manifestOCI1FromComponentsLikeFixture(nil), + } { + assert.Equal(t, types.BlobInfo{ + Size: 5940, + Digest: "sha256:9ca4bda0a6b3727a6ffcc43e981cad0f24e2ec79d338f6ba325b4dfd0756fb8f", + Annotations: map[string]string{ + "test-annotation-1": "one", + }, + MediaType: "application/vnd.oci.image.config.v1+json", + }, m.ConfigInfo()) + } +} + +func TestManifestOCI1ConfigBlob(t *testing.T) { + realConfigJSON, err := os.ReadFile("fixtures/oci1-config.json") + require.NoError(t, err) + + for _, c := range []struct { + cbISfn func() (io.ReadCloser, int64, error) + blob []byte + }{ + // Success + {func() (io.ReadCloser, int64, error) { + return io.NopCloser(bytes.NewReader(realConfigJSON)), int64(len(realConfigJSON)), nil + }, realConfigJSON}, + // Various kinds of failures + {nil, nil}, + {func() (io.ReadCloser, int64, error) { + return nil, -1, errors.New("Error returned from GetBlob") + }, nil}, + {func() (io.ReadCloser, int64, error) { + reader, writer := io.Pipe() + err = writer.CloseWithError(errors.New("Expected error reading input in ConfigBlob")) + require.NoError(t, err) + return reader, 1, nil + }, nil}, + {func() (io.ReadCloser, int64, error) { + nonmatchingJSON := []byte("This does not match ConfigDescriptor.Digest") + return io.NopCloser(bytes.NewReader(nonmatchingJSON)), int64(len(nonmatchingJSON)), nil + }, nil}, + } { + var src types.ImageSource + if c.cbISfn != nil { + src = configBlobImageSource{f: c.cbISfn} + } else { + src = nil + } + m := manifestOCI1FromFixture(t, src, "oci1.json") + blob, err := m.ConfigBlob(context.Background()) + if c.blob != nil { + assert.NoError(t, err) + assert.Equal(t, c.blob, blob) + } else { + assert.Error(t, err) + } + } + + // Generally configBlob should match ConfigInfo; we don’t quite need it to, and this will + // guarantee that the returned object is returning the original contents instead + // of reading an object from elsewhere. + configBlob := []byte("config blob which does not match ConfigInfo") + // This just tests that the manifest can be created; we test that the parsed + // values are correctly returned in tests for the individual getter methods. + m := manifestOCI1FromComponentsLikeFixture(configBlob) + cb, err := m.ConfigBlob(context.Background()) + require.NoError(t, err) + assert.Equal(t, configBlob, cb) +} + +func TestManifestOCI1OCIConfig(t *testing.T) { + // Just a smoke-test that the code can read the data… + configJSON, err := os.ReadFile("fixtures/oci1-config.json") + require.NoError(t, err) + expectedConfig := imgspecv1.Image{} + err = json.Unmarshal(configJSON, &expectedConfig) + require.NoError(t, err) + + originalSrc := newOCI1ImageSource(t, "httpd:latest") + for _, m := range []genericManifest{ + manifestOCI1FromFixture(t, originalSrc, "oci1.json"), + manifestOCI1FromComponentsLikeFixture(configJSON), + } { + config, err := m.OCIConfig(context.Background()) + require.NoError(t, err) + assert.Equal(t, &expectedConfig, config) + } + + // This can share originalSrc because the config digest is the same between oci1-artifact.json and oci1.json + artifact := manifestOCI1FromFixture(t, originalSrc, "oci1-artifact.json") + _, err = artifact.OCIConfig(context.Background()) + var expected manifest.NonImageArtifactError + assert.ErrorAs(t, err, &expected) +} + +func TestManifestOCI1LayerInfo(t *testing.T) { + for _, m := range []genericManifest{ + manifestOCI1FromFixture(t, mocks.ForbiddenImageSource{}, "oci1.json"), + manifestOCI1FromComponentsLikeFixture(nil), + } { + assert.Equal(t, []types.BlobInfo{ + { + Digest: "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb", + Size: 51354364, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + }, + { + Digest: "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c", + Size: 150, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + }, + { + Digest: "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9", + Size: 11739507, + URLs: []string{ + "https://layer.url", + }, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + }, + { + Digest: "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909", + Size: 8841833, + Annotations: map[string]string{ + "test-annotation-2": "two", + }, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + }, + { + Digest: "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa", + Size: 291, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + }, + }, m.LayerInfos()) + } +} + +func TestManifestOCI1EmbeddedDockerReferenceConflicts(t *testing.T) { + for _, m := range []genericManifest{ + manifestOCI1FromFixture(t, mocks.ForbiddenImageSource{}, "oci1.json"), + manifestOCI1FromComponentsLikeFixture(nil), + } { + for _, name := range []string{"busybox", "example.com:5555/ns/repo:tag"} { + ref, err := reference.ParseNormalizedNamed(name) + require.NoError(t, err) + conflicts := m.EmbeddedDockerReferenceConflicts(ref) + assert.False(t, conflicts) + } + } +} + +func TestManifestOCI1Inspect(t *testing.T) { + configJSON, err := os.ReadFile("fixtures/oci1-config.json") + require.NoError(t, err) + var emptyAnnotations map[string]string + m := manifestOCI1FromComponentsLikeFixture(configJSON) + ii, err := m.Inspect(context.Background()) + require.NoError(t, err) + created := time.Date(2016, 9, 23, 23, 20, 45, 789764590, time.UTC) + assert.Equal(t, types.ImageInspectInfo{ + Tag: "", + Created: &created, + DockerVersion: "1.12.1", + Labels: map[string]string{}, + Architecture: "amd64", + Os: "linux", + Layers: []string{ + "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb", + "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c", + "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9", + "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909", + "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa", + }, + LayersData: []types.ImageInspectLayer{{ + MIMEType: "application/vnd.oci.image.layer.v1.tar+gzip", + Digest: "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb", + Size: 51354364, + Annotations: emptyAnnotations, + }, { + MIMEType: "application/vnd.oci.image.layer.v1.tar+gzip", + Digest: "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c", + Size: 150, + Annotations: emptyAnnotations, + }, { + MIMEType: "application/vnd.oci.image.layer.v1.tar+gzip", + Digest: "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9", + Size: 11739507, + Annotations: emptyAnnotations, + }, { + MIMEType: "application/vnd.oci.image.layer.v1.tar+gzip", + Digest: "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909", + Size: 8841833, + Annotations: map[string]string{"test-annotation-2": "two"}, + }, { + MIMEType: "application/vnd.oci.image.layer.v1.tar+gzip", + Digest: "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa", + Size: 291, + Annotations: emptyAnnotations, + }, + }, + Author: "", + Env: []string{ + "PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "HTTPD_PREFIX=/usr/local/apache2", + "HTTPD_VERSION=2.4.23", + "HTTPD_SHA1=5101be34ac4a509b245adb70a56690a84fcc4e7f", + "HTTPD_BZ2_URL=https://www.apache.org/dyn/closer.cgi?action=download&filename=httpd/httpd-2.4.23.tar.bz2", + "HTTPD_ASC_URL=https://www.apache.org/dist/httpd/httpd-2.4.23.tar.bz2.asc", + }, + }, *ii) + + // nil configBlob will trigger an error in m.ConfigBlob() + m = manifestOCI1FromComponentsLikeFixture(nil) + _, err = m.Inspect(context.Background()) + assert.Error(t, err) + + m = manifestOCI1FromComponentsLikeFixture([]byte("invalid JSON")) + _, err = m.Inspect(context.Background()) + assert.Error(t, err) +} + +func TestManifestOCI1UpdatedImageNeedsLayerDiffIDs(t *testing.T) { + for _, m := range []genericManifest{ + manifestOCI1FromFixture(t, mocks.ForbiddenImageSource{}, "oci1.json"), + manifestOCI1FromComponentsLikeFixture(nil), + } { + assert.False(t, m.UpdatedImageNeedsLayerDiffIDs(types.ManifestUpdateOptions{ + ManifestMIMEType: manifest.DockerV2Schema2MediaType, + })) + } +} + +// oci1ImageSource is plausible enough for schema conversions in manifestOCI1.UpdatedImage() to work. +type oci1ImageSource struct { + configBlobImageSource + ref reference.Named +} + +func (OCIis *oci1ImageSource) Reference() types.ImageReference { + return refImageReferenceMock{ref: OCIis.ref} +} + +func newOCI1ImageSource(t *testing.T, dockerRef string) *oci1ImageSource { + realConfigJSON, err := os.ReadFile("fixtures/oci1-config.json") + require.NoError(t, err) + + ref, err := reference.ParseNormalizedNamed(dockerRef) + require.NoError(t, err) + + return &oci1ImageSource{ + configBlobImageSource: configBlobImageSource{ + f: func() (io.ReadCloser, int64, error) { + return io.NopCloser(bytes.NewReader(realConfigJSON)), int64(len(realConfigJSON)), nil + }, + }, + ref: ref, + } +} + +func TestManifestOCI1UpdatedImage(t *testing.T) { + originalSrc := newOCI1ImageSource(t, "httpd:latest") + original := manifestOCI1FromFixture(t, originalSrc, "oci1.json") + + // LayerInfos: + layerInfos := append(original.LayerInfos()[1:], original.LayerInfos()[0]) + res, err := original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + LayerInfos: layerInfos, + }) + require.NoError(t, err) + assert.Equal(t, layerInfos, res.LayerInfos()) + _, err = original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + LayerInfos: append(layerInfos, layerInfos[0]), + }) + assert.Error(t, err) + + // EmbeddedDockerReference: + // … is ignored + embeddedRef, err := reference.ParseNormalizedNamed("busybox") + require.NoError(t, err) + res, err = original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + EmbeddedDockerReference: embeddedRef, + }) + require.NoError(t, err) + nonEmbeddedRef, err := reference.ParseNormalizedNamed("notbusybox:notlatest") + require.NoError(t, err) + conflicts := res.EmbeddedDockerReferenceConflicts(nonEmbeddedRef) + assert.False(t, conflicts) + + // ManifestMIMEType: + // Only smoke-test the valid conversions, detailed tests are below. (This also verifies that “original” is not affected.) + for _, mime := range []string{ + manifest.DockerV2Schema2MediaType, + } { + _, err = original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + ManifestMIMEType: mime, + InformationOnly: types.ManifestUpdateInformation{ + Destination: &memoryImageDest{ref: originalSrc.ref}, + }, + }) + assert.NoError(t, err, mime) + } + for _, mime := range []string{ + imgspecv1.MediaTypeImageManifest, // This indicates a confused caller, not a no-op. + "this is invalid", + } { + _, err = original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + ManifestMIMEType: mime, + }) + assert.Error(t, err, mime) + } + + // m hasn’t been changed: + m2 := manifestOCI1FromFixture(t, originalSrc, "oci1.json") + typedOriginal, ok := original.(*manifestOCI1) + require.True(t, ok) + typedM2, ok := m2.(*manifestOCI1) + require.True(t, ok) + assert.Equal(t, *typedM2, *typedOriginal) +} + +func TestManifestOCI1ConvertToManifestSchema1(t *testing.T) { + originalSrc := newOCI1ImageSource(t, "httpd-copy:latest") + original := manifestOCI1FromFixture(t, originalSrc, "oci1.json") + memoryDest := &memoryImageDest{ref: originalSrc.ref} + res, err := original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + ManifestMIMEType: manifest.DockerV2Schema1SignedMediaType, + InformationOnly: types.ManifestUpdateInformation{ + Destination: memoryDest, + }, + }) + require.NoError(t, err) + + convertedJSON, mt, err := res.Manifest(context.Background()) + require.NoError(t, err) + assert.Equal(t, manifest.DockerV2Schema1SignedMediaType, mt) + assertJSONEqualsFixture(t, convertedJSON, "oci1-to-schema1.json", "signatures") + + assert.Equal(t, GzippedEmptyLayer, memoryDest.storedBlobs[GzippedEmptyLayerDigest]) + + // Conversion to schema1 together with changing LayerInfos works as expected (which requires + // handling schema1 empty layers): + updatedLayers, updatedLayersCopy := modifiedLayerInfos(t, original.LayerInfos()) + res, err = original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + LayerInfos: updatedLayers, + ManifestMIMEType: manifest.DockerV2Schema1SignedMediaType, + InformationOnly: types.ManifestUpdateInformation{ + Destination: memoryDest, + }, + }) + require.NoError(t, err) + assert.Equal(t, updatedLayersCopy, updatedLayers) // updatedLayers have not been modified in place + convertedJSON, mt, err = res.Manifest(context.Background()) + require.NoError(t, err) + assert.Equal(t, manifest.DockerV2Schema1SignedMediaType, mt) + // Layers have been updated as expected + s1Manifest, err := manifestSchema1FromManifest(convertedJSON) + require.NoError(t, err) + assert.Equal(t, []types.BlobInfo{ + {Digest: "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5ba", Size: -1}, + {Digest: GzippedEmptyLayerDigest, Size: -1}, + {Digest: GzippedEmptyLayerDigest, Size: -1}, + {Digest: GzippedEmptyLayerDigest, Size: -1}, + {Digest: "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680d", Size: -1}, + {Digest: GzippedEmptyLayerDigest, Size: -1}, + {Digest: "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a8", Size: -1}, + {Digest: GzippedEmptyLayerDigest, Size: -1}, + {Digest: GzippedEmptyLayerDigest, Size: -1}, + {Digest: GzippedEmptyLayerDigest, Size: -1}, + {Digest: GzippedEmptyLayerDigest, Size: -1}, + {Digest: "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25908", Size: -1}, + {Digest: "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fb", Size: -1}, + {Digest: GzippedEmptyLayerDigest, Size: -1}, + {Digest: GzippedEmptyLayerDigest, Size: -1}, + }, s1Manifest.LayerInfos()) + + // This can share originalSrc because the config digest is the same between oci1-artifact.json and oci1.json + artifact := manifestOCI1FromFixture(t, originalSrc, "oci1-artifact.json") + _, err = artifact.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + ManifestMIMEType: manifest.DockerV2Schema1SignedMediaType, + InformationOnly: types.ManifestUpdateInformation{ + Destination: memoryDest, + }, + }) + var expected manifest.NonImageArtifactError + assert.ErrorAs(t, err, &expected) + + // FIXME? Test also the other failure cases, if only to see that we don't crash? +} + +func TestConvertToManifestSchema2(t *testing.T) { + originalSrc := newOCI1ImageSource(t, "httpd-copy:latest") + original := manifestOCI1FromFixture(t, originalSrc, "oci1.json") + res, err := original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + ManifestMIMEType: manifest.DockerV2Schema2MediaType, + }) + require.NoError(t, err) + + convertedJSON, mt, err := res.Manifest(context.Background()) + require.NoError(t, err) + assert.Equal(t, manifest.DockerV2Schema2MediaType, mt) + assertJSONEqualsFixture(t, convertedJSON, "oci1-to-schema2.json") + + convertedConfig, err := res.ConfigBlob(context.Background()) + require.NoError(t, err) + assertJSONEqualsFixture(t, convertedConfig, "oci1-to-schema2-config.json") + + // This can share originalSrc because the config digest is the same between oci1-artifact.json and oci1.json + artifact := manifestOCI1FromFixture(t, originalSrc, "oci1-artifact.json") + _, err = artifact.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + ManifestMIMEType: manifest.DockerV2Schema2MediaType, + }) + var expected manifest.NonImageArtifactError + assert.ErrorAs(t, err, &expected) + + // FIXME? Test also the other failure cases, if only to see that we don't crash? +} + +func TestConvertToManifestSchema2AllMediaTypes(t *testing.T) { + originalSrc := newOCI1ImageSource(t, "httpd-copy:latest") + original := manifestOCI1FromFixture(t, originalSrc, "oci1-all-media-types.json") + _, err := original.UpdatedImage(context.Background(), types.ManifestUpdateOptions{ + ManifestMIMEType: manifest.DockerV2Schema2MediaType, + }) + require.Error(t, err) // zstd compression is not supported for docker images +} + +func TestConvertToV2S2WithInvalidMIMEType(t *testing.T) { + originalSrc := newOCI1ImageSource(t, "httpd-copy:latest") + manifest, err := os.ReadFile(filepath.Join("fixtures", "oci1-invalid-media-type.json")) + require.NoError(t, err) + + _, err = manifestOCI1FromManifest(originalSrc, manifest) + require.NoError(t, err) +} + +func TestManifestOCI1CanChangeLayerCompression(t *testing.T) { + for _, m := range []genericManifest{ + manifestOCI1FromFixture(t, mocks.ForbiddenImageSource{}, "oci1.json"), + manifestOCI1FromComponentsLikeFixture(nil), + } { + assert.True(t, m.CanChangeLayerCompression(imgspecv1.MediaTypeImageLayerGzip)) + // Some projects like to use squashfs and other unspecified formats for layers; don’t touch those. + assert.False(t, m.CanChangeLayerCompression("a completely unknown and quite possibly invalid MIME type")) + } + + artifact := manifestOCI1FromFixture(t, mocks.ForbiddenImageSource{}, "oci1-artifact.json") + assert.False(t, artifact.CanChangeLayerCompression(imgspecv1.MediaTypeImageLayerGzip)) +} diff --git a/vendor/github.com/containers/image/v5/internal/imagedestination/impl/helpers.go b/vendor/github.com/containers/image/v5/internal/imagedestination/impl/helpers.go new file mode 100644 index 00000000000..d5de81a613b --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/imagedestination/impl/helpers.go @@ -0,0 +1,20 @@ +package impl + +import ( + "github.com/containers/image/v5/internal/private" + compression "github.com/containers/image/v5/pkg/compression/types" +) + +// BlobMatchesRequiredCompression validates if compression is required by the caller while selecting a blob, if it is required +// then function performs a match against the compression requested by the caller and compression of existing blob +// (which can be nil to represent uncompressed or unknown) +func BlobMatchesRequiredCompression(options private.TryReusingBlobOptions, candidateCompression *compression.Algorithm) bool { + if options.RequiredCompression == nil { + return true // no requirement imposed + } + return candidateCompression != nil && (options.RequiredCompression.Name() == candidateCompression.Name()) +} + +func OriginalBlobMatchesRequiredCompression(opts private.TryReusingBlobOptions) bool { + return BlobMatchesRequiredCompression(opts, opts.OriginalCompression) +} diff --git a/vendor/github.com/containers/image/v5/internal/imagedestination/impl/helpers_test.go b/vendor/github.com/containers/image/v5/internal/imagedestination/impl/helpers_test.go new file mode 100644 index 00000000000..26af382d31a --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/imagedestination/impl/helpers_test.go @@ -0,0 +1,29 @@ +package impl + +import ( + "testing" + + "github.com/containers/image/v5/internal/private" + "github.com/containers/image/v5/pkg/compression" + compressionTypes "github.com/containers/image/v5/pkg/compression/types" + "github.com/stretchr/testify/assert" +) + +func TestBlobMatchesRequiredCompression(t *testing.T) { + var opts private.TryReusingBlobOptions + cases := []struct { + requiredCompression *compressionTypes.Algorithm + candidateCompression *compressionTypes.Algorithm + result bool + }{ + {&compression.Zstd, &compression.Zstd, true}, + {&compression.Gzip, &compression.Zstd, false}, + {&compression.Zstd, nil, false}, + {nil, &compression.Zstd, true}, + } + + for _, c := range cases { + opts = private.TryReusingBlobOptions{RequiredCompression: c.requiredCompression} + assert.Equal(t, BlobMatchesRequiredCompression(opts, c.candidateCompression), c.result) + } +} diff --git a/vendor/github.com/containers/image/v5/internal/imagedestination/wrapper.go b/vendor/github.com/containers/image/v5/internal/imagedestination/wrapper.go index 41a81628bd6..17e1870c19a 100644 --- a/vendor/github.com/containers/image/v5/internal/imagedestination/wrapper.go +++ b/vendor/github.com/containers/image/v5/internal/imagedestination/wrapper.go @@ -64,6 +64,9 @@ func (w *wrapped) PutBlobWithOptions(ctx context.Context, stream io.Reader, inpu // If the blob has been successfully reused, returns (true, info, nil). // If the transport can not reuse the requested blob, TryReusingBlob returns (false, {}, nil); it returns a non-nil error only on an unexpected failure. func (w *wrapped) TryReusingBlobWithOptions(ctx context.Context, info types.BlobInfo, options private.TryReusingBlobOptions) (bool, private.ReusedBlob, error) { + if options.RequiredCompression != nil { + return false, private.ReusedBlob{}, nil + } reused, blob, err := w.TryReusingBlob(ctx, info, options.Cache, options.CanSubstitute) if !reused || err != nil { return reused, private.ReusedBlob{}, err diff --git a/vendor/github.com/containers/image/v5/internal/iolimits/iolimits_test.go b/vendor/github.com/containers/image/v5/internal/iolimits/iolimits_test.go new file mode 100644 index 00000000000..630b38ca555 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/iolimits/iolimits_test.go @@ -0,0 +1,37 @@ +package iolimits + +import ( + "bytes" + "math/rand" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestReadAtMost(t *testing.T) { + rng := rand.New(rand.NewSource(0)) + for _, c := range []struct { + input, limit int + shouldSucceed bool + }{ + {0, 0, true}, + {0, 1, true}, + {1, 0, false}, + {1, 1, true}, + {bytes.MinRead*5 - 1, bytes.MinRead * 5, true}, + {bytes.MinRead * 5, bytes.MinRead * 5, true}, + {bytes.MinRead*5 + 1, bytes.MinRead * 5, false}, + } { + input := make([]byte, c.input) + _, err := rng.Read(input) + require.NoError(t, err) + result, err := ReadAtMost(bytes.NewReader(input), c.limit) + if c.shouldSucceed { + assert.NoError(t, err) + assert.Equal(t, result, input) + } else { + assert.Error(t, err) + } + } +} diff --git a/vendor/github.com/containers/image/v5/internal/manifest/common_test.go b/vendor/github.com/containers/image/v5/internal/manifest/common_test.go new file mode 100644 index 00000000000..553cc362952 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/manifest/common_test.go @@ -0,0 +1,91 @@ +package manifest + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestValidateUnambiguousManifestFormat(t *testing.T) { + const allAllowedFields = AllowedFieldFirstUnusedBit - 1 + const mt = "text/plain" // Just some MIME type that shows up in error messages + + type test struct { + manifest string + allowed AllowedManifestFields + } + + // Smoke tests: Success + for _, c := range []test{ + {"{}", allAllowedFields}, + {"{}", 0}, + } { + err := ValidateUnambiguousManifestFormat([]byte(c.manifest), mt, c.allowed) + assert.NoError(t, err, c) + } + // Smoke tests: Failure + for _, c := range []test{ + {"{}", AllowedFieldFirstUnusedBit}, // Invalid "allowed" + {"@", allAllowedFields}, // Invalid JSON + } { + err := ValidateUnambiguousManifestFormat([]byte(c.manifest), mt, c.allowed) + assert.Error(t, err, c) + } + + fields := map[AllowedManifestFields]string{ + AllowedFieldConfig: "config", + AllowedFieldFSLayers: "fsLayers", + AllowedFieldHistory: "history", + AllowedFieldLayers: "layers", + AllowedFieldManifests: "manifests", + } + // Ensure this test covers all defined AllowedManifestFields values + allFields := AllowedManifestFields(0) + for k := range fields { + allFields |= k + } + assert.Equal(t, allAllowedFields, allFields) + + // Every single field is allowed by its bit, and rejected by any other bit + for bit, fieldName := range fields { + json := []byte(fmt.Sprintf(`{"%s":[]}`, fieldName)) + err := ValidateUnambiguousManifestFormat(json, mt, bit) + assert.NoError(t, err, fieldName) + err = ValidateUnambiguousManifestFormat(json, mt, allAllowedFields^bit) + assert.Error(t, err, fieldName) + } +} + +// Test that parser() rejects all of the provided manifest fixtures. +// Intended to help test manifest parsers' detection of schema mismatches. +func testManifestFixturesAreRejected(t *testing.T, parser func([]byte) error, fixtures []string) { + for _, fixture := range fixtures { + manifest, err := os.ReadFile(filepath.Join("testdata", fixture)) + require.NoError(t, err, fixture) + err = parser(manifest) + assert.Error(t, err, fixture) + } +} + +// Test that parser() rejects validManifest with an added top-level field with any of the provided field names. +// Intended to help test callers of validateUnambiguousManifestFormat. +func testValidManifestWithExtraFieldsIsRejected(t *testing.T, parser func([]byte) error, + validManifest []byte, fields []string) { + for _, field := range fields { + // end (the final '}') is not always at len(validManifest)-1 because the manifest can end with + // white space. + end := bytes.LastIndexByte(validManifest, '}') + require.NotEqual(t, end, -1) + updatedManifest := []byte(string(validManifest[:end]) + + fmt.Sprintf(`,"%s":[]}`, field)) + err := parser(updatedManifest) + // Make sure it is the error from validateUnambiguousManifestFormat, not something that + // went wrong with creating updatedManifest. + assert.ErrorContains(t, err, "rejecting ambiguous manifest", field) + } +} diff --git a/vendor/github.com/containers/image/v5/internal/manifest/docker_schema2_list.go b/vendor/github.com/containers/image/v5/internal/manifest/docker_schema2_list.go index 516ca7ac94b..14e7f42157c 100644 --- a/vendor/github.com/containers/image/v5/internal/manifest/docker_schema2_list.go +++ b/vendor/github.com/containers/image/v5/internal/manifest/docker_schema2_list.go @@ -61,6 +61,13 @@ func (list *Schema2ListPublic) Instance(instanceDigest digest.Digest) (ListUpdat Digest: manifest.Digest, Size: manifest.Size, MediaType: manifest.MediaType, + Platform: &imgspecv1.Platform{ + OS: manifest.Platform.OS, + Architecture: manifest.Platform.Architecture, + OSVersion: manifest.Platform.OSVersion, + OSFeatures: manifest.Platform.OSFeatures, + Variant: manifest.Platform.Variant, + }, }, nil } } diff --git a/vendor/github.com/containers/image/v5/internal/manifest/docker_schema2_list_test.go b/vendor/github.com/containers/image/v5/internal/manifest/docker_schema2_list_test.go new file mode 100644 index 00000000000..a13505c19de --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/manifest/docker_schema2_list_test.go @@ -0,0 +1,106 @@ +package manifest + +import ( + "os" + "path/filepath" + "testing" + + "github.com/opencontainers/go-digest" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSchema2ListPublicFromManifest(t *testing.T) { + validManifest, err := os.ReadFile(filepath.Join("testdata", "v2list.manifest.json")) + require.NoError(t, err) + + parser := func(m []byte) error { + _, err := Schema2ListPublicFromManifest(m) + return err + } + // Schema mismatch is rejected + testManifestFixturesAreRejected(t, parser, []string{ + "schema2-to-schema1-by-docker.json", + "v2s2.manifest.json", + "ociv1.manifest.json", + // Not "ociv1.image.index.json" yet, without validating mediaType the two are too similar to tell the difference. + }) + // Extra fields are rejected + testValidManifestWithExtraFieldsIsRejected(t, parser, validManifest, []string{"config", "fsLayers", "history", "layers"}) +} + +func TestSchema2ListEditInstances(t *testing.T) { + validManifest, err := os.ReadFile(filepath.Join("testdata", "v2list.manifest.json")) + require.NoError(t, err) + list, err := ListFromBlob(validManifest, GuessMIMEType(validManifest)) + require.NoError(t, err) + + expectedDigests := list.Instances() + editInstances := []ListEdit{} + editInstances = append(editInstances, ListEdit{ + UpdateOldDigest: list.Instances()[0], + UpdateDigest: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + UpdateSize: 32, + UpdateMediaType: "something", + ListOperation: ListOpUpdate}) + err = list.EditInstances(editInstances) + require.NoError(t, err) + + expectedDigests[0] = editInstances[0].UpdateDigest + // order of old elements must remain same. + assert.Equal(t, list.Instances(), expectedDigests) + + instance, err := list.Instance(list.Instances()[0]) + require.NoError(t, err) + assert.Equal(t, "something", instance.MediaType) + assert.Equal(t, int64(32), instance.Size) + // platform must match with instance platform set in `v2list.manifest.json` for the first instance + assert.Equal(t, &imgspecv1.Platform{Architecture: "ppc64le", OS: "linux", OSVersion: "", OSFeatures: []string(nil), Variant: ""}, instance.Platform) + + // Create a fresh list + list, err = ListFromBlob(validManifest, GuessMIMEType(validManifest)) + require.NoError(t, err) + originalListOrder := list.Instances() + + editInstances = []ListEdit{} + editInstances = append(editInstances, ListEdit{ + AddDigest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + AddSize: 32, + AddMediaType: "application/vnd.oci.image.manifest.v1+json", + AddPlatform: &imgspecv1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"sse4"}}, + ListOperation: ListOpAdd}) + editInstances = append(editInstances, ListEdit{ + AddDigest: "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + AddSize: 32, + AddMediaType: "application/vnd.oci.image.manifest.v1+json", + AddPlatform: &imgspecv1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"sse4"}}, + ListOperation: ListOpAdd}) + err = list.EditInstances(editInstances) + require.NoError(t, err) + + // Add new elements to the end of old list to maintain order + originalListOrder = append(originalListOrder, digest.Digest("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) + originalListOrder = append(originalListOrder, digest.Digest("sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc")) + // Verify order + assert.Equal(t, list.Instances(), originalListOrder) +} + +func TestSchema2ListFromManifest(t *testing.T) { + validManifest, err := os.ReadFile(filepath.Join("testdata", "v2list.manifest.json")) + require.NoError(t, err) + + parser := func(m []byte) error { + _, err := Schema2ListFromManifest(m) + return err + } + // Schema mismatch is rejected + testManifestFixturesAreRejected(t, parser, []string{ + "schema2-to-schema1-by-docker.json", + "v2s2.manifest.json", + "ociv1.manifest.json", + // Not "ociv1.image.index.json" yet, without validating mediaType the two are too similar to tell the difference. + }) + // Extra fields are rejected + testValidManifestWithExtraFieldsIsRejected(t, parser, validManifest, []string{"config", "fsLayers", "history", "layers"}) +} diff --git a/vendor/github.com/containers/image/v5/internal/manifest/list.go b/vendor/github.com/containers/image/v5/internal/manifest/list.go index 8786324ea40..58c03b8a35c 100644 --- a/vendor/github.com/containers/image/v5/internal/manifest/list.go +++ b/vendor/github.com/containers/image/v5/internal/manifest/list.go @@ -68,6 +68,7 @@ type ListUpdate struct { Digest digest.Digest Size int64 MediaType string + Platform *imgspecv1.Platform // read-only field: may be set by Instance(), ignored by UpdateInstance() } type ListOp int diff --git a/vendor/github.com/containers/image/v5/internal/manifest/list_test.go b/vendor/github.com/containers/image/v5/internal/manifest/list_test.go new file mode 100644 index 00000000000..2f1479b1adb --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/manifest/list_test.go @@ -0,0 +1,161 @@ +package manifest + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/containers/image/v5/types" + "github.com/opencontainers/go-digest" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func pare(m List) { + if impl, ok := m.(*OCI1Index); ok { + impl.Annotations = nil + } + if impl, ok := m.(*Schema2List); ok { + for i := range impl.Manifests { + impl.Manifests[i].Platform.Features = nil + } + } +} + +func TestParseLists(t *testing.T) { + cases := []struct { + path string + mimeType string + }{ + {"ociv1.image.index.json", imgspecv1.MediaTypeImageIndex}, + {"v2list.manifest.json", DockerV2ListMediaType}, + } + for _, c := range cases { + manifest, err := os.ReadFile(filepath.Join("testdata", c.path)) + require.NoError(t, err, "error reading file %q", filepath.Join("testdata", c.path)) + assert.Equal(t, GuessMIMEType(manifest), c.mimeType) + + // c/image/manifest.TestParseLists verifies that FromBlob refuses to parse the manifest list + + m, err := ListFromBlob(manifest, c.mimeType) + require.NoError(t, err, "manifest list %q should parse as list types", c.path) + assert.Equal(t, m.MIMEType(), c.mimeType, "manifest %q is not of the expected MIME type", c.path) + + clone := m.Clone() + assert.Equal(t, clone, m, "manifest %q is missing some fields after being cloned", c.path) + + pare(m) + + index, err := m.ConvertToMIMEType(imgspecv1.MediaTypeImageIndex) + require.NoError(t, err, "error converting %q to an OCI1Index", c.path) + + list, err := m.ConvertToMIMEType(DockerV2ListMediaType) + require.NoError(t, err, "error converting %q to an Schema2List", c.path) + + index2, err := list.ConvertToMIMEType(imgspecv1.MediaTypeImageIndex) + require.NoError(t, err) + assert.Equal(t, index, index2, "index %q lost data in conversion", c.path) + + list2, err := index.ConvertToMIMEType(DockerV2ListMediaType) + require.NoError(t, err) + assert.Equal(t, list, list2, "list %q lost data in conversion", c.path) + } +} + +func TestChooseInstance(t *testing.T) { + type expectedMatch struct { + arch, variant string + instanceDigest digest.Digest + } + chooseInstanceCalls := []func(sys *types.SystemContext, rawManifest []byte) (digest.Digest, error){ + func(sys *types.SystemContext, rawManifest []byte) (digest.Digest, error) { + list, err := ListPublicFromBlob(rawManifest, GuessMIMEType(rawManifest)) + require.NoError(t, err) + return list.ChooseInstance(sys) + }, + // Gzip preference true. + func(sys *types.SystemContext, rawManifest []byte) (digest.Digest, error) { + list, err := ListFromBlob(rawManifest, GuessMIMEType(rawManifest)) + require.NoError(t, err) + return list.ChooseInstanceByCompression(sys, types.OptionalBoolTrue) + }, + // Gzip preference false. + func(sys *types.SystemContext, rawManifest []byte) (digest.Digest, error) { + list, err := ListFromBlob(rawManifest, GuessMIMEType(rawManifest)) + require.NoError(t, err) + return list.ChooseInstanceByCompression(sys, types.OptionalBoolFalse) + }, + func(sys *types.SystemContext, rawManifest []byte) (digest.Digest, error) { + list, err := ListFromBlob(rawManifest, GuessMIMEType(rawManifest)) + require.NoError(t, err) + return list.ChooseInstanceByCompression(sys, types.OptionalBoolUndefined) + }, + } + for _, manifestList := range []struct { + listFile string + matchedInstances []expectedMatch + unmatchedInstances []string + }{ + { + listFile: "schema2list.json", + matchedInstances: []expectedMatch{ + {"amd64", "", "sha256:030fcb92e1487b18c974784dcc110a93147c9fc402188370fbfd17efabffc6af"}, + {"s390x", "", "sha256:e5aa1b0a24620228b75382997a0977f609b3ca3a95533dafdef84c74cc8df642"}, + {"arm", "v7", "sha256:b5dbad4bdb4444d919294afe49a095c23e86782f98cdf0aa286198ddb814b50b"}, + {"arm64", "", "sha256:dc472a59fb006797aa2a6bfb54cc9c57959bb0a6d11fadaa608df8c16dea39cf"}, + }, + unmatchedInstances: []string{ + "unmatched", + }, + }, + { // Focus on ARM variant field testing + listFile: "schema2list-variants.json", + matchedInstances: []expectedMatch{ + {"amd64", "", "sha256:59eec8837a4d942cc19a52b8c09ea75121acc38114a2c68b98983ce9356b8610"}, + {"arm", "v7", "sha256:f365626a556e58189fc21d099fc64603db0f440bff07f77c740989515c544a39"}, + {"arm", "v6", "sha256:f365626a556e58189fc21d099fc64603db0f440bff07f77c740989515c544a39"}, + {"arm", "v5", "sha256:c84b0a3a07b628bc4d62e5047d0f8dff80f7c00979e1e28a821a033ecda8fe53"}, + {"arm", "", "sha256:c84b0a3a07b628bc4d62e5047d0f8dff80f7c00979e1e28a821a033ecda8fe53"}, + {"arm", "unrecognized-present", "sha256:bcf9771c0b505e68c65440474179592ffdfa98790eb54ffbf129969c5e429990"}, + {"arm", "unrecognized-not-present", "sha256:c84b0a3a07b628bc4d62e5047d0f8dff80f7c00979e1e28a821a033ecda8fe53"}, + }, + unmatchedInstances: []string{ + "unmatched", + }, + }, + { + listFile: "oci1index.json", + matchedInstances: []expectedMatch{ + {"amd64", "", "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270"}, + {"ppc64le", "", "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f"}, + }, + unmatchedInstances: []string{ + "unmatched", + }, + }, + } { + rawManifest, err := os.ReadFile(filepath.Join("testdata", manifestList.listFile)) + require.NoError(t, err) + for _, chooseInstance := range chooseInstanceCalls { + for _, match := range manifestList.matchedInstances { + testName := fmt.Sprintf("%s %q+%q", manifestList.listFile, match.arch, match.variant) + digest, err := chooseInstance(&types.SystemContext{ + ArchitectureChoice: match.arch, + VariantChoice: match.variant, + OSChoice: "linux", + }, rawManifest) + require.NoError(t, err, testName) + assert.Equal(t, match.instanceDigest, digest, testName) + } + for _, arch := range manifestList.unmatchedInstances { + _, err := chooseInstance(&types.SystemContext{ + ArchitectureChoice: arch, + OSChoice: "linux", + }, rawManifest) + assert.Error(t, err) + } + } + } +} diff --git a/vendor/github.com/containers/image/v5/internal/manifest/manifest_test.go b/vendor/github.com/containers/image/v5/internal/manifest/manifest_test.go new file mode 100644 index 00000000000..8dc98791924 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/manifest/manifest_test.go @@ -0,0 +1,134 @@ +package manifest + +import ( + "os" + "path/filepath" + "testing" + + digest "github.com/opencontainers/go-digest" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + digestSha256EmptyTar = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +) + +func TestGuessMIMEType(t *testing.T) { + cases := []struct { + path string + mimeType string + }{ + {"v2s2.manifest.json", DockerV2Schema2MediaType}, + {"v2list.manifest.json", DockerV2ListMediaType}, + {"v2s1.manifest.json", DockerV2Schema1SignedMediaType}, + {"v2s1-unsigned.manifest.json", DockerV2Schema1MediaType}, + {"v2s1-invalid-signatures.manifest.json", DockerV2Schema1SignedMediaType}, + {"v2s2nomime.manifest.json", DockerV2Schema2MediaType}, // It is unclear whether this one is legal, but we should guess v2s2 if anything at all. + {"unknown-version.manifest.json", ""}, + {"non-json.manifest.json", ""}, // Not a manifest (nor JSON) at all + {"ociv1.manifest.json", imgspecv1.MediaTypeImageManifest}, + {"ociv1.artifact.json", imgspecv1.MediaTypeImageManifest}, + {"ociv1.image.index.json", imgspecv1.MediaTypeImageIndex}, + {"ociv1nomime.manifest.json", imgspecv1.MediaTypeImageManifest}, + {"ociv1nomime.artifact.json", imgspecv1.MediaTypeImageManifest}, + {"ociv1nomime.image.index.json", imgspecv1.MediaTypeImageIndex}, + } + + for _, c := range cases { + manifest, err := os.ReadFile(filepath.Join("testdata", c.path)) + require.NoError(t, err) + mimeType := GuessMIMEType(manifest) + assert.Equal(t, c.mimeType, mimeType, c.path) + } +} + +func TestDigest(t *testing.T) { + cases := []struct { + path string + expectedDigest digest.Digest + }{ + {"v2s2.manifest.json", TestDockerV2S2ManifestDigest}, + {"v2s1.manifest.json", TestDockerV2S1ManifestDigest}, + {"v2s1-unsigned.manifest.json", TestDockerV2S1UnsignedManifestDigest}, + } + for _, c := range cases { + manifest, err := os.ReadFile(filepath.Join("testdata", c.path)) + require.NoError(t, err) + actualDigest, err := Digest(manifest) + require.NoError(t, err) + assert.Equal(t, c.expectedDigest, actualDigest) + } + + manifest, err := os.ReadFile("testdata/v2s1-invalid-signatures.manifest.json") + require.NoError(t, err) + _, err = Digest(manifest) + assert.Error(t, err) + + actualDigest, err := Digest([]byte{}) + require.NoError(t, err) + assert.Equal(t, digest.Digest(digestSha256EmptyTar), actualDigest) +} + +func TestMatchesDigest(t *testing.T) { + cases := []struct { + path string + expectedDigest digest.Digest + result bool + }{ + // Success + {"v2s2.manifest.json", TestDockerV2S2ManifestDigest, true}, + {"v2s1.manifest.json", TestDockerV2S1ManifestDigest, true}, + // No match (switched s1/s2) + {"v2s2.manifest.json", TestDockerV2S1ManifestDigest, false}, + {"v2s1.manifest.json", TestDockerV2S2ManifestDigest, false}, + // Unrecognized algorithm + {"v2s2.manifest.json", digest.Digest("md5:2872f31c5c1f62a694fbd20c1e85257c"), false}, + // Mangled format + {"v2s2.manifest.json", digest.Digest(TestDockerV2S2ManifestDigest.String() + "abc"), false}, + {"v2s2.manifest.json", digest.Digest(TestDockerV2S2ManifestDigest.String()[:20]), false}, + {"v2s2.manifest.json", digest.Digest(""), false}, + } + for _, c := range cases { + manifest, err := os.ReadFile(filepath.Join("testdata", c.path)) + require.NoError(t, err) + res, err := MatchesDigest(manifest, c.expectedDigest) + require.NoError(t, err) + assert.Equal(t, c.result, res) + } + + manifest, err := os.ReadFile("testdata/v2s1-invalid-signatures.manifest.json") + require.NoError(t, err) + // Even a correct SHA256 hash is rejected if we can't strip the JSON signature. + res, err := MatchesDigest(manifest, digest.FromBytes(manifest)) + assert.False(t, res) + assert.Error(t, err) + + res, err = MatchesDigest([]byte{}, digest.Digest(digestSha256EmptyTar)) + assert.True(t, res) + assert.NoError(t, err) +} + +func TestNormalizedMIMEType(t *testing.T) { + for _, c := range []string{ // Valid MIME types, normalized to themselves + DockerV2Schema1MediaType, + DockerV2Schema1SignedMediaType, + DockerV2Schema2MediaType, + DockerV2ListMediaType, + imgspecv1.MediaTypeImageManifest, + imgspecv1.MediaTypeImageIndex, + } { + res := NormalizedMIMEType(c) + assert.Equal(t, c, res, c) + } + for _, c := range []string{ + "application/json", + "text/plain", + "not at all a valid MIME type", + "", + } { + res := NormalizedMIMEType(c) + assert.Equal(t, DockerV2Schema1SignedMediaType, res, c) + } +} diff --git a/vendor/github.com/containers/image/v5/internal/manifest/oci_index.go b/vendor/github.com/containers/image/v5/internal/manifest/oci_index.go index fd251d95121..d5c09292397 100644 --- a/vendor/github.com/containers/image/v5/internal/manifest/oci_index.go +++ b/vendor/github.com/containers/image/v5/internal/manifest/oci_index.go @@ -57,6 +57,7 @@ func (index *OCI1IndexPublic) Instance(instanceDigest digest.Digest) (ListUpdate Digest: manifest.Digest, Size: manifest.Size, MediaType: manifest.MediaType, + Platform: manifest.Platform, }, nil } } diff --git a/vendor/github.com/containers/image/v5/internal/manifest/oci_index_test.go b/vendor/github.com/containers/image/v5/internal/manifest/oci_index_test.go new file mode 100644 index 00000000000..ccaf68147dd --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/manifest/oci_index_test.go @@ -0,0 +1,259 @@ +package manifest + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/containers/image/v5/pkg/compression" + "github.com/containers/image/v5/types" + "github.com/opencontainers/go-digest" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestOCI1IndexPublicFromManifest(t *testing.T) { + validManifest, err := os.ReadFile(filepath.Join("testdata", "ociv1.image.index.json")) + require.NoError(t, err) + + parser := func(m []byte) error { + _, err := OCI1IndexPublicFromManifest(m) + return err + } + // Schema mismatch is rejected + testManifestFixturesAreRejected(t, parser, []string{ + "schema2-to-schema1-by-docker.json", + "v2s2.manifest.json", + // Not "v2list.manifest.json" yet, without mediaType the two are too similar to tell the difference. + "ociv1.manifest.json", + }) + // Extra fields are rejected + testValidManifestWithExtraFieldsIsRejected(t, parser, validManifest, []string{"config", "fsLayers", "history", "layers"}) +} + +func TestOCI1IndexFromManifest(t *testing.T) { + validManifest, err := os.ReadFile(filepath.Join("testdata", "ociv1.image.index.json")) + require.NoError(t, err) + + parser := func(m []byte) error { + _, err := OCI1IndexFromManifest(m) + return err + } + // Schema mismatch is rejected + testManifestFixturesAreRejected(t, parser, []string{ + "schema2-to-schema1-by-docker.json", + "v2s2.manifest.json", + // Not "v2list.manifest.json" yet, without mediaType the two are too similar to tell the difference. + "ociv1.manifest.json", + }) + // Extra fields are rejected + testValidManifestWithExtraFieldsIsRejected(t, parser, validManifest, []string{"config", "fsLayers", "history", "layers"}) +} + +func TestOCI1EditInstances(t *testing.T) { + validManifest, err := os.ReadFile(filepath.Join("testdata", "ociv1.image.index.json")) + require.NoError(t, err) + list, err := ListFromBlob(validManifest, GuessMIMEType(validManifest)) + require.NoError(t, err) + + expectedDigests := list.Instances() + editInstances := []ListEdit{} + editInstances = append(editInstances, ListEdit{ + UpdateOldDigest: list.Instances()[0], + UpdateDigest: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + UpdateSize: 32, + UpdateMediaType: "something", + ListOperation: ListOpUpdate}) + err = list.EditInstances(editInstances) + require.NoError(t, err) + + expectedDigests[0] = editInstances[0].UpdateDigest + // order of old elements must remain same. + assert.Equal(t, list.Instances(), expectedDigests) + + instance, err := list.Instance(list.Instances()[0]) + require.NoError(t, err) + assert.Equal(t, "something", instance.MediaType) + assert.Equal(t, int64(32), instance.Size) + // platform must match with what was set in `ociv1.image.index.json` for the first instance + assert.Equal(t, &imgspecv1.Platform{Architecture: "ppc64le", OS: "linux", OSVersion: "", OSFeatures: []string(nil), Variant: ""}, instance.Platform) + + // Create a fresh list + list, err = ListFromBlob(validManifest, GuessMIMEType(validManifest)) + require.NoError(t, err) + + // Verfiy correct zstd sorting + editInstances = []ListEdit{} + annotations := map[string]string{"io.github.containers.compression.zstd": "true"} + // without zstd + editInstances = append(editInstances, ListEdit{ + AddDigest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + AddSize: 32, + AddMediaType: "application/vnd.oci.image.manifest.v1+json", + AddPlatform: &imgspecv1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"sse4"}}, + ListOperation: ListOpAdd}) + // with zstd + editInstances = append(editInstances, ListEdit{ + AddDigest: "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + AddSize: 32, + AddMediaType: "application/vnd.oci.image.manifest.v1+json", + AddPlatform: &imgspecv1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"sse4"}}, + AddAnnotations: annotations, + ListOperation: ListOpAdd}) + // with zstd but with compression, annotation must be added automatically + editInstances = append(editInstances, ListEdit{ + AddDigest: "sha256:hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh", + AddSize: 32, + AddMediaType: "application/vnd.oci.image.manifest.v1+json", + AddPlatform: &imgspecv1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"sse4"}}, + AddCompressionAlgorithms: []compression.Algorithm{compression.Zstd}, + AddAnnotations: map[string]string{}, + ListOperation: ListOpAdd}) + // with zstd but with compression, annotation must be added automatically and AddAnnotations is unset + editInstances = append(editInstances, ListEdit{ + AddDigest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + AddSize: 32, + AddMediaType: "application/vnd.oci.image.manifest.v1+json", + AddPlatform: &imgspecv1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"sse4"}}, + AddCompressionAlgorithms: []compression.Algorithm{compression.Zstd}, + ListOperation: ListOpAdd}) + // without zstd + editInstances = append(editInstances, ListEdit{ + AddDigest: "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + AddSize: 32, + AddMediaType: "application/vnd.oci.image.manifest.v1+json", + AddPlatform: &imgspecv1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"sse4"}}, + ListOperation: ListOpAdd}) + err = list.EditInstances(editInstances) + require.NoError(t, err) + + // Zstd should be kept on lowest priority as compared to the default gzip ones and order of prior elements must be preserved. + assert.Equal(t, list.Instances(), []digest.Digest{digest.Digest("sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f"), digest.Digest("sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270"), digest.Digest("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), digest.Digest("sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"), digest.Digest("sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"), digest.Digest("sha256:hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh"), digest.Digest("sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")}) + + // Update list and remove zstd annotation from existing instance, and verify if resorting works + editInstances = []ListEdit{} + editInstances = append(editInstances, ListEdit{ + UpdateOldDigest: digest.Digest("sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), + UpdateDigest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + UpdateSize: 32, + UpdateMediaType: "application/vnd.oci.image.manifest.v1+json", + UpdateAffectAnnotations: true, + UpdateAnnotations: map[string]string{}, + ListOperation: ListOpUpdate}) + err = list.EditInstances(editInstances) + require.NoError(t, err) + // Digest `ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff` should be re-ordered on update. + assert.Equal(t, list.Instances(), []digest.Digest{digest.Digest("sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f"), digest.Digest("sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270"), digest.Digest("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), digest.Digest("sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"), digest.Digest("sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"), digest.Digest("sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"), digest.Digest("sha256:hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh")}) + +} + +func TestOCI1IndexChooseInstanceByCompression(t *testing.T) { + type expectedMatch struct { + arch, variant string + instanceDigest digest.Digest + preferGzip bool + } + for _, manifestList := range []struct { + listFile string + matchedInstances []expectedMatch + unmatchedInstances []string + }{ + { + listFile: "oci1.index.zstd-selection.json", + matchedInstances: []expectedMatch{ + // out of gzip and zstd in amd64 select the first zstd image + {"amd64", "", "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", false}, + // out of multiple gzip in arm64 select the first one to ensure original logic is prevented + {"arm64", "", "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", false}, + // select a signle gzip s390x image + {"s390x", "", "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", false}, + // out of gzip and zstd in amd64 select the first gzip image + {"amd64", "", "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true}, + // out of multiple gzip in arm64 select the first one to ensure original logic is prevented + {"arm64", "", "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", true}, + // select a signle gzip s390x image + {"s390x", "", "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", true}, + }, + unmatchedInstances: []string{ + "unmatched", + }, + }, + { // Focus on ARM variant field testing + listFile: "ocilist-variants.json", + matchedInstances: []expectedMatch{ + {"amd64", "", "sha256:59eec8837a4d942cc19a52b8c09ea75121acc38114a2c68b98983ce9356b8610", false}, + {"arm", "v7", "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", false}, + {"arm", "v6", "sha256:f365626a556e58189fc21d099fc64603db0f440bff07f77c740989515c544a39", false}, + {"arm", "v5", "sha256:c84b0a3a07b628bc4d62e5047d0f8dff80f7c00979e1e28a821a033ecda8fe53", false}, + {"arm", "", "sha256:c84b0a3a07b628bc4d62e5047d0f8dff80f7c00979e1e28a821a033ecda8fe53", false}, + {"arm", "unrecognized-present", "sha256:bcf9771c0b505e68c65440474179592ffdfa98790eb54ffbf129969c5e429990", false}, + {"arm", "unrecognized-not-present", "sha256:c84b0a3a07b628bc4d62e5047d0f8dff80f7c00979e1e28a821a033ecda8fe53", false}, + // preferGzip true + {"amd64", "", "sha256:59eec8837a4d942cc19a52b8c09ea75121acc38114a2c68b98983ce9356b8610", true}, + {"arm", "v7", "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", true}, + {"arm", "v6", "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", true}, + {"arm", "v5", "sha256:c84b0a3a07b628bc4d62e5047d0f8dff80f7c00979e1e28a821a033ecda8fe53", true}, + {"arm", "", "sha256:c84b0a3a07b628bc4d62e5047d0f8dff80f7c00979e1e28a821a033ecda8fe53", true}, + {"arm", "unrecognized-present", "sha256:bcf9771c0b505e68c65440474179592ffdfa98790eb54ffbf129969c5e429990", true}, + {"arm", "unrecognized-not-present", "sha256:c84b0a3a07b628bc4d62e5047d0f8dff80f7c00979e1e28a821a033ecda8fe53", true}, + }, + unmatchedInstances: []string{ + "unmatched", + }, + }, + { + listFile: "oci1.index.zstd-selection2.json", + // out of list where first instance is gzip , select the first occurance of zstd out of many + matchedInstances: []expectedMatch{ + {"amd64", "", "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", false}, + {"amd64", "", "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true}, + // must return first gzip even if the first entry is zstd + {"arm64", "", "sha256:6dc14a60d2ba724646cfbf5fccbb9a618a5978a64a352e060b17caf5e005da9d", true}, + // must return first zstd even if the first entry for same platform is gzip + {"arm64", "", "sha256:1c98002b30a71b08ab175915ce7c8fb8da9e9b502ae082d6f0c572bac9dee324", false}, + // must return first zstd instance agnostic of platform + {"", "", "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", false}, + // must return first gzip instance agnostic of platform + {"", "", "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true}, + // must return first zstd instance with no platform + {"matchesImageWithNoPlatform", "", "sha256:f2f5f52a2cf2c51d4cac6df0545f751c0adc3f3427eb47c59fcb32894503e18f", false}, + // must return first gzip instance with no platform + {"matchesImageWithNoPlatform", "", "sha256:c76757bb6006babdd8464dbf2f1157fdfa6fead0bc6f84f15816a32d6f68f706", true}, + }, + }, + { + listFile: "oci1index.json", + matchedInstances: []expectedMatch{ + {"amd64", "", "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", false}, + {"ppc64le", "", "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", false}, + }, + unmatchedInstances: []string{ + "unmatched", + }, + }, + } { + rawManifest, err := os.ReadFile(filepath.Join("testdata", manifestList.listFile)) + require.NoError(t, err) + list, err := ListFromBlob(rawManifest, GuessMIMEType(rawManifest)) + require.NoError(t, err) + for _, match := range manifestList.matchedInstances { + testName := fmt.Sprintf("%s %q+%q", manifestList.listFile, match.arch, match.variant) + digest, err := list.ChooseInstanceByCompression(&types.SystemContext{ + ArchitectureChoice: match.arch, + VariantChoice: match.variant, + OSChoice: "linux", + }, types.NewOptionalBool(match.preferGzip)) + require.NoError(t, err, testName) + assert.Equal(t, match.instanceDigest, digest, testName) + } + for _, arch := range manifestList.unmatchedInstances { + _, err := list.ChooseInstanceByCompression(&types.SystemContext{ + ArchitectureChoice: arch, + OSChoice: "linux", + }, types.NewOptionalBool(false)) + assert.Error(t, err) + } + } +} diff --git a/vendor/github.com/containers/image/v5/internal/manifest/testdata/non-json.manifest.json b/vendor/github.com/containers/image/v5/internal/manifest/testdata/non-json.manifest.json new file mode 100644 index 00000000000..f8927212757 Binary files /dev/null and b/vendor/github.com/containers/image/v5/internal/manifest/testdata/non-json.manifest.json differ diff --git a/vendor/github.com/containers/image/v5/internal/manifest/testdata/oci1.index.zstd-selection.json b/vendor/github.com/containers/image/v5/internal/manifest/testdata/oci1.index.zstd-selection.json new file mode 100644 index 00000000000..a55e6b4a34f --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/manifest/testdata/oci1.index.zstd-selection.json @@ -0,0 +1,66 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "size": 758, + "platform": { + "architecture": "amd64", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "size": 772, + "annotations": { + "io.github.containers.compression.zstd": "true" + }, + "platform": { + "architecture": "amd64", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "size": 758, + "platform": { + "architecture": "arm64", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "size": 758, + "platform": { + "architecture": "arm64", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "size": 758, + "platform": { + "architecture": "s390x", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg", + "size": 772, + "annotations": { + "io.github.containers.compression.zstd": "true" + }, + "platform": { + "architecture": "amd64", + "os": "linux" + } + } + ] +} diff --git a/vendor/github.com/containers/image/v5/internal/manifest/testdata/oci1.index.zstd-selection2.json b/vendor/github.com/containers/image/v5/internal/manifest/testdata/oci1.index.zstd-selection2.json new file mode 100644 index 00000000000..7153ccf5e79 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/manifest/testdata/oci1.index.zstd-selection2.json @@ -0,0 +1,96 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "size": 758, + "platform": { + "architecture": "amd64", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "size": 759, + "platform": { + "architecture": "amd64", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "size": 772, + "annotations": { + "io.github.containers.compression.zstd": "true" + }, + "platform": { + "architecture": "amd64", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:1c98002b30a71b08ab175915ce7c8fb8da9e9b502ae082d6f0c572bac9dee324", + "size": 772, + "annotations": { + "io.github.containers.compression.zstd": "true" + }, + "platform": { + "architecture": "arm64", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:c76757bb6006babdd8464dbf2f1157fdfa6fead0bc6f84f15816a32d6f68f706", + "size": 772 + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:f2f5f52a2cf2c51d4cac6df0545f751c0adc3f3427eb47c59fcb32894503e18f", + "size": 772, + "annotations": { + "io.github.containers.compression.zstd": "true" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:4612d27c6875f1dd3c28869dfd33c7be24f838261403cfb7940b76b6fd6ea4e2", + "size": 772 + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:6dc14a60d2ba724646cfbf5fccbb9a618a5978a64a352e060b17caf5e005da9d", + "size": 772, + "platform": { + "architecture": "arm64", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "size": 759, + "platform": { + "architecture": "amd64", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "size": 772, + "annotations": { + "io.github.containers.compression.zstd": "true" + }, + "platform": { + "architecture": "amd64", + "os": "linux" + } + } + ] +} diff --git a/vendor/github.com/containers/image/v5/internal/manifest/testdata/oci1index.json b/vendor/github.com/containers/image/v5/internal/manifest/testdata/oci1index.json new file mode 100644 index 00000000000..a85b4d889c1 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/manifest/testdata/oci1index.json @@ -0,0 +1,31 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7143, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + "platform": { + "architecture": "ppc64le", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7682, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + "platform": { + "architecture": "amd64", + "os": "linux", + "os.features": [ + "sse4" + ] + } + } + ], + "annotations": { + "com.example.key1": "value1", + "com.example.key2": "value2" + } +} diff --git a/vendor/github.com/containers/image/v5/internal/manifest/testdata/ocilist-variants.json b/vendor/github.com/containers/image/v5/internal/manifest/testdata/ocilist-variants.json new file mode 100644 index 00000000000..396ae045236 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/manifest/testdata/ocilist-variants.json @@ -0,0 +1,67 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 527, + "digest": "sha256:59eec8837a4d942cc19a52b8c09ea75121acc38114a2c68b98983ce9356b8610", + "platform": { + "architecture": "amd64", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 527, + "digest": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "platform": { + "architecture": "arm", + "variant": "v7", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 527, + "digest": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "platform": { + "architecture": "arm", + "variant": "v6", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 527, + "digest": "sha256:f365626a556e58189fc21d099fc64603db0f440bff07f77c740989515c544a39", + "annotations": { + "io.github.containers.compression.zstd": "true" + }, + "platform": { + "architecture": "arm", + "variant": "v6", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 527, + "digest": "sha256:bcf9771c0b505e68c65440474179592ffdfa98790eb54ffbf129969c5e429990", + "platform": { + "architecture": "arm", + "variant": "unrecognized-present", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 527, + "digest": "sha256:c84b0a3a07b628bc4d62e5047d0f8dff80f7c00979e1e28a821a033ecda8fe53", + "platform": { + "architecture": "arm", + "os": "linux" + } + } + ] +} diff --git a/vendor/github.com/containers/image/v5/internal/manifest/testdata/ociv1.artifact.json b/vendor/github.com/containers/image/v5/internal/manifest/testdata/ociv1.artifact.json new file mode 100644 index 00000000000..a53807924e3 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/manifest/testdata/ociv1.artifact.json @@ -0,0 +1,10 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.custom.artifact.config.v1+json", + "digest": "", + "size": 0 + }, + "layers": null +} diff --git a/vendor/github.com/containers/image/v5/internal/manifest/testdata/ociv1.image.index.json b/vendor/github.com/containers/image/v5/internal/manifest/testdata/ociv1.image.index.json new file mode 100644 index 00000000000..a85b4d889c1 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/manifest/testdata/ociv1.image.index.json @@ -0,0 +1,31 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7143, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + "platform": { + "architecture": "ppc64le", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7682, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + "platform": { + "architecture": "amd64", + "os": "linux", + "os.features": [ + "sse4" + ] + } + } + ], + "annotations": { + "com.example.key1": "value1", + "com.example.key2": "value2" + } +} diff --git a/vendor/github.com/containers/image/v5/internal/manifest/testdata/ociv1.manifest.json b/vendor/github.com/containers/image/v5/internal/manifest/testdata/ociv1.manifest.json new file mode 100644 index 00000000000..7e2e2e8274f --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/manifest/testdata/ociv1.manifest.json @@ -0,0 +1,30 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 7023, + "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 32654, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 16724, + "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 73109, + "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736" + } + ], + "annotations": { + "com.example.key1": "value1", + "com.example.key2": "value2" + } +} diff --git a/vendor/github.com/containers/image/v5/internal/manifest/testdata/ociv1nomime.artifact.json b/vendor/github.com/containers/image/v5/internal/manifest/testdata/ociv1nomime.artifact.json new file mode 100644 index 00000000000..5091d1f61e5 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/manifest/testdata/ociv1nomime.artifact.json @@ -0,0 +1,9 @@ +{ + "schemaVersion": 2, + "config": { + "mediaType": "application/vnd.oci.custom.artifact.config.v1+json", + "digest": "", + "size": 0 + }, + "layers": null +} diff --git a/vendor/github.com/containers/image/v5/internal/manifest/testdata/ociv1nomime.image.index.json b/vendor/github.com/containers/image/v5/internal/manifest/testdata/ociv1nomime.image.index.json new file mode 100644 index 00000000000..066f058db13 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/manifest/testdata/ociv1nomime.image.index.json @@ -0,0 +1,30 @@ +{ + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7143, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + "platform": { + "architecture": "ppc64le", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7682, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + "platform": { + "architecture": "amd64", + "os": "linux", + "os.features": [ + "sse4" + ] + } + } + ], + "annotations": { + "com.example.key1": "value1", + "com.example.key2": "value2" + } +} diff --git a/vendor/github.com/containers/image/v5/internal/manifest/testdata/ociv1nomime.manifest.json b/vendor/github.com/containers/image/v5/internal/manifest/testdata/ociv1nomime.manifest.json new file mode 100644 index 00000000000..1e1047ca7f3 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/manifest/testdata/ociv1nomime.manifest.json @@ -0,0 +1,29 @@ +{ + "schemaVersion": 2, + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 7023, + "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 32654, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 16724, + "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 73109, + "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736" + } + ], + "annotations": { + "com.example.key1": "value1", + "com.example.key2": "value2" + } +} diff --git a/vendor/github.com/containers/image/v5/internal/manifest/testdata/schema2-to-schema1-by-docker.json b/vendor/github.com/containers/image/v5/internal/manifest/testdata/schema2-to-schema1-by-docker.json new file mode 120000 index 00000000000..437a6be334d --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/manifest/testdata/schema2-to-schema1-by-docker.json @@ -0,0 +1 @@ +../../../internal/image/fixtures/schema2-to-schema1-by-docker.json \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/internal/manifest/testdata/schema2list-variants.json b/vendor/github.com/containers/image/v5/internal/manifest/testdata/schema2list-variants.json new file mode 100644 index 00000000000..0f5214cced5 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/manifest/testdata/schema2list-variants.json @@ -0,0 +1,44 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", + "manifests": [ + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "size": 527, + "digest": "sha256:59eec8837a4d942cc19a52b8c09ea75121acc38114a2c68b98983ce9356b8610", + "platform": { + "architecture": "amd64", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "size": 527, + "digest": "sha256:f365626a556e58189fc21d099fc64603db0f440bff07f77c740989515c544a39", + "platform": { + "architecture": "arm", + "variant": "v6", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "size": 527, + "digest": "sha256:bcf9771c0b505e68c65440474179592ffdfa98790eb54ffbf129969c5e429990", + "platform": { + "architecture": "arm", + "variant": "unrecognized-present", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "size": 527, + "digest": "sha256:c84b0a3a07b628bc4d62e5047d0f8dff80f7c00979e1e28a821a033ecda8fe53", + "platform": { + "architecture": "arm", + "os": "linux" + } + } + ] +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/internal/manifest/testdata/schema2list.json b/vendor/github.com/containers/image/v5/internal/manifest/testdata/schema2list.json new file mode 100644 index 00000000000..398b746cbd4 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/manifest/testdata/schema2list.json @@ -0,0 +1,72 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", + "manifests": [ + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "size": 527, + "digest": "sha256:030fcb92e1487b18c974784dcc110a93147c9fc402188370fbfd17efabffc6af", + "platform": { + "architecture": "amd64", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "size": 527, + "digest": "sha256:9142d97ef280a7953cf1a85716de49a24cc1dd62776352afad67e635331ff77a", + "platform": { + "architecture": "arm", + "os": "linux", + "variant": "v5" + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "size": 527, + "digest": "sha256:b5dbad4bdb4444d919294afe49a095c23e86782f98cdf0aa286198ddb814b50b", + "platform": { + "architecture": "arm", + "os": "linux", + "variant": "v6" + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "size": 527, + "digest": "sha256:dc472a59fb006797aa2a6bfb54cc9c57959bb0a6d11fadaa608df8c16dea39cf", + "platform": { + "architecture": "arm64", + "os": "linux", + "variant": "v8" + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "size": 527, + "digest": "sha256:9a33b9909e56b0a2092a65fb1b79ef6717fa160b1f084476b860418780e8d53b", + "platform": { + "architecture": "386", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "size": 528, + "digest": "sha256:59117d7c016fba6ede7f87991204bd672a1dca444102de66db632383507ed90b", + "platform": { + "architecture": "ppc64le", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "size": 528, + "digest": "sha256:e5aa1b0a24620228b75382997a0977f609b3ca3a95533dafdef84c74cc8df642", + "platform": { + "architecture": "s390x", + "os": "linux" + } + } + ] +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/internal/manifest/testdata/unknown-version.manifest.json b/vendor/github.com/containers/image/v5/internal/manifest/testdata/unknown-version.manifest.json new file mode 100644 index 00000000000..b0f34b631c7 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/manifest/testdata/unknown-version.manifest.json @@ -0,0 +1,5 @@ +{ + "schemaVersion": 99999, + "name": "mitr/noversion-nonsense", + "tag": "latest" +} diff --git a/vendor/github.com/containers/image/v5/internal/manifest/testdata/v2list.manifest.json b/vendor/github.com/containers/image/v5/internal/manifest/testdata/v2list.manifest.json new file mode 100644 index 00000000000..1bf9896e04b --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/manifest/testdata/v2list.manifest.json @@ -0,0 +1,56 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", + "manifests": [ + { + "mediaType": "application/vnd.docker.distribution.manifest.v1+json", + "size": 2094, + "digest": "sha256:7820f9a86d4ad15a2c4f0c0e5479298df2aa7c2f6871288e2ef8546f3e7b6783", + "platform": { + "architecture": "ppc64le", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v1+json", + "size": 1922, + "digest": "sha256:ae1b0e06e8ade3a11267564a26e750585ba2259c0ecab59ab165ad1af41d1bdd", + "platform": { + "architecture": "amd64", + "os": "linux", + "features": [ + "sse" + ] + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v1+json", + "size": 2084, + "digest": "sha256:e4c0df75810b953d6717b8f8f28298d73870e8aa2a0d5e77b8391f16fdfbbbe2", + "platform": { + "architecture": "s390x", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v1+json", + "size": 2084, + "digest": "sha256:07ebe243465ef4a667b78154ae6c3ea46fdb1582936aac3ac899ea311a701b40", + "platform": { + "architecture": "arm", + "os": "linux", + "variant": "armv7" + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v1+json", + "size": 2090, + "digest": "sha256:fb2fc0707b86dafa9959fe3d29e66af8787aee4d9a23581714be65db4265ad8a", + "platform": { + "architecture": "arm64", + "os": "linux", + "variant": "armv8" + } + } + ] +} diff --git a/vendor/github.com/containers/image/v5/internal/manifest/testdata/v2s1-invalid-signatures.manifest.json b/vendor/github.com/containers/image/v5/internal/manifest/testdata/v2s1-invalid-signatures.manifest.json new file mode 100644 index 00000000000..96def402320 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/manifest/testdata/v2s1-invalid-signatures.manifest.json @@ -0,0 +1,11 @@ +{ + "schemaVersion": 1, + "name": "mitr/busybox", + "tag": "latest", + "architecture": "amd64", + "fsLayers": [ + ], + "history": [ + ], + "signatures": 1 +} diff --git a/vendor/github.com/containers/image/v5/internal/manifest/testdata/v2s1-unsigned.manifest.json b/vendor/github.com/containers/image/v5/internal/manifest/testdata/v2s1-unsigned.manifest.json new file mode 100644 index 00000000000..16764b409e1 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/manifest/testdata/v2s1-unsigned.manifest.json @@ -0,0 +1,28 @@ +{ + "schemaVersion": 1, + "name": "mitr/busybox", + "tag": "latest", + "architecture": "amd64", + "fsLayers": [ + { + "blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + }, + { + "blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + }, + { + "blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + } + ], + "history": [ + { + "v1Compatibility": "{\"id\":\"f1b5eb0a1215f663765d509b6cdf3841bc2bcff0922346abb943d1342d469a97\",\"parent\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"created\":\"2016-03-03T11:29:44.222098366Z\",\"container\":\"c0924f5b281a1992127d0afc065e59548ded8880b08aea4debd56d4497acb17a\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) LABEL Checksum=4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\"],\"Image\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Checksum\":\"4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\",\"Name\":\"atomic-test-2\"}},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Checksum\":\"4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\",\"Name\":\"atomic-test-2\"}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" + }, + { + "v1Compatibility": "{\"id\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"parent\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"created\":\"2016-03-03T11:29:38.563048924Z\",\"container\":\"fd4cf54dcd239fbae9bdade9db48e41880b436d27cb5313f60952a46ab04deff\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) LABEL Name=atomic-test-2\"],\"Image\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Name\":\"atomic-test-2\"}},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Name\":\"atomic-test-2\"}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" + }, + { + "v1Compatibility": "{\"id\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"created\":\"2016-03-03T11:29:32.948089874Z\",\"container\":\"56f0fe1dfc95755dd6cda10f7215c9937a8d9c6348d079c581a261fd4c2f3a5f\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) MAINTAINER \\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\"],\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" + } + ] +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/internal/manifest/testdata/v2s1.manifest.json b/vendor/github.com/containers/image/v5/internal/manifest/testdata/v2s1.manifest.json new file mode 100644 index 00000000000..f7bcd074011 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/manifest/testdata/v2s1.manifest.json @@ -0,0 +1,44 @@ +{ + "schemaVersion": 1, + "name": "mitr/busybox", + "tag": "latest", + "architecture": "amd64", + "fsLayers": [ + { + "blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + }, + { + "blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + }, + { + "blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + } + ], + "history": [ + { + "v1Compatibility": "{\"id\":\"f1b5eb0a1215f663765d509b6cdf3841bc2bcff0922346abb943d1342d469a97\",\"parent\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"created\":\"2016-03-03T11:29:44.222098366Z\",\"container\":\"c0924f5b281a1992127d0afc065e59548ded8880b08aea4debd56d4497acb17a\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) LABEL Checksum=4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\"],\"Image\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Checksum\":\"4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\",\"Name\":\"atomic-test-2\"}},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Checksum\":\"4fef81d30f31f9213c642881357e6662846a0f884c2366c13ebad807b4031368 ./tests/test-images/Dockerfile.2\",\"Name\":\"atomic-test-2\"}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" + }, + { + "v1Compatibility": "{\"id\":\"594075be8d003f784074cc639d970d1fa091a8197850baaae5052c01564ac535\",\"parent\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"created\":\"2016-03-03T11:29:38.563048924Z\",\"container\":\"fd4cf54dcd239fbae9bdade9db48e41880b436d27cb5313f60952a46ab04deff\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) LABEL Name=atomic-test-2\"],\"Image\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Name\":\"atomic-test-2\"}},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{\"Name\":\"atomic-test-2\"}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" + }, + { + "v1Compatibility": "{\"id\":\"03dfa1cd1abe452bc2b69b8eb2362fa6beebc20893e65437906318954f6276d4\",\"created\":\"2016-03-03T11:29:32.948089874Z\",\"container\":\"56f0fe1dfc95755dd6cda10f7215c9937a8d9c6348d079c581a261fd4c2f3a5f\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) MAINTAINER \\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\"],\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" + } + ], + "signatures": [ + { + "header": { + "jwk": { + "crv": "P-256", + "kid": "OZ45:U3IG:TDOI:PMBD:NGP2:LDIW:II2U:PSBI:MMCZ:YZUP:TUUO:XPZT", + "kty": "EC", + "x": "ReC5c0J9tgXSdUL4_xzEt5RsD8kFt2wWSgJcpAcOQx8", + "y": "3sBGEqQ3ZMeqPKwQBAadN2toOUEASha18xa0WwsDF-M" + }, + "alg": "ES256" + }, + "signature": "dV1paJ3Ck1Ph4FcEhg_frjqxdlGdI6-ywRamk6CvMOcaOEUdCWCpCPQeBQpD2N6tGjkoG1BbstkFNflllfenCw", + "protected": "eyJmb3JtYXRMZW5ndGgiOjU0NzgsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNi0wNC0xOFQyMDo1NDo0MloifQ" + } + ] +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/internal/manifest/testdata/v2s2.manifest.json b/vendor/github.com/containers/image/v5/internal/manifest/testdata/v2s2.manifest.json new file mode 100644 index 00000000000..198da23f926 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/manifest/testdata/v2s2.manifest.json @@ -0,0 +1,26 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 7023, + "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 32654, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 16724, + "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 73109, + "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736" + } + ] +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/internal/manifest/testdata/v2s2nomime.manifest.json b/vendor/github.com/containers/image/v5/internal/manifest/testdata/v2s2nomime.manifest.json new file mode 100644 index 00000000000..a0b06c233b5 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/manifest/testdata/v2s2nomime.manifest.json @@ -0,0 +1,10 @@ +{ + "schemaVersion": 2, + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 7023, + "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" + }, + "layers": [ + ] +} diff --git a/vendor/github.com/containers/image/v5/internal/manifest/testdata_info_test.go b/vendor/github.com/containers/image/v5/internal/manifest/testdata_info_test.go new file mode 100644 index 00000000000..bfdaed1a0b4 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/manifest/testdata_info_test.go @@ -0,0 +1,12 @@ +package manifest + +import "github.com/opencontainers/go-digest" + +const ( + // TestV2S2ManifestDigest is the Docker manifest digest of "v2s2.manifest.json" + TestDockerV2S2ManifestDigest = digest.Digest("sha256:20bf21ed457b390829cdbeec8795a7bea1626991fda603e0d01b4e7f60427e55") + // TestV2S1ManifestDigest is the Docker manifest digest of "v2s1.manifest.json" + TestDockerV2S1ManifestDigest = digest.Digest("sha256:7364fea9d84ee548ab67d4c46c6006289800c98de3fbf8c0a97138dfcc23f000") + // TestV2S1UnsignedManifestDigest is the Docker manifest digest of "v2s1unsigned.manifest.json" + TestDockerV2S1UnsignedManifestDigest = digest.Digest("sha256:7364fea9d84ee548ab67d4c46c6006289800c98de3fbf8c0a97138dfcc23f000") +) diff --git a/vendor/github.com/containers/image/v5/internal/pkg/platform/platform_matcher_test.go b/vendor/github.com/containers/image/v5/internal/pkg/platform/platform_matcher_test.go new file mode 100644 index 00000000000..9647a344448 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/pkg/platform/platform_matcher_test.go @@ -0,0 +1,61 @@ +package platform + +import ( + "fmt" + "testing" + + "github.com/containers/image/v5/types" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/assert" +) + +func TestWantedPlatforms(t *testing.T) { + for _, c := range []struct { + ctx types.SystemContext + expected []imgspecv1.Platform + }{ + { // X86_64 does not have variants + types.SystemContext{ArchitectureChoice: "amd64", OSChoice: "linux"}, + []imgspecv1.Platform{ + {OS: "linux", Architecture: "amd64", Variant: ""}, + }, + }, + { // ARM with variant + types.SystemContext{ArchitectureChoice: "arm", OSChoice: "linux", VariantChoice: "v6"}, + []imgspecv1.Platform{ + {OS: "linux", Architecture: "arm", Variant: "v6"}, + {OS: "linux", Architecture: "arm", Variant: "v5"}, + {OS: "linux", Architecture: "arm", Variant: ""}, + }, + }, + { // ARM without variant + types.SystemContext{ArchitectureChoice: "arm", OSChoice: "linux"}, + []imgspecv1.Platform{ + {OS: "linux", Architecture: "arm", Variant: ""}, + {OS: "linux", Architecture: "arm", Variant: "v8"}, + {OS: "linux", Architecture: "arm", Variant: "v7"}, + {OS: "linux", Architecture: "arm", Variant: "v6"}, + {OS: "linux", Architecture: "arm", Variant: "v5"}, + }, + }, + { // ARM64 has a base variant + types.SystemContext{ArchitectureChoice: "arm64", OSChoice: "linux"}, + []imgspecv1.Platform{ + {OS: "linux", Architecture: "arm64", Variant: ""}, + {OS: "linux", Architecture: "arm64", Variant: "v8"}, + }, + }, + { // Custom (completely unrecognized data) + types.SystemContext{ArchitectureChoice: "armel", OSChoice: "freeBSD", VariantChoice: "custom"}, + []imgspecv1.Platform{ + {OS: "freeBSD", Architecture: "armel", Variant: "custom"}, + {OS: "freeBSD", Architecture: "armel", Variant: ""}, + }, + }, + } { + testName := fmt.Sprintf("%q/%q/%q", c.ctx.ArchitectureChoice, c.ctx.OSChoice, c.ctx.VariantChoice) + platforms, err := WantedPlatforms(&c.ctx) + assert.Nil(t, err, testName) + assert.Equal(t, c.expected, platforms, testName) + } +} diff --git a/vendor/github.com/containers/image/v5/internal/private/private.go b/vendor/github.com/containers/image/v5/internal/private/private.go index b1dd4ceb0de..95d561fcdd8 100644 --- a/vendor/github.com/containers/image/v5/internal/private/private.go +++ b/vendor/github.com/containers/image/v5/internal/private/private.go @@ -112,10 +112,11 @@ type TryReusingBlobOptions struct { // Transports, OTOH, MUST support these fields being zero-valued for types.ImageDestination callers // if they use internal/imagedestination/impl.Compat; // in that case, they will all be consistently zero-valued. - - EmptyLayer bool // True if the blob is an "empty"/"throwaway" layer, and may not necessarily be physically represented. - LayerIndex *int // If the blob is a layer, a zero-based index of the layer within the image; nil otherwise. - SrcRef reference.Named // A reference to the source image that contains the input blob. + RequiredCompression *compression.Algorithm // If set, reuse blobs with a matching algorithm as per implementations in internal/imagedestination/impl.helpers.go + OriginalCompression *compression.Algorithm // Must be set if RequiredCompression is set; can be set to nil to indicate “uncompressed” or “unknown”. + EmptyLayer bool // True if the blob is an "empty"/"throwaway" layer, and may not necessarily be physically represented. + LayerIndex *int // If the blob is a layer, a zero-based index of the layer within the image; nil otherwise. + SrcRef reference.Named // A reference to the source image that contains the input blob. } // ReusedBlob is information about a blob reused in a destination. diff --git a/vendor/github.com/containers/image/v5/internal/putblobdigest/put_blob_digest_test.go b/vendor/github.com/containers/image/v5/internal/putblobdigest/put_blob_digest_test.go new file mode 100644 index 00000000000..eb8ebbc05ae --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/putblobdigest/put_blob_digest_test.go @@ -0,0 +1,74 @@ +package putblobdigest + +import ( + "bytes" + "io" + "testing" + + "github.com/containers/image/v5/types" + "github.com/opencontainers/go-digest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var testData = []byte("test data") + +type testCase struct { + inputDigest digest.Digest + computesDigest bool + expectedDigest digest.Digest +} + +func testDigester(t *testing.T, constructor func(io.Reader, types.BlobInfo) (Digester, io.Reader), + cases []testCase) { + for _, c := range cases { + stream := bytes.NewReader(testData) + digester, newStream := constructor(stream, types.BlobInfo{Digest: c.inputDigest}) + assert.Equal(t, c.computesDigest, newStream != stream, c.inputDigest) + data, err := io.ReadAll(newStream) + require.NoError(t, err, c.inputDigest) + assert.Equal(t, testData, data, c.inputDigest) + digest := digester.Digest() + assert.Equal(t, c.expectedDigest, digest, c.inputDigest) + } +} + +func TestDigestIfUnknown(t *testing.T) { + testDigester(t, DigestIfUnknown, []testCase{ + { + inputDigest: digest.Digest("sha256:uninspected-value"), + computesDigest: false, + expectedDigest: digest.Digest("sha256:uninspected-value"), + }, + { + inputDigest: digest.Digest("unknown-algorithm:uninspected-value"), + computesDigest: false, + expectedDigest: digest.Digest("unknown-algorithm:uninspected-value"), + }, + { + inputDigest: "", + computesDigest: true, + expectedDigest: digest.Canonical.FromBytes(testData), + }, + }) +} + +func TestDigestIfCanonicalUnknown(t *testing.T) { + testDigester(t, DigestIfCanonicalUnknown, []testCase{ + { + inputDigest: digest.Digest("sha256:uninspected-value"), + computesDigest: false, + expectedDigest: digest.Digest("sha256:uninspected-value"), + }, + { + inputDigest: digest.Digest("unknown-algorithm:uninspected-value"), + computesDigest: true, + expectedDigest: digest.Canonical.FromBytes(testData), + }, + { + inputDigest: "", + computesDigest: true, + expectedDigest: digest.Canonical.FromBytes(testData), + }, + }) +} diff --git a/vendor/github.com/containers/image/v5/internal/set/set_test.go b/vendor/github.com/containers/image/v5/internal/set/set_test.go new file mode 100644 index 00000000000..f568730701e --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/set/set_test.go @@ -0,0 +1,66 @@ +package set + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNew(t *testing.T) { + s := New[int]() + assert.True(t, s.Empty()) +} + +func TestNewWithValues(t *testing.T) { + s := NewWithValues(1, 3) + assert.True(t, s.Contains(1)) + assert.False(t, s.Contains(2)) + assert.True(t, s.Contains(3)) +} + +func TestAdd(t *testing.T) { + s := NewWithValues(1) + assert.False(t, s.Contains(2)) + s.Add(2) + assert.True(t, s.Contains(2)) + s.Add(2) // Adding an already-present element + assert.True(t, s.Contains(2)) + // Unrelated elements are unaffected + assert.True(t, s.Contains(1)) + assert.False(t, s.Contains(3)) +} + +func TestDelete(t *testing.T) { + s := NewWithValues(1, 2) + assert.True(t, s.Contains(2)) + s.Delete(2) + assert.False(t, s.Contains(2)) + s.Delete(2) // Deleting a missing element + assert.False(t, s.Contains(2)) + // Unrelated elements are unaffected + assert.True(t, s.Contains(1)) +} + +func TestContains(t *testing.T) { + s := NewWithValues(1, 2) + assert.True(t, s.Contains(1)) + assert.True(t, s.Contains(2)) + assert.False(t, s.Contains(3)) +} + +func TestEmpty(t *testing.T) { + s := New[int]() + assert.True(t, s.Empty()) + s.Add(1) + assert.False(t, s.Empty()) + s.Delete(1) + assert.True(t, s.Empty()) +} + +func TestValues(t *testing.T) { + s := New[int]() + assert.Empty(t, s.Values()) + s.Add(1) + s.Add(2) + assert.ElementsMatch(t, []int{1, 2}, s.Values()) +} diff --git a/vendor/github.com/containers/image/v5/internal/signature/signature_test.go b/vendor/github.com/containers/image/v5/internal/signature/signature_test.go new file mode 100644 index 00000000000..924c32d85eb --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/signature/signature_test.go @@ -0,0 +1,94 @@ +package signature + +import ( + "bytes" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBlobSimpleSigning(t *testing.T) { + simpleSigData, err := os.ReadFile("testdata/simple.signature") + require.NoError(t, err) + simpleSig := SimpleSigningFromBlob(simpleSigData) + + simpleBlob, err := Blob(simpleSig) + require.NoError(t, err) + assert.Equal(t, simpleSigData, simpleBlob) + + fromBlob, err := FromBlob(simpleBlob) + require.NoError(t, err) + fromBlobSimple, ok := fromBlob.(SimpleSigning) + require.True(t, ok) + assert.Equal(t, simpleSigData, fromBlobSimple.UntrustedSignature()) + + // Using the newer format is accepted as well. + fromBlob, err = FromBlob(append([]byte("\x00simple-signing\n"), simpleSigData...)) + require.NoError(t, err) + fromBlobSimple, ok = fromBlob.(SimpleSigning) + require.True(t, ok) + assert.Equal(t, simpleSigData, fromBlobSimple.UntrustedSignature()) + +} + +func TestBlobSigstore(t *testing.T) { + sigstoreSig := SigstoreFromComponents("mime-type", []byte("payload"), + map[string]string{"a": "b", "c": "d"}) + + sigstoreBlob, err := Blob(sigstoreSig) + require.NoError(t, err) + assert.True(t, bytes.HasPrefix(sigstoreBlob, []byte("\x00sigstore-json\n{"))) + + fromBlob, err := FromBlob(sigstoreBlob) + require.NoError(t, err) + fromBlobSigstore, ok := fromBlob.(Sigstore) + require.True(t, ok) + assert.Equal(t, sigstoreSig.UntrustedMIMEType(), fromBlobSigstore.UntrustedMIMEType()) + assert.Equal(t, sigstoreSig.UntrustedPayload(), fromBlobSigstore.UntrustedPayload()) + assert.Equal(t, sigstoreSig.UntrustedAnnotations(), fromBlobSigstore.UntrustedAnnotations()) +} + +func TestFromBlobInvalid(t *testing.T) { + // Round-tripping valid data has been tested in TestBlobSimpleSigning and TestBlobSigstore above. + for _, c := range []string{ + "", // Empty + "\xFFsimple-signing\nhello", // Invalid first byte + "\x00simple-signing", // No newline + "\x00format\xFFname\ndata", // Non-ASCII format value + "\x00unknown-format\ndata", // Unknown format + } { + _, err := FromBlob([]byte(c)) + assert.Error(t, err, fmt.Sprintf("%#v", c)) + } +} + +// mockFormatSignature returns a specified format +type mockFormatSignature struct { + fmt FormatID +} + +func (ms mockFormatSignature) FormatID() FormatID { + return ms.fmt +} + +func (ms mockFormatSignature) blobChunk() ([]byte, error) { + panic("Unexpected call to a mock function") +} + +func TestUnsuportedFormatError(t *testing.T) { + // Warning: The exact text returned by the function is not an API commitment. + for _, c := range []struct { + input Signature + expected string + }{ + {SimpleSigningFromBlob(nil), "unsupported signature format simple-signing"}, + {SigstoreFromComponents("mime-type", nil, nil), "unsupported signature format sigstore-json"}, + {mockFormatSignature{FormatID("invalid")}, `unsupported, and unrecognized, signature format "invalid"`}, + } { + res := UnsupportedFormatError(c.input) + assert.Equal(t, c.expected, res.Error(), string(c.input.FormatID())) + } +} diff --git a/vendor/github.com/containers/image/v5/internal/signature/sigstore_test.go b/vendor/github.com/containers/image/v5/internal/signature/sigstore_test.go new file mode 100644 index 00000000000..c1d3c9b90d6 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/signature/sigstore_test.go @@ -0,0 +1,71 @@ +package signature + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSigstoreFromComponents(t *testing.T) { + const mimeType = "mime-type" + payload := []byte("payload") + annotations := map[string]string{"a": "b", "c": "d"} + + sig := SigstoreFromComponents(mimeType, payload, annotations) + assert.Equal(t, Sigstore{ + untrustedMIMEType: mimeType, + untrustedPayload: payload, + untrustedAnnotations: annotations, + }, sig) +} + +func TestSigstoreFromBlobChunk(t *testing.T) { + // Success + json := []byte(`{"mimeType":"mime-type","payload":"cGF5bG9hZA==", "annotations":{"a":"b","c":"d"}}`) + res, err := sigstoreFromBlobChunk(json) + require.NoError(t, err) + assert.Equal(t, "mime-type", res.UntrustedMIMEType()) + assert.Equal(t, []byte("payload"), res.UntrustedPayload()) + assert.Equal(t, map[string]string{"a": "b", "c": "d"}, res.UntrustedAnnotations()) + + // Invalid JSON + _, err = sigstoreFromBlobChunk([]byte("&")) + assert.Error(t, err) +} + +func TestSigstoreFormatID(t *testing.T) { + sig := SigstoreFromComponents("mime-type", []byte("payload"), + map[string]string{"a": "b", "c": "d"}) + assert.Equal(t, SigstoreFormat, sig.FormatID()) +} + +func TestSigstoreBlobChunk(t *testing.T) { + sig := SigstoreFromComponents("mime-type", []byte("payload"), + map[string]string{"a": "b", "c": "d"}) + res, err := sig.blobChunk() + require.NoError(t, err) + + expectedJSON := []byte(`{"mimeType":"mime-type","payload":"cGF5bG9hZA==", "annotations":{"a":"b","c":"d"}}`) + // Don’t directly compare the JSON representation so that we don’t test for formatting differences, just verify that it contains exactly the expected data. + var raw, expectedRaw map[string]any + err = json.Unmarshal(res, &raw) + require.NoError(t, err) + err = json.Unmarshal(expectedJSON, &expectedRaw) + require.NoError(t, err) + assert.Equal(t, expectedRaw, raw) +} + +func TestSigstoreUntrustedPayload(t *testing.T) { + var payload = []byte("payload") + sig := SigstoreFromComponents("mime-type", payload, + map[string]string{"a": "b", "c": "d"}) + assert.Equal(t, payload, sig.UntrustedPayload()) +} + +func TestSigstoreUntrustedAnnotations(t *testing.T) { + annotations := map[string]string{"a": "b", "c": "d"} + sig := SigstoreFromComponents("mime-type", []byte("payload"), annotations) + assert.Equal(t, annotations, sig.UntrustedAnnotations()) +} diff --git a/vendor/github.com/containers/image/v5/internal/signature/simple_test.go b/vendor/github.com/containers/image/v5/internal/signature/simple_test.go new file mode 100644 index 00000000000..76537e21d93 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/signature/simple_test.go @@ -0,0 +1,36 @@ +package signature + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSimpleSigningFromBlob(t *testing.T) { + var data = []byte("some contents") + + sig := SimpleSigningFromBlob(data) + assert.Equal(t, SimpleSigning{untrustedSignature: data}, sig) +} + +func TestSimpleSigningFormatID(t *testing.T) { + sig := SimpleSigningFromBlob([]byte("some contents")) + assert.Equal(t, SimpleSigningFormat, sig.FormatID()) +} + +func TestSimpleSigningBlobChunk(t *testing.T) { + var data = []byte("some contents") + + sig := SimpleSigningFromBlob(data) + chunk, err := sig.blobChunk() + require.NoError(t, err) + assert.Equal(t, data, chunk) +} + +func TestSimpleSigningUntrustedSignature(t *testing.T) { + var data = []byte("some contents") + + sig := SimpleSigningFromBlob(data) + assert.Equal(t, data, sig.UntrustedSignature()) +} diff --git a/vendor/github.com/containers/image/v5/internal/signature/testdata/simple.signature b/vendor/github.com/containers/image/v5/internal/signature/testdata/simple.signature new file mode 120000 index 00000000000..dae8bd52870 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/signature/testdata/simple.signature @@ -0,0 +1 @@ +../../../signature/fixtures/image.signature \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/internal/signer/signer_test.go b/vendor/github.com/containers/image/v5/internal/signer/signer_test.go new file mode 100644 index 00000000000..eaa18cf8ae8 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/signer/signer_test.go @@ -0,0 +1,87 @@ +package signer + +import ( + "context" + "errors" + "testing" + + "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/internal/signature" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// mockSignerImplementation is a SignerImplementation used only for tests. +type mockSignerImplementation struct { + progressMessage func() string + signImageManifest func(ctx context.Context, m []byte, dockerReference reference.Named) (signature.Signature, error) + close func() error +} + +func (ms *mockSignerImplementation) Close() error { + return ms.close() +} + +func (ms *mockSignerImplementation) ProgressMessage() string { + return ms.progressMessage() +} + +func (ms *mockSignerImplementation) SignImageManifest(ctx context.Context, m []byte, dockerReference reference.Named) (signature.Signature, error) { + return ms.signImageManifest(ctx, m, dockerReference) +} + +func TestNewSigner(t *testing.T) { + closeError := errors.New("unique error") + + si := mockSignerImplementation{ + // Other functions are nil, so this ensures they are not called. + close: func() error { return closeError }, + } + s := NewSigner(&si) + // Verify SignerImplementation methods are not visible even to determined callers + _, visible := any(s).(SignerImplementation) + assert.False(t, visible) + err := s.Close() + assert.Equal(t, closeError, err) +} + +func TestProgressMessage(t *testing.T) { + si := mockSignerImplementation{ + // Other functions are nil, so this ensures they are not called. + close: func() error { return nil }, + } + s := NewSigner(&si) + defer s.Close() + + const testMessage = "some unique string" + si.progressMessage = func() string { + return testMessage + } + message := ProgressMessage(s) + assert.Equal(t, testMessage, message) +} + +func TestSignImageManifest(t *testing.T) { + si := mockSignerImplementation{ + // Other functions are nil, so this ensures they are not called. + close: func() error { return nil }, + } + s := NewSigner(&si) + defer s.Close() + + testManifest := []byte("some manifest") + testDR, err := reference.ParseNormalizedNamed("busybox") + require.NoError(t, err) + testContext := context.WithValue(context.Background(), struct{}{}, "make this context unique") + testSig := signature.SigstoreFromComponents(signature.SigstoreSignatureMIMEType, []byte("payload"), nil) + testErr := errors.New("some unique error") + si.signImageManifest = func(ctx context.Context, m []byte, dockerReference reference.Named) (signature.Signature, error) { + assert.Equal(t, testContext, ctx) + assert.Equal(t, testManifest, m) + assert.Equal(t, testDR, dockerReference) + return testSig, testErr + } + sig, err := SignImageManifest(testContext, s, testManifest, testDR) + assert.Equal(t, testSig, sig) + assert.Equal(t, testErr, err) +} diff --git a/vendor/github.com/containers/image/v5/internal/streamdigest/fixtures/Hello.uncompressed b/vendor/github.com/containers/image/v5/internal/streamdigest/fixtures/Hello.uncompressed new file mode 100644 index 00000000000..5ab2f8a4323 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/streamdigest/fixtures/Hello.uncompressed @@ -0,0 +1 @@ +Hello \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/internal/streamdigest/stream_digest_test.go b/vendor/github.com/containers/image/v5/internal/streamdigest/stream_digest_test.go new file mode 100644 index 00000000000..b0a1327d3b7 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/streamdigest/stream_digest_test.go @@ -0,0 +1,36 @@ +package streamdigest + +import ( + "io" + "os" + "testing" + + "github.com/containers/image/v5/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestComputeBlobInfo(t *testing.T) { + inputInfo := types.BlobInfo{Digest: "", Size: -1} + fixtureFname := "fixtures/Hello.uncompressed" + fixtureInfo := types.BlobInfo{Digest: "sha256:185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969", Size: 5} + fixtureBytes := []byte("Hello") + + // open fixture + stream, err := os.Open(fixtureFname) + require.NoError(t, err, fixtureFname) + defer stream.Close() + + // fill in Digest and Size for inputInfo + streamCopy, cleanup, err := ComputeBlobInfo(nil, stream, &inputInfo) + require.NoError(t, err) + defer cleanup() + + // ensure inputInfo has been filled in with Digest and Size of fixture + assert.Equal(t, inputInfo, fixtureInfo) + + // ensure streamCopy is the same as fixture + b, err := io.ReadAll(streamCopy) + require.NoError(t, err) + assert.Equal(t, b, fixtureBytes) +} diff --git a/vendor/github.com/containers/image/v5/internal/testing/explicitfilepath-tmpdir/tmpdir.go b/vendor/github.com/containers/image/v5/internal/testing/explicitfilepath-tmpdir/tmpdir.go new file mode 100644 index 00000000000..a47ada4b735 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/testing/explicitfilepath-tmpdir/tmpdir.go @@ -0,0 +1,29 @@ +// Package tmpdir is a TESTING-ONLY utility. +// +// Some tests directly or indirectly exercising the directory/explicitfilepath +// subpackage expect the path returned by os.MkdirTemp to be canonical in the +// directory/explicitfilepath sense (absolute, no symlinks, cleaned up). +// +// os.MkdirTemp uses $TMPDIR by default, and on macOS, $TMPDIR is by +// default set to /var/folders/…, with /var a symlink to /private/var , +// which does not match our expectations. So, tests which want to use +// os.MkdirTemp that way, can +// import _ "github.com/containers/image/internal/testing/explicitfilepath-tmpdir" +// to ensure that $TMPDIR is canonical and usable as a base for testing +// path canonicalization in its subdirectories. +// +// NEVER use this in non-testing subpackages! +package tmpdir + +import ( + "os" + "path/filepath" +) + +func init() { + tmpDir := os.TempDir() + explicitTmpDir, err := filepath.EvalSymlinks(tmpDir) + if err == nil { + os.Setenv("TMPDIR", explicitTmpDir) + } +} diff --git a/vendor/github.com/containers/image/v5/internal/testing/gpgagent/gpg_agent.go b/vendor/github.com/containers/image/v5/internal/testing/gpgagent/gpg_agent.go new file mode 100644 index 00000000000..3de34eb6627 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/testing/gpgagent/gpg_agent.go @@ -0,0 +1,14 @@ +package gpgagent + +import ( + "os" + "os/exec" +) + +// Kill the running gpg-agent to drop unlocked keys. +// This is useful to ensure tests don’t leave processes around (in TestMain), or for testing handling of invalid passphrases. +func KillGPGAgent(gpgHomeDir string) error { + cmd := exec.Command("gpgconf", "--kill", "gpg-agent") + cmd.Env = append(os.Environ(), "GNUPGHOME="+gpgHomeDir) + return cmd.Run() +} diff --git a/vendor/github.com/containers/image/v5/internal/testing/mocks/image_reference.go b/vendor/github.com/containers/image/v5/internal/testing/mocks/image_reference.go new file mode 100644 index 00000000000..bf01d004787 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/testing/mocks/image_reference.go @@ -0,0 +1,56 @@ +package mocks + +import ( + "context" + + "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/types" +) + +// ForbiddenImageReference is used when we don’t expect the ImageReference to be used in our tests. +type ForbiddenImageReference struct{} + +// Transport is a mock that panics. +func (ref ForbiddenImageReference) Transport() types.ImageTransport { + panic("unexpected call to a mock function") +} + +// StringWithinTransport is a mock that panics. +func (ref ForbiddenImageReference) StringWithinTransport() string { + panic("unexpected call to a mock function") +} + +// DockerReference is a mock that panics. +func (ref ForbiddenImageReference) DockerReference() reference.Named { + panic("unexpected call to a mock function") +} + +// PolicyConfigurationIdentity is a mock that panics. +func (ref ForbiddenImageReference) PolicyConfigurationIdentity() string { + panic("unexpected call to a mock function") +} + +// PolicyConfigurationNamespaces is a mock that panics. +func (ref ForbiddenImageReference) PolicyConfigurationNamespaces() []string { + panic("unexpected call to a mock function") +} + +// NewImage is a mock that panics. +func (ref ForbiddenImageReference) NewImage(ctx context.Context, sys *types.SystemContext) (types.ImageCloser, error) { + panic("unexpected call to a mock function") +} + +// NewImageSource is a mock that panics. +func (ref ForbiddenImageReference) NewImageSource(ctx context.Context, sys *types.SystemContext) (types.ImageSource, error) { + panic("unexpected call to a mock function") +} + +// NewImageDestination is a mock that panics. +func (ref ForbiddenImageReference) NewImageDestination(ctx context.Context, sys *types.SystemContext) (types.ImageDestination, error) { + panic("unexpected call to a mock function") +} + +// DeleteImage is a mock that panics. +func (ref ForbiddenImageReference) DeleteImage(ctx context.Context, sys *types.SystemContext) error { + panic("unexpected call to a mock function") +} diff --git a/vendor/github.com/containers/image/v5/internal/testing/mocks/image_source.go b/vendor/github.com/containers/image/v5/internal/testing/mocks/image_source.go new file mode 100644 index 00000000000..754f5f176e1 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/testing/mocks/image_source.go @@ -0,0 +1,47 @@ +package mocks + +import ( + "context" + "io" + + "github.com/containers/image/v5/types" + "github.com/opencontainers/go-digest" +) + +// ForbiddenImageSource is used when we don't expect the ImageSource to be used in our tests. +type ForbiddenImageSource struct{} + +// Reference is a mock that panics. +func (f ForbiddenImageSource) Reference() types.ImageReference { + panic("Unexpected call to a mock function") +} + +// Close is a mock that panics. +func (f ForbiddenImageSource) Close() error { + panic("Unexpected call to a mock function") +} + +// GetManifest is a mock that panics. +func (f ForbiddenImageSource) GetManifest(context.Context, *digest.Digest) ([]byte, string, error) { + panic("Unexpected call to a mock function") +} + +// GetBlob is a mock that panics. +func (f ForbiddenImageSource) GetBlob(context.Context, types.BlobInfo, types.BlobInfoCache) (io.ReadCloser, int64, error) { + panic("Unexpected call to a mock function") +} + +// HasThreadSafeGetBlob is a mock that panics. +func (f ForbiddenImageSource) HasThreadSafeGetBlob() bool { + panic("Unexpected call to a mock function") +} + +// GetSignatures is a mock that panics. +func (f ForbiddenImageSource) GetSignatures(context.Context, *digest.Digest) ([][]byte, error) { + panic("Unexpected call to a mock function") +} + +// LayerInfosForCopy is a mock that panics. +func (f ForbiddenImageSource) LayerInfosForCopy(context.Context, *digest.Digest) ([]types.BlobInfo, error) { + panic("Unexpected call to a mock function") +} diff --git a/vendor/github.com/containers/image/v5/internal/testing/mocks/image_transport.go b/vendor/github.com/containers/image/v5/internal/testing/mocks/image_transport.go new file mode 100644 index 00000000000..c551949f747 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/testing/mocks/image_transport.go @@ -0,0 +1,24 @@ +package mocks + +import "github.com/containers/image/v5/types" + +// NameImageTransport is a mock of types.ImageTransport which returns itself in Name. +type NameImageTransport string + +// Name returns the name of the transport, which must be unique among other transports. +func (name NameImageTransport) Name() string { + return string(name) +} + +// ParseReference converts a string, which should not start with the ImageTransport.Name prefix, into an ImageReference. +func (name NameImageTransport) ParseReference(reference string) (types.ImageReference, error) { + panic("unexpected call to a mock function") +} + +// ValidatePolicyConfigurationScope checks that scope is a valid name for a signature.PolicyTransportScopes keys +// (i.e. a valid PolicyConfigurationIdentity() or PolicyConfigurationNamespaces() return value). +// It is acceptable to allow an invalid value which will never be matched, it can "only" cause user confusion. +// scope passed to this function will not be "", that value is always allowed. +func (name NameImageTransport) ValidatePolicyConfigurationScope(scope string) error { + panic("unexpected call to a mock function") +} diff --git a/vendor/github.com/containers/image/v5/internal/testing/mocks/unparsed_image.go b/vendor/github.com/containers/image/v5/internal/testing/mocks/unparsed_image.go new file mode 100644 index 00000000000..a2e2f84cd28 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/testing/mocks/unparsed_image.go @@ -0,0 +1,31 @@ +package mocks + +import ( + "context" + + "github.com/containers/image/v5/internal/signature" + "github.com/containers/image/v5/types" +) + +// ForbiddenUnparsedImage is used when we don't expect the UnparsedImage to be used in our tests. +type ForbiddenUnparsedImage struct{} + +// Reference is a mock that panics. +func (ref ForbiddenUnparsedImage) Reference() types.ImageReference { + panic("unexpected call to a mock function") +} + +// Manifest is a mock that panics. +func (ref ForbiddenUnparsedImage) Manifest(ctx context.Context) ([]byte, string, error) { + panic("unexpected call to a mock function") +} + +// Signatures is a mock that panics. +func (ref ForbiddenUnparsedImage) Signatures(context.Context) ([][]byte, error) { + panic("unexpected call to a mock function") +} + +// UntrustedSignatures is a mock that panics. +func (ref ForbiddenUnparsedImage) UntrustedSignatures(ctx context.Context) ([]signature.Signature, error) { + panic("unexpected call to a mock function") +} diff --git a/vendor/github.com/containers/image/v5/internal/uploadreader/upload_reader_test.go b/vendor/github.com/containers/image/v5/internal/uploadreader/upload_reader_test.go new file mode 100644 index 00000000000..c91967a15d0 --- /dev/null +++ b/vendor/github.com/containers/image/v5/internal/uploadreader/upload_reader_test.go @@ -0,0 +1,34 @@ +package uploadreader + +import ( + "bytes" + "errors" + "io" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUploadReader(t *testing.T) { + // This is a smoke test in a single goroutine, without really testing the locking. + + data := bytes.Repeat([]byte{0x01}, 65535) + // No termination + ur := NewUploadReader(bytes.NewReader(data)) + read, err := io.ReadAll(ur) + require.NoError(t, err) + assert.Equal(t, data, read) + + // Terminated + ur = NewUploadReader(bytes.NewReader(data)) + readLen := len(data) / 2 + read, err = io.ReadAll(io.LimitReader(ur, int64(readLen))) + require.NoError(t, err) + assert.Equal(t, data[:readLen], read) + terminationErr := errors.New("Terminated") + ur.Terminate(terminationErr) + read, err = io.ReadAll(ur) + assert.Equal(t, terminationErr, err) + assert.Len(t, read, 0) +} diff --git a/vendor/github.com/containers/image/v5/manifest/common_test.go b/vendor/github.com/containers/image/v5/manifest/common_test.go new file mode 100644 index 00000000000..db19b31eb8e --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/common_test.go @@ -0,0 +1,328 @@ +package manifest + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/containers/image/v5/pkg/compression" + compressiontypes "github.com/containers/image/v5/pkg/compression/types" + "github.com/containers/image/v5/types" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Test that parser() rejects all of the provided manifest fixtures. +// Intended to help test manifest parsers' detection of schema mismatches. +func testManifestFixturesAreRejected(t *testing.T, parser func([]byte) error, fixtures []string) { + for _, fixture := range fixtures { + manifest, err := os.ReadFile(filepath.Join("fixtures", fixture)) + require.NoError(t, err, fixture) + err = parser(manifest) + assert.Error(t, err, fixture) + } +} + +// Test that parser() rejects validManifest with an added top-level field with any of the provided field names. +// Intended to help test callers of validateUnambiguousManifestFormat. +func testValidManifestWithExtraFieldsIsRejected(t *testing.T, parser func([]byte) error, + validManifest []byte, fields []string) { + for _, field := range fields { + // end (the final '}') is not always at len(validManifest)-1 because the manifest can end with + // white space. + end := bytes.LastIndexByte(validManifest, '}') + require.NotEqual(t, end, -1) + updatedManifest := []byte(string(validManifest[:end]) + + fmt.Sprintf(`,"%s":[]}`, field)) + err := parser(updatedManifest) + // Make sure it is the error from validateUnambiguousManifestFormat, not something that + // went wrong with creating updatedManifest. + assert.ErrorContains(t, err, "rejecting ambiguous manifest", field) + } +} + +func TestLayerInfosToStrings(t *testing.T) { + strings := layerInfosToStrings([]LayerInfo{}) + assert.Equal(t, []string{}, strings) + + strings = layerInfosToStrings([]LayerInfo{ + { + BlobInfo: types.BlobInfo{ + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Digest: "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", + Size: 32, + }, + EmptyLayer: true, + }, + { + BlobInfo: types.BlobInfo{ + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Digest: "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909", + Size: 8841833, + }, + EmptyLayer: false, + }, + { + BlobInfo: types.BlobInfo{ + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Digest: "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa", + Size: 291, + }, + EmptyLayer: false, + }, + { + BlobInfo: types.BlobInfo{ + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Digest: "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", + Size: 32, + }, + EmptyLayer: true, + }, + }) + assert.Equal(t, []string{ + "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", + "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909", + "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa", + "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", + }, strings) +} + +func TestCompressionVariantMIMEType(t *testing.T) { + sets := []compressionMIMETypeSet{ + {mtsUncompressed: "AU", compressiontypes.GzipAlgorithmName: "AG" /* No zstd variant */}, + {mtsUncompressed: "BU", compressiontypes.GzipAlgorithmName: "BG", compressiontypes.ZstdAlgorithmName: mtsUnsupportedMIMEType}, + { /* No uncompressed variant */ compressiontypes.GzipAlgorithmName: "CG", compressiontypes.ZstdAlgorithmName: "CZ"}, + {mtsUncompressed: "", compressiontypes.GzipAlgorithmName: "DG"}, + } + + for _, c := range []struct { + input string + algo *compressiontypes.Algorithm + expected string + }{ + {"AU", nil, "AU"}, {"AU", &compression.Gzip, "AG"}, {"AU", &compression.Zstd, ""}, + {"AG", nil, "AU"}, {"AG", &compression.Gzip, "AG"}, {"AG", &compression.Zstd, ""}, + {"BU", &compression.Zstd, ""}, + {"BG", &compression.Zstd, ""}, + {"CG", nil, ""}, {"CG", &compression.Zstd, "CZ"}, + {"CZ", nil, ""}, {"CZ", &compression.Gzip, "CG"}, + {"DG", nil, ""}, + {"unknown", nil, ""}, {"unknown", &compression.Gzip, ""}, + {"", nil, ""}, {"", &compression.Gzip, ""}, + } { + res, err := compressionVariantMIMEType(sets, c.input, c.algo) + if c.expected == "" { + assert.Error(t, err, c.input) + } else { + require.NoError(t, err, c.input) + assert.Equal(t, c.expected, res, c.input) + } + } +} + +func TestUpdatedMIMEType(t *testing.T) { + // all known types, PreserveOriginal + preserve := []struct { + compression []compressionMIMETypeSet + mimeType string + }{ + {schema2CompressionMIMETypeSets, DockerV2Schema1MediaType}, + {schema2CompressionMIMETypeSets, DockerV2Schema1SignedMediaType}, + {schema2CompressionMIMETypeSets, DockerV2Schema2MediaType}, + {schema2CompressionMIMETypeSets, DockerV2Schema2ConfigMediaType}, + {schema2CompressionMIMETypeSets, DockerV2Schema2LayerMediaType}, + {schema2CompressionMIMETypeSets, DockerV2SchemaLayerMediaTypeUncompressed}, + {schema2CompressionMIMETypeSets, DockerV2ListMediaType}, + {schema2CompressionMIMETypeSets, DockerV2Schema2ForeignLayerMediaType}, + {schema2CompressionMIMETypeSets, DockerV2Schema2ForeignLayerMediaTypeGzip}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeDescriptor}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeLayoutHeader}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageManifest}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageIndex}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageLayer}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageLayerGzip}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageLayerZstd}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageLayerNonDistributable}, //nolint:staticcheck // NonDistributable layers are deprecated, but we want to continue to support manipulating pre-existing images. + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageLayerNonDistributableGzip}, //nolint:staticcheck // NonDistributable layers are deprecated, but we want to continue to support manipulating pre-existing images. + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageLayerNonDistributableZstd}, //nolint:staticcheck // NonDistributable layers are deprecated, but we want to continue to support manipulating pre-existing images. + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageConfig}, + } + for i, c := range preserve { + update := types.BlobInfo{ + MediaType: c.mimeType, + CompressionOperation: types.PreserveOriginal, + } + updatedType, err := updatedMIMEType(c.compression, c.mimeType, update) + require.NoErrorf(t, err, "%d: updatedMIMEType(%q, %+v) failed unexpectedly", i, c.mimeType, update) + assert.Equalf(t, c.mimeType, updatedType, "%d: updatedMIMEType(%q, %+v)", i, c.mimeType, update) + } + + // known types where Decompress is expected to succeed + decompressSuccess := []struct { + compression []compressionMIMETypeSet + mimeType string + updatedType string + }{ + {schema2CompressionMIMETypeSets, DockerV2SchemaLayerMediaTypeUncompressed, DockerV2SchemaLayerMediaTypeUncompressed}, + {schema2CompressionMIMETypeSets, DockerV2Schema2ForeignLayerMediaTypeGzip, DockerV2Schema2ForeignLayerMediaType}, + {schema2CompressionMIMETypeSets, DockerV2Schema2LayerMediaType, DockerV2SchemaLayerMediaTypeUncompressed}, + {schema2CompressionMIMETypeSets, DockerV2Schema2ForeignLayerMediaType, DockerV2Schema2ForeignLayerMediaType}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageLayer, imgspecv1.MediaTypeImageLayer}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageLayerGzip, imgspecv1.MediaTypeImageLayer}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageLayerZstd, imgspecv1.MediaTypeImageLayer}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageLayerNonDistributable, imgspecv1.MediaTypeImageLayerNonDistributable}, //nolint:staticcheck // NonDistributable layers are deprecated, but we want to continue to support manipulating pre-existing images. + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageLayerNonDistributableGzip, imgspecv1.MediaTypeImageLayerNonDistributable}, //nolint:staticcheck // NonDistributable layers are deprecated, but we want to continue to support manipulating pre-existing images. + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageLayerNonDistributableZstd, imgspecv1.MediaTypeImageLayerNonDistributable}, //nolint:staticcheck // NonDistributable layers are deprecated, but we want to continue to support manipulating pre-existing images. + } + for i, c := range decompressSuccess { + update := types.BlobInfo{ + MediaType: c.mimeType, + CompressionOperation: types.Decompress, + } + updatedType, err := updatedMIMEType(c.compression, c.mimeType, update) + require.NoErrorf(t, err, "%d: updatedMIMEType(%q, %+v) failed unexpectedly", i, c.mimeType, update) + assert.Equalf(t, c.updatedType, updatedType, "%d: updatedMIMEType(%q, %+v)", i, c.mimeType, update) + } + + // known types where Decompress is expected to fail + decompressFailure := []struct { + compression []compressionMIMETypeSet + mimeType string + }{ + {schema2CompressionMIMETypeSets, DockerV2Schema1MediaType}, + {schema2CompressionMIMETypeSets, DockerV2Schema1SignedMediaType}, + {schema2CompressionMIMETypeSets, DockerV2Schema2MediaType}, + {schema2CompressionMIMETypeSets, DockerV2Schema2ConfigMediaType}, + {schema2CompressionMIMETypeSets, DockerV2ListMediaType}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeDescriptor}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeLayoutHeader}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageManifest}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageIndex}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageConfig}, + } + for i, c := range decompressFailure { + update := types.BlobInfo{ + MediaType: c.mimeType, + CompressionOperation: types.Decompress, + } + _, err := updatedMIMEType(c.compression, c.mimeType, update) + require.Errorf(t, err, "%d: updatedMIMEType(%q, %+v) should have failed", i, c.mimeType, update) + } + + require.Equalf(t, len(preserve), len(decompressSuccess)+len(decompressFailure), "missing some decompression tests") + + // all known types where Compress with gzip should succeed + compressGzipSuccess := []struct { + compression []compressionMIMETypeSet + mimeType string + updatedType string + }{ + {schema2CompressionMIMETypeSets, DockerV2Schema2LayerMediaType, DockerV2Schema2LayerMediaType}, + {schema2CompressionMIMETypeSets, DockerV2SchemaLayerMediaTypeUncompressed, DockerV2Schema2LayerMediaType}, + {schema2CompressionMIMETypeSets, DockerV2Schema2ForeignLayerMediaType, DockerV2Schema2ForeignLayerMediaTypeGzip}, + {schema2CompressionMIMETypeSets, DockerV2Schema2ForeignLayerMediaTypeGzip, DockerV2Schema2ForeignLayerMediaTypeGzip}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageLayer, imgspecv1.MediaTypeImageLayerGzip}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageLayerGzip, imgspecv1.MediaTypeImageLayerGzip}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageLayerZstd, imgspecv1.MediaTypeImageLayerGzip}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageLayerNonDistributable, imgspecv1.MediaTypeImageLayerNonDistributableGzip}, //nolint:staticcheck // NonDistributable layers are deprecated, but we want to continue to support manipulating pre-existing images. + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageLayerNonDistributableGzip, imgspecv1.MediaTypeImageLayerNonDistributableGzip}, //nolint:staticcheck // NonDistributable layers are deprecated, but we want to continue to support manipulating pre-existing images. + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageLayerNonDistributableZstd, imgspecv1.MediaTypeImageLayerNonDistributableGzip}, //nolint:staticcheck // NonDistributable layers are deprecated, but we want to continue to support manipulating pre-existing images. + } + for i, c := range compressGzipSuccess { + update := types.BlobInfo{ + MediaType: c.mimeType, + CompressionOperation: types.Compress, + CompressionAlgorithm: &compression.Gzip, + } + updatedType, err := updatedMIMEType(c.compression, c.mimeType, update) + require.NoErrorf(t, err, "%d: updatedMIMEType(%q, %+v) failed unexpectedly", i, c.mimeType, update) + assert.Equalf(t, c.updatedType, updatedType, "%d: updatedMIMEType(%q, %+v)", i, c.mimeType, update) + } + + // known types where Compress with gzip is expected to fail + compressGzipFailure := []struct { + compression []compressionMIMETypeSet + mimeType string + }{ + {schema2CompressionMIMETypeSets, DockerV2Schema1MediaType}, + {schema2CompressionMIMETypeSets, DockerV2Schema1SignedMediaType}, + {schema2CompressionMIMETypeSets, DockerV2Schema2MediaType}, + {schema2CompressionMIMETypeSets, DockerV2Schema2ConfigMediaType}, + {schema2CompressionMIMETypeSets, DockerV2ListMediaType}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeDescriptor}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeLayoutHeader}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageManifest}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageIndex}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageConfig}, + } + for i, c := range compressGzipFailure { + update := types.BlobInfo{ + MediaType: c.mimeType, + CompressionOperation: types.Compress, + CompressionAlgorithm: &compression.Gzip, + } + _, err := updatedMIMEType(c.compression, c.mimeType, update) + require.Errorf(t, err, "%d: updatedMIMEType(%q, %+v) should have failed", i, c.mimeType, update) + } + + require.Equalf(t, len(preserve), len(compressGzipSuccess)+len(compressGzipFailure), "missing some gzip compression tests") + + // known types where Compress with zstd is expected to succeed + compressZstdSuccess := []struct { + compression []compressionMIMETypeSet + mimeType string + updatedType string + }{ + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageLayer, imgspecv1.MediaTypeImageLayerZstd}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageLayerGzip, imgspecv1.MediaTypeImageLayerZstd}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageLayerZstd, imgspecv1.MediaTypeImageLayerZstd}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageLayerNonDistributable, imgspecv1.MediaTypeImageLayerNonDistributableZstd}, //nolint:staticcheck // NonDistributable layers are deprecated, but we want to continue to support manipulating pre-existing images. + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageLayerNonDistributableGzip, imgspecv1.MediaTypeImageLayerNonDistributableZstd}, //nolint:staticcheck // NonDistributable layers are deprecated, but we want to continue to support manipulating pre-existing images. + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageLayerNonDistributableZstd, imgspecv1.MediaTypeImageLayerNonDistributableZstd}, //nolint:staticcheck // NonDistributable layers are deprecated, but we want to continue to support manipulating pre-existing images. + } + for i, c := range compressZstdSuccess { + update := types.BlobInfo{ + MediaType: c.mimeType, + CompressionOperation: types.Compress, + CompressionAlgorithm: &compression.Zstd, + } + updatedType, err := updatedMIMEType(c.compression, c.mimeType, update) + require.NoErrorf(t, err, "%d: updatedMIMEType(%q, %+v) failed unexpectedly", i, c.mimeType, update) + assert.Equalf(t, c.updatedType, updatedType, "%d: updatedMIMEType(%q, %+v)", i, c.mimeType, update) + } + + // known types where Compress with zstd is expected to fail + compressZstdFailure := []struct { + compression []compressionMIMETypeSet + mimeType string + }{ + {schema2CompressionMIMETypeSets, DockerV2SchemaLayerMediaTypeUncompressed}, + {schema2CompressionMIMETypeSets, DockerV2Schema2LayerMediaType}, + {schema2CompressionMIMETypeSets, DockerV2Schema2ForeignLayerMediaType}, + {schema2CompressionMIMETypeSets, DockerV2Schema2ForeignLayerMediaTypeGzip}, + {schema2CompressionMIMETypeSets, DockerV2Schema1MediaType}, + {schema2CompressionMIMETypeSets, DockerV2Schema1SignedMediaType}, + {schema2CompressionMIMETypeSets, DockerV2Schema2MediaType}, + {schema2CompressionMIMETypeSets, DockerV2Schema2ConfigMediaType}, + {schema2CompressionMIMETypeSets, DockerV2ListMediaType}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeDescriptor}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeLayoutHeader}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageManifest}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageIndex}, + {oci1CompressionMIMETypeSets, imgspecv1.MediaTypeImageConfig}, + } + for i, c := range compressZstdFailure { + update := types.BlobInfo{ + MediaType: c.mimeType, + CompressionOperation: types.Compress, + CompressionAlgorithm: &compression.Zstd, + } + _, err := updatedMIMEType(c.compression, c.mimeType, update) + require.Errorf(t, err, "%d: updatedMIMEType(%q, %+v) should have failed", i, c.mimeType, update) + } + + require.Equalf(t, len(preserve), len(compressZstdSuccess)+len(compressZstdFailure), "missing some zstd compression tests") +} diff --git a/vendor/github.com/containers/image/v5/manifest/docker_schema1_test.go b/vendor/github.com/containers/image/v5/manifest/docker_schema1_test.go new file mode 100644 index 00000000000..390c8816a02 --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/docker_schema1_test.go @@ -0,0 +1,187 @@ +package manifest + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/containers/image/v5/types" + "github.com/opencontainers/go-digest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// DiffID values corresponding to layers of schema2-to-schema1-by-docker.json +var schema1FixtureLayerDiffIDs = []digest.Digest{ + "sha256:142a601d97936307e75220c35dde0348971a9584c21e7cb42e1f7004005432ab", + "sha256:90fcc66ad3be9f1757f954b750deb37032f208428aa12599fcb02182b9065a9c", + "sha256:5a8624bb7e76d1e6829f9c64c43185e02bc07f97a2189eb048609a8914e72c56", + "sha256:d349ff6b3afc6a2800054768c82bfbf4289c9aa5da55c1290f802943dcd4d1e9", + "sha256:8c064bb1f60e84fa8cc6079b6d2e76e0423389fd6aeb7e497dfdae5e05b2b25b", +} + +func manifestSchema1FromFixture(t *testing.T, fixture string) *Schema1 { + manifest, err := os.ReadFile(filepath.Join("fixtures", fixture)) + require.NoError(t, err) + + m, err := Schema1FromManifest(manifest) + require.NoError(t, err) + return m +} + +func TestSchema1FromManifest(t *testing.T) { + validManifest, err := os.ReadFile(filepath.Join("fixtures", "schema2-to-schema1-by-docker.json")) + require.NoError(t, err) + + // Invalid manifest version is rejected + m, err := Schema1FromManifest(validManifest) + require.NoError(t, err) + m.SchemaVersion = 2 + manifest, err := m.Serialize() + require.NoError(t, err) + _, err = Schema1FromManifest(manifest) + assert.Error(t, err) + + parser := func(m []byte) error { + _, err := Schema1FromManifest(m) + return err + } + // Schema mismatch is rejected + testManifestFixturesAreRejected(t, parser, []string{ + "v2s2.manifest.json", "v2list.manifest.json", + "ociv1.manifest.json", "ociv1.image.index.json", + }) + // Extra fields are rejected + testValidManifestWithExtraFieldsIsRejected(t, parser, validManifest, []string{"config", "layers", "manifests"}) +} + +func TestSchema1Initialize(t *testing.T) { + // Test this indirectly via Schema1FromComponents; otherwise we would have to break the API and create an instance manually. + + // FIXME: this should eventually share a fixture with the other parsing tests. + fsLayers := []Schema1FSLayers{ + {BlobSum: "sha256:e623934bca8d1a74f51014256445937714481e49343a31bda2bc5f534748184d"}, + {BlobSum: "sha256:62e48e39dc5b30b75a97f05bccc66efbae6058b860ee20a5c9a184b9d5e25788"}, + {BlobSum: "sha256:9e92df2aea7dc0baf5f1f8d509678d6a6306de27ad06513f8e218371938c07a6"}, + {BlobSum: "sha256:f576d102e09b9eef0e305aaef705d2d43a11bebc3fd5810a761624bd5e11997e"}, + {BlobSum: "sha256:4aa565ad8b7a87248163ce7dba1dd3894821aac97e846b932ff6b8ef9a8a508a"}, + {BlobSum: "sha256:9cadd93b16ff2a0c51ac967ea2abfadfac50cfa3af8b5bf983d89b8f8647f3e4"}, + } + history := []Schema1History{ + {V1Compatibility: "{\"architecture\":\"amd64\",\"config\":{\"Hostname\":\"9428cdea83ba\",\"Domainname\":\"\",\"User\":\"nova\",\"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\",\"container=oci\",\"KOLLA_BASE_DISTRO=rhel\",\"KOLLA_INSTALL_TYPE=binary\",\"KOLLA_INSTALL_METATYPE=rhos\",\"PS1=$(tput bold)($(printenv KOLLA_SERVICE_NAME))$(tput sgr0)[$(id -un)@$(hostname -s) $(pwd)]$ \"],\"Cmd\":[\"kolla_start\"],\"Healthcheck\":{\"Test\":[\"CMD-SHELL\",\"/openstack/healthcheck\"]},\"ArgsEscaped\":true,\"Image\":\"3bf9afe371220b1eb1c57bec39b5a99ba976c36c92d964a1c014584f95f51e33\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":[],\"Labels\":{\"Kolla-SHA\":\"5.0.0-39-g6f1b947b\",\"architecture\":\"x86_64\",\"authoritative-source-url\":\"registry.access.redhat.com\",\"build-date\":\"2018-01-25T00:32:27.807261\",\"com.redhat.build-host\":\"ip-10-29-120-186.ec2.internal\",\"com.redhat.component\":\"openstack-nova-api-docker\",\"description\":\"Red Hat OpenStack Platform 12.0 nova-api\",\"distribution-scope\":\"public\",\"io.k8s.description\":\"Red Hat OpenStack Platform 12.0 nova-api\",\"io.k8s.display-name\":\"Red Hat OpenStack Platform 12.0 nova-api\",\"io.openshift.tags\":\"rhosp osp openstack osp-12.0\",\"kolla_version\":\"stable/pike\",\"name\":\"rhosp12/openstack-nova-api\",\"release\":\"20180124.1\",\"summary\":\"Red Hat OpenStack Platform 12.0 nova-api\",\"tripleo-common_version\":\"7.6.3-23-g4891cfe\",\"url\":\"https://access.redhat.com/containers/#/registry.access.redhat.com/rhosp12/openstack-nova-api/images/12.0-20180124.1\",\"vcs-ref\":\"9b31243b7b448eb2fc3b6e2c96935b948f806e98\",\"vcs-type\":\"git\",\"vendor\":\"Red Hat, Inc.\",\"version\":\"12.0\",\"version-release\":\"12.0-20180124.1\"}},\"container_config\":{\"Hostname\":\"9428cdea83ba\",\"Domainname\":\"\",\"User\":\"nova\",\"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\",\"container=oci\",\"KOLLA_BASE_DISTRO=rhel\",\"KOLLA_INSTALL_TYPE=binary\",\"KOLLA_INSTALL_METATYPE=rhos\",\"PS1=$(tput bold)($(printenv KOLLA_SERVICE_NAME))$(tput sgr0)[$(id -un)@$(hostname -s) $(pwd)]$ \"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) \",\"USER [nova]\"],\"Healthcheck\":{\"Test\":[\"CMD-SHELL\",\"/openstack/healthcheck\"]},\"ArgsEscaped\":true,\"Image\":\"sha256:274ce4dcbeb09fa173a5d50203ae5cec28f456d1b8b59477b47a42bd74d068bf\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":[],\"Labels\":{\"Kolla-SHA\":\"5.0.0-39-g6f1b947b\",\"architecture\":\"x86_64\",\"authoritative-source-url\":\"registry.access.redhat.com\",\"build-date\":\"2018-01-25T00:32:27.807261\",\"com.redhat.build-host\":\"ip-10-29-120-186.ec2.internal\",\"com.redhat.component\":\"openstack-nova-api-docker\",\"description\":\"Red Hat OpenStack Platform 12.0 nova-api\",\"distribution-scope\":\"public\",\"io.k8s.description\":\"Red Hat OpenStack Platform 12.0 nova-api\",\"io.k8s.display-name\":\"Red Hat OpenStack Platform 12.0 nova-api\",\"io.openshift.tags\":\"rhosp osp openstack osp-12.0\",\"kolla_version\":\"stable/pike\",\"name\":\"rhosp12/openstack-nova-api\",\"release\":\"20180124.1\",\"summary\":\"Red Hat OpenStack Platform 12.0 nova-api\",\"tripleo-common_version\":\"7.6.3-23-g4891cfe\",\"url\":\"https://access.redhat.com/containers/#/registry.access.redhat.com/rhosp12/openstack-nova-api/images/12.0-20180124.1\",\"vcs-ref\":\"9b31243b7b448eb2fc3b6e2c96935b948f806e98\",\"vcs-type\":\"git\",\"vendor\":\"Red Hat, Inc.\",\"version\":\"12.0\",\"version-release\":\"12.0-20180124.1\"}},\"created\":\"2018-01-25T00:37:48.268558Z\",\"docker_version\":\"1.12.6\",\"id\":\"486cbbaf6c6f7d890f9368c86eda3f4ebe3ae982b75098037eb3c3cc6f0e0cdf\",\"os\":\"linux\",\"parent\":\"20d0c9c79f9fee83c4094993335b9b321112f13eef60ed9ec1599c7593dccf20\"}"}, + {V1Compatibility: "{\"id\":\"20d0c9c79f9fee83c4094993335b9b321112f13eef60ed9ec1599c7593dccf20\",\"parent\":\"47a1014db2116c312736e11adcc236fb77d0ad32457f959cbaec0c3fc9ab1caa\",\"created\":\"2018-01-24T23:08:25.300741Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c rm -f '/etc/yum.repos.d/rhel-7.4.repo' '/etc/yum.repos.d/rhos-optools-12.0.repo' '/etc/yum.repos.d/rhos-12.0-container-yum-need_images.repo'\"]}}"}, + {V1Compatibility: "{\"id\":\"47a1014db2116c312736e11adcc236fb77d0ad32457f959cbaec0c3fc9ab1caa\",\"parent\":\"cec66cab6c92a5f7b50ef407b80b83840a0d089b9896257609fd01de3a595824\",\"created\":\"2018-01-24T22:00:57.807862Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c rm -f '/etc/yum.repos.d/rhel-7.4.repo' '/etc/yum.repos.d/rhos-optools-12.0.repo' '/etc/yum.repos.d/rhos-12.0-container-yum-need_images.repo'\"]}}"}, + {V1Compatibility: "{\"id\":\"cec66cab6c92a5f7b50ef407b80b83840a0d089b9896257609fd01de3a595824\",\"parent\":\"0e7730eccb3d014b33147b745d771bc0e38a967fd932133a6f5325a3c84282e2\",\"created\":\"2018-01-24T21:40:32.494686Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c rm -f '/etc/yum.repos.d/rhel-7.4.repo' '/etc/yum.repos.d/rhos-optools-12.0.repo' '/etc/yum.repos.d/rhos-12.0-container-yum-need_images.repo'\"]}}"}, + {V1Compatibility: "{\"id\":\"0e7730eccb3d014b33147b745d771bc0e38a967fd932133a6f5325a3c84282e2\",\"parent\":\"3e49094c0233214ab73f8e5c204af8a14cfc6f0403384553c17fbac2e9d38345\",\"created\":\"2017-11-21T16:49:37.292899Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c rm -f '/etc/yum.repos.d/compose-rpms-1.repo'\"]},\"author\":\"Red Hat, Inc.\"}"}, + {V1Compatibility: "{\"id\":\"3e49094c0233214ab73f8e5c204af8a14cfc6f0403384553c17fbac2e9d38345\",\"comment\":\"Imported from -\",\"created\":\"2017-11-21T16:47:27.755341705Z\",\"container_config\":{\"Cmd\":[\"\"]}}"}, + } + + // Valid input + m, err := Schema1FromComponents(nil, fsLayers, history, "amd64") + assert.NoError(t, err) + assert.Equal(t, []Schema1V1Compatibility{ + { + ID: "486cbbaf6c6f7d890f9368c86eda3f4ebe3ae982b75098037eb3c3cc6f0e0cdf", + Parent: "20d0c9c79f9fee83c4094993335b9b321112f13eef60ed9ec1599c7593dccf20", + Created: time.Date(2018, 1, 25, 0, 37, 48, 268558000, time.UTC), + ContainerConfig: schema1V1CompatibilityContainerConfig{ + Cmd: []string{"/bin/sh", "-c", "#(nop) ", "USER [nova]"}, + }, + ThrowAway: false, + }, + { + ID: "20d0c9c79f9fee83c4094993335b9b321112f13eef60ed9ec1599c7593dccf20", + Parent: "47a1014db2116c312736e11adcc236fb77d0ad32457f959cbaec0c3fc9ab1caa", + Created: time.Date(2018, 1, 24, 23, 8, 25, 300741000, time.UTC), + ContainerConfig: schema1V1CompatibilityContainerConfig{ + Cmd: []string{"/bin/sh -c rm -f '/etc/yum.repos.d/rhel-7.4.repo' '/etc/yum.repos.d/rhos-optools-12.0.repo' '/etc/yum.repos.d/rhos-12.0-container-yum-need_images.repo'"}, + }, + ThrowAway: false, + }, + { + ID: "47a1014db2116c312736e11adcc236fb77d0ad32457f959cbaec0c3fc9ab1caa", + Parent: "cec66cab6c92a5f7b50ef407b80b83840a0d089b9896257609fd01de3a595824", + Created: time.Date(2018, 1, 24, 22, 0, 57, 807862000, time.UTC), + ContainerConfig: schema1V1CompatibilityContainerConfig{ + Cmd: []string{"/bin/sh -c rm -f '/etc/yum.repos.d/rhel-7.4.repo' '/etc/yum.repos.d/rhos-optools-12.0.repo' '/etc/yum.repos.d/rhos-12.0-container-yum-need_images.repo'"}, + }, + ThrowAway: false, + }, + { + ID: "cec66cab6c92a5f7b50ef407b80b83840a0d089b9896257609fd01de3a595824", + Parent: "0e7730eccb3d014b33147b745d771bc0e38a967fd932133a6f5325a3c84282e2", + Created: time.Date(2018, 1, 24, 21, 40, 32, 494686000, time.UTC), + ContainerConfig: schema1V1CompatibilityContainerConfig{ + Cmd: []string{"/bin/sh -c rm -f '/etc/yum.repos.d/rhel-7.4.repo' '/etc/yum.repos.d/rhos-optools-12.0.repo' '/etc/yum.repos.d/rhos-12.0-container-yum-need_images.repo'"}, + }, + ThrowAway: false, + }, + { + ID: "0e7730eccb3d014b33147b745d771bc0e38a967fd932133a6f5325a3c84282e2", + Parent: "3e49094c0233214ab73f8e5c204af8a14cfc6f0403384553c17fbac2e9d38345", + Created: time.Date(2017, 11, 21, 16, 49, 37, 292899000, time.UTC), + ContainerConfig: schema1V1CompatibilityContainerConfig{ + Cmd: []string{"/bin/sh -c rm -f '/etc/yum.repos.d/compose-rpms-1.repo'"}, + }, + Author: "Red Hat, Inc.", + ThrowAway: false, + }, + { + ID: "3e49094c0233214ab73f8e5c204af8a14cfc6f0403384553c17fbac2e9d38345", + Comment: "Imported from -", + Created: time.Date(2017, 11, 21, 16, 47, 27, 755341705, time.UTC), + ContainerConfig: schema1V1CompatibilityContainerConfig{ + Cmd: []string{""}, + }, + ThrowAway: false, + }, + }, m.ExtractedV1Compatibility) + + // Layer and history length mismatch + _, err = Schema1FromComponents(nil, fsLayers, history[1:], "amd64") + assert.Error(t, err) + + // No layers/history + _, err = Schema1FromComponents(nil, []Schema1FSLayers{}, []Schema1History{}, "amd64") + assert.Error(t, err) + + // Invalid history JSON + _, err = Schema1FromComponents(nil, + []Schema1FSLayers{{BlobSum: "sha256:e623934bca8d1a74f51014256445937714481e49343a31bda2bc5f534748184d"}}, + []Schema1History{{V1Compatibility: "-"}}, + "amd64") + assert.Error(t, err) +} + +func TestSchema1LayerInfos(t *testing.T) { + // We use this instead of original schema1 manifests, because those, surprisingly, + // seem not to set the "throwaway" flag. + m := manifestSchema1FromFixture(t, "schema2-to-schema1-by-docker.json") // FIXME: Test also Schema1FromComponents + assert.Equal(t, []LayerInfo{ + {BlobInfo: types.BlobInfo{Digest: "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb", Size: -1}, EmptyLayer: false}, + {BlobInfo: types.BlobInfo{Digest: "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", Size: -1}, EmptyLayer: true}, + {BlobInfo: types.BlobInfo{Digest: "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", Size: -1}, EmptyLayer: true}, + {BlobInfo: types.BlobInfo{Digest: "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", Size: -1}, EmptyLayer: true}, + {BlobInfo: types.BlobInfo{Digest: "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c", Size: -1}, EmptyLayer: false}, + {BlobInfo: types.BlobInfo{Digest: "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", Size: -1}, EmptyLayer: true}, + {BlobInfo: types.BlobInfo{Digest: "sha256:8f5dc8a4b12c307ac84de90cdd9a7f3915d1be04c9388868ca118831099c67a9", Size: -1}, EmptyLayer: false}, + {BlobInfo: types.BlobInfo{Digest: "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", Size: -1}, EmptyLayer: true}, + {BlobInfo: types.BlobInfo{Digest: "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", Size: -1}, EmptyLayer: true}, + {BlobInfo: types.BlobInfo{Digest: "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", Size: -1}, EmptyLayer: true}, + {BlobInfo: types.BlobInfo{Digest: "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", Size: -1}, EmptyLayer: true}, + {BlobInfo: types.BlobInfo{Digest: "sha256:bbd6b22eb11afce63cc76f6bc41042d99f10d6024c96b655dafba930b8d25909", Size: -1}, EmptyLayer: false}, + {BlobInfo: types.BlobInfo{Digest: "sha256:960e52ecf8200cbd84e70eb2ad8678f4367e50d14357021872c10fa3fc5935fa", Size: -1}, EmptyLayer: false}, + {BlobInfo: types.BlobInfo{Digest: "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", Size: -1}, EmptyLayer: true}, + {BlobInfo: types.BlobInfo{Digest: "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", Size: -1}, EmptyLayer: true}, + }, m.LayerInfos()) +} + +func TestSchema1ImageID(t *testing.T) { + m := manifestSchema1FromFixture(t, "schema2-to-schema1-by-docker.json") + id, err := m.ImageID(schema1FixtureLayerDiffIDs) + require.NoError(t, err) + // NOTE: This value is dependent on the Schema1.ToSchema2Config implementation, and not necessarily stable over time. + // This is mostly a smoke-test; it’s fine to just update this value if that implementation changes. + assert.Equal(t, "9ca4bda0a6b3727a6ffcc43e981cad0f24e2ec79d338f6ba325b4dfd0756fb8f", id) +} diff --git a/vendor/github.com/containers/image/v5/manifest/docker_schema2_list_test.go b/vendor/github.com/containers/image/v5/manifest/docker_schema2_list_test.go new file mode 100644 index 00000000000..0d7807ffac2 --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/docker_schema2_list_test.go @@ -0,0 +1,28 @@ +package manifest + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSchema2ListFromManifest(t *testing.T) { + validManifest, err := os.ReadFile(filepath.Join("fixtures", "v2list.manifest.json")) + require.NoError(t, err) + + parser := func(m []byte) error { + _, err := Schema2ListFromManifest(m) + return err + } + // Schema mismatch is rejected + testManifestFixturesAreRejected(t, parser, []string{ + "schema2-to-schema1-by-docker.json", + "v2s2.manifest.json", + "ociv1.manifest.json", + // Not "ociv1.image.index.json" yet, without validating mediaType the two are too similar to tell the difference. + }) + // Extra fields are rejected + testValidManifestWithExtraFieldsIsRejected(t, parser, validManifest, []string{"config", "fsLayers", "history", "layers"}) +} diff --git a/vendor/github.com/containers/image/v5/manifest/docker_schema2_test.go b/vendor/github.com/containers/image/v5/manifest/docker_schema2_test.go new file mode 100644 index 00000000000..bdd10d09871 --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/docker_schema2_test.go @@ -0,0 +1,213 @@ +package manifest + +import ( + "os" + "path/filepath" + "testing" + + "github.com/containers/image/v5/pkg/compression" + "github.com/containers/image/v5/types" + "github.com/opencontainers/go-digest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func manifestSchema2FromFixture(t *testing.T, fixture string) *Schema2 { + manifest, err := os.ReadFile(filepath.Join("fixtures", fixture)) + require.NoError(t, err) + + m, err := Schema2FromManifest(manifest) + require.NoError(t, err) + return m +} + +func TestSupportedSchema2MediaType(t *testing.T) { + type testData struct { + m string + mustFail bool + } + data := []testData{ + {DockerV2Schema2MediaType, false}, + {DockerV2Schema2ConfigMediaType, false}, + {DockerV2Schema2LayerMediaType, false}, + {DockerV2SchemaLayerMediaTypeUncompressed, false}, + {DockerV2ListMediaType, false}, + {DockerV2Schema2ForeignLayerMediaType, false}, + {DockerV2Schema2ForeignLayerMediaTypeGzip, false}, + {"application/vnd.docker.image.rootfs.foreign.diff.unknown", true}, + } + for _, d := range data { + err := SupportedSchema2MediaType(d.m) + if d.mustFail { + assert.NotNil(t, err) + } else { + assert.Nil(t, err) + } + } +} + +func TestSchema2FromManifest(t *testing.T) { + validManifest, err := os.ReadFile(filepath.Join("fixtures", "v2s2.manifest.json")) + require.NoError(t, err) + + parser := func(m []byte) error { + _, err := Schema2FromManifest(m) + return err + } + // Schema mismatch is rejected + testManifestFixturesAreRejected(t, parser, []string{ + "schema2-to-schema1-by-docker.json", + "v2list.manifest.json", + "ociv1.manifest.json", "ociv1.image.index.json", + }) + // Extra fields are rejected + testValidManifestWithExtraFieldsIsRejected(t, parser, validManifest, []string{"fsLayers", "history", "manifests"}) +} + +func TestUpdateLayerInfosV2S2GzipToZstd(t *testing.T) { + origManifest := manifestSchema2FromFixture(t, "v2s2.manifest.json") + err := origManifest.UpdateLayerInfos([]types.BlobInfo{ + { + Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + Size: 32654, + MediaType: DockerV2Schema2LayerMediaType, + CompressionOperation: types.Compress, + CompressionAlgorithm: &compression.Zstd, + }, + { + Digest: "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b", + Size: 16724, + MediaType: DockerV2Schema2LayerMediaType, + CompressionOperation: types.Compress, + CompressionAlgorithm: &compression.Zstd, + }, + { + Digest: "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736", + Size: 73109, + MediaType: DockerV2Schema2LayerMediaType, + CompressionOperation: types.Compress, + CompressionAlgorithm: &compression.Zstd, + }, + }) + assert.NotNil(t, err) // zstd is not supported for docker images +} + +func TestUpdateLayerInfosV2S2InvalidCompressionOperation(t *testing.T) { + origManifest := manifestSchema2FromFixture(t, "v2s2.manifest.json") + err := origManifest.UpdateLayerInfos([]types.BlobInfo{ + { + Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + Size: 32654, + MediaType: DockerV2Schema2LayerMediaType, + CompressionOperation: types.Decompress, + }, + { + Digest: "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b", + Size: 16724, + MediaType: DockerV2Schema2LayerMediaType, + CompressionOperation: types.Decompress, + }, + { + Digest: "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736", + Size: 73109, + MediaType: DockerV2Schema2LayerMediaType, + CompressionOperation: 42, // MUST fail here + }, + }) + assert.NotNil(t, err) +} + +func TestUpdateLayerInfosV2S2InvalidCompressionAlgorithm(t *testing.T) { + origManifest := manifestSchema2FromFixture(t, "v2s2.manifest.json") + err := origManifest.UpdateLayerInfos([]types.BlobInfo{ + { + Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + Size: 32654, + MediaType: DockerV2Schema2LayerMediaType, + CompressionOperation: types.Compress, + CompressionAlgorithm: &compression.Gzip, + }, + { + Digest: "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b", + Size: 16724, + MediaType: DockerV2Schema2LayerMediaType, + CompressionOperation: types.Compress, + CompressionAlgorithm: &compression.Gzip, + }, + { + Digest: "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736", + Size: 73109, + MediaType: DockerV2Schema2LayerMediaType, + CompressionOperation: types.Compress, + CompressionAlgorithm: &compression.Zstd, // MUST fail here + }, + }) + assert.NotNil(t, err) +} + +func TestUpdateLayerInfosV2S2NondistributableToGzip(t *testing.T) { + origManifest := manifestSchema2FromFixture(t, "v2s2.nondistributable.manifest.json") + + err := origManifest.UpdateLayerInfos([]types.BlobInfo{ + { + Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + Size: 32654, + MediaType: DockerV2Schema2ForeignLayerMediaType, + CompressionOperation: types.Compress, + CompressionAlgorithm: &compression.Gzip, + }, + }) + assert.Nil(t, err) + + updatedManifestBytes, err := origManifest.Serialize() + assert.Nil(t, err) + + expectedManifest := manifestSchema2FromFixture(t, "v2s2.nondistributable.gzip.manifest.json") + expectedManifestBytes, err := expectedManifest.Serialize() + assert.Nil(t, err) + + assert.Equal(t, string(expectedManifestBytes), string(updatedManifestBytes)) +} + +func TestUpdateLayerInfosV2S2NondistributableGzipToUncompressed(t *testing.T) { + origManifest := manifestSchema2FromFixture(t, "v2s2.nondistributable.gzip.manifest.json") + + err := origManifest.UpdateLayerInfos([]types.BlobInfo{ + { + Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + Size: 32654, + MediaType: DockerV2Schema2ForeignLayerMediaType, + CompressionOperation: types.Decompress, + }, + }) + assert.Nil(t, err) + + updatedManifestBytes, err := origManifest.Serialize() + assert.Nil(t, err) + + expectedManifest := manifestSchema2FromFixture(t, "v2s2.nondistributable.manifest.json") + expectedManifestBytes, err := expectedManifest.Serialize() + assert.Nil(t, err) + + assert.Equal(t, string(expectedManifestBytes), string(updatedManifestBytes)) +} + +func TestSchema2ImageID(t *testing.T) { + m := manifestSchema2FromFixture(t, "v2s2.manifest.json") + // These are not the real DiffID values, but they don’t actually matter in our implementation. + id, err := m.ImageID([]digest.Digest{ + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + }) + require.NoError(t, err) + assert.Equal(t, "b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7", id) +} + +func TestSchema2CanChangeLayerCompression(t *testing.T) { + m := manifestSchema2FromFixture(t, "v2s2.manifest.json") + + assert.True(t, m.CanChangeLayerCompression(DockerV2Schema2LayerMediaType)) + // Some projects like to use squashfs and other unspecified formats for layers; don’t touch those. + assert.False(t, m.CanChangeLayerCompression("a completely unknown and quite possibly invalid MIME type")) +} diff --git a/vendor/github.com/containers/image/v5/manifest/fixtures/non-json.manifest.json b/vendor/github.com/containers/image/v5/manifest/fixtures/non-json.manifest.json new file mode 120000 index 00000000000..367b7de9c9f --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/fixtures/non-json.manifest.json @@ -0,0 +1 @@ +../../internal/manifest/testdata/non-json.manifest.json \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1.artifact.json b/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1.artifact.json new file mode 120000 index 00000000000..fcec28a2fef --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1.artifact.json @@ -0,0 +1 @@ +../../internal/manifest/testdata/ociv1.artifact.json \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1.image.index.json b/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1.image.index.json new file mode 120000 index 00000000000..d5373fcfeef --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1.image.index.json @@ -0,0 +1 @@ +../../internal/manifest/testdata/ociv1.image.index.json \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1.invalid.mediatype.manifest.json b/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1.invalid.mediatype.manifest.json new file mode 100644 index 00000000000..8a93b2b145b --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1.invalid.mediatype.manifest.json @@ -0,0 +1,29 @@ +{ + "schemaVersion": 2, + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 7023, + "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+unknown", + "size": 32654, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 16724, + "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 73109, + "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736" + } + ], + "annotations": { + "com.example.key1": "value1", + "com.example.key2": "value2" + } + } diff --git a/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1.manifest.json b/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1.manifest.json new file mode 120000 index 00000000000..4927dc985b9 --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1.manifest.json @@ -0,0 +1 @@ +../../internal/manifest/testdata/ociv1.manifest.json \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1.nondistributable.gzip.manifest.json b/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1.nondistributable.gzip.manifest.json new file mode 100644 index 00000000000..ba1d377411e --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1.nondistributable.gzip.manifest.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 7023, + "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip", + "size": 32654, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" + } + ], + "annotations": { + "com.example.key1": "value1", + "com.example.key2": "value2" + } +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1.nondistributable.manifest.json b/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1.nondistributable.manifest.json new file mode 100644 index 00000000000..6ed34899805 --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1.nondistributable.manifest.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 7023, + "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.nondistributable.v1.tar", + "size": 32654, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" + } + ], + "annotations": { + "com.example.key1": "value1", + "com.example.key2": "value2" + } +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1.nondistributable.zstd.manifest.json b/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1.nondistributable.zstd.manifest.json new file mode 100644 index 00000000000..85e2b451355 --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1.nondistributable.zstd.manifest.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 7023, + "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.nondistributable.v1.tar+zstd", + "size": 32654, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" + } + ], + "annotations": { + "com.example.key1": "value1", + "com.example.key2": "value2" + } +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1.uncompressed.manifest.json b/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1.uncompressed.manifest.json new file mode 100644 index 00000000000..e3a137c8ab3 --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1.uncompressed.manifest.json @@ -0,0 +1,30 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 7023, + "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar", + "size": 32654, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar", + "size": 16724, + "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar", + "size": 73109, + "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736" + } + ], + "annotations": { + "com.example.key1": "value1", + "com.example.key2": "value2" + } +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1.zstd.manifest.json b/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1.zstd.manifest.json new file mode 100644 index 00000000000..b1891582f6b --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1.zstd.manifest.json @@ -0,0 +1,30 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 7023, + "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+zstd", + "size": 32654, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+zstd", + "size": 16724, + "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b" + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+zstd", + "size": 73109, + "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736" + } + ], + "annotations": { + "com.example.key1": "value1", + "com.example.key2": "value2" + } +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1nomime.artifact.json b/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1nomime.artifact.json new file mode 120000 index 00000000000..4b1ebfb0f30 --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1nomime.artifact.json @@ -0,0 +1 @@ +../../internal/manifest/testdata/ociv1nomime.artifact.json \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1nomime.image.index.json b/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1nomime.image.index.json new file mode 120000 index 00000000000..29b3d9be46b --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1nomime.image.index.json @@ -0,0 +1 @@ +../../internal/manifest/testdata/ociv1nomime.image.index.json \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1nomime.manifest.json b/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1nomime.manifest.json new file mode 120000 index 00000000000..037a21ff9b8 --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/fixtures/ociv1nomime.manifest.json @@ -0,0 +1 @@ +../../internal/manifest/testdata/ociv1nomime.manifest.json \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/manifest/fixtures/schema2-to-schema1-by-docker.json b/vendor/github.com/containers/image/v5/manifest/fixtures/schema2-to-schema1-by-docker.json new file mode 120000 index 00000000000..322c5b7415c --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/fixtures/schema2-to-schema1-by-docker.json @@ -0,0 +1 @@ +../../internal/manifest/testdata/schema2-to-schema1-by-docker.json \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/manifest/fixtures/unknown-version.manifest.json b/vendor/github.com/containers/image/v5/manifest/fixtures/unknown-version.manifest.json new file mode 120000 index 00000000000..c9136e913c3 --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/fixtures/unknown-version.manifest.json @@ -0,0 +1 @@ +../../internal/manifest/testdata/unknown-version.manifest.json \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/manifest/fixtures/v2list.manifest.json b/vendor/github.com/containers/image/v5/manifest/fixtures/v2list.manifest.json new file mode 120000 index 00000000000..8fb6441786a --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/fixtures/v2list.manifest.json @@ -0,0 +1 @@ +../../internal/manifest/testdata/v2list.manifest.json \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/manifest/fixtures/v2s1-invalid-signatures.manifest.json b/vendor/github.com/containers/image/v5/manifest/fixtures/v2s1-invalid-signatures.manifest.json new file mode 120000 index 00000000000..832703ed562 --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/fixtures/v2s1-invalid-signatures.manifest.json @@ -0,0 +1 @@ +../../internal/manifest/testdata/v2s1-invalid-signatures.manifest.json \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/manifest/fixtures/v2s1-unsigned.manifest.json b/vendor/github.com/containers/image/v5/manifest/fixtures/v2s1-unsigned.manifest.json new file mode 120000 index 00000000000..d6a55a7475f --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/fixtures/v2s1-unsigned.manifest.json @@ -0,0 +1 @@ +../../internal/manifest/testdata/v2s1-unsigned.manifest.json \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/manifest/fixtures/v2s1.manifest.json b/vendor/github.com/containers/image/v5/manifest/fixtures/v2s1.manifest.json new file mode 120000 index 00000000000..8021e76f016 --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/fixtures/v2s1.manifest.json @@ -0,0 +1 @@ +../../internal/manifest/testdata/v2s1.manifest.json \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/manifest/fixtures/v2s2.manifest.json b/vendor/github.com/containers/image/v5/manifest/fixtures/v2s2.manifest.json new file mode 120000 index 00000000000..f172a45c59a --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/fixtures/v2s2.manifest.json @@ -0,0 +1 @@ +../../internal/manifest/testdata/v2s2.manifest.json \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/manifest/fixtures/v2s2.nondistributable.gzip.manifest.json b/vendor/github.com/containers/image/v5/manifest/fixtures/v2s2.nondistributable.gzip.manifest.json new file mode 100644 index 00000000000..56bd5160f82 --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/fixtures/v2s2.nondistributable.gzip.manifest.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 7023, + "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip", + "size": 32654, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" + } + ], + "annotations": { + "com.example.key1": "value1", + "com.example.key2": "value2" + } +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/manifest/fixtures/v2s2.nondistributable.manifest.json b/vendor/github.com/containers/image/v5/manifest/fixtures/v2s2.nondistributable.manifest.json new file mode 100644 index 00000000000..66920c1a817 --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/fixtures/v2s2.nondistributable.manifest.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 7023, + "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.foreign.diff.tar", + "size": 32654, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" + } + ], + "annotations": { + "com.example.key1": "value1", + "com.example.key2": "value2" + } +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/manifest/fixtures/v2s2.uncompressed.manifest.json b/vendor/github.com/containers/image/v5/manifest/fixtures/v2s2.uncompressed.manifest.json new file mode 100644 index 00000000000..869d97e94a3 --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/fixtures/v2s2.uncompressed.manifest.json @@ -0,0 +1,26 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 7023, + "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar", + "size": 32654, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar", + "size": 16724, + "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar", + "size": 73109, + "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736" + } + ] +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/manifest/fixtures/v2s2nomime.manifest.json b/vendor/github.com/containers/image/v5/manifest/fixtures/v2s2nomime.manifest.json new file mode 120000 index 00000000000..bf022a4426a --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/fixtures/v2s2nomime.manifest.json @@ -0,0 +1 @@ +../../internal/manifest/testdata/v2s2nomime.manifest.json \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/manifest/fixtures_info_test.go b/vendor/github.com/containers/image/v5/manifest/fixtures_info_test.go new file mode 100644 index 00000000000..bfdaed1a0b4 --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/fixtures_info_test.go @@ -0,0 +1,12 @@ +package manifest + +import "github.com/opencontainers/go-digest" + +const ( + // TestV2S2ManifestDigest is the Docker manifest digest of "v2s2.manifest.json" + TestDockerV2S2ManifestDigest = digest.Digest("sha256:20bf21ed457b390829cdbeec8795a7bea1626991fda603e0d01b4e7f60427e55") + // TestV2S1ManifestDigest is the Docker manifest digest of "v2s1.manifest.json" + TestDockerV2S1ManifestDigest = digest.Digest("sha256:7364fea9d84ee548ab67d4c46c6006289800c98de3fbf8c0a97138dfcc23f000") + // TestV2S1UnsignedManifestDigest is the Docker manifest digest of "v2s1unsigned.manifest.json" + TestDockerV2S1UnsignedManifestDigest = digest.Digest("sha256:7364fea9d84ee548ab67d4c46c6006289800c98de3fbf8c0a97138dfcc23f000") +) diff --git a/vendor/github.com/containers/image/v5/manifest/list_test.go b/vendor/github.com/containers/image/v5/manifest/list_test.go new file mode 100644 index 00000000000..25261b91a0e --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/list_test.go @@ -0,0 +1,141 @@ +package manifest + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/containers/image/v5/internal/manifest" + "github.com/containers/image/v5/types" + digest "github.com/opencontainers/go-digest" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func pare(m List) { + if impl, ok := m.(*manifest.OCI1Index); ok { + impl.Annotations = nil + } + if impl, ok := m.(*manifest.Schema2List); ok { + for i := range impl.Manifests { + impl.Manifests[i].Platform.Features = nil + } + } +} + +func TestParseLists(t *testing.T) { + cases := []struct { + path string + mimeType string + }{ + {"ociv1.image.index.json", imgspecv1.MediaTypeImageIndex}, + {"v2list.manifest.json", DockerV2ListMediaType}, + } + for _, c := range cases { + manifest, err := os.ReadFile(filepath.Join("fixtures", c.path)) + require.NoError(t, err, "error reading file %q", filepath.Join("fixtures", c.path)) + assert.Equal(t, GuessMIMEType(manifest), c.mimeType) + + _, err = FromBlob(manifest, c.mimeType) + require.Error(t, err, "manifest list %q should not parse as single images", c.path) + + m, err := ListFromBlob(manifest, c.mimeType) + require.NoError(t, err, "manifest list %q should parse as list types", c.path) + assert.Equal(t, m.MIMEType(), c.mimeType, "manifest %q is not of the expected MIME type", c.path) + + clone := m.Clone() + assert.Equal(t, clone, m, "manifest %q is missing some fields after being cloned", c.path) + + pare(m) + + index, err := m.ConvertToMIMEType(imgspecv1.MediaTypeImageIndex) + require.NoError(t, err, "error converting %q to an OCI1Index", c.path) + + list, err := m.ConvertToMIMEType(DockerV2ListMediaType) + require.NoError(t, err, "error converting %q to an Schema2List", c.path) + + index2, err := list.ConvertToMIMEType(imgspecv1.MediaTypeImageIndex) + require.NoError(t, err) + assert.Equal(t, index, index2, "index %q lost data in conversion", c.path) + + list2, err := index.ConvertToMIMEType(DockerV2ListMediaType) + require.NoError(t, err) + assert.Equal(t, list, list2, "list %q lost data in conversion", c.path) + } +} + +func TestChooseInstance(t *testing.T) { + type expectedMatch struct { + arch, variant string + instanceDigest digest.Digest + } + for _, manifestList := range []struct { + listFile string + matchedInstances []expectedMatch + unmatchedInstances []string + }{ + { + listFile: "schema2list.json", + matchedInstances: []expectedMatch{ + {"amd64", "", "sha256:030fcb92e1487b18c974784dcc110a93147c9fc402188370fbfd17efabffc6af"}, + {"s390x", "", "sha256:e5aa1b0a24620228b75382997a0977f609b3ca3a95533dafdef84c74cc8df642"}, + {"arm", "v7", "sha256:b5dbad4bdb4444d919294afe49a095c23e86782f98cdf0aa286198ddb814b50b"}, + {"arm64", "", "sha256:dc472a59fb006797aa2a6bfb54cc9c57959bb0a6d11fadaa608df8c16dea39cf"}, + }, + unmatchedInstances: []string{ + "unmatched", + }, + }, + { // Focus on ARM variant field testing + listFile: "schema2list-variants.json", + matchedInstances: []expectedMatch{ + {"amd64", "", "sha256:59eec8837a4d942cc19a52b8c09ea75121acc38114a2c68b98983ce9356b8610"}, + {"arm", "v7", "sha256:f365626a556e58189fc21d099fc64603db0f440bff07f77c740989515c544a39"}, + {"arm", "v6", "sha256:f365626a556e58189fc21d099fc64603db0f440bff07f77c740989515c544a39"}, + {"arm", "v5", "sha256:c84b0a3a07b628bc4d62e5047d0f8dff80f7c00979e1e28a821a033ecda8fe53"}, + {"arm", "", "sha256:c84b0a3a07b628bc4d62e5047d0f8dff80f7c00979e1e28a821a033ecda8fe53"}, + {"arm", "unrecognized-present", "sha256:bcf9771c0b505e68c65440474179592ffdfa98790eb54ffbf129969c5e429990"}, + {"arm", "unrecognized-not-present", "sha256:c84b0a3a07b628bc4d62e5047d0f8dff80f7c00979e1e28a821a033ecda8fe53"}, + }, + unmatchedInstances: []string{ + "unmatched", + }, + }, + { + listFile: "oci1index.json", + matchedInstances: []expectedMatch{ + {"amd64", "", "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270"}, + {"ppc64le", "", "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f"}, + }, + unmatchedInstances: []string{ + "unmatched", + }, + }, + } { + rawManifest, err := os.ReadFile(filepath.Join("..", "internal", "manifest", "testdata", manifestList.listFile)) + require.NoError(t, err) + list, err := ListFromBlob(rawManifest, GuessMIMEType(rawManifest)) + require.NoError(t, err) + // Match found + for _, match := range manifestList.matchedInstances { + testName := fmt.Sprintf("%s %q+%q", manifestList.listFile, match.arch, match.variant) + digest, err := list.ChooseInstance(&types.SystemContext{ + ArchitectureChoice: match.arch, + VariantChoice: match.variant, + OSChoice: "linux", + }) + require.NoError(t, err, testName) + assert.Equal(t, match.instanceDigest, digest, testName) + } + // Not found + for _, arch := range manifestList.unmatchedInstances { + _, err := list.ChooseInstance(&types.SystemContext{ + ArchitectureChoice: arch, + OSChoice: "linux", + }) + assert.Error(t, err) + } + } +} diff --git a/vendor/github.com/containers/image/v5/manifest/manifest_test.go b/vendor/github.com/containers/image/v5/manifest/manifest_test.go new file mode 100644 index 00000000000..677b13650e4 --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/manifest_test.go @@ -0,0 +1,169 @@ +package manifest + +import ( + "os" + "path/filepath" + "testing" + + "github.com/containers/libtrust" + digest "github.com/opencontainers/go-digest" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + digestSha256EmptyTar = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +) + +func TestGuessMIMEType(t *testing.T) { + cases := []struct { + path string + mimeType string + }{ + {"v2s2.manifest.json", DockerV2Schema2MediaType}, + {"v2list.manifest.json", DockerV2ListMediaType}, + {"v2s1.manifest.json", DockerV2Schema1SignedMediaType}, + {"v2s1-unsigned.manifest.json", DockerV2Schema1MediaType}, + {"v2s1-invalid-signatures.manifest.json", DockerV2Schema1SignedMediaType}, + {"v2s2nomime.manifest.json", DockerV2Schema2MediaType}, // It is unclear whether this one is legal, but we should guess v2s2 if anything at all. + {"unknown-version.manifest.json", ""}, + {"non-json.manifest.json", ""}, // Not a manifest (nor JSON) at all + {"ociv1.manifest.json", imgspecv1.MediaTypeImageManifest}, + {"ociv1.artifact.json", imgspecv1.MediaTypeImageManifest}, + {"ociv1.image.index.json", imgspecv1.MediaTypeImageIndex}, + {"ociv1nomime.manifest.json", imgspecv1.MediaTypeImageManifest}, + {"ociv1nomime.artifact.json", imgspecv1.MediaTypeImageManifest}, + {"ociv1nomime.image.index.json", imgspecv1.MediaTypeImageIndex}, + } + + for _, c := range cases { + manifest, err := os.ReadFile(filepath.Join("fixtures", c.path)) + require.NoError(t, err) + mimeType := GuessMIMEType(manifest) + assert.Equal(t, c.mimeType, mimeType, c.path) + } +} + +func TestDigest(t *testing.T) { + cases := []struct { + path string + expectedDigest digest.Digest + }{ + {"v2s2.manifest.json", TestDockerV2S2ManifestDigest}, + {"v2s1.manifest.json", TestDockerV2S1ManifestDigest}, + {"v2s1-unsigned.manifest.json", TestDockerV2S1UnsignedManifestDigest}, + } + for _, c := range cases { + manifest, err := os.ReadFile(filepath.Join("fixtures", c.path)) + require.NoError(t, err) + actualDigest, err := Digest(manifest) + require.NoError(t, err) + assert.Equal(t, c.expectedDigest, actualDigest) + } + + manifest, err := os.ReadFile("fixtures/v2s1-invalid-signatures.manifest.json") + require.NoError(t, err) + _, err = Digest(manifest) + assert.Error(t, err) + + actualDigest, err := Digest([]byte{}) + require.NoError(t, err) + assert.Equal(t, digest.Digest(digestSha256EmptyTar), actualDigest) +} + +func TestMatchesDigest(t *testing.T) { + cases := []struct { + path string + expectedDigest digest.Digest + result bool + }{ + // Success + {"v2s2.manifest.json", TestDockerV2S2ManifestDigest, true}, + {"v2s1.manifest.json", TestDockerV2S1ManifestDigest, true}, + // No match (switched s1/s2) + {"v2s2.manifest.json", TestDockerV2S1ManifestDigest, false}, + {"v2s1.manifest.json", TestDockerV2S2ManifestDigest, false}, + // Unrecognized algorithm + {"v2s2.manifest.json", digest.Digest("md5:2872f31c5c1f62a694fbd20c1e85257c"), false}, + // Mangled format + {"v2s2.manifest.json", digest.Digest(TestDockerV2S2ManifestDigest.String() + "abc"), false}, + {"v2s2.manifest.json", digest.Digest(TestDockerV2S2ManifestDigest.String()[:20]), false}, + {"v2s2.manifest.json", digest.Digest(""), false}, + } + for _, c := range cases { + manifest, err := os.ReadFile(filepath.Join("fixtures", c.path)) + require.NoError(t, err) + res, err := MatchesDigest(manifest, c.expectedDigest) + require.NoError(t, err) + assert.Equal(t, c.result, res) + } + + manifest, err := os.ReadFile("fixtures/v2s1-invalid-signatures.manifest.json") + require.NoError(t, err) + // Even a correct SHA256 hash is rejected if we can't strip the JSON signature. + res, err := MatchesDigest(manifest, digest.FromBytes(manifest)) + assert.False(t, res) + assert.Error(t, err) + + res, err = MatchesDigest([]byte{}, digest.Digest(digestSha256EmptyTar)) + assert.True(t, res) + assert.NoError(t, err) +} + +func TestAddDummyV2S1Signature(t *testing.T) { + manifest, err := os.ReadFile("fixtures/v2s1-unsigned.manifest.json") + require.NoError(t, err) + + signedManifest, err := AddDummyV2S1Signature(manifest) + require.NoError(t, err) + + sig, err := libtrust.ParsePrettySignature(signedManifest, "signatures") + require.NoError(t, err) + signaturePayload, err := sig.Payload() + require.NoError(t, err) + assert.Equal(t, manifest, signaturePayload) + + _, err = AddDummyV2S1Signature([]byte("}this is invalid JSON")) + assert.Error(t, err) +} + +func TestMIMETypeIsMultiImage(t *testing.T) { + for _, c := range []struct { + mt string + expected bool + }{ + {DockerV2ListMediaType, true}, + {DockerV2Schema1MediaType, false}, + {DockerV2Schema1SignedMediaType, false}, + {DockerV2Schema2MediaType, false}, + {imgspecv1.MediaTypeImageIndex, true}, + {imgspecv1.MediaTypeImageManifest, false}, + } { + res := MIMETypeIsMultiImage(c.mt) + assert.Equal(t, c.expected, res, c.mt) + } +} + +func TestNormalizedMIMEType(t *testing.T) { + for _, c := range []string{ // Valid MIME types, normalized to themselves + DockerV2Schema1MediaType, + DockerV2Schema1SignedMediaType, + DockerV2Schema2MediaType, + DockerV2ListMediaType, + imgspecv1.MediaTypeImageManifest, + imgspecv1.MediaTypeImageIndex, + } { + res := NormalizedMIMEType(c) + assert.Equal(t, c, res, c) + } + for _, c := range []string{ + "application/json", + "text/plain", + "not at all a valid MIME type", + "", + } { + res := NormalizedMIMEType(c) + assert.Equal(t, DockerV2Schema1SignedMediaType, res, c) + } +} diff --git a/vendor/github.com/containers/image/v5/manifest/oci_index_test.go b/vendor/github.com/containers/image/v5/manifest/oci_index_test.go new file mode 100644 index 00000000000..defa85f543b --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/oci_index_test.go @@ -0,0 +1,28 @@ +package manifest + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestOCI1IndexFromManifest(t *testing.T) { + validManifest, err := os.ReadFile(filepath.Join("fixtures", "ociv1.image.index.json")) + require.NoError(t, err) + + parser := func(m []byte) error { + _, err := OCI1IndexFromManifest(m) + return err + } + // Schema mismatch is rejected + testManifestFixturesAreRejected(t, parser, []string{ + "schema2-to-schema1-by-docker.json", + "v2s2.manifest.json", + // Not "v2list.manifest.json" yet, without mediaType the two are too similar to tell the difference. + "ociv1.manifest.json", + }) + // Extra fields are rejected + testValidManifestWithExtraFieldsIsRejected(t, parser, validManifest, []string{"config", "fsLayers", "history", "layers"}) +} diff --git a/vendor/github.com/containers/image/v5/manifest/oci_test.go b/vendor/github.com/containers/image/v5/manifest/oci_test.go new file mode 100644 index 00000000000..470724226bc --- /dev/null +++ b/vendor/github.com/containers/image/v5/manifest/oci_test.go @@ -0,0 +1,386 @@ +package manifest + +import ( + "os" + "path/filepath" + "testing" + + "github.com/containers/image/v5/pkg/compression" + "github.com/containers/image/v5/types" + "github.com/opencontainers/go-digest" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func manifestOCI1FromFixture(t *testing.T, fixture string) *OCI1 { + manifest, err := os.ReadFile(filepath.Join("fixtures", fixture)) + require.NoError(t, err) + + m, err := OCI1FromManifest(manifest) + require.NoError(t, err) + return m +} + +func TestSupportedOCI1MediaType(t *testing.T) { + type testData struct { + m string + mustFail bool + } + data := []testData{ + {imgspecv1.MediaTypeDescriptor, false}, + {imgspecv1.MediaTypeImageConfig, false}, + {imgspecv1.MediaTypeImageLayer, false}, + {imgspecv1.MediaTypeImageLayerGzip, false}, + {imgspecv1.MediaTypeImageLayerNonDistributable, false}, //nolint:staticcheck // NonDistributable layers are deprecated, but we want to continue to support manipulating pre-existing images. + {imgspecv1.MediaTypeImageLayerNonDistributableGzip, false}, //nolint:staticcheck // NonDistributable layers are deprecated, but we want to continue to support manipulating pre-existing images. + {imgspecv1.MediaTypeImageLayerNonDistributableZstd, false}, //nolint:staticcheck // NonDistributable layers are deprecated, but we want to continue to support manipulating pre-existing images. + {imgspecv1.MediaTypeImageLayerZstd, false}, + {imgspecv1.MediaTypeImageManifest, false}, + {imgspecv1.MediaTypeLayoutHeader, false}, + {"application/vnd.oci.image.layer.nondistributable.v1.tar+unknown", true}, + } + for _, d := range data { + err := SupportedOCI1MediaType(d.m) + if d.mustFail { + assert.NotNil(t, err) + } else { + assert.Nil(t, err) + } + } +} + +func TestOCI1FromManifest(t *testing.T) { + validManifest, err := os.ReadFile(filepath.Join("fixtures", "ociv1.manifest.json")) + require.NoError(t, err) + + parser := func(m []byte) error { + _, err := OCI1FromManifest(m) + return err + } + // Schema mismatch is rejected + testManifestFixturesAreRejected(t, parser, []string{ + "schema2-to-schema1-by-docker.json", + // Not "v2s2.manifest.json" yet, without mediaType the two are too similar to tell the difference. + "v2list.manifest.json", + "ociv1.image.index.json", + }) + // Extra fields are rejected + testValidManifestWithExtraFieldsIsRejected(t, parser, validManifest, []string{"fsLayers", "history", "manifests"}) +} + +func TestUpdateLayerInfosOCIGzipToZstd(t *testing.T) { + manifest := manifestOCI1FromFixture(t, "ociv1.manifest.json") + + err := manifest.UpdateLayerInfos([]types.BlobInfo{ + { + Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + Size: 32654, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + CompressionOperation: types.Compress, + CompressionAlgorithm: &compression.Zstd, + }, + { + Digest: "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b", + Size: 16724, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + CompressionOperation: types.Compress, + CompressionAlgorithm: &compression.Zstd, + }, + { + Digest: "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736", + Size: 73109, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + CompressionOperation: types.Compress, + CompressionAlgorithm: &compression.Zstd, + }, + }) + assert.Nil(t, err) + + updatedManifestBytes, err := manifest.Serialize() + assert.Nil(t, err) + + expectedManifest := manifestOCI1FromFixture(t, "ociv1.zstd.manifest.json") + expectedManifestBytes, err := expectedManifest.Serialize() + assert.Nil(t, err) + + assert.Equal(t, string(expectedManifestBytes), string(updatedManifestBytes)) +} + +func TestUpdateLayerInfosOCIZstdToGzip(t *testing.T) { + manifest := manifestOCI1FromFixture(t, "ociv1.zstd.manifest.json") + + err := manifest.UpdateLayerInfos([]types.BlobInfo{ + { + Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + Size: 32654, + MediaType: imgspecv1.MediaTypeImageLayerZstd, + CompressionOperation: types.Compress, + CompressionAlgorithm: &compression.Gzip, + }, + { + Digest: "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b", + Size: 16724, + MediaType: imgspecv1.MediaTypeImageLayerZstd, + CompressionOperation: types.Compress, + CompressionAlgorithm: &compression.Gzip, + }, + { + Digest: "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736", + Size: 73109, + MediaType: imgspecv1.MediaTypeImageLayerZstd, + CompressionOperation: types.Compress, + CompressionAlgorithm: &compression.Gzip, + }, + }) + assert.Nil(t, err) + + updatedManifestBytes, err := manifest.Serialize() + assert.Nil(t, err) + + expectedManifest := manifestOCI1FromFixture(t, "ociv1.manifest.json") + expectedManifestBytes, err := expectedManifest.Serialize() + assert.Nil(t, err) + + assert.Equal(t, string(expectedManifestBytes), string(updatedManifestBytes)) +} + +func TestUpdateLayerInfosOCIZstdToUncompressed(t *testing.T) { + manifest := manifestOCI1FromFixture(t, "ociv1.zstd.manifest.json") + + err := manifest.UpdateLayerInfos([]types.BlobInfo{ + { + Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + Size: 32654, + MediaType: imgspecv1.MediaTypeImageLayerZstd, + CompressionOperation: types.Decompress, + }, + { + Digest: "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b", + Size: 16724, + MediaType: imgspecv1.MediaTypeImageLayerZstd, + CompressionOperation: types.Decompress, + }, + { + Digest: "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736", + Size: 73109, + MediaType: imgspecv1.MediaTypeImageLayerZstd, + CompressionOperation: types.Decompress, + }, + }) + assert.Nil(t, err) + + updatedManifestBytes, err := manifest.Serialize() + assert.Nil(t, err) + + expectedManifest := manifestOCI1FromFixture(t, "ociv1.uncompressed.manifest.json") + expectedManifestBytes, err := expectedManifest.Serialize() + assert.Nil(t, err) + + assert.Equal(t, string(expectedManifestBytes), string(updatedManifestBytes)) +} + +func TestUpdateLayerInfosInvalidCompressionOperation(t *testing.T) { + manifest := manifestOCI1FromFixture(t, "ociv1.zstd.manifest.json") + err := manifest.UpdateLayerInfos([]types.BlobInfo{ + { + Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + Size: 32654, + MediaType: imgspecv1.MediaTypeImageLayerZstd, + CompressionOperation: types.Compress, + CompressionAlgorithm: &compression.Gzip, + }, + { + Digest: "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b", + Size: 16724, + MediaType: imgspecv1.MediaTypeImageLayerZstd, + CompressionOperation: 42, // MUST fail here + CompressionAlgorithm: &compression.Gzip, + }, + { + Digest: "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736", + Size: 73109, + MediaType: imgspecv1.MediaTypeImageLayerZstd, + CompressionOperation: types.Compress, + CompressionAlgorithm: &compression.Gzip, + }, + }) + assert.NotNil(t, err) +} + +func TestUpdateLayerInfosInvalidCompressionAlgorithm(t *testing.T) { + manifest := manifestOCI1FromFixture(t, "ociv1.zstd.manifest.json") + + customCompression := compression.Algorithm{} + err := manifest.UpdateLayerInfos([]types.BlobInfo{ + { + Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + Size: 32654, + MediaType: imgspecv1.MediaTypeImageLayerZstd, + CompressionOperation: types.Compress, + CompressionAlgorithm: &compression.Gzip, + }, + { + Digest: "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b", + Size: 16724, + MediaType: imgspecv1.MediaTypeImageLayerZstd, + CompressionOperation: 42, + CompressionAlgorithm: &compression.Gzip, + }, + { + Digest: "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736", + Size: 73109, + MediaType: imgspecv1.MediaTypeImageLayerZstd, + CompressionOperation: types.Compress, + CompressionAlgorithm: &customCompression, // MUST fail here + }, + }) + assert.NotNil(t, err) +} + +func TestUpdateLayerInfosOCIGzipToUncompressed(t *testing.T) { + manifest := manifestOCI1FromFixture(t, "ociv1.manifest.json") + + err := manifest.UpdateLayerInfos([]types.BlobInfo{ + { + Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + Size: 32654, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + CompressionOperation: types.Decompress, + }, + { + Digest: "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b", + Size: 16724, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + CompressionOperation: types.Decompress, + }, + { + Digest: "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736", + Size: 73109, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + CompressionOperation: types.Decompress, + }, + }) + assert.Nil(t, err) + + updatedManifestBytes, err := manifest.Serialize() + assert.Nil(t, err) + + expectedManifest := manifestOCI1FromFixture(t, "ociv1.uncompressed.manifest.json") + expectedManifestBytes, err := expectedManifest.Serialize() + assert.Nil(t, err) + + assert.Equal(t, string(expectedManifestBytes), string(updatedManifestBytes)) +} + +func TestUpdateLayerInfosOCINondistributableToGzip(t *testing.T) { + manifest := manifestOCI1FromFixture(t, "ociv1.nondistributable.manifest.json") + + err := manifest.UpdateLayerInfos([]types.BlobInfo{ + { + Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + Size: 32654, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + CompressionOperation: types.Compress, + CompressionAlgorithm: &compression.Gzip, + }, + }) + assert.Nil(t, err) + + updatedManifestBytes, err := manifest.Serialize() + assert.Nil(t, err) + + expectedManifest := manifestOCI1FromFixture(t, "ociv1.nondistributable.gzip.manifest.json") + expectedManifestBytes, err := expectedManifest.Serialize() + assert.Nil(t, err) + + assert.Equal(t, string(expectedManifestBytes), string(updatedManifestBytes)) +} + +func TestUpdateLayerInfosOCINondistributableToZstd(t *testing.T) { + manifest := manifestOCI1FromFixture(t, "ociv1.nondistributable.manifest.json") + + err := manifest.UpdateLayerInfos([]types.BlobInfo{ + { + Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + Size: 32654, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + CompressionOperation: types.Compress, + CompressionAlgorithm: &compression.Zstd, + }, + }) + assert.Nil(t, err) + + updatedManifestBytes, err := manifest.Serialize() + assert.Nil(t, err) + + expectedManifest := manifestOCI1FromFixture(t, "ociv1.nondistributable.zstd.manifest.json") + expectedManifestBytes, err := expectedManifest.Serialize() + assert.Nil(t, err) + + assert.Equal(t, string(expectedManifestBytes), string(updatedManifestBytes)) +} + +func TestUpdateLayerInfosOCINondistributableGzipToUncompressed(t *testing.T) { + manifest := manifestOCI1FromFixture(t, "ociv1.nondistributable.gzip.manifest.json") + + err := manifest.UpdateLayerInfos([]types.BlobInfo{ + { + Digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + Size: 32654, + MediaType: imgspecv1.MediaTypeImageLayerGzip, + CompressionOperation: types.Decompress, + }, + }) + assert.Nil(t, err) + + updatedManifestBytes, err := manifest.Serialize() + assert.Nil(t, err) + + expectedManifest := manifestOCI1FromFixture(t, "ociv1.nondistributable.manifest.json") + expectedManifestBytes, err := expectedManifest.Serialize() + assert.Nil(t, err) + + assert.Equal(t, string(expectedManifestBytes), string(updatedManifestBytes)) +} + +func TestOCI1Inspect(t *testing.T) { + // Success is tested in image.TestManifestOCI1Inspect . + m := manifestOCI1FromFixture(t, "ociv1.artifact.json") + _, err := m.Inspect(func(info types.BlobInfo) ([]byte, error) { + require.Equal(t, m.Config.Digest, info.Digest) + // This just-enough-artifact contains a zero-byte config, sanity-check that’s till the case. + require.Equal(t, int64(0), m.Config.Size) + return []byte{}, nil + }) + var expected NonImageArtifactError + assert.ErrorAs(t, err, &expected) +} + +func TestOCI1ImageID(t *testing.T) { + m := manifestOCI1FromFixture(t, "ociv1.manifest.json") + // These are not the real DiffID values, but they don’t actually matter in our implementation. + id, err := m.ImageID([]digest.Digest{ + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + }) + require.NoError(t, err) + assert.Equal(t, "b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7", id) + + m = manifestOCI1FromFixture(t, "ociv1.artifact.json") + _, err = m.ImageID([]digest.Digest{}) + var expected NonImageArtifactError + assert.ErrorAs(t, err, &expected) +} + +func TestOCI1CanChangeLayerCompression(t *testing.T) { + m := manifestOCI1FromFixture(t, "ociv1.manifest.json") + + assert.True(t, m.CanChangeLayerCompression(imgspecv1.MediaTypeImageLayerGzip)) + // Some projects like to use squashfs and other unspecified formats for layers; don’t touch those. + assert.False(t, m.CanChangeLayerCompression("a completely unknown and quite possibly invalid MIME type")) + + artifact := manifestOCI1FromFixture(t, "ociv1.artifact.json") + assert.False(t, artifact.CanChangeLayerCompression(imgspecv1.MediaTypeImageLayerGzip)) +} diff --git a/vendor/github.com/containers/image/v5/oci/archive/oci_dest_test.go b/vendor/github.com/containers/image/v5/oci/archive/oci_dest_test.go new file mode 100644 index 00000000000..a67112cf30f --- /dev/null +++ b/vendor/github.com/containers/image/v5/oci/archive/oci_dest_test.go @@ -0,0 +1,5 @@ +package archive + +import "github.com/containers/image/v5/internal/private" + +var _ private.ImageDestination = (*ociArchiveImageDestination)(nil) diff --git a/vendor/github.com/containers/image/v5/oci/archive/oci_src_test.go b/vendor/github.com/containers/image/v5/oci/archive/oci_src_test.go new file mode 100644 index 00000000000..6f00afc0cc0 --- /dev/null +++ b/vendor/github.com/containers/image/v5/oci/archive/oci_src_test.go @@ -0,0 +1,5 @@ +package archive + +import "github.com/containers/image/v5/internal/private" + +var _ private.ImageSource = (*ociArchiveImageSource)(nil) diff --git a/vendor/github.com/containers/image/v5/oci/archive/oci_transport_test.go b/vendor/github.com/containers/image/v5/oci/archive/oci_transport_test.go new file mode 100644 index 00000000000..f521a27573f --- /dev/null +++ b/vendor/github.com/containers/image/v5/oci/archive/oci_transport_test.go @@ -0,0 +1,274 @@ +package archive + +import ( + "context" + "os" + "path/filepath" + "testing" + + _ "github.com/containers/image/v5/internal/testing/explicitfilepath-tmpdir" + "github.com/containers/image/v5/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTransportName(t *testing.T) { + assert.Equal(t, "oci-archive", Transport.Name()) +} + +func TestTransportParseReference(t *testing.T) { + testParseReference(t, Transport.ParseReference) +} + +func TestTransportValidatePolicyConfigurationScope(t *testing.T) { + for _, scope := range []string{ + "/etc", + "/this/does/not/exist", + } { + err := Transport.ValidatePolicyConfigurationScope(scope) + assert.NoError(t, err, scope) + } + + for _, scope := range []string{ + "relative/path", + "/", + "/double//slashes", + "/has/./dot", + "/has/dot/../dot", + "/trailing/slash/", + } { + err := Transport.ValidatePolicyConfigurationScope(scope) + assert.Error(t, err, scope) + } +} + +func TestParseReference(t *testing.T) { + testParseReference(t, ParseReference) +} + +// testParseReference is a test shared for Transport.ParseReference and ParseReference. +func testParseReference(t *testing.T, fn func(string) (types.ImageReference, error)) { + tmpDir := t.TempDir() + + for _, path := range []string{ + "/", + "/etc", + tmpDir, + "relativepath", + tmpDir + "/thisdoesnotexist", + } { + for _, image := range []struct{ suffix, image string }{ + {":notlatest:image", "notlatest:image"}, + {":latestimage", "latestimage"}, + {":", ""}, + {"", ""}, + } { + input := path + image.suffix + ref, err := fn(input) + require.NoError(t, err, input) + ociArchRef, ok := ref.(ociArchiveReference) + require.True(t, ok) + assert.Equal(t, path, ociArchRef.file, input) + assert.Equal(t, image.image, ociArchRef.image, input) + } + } + + _, err := fn(tmpDir + ":invalid'image!value@") + assert.Error(t, err) +} + +func TestNewReference(t *testing.T) { + const ( + imageValue = "imageValue" + noImageValue = "" + ) + + tmpDir := t.TempDir() + + ref, err := NewReference(tmpDir, imageValue) + require.NoError(t, err) + ociArchRef, ok := ref.(ociArchiveReference) + require.True(t, ok) + assert.Equal(t, tmpDir, ociArchRef.file) + assert.Equal(t, imageValue, ociArchRef.image) + + ref, err = NewReference(tmpDir, noImageValue) + require.NoError(t, err) + ociArchRef, ok = ref.(ociArchiveReference) + require.True(t, ok) + assert.Equal(t, tmpDir, ociArchRef.file) + assert.Equal(t, noImageValue, ociArchRef.image) + + _, err = NewReference(tmpDir+"/thisparentdoesnotexist/something", imageValue) + assert.Error(t, err) + + _, err = NewReference(tmpDir, "invalid'image!value@") + assert.Error(t, err) + + _, err = NewReference(tmpDir+"/has:colon", imageValue) + assert.Error(t, err) +} + +// refToTempOCI creates a temporary directory and returns an reference to it. +func refToTempOCI(t *testing.T) (types.ImageReference, string) { + tmpDir := t.TempDir() + m := `{ + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7143, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + "platform": { + "architecture": "ppc64le", + "os": "linux" + }, + "annotations": { + "org.opencontainers.image.ref.name": "imageValue" + } + } + ] + } +` + err := os.WriteFile(filepath.Join(tmpDir, "index.json"), []byte(m), 0644) + require.NoError(t, err) + ref, err := NewReference(tmpDir, "imageValue") + require.NoError(t, err) + return ref, tmpDir +} + +// refToTempOCIArchive creates a temporary directory, copies the contents of that directory +// to a temporary tar file and returns a reference to the temporary tar file +func refToTempOCIArchive(t *testing.T) (ref types.ImageReference, tmpTarFile string) { + tmpDir := t.TempDir() + m := `{ + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7143, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + "platform": { + "architecture": "ppc64le", + "os": "linux" + }, + "annotations": { + "org.opencontainers.image.ref.name": "imageValue" + } + } + ] + } +` + err := os.WriteFile(filepath.Join(tmpDir, "index.json"), []byte(m), 0644) + require.NoError(t, err) + tarFile, err := os.CreateTemp("", "oci-transport-test.tar") + require.NoError(t, err) + err = tarDirectory(tmpDir, tarFile.Name()) + require.NoError(t, err) + ref, err = NewReference(tarFile.Name(), "") + require.NoError(t, err) + return ref, tarFile.Name() +} + +func TestReferenceTransport(t *testing.T) { + ref, _ := refToTempOCI(t) + assert.Equal(t, Transport, ref.Transport()) +} + +func TestReferenceStringWithinTransport(t *testing.T) { + tmpDir := t.TempDir() + + for _, c := range []struct{ input, result string }{ + {"/dir1:notlatest:notlatest", "/dir1:notlatest:notlatest"}, // Explicit image + {"/dir3:", "/dir3:"}, // No image + } { + ref, err := ParseReference(tmpDir + c.input) + require.NoError(t, err, c.input) + stringRef := ref.StringWithinTransport() + assert.Equal(t, tmpDir+c.result, stringRef, c.input) + // Do one more round to verify that the output can be parsed, to an equal value. + ref2, err := Transport.ParseReference(stringRef) + require.NoError(t, err, c.input) + stringRef2 := ref2.StringWithinTransport() + assert.Equal(t, stringRef, stringRef2, c.input) + } +} + +func TestReferenceDockerReference(t *testing.T) { + ref, _ := refToTempOCI(t) + assert.Nil(t, ref.DockerReference()) +} + +func TestReferencePolicyConfigurationIdentity(t *testing.T) { + ref, tmpDir := refToTempOCI(t) + + assert.Equal(t, tmpDir, ref.PolicyConfigurationIdentity()) + // A non-canonical path. Test just one, the various other cases are + // tested in explicitfilepath.ResolvePathToFullyExplicit. + ref, err := NewReference(tmpDir+"/.", "image2") + require.NoError(t, err) + assert.Equal(t, tmpDir, ref.PolicyConfigurationIdentity()) + + // "/" as a corner case. + ref, err = NewReference("/", "image3") + require.NoError(t, err) + assert.Equal(t, "/", ref.PolicyConfigurationIdentity()) +} + +func TestReferencePolicyConfigurationNamespaces(t *testing.T) { + ref, tmpDir := refToTempOCI(t) + // We don't really know enough to make a full equality test here. + ns := ref.PolicyConfigurationNamespaces() + require.NotNil(t, ns) + assert.True(t, len(ns) >= 2) + assert.Equal(t, tmpDir, ns[0]) + assert.Equal(t, filepath.Dir(tmpDir), ns[1]) + + // Test with a known path which should exist. Test just one non-canonical + // path, the various other cases are tested in explicitfilepath.ResolvePathToFullyExplicit. + // + // It would be nice to test a deeper hierarchy, but it is not obvious what + // deeper path is always available in the various distros, AND is not likely + // to contains a symbolic link. + for _, path := range []string{"/usr/share", "/usr/share/./."} { + _, err := os.Lstat(path) + require.NoError(t, err) + ref, err := NewReference(path, "someimage") + require.NoError(t, err) + ns := ref.PolicyConfigurationNamespaces() + require.NotNil(t, ns) + assert.Equal(t, []string{"/usr/share", "/usr"}, ns) + } + + // "/" as a corner case. + ref, err := NewReference("/", "image3") + require.NoError(t, err) + assert.Equal(t, []string{}, ref.PolicyConfigurationNamespaces()) +} + +func TestReferenceNewImage(t *testing.T) { + ref, _ := refToTempOCI(t) + _, err := ref.NewImage(context.Background(), nil) + assert.Error(t, err) +} + +func TestReferenceNewImageSource(t *testing.T) { + ref, tmpTarFile := refToTempOCIArchive(t) + defer os.RemoveAll(tmpTarFile) + src, err := ref.NewImageSource(context.Background(), nil) + assert.NoError(t, err) + defer src.Close() +} + +func TestReferenceNewImageDestination(t *testing.T) { + ref, _ := refToTempOCI(t) + dest, err := ref.NewImageDestination(context.Background(), nil) + assert.NoError(t, err) + defer dest.Close() +} + +func TestReferenceDeleteImage(t *testing.T) { + ref, _ := refToTempOCI(t) + err := ref.DeleteImage(context.Background(), nil) + assert.Error(t, err) +} diff --git a/vendor/github.com/containers/image/v5/oci/internal/oci_util_test.go b/vendor/github.com/containers/image/v5/oci/internal/oci_util_test.go new file mode 100644 index 00000000000..2438acf1d3a --- /dev/null +++ b/vendor/github.com/containers/image/v5/oci/internal/oci_util_test.go @@ -0,0 +1,63 @@ +package internal + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +type testDataSplitReference struct { + ref string + dir string + image string +} + +type testDataScopeValidation struct { + scope string + errMessage string +} + +func TestSplitReferenceIntoDirAndImageWindows(t *testing.T) { + tests := []testDataSplitReference{ + {`C:\foo\bar:busybox:latest`, `C:\foo\bar`, "busybox:latest"}, + {`C:\foo\bar:busybox`, `C:\foo\bar`, "busybox"}, + {`C:\foo\bar`, `C:\foo\bar`, ""}, + } + for _, test := range tests { + dir, image := splitPathAndImageWindows(test.ref) + assert.Equal(t, test.dir, dir, "Unexpected OCI directory") + assert.Equal(t, test.image, image, "Unexpected image") + } +} + +func TestSplitReferenceIntoDirAndImageNonWindows(t *testing.T) { + tests := []testDataSplitReference{ + {"/foo/bar:busybox:latest", "/foo/bar", "busybox:latest"}, + {"/foo/bar:busybox", "/foo/bar", "busybox"}, + {"/foo/bar", "/foo/bar", ""}, + } + for _, test := range tests { + dir, image := splitPathAndImageNonWindows(test.ref) + assert.Equal(t, test.dir, dir, "Unexpected OCI directory") + assert.Equal(t, test.image, image, "Unexpected image") + } +} + +func TestValidateScopeWindows(t *testing.T) { + tests := []testDataScopeValidation{ + {`C:\foo`, ""}, + {`D:\`, ""}, + {"C:", "Invalid scope 'C:'. Must be an absolute path"}, + {"E", "Invalid scope 'E'. Must be an absolute path"}, + {"", "Invalid scope ''. Must be an absolute path"}, + } + for _, test := range tests { + err := validateScopeWindows(test.scope) + if test.errMessage == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, test.errMessage, fmt.Sprintf("No error for scope '%s'", test.scope)) + } + } +} diff --git a/vendor/github.com/containers/image/v5/oci/layout/fixtures/accepted_certs/cacert.crt b/vendor/github.com/containers/image/v5/oci/layout/fixtures/accepted_certs/cacert.crt new file mode 100644 index 00000000000..ef24c572157 --- /dev/null +++ b/vendor/github.com/containers/image/v5/oci/layout/fixtures/accepted_certs/cacert.crt @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICDTCCAW+gAwIBAgITALhjLVYYA/83IbFqkDcaeQwEQTAKBggqhkjOPQQDBDAS +MRAwDgYDVQQKDAdBY21lIENvMB4XDTIwMDkzMDEyMzkzNloXDTMwMDkyODEyMzkz +NlowEjEQMA4GA1UECgwHQWNtZSBDbzCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAE +AI3pxckijV44L3ffAlLOqB4oA/HpP7S5gTpWrIUU+2SxFJU/bcTKDLPk1cEC87vW ++UCYIXAyYGlyMAGSm0GxAFHnAIIrQzx9m3yiHbUyIPvRMW4BoDKsLaf5+GIZMm9n +Oq2qnjvHr9ag2J3IzxEqQ8KZ95ivmHYrh3VsnfisI7c3opiro2EwXzAOBgNVHQ8B +Af8EBAMCAqQwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB +/wQFMAMBAf8wHQYDVR0RBBYwFIIJbG9jYWxob3N0gQdhQGEuY29tMAoGCCqGSM49 +BAMEA4GLADCBhwJBONccd6jEGC+KO+gvc6xRqCCyn4jT8Dod5u1AxnvAUSQNFxEp +YE8LzdaqyRQbB8cbubtN2FOuBXVu/enOQp/9AJMCQgEgwEDarjW47JDEjplFF8to +LsrEtr2NLL5PLIgBpdVAnpQLumI78x/PvlmABwH6/mw8ZRNFRuUJJ5JajmE5zjbZ +4Q== +-----END CERTIFICATE----- diff --git a/vendor/github.com/containers/image/v5/oci/layout/fixtures/accepted_certs/cert.cert b/vendor/github.com/containers/image/v5/oci/layout/fixtures/accepted_certs/cert.cert new file mode 100644 index 00000000000..ef24c572157 --- /dev/null +++ b/vendor/github.com/containers/image/v5/oci/layout/fixtures/accepted_certs/cert.cert @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICDTCCAW+gAwIBAgITALhjLVYYA/83IbFqkDcaeQwEQTAKBggqhkjOPQQDBDAS +MRAwDgYDVQQKDAdBY21lIENvMB4XDTIwMDkzMDEyMzkzNloXDTMwMDkyODEyMzkz +NlowEjEQMA4GA1UECgwHQWNtZSBDbzCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAE +AI3pxckijV44L3ffAlLOqB4oA/HpP7S5gTpWrIUU+2SxFJU/bcTKDLPk1cEC87vW ++UCYIXAyYGlyMAGSm0GxAFHnAIIrQzx9m3yiHbUyIPvRMW4BoDKsLaf5+GIZMm9n +Oq2qnjvHr9ag2J3IzxEqQ8KZ95ivmHYrh3VsnfisI7c3opiro2EwXzAOBgNVHQ8B +Af8EBAMCAqQwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB +/wQFMAMBAf8wHQYDVR0RBBYwFIIJbG9jYWxob3N0gQdhQGEuY29tMAoGCCqGSM49 +BAMEA4GLADCBhwJBONccd6jEGC+KO+gvc6xRqCCyn4jT8Dod5u1AxnvAUSQNFxEp +YE8LzdaqyRQbB8cbubtN2FOuBXVu/enOQp/9AJMCQgEgwEDarjW47JDEjplFF8to +LsrEtr2NLL5PLIgBpdVAnpQLumI78x/PvlmABwH6/mw8ZRNFRuUJJ5JajmE5zjbZ +4Q== +-----END CERTIFICATE----- diff --git a/vendor/github.com/containers/image/v5/oci/layout/fixtures/accepted_certs/cert.key b/vendor/github.com/containers/image/v5/oci/layout/fixtures/accepted_certs/cert.key new file mode 100644 index 00000000000..b221f74b77e --- /dev/null +++ b/vendor/github.com/containers/image/v5/oci/layout/fixtures/accepted_certs/cert.key @@ -0,0 +1,7 @@ +-----BEGIN EC PRIVATE KEY----- +MIHcAgEBBEIAMDtdVU5PeUWCo1Ndvr+1X+Hry4I7+NdTqxLlU0ZBudm2ov0iJdZj +O2PdSW6pRHJl9gYL+D/QjcEIwQBK4vsHS3SgBwYFK4EEACOhgYkDgYYABACN6cXJ +Io1eOC933wJSzqgeKAPx6T+0uYE6VqyFFPtksRSVP23Eygyz5NXBAvO71vlAmCFw +MmBpcjABkptBsQBR5wCCK0M8fZt8oh21MiD70TFuAaAyrC2n+fhiGTJvZzqtqp47 +x6/WoNidyM8RKkPCmfeYr5h2K4d1bJ34rCO3N6KYqw== +-----END EC PRIVATE KEY----- diff --git a/vendor/github.com/containers/image/v5/oci/layout/fixtures/accepted_certs/gencert.sh b/vendor/github.com/containers/image/v5/oci/layout/fixtures/accepted_certs/gencert.sh new file mode 100755 index 00000000000..b29a5173498 --- /dev/null +++ b/vendor/github.com/containers/image/v5/oci/layout/fixtures/accepted_certs/gencert.sh @@ -0,0 +1,23 @@ +#!/bin/bash -e +config=$(mktemp -t) +if test -z "$config" ; then + echo error creating temporary file for configuration + exit 1 +fi +trap 'rm -f "$config"' EXIT +cat > "$config" << EOF +[req] +prompt=no +distinguished_name=dn +x509_extensions=extensions +[extensions] +keyUsage=critical,digitalSignature,keyEncipherment,keyCertSign +extendedKeyUsage=serverAuth,clientAuth +basicConstraints=critical,CA:TRUE +subjectAltName=DNS:localhost,email:a@a.com +[dn] +O=Acme Co +EOF +serial=$(dd if=/dev/random bs=1 count=16 status=none | hexdump -e '"%x1"') +openssl req -new -set_serial 0x"$serial" -x509 -sha512 -days 3650 -key cert.key -config "$config" -out cert.cert +cp cert.cert cacert.crt diff --git a/vendor/github.com/containers/image/v5/oci/layout/fixtures/manifest/index.json b/vendor/github.com/containers/image/v5/oci/layout/fixtures/manifest/index.json new file mode 100644 index 00000000000..fd6930cf1ce --- /dev/null +++ b/vendor/github.com/containers/image/v5/oci/layout/fixtures/manifest/index.json @@ -0,0 +1 @@ +{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:84afb6189c4d69f2d040c5f1dc4e0a16fed9b539ce9cfb4ac2526ae4e0576cc0","size":496,"annotations":{"org.opencontainers.image.ref.name":"v0.1.1"},"platform":{"architecture":"amd64","os":"linux"}}]} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/oci/layout/fixtures/name_lookups/index.json b/vendor/github.com/containers/image/v5/oci/layout/fixtures/name_lookups/index.json new file mode 100644 index 00000000000..b79d1089806 --- /dev/null +++ b/vendor/github.com/containers/image/v5/oci/layout/fixtures/name_lookups/index.json @@ -0,0 +1,38 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "size": 1, + "annotations": { + "org.opencontainers.image.ref.name": "a" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "size": 2, + "annotations": { + "org.opencontainers.image.ref.name": "b" + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "size": 3, + "annotations": { + "org.opencontainers.image.ref.name": "invalid-mime" + } + }, + { + "mediaType": "x-completely-unknown", + "digest": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "size": 4, + "annotations": { + "org.opencontainers.image.ref.name": "invalid-mime" + } + } + ] +} diff --git a/vendor/github.com/containers/image/v5/oci/layout/fixtures/rejected_certs/cert.cert b/vendor/github.com/containers/image/v5/oci/layout/fixtures/rejected_certs/cert.cert new file mode 100644 index 00000000000..98db6b72b58 --- /dev/null +++ b/vendor/github.com/containers/image/v5/oci/layout/fixtures/rejected_certs/cert.cert @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICDjCCAW+gAwIBAgITALVv4wUc4xK6AbXE9Xge43LAcTAKBggqhkjOPQQDBDAS +MRAwDgYDVQQKDAdBY21lIENvMB4XDTIwMDkzMDEyMzk1MloXDTMwMDkyODEyMzk1 +MlowEjEQMA4GA1UECgwHQWNtZSBDbzCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAE +AUePiRf7kRYLuiitqOB9YkcOz1PHYfZWO/PMuu4mebNzr5NUwoNehqrEHK/FQnod +cRVW2FiCN6agYXfbJZvNX4PnAZOWHfsgpY6kdUoYufq9DuQVoVROXrmXcgbOkZ34 +IObGtPYZhV0uOatEg01vLkkLt+WwKq/oU+VsP+oD1LxrdTmvo2EwXzAOBgNVHQ8B +Af8EBAMCAqQwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB +/wQFMAMBAf8wHQYDVR0RBBYwFIIJbG9jYWxob3N0gQdiQGIuY29tMAoGCCqGSM49 +BAMEA4GMADCBiAJCAYrKggJuI6r+SE/2URLnPc33sIQYahLu1EwIhk32PrSAMo/Y +uJHSoW9NB69/9o7QhjAp7jcdkmi9QMAx0s1ocLd4AkIA6XDWHmRXpQ6g4IEmJFvO +ayk0+DU4esE9LLD5J0te33sck76+q9gnjr5mdw86TD1+VHqDDAbZ0bLiyxOri2WG +jVM= +-----END CERTIFICATE----- diff --git a/vendor/github.com/containers/image/v5/oci/layout/fixtures/rejected_certs/cert.key b/vendor/github.com/containers/image/v5/oci/layout/fixtures/rejected_certs/cert.key new file mode 100644 index 00000000000..e68f355d911 --- /dev/null +++ b/vendor/github.com/containers/image/v5/oci/layout/fixtures/rejected_certs/cert.key @@ -0,0 +1,7 @@ +-----BEGIN EC PRIVATE KEY----- +MIHcAgEBBEIB3BPUEOohwxGCV8V2fwIBdZ3S7yWADrbz5w17YITBt0p6j1C0NKRx +xL9V7Cq+P2OkfQa6rxiD7cM8DjP/6y1//XKgBwYFK4EEACOhgYkDgYYABAFHj4kX ++5EWC7oorajgfWJHDs9Tx2H2VjvzzLruJnmzc6+TVMKDXoaqxByvxUJ6HXEVVthY +gjemoGF32yWbzV+D5wGTlh37IKWOpHVKGLn6vQ7kFaFUTl65l3IGzpGd+CDmxrT2 +GYVdLjmrRINNby5JC7flsCqv6FPlbD/qA9S8a3U5rw== +-----END EC PRIVATE KEY----- diff --git a/vendor/github.com/containers/image/v5/oci/layout/fixtures/rejected_certs/gencert.sh b/vendor/github.com/containers/image/v5/oci/layout/fixtures/rejected_certs/gencert.sh new file mode 100755 index 00000000000..5d81894be1b --- /dev/null +++ b/vendor/github.com/containers/image/v5/oci/layout/fixtures/rejected_certs/gencert.sh @@ -0,0 +1,22 @@ +#!/bin/bash -e +config=$(mktemp -t) +if test -z "$config" ; then + echo error creating temporary file for configuration + exit 1 +fi +trap 'rm -f "$config"' EXIT +cat > "$config" << EOF +[req] +prompt=no +distinguished_name=dn +x509_extensions=extensions +[extensions] +keyUsage=critical,digitalSignature,keyEncipherment,keyCertSign +extendedKeyUsage=serverAuth,clientAuth +basicConstraints=critical,CA:TRUE +subjectAltName=DNS:localhost,email:b@b.com +[dn] +O=Acme Co +EOF +serial=$(dd if=/dev/random bs=1 count=16 status=none | hexdump -e '"%x1"') +openssl req -new -set_serial 0x"$serial" -x509 -sha512 -days 3650 -key cert.key -config "$config" -out cert.cert diff --git a/vendor/github.com/containers/image/v5/oci/layout/fixtures/two_images_manifest/index.json b/vendor/github.com/containers/image/v5/oci/layout/fixtures/two_images_manifest/index.json new file mode 100644 index 00000000000..0dd68afc3e6 --- /dev/null +++ b/vendor/github.com/containers/image/v5/oci/layout/fixtures/two_images_manifest/index.json @@ -0,0 +1,27 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7143, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + "platform": { + "architecture": "ppc64le", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7682, + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + "platform": { + "architecture": "amd64", + "os": "linux", + "features": [ + "sse4" + ] + } + } + ] +} diff --git a/vendor/github.com/containers/image/v5/oci/layout/oci_dest.go b/vendor/github.com/containers/image/v5/oci/layout/oci_dest.go index 0a9e4eab91b..8ff43d4480f 100644 --- a/vendor/github.com/containers/image/v5/oci/layout/oci_dest.go +++ b/vendor/github.com/containers/image/v5/oci/layout/oci_dest.go @@ -172,6 +172,9 @@ func (d *ociImageDestination) PutBlobWithOptions(ctx context.Context, stream io. // If the blob has been successfully reused, returns (true, info, nil). // If the transport can not reuse the requested blob, TryReusingBlob returns (false, {}, nil); it returns a non-nil error only on an unexpected failure. func (d *ociImageDestination) TryReusingBlobWithOptions(ctx context.Context, info types.BlobInfo, options private.TryReusingBlobOptions) (bool, private.ReusedBlob, error) { + if !impl.OriginalBlobMatchesRequiredCompression(options) { + return false, private.ReusedBlob{}, nil + } if info.Digest == "" { return false, private.ReusedBlob{}, errors.New("Can not check for a blob with unknown digest") } diff --git a/vendor/github.com/containers/image/v5/oci/layout/oci_dest_test.go b/vendor/github.com/containers/image/v5/oci/layout/oci_dest_test.go new file mode 100644 index 00000000000..33cf8516bc4 --- /dev/null +++ b/vendor/github.com/containers/image/v5/oci/layout/oci_dest_test.go @@ -0,0 +1,180 @@ +package layout + +import ( + "bytes" + "context" + "errors" + "io/fs" + "os" + "path/filepath" + "testing" + + "github.com/containers/image/v5/internal/private" + "github.com/containers/image/v5/pkg/blobinfocache/memory" + "github.com/containers/image/v5/types" + digest "github.com/opencontainers/go-digest" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var _ private.ImageDestination = (*ociImageDestination)(nil) + +// readerFromFunc allows implementing Reader by any function, e.g. a closure. +type readerFromFunc func([]byte) (int, error) + +func (fn readerFromFunc) Read(p []byte) (int, error) { + return fn(p) +} + +// TestPutBlobDigestFailure simulates behavior on digest verification failure. +func TestPutBlobDigestFailure(t *testing.T) { + const digestErrorString = "Simulated digest error" + const blobDigest = "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" + + ref, _ := refToTempOCI(t) + dirRef, ok := ref.(ociReference) + require.True(t, ok) + blobPath, err := dirRef.blobPath(blobDigest, "") + assert.NoError(t, err) + cache := memory.New() + + firstRead := true + reader := readerFromFunc(func(p []byte) (int, error) { + _, err := os.Lstat(blobPath) + require.Error(t, err) + require.True(t, os.IsNotExist(err)) + if firstRead { + if len(p) > 0 { + firstRead = false + } + for i := 0; i < len(p); i++ { + p[i] = 0xAA + } + return len(p), nil + } + return 0, errors.New(digestErrorString) + }) + + dest, err := ref.NewImageDestination(context.Background(), nil) + require.NoError(t, err) + defer dest.Close() + _, err = dest.PutBlob(context.Background(), reader, types.BlobInfo{Digest: blobDigest, Size: -1}, cache, false) + assert.ErrorContains(t, err, digestErrorString) + err = dest.Commit(context.Background(), nil) // nil unparsedToplevel is invalid, we don’t currently use the value + assert.NoError(t, err) + + _, err = os.Lstat(blobPath) + require.Error(t, err) + require.True(t, os.IsNotExist(err)) +} + +// TestPutManifestAppendsToExistingManifest tests that new manifests are getting added to existing index. +func TestPutManifestAppendsToExistingManifest(t *testing.T) { + ref, tmpDir := refToTempOCI(t) + + ociRef, ok := ref.(ociReference) + require.True(t, ok) + + // initially we have one manifest + index, err := ociRef.getIndex() + assert.NoError(t, err) + assert.Equal(t, 1, len(index.Manifests), "Unexpected number of manifests") + + // create a new test reference + ociRef2, err := NewReference(tmpDir, "new-image") + assert.NoError(t, err) + + putTestManifest(t, ociRef2.(ociReference), tmpDir) + + index, err = ociRef.getIndex() + assert.NoError(t, err) + assert.Equal(t, 2, len(index.Manifests), "Unexpected number of manifests") +} + +// TestPutManifestTwice tests that existing manifest gets updated and not appended. +func TestPutManifestTwice(t *testing.T) { + ref, tmpDir := refToTempOCI(t) + + ociRef, ok := ref.(ociReference) + require.True(t, ok) + + putTestConfig(t, ociRef, tmpDir) + putTestManifest(t, ociRef, tmpDir) + putTestManifest(t, ociRef, tmpDir) + + index, err := ociRef.getIndex() + assert.NoError(t, err) + assert.Len(t, index.Manifests, 2, "Unexpected number of manifests") +} + +func TestPutTwoDifferentTags(t *testing.T) { + ref, tmpDir := refToTempOCI(t) + + ociRef, ok := ref.(ociReference) + require.True(t, ok) + + putTestConfig(t, ociRef, tmpDir) + putTestManifest(t, ociRef, tmpDir) + + // add the same manifest with a different tag; it shouldn't get overwritten + ref, err := NewReference(tmpDir, "zomg") + assert.NoError(t, err) + ociRef, ok = ref.(ociReference) + require.True(t, ok) + putTestManifest(t, ociRef, tmpDir) + + index, err := ociRef.getIndex() + assert.NoError(t, err) + assert.Len(t, index.Manifests, 3, "Unexpected number of manifests") + assert.Equal(t, "imageValue", index.Manifests[1].Annotations[imgspecv1.AnnotationRefName]) + assert.Equal(t, "zomg", index.Manifests[2].Annotations[imgspecv1.AnnotationRefName]) +} + +func putTestConfig(t *testing.T, ociRef ociReference, tmpDir string) { + data, err := os.ReadFile("../../internal/image/fixtures/oci1-config.json") + assert.NoError(t, err) + imageDest, err := newImageDestination(nil, ociRef) + assert.NoError(t, err) + + cache := memory.New() + + _, err = imageDest.PutBlob(context.Background(), bytes.NewReader(data), types.BlobInfo{Size: int64(len(data)), Digest: digest.FromBytes(data)}, cache, true) + assert.NoError(t, err) + + err = imageDest.Commit(context.Background(), nil) // nil unparsedToplevel is invalid, we don’t currently use the value + assert.NoError(t, err) + + paths := []string{} + err = filepath.WalkDir(tmpDir, func(path string, _ fs.DirEntry, err error) error { + paths = append(paths, path) + return nil + }) + assert.NoError(t, err) + + digest := digest.FromBytes(data).Encoded() + assert.Contains(t, paths, filepath.Join(tmpDir, "blobs", "sha256", digest), "The OCI directory does not contain the new config data") +} + +func putTestManifest(t *testing.T, ociRef ociReference, tmpDir string) { + data, err := os.ReadFile("../../internal/image/fixtures/oci1.json") + assert.NoError(t, err) + imageDest, err := newImageDestination(nil, ociRef) + assert.NoError(t, err) + + err = imageDest.PutManifest(context.Background(), data, nil) + assert.NoError(t, err) + + err = imageDest.Commit(context.Background(), nil) // nil unparsedToplevel is invalid, we don’t currently use the value + assert.NoError(t, err) + + paths := []string{} + err = filepath.WalkDir(tmpDir, func(path string, _ fs.DirEntry, err error) error { + paths = append(paths, path) + return nil + }) + assert.NoError(t, err) + + digest := digest.FromBytes(data).Encoded() + assert.Contains(t, paths, filepath.Join(tmpDir, "blobs", "sha256", digest), "The OCI directory does not contain the new manifest data") +} diff --git a/vendor/github.com/containers/image/v5/oci/layout/oci_src_test.go b/vendor/github.com/containers/image/v5/oci/layout/oci_src_test.go new file mode 100644 index 00000000000..3fe49a2072e --- /dev/null +++ b/vendor/github.com/containers/image/v5/oci/layout/oci_src_test.go @@ -0,0 +1,143 @@ +package layout + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + + "github.com/containers/image/v5/internal/private" + "github.com/containers/image/v5/pkg/blobinfocache/memory" + "github.com/containers/image/v5/types" + digest "github.com/opencontainers/go-digest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var _ private.ImageSource = (*ociImageSource)(nil) + +const RemoteLayerContent = "This is the remote layer content" + +var httpServerAddr string + +func TestMain(m *testing.M) { + httpServer, err := startRemoteLayerServer() + if err != nil { + fmt.Fprintf(os.Stderr, "Error starting test TLS server: %v", err.Error()) + os.Exit(1) + } + + httpServerAddr = strings.Replace(httpServer.URL, "127.0.0.1", "localhost", 1) + code := m.Run() + httpServer.Close() + os.Exit(code) +} + +func TestGetBlobForRemoteLayers(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, "Hello world") + })) + defer ts.Close() + cache := memory.New() + + imageSource := createImageSource(t, &types.SystemContext{}) + defer imageSource.Close() + layerInfo := types.BlobInfo{ + Digest: digest.FromBytes([]byte("Hello world")), + Size: -1, + URLs: []string{ + "brokenurl", + ts.URL, + }, + } + + reader, _, err := imageSource.GetBlob(context.Background(), layerInfo, cache) + require.NoError(t, err) + defer reader.Close() + + data, err := io.ReadAll(reader) + require.NoError(t, err) + assert.Contains(t, string(data), "Hello world") +} + +func TestGetBlobForRemoteLayersWithTLS(t *testing.T) { + imageSource := createImageSource(t, &types.SystemContext{ + OCICertPath: "fixtures/accepted_certs", + }) + defer imageSource.Close() + cache := memory.New() + + layer, size, err := imageSource.GetBlob(context.Background(), types.BlobInfo{ + URLs: []string{httpServerAddr}, + }, cache) + require.NoError(t, err) + + layerContent, _ := io.ReadAll(layer) + assert.Equal(t, RemoteLayerContent, string(layerContent)) + assert.Equal(t, int64(len(RemoteLayerContent)), size) +} + +func TestGetBlobForRemoteLayersOnTLSFailure(t *testing.T) { + imageSource := createImageSource(t, &types.SystemContext{ + OCICertPath: "fixtures/rejected_certs", + }) + defer imageSource.Close() + cache := memory.New() + layer, size, err := imageSource.GetBlob(context.Background(), types.BlobInfo{ + URLs: []string{httpServerAddr}, + }, cache) + + require.Error(t, err) + assert.Nil(t, layer) + assert.Equal(t, int64(0), size) +} + +func remoteLayerContent(w http.ResponseWriter, req *http.Request) { + fmt.Fprint(w, RemoteLayerContent) +} + +func startRemoteLayerServer() (*httptest.Server, error) { + certBytes, err := os.ReadFile("fixtures/accepted_certs/cert.cert") + if err != nil { + return nil, err + } + + clientCertPool := x509.NewCertPool() + if !clientCertPool.AppendCertsFromPEM(certBytes) { + return nil, fmt.Errorf("Could not append certificate") + } + + cert, err := tls.LoadX509KeyPair("fixtures/accepted_certs/cert.cert", "fixtures/accepted_certs/cert.key") + if err != nil { + return nil, err + } + + tlsConfig := &tls.Config{ + // Reject any TLS certificate that cannot be validated + ClientAuth: tls.RequireAndVerifyClientCert, + // Ensure that we only use our "CA" to validate certificates + ClientCAs: clientCertPool, + Certificates: []tls.Certificate{cert}, + } + + httpServer := httptest.NewUnstartedServer(http.HandlerFunc(remoteLayerContent)) + httpServer.TLS = tlsConfig + + httpServer.StartTLS() + + return httpServer, nil +} + +func createImageSource(t *testing.T, sys *types.SystemContext) types.ImageSource { + imageRef, err := NewReference("fixtures/manifest", "") + require.NoError(t, err) + imageSource, err := imageRef.NewImageSource(context.Background(), sys) + require.NoError(t, err) + return imageSource +} diff --git a/vendor/github.com/containers/image/v5/oci/layout/oci_transport_test.go b/vendor/github.com/containers/image/v5/oci/layout/oci_transport_test.go new file mode 100644 index 00000000000..fd348deb691 --- /dev/null +++ b/vendor/github.com/containers/image/v5/oci/layout/oci_transport_test.go @@ -0,0 +1,372 @@ +package layout + +import ( + "context" + "os" + "path/filepath" + "testing" + + _ "github.com/containers/image/v5/internal/testing/explicitfilepath-tmpdir" + "github.com/containers/image/v5/types" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetManifestDescriptor(t *testing.T) { + emptyDir := t.TempDir() + + for _, c := range []struct { + dir, image string + expected *imgspecv1.Descriptor // nil if a failure ie expected. errorIs / errorAs allows more specific checks. + errorIs error + errorAs any + }{ + { // Index is missing + dir: emptyDir, + image: "", + expected: nil, + }, + { // A valid reference to the only manifest + dir: "fixtures/manifest", + image: "", + expected: &imgspecv1.Descriptor{ + MediaType: "application/vnd.oci.image.manifest.v1+json", + Digest: "sha256:84afb6189c4d69f2d040c5f1dc4e0a16fed9b539ce9cfb4ac2526ae4e0576cc0", + Size: 496, + Annotations: map[string]string{"org.opencontainers.image.ref.name": "v0.1.1"}, + Platform: &imgspecv1.Platform{ + Architecture: "amd64", + OS: "linux", + }, + }, + }, + { // An ambiguous reference to a multi-manifest directory + dir: "fixtures/two_images_manifest", + image: "", + expected: nil, + errorIs: ErrMoreThanOneImage, + }, + { // A valid reference in a multi-manifest directory + dir: "fixtures/name_lookups", + image: "a", + expected: &imgspecv1.Descriptor{ + MediaType: "application/vnd.oci.image.manifest.v1+json", + Digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + Size: 1, + Annotations: map[string]string{"org.opencontainers.image.ref.name": "a"}, + }, + }, + { // A valid reference in a multi-manifest directory + dir: "fixtures/name_lookups", + image: "b", + expected: &imgspecv1.Descriptor{ + MediaType: "application/vnd.oci.image.manifest.v1+json", + Digest: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + Size: 2, + Annotations: map[string]string{"org.opencontainers.image.ref.name": "b"}, + }, + }, + { // No entry found + dir: "fixtures/name_lookups", + image: "this-does-not-exist", + expected: nil, + errorAs: &ImageNotFoundError{}, + }, + { // Entries with invalid MIME types found + dir: "fixtures/name_lookups", + image: "invalid-mime", + expected: nil, + }, + } { + ref, err := NewReference(c.dir, c.image) + require.NoError(t, err) + + res, err := ref.(ociReference).getManifestDescriptor() + if c.expected != nil { + require.NoError(t, err) + assert.Equal(t, *c.expected, res) + } else { + require.Error(t, err) + if c.errorIs != nil { + assert.ErrorIs(t, err, c.errorIs) + } + if c.errorAs != nil { + assert.ErrorAs(t, err, &c.errorAs) + } + } + } +} + +func TestTransportName(t *testing.T) { + assert.Equal(t, "oci", Transport.Name()) +} + +func TestTransportParseReference(t *testing.T) { + testParseReference(t, Transport.ParseReference) +} + +func TestTransportValidatePolicyConfigurationScope(t *testing.T) { + for _, scope := range []string{ + "/etc", + "/this/does/not/exist", + } { + err := Transport.ValidatePolicyConfigurationScope(scope) + assert.NoError(t, err, scope) + } + + for _, scope := range []string{ + "relative/path", + "/", + "/double//slashes", + "/has/./dot", + "/has/dot/../dot", + "/trailing/slash/", + } { + err := Transport.ValidatePolicyConfigurationScope(scope) + assert.Error(t, err, scope) + } +} + +func TestParseReference(t *testing.T) { + testParseReference(t, ParseReference) +} + +// testParseReference is a test shared for Transport.ParseReference and ParseReference. +func testParseReference(t *testing.T, fn func(string) (types.ImageReference, error)) { + tmpDir := t.TempDir() + + for _, path := range []string{ + "/", + "/etc", + tmpDir, + "relativepath", + tmpDir + "/thisdoesnotexist", + } { + for _, image := range []struct{ suffix, image string }{ + {":notlatest:image", "notlatest:image"}, + {":latestimage", "latestimage"}, + {":", ""}, + {"", ""}, + } { + input := path + image.suffix + ref, err := fn(input) + require.NoError(t, err, input) + ociRef, ok := ref.(ociReference) + require.True(t, ok) + assert.Equal(t, path, ociRef.dir, input) + assert.Equal(t, image.image, ociRef.image, input) + } + } + + _, err := fn(tmpDir + ":invalid'image!value@") + assert.Error(t, err) +} + +func TestNewReference(t *testing.T) { + const ( + imageValue = "imageValue" + noImageValue = "" + ) + + tmpDir := t.TempDir() + + ref, err := NewReference(tmpDir, imageValue) + require.NoError(t, err) + ociRef, ok := ref.(ociReference) + require.True(t, ok) + assert.Equal(t, tmpDir, ociRef.dir) + assert.Equal(t, imageValue, ociRef.image) + + ref, err = NewReference(tmpDir, noImageValue) + require.NoError(t, err) + ociRef, ok = ref.(ociReference) + require.True(t, ok) + assert.Equal(t, tmpDir, ociRef.dir) + assert.Equal(t, noImageValue, ociRef.image) + + _, err = NewReference(tmpDir+"/thisparentdoesnotexist/something", imageValue) + assert.Error(t, err) + + _, err = NewReference(tmpDir, "invalid'image!value@") + assert.Error(t, err) + + _, err = NewReference(tmpDir+"/has:colon", imageValue) + assert.Error(t, err) +} + +// refToTempOCI creates a temporary directory and returns an reference to it. +func refToTempOCI(t *testing.T) (types.ImageReference, string) { + tmpDir := t.TempDir() + m := `{ + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 7143, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + "platform": { + "architecture": "ppc64le", + "os": "linux" + }, + "annotations": { + "org.opencontainers.image.ref.name": "imageValue" + } + } + ] + } +` + err := os.WriteFile(filepath.Join(tmpDir, "index.json"), []byte(m), 0644) + require.NoError(t, err) + ref, err := NewReference(tmpDir, "imageValue") + require.NoError(t, err) + return ref, tmpDir +} + +func TestReferenceTransport(t *testing.T) { + ref, _ := refToTempOCI(t) + assert.Equal(t, Transport, ref.Transport()) +} + +func TestReferenceStringWithinTransport(t *testing.T) { + tmpDir := t.TempDir() + + for _, c := range []struct{ input, result string }{ + {"/dir1:notlatest:notlatest", "/dir1:notlatest:notlatest"}, // Explicit image + {"/dir3:", "/dir3:"}, // No image + } { + ref, err := ParseReference(tmpDir + c.input) + require.NoError(t, err, c.input) + stringRef := ref.StringWithinTransport() + assert.Equal(t, tmpDir+c.result, stringRef, c.input) + // Do one more round to verify that the output can be parsed, to an equal value. + ref2, err := Transport.ParseReference(stringRef) + require.NoError(t, err, c.input) + stringRef2 := ref2.StringWithinTransport() + assert.Equal(t, stringRef, stringRef2, c.input) + } +} + +func TestReferenceDockerReference(t *testing.T) { + ref, _ := refToTempOCI(t) + assert.Nil(t, ref.DockerReference()) +} + +func TestReferencePolicyConfigurationIdentity(t *testing.T) { + ref, tmpDir := refToTempOCI(t) + + assert.Equal(t, tmpDir, ref.PolicyConfigurationIdentity()) + // A non-canonical path. Test just one, the various other cases are + // tested in explicitfilepath.ResolvePathToFullyExplicit. + ref, err := NewReference(tmpDir+"/.", "image2") + require.NoError(t, err) + assert.Equal(t, tmpDir, ref.PolicyConfigurationIdentity()) + + // "/" as a corner case. + ref, err = NewReference("/", "image3") + require.NoError(t, err) + assert.Equal(t, "/", ref.PolicyConfigurationIdentity()) +} + +func TestReferencePolicyConfigurationNamespaces(t *testing.T) { + ref, tmpDir := refToTempOCI(t) + // We don't really know enough to make a full equality test here. + ns := ref.PolicyConfigurationNamespaces() + require.NotNil(t, ns) + assert.True(t, len(ns) >= 2) + assert.Equal(t, tmpDir, ns[0]) + assert.Equal(t, filepath.Dir(tmpDir), ns[1]) + + // Test with a known path which should exist. Test just one non-canonical + // path, the various other cases are tested in explicitfilepath.ResolvePathToFullyExplicit. + // + // It would be nice to test a deeper hierarchy, but it is not obvious what + // deeper path is always available in the various distros, AND is not likely + // to contains a symbolic link. + for _, path := range []string{"/usr/share", "/usr/share/./."} { + _, err := os.Lstat(path) + require.NoError(t, err) + ref, err := NewReference(path, "someimage") + require.NoError(t, err) + ns := ref.PolicyConfigurationNamespaces() + require.NotNil(t, ns) + assert.Equal(t, []string{"/usr/share", "/usr"}, ns) + } + + // "/" as a corner case. + ref, err := NewReference("/", "image3") + require.NoError(t, err) + assert.Equal(t, []string{}, ref.PolicyConfigurationNamespaces()) +} + +func TestReferenceNewImage(t *testing.T) { + ref, _ := refToTempOCI(t) + _, err := ref.NewImage(context.Background(), nil) + assert.Error(t, err) +} + +func TestReferenceNewImageSource(t *testing.T) { + ref, _ := refToTempOCI(t) + src, err := ref.NewImageSource(context.Background(), nil) + assert.NoError(t, err) + defer src.Close() +} + +func TestReferenceNewImageDestination(t *testing.T) { + ref, _ := refToTempOCI(t) + dest, err := ref.NewImageDestination(context.Background(), nil) + assert.NoError(t, err) + defer dest.Close() +} + +func TestReferenceDeleteImage(t *testing.T) { + ref, _ := refToTempOCI(t) + err := ref.DeleteImage(context.Background(), nil) + assert.Error(t, err) +} + +func TestReferenceOCILayoutPath(t *testing.T) { + ref, tmpDir := refToTempOCI(t) + ociRef, ok := ref.(ociReference) + require.True(t, ok) + assert.Equal(t, tmpDir+"/oci-layout", ociRef.ociLayoutPath()) +} + +func TestReferenceIndexPath(t *testing.T) { + ref, tmpDir := refToTempOCI(t) + ociRef, ok := ref.(ociReference) + require.True(t, ok) + assert.Equal(t, tmpDir+"/index.json", ociRef.indexPath()) +} + +func TestReferenceBlobPath(t *testing.T) { + const hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + ref, tmpDir := refToTempOCI(t) + ociRef, ok := ref.(ociReference) + require.True(t, ok) + bp, err := ociRef.blobPath("sha256:"+hex, "") + assert.NoError(t, err) + assert.Equal(t, tmpDir+"/blobs/sha256/"+hex, bp) +} + +func TestReferenceSharedBlobPathShared(t *testing.T) { + const hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + ref, _ := refToTempOCI(t) + ociRef, ok := ref.(ociReference) + require.True(t, ok) + bp, err := ociRef.blobPath("sha256:"+hex, "/external/path") + assert.NoError(t, err) + assert.Equal(t, "/external/path/sha256/"+hex, bp) +} + +func TestReferenceBlobPathInvalid(t *testing.T) { + const hex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + ref, _ := refToTempOCI(t) + ociRef, ok := ref.(ociReference) + require.True(t, ok) + _, err := ociRef.blobPath(hex, "") + assert.ErrorContains(t, err, "unexpected digest reference "+hex) +} diff --git a/vendor/github.com/containers/image/v5/oci/oci.go b/vendor/github.com/containers/image/v5/oci/oci.go new file mode 100644 index 00000000000..03607d3288b --- /dev/null +++ b/vendor/github.com/containers/image/v5/oci/oci.go @@ -0,0 +1 @@ +package oci diff --git a/vendor/github.com/containers/image/v5/openshift/openshift-copies_test.go b/vendor/github.com/containers/image/v5/openshift/openshift-copies_test.go new file mode 100644 index 00000000000..1fdc37d81aa --- /dev/null +++ b/vendor/github.com/containers/image/v5/openshift/openshift-copies_test.go @@ -0,0 +1,115 @@ +package openshift + +import ( + "encoding" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +const fixtureKubeConfigPath = "testdata/admin.kubeconfig" + +var ( + _ yaml.Unmarshaler = (*clustersMap)(nil) + _ yaml.Unmarshaler = (*authInfosMap)(nil) + _ yaml.Unmarshaler = (*contextsMap)(nil) + _ encoding.TextUnmarshaler = (*yamlBinaryAsBase64String)(nil) +) + +// These are only smoke tests based on the skopeo integration test cluster. Error handling, non-trivial configuration merging, +// and any other situations are not currently covered. + +// Set up KUBECONFIG to point at the fixture. +// Callers MUST NOT call testing.T.Parallel(). +func setupKubeConfigForSerialTest(t *testing.T) { + t.Setenv("KUBECONFIG", fixtureKubeConfigPath) +} + +func TestClientConfigLoadingRules(t *testing.T) { + setupKubeConfigForSerialTest(t) + + rules := newOpenShiftClientConfigLoadingRules() + res, err := rules.Load() + require.NoError(t, err) + expected := clientcmdConfig{ + Clusters: clustersMap{ + "172-17-0-2:8443": &clientcmdCluster{ + LocationOfOrigin: fixtureKubeConfigPath, + Server: "https://172.17.0.2:8443", + CertificateAuthorityData: []byte("Cluster CA"), + }, + }, + AuthInfos: authInfosMap{ + "system:admin/172-17-0-2:8443": &clientcmdAuthInfo{ + LocationOfOrigin: fixtureKubeConfigPath, + ClientCertificateData: []byte("Client cert"), + ClientKeyData: []byte("Client key"), + }, + }, + Contexts: contextsMap{ + "default/172-17-0-2:8443/system:admin": &clientcmdContext{ + LocationOfOrigin: fixtureKubeConfigPath, + Cluster: "172-17-0-2:8443", + AuthInfo: "system:admin/172-17-0-2:8443", + Namespace: "default", + }, + }, + CurrentContext: "default/172-17-0-2:8443/system:admin", + } + assert.Equal(t, &expected, res) +} + +func TestDirectClientConfig(t *testing.T) { + setupKubeConfigForSerialTest(t) + + rules := newOpenShiftClientConfigLoadingRules() + config, err := rules.Load() + require.NoError(t, err) + + direct := newNonInteractiveClientConfig(*config) + res, err := direct.ClientConfig() + require.NoError(t, err) + assert.Equal(t, &restConfig{ + Host: "https://172.17.0.2:8443", + TLSClientConfig: restTLSClientConfig{ + CertData: []byte("Client cert"), + KeyData: []byte("Client key"), + CAData: []byte("Cluster CA"), + }, + }, res) +} + +func TestDeferredLoadingClientConfig(t *testing.T) { + setupKubeConfigForSerialTest(t) + + rules := newOpenShiftClientConfigLoadingRules() + deferred := newNonInteractiveDeferredLoadingClientConfig(rules) + res, err := deferred.ClientConfig() + require.NoError(t, err) + assert.Equal(t, &restConfig{ + Host: "https://172.17.0.2:8443", + TLSClientConfig: restTLSClientConfig{ + CertData: []byte("Client cert"), + KeyData: []byte("Client key"), + CAData: []byte("Cluster CA"), + }, + }, res) +} + +func TestDefaultClientConfig(t *testing.T) { + setupKubeConfigForSerialTest(t) + + config := defaultClientConfig() + res, err := config.ClientConfig() + require.NoError(t, err) + assert.Equal(t, &restConfig{ + Host: "https://172.17.0.2:8443", + TLSClientConfig: restTLSClientConfig{ + CertData: []byte("Client cert"), + KeyData: []byte("Client key"), + CAData: []byte("Cluster CA"), + }, + }, res) +} diff --git a/vendor/github.com/containers/image/v5/openshift/openshift.go b/vendor/github.com/containers/image/v5/openshift/openshift.go index f3d5662e669..2c69afbe94f 100644 --- a/vendor/github.com/containers/image/v5/openshift/openshift.go +++ b/vendor/github.com/containers/image/v5/openshift/openshift.go @@ -65,6 +65,10 @@ func newOpenshiftClient(ref openshiftReference) (*openshiftClient, error) { }, nil } +func (c *openshiftClient) close() { + c.httpClient.CloseIdleConnections() +} + // doRequest performs a correctly authenticated request to a specified path, and returns response body or an error object. func (c *openshiftClient) doRequest(ctx context.Context, method, path string, requestBody []byte) ([]byte, error) { requestURL := *c.baseURL diff --git a/vendor/github.com/containers/image/v5/openshift/openshift_dest.go b/vendor/github.com/containers/image/v5/openshift/openshift_dest.go index 7b1b5dfcded..50a5339e1b8 100644 --- a/vendor/github.com/containers/image/v5/openshift/openshift_dest.go +++ b/vendor/github.com/containers/image/v5/openshift/openshift_dest.go @@ -71,7 +71,9 @@ func (d *openshiftImageDestination) Reference() types.ImageReference { // Close removes resources associated with an initialized ImageDestination, if any. func (d *openshiftImageDestination) Close() error { - return d.docker.Close() + err := d.docker.Close() + d.client.close() + return err } func (d *openshiftImageDestination) SupportedManifestMIMETypes() []string { diff --git a/vendor/github.com/containers/image/v5/openshift/openshift_dest_test.go b/vendor/github.com/containers/image/v5/openshift/openshift_dest_test.go new file mode 100644 index 00000000000..795aa711186 --- /dev/null +++ b/vendor/github.com/containers/image/v5/openshift/openshift_dest_test.go @@ -0,0 +1,5 @@ +package openshift + +import "github.com/containers/image/v5/internal/private" + +var _ private.ImageDestination = (*openshiftImageDestination)(nil) diff --git a/vendor/github.com/containers/image/v5/openshift/openshift_src.go b/vendor/github.com/containers/image/v5/openshift/openshift_src.go index 93ba8d10e30..0ac0127ee77 100644 --- a/vendor/github.com/containers/image/v5/openshift/openshift_src.go +++ b/vendor/github.com/containers/image/v5/openshift/openshift_src.go @@ -60,14 +60,15 @@ func (s *openshiftImageSource) Reference() types.ImageReference { // Close removes resources associated with an initialized ImageSource, if any. func (s *openshiftImageSource) Close() error { + var err error if s.docker != nil { - err := s.docker.Close() + err = s.docker.Close() s.docker = nil - - return err } - return nil + s.client.close() + + return err } // GetManifest returns the image's manifest along with its MIME type (which may be empty when it can't be determined but the manifest is available). diff --git a/vendor/github.com/containers/image/v5/openshift/openshift_src_test.go b/vendor/github.com/containers/image/v5/openshift/openshift_src_test.go new file mode 100644 index 00000000000..0f88892890d --- /dev/null +++ b/vendor/github.com/containers/image/v5/openshift/openshift_src_test.go @@ -0,0 +1,5 @@ +package openshift + +import "github.com/containers/image/v5/internal/private" + +var _ private.ImageSource = (*openshiftImageSource)(nil) diff --git a/vendor/github.com/containers/image/v5/openshift/openshift_transport_test.go b/vendor/github.com/containers/image/v5/openshift/openshift_transport_test.go new file mode 100644 index 00000000000..af8dfd869d9 --- /dev/null +++ b/vendor/github.com/containers/image/v5/openshift/openshift_transport_test.go @@ -0,0 +1,130 @@ +package openshift + +import ( + "context" + "testing" + + "github.com/containers/image/v5/docker/reference" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + sha256digestHex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + sha256digest = "@sha256:" + sha256digestHex +) + +func TestTransportName(t *testing.T) { + assert.Equal(t, "atomic", Transport.Name()) +} + +func TestTransportValidatePolicyConfigurationScope(t *testing.T) { + for _, scope := range []string{ + "registry.example.com/ns/stream" + sha256digest, + "registry.example.com/ns/stream:notlatest", + "registry.example.com/ns/stream", + "registry.example.com/ns", + "registry.example.com", + "*.example.com", + "*.com", + } { + err := Transport.ValidatePolicyConfigurationScope(scope) + assert.NoError(t, err, scope) + } + + for _, scope := range []string{ + "registry.example.com/too/deep/hierarchy", + "registry.example.com/ns/stream:tag1:tag2", + } { + err := Transport.ValidatePolicyConfigurationScope(scope) + assert.Error(t, err, scope) + } +} + +func TestNewReference(t *testing.T) { + // too many ns + r, err := reference.ParseNormalizedNamed("registry.example.com/ns1/ns2/ns3/stream:tag") + require.NoError(t, err) + tagged, ok := r.(reference.NamedTagged) + require.True(t, ok) + _, err = NewReference(tagged) + assert.Error(t, err) + + r, err = reference.ParseNormalizedNamed("registry.example.com/ns/stream:tag") + require.NoError(t, err) + tagged, ok = r.(reference.NamedTagged) + require.True(t, ok) + _, err = NewReference(tagged) + assert.NoError(t, err) +} + +func TestParseReference(t *testing.T) { + // Success + ref, err := ParseReference("registry.example.com:8443/ns/stream:notlatest") + require.NoError(t, err) + osRef, ok := ref.(openshiftReference) + require.True(t, ok) + assert.Equal(t, "ns", osRef.namespace) + assert.Equal(t, "stream", osRef.stream) + assert.Equal(t, "notlatest", osRef.dockerReference.Tag()) + assert.Equal(t, "registry.example.com:8443", reference.Domain(osRef.dockerReference)) + + // Components creating an invalid Docker Reference name + _, err = ParseReference("registry.example.com/ns/UPPERCASEISINVALID:notlatest") + assert.Error(t, err) + + _, err = ParseReference("registry.example.com/ns/stream:invalid!tag@value=") + assert.Error(t, err) +} + +func TestReferenceDockerReference(t *testing.T) { + ref, err := ParseReference("registry.example.com:8443/ns/stream:notlatest") + require.NoError(t, err) + dockerRef := ref.DockerReference() + require.NotNil(t, dockerRef) + assert.Equal(t, "registry.example.com:8443/ns/stream:notlatest", dockerRef.String()) +} + +func TestReferenceTransport(t *testing.T) { + ref, err := ParseReference("registry.example.com:8443/ns/stream:notlatest") + require.NoError(t, err) + assert.Equal(t, Transport, ref.Transport()) +} + +func TestReferenceStringWithinTransport(t *testing.T) { + ref, err := ParseReference("registry.example.com:8443/ns/stream:notlatest") + require.NoError(t, err) + assert.Equal(t, "registry.example.com:8443/ns/stream:notlatest", ref.StringWithinTransport()) + // We should do one more round to verify that the output can be parsed, to an equal value, + // but that is untested because it depends on per-user configuration. +} + +func TestReferencePolicyConfigurationIdentity(t *testing.T) { + // Just a smoke test, the substance is tested in policyconfiguration.TestDockerReference. + ref, err := ParseReference("registry.example.com:8443/ns/stream:notlatest") + require.NoError(t, err) + assert.Equal(t, "registry.example.com:8443/ns/stream:notlatest", ref.PolicyConfigurationIdentity()) +} + +func TestReferencePolicyConfigurationNamespaces(t *testing.T) { + // Just a smoke test, the substance is tested in policyconfiguration.TestDockerReference. + ref, err := ParseReference("registry.example.com:8443/ns/stream:notlatest") + require.NoError(t, err) + assert.Equal(t, []string{ + "registry.example.com:8443/ns/stream", + "registry.example.com:8443/ns", + "registry.example.com:8443", + "*.example.com", + "*.com", + }, ref.PolicyConfigurationNamespaces()) +} + +// openshiftReference.NewImage, openshiftReference.NewImageSource, openshiftReference.NewImageDestination untested because they depend +// on per-user configuration when initializing httpClient. + +func TestReferenceDeleteImage(t *testing.T) { + ref, err := ParseReference("registry.example.com:8443/ns/stream:notlatest") + require.NoError(t, err) + err = ref.DeleteImage(context.Background(), nil) + assert.Error(t, err) +} diff --git a/vendor/github.com/containers/image/v5/openshift/testdata/admin.kubeconfig b/vendor/github.com/containers/image/v5/openshift/testdata/admin.kubeconfig new file mode 100644 index 00000000000..8f234775cd4 --- /dev/null +++ b/vendor/github.com/containers/image/v5/openshift/testdata/admin.kubeconfig @@ -0,0 +1,20 @@ +apiVersion: v1 +clusters: +- cluster: + certificate-authority-data: Q2x1c3RlciBDQQ== + server: https://172.17.0.2:8443 + name: 172-17-0-2:8443 +contexts: +- context: + cluster: 172-17-0-2:8443 + namespace: default + user: system:admin/172-17-0-2:8443 + name: default/172-17-0-2:8443/system:admin +current-context: default/172-17-0-2:8443/system:admin +kind: Config +preferences: {} +users: +- name: system:admin/172-17-0-2:8443 + user: + client-certificate-data: Q2xpZW50IGNlcnQ= + client-key-data: Q2xpZW50IGtleQ== diff --git a/vendor/github.com/containers/image/v5/ostree/ostree_dest.go b/vendor/github.com/containers/image/v5/ostree/ostree_dest.go index 48f3ee5a728..d00a0cdf861 100644 --- a/vendor/github.com/containers/image/v5/ostree/ostree_dest.go +++ b/vendor/github.com/containers/image/v5/ostree/ostree_dest.go @@ -335,6 +335,9 @@ func (d *ostreeImageDestination) importConfig(repo *otbuiltin.Repo, blob *blobTo // reflected in the manifest that will be written. // If the transport can not reuse the requested blob, TryReusingBlob returns (false, {}, nil); it returns a non-nil error only on an unexpected failure. func (d *ostreeImageDestination) TryReusingBlobWithOptions(ctx context.Context, info types.BlobInfo, options private.TryReusingBlobOptions) (bool, private.ReusedBlob, error) { + if !impl.OriginalBlobMatchesRequiredCompression(options) { + return false, private.ReusedBlob{}, nil + } if d.repo == nil { repo, err := openRepo(d.ref.repo) if err != nil { diff --git a/vendor/github.com/containers/image/v5/ostree/ostree_dest_test.go b/vendor/github.com/containers/image/v5/ostree/ostree_dest_test.go new file mode 100644 index 00000000000..bca3d7ce0aa --- /dev/null +++ b/vendor/github.com/containers/image/v5/ostree/ostree_dest_test.go @@ -0,0 +1,6 @@ +//go:build containers_image_ostree +// +build containers_image_ostree + +package ostree + +var _ private.ImageDestination = (*ostreeImageDestination)(nil) diff --git a/vendor/github.com/containers/image/v5/ostree/ostree_src_test.go b/vendor/github.com/containers/image/v5/ostree/ostree_src_test.go new file mode 100644 index 00000000000..60c35e69a28 --- /dev/null +++ b/vendor/github.com/containers/image/v5/ostree/ostree_src_test.go @@ -0,0 +1,8 @@ +//go:build containers_image_ostree +// +build containers_image_ostree + +package ostree + +import "github.com/containers/image/v5/internal/private" + +var _ private.ImageSource = (*ostreeImageSource)(nil) diff --git a/vendor/github.com/containers/image/v5/ostree/ostree_transport_test.go b/vendor/github.com/containers/image/v5/ostree/ostree_transport_test.go new file mode 100644 index 00000000000..bb48d9b9ef3 --- /dev/null +++ b/vendor/github.com/containers/image/v5/ostree/ostree_transport_test.go @@ -0,0 +1,305 @@ +//go:build containers_image_ostree +// +build containers_image_ostree + +package ostree + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + _ "github.com/containers/image/v5/internal/testing/explicitfilepath-tmpdir" + "github.com/containers/image/v5/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + sha256digestHex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + sha256digest = "@sha256:" + sha256digestHex +) + +func TestTransportName(t *testing.T) { + assert.Equal(t, "ostree", Transport.Name()) +} + +// A helper to replace $TMP in a repo path with a real temporary directory +func withTmpDir(repo string, tmpDir string) string { + return strings.ReplaceAll(repo, "$TMP", tmpDir) +} + +// A common list of repo suffixes to test for the various ImageReference methods. +var repoSuffixes = []struct{ repoSuffix, resolvedRepo string }{ + {"", "/ostree/repo"}, + {"@/ostree/repo", "/ostree/repo"}, // /ostree/repo is accepted even if neither /ostree/repo nor /ostree exists, as a special case. + {"@$TMP/at@sign@repo", "$TMP/at@sign@repo"}, + // Rejected as ambiguous: /repo:with:colons could either be an (/repo, with:colons) policy configuration identity, or a (/repo:with, colons) policy configuration namespace. + {"@$TMP/repo:with:colons", ""}, +} + +// A common list of cases for image name parsing and normalization +var imageNameTestcases = []struct{ input, normalized, branchName string }{ + {"busybox:notlatest", "busybox:notlatest", "busybox_3Anotlatest"}, // Explicit tag + {"busybox", "busybox:latest", "busybox_3Alatest"}, // Default tag + {"docker.io/library/busybox:latest", "docker.io/library/busybox:latest", "docker.io_2Flibrary_2Fbusybox_3Alatest"}, // A hierarchical name + {"127.0.0.1:5000/busybox:latest", "127.0.0.1:5000/busybox:latest", "127.0.0.1_3A5000_2Fbusybox_3Alatest"}, // Port usage + {"busybox" + sha256digest, "busybox" + sha256digest, "busybox_40sha256_3A0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}, + {"UPPERCASEISINVALID", "", ""}, // Invalid input + {"busybox:invalid+tag", "", ""}, // Invalid tag value + {"busybox:tag:with:colons", "", ""}, // Multiple colons - treated as a tag which contains a colon, which is invalid + {"", "", ""}, // Empty input is rejected (invalid repository.Named) +} + +func TestTransportParseReference(t *testing.T) { + tmpDir := t.TempDir() + + for _, c := range imageNameTestcases { + for _, suffix := range repoSuffixes { + fullInput := c.input + withTmpDir(suffix.repoSuffix, tmpDir) + ref, err := Transport.ParseReference(fullInput) + if c.normalized == "" || suffix.resolvedRepo == "" { + assert.Error(t, err, fullInput) + } else { + require.NoError(t, err, fullInput) + ostreeRef, ok := ref.(ostreeReference) + require.True(t, ok, fullInput) + assert.Equal(t, c.normalized, ostreeRef.image, fullInput) + assert.Equal(t, c.branchName, ostreeRef.branchName, fullInput) + assert.Equal(t, withTmpDir(suffix.resolvedRepo, tmpDir), ostreeRef.repo, fullInput) + } + } + } +} + +func TestTransportValidatePolicyConfigurationScope(t *testing.T) { + for _, scope := range []string{ + "/etc:docker.io/library/busybox:notlatest", // This also demonstrates that two colons are interpreted as repo:name:tag. + "/etc:docker.io/library/busybox", + "/etc:docker.io/library", + "/etc:docker.io", + "/etc:repo", + "/this/does/not/exist:notlatest", + } { + err := Transport.ValidatePolicyConfigurationScope(scope) + assert.NoError(t, err, scope) + } + + for _, scope := range []string{ + "/colon missing as a path-reference delimiter", + "relative/path:busybox", + "/double//slashes:busybox", + "/has/./dot:busybox", + "/has/dot/../dot:busybox", + "/trailing/slash/:busybox", + } { + err := Transport.ValidatePolicyConfigurationScope(scope) + assert.Error(t, err, scope) + } +} + +func TestNewReference(t *testing.T) { + tmpDir := t.TempDir() + + for _, c := range imageNameTestcases { + for _, suffix := range repoSuffixes { + if suffix.repoSuffix == "" { + continue + } + caseName := c.input + suffix.repoSuffix + ref, err := NewReference(c.input, withTmpDir(strings.TrimPrefix(suffix.repoSuffix, "@"), tmpDir)) + if c.normalized == "" || suffix.resolvedRepo == "" { + assert.Error(t, err, caseName) + } else { + require.NoError(t, err, caseName) + ostreeRef, ok := ref.(ostreeReference) + require.True(t, ok, caseName) + assert.Equal(t, c.normalized, ostreeRef.image, caseName) + assert.Equal(t, c.branchName, ostreeRef.branchName, caseName) + assert.Equal(t, withTmpDir(suffix.resolvedRepo, tmpDir), ostreeRef.repo, caseName) + } + } + } + + for _, path := range []string{ + "/", + "/etc", + tmpDir, + "relativepath", + tmpDir + "/thisdoesnotexist", + } { + _, err := NewReference("busybox", path) + require.NoError(t, err, path) + } + + _, err = NewReference("busybox", tmpDir+"/thisparentdoesnotexist/something") + assert.Error(t, err) +} + +// A common list of reference formats to test for the various ImageReference methods. +var validReferenceTestCases = []struct{ input, stringWithinTransport, policyConfigurationIdentity string }{ + {"busybox", "busybox:latest@/ostree/repo", "/ostree/repo:busybox:latest"}, // Everything implied + {"busybox:latest@/ostree/repo", "busybox:latest@/ostree/repo", "/ostree/repo:busybox:latest"}, // All implied values explicitly specified + {"example.com/ns/foo:bar@$TMP/non-DEFAULT", "example.com/ns/foo:bar@$TMP/non-DEFAULT", "$TMP/non-DEFAULT:example.com/ns/foo:bar"}, // All values explicitly specified, a hierarchical name + // A non-canonical path. Testing just one, the various other cases are tested in explicitfilepath.ResolvePathToFullyExplicit. + {"busybox@$TMP/.", "busybox:latest@$TMP", "$TMP:busybox:latest"}, + // "/" as a corner case + {"busybox@/", "busybox:latest@/", "/:busybox:latest"}, +} + +func TestReferenceTransport(t *testing.T) { + ref, err := Transport.ParseReference("busybox") + require.NoError(t, err) + assert.Equal(t, Transport, ref.Transport()) +} + +func TestReferenceStringWithinTransport(t *testing.T) { + tmpDir := t.TempDir() + + for _, c := range validReferenceTestCases { + ref, err := Transport.ParseReference(withTmpDir(c.input, tmpDir)) + require.NoError(t, err, c.input) + stringRef := ref.StringWithinTransport() + assert.Equal(t, withTmpDir(c.stringWithinTransport, tmpDir), stringRef, c.input) + // Do one more round to verify that the output can be parsed, to an equal value. + ref2, err := Transport.ParseReference(stringRef) + require.NoError(t, err, c.input) + stringRef2 := ref2.StringWithinTransport() + assert.Equal(t, stringRef, stringRef2, c.input) + } +} + +func TestReferenceDockerReference(t *testing.T) { + tmpDir := t.TempDir() + + for _, c := range validReferenceTestCases { + ref, err := Transport.ParseReference(withTmpDir(c.input, tmpDir)) + require.NoError(t, err, c.input) + dockerRef := ref.DockerReference() + assert.Nil(t, dockerRef, c.input) + } +} + +func TestReferencePolicyConfigurationIdentity(t *testing.T) { + tmpDir := t.TempDir() + + for _, c := range validReferenceTestCases { + ref, err := Transport.ParseReference(withTmpDir(c.input, tmpDir)) + require.NoError(t, err, c.input) + assert.Equal(t, withTmpDir(c.policyConfigurationIdentity, tmpDir), ref.PolicyConfigurationIdentity(), c.input) + } +} + +func TestReferencePolicyConfigurationNamespaces(t *testing.T) { + tmpDir := t.TempDir() + + // Test both that DockerReferenceIdentity returns the expected value (fullName+suffix), + // and that DockerReferenceNamespaces starts with the expected value (fullName), i.e. that the two functions are + // consistent. + for inputName, expectedNS := range map[string][]string{ + "example.com/ns/repo": {"example.com/ns/repo", "example.com/ns", "example.com"}, + "example.com/repo": {"example.com/repo", "example.com"}, + "localhost/ns/repo": {"localhost/ns/repo", "localhost/ns", "localhost"}, + "localhost/repo": {"localhost/repo", "localhost"}, + "ns/repo": {"ns/repo", "ns"}, + "repo": {"repo"}, + } { + // Test with a known path which should exist. Test just one non-canonical + // path, the various other cases are tested in explicitfilepath.ResolvePathToFullyExplicit. + for _, repoInput := range []string{tmpDir, tmpDir + "/./."} { + fullName := inputName + ":notlatest" + ref, err := NewReference(fullName, repoInput) + require.NoError(t, err, fullName) + + identity := ref.PolicyConfigurationIdentity() + assert.Equal(t, tmpDir+":"+expectedNS[0]+":notlatest", identity, fullName) + + ns := ref.PolicyConfigurationNamespaces() + require.NotNil(t, ns, fullName) + require.Len(t, ns, len(expectedNS), fullName) + moreSpecific := identity + for i := range expectedNS { + assert.Equal(t, tmpDir+":"+expectedNS[i], ns[i], fmt.Sprintf("%s item %d", fullName, i)) + assert.True(t, strings.HasPrefix(moreSpecific, ns[i])) + moreSpecific = ns[i] + } + } + } +} + +func TestReferenceNewImage(t *testing.T) { + ref, err := Transport.ParseReference("busybox") + require.NoError(t, err) + _, err = ref.NewImage(context.Background(), nil) + assert.Error(t, err) +} + +func TestReferenceNewImageSource(t *testing.T) { + ref, err := Transport.ParseReference("busybox") + require.NoError(t, err) + src, err = ref.NewImageSource(context.Background(), nil) + require.NoError(t, err) + defer src.Close() +} + +func TestReferenceNewImageDestination(t *testing.T) { + otherTmpDir := t.TempDir() + + for _, c := range []struct { + sys *types.SystemContext + tmpDir string + }{ + {nil, os.TempDir()}, + {&types.SystemContext{}, os.TempDir()}, + {&types.SystemContext{OSTreeTmpDirPath: otherTmpDir}, otherTmpDir}, + } { + ref, err := Transport.ParseReference("busybox") + require.NoError(t, err) + dest, err := ref.NewImageDestination(context.Background(), c.sys) + require.NoError(t, err) + ostreeDest, ok := dest.(*ostreeImageDestination) + require.True(t, ok) + assert.Equal(t, c.tmpDir+"/busybox_3Alatest", ostreeDest.tmpDirPath) + defer dest.Close() + } +} + +func TestReferenceDeleteImage(t *testing.T) { + tmpDir := t.TempDir() + + ref, err := Transport.ParseReference(withTmpDir("busybox@$TMP/this-repo-does-not-exist", tmpDir)) + require.NoError(t, err) + err = ref.DeleteImage(context.Background(), nil) + assert.Error(t, err) +} + +func TestEncodeOSTreeRef(t *testing.T) { + // Just a smoke test + assert.Equal(t, "busybox_3Alatest", encodeOStreeRef("busybox:latest")) +} + +func TestReferenceManifestPath(t *testing.T) { + ref, err := Transport.ParseReference("busybox") + require.NoError(t, err) + ostreeRef, ok := ref.(ostreeReference) + require.True(t, ok) + assert.Equal(t, fmt.Sprintf("manifest%cmanifest.json", filepath.Separator), ostreeRef.manifestPath()) +} + +func TestReferenceSignaturePath(t *testing.T) { + ref, err := Transport.ParseReference("busybox") + require.NoError(t, err) + ostreeRef, ok := ref.(ostreeReference) + require.True(t, ok) + for _, c := range []struct { + input int + suffix string + }{ + {0, "-1"}, + {42, "-43"}, + } { + assert.Equal(t, fmt.Sprintf("manifest%csignature%s", filepath.Separator, c.suffix), ostreeRef.signaturePath(c.input), fmt.Sprintf("%d", c.input)) + } +} diff --git a/vendor/github.com/containers/image/v5/pkg/blobcache/blobcache_test.go b/vendor/github.com/containers/image/v5/pkg/blobcache/blobcache_test.go new file mode 100644 index 00000000000..fa20d003b42 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/blobcache/blobcache_test.go @@ -0,0 +1,250 @@ +package blobcache + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "flag" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "strings" + "testing" + + cp "github.com/containers/image/v5/copy" + "github.com/containers/image/v5/directory" + "github.com/containers/image/v5/internal/image" + "github.com/containers/image/v5/internal/private" + "github.com/containers/image/v5/pkg/blobinfocache/none" + "github.com/containers/image/v5/signature" + "github.com/containers/image/v5/types" + "github.com/containers/storage/pkg/archive" + digest "github.com/opencontainers/go-digest" + specs "github.com/opencontainers/image-spec/specs-go" + v1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/sirupsen/logrus" +) + +var ( + _ types.ImageReference = &BlobCache{} + _ types.ImageSource = &blobCacheSource{} + _ private.ImageSource = (*blobCacheSource)(nil) + _ types.ImageDestination = &blobCacheDestination{} + _ private.ImageDestination = (*blobCacheDestination)(nil) +) + +func TestMain(m *testing.M) { + flag.Parse() + if testing.Verbose() { + logrus.SetLevel(logrus.DebugLevel) + } + os.Exit(m.Run()) +} + +// Create a layer containing a single file with the specified name (and its +// name as its contents), compressed using the specified compression type, and +// return the . +func makeLayer(filename string, repeat int, compression archive.Compression) ([]byte, digest.Digest, error) { + var compressed, uncompressed bytes.Buffer + layer, err := archive.Generate(filename, strings.Repeat(filename, repeat)) + if err != nil { + return nil, "", err + } + writer, err := archive.CompressStream(&compressed, compression) + if err != nil { + return nil, "", err + } + reader := io.TeeReader(layer, &uncompressed) + _, err = io.Copy(writer, reader) + writer.Close() + if err != nil { + return nil, "", err + } + return compressed.Bytes(), digest.FromBytes(uncompressed.Bytes()), nil +} + +func TestBlobCache(t *testing.T) { + cacheDir := t.TempDir() + + systemContext := types.SystemContext{BlobInfoCacheDir: "/dev/null/this/does/not/exist"} + + for _, repeat := range []int{1, 10000} { + for _, desiredCompression := range []types.LayerCompression{types.PreserveOriginal, types.Compress, types.Decompress} { + for _, layerCompression := range []archive.Compression{archive.Uncompressed, archive.Gzip} { + // Create a layer with the specified layerCompression. + blobBytes, diffID, err := makeLayer(fmt.Sprintf("layer-content-%d", int(layerCompression)), repeat, layerCompression) + if err != nil { + t.Fatalf("error making layer: %v", err) + } + blobInfo := types.BlobInfo{ + Digest: digest.FromBytes(blobBytes), + Size: int64(len(blobBytes)), + } + // Create a configuration that includes the diffID for the layer and not much else. + config := v1.Image{ + RootFS: v1.RootFS{ + Type: "layers", + DiffIDs: []digest.Digest{diffID}, + }, + } + configBytes, err := json.Marshal(&config) + if err != nil { + t.Fatalf("error encoding image configuration: %v", err) + } + configInfo := types.BlobInfo{ + Digest: digest.FromBytes(configBytes), + Size: int64(len(configBytes)), + } + // Create a manifest that uses this configuration and layer. + manifest := v1.Manifest{ + Versioned: specs.Versioned{ + SchemaVersion: 2, + }, + MediaType: v1.MediaTypeImageManifest, + Config: v1.Descriptor{ + MediaType: v1.MediaTypeImageConfig, + Digest: configInfo.Digest, + Size: configInfo.Size, + }, + Layers: []v1.Descriptor{{ + MediaType: v1.MediaTypeImageLayer, + Digest: blobInfo.Digest, + Size: blobInfo.Size, + }}, + } + manifestBytes, err := json.Marshal(&manifest) + if err != nil { + t.Fatalf("error encoding image manifest: %v", err) + } + // Write this image to a "dir" destination with blob caching using this directory. + srcdir := t.TempDir() + srcRef, err := directory.NewReference(srcdir) + if err != nil { + t.Fatalf("error creating source image name reference for %q: %v", srcdir, err) + } + cachedSrcRef, err := NewBlobCache(srcRef, cacheDir, desiredCompression) + if err != nil { + t.Fatalf("failed to wrap reference in cache: %v", err) + } + destImage, err := cachedSrcRef.NewImageDestination(context.TODO(), nil) + if err != nil { + t.Fatalf("error opening source image for writing: %v", err) + } + _, err = destImage.PutBlob(context.TODO(), bytes.NewReader(blobBytes), blobInfo, none.NoCache, false) + if err != nil { + t.Fatalf("error writing layer blob to source image: %v", err) + } + _, err = destImage.PutBlob(context.TODO(), bytes.NewReader(configBytes), configInfo, none.NoCache, true) + if err != nil { + t.Fatalf("error writing config blob to source image: %v", err) + } + srcImage, err := srcRef.NewImageSource(context.TODO(), &systemContext) + if err != nil { + t.Fatalf("error opening source image: %v", err) + } + defer func() { + err := srcImage.Close() + if err != nil { + t.Fatalf("error closing source image: %v", err) + } + }() + err = destImage.PutManifest(context.TODO(), manifestBytes, nil) + if err != nil { + t.Fatalf("error writing manifest to source image: %v", err) + } + err = destImage.Commit(context.TODO(), image.UnparsedInstance(srcImage, nil)) + if err != nil { + t.Fatalf("error committing source image: %v", err) + } + if err = destImage.Close(); err != nil { + t.Fatalf("error closing source image: %v", err) + } + // Check that the cache was populated. + cache, err := os.Open(cacheDir) + if err != nil { + t.Fatalf("error opening cache directory %q: %v", cacheDir, err) + } + defer cache.Close() + cachedNames, err := cache.Readdirnames(-1) + if err != nil { + t.Fatalf("error reading contents of cache directory %q: %v", cacheDir, err) + } + // Expect a layer blob, a config blob, and the manifest. + expected := 3 + if layerCompression != archive.Uncompressed { + // Expect a compressed blob, an uncompressed blob, notes for each about the other, a config blob, and the manifest. + expected = 6 + } + if len(cachedNames) != expected { + t.Fatalf("expected %d items in cache directory %q, got %d: %v", expected, cacheDir, len(cachedNames), cachedNames) + } + // Check that the blobs were all correctly stored. + for _, cachedName := range cachedNames { + if digest.Digest(cachedName).Validate() == nil { + cacheMember := filepath.Join(cacheDir, cachedName) + cacheMemberBytes, err := os.ReadFile(cacheMember) + if err != nil { + t.Fatalf("error reading cache member %q: %v", cacheMember, err) + } + if digest.FromBytes(cacheMemberBytes).String() != cachedName { + t.Fatalf("cache member %q was stored incorrectly!", cacheMember) + } + } + } + // Clear out anything in the source directory that probably isn't a manifest, so that we'll + // have to depend on the cached copies of some of the blobs. + srcNameDir, err := os.Open(srcdir) + if err != nil { + t.Fatalf("error opening source directory %q: %v", srcdir, err) + } + defer srcNameDir.Close() + srcNames, err := srcNameDir.Readdirnames(-1) + if err != nil { + t.Fatalf("error reading contents of source directory %q: %v", srcdir, err) + } + for _, name := range srcNames { + if !strings.HasPrefix(name, "manifest") { + os.Remove(filepath.Join(srcdir, name)) + } + } + // Now that we've deleted some of the contents, try to copy from the source image + // to a second image. It should fail because the source is missing some blobs. + destdir := t.TempDir() + destRef, err := directory.NewReference(destdir) + if err != nil { + t.Fatalf("error creating destination image reference for %q: %v", destdir, err) + } + options := cp.Options{ + SourceCtx: &systemContext, + DestinationCtx: &systemContext, + } + policyContext, err := signature.NewPolicyContext(&signature.Policy{ + Default: []signature.PolicyRequirement{signature.NewPRInsecureAcceptAnything()}, + }) + if err != nil { + t.Fatalf("error creating signature policy context: %v", err) + } + _, err = cp.Image(context.TODO(), policyContext, destRef, srcRef, &options) + if err == nil { + t.Fatalf("expected an error copying the image, but got success") + } else { + if errors.Is(err, fs.ErrNotExist) { + t.Logf("ok: got expected does-not-exist error copying the image with blobs missing: %v", err) + } else { + t.Logf("got an error copying the image with missing blobs, but not sure which error: %v", err) + } + } + _, err = cp.Image(context.TODO(), policyContext, destRef, cachedSrcRef, &options) + if err != nil { + t.Fatalf("unexpected error copying the image using the cache: %v", err) + } + if err = cachedSrcRef.ClearCache(); err != nil { + t.Fatalf("error clearing cache: %v", err) + } + } + } + } +} diff --git a/vendor/github.com/containers/image/v5/pkg/blobcache/dest.go b/vendor/github.com/containers/image/v5/pkg/blobcache/dest.go index a0e353d46f1..9bda085158e 100644 --- a/vendor/github.com/containers/image/v5/pkg/blobcache/dest.go +++ b/vendor/github.com/containers/image/v5/pkg/blobcache/dest.go @@ -237,6 +237,9 @@ func (d *blobCacheDestination) PutBlobPartial(ctx context.Context, chunkAccessor // If the blob has been successfully reused, returns (true, info, nil). // If the transport can not reuse the requested blob, TryReusingBlob returns (false, {}, nil); it returns a non-nil error only on an unexpected failure. func (d *blobCacheDestination) TryReusingBlobWithOptions(ctx context.Context, info types.BlobInfo, options private.TryReusingBlobOptions) (bool, private.ReusedBlob, error) { + if !impl.OriginalBlobMatchesRequiredCompression(options) { + return false, private.ReusedBlob{}, nil + } present, reusedInfo, err := d.destination.TryReusingBlobWithOptions(ctx, info, options) if err != nil || present { return present, reusedInfo, err diff --git a/vendor/github.com/containers/image/v5/pkg/blobcache/src_test.go b/vendor/github.com/containers/image/v5/pkg/blobcache/src_test.go new file mode 100644 index 00000000000..a15498c21ba --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/blobcache/src_test.go @@ -0,0 +1,56 @@ +package blobcache + +import ( + "bytes" + "io" + "testing" + + "github.com/containers/image/v5/internal/private" + "github.com/stretchr/testify/assert" +) + +func readNextStream(streams chan io.ReadCloser, errs chan error) ([]byte, error) { + select { + case r := <-streams: + if r == nil { + return nil, nil + } + defer r.Close() + return io.ReadAll(r) + case err := <-errs: + return nil, err + } +} + +// readSeekerNopCloser adds a no-op Close() method to a readSeeker +type readSeekerNopCloser struct { + io.ReadSeeker +} + +func (c *readSeekerNopCloser) Close() error { + return nil +} + +func TestStreamChunksFromFile(t *testing.T) { + file := &readSeekerNopCloser{bytes.NewReader([]byte("123456789"))} + streams := make(chan io.ReadCloser) + errs := make(chan error) + chunks := []private.ImageSourceChunk{ + {Offset: 1, Length: 2}, + {Offset: 4, Length: 1}, + } + go streamChunksFromFile(streams, errs, file, chunks) + + for _, c := range []struct { + expectedData []byte + expectedError error + }{ + {[]byte("23"), nil}, + {[]byte("5"), nil}, + {[]byte(nil), nil}, + } { + data, err := readNextStream(streams, errs) + assert.Equal(t, c.expectedData, data) + assert.Equal(t, c.expectedError, err) + } +} diff --git a/vendor/github.com/containers/image/v5/pkg/blobinfocache/boltdb/boltdb_test.go b/vendor/github.com/containers/image/v5/pkg/blobinfocache/boltdb/boltdb_test.go new file mode 100644 index 00000000000..50b47845967 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/blobinfocache/boltdb/boltdb_test.go @@ -0,0 +1,26 @@ +package boltdb + +import ( + "path/filepath" + "testing" + + "github.com/containers/image/v5/internal/blobinfocache" + "github.com/containers/image/v5/pkg/blobinfocache/internal/test" +) + +var _ blobinfocache.BlobInfoCache2 = &cache{} + +func newTestCache(t *testing.T) blobinfocache.BlobInfoCache2 { + // We need a separate temporary directory here, because bolt.Open(…, &bolt.Options{Readonly:true}) can't deal with + // an existing but empty file, and incorrectly fails without releasing the lock - which in turn causes + // any future writes to hang. Creating a temporary directory allows us to use a path to a + // non-existent file, thus replicating the expected conditions for creating a new DB. + dir := t.TempDir() + return new2(filepath.Join(dir, "db")) +} + +func TestNew(t *testing.T) { + test.GenericCache(t, newTestCache) +} + +// FIXME: Tests for the various corner cases / failure cases of boltDBCache should be added here. diff --git a/vendor/github.com/containers/image/v5/pkg/blobinfocache/default_test.go b/vendor/github.com/containers/image/v5/pkg/blobinfocache/default_test.go new file mode 100644 index 00000000000..83a3fea98aa --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/blobinfocache/default_test.go @@ -0,0 +1,132 @@ +package blobinfocache + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" + + "github.com/containers/image/v5/pkg/blobinfocache/boltdb" + "github.com/containers/image/v5/pkg/blobinfocache/memory" + "github.com/containers/image/v5/types" + "github.com/stretchr/testify/assert" +) + +func TestBlobInfoCacheDir(t *testing.T) { + const nondefaultDir = "/this/is/not/the/default/cache/dir" + const rootPrefix = "/root/prefix" + const homeDir = "/fake/home/directory" + const xdgDataHome = "/fake/home/directory/XDG" + + t.Setenv("HOME", homeDir) + t.Setenv("XDG_DATA_HOME", xdgDataHome) + + // The default paths and explicit overrides + for _, c := range []struct { + sys *types.SystemContext + euid int + expected string + }{ + // The common case + {nil, 0, systemBlobInfoCacheDir}, + {nil, 1, filepath.Join(xdgDataHome, "containers", "cache")}, + // There is a context, but it does not override the path. + {&types.SystemContext{}, 0, systemBlobInfoCacheDir}, + {&types.SystemContext{}, 1, filepath.Join(xdgDataHome, "containers", "cache")}, + // Path overridden + {&types.SystemContext{BlobInfoCacheDir: nondefaultDir}, 0, nondefaultDir}, + {&types.SystemContext{BlobInfoCacheDir: nondefaultDir}, 1, nondefaultDir}, + // Root overridden + {&types.SystemContext{RootForImplicitAbsolutePaths: rootPrefix}, 0, filepath.Join(rootPrefix, systemBlobInfoCacheDir)}, + {&types.SystemContext{RootForImplicitAbsolutePaths: rootPrefix}, 1, filepath.Join(xdgDataHome, "containers", "cache")}, + // Root and path overrides present simultaneously, + { + &types.SystemContext{ + RootForImplicitAbsolutePaths: rootPrefix, + BlobInfoCacheDir: nondefaultDir, + }, + 0, nondefaultDir, + }, + { + &types.SystemContext{ + RootForImplicitAbsolutePaths: rootPrefix, + BlobInfoCacheDir: nondefaultDir, + }, + 1, nondefaultDir, + }, + } { + path, err := blobInfoCacheDir(c.sys, c.euid) + require.NoError(t, err) + assert.Equal(t, c.expected, path) + } + + // Paths used by unprivileged users + for caseIndex, c := range []struct { + xdgDH, home, expected string + }{ + {"", homeDir, filepath.Join(homeDir, ".local", "share", "containers", "cache")}, // HOME only + {xdgDataHome, "", filepath.Join(xdgDataHome, "containers", "cache")}, // XDG_DATA_HOME only + {xdgDataHome, homeDir, filepath.Join(xdgDataHome, "containers", "cache")}, // both + {"", "", ""}, // neither + } { + t.Run(fmt.Sprintf("unprivileged %d", caseIndex), func(t *testing.T) { + // Always use t.Setenv() to ensure the environment variable is restored to the original value after the test. + // Then, in cases where the test needs the variable unset (not just set to empty), use a raw os.Unsetenv() + // to override the situation. (Sadly there isn’t a t.Unsetenv() as of Go 1.17.) + t.Setenv("XDG_DATA_HOME", c.xdgDH) + if c.xdgDH == "" { + os.Unsetenv("XDG_DATA_HOME") + } + t.Setenv("HOME", c.home) + if c.home == "" { + os.Unsetenv("HOME") + } + for _, sys := range []*types.SystemContext{nil, {}} { + path, err := blobInfoCacheDir(sys, 1) + if c.expected != "" { + require.NoError(t, err) + assert.Equal(t, c.expected, path) + } else { + assert.Error(t, err) + } + } + }) + } +} + +func TestDefaultCache(t *testing.T) { + tmpDir := t.TempDir() + + // Success + normalDir := filepath.Join(tmpDir, "normal") + c := DefaultCache(&types.SystemContext{BlobInfoCacheDir: normalDir}) + // This is ugly hard-coding internals of boltDBCache: + assert.Equal(t, boltdb.New(filepath.Join(normalDir, blobInfoCacheFilename)), c) + + // Error running blobInfoCacheDir: + // Use t.Setenv() just as a way to set up cleanup to original values; then os.Unsetenv() to test a situation where the values are not set. + t.Setenv("HOME", "") + os.Unsetenv("HOME") + t.Setenv("XDG_DATA_HOME", "") + os.Unsetenv("XDG_DATA_HOME") + c = DefaultCache(nil) + assert.IsType(t, memory.New(), c) + + // Error creating the parent directory: + unwritableDir := filepath.Join(tmpDir, "unwritable") + err := os.Mkdir(unwritableDir, 0700) + require.NoError(t, err) + defer func() { + err = os.Chmod(unwritableDir, 0700) // To make it possible to remove it again + require.NoError(t, err) + }() + err = os.Chmod(unwritableDir, 0500) + require.NoError(t, err) + st, _ := os.Stat(unwritableDir) + logrus.Errorf("%s: %#v", unwritableDir, st) + c = DefaultCache(&types.SystemContext{BlobInfoCacheDir: filepath.Join(unwritableDir, "subdirectory")}) + assert.IsType(t, memory.New(), c) +} diff --git a/vendor/github.com/containers/image/v5/pkg/blobinfocache/internal/prioritize/prioritize_test.go b/vendor/github.com/containers/image/v5/pkg/blobinfocache/internal/prioritize/prioritize_test.go new file mode 100644 index 00000000000..77167e904ca --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/blobinfocache/internal/prioritize/prioritize_test.go @@ -0,0 +1,173 @@ +package prioritize + +import ( + "fmt" + "testing" + "time" + + "github.com/containers/image/v5/internal/blobinfocache" + compressiontypes "github.com/containers/image/v5/pkg/compression/types" + "github.com/containers/image/v5/types" + "github.com/opencontainers/go-digest" + "github.com/stretchr/testify/assert" + "golang.org/x/exp/slices" +) + +const ( + digestUncompressed = digest.Digest("sha256:2222222222222222222222222222222222222222222222222222222222222222") + digestCompressedA = digest.Digest("sha256:3333333333333333333333333333333333333333333333333333333333333333") + digestCompressedB = digest.Digest("sha256:4444444444444444444444444444444444444444444444444444444444444444") + digestCompressedPrimary = digest.Digest("sha256:6666666666666666666666666666666666666666666666666666666666666666") +) + +var ( + // cssLiteral contains a non-trivial candidateSortState shared among several tests below. + cssLiteral = candidateSortState{ + cs: []CandidateWithTime{ + {blobinfocache.BICReplacementCandidate2{Digest: digestCompressedA, Location: types.BICLocationReference{Opaque: "A1"}, CompressorName: compressiontypes.XzAlgorithmName}, time.Unix(1, 0)}, + {blobinfocache.BICReplacementCandidate2{Digest: digestUncompressed, Location: types.BICLocationReference{Opaque: "U2"}, CompressorName: compressiontypes.GzipAlgorithmName}, time.Unix(1, 1)}, + {blobinfocache.BICReplacementCandidate2{Digest: digestCompressedA, Location: types.BICLocationReference{Opaque: "A2"}, CompressorName: blobinfocache.Uncompressed}, time.Unix(1, 1)}, + {blobinfocache.BICReplacementCandidate2{Digest: digestCompressedPrimary, Location: types.BICLocationReference{Opaque: "P1"}, CompressorName: blobinfocache.UnknownCompression}, time.Unix(1, 0)}, + {blobinfocache.BICReplacementCandidate2{Digest: digestCompressedB, Location: types.BICLocationReference{Opaque: "B1"}, CompressorName: compressiontypes.Bzip2AlgorithmName}, time.Unix(1, 1)}, + {blobinfocache.BICReplacementCandidate2{Digest: digestCompressedPrimary, Location: types.BICLocationReference{Opaque: "P2"}, CompressorName: compressiontypes.GzipAlgorithmName}, time.Unix(1, 1)}, + {blobinfocache.BICReplacementCandidate2{Digest: digestCompressedB, Location: types.BICLocationReference{Opaque: "B2"}, CompressorName: blobinfocache.Uncompressed}, time.Unix(2, 0)}, + {blobinfocache.BICReplacementCandidate2{Digest: digestUncompressed, Location: types.BICLocationReference{Opaque: "U1"}, CompressorName: blobinfocache.UnknownCompression}, time.Unix(1, 0)}, + }, + primaryDigest: digestCompressedPrimary, + uncompressedDigest: digestUncompressed, + } + // cssExpectedReplacementCandidates is the fully-sorted, unlimited, result of prioritizing cssLiteral. + cssExpectedReplacementCandidates = []blobinfocache.BICReplacementCandidate2{ + {Digest: digestCompressedPrimary, Location: types.BICLocationReference{Opaque: "P2"}, CompressorName: compressiontypes.GzipAlgorithmName}, + {Digest: digestCompressedPrimary, Location: types.BICLocationReference{Opaque: "P1"}, CompressorName: blobinfocache.UnknownCompression}, + {Digest: digestCompressedB, Location: types.BICLocationReference{Opaque: "B2"}, CompressorName: blobinfocache.Uncompressed}, + {Digest: digestCompressedA, Location: types.BICLocationReference{Opaque: "A2"}, CompressorName: blobinfocache.Uncompressed}, + {Digest: digestCompressedB, Location: types.BICLocationReference{Opaque: "B1"}, CompressorName: compressiontypes.Bzip2AlgorithmName}, + {Digest: digestCompressedA, Location: types.BICLocationReference{Opaque: "A1"}, CompressorName: compressiontypes.XzAlgorithmName}, + {Digest: digestUncompressed, Location: types.BICLocationReference{Opaque: "U2"}, CompressorName: compressiontypes.GzipAlgorithmName}, + {Digest: digestUncompressed, Location: types.BICLocationReference{Opaque: "U1"}, CompressorName: blobinfocache.UnknownCompression}, + } +) + +func TestCandidateSortStateLen(t *testing.T) { + css := cssLiteral + assert.Equal(t, 8, css.Len()) + + css.cs = []CandidateWithTime{} + assert.Equal(t, 0, css.Len()) +} + +func TestCandidateSortStateLess(t *testing.T) { + type p struct { + d digest.Digest + t int64 + } + + // Primary criteria: Also ensure that time does not matter + for _, c := range []struct { + name string + res int + d0, d1 digest.Digest + }{ + {"primary < any", -1, digestCompressedPrimary, digestCompressedA}, + {"any < uncompressed", -1, digestCompressedA, digestUncompressed}, + {"primary < uncompressed", -1, digestCompressedPrimary, digestUncompressed}, + } { + for _, tms := range [][2]int64{{1, 2}, {2, 1}, {1, 1}} { + caseName := fmt.Sprintf("%s %v", c.name, tms) + css := candidateSortState{ + cs: []CandidateWithTime{ + {blobinfocache.BICReplacementCandidate2{Digest: c.d0, Location: types.BICLocationReference{Opaque: "L0"}, CompressorName: compressiontypes.GzipAlgorithmName}, time.Unix(tms[0], 0)}, + {blobinfocache.BICReplacementCandidate2{Digest: c.d1, Location: types.BICLocationReference{Opaque: "L1"}, CompressorName: compressiontypes.ZstdAlgorithmName}, time.Unix(tms[1], 0)}, + }, + primaryDigest: digestCompressedPrimary, + uncompressedDigest: digestUncompressed, + } + assert.Equal(t, c.res < 0, css.Less(0, 1), caseName) + assert.Equal(t, c.res > 0, css.Less(1, 0), caseName) + + if c.d0 != digestUncompressed && c.d1 != digestUncompressed { + css.uncompressedDigest = "" + assert.Equal(t, c.res < 0, css.Less(0, 1), caseName) + assert.Equal(t, c.res > 0, css.Less(1, 0), caseName) + + css.uncompressedDigest = css.primaryDigest + assert.Equal(t, c.res < 0, css.Less(0, 1), caseName) + assert.Equal(t, c.res > 0, css.Less(1, 0), caseName) + } + } + } + + // Ordering within the three primary groups + for _, c := range []struct { + name string + res int + p0, p1 p + }{ + {"primary: t=2 < t=1", -1, p{digestCompressedPrimary, 2}, p{digestCompressedPrimary, 1}}, + {"primary: t=1 == t=1", 0, p{digestCompressedPrimary, 1}, p{digestCompressedPrimary, 1}}, + {"uncompressed: t=2 < t=1", -1, p{digestUncompressed, 2}, p{digestUncompressed, 1}}, + {"uncompressed: t=1 == t=1", 0, p{digestUncompressed, 1}, p{digestUncompressed, 1}}, + {"any: t=2 < t=1, [d=A vs. d=B lower-priority]", -1, p{digestCompressedA, 2}, p{digestCompressedB, 1}}, + {"any: t=2 < t=1, [d=B vs. d=A lower-priority]", -1, p{digestCompressedB, 2}, p{digestCompressedA, 1}}, + {"any: t=2 < t=1, [d=A vs. d=A lower-priority]", -1, p{digestCompressedA, 2}, p{digestCompressedA, 1}}, + {"any: t=1 == t=1, d=A < d=B", -1, p{digestCompressedA, 1}, p{digestCompressedB, 1}}, + {"any: t=1 == t=1, d=A == d=A", 0, p{digestCompressedA, 1}, p{digestCompressedA, 1}}, + } { + css := candidateSortState{ + cs: []CandidateWithTime{ + {blobinfocache.BICReplacementCandidate2{Digest: c.p0.d, Location: types.BICLocationReference{Opaque: "L0"}, CompressorName: compressiontypes.GzipAlgorithmName}, time.Unix(c.p0.t, 0)}, + {blobinfocache.BICReplacementCandidate2{Digest: c.p1.d, Location: types.BICLocationReference{Opaque: "L1"}, CompressorName: compressiontypes.ZstdAlgorithmName}, time.Unix(c.p1.t, 0)}, + }, + primaryDigest: digestCompressedPrimary, + uncompressedDigest: digestUncompressed, + } + assert.Equal(t, c.res < 0, css.Less(0, 1), c.name) + assert.Equal(t, c.res > 0, css.Less(1, 0), c.name) + + if c.p0.d != digestUncompressed && c.p1.d != digestUncompressed { + css.uncompressedDigest = "" + assert.Equal(t, c.res < 0, css.Less(0, 1), c.name) + assert.Equal(t, c.res > 0, css.Less(1, 0), c.name) + + css.uncompressedDigest = css.primaryDigest + assert.Equal(t, c.res < 0, css.Less(0, 1), c.name) + assert.Equal(t, c.res > 0, css.Less(1, 0), c.name) + } + } +} + +func TestCandidateSortStateSwap(t *testing.T) { + freshCSS := func() candidateSortState { // Return a deep copy of cssLiteral which is safe to modify. + res := cssLiteral + res.cs = slices.Clone(cssLiteral.cs) + return res + } + + css := freshCSS() + css.Swap(0, 1) + assert.Equal(t, cssLiteral.cs[1], css.cs[0]) + assert.Equal(t, cssLiteral.cs[0], css.cs[1]) + assert.Equal(t, cssLiteral.cs[2], css.cs[2]) + + css = freshCSS() + css.Swap(1, 1) + assert.Equal(t, cssLiteral, css) +} + +func TestDestructivelyPrioritizeReplacementCandidatesWithMax(t *testing.T) { + for _, max := range []int{0, 1, replacementAttempts, 100} { + // Just a smoke test; we mostly rely on test coverage in TestCandidateSortStateLess + res := destructivelyPrioritizeReplacementCandidatesWithMax(slices.Clone(cssLiteral.cs), digestCompressedPrimary, digestUncompressed, max) + if max > len(cssExpectedReplacementCandidates) { + max = len(cssExpectedReplacementCandidates) + } + assert.Equal(t, cssExpectedReplacementCandidates[:max], res) + } +} + +func TestDestructivelyPrioritizeReplacementCandidates(t *testing.T) { + // Just a smoke test; we mostly rely on test coverage in TestCandidateSortStateLess + res := DestructivelyPrioritizeReplacementCandidates(slices.Clone(cssLiteral.cs), digestCompressedPrimary, digestUncompressed) + assert.Equal(t, cssExpectedReplacementCandidates[:replacementAttempts], res) +} diff --git a/vendor/github.com/containers/image/v5/pkg/blobinfocache/internal/test/test.go b/vendor/github.com/containers/image/v5/pkg/blobinfocache/internal/test/test.go new file mode 100644 index 00000000000..ce4ce5e302f --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/blobinfocache/internal/test/test.go @@ -0,0 +1,352 @@ +// Package test provides generic BlobInfoCache test helpers. +package test + +import ( + "testing" + + "github.com/containers/image/v5/internal/blobinfocache" + "github.com/containers/image/v5/internal/testing/mocks" + "github.com/containers/image/v5/types" + digest "github.com/opencontainers/go-digest" + "github.com/stretchr/testify/assert" +) + +const ( + digestUnknown = digest.Digest("sha256:1111111111111111111111111111111111111111111111111111111111111111") + digestUncompressed = digest.Digest("sha256:2222222222222222222222222222222222222222222222222222222222222222") + digestCompressedA = digest.Digest("sha256:3333333333333333333333333333333333333333333333333333333333333333") + digestCompressedB = digest.Digest("sha256:4444444444444444444444444444444444444444444444444444444444444444") + digestCompressedUnrelated = digest.Digest("sha256:5555555555555555555555555555555555555555555555555555555555555555") + compressorNameU = "compressorName/U" + compressorNameA = "compressorName/A" + compressorNameB = "compressorName/B" + compressorNameCU = "compressorName/CU" +) + +// GenericCache runs an implementation-independent set of tests, given a +// newTestCache, which can be called repeatedly and always returns a fresh cache instance +func GenericCache(t *testing.T, newTestCache func(t *testing.T) blobinfocache.BlobInfoCache2) { + for _, s := range []struct { + name string + fn func(t *testing.T, cache blobinfocache.BlobInfoCache2) + }{ + {"UncompressedDigest", testGenericUncompressedDigest}, + {"RecordDigestUncompressedPair", testGenericRecordDigestUncompressedPair}, + {"RecordKnownLocations", testGenericRecordKnownLocations}, + {"CandidateLocations", testGenericCandidateLocations}, + {"CandidateLocations2", testGenericCandidateLocations2}, + } { + t.Run(s.name, func(t *testing.T) { + cache := newTestCache(t) + s.fn(t, cache) + }) + } +} + +func testGenericUncompressedDigest(t *testing.T, cache blobinfocache.BlobInfoCache2) { + // Nothing is known. + assert.Equal(t, digest.Digest(""), cache.UncompressedDigest(digestUnknown)) + + cache.RecordDigestUncompressedPair(digestCompressedA, digestUncompressed) + cache.RecordDigestUncompressedPair(digestCompressedB, digestUncompressed) + // Known compressed→uncompressed mapping + assert.Equal(t, digestUncompressed, cache.UncompressedDigest(digestCompressedA)) + assert.Equal(t, digestUncompressed, cache.UncompressedDigest(digestCompressedB)) + // This implicitly marks digestUncompressed as uncompressed. + assert.Equal(t, digestUncompressed, cache.UncompressedDigest(digestUncompressed)) + + // Known uncompressed→self mapping + cache.RecordDigestUncompressedPair(digestCompressedUnrelated, digestCompressedUnrelated) + assert.Equal(t, digestCompressedUnrelated, cache.UncompressedDigest(digestCompressedUnrelated)) +} + +func testGenericRecordDigestUncompressedPair(t *testing.T, cache blobinfocache.BlobInfoCache2) { + for i := 0; i < 2; i++ { // Record the same data twice to ensure redundant writes don’t break things. + // Known compressed→uncompressed mapping + cache.RecordDigestUncompressedPair(digestCompressedA, digestUncompressed) + assert.Equal(t, digestUncompressed, cache.UncompressedDigest(digestCompressedA)) + // Two mappings to the same uncompressed digest + cache.RecordDigestUncompressedPair(digestCompressedB, digestUncompressed) + assert.Equal(t, digestUncompressed, cache.UncompressedDigest(digestCompressedB)) + + // Mapping an uncompressed digest to self + cache.RecordDigestUncompressedPair(digestUncompressed, digestUncompressed) + assert.Equal(t, digestUncompressed, cache.UncompressedDigest(digestUncompressed)) + } +} + +func testGenericRecordKnownLocations(t *testing.T, cache blobinfocache.BlobInfoCache2) { + transport := mocks.NameImageTransport("==BlobInfocache transport mock") + for i := 0; i < 2; i++ { // Record the same data twice to ensure redundant writes don’t break things. + for _, scopeName := range []string{"A", "B"} { // Run the test in two different scopes to verify they don't affect each other. + scope := types.BICTransportScope{Opaque: scopeName} + for _, digest := range []digest.Digest{digestCompressedA, digestCompressedB} { // Two different digests should not affect each other either. + lr1 := types.BICLocationReference{Opaque: scopeName + "1"} + lr2 := types.BICLocationReference{Opaque: scopeName + "2"} + cache.RecordKnownLocation(transport, scope, digest, lr2) + cache.RecordKnownLocation(transport, scope, digest, lr1) + assert.Equal(t, []types.BICReplacementCandidate{ + {Digest: digest, Location: lr1}, + {Digest: digest, Location: lr2}, + }, cache.CandidateLocations(transport, scope, digest, false)) + assert.Equal(t, []blobinfocache.BICReplacementCandidate2{}, cache.CandidateLocations2(transport, scope, digest, false)) + } + } + } +} + +// candidate is a shorthand for types.BICReplacementCandidate +type candidate struct { + d digest.Digest + cn string + lr string +} + +func assertCandidatesMatch(t *testing.T, scopeName string, expected []candidate, actual []types.BICReplacementCandidate) { + e := make([]types.BICReplacementCandidate, len(expected)) + for i, ev := range expected { + e[i] = types.BICReplacementCandidate{Digest: ev.d, Location: types.BICLocationReference{Opaque: scopeName + ev.lr}} + } + assert.Equal(t, e, actual) +} + +func assertCandidatesMatch2(t *testing.T, scopeName string, expected []candidate, actual []blobinfocache.BICReplacementCandidate2) { + e := make([]blobinfocache.BICReplacementCandidate2, len(expected)) + for i, ev := range expected { + e[i] = blobinfocache.BICReplacementCandidate2{Digest: ev.d, CompressorName: ev.cn, Location: types.BICLocationReference{Opaque: scopeName + ev.lr}} + } + assert.Equal(t, e, actual) +} + +func testGenericCandidateLocations(t *testing.T, cache blobinfocache.BlobInfoCache2) { + transport := mocks.NameImageTransport("==BlobInfocache transport mock") + cache.RecordDigestUncompressedPair(digestCompressedA, digestUncompressed) + cache.RecordDigestUncompressedPair(digestCompressedB, digestUncompressed) + cache.RecordDigestUncompressedPair(digestUncompressed, digestUncompressed) + digestNameSet := []struct { + n string + d digest.Digest + }{ + {"U", digestUncompressed}, + {"A", digestCompressedA}, + {"B", digestCompressedB}, + {"CU", digestCompressedUnrelated}, + } + + for _, scopeName := range []string{"A", "B"} { // Run the test in two different scopes to verify they don't affect each other. + scope := types.BICTransportScope{Opaque: scopeName} + // Nothing is known. + assert.Equal(t, []types.BICReplacementCandidate{}, cache.CandidateLocations(transport, scope, digestUnknown, false)) + assert.Equal(t, []types.BICReplacementCandidate{}, cache.CandidateLocations(transport, scope, digestUnknown, true)) + + // Record "2" entries before "1" entries; then results should sort "1" (more recent) before "2" (older) + for _, suffix := range []string{"2", "1"} { + for _, e := range digestNameSet { + cache.RecordKnownLocation(transport, scope, e.d, types.BICLocationReference{Opaque: scopeName + e.n + suffix}) + } + } + + // No substitutions allowed: + for _, e := range digestNameSet { + assertCandidatesMatch(t, scopeName, []candidate{ + {d: e.d, lr: e.n + "1"}, {d: e.d, lr: e.n + "2"}, + }, cache.CandidateLocations(transport, scope, e.d, false)) + } + + // With substitutions: The original digest is always preferred, then other compressed, then the uncompressed one. + assertCandidatesMatch(t, scopeName, []candidate{ + {d: digestCompressedA, lr: "A1"}, {d: digestCompressedA, lr: "A2"}, + {d: digestCompressedB, lr: "B1"}, {d: digestCompressedB, lr: "B2"}, + {d: digestUncompressed, lr: "U1"}, // Beyond the replacementAttempts limit: {d: digestUncompressed, lr: "U2"}, + }, cache.CandidateLocations(transport, scope, digestCompressedA, true)) + + assertCandidatesMatch(t, scopeName, []candidate{ + {d: digestCompressedB, lr: "B1"}, {d: digestCompressedB, lr: "B2"}, + {d: digestCompressedA, lr: "A1"}, {d: digestCompressedA, lr: "A2"}, + {d: digestUncompressed, lr: "U1"}, // Beyond the replacementAttempts limit: {d: digestUncompressed, lr: "U2"}, + }, cache.CandidateLocations(transport, scope, digestCompressedB, true)) + + assertCandidatesMatch(t, scopeName, []candidate{ + {d: digestUncompressed, lr: "U1"}, {d: digestUncompressed, lr: "U2"}, + // "1" entries were added after "2", and A/Bs are sorted in the reverse of digestNameSet order + {d: digestCompressedB, lr: "B1"}, + {d: digestCompressedA, lr: "A1"}, + {d: digestCompressedB, lr: "B2"}, + // Beyond the replacementAttempts limit: {d: digestCompressedA, lr: "A2"}, + }, cache.CandidateLocations(transport, scope, digestUncompressed, true)) + + // Locations are known, but no relationships + assertCandidatesMatch(t, scopeName, []candidate{ + {d: digestCompressedUnrelated, lr: "CU1"}, {d: digestCompressedUnrelated, lr: "CU2"}, + }, cache.CandidateLocations(transport, scope, digestCompressedUnrelated, true)) + } +} + +func testGenericCandidateLocations2(t *testing.T, cache blobinfocache.BlobInfoCache2) { + transport := mocks.NameImageTransport("==BlobInfocache transport mock") + cache.RecordDigestUncompressedPair(digestCompressedA, digestUncompressed) + cache.RecordDigestUncompressedPair(digestCompressedB, digestUncompressed) + cache.RecordDigestUncompressedPair(digestUncompressed, digestUncompressed) + digestNameSet := []struct { + n string + d digest.Digest + m string + }{ + {"U", digestUncompressed, compressorNameU}, + {"A", digestCompressedA, compressorNameA}, + {"B", digestCompressedB, compressorNameB}, + {"CU", digestCompressedUnrelated, compressorNameCU}, + } + + for scopeIndex, scopeName := range []string{"A", "B", "C"} { // Run the test in two different scopes to verify they don't affect each other. + scope := types.BICTransportScope{Opaque: scopeName} + + // Nothing is known. + assert.Equal(t, []blobinfocache.BICReplacementCandidate2{}, cache.CandidateLocations2(transport, scope, digestUnknown, false)) + assert.Equal(t, []blobinfocache.BICReplacementCandidate2{}, cache.CandidateLocations2(transport, scope, digestUnknown, true)) + + // Record "2" entries before "1" entries; then results should sort "1" (more recent) before "2" (older) + for _, suffix := range []string{"2", "1"} { + for _, e := range digestNameSet { + cache.RecordKnownLocation(transport, scope, e.d, types.BICLocationReference{Opaque: scopeName + e.n + suffix}) + } + } + + // Clear any "known" compression values, except on the first loop where they've never been set + if scopeIndex != 0 { + for _, e := range digestNameSet { + cache.RecordDigestCompressorName(e.d, blobinfocache.UnknownCompression) + } + } + + // No substitutions allowed: + for _, e := range digestNameSet { + assertCandidatesMatch(t, scopeName, []candidate{ + {d: e.d, lr: e.n + "1"}, + {d: e.d, lr: e.n + "2"}, + }, cache.CandidateLocations(transport, scope, e.d, false)) + assertCandidatesMatch2(t, scopeName, []candidate{}, cache.CandidateLocations2(transport, scope, e.d, false)) + } + + // With substitutions: The original digest is always preferred, then other compressed, then the uncompressed one. + assertCandidatesMatch(t, scopeName, []candidate{ + {d: digestCompressedA, lr: "A1"}, + {d: digestCompressedA, lr: "A2"}, + {d: digestCompressedB, lr: "B1"}, + {d: digestCompressedB, lr: "B2"}, + {d: digestUncompressed, lr: "U1"}, + // Beyond the replacementAttempts limit: {d: digestUncompressed, cn: compressorNameCU, lr: "U2"}, + }, cache.CandidateLocations(transport, scope, digestCompressedA, true)) + // Unknown compression -> no candidates + assertCandidatesMatch2(t, scopeName, []candidate{}, cache.CandidateLocations2(transport, scope, digestCompressedA, true)) + + assertCandidatesMatch(t, scopeName, []candidate{ + {d: digestCompressedB, lr: "B1"}, + {d: digestCompressedB, lr: "B2"}, + {d: digestCompressedA, lr: "A1"}, + {d: digestCompressedA, lr: "A2"}, + {d: digestUncompressed, lr: "U1"}, // Beyond the replacementAttempts limit: {d: digestUncompressed, lr: "U2"}, + }, cache.CandidateLocations(transport, scope, digestCompressedB, true)) + // Unknown compression -> no candidates + assertCandidatesMatch2(t, scopeName, []candidate{}, cache.CandidateLocations2(transport, scope, digestCompressedB, true)) + + assertCandidatesMatch(t, scopeName, []candidate{ + {d: digestUncompressed, lr: "U1"}, + {d: digestUncompressed, lr: "U2"}, + // "1" entries were added after "2", and A/Bs are sorted in the reverse of digestNameSet order + {d: digestCompressedB, lr: "B1"}, + {d: digestCompressedA, lr: "A1"}, + {d: digestCompressedB, lr: "B2"}, + // Beyond the replacementAttempts limit: {d: digestCompressedA, lr: "A2"}, + }, cache.CandidateLocations(transport, scope, digestUncompressed, true)) + // Unknown compression -> no candidates + assertCandidatesMatch2(t, scopeName, []candidate{}, cache.CandidateLocations2(transport, scope, digestUncompressed, true)) + + // Locations are known, but no relationships + assertCandidatesMatch(t, scopeName, []candidate{ + {d: digestCompressedUnrelated, lr: "CU1"}, + {d: digestCompressedUnrelated, lr: "CU2"}, + }, cache.CandidateLocations(transport, scope, digestCompressedUnrelated, true)) + // Unknown compression -> no candidates + assertCandidatesMatch2(t, scopeName, []candidate{}, cache.CandidateLocations2(transport, scope, digestCompressedUnrelated, true)) + + // Set the "known" compression values + for _, e := range digestNameSet { + cache.RecordDigestCompressorName(e.d, e.m) + } + + // No substitutions allowed: + for _, e := range digestNameSet { + assertCandidatesMatch(t, scopeName, []candidate{ + {d: e.d, lr: e.n + "1"}, + {d: e.d, lr: e.n + "2"}, + }, cache.CandidateLocations(transport, scope, e.d, false)) + assertCandidatesMatch2(t, scopeName, []candidate{ + {d: e.d, cn: e.m, lr: e.n + "1"}, + {d: e.d, cn: e.m, lr: e.n + "2"}, + }, cache.CandidateLocations2(transport, scope, e.d, false)) + } + + // With substitutions: The original digest is always preferred, then other compressed, then the uncompressed one. + assertCandidatesMatch(t, scopeName, []candidate{ + {d: digestCompressedA, lr: "A1"}, + {d: digestCompressedA, lr: "A2"}, + {d: digestCompressedB, lr: "B1"}, + {d: digestCompressedB, lr: "B2"}, + {d: digestUncompressed, lr: "U1"}, + // Beyond the replacementAttempts limit: {d: digestUncompressed, lr: "U2"}, + }, cache.CandidateLocations(transport, scope, digestCompressedA, true)) + assertCandidatesMatch2(t, scopeName, []candidate{ + {d: digestCompressedA, cn: compressorNameA, lr: "A1"}, + {d: digestCompressedA, cn: compressorNameA, lr: "A2"}, + {d: digestCompressedB, cn: compressorNameB, lr: "B1"}, + {d: digestCompressedB, cn: compressorNameB, lr: "B2"}, + {d: digestUncompressed, cn: compressorNameU, lr: "U1"}, + // Beyond the replacementAttempts limit: {d: digestUncompressed, cn: compressorNameCU, lr: "U2"}, + }, cache.CandidateLocations2(transport, scope, digestCompressedA, true)) + + assertCandidatesMatch(t, scopeName, []candidate{ + {d: digestCompressedB, lr: "B1"}, + {d: digestCompressedB, lr: "B2"}, + {d: digestCompressedA, lr: "A1"}, + {d: digestCompressedA, lr: "A2"}, + {d: digestUncompressed, lr: "U1"}, // Beyond the replacementAttempts limit: {d: digestUncompressed, lr: "U2"}, + }, cache.CandidateLocations(transport, scope, digestCompressedB, true)) + assertCandidatesMatch2(t, scopeName, []candidate{ + {d: digestCompressedB, cn: compressorNameB, lr: "B1"}, + {d: digestCompressedB, cn: compressorNameB, lr: "B2"}, + {d: digestCompressedA, cn: compressorNameA, lr: "A1"}, + {d: digestCompressedA, cn: compressorNameA, lr: "A2"}, + {d: digestUncompressed, cn: compressorNameU, lr: "U1"}, // Beyond the replacementAttempts limit: {d: digestUncompressed, cn: compressorNameU, lr: "U2"}, + }, cache.CandidateLocations2(transport, scope, digestCompressedB, true)) + + assertCandidatesMatch(t, scopeName, []candidate{ + {d: digestUncompressed, lr: "U1"}, + {d: digestUncompressed, lr: "U2"}, + // "1" entries were added after "2", and A/Bs are sorted in the reverse of digestNameSet order + {d: digestCompressedB, lr: "B1"}, + {d: digestCompressedA, lr: "A1"}, + {d: digestCompressedB, lr: "B2"}, + // Beyond the replacementAttempts limit: {d: digestCompressedA, lr: "A2"}, + }, cache.CandidateLocations(transport, scope, digestUncompressed, true)) + assertCandidatesMatch2(t, scopeName, []candidate{ + {d: digestUncompressed, cn: compressorNameU, lr: "U1"}, + {d: digestUncompressed, cn: compressorNameU, lr: "U2"}, + // "1" entries were added after "2", and A/Bs are sorted in the reverse of digestNameSet order + {d: digestCompressedB, cn: compressorNameB, lr: "B1"}, + {d: digestCompressedA, cn: compressorNameA, lr: "A1"}, + {d: digestCompressedB, cn: compressorNameB, lr: "B2"}, + // Beyond the replacementAttempts limit: {d: digestCompressedA, cn: compressorNameA, lr: "A2"}, + }, cache.CandidateLocations2(transport, scope, digestUncompressed, true)) + + // Locations are known, but no relationships + assertCandidatesMatch(t, scopeName, []candidate{ + {d: digestCompressedUnrelated, lr: "CU1"}, + {d: digestCompressedUnrelated, lr: "CU2"}, + }, cache.CandidateLocations(transport, scope, digestCompressedUnrelated, true)) + assertCandidatesMatch2(t, scopeName, []candidate{ + {d: digestCompressedUnrelated, cn: compressorNameCU, lr: "CU1"}, + {d: digestCompressedUnrelated, cn: compressorNameCU, lr: "CU2"}, + }, cache.CandidateLocations2(transport, scope, digestCompressedUnrelated, true)) + } +} diff --git a/vendor/github.com/containers/image/v5/pkg/blobinfocache/memory/memory_test.go b/vendor/github.com/containers/image/v5/pkg/blobinfocache/memory/memory_test.go new file mode 100644 index 00000000000..322f008f31c --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/blobinfocache/memory/memory_test.go @@ -0,0 +1,18 @@ +package memory + +import ( + "testing" + + "github.com/containers/image/v5/internal/blobinfocache" + "github.com/containers/image/v5/pkg/blobinfocache/internal/test" +) + +var _ blobinfocache.BlobInfoCache2 = &cache{} + +func newTestCache(t *testing.T) blobinfocache.BlobInfoCache2 { + return new2() +} + +func TestNew(t *testing.T) { + test.GenericCache(t, newTestCache) +} diff --git a/vendor/github.com/containers/image/v5/pkg/blobinfocache/none/none_test.go b/vendor/github.com/containers/image/v5/pkg/blobinfocache/none/none_test.go new file mode 100644 index 00000000000..57d20109eec --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/blobinfocache/none/none_test.go @@ -0,0 +1,7 @@ +package none + +import ( + "github.com/containers/image/v5/types" +) + +var _ types.BlobInfoCache = &noCache{} diff --git a/vendor/github.com/containers/image/v5/pkg/cli/environment/environment.go b/vendor/github.com/containers/image/v5/pkg/cli/environment/environment.go new file mode 100644 index 00000000000..1126283f8de --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/cli/environment/environment.go @@ -0,0 +1,31 @@ +package environment + +import ( + "errors" + "os" + + "github.com/containers/image/v5/types" +) + +// UpdateRegistriesConf sets the SystemRegistriesConfPath in the system +// context, unless already set. Possible values are, in priority and only if +// set, the CONTAINERS_REGISTRIES_CONF or REGISTRIES_CONFIG_PATH environment +// variable. +func UpdateRegistriesConf(sys *types.SystemContext) error { + if sys == nil { + return errors.New("internal error: UpdateRegistriesConf: nil argument") + } + if sys.SystemRegistriesConfPath != "" { + return nil + } + if envOverride, ok := os.LookupEnv("CONTAINERS_REGISTRIES_CONF"); ok { + sys.SystemRegistriesConfPath = envOverride + return nil + } + if envOverride, ok := os.LookupEnv("REGISTRIES_CONFIG_PATH"); ok { + sys.SystemRegistriesConfPath = envOverride + return nil + } + + return nil +} diff --git a/vendor/github.com/containers/image/v5/pkg/cli/passphrase.go b/vendor/github.com/containers/image/v5/pkg/cli/passphrase.go new file mode 100644 index 00000000000..c46650cdcfa --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/cli/passphrase.go @@ -0,0 +1,36 @@ +package cli + +import ( + "bufio" + "errors" + "fmt" + "io" + "os" + "strings" + + "github.com/sirupsen/logrus" +) + +// ReadPassphraseFile returns the first line of the specified path. +// For convenience, an empty string is returned if the path is empty. +func ReadPassphraseFile(path string) (string, error) { + if path == "" { + return "", nil + } + + logrus.Debugf("Reading user-specified passphrase for signing from %s", path) + + ppf, err := os.Open(path) + if err != nil { + return "", err + } + defer ppf.Close() + + // Read the *first* line in the passphrase file, just as gpg(1) does. + buf, err := bufio.NewReader(ppf).ReadBytes('\n') + if err != nil && !errors.Is(err, io.EOF) { + return "", fmt.Errorf("reading passphrase file: %w", err) + } + + return strings.TrimSuffix(string(buf), "\n"), nil +} diff --git a/vendor/github.com/containers/image/v5/pkg/cli/sigstore/params/sigstore.go b/vendor/github.com/containers/image/v5/pkg/cli/sigstore/params/sigstore.go new file mode 100644 index 00000000000..0151b9acb02 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/cli/sigstore/params/sigstore.go @@ -0,0 +1,75 @@ +package params + +import ( + "bytes" + "fmt" + "os" + + "gopkg.in/yaml.v3" +) + +// SigningParameterFile collects parameters used for creating sigstore signatures. +// +// To consume such a file, most callers should use c/image/pkg/cli/sigstore instead +// of dealing with this type explicitly using ParseFile. +// +// This type is exported primarily to allow creating parameter files programmatically +// (and eventually this subpackage should provide an API to convert this type into +// the appropriate file contents, so that callers don’t need to do that manually). +type SigningParameterFile struct { + // Keep this in sync with docs/containers-sigstore-signing-params.yaml.5.md ! + + PrivateKeyFile string `yaml:"privateKeyFile,omitempty"` // If set, sign using a private key stored in this file. + PrivateKeyPassphraseFile string `yaml:"privateKeyPassphraseFile,omitempty"` // A file that contains the passprase required for PrivateKeyFile. + + Fulcio *SigningParameterFileFulcio `yaml:"fulcio,omitempty"` // If set, sign using a short-lived key and a Fulcio-issued certificate. + + RekorURL string `yaml:"rekorURL,omitempty"` // If set, upload the signature to the specified Rekor server, and include a log inclusion proof in the signature. +} + +// SigningParameterFileFulcio is a subset of SigningParameterFile dedicated to Fulcio parameters. +type SigningParameterFileFulcio struct { + // Keep this in sync with docs/containers-sigstore-signing-params.yaml.5.md ! + + FulcioURL string `yaml:"fulcioURL,omitempty"` // URL of the Fulcio server. Required. + + // How to obtain the OIDC ID token required by Fulcio. Required. + OIDCMode OIDCMode `yaml:"oidcMode,omitempty"` + + // oidcMode = staticToken + OIDCIDToken string `yaml:"oidcIDToken,omitempty"` + + // oidcMode = deviceGrant || interactive + OIDCIssuerURL string `yaml:"oidcIssuerURL,omitempty"` // + OIDCClientID string `yaml:"oidcClientID,omitempty"` + OIDCClientSecret string `yaml:"oidcClientSecret,omitempty"` +} + +type OIDCMode string + +const ( + // OIDCModeStaticToken means the parameter file contains an user-provided OIDC ID token value. + OIDCModeStaticToken OIDCMode = "staticToken" + // OIDCModeDeviceGrant specifies the OIDC ID token should be obtained using a device authorization grant (RFC 8628). + OIDCModeDeviceGrant OIDCMode = "deviceGrant" + // OIDCModeInteractive specifies the OIDC ID token should be obtained interactively (automatically opening a browser, + // or interactively prompting the user.) + OIDCModeInteractive OIDCMode = "interactive" +) + +// ParseFile parses a SigningParameterFile at the specified path. +// +// Most consumers of the parameter file should use c/image/pkg/cli/sigstore to obtain a *signer.Signer instead. +func ParseFile(path string) (*SigningParameterFile, error) { + var res SigningParameterFile + source, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("reading %q: %w", path, err) + } + dec := yaml.NewDecoder(bytes.NewReader(source)) + dec.KnownFields(true) + if err = dec.Decode(&res); err != nil { + return nil, fmt.Errorf("parsing %q: %w", path, err) + } + return &res, nil +} diff --git a/vendor/github.com/containers/image/v5/pkg/cli/sigstore/sigstore.go b/vendor/github.com/containers/image/v5/pkg/cli/sigstore/sigstore.go new file mode 100644 index 00000000000..62520c212f1 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/cli/sigstore/sigstore.go @@ -0,0 +1,117 @@ +package sigstore + +import ( + "errors" + "fmt" + "io" + "net/url" + + "github.com/containers/image/v5/pkg/cli" + "github.com/containers/image/v5/pkg/cli/sigstore/params" + "github.com/containers/image/v5/signature/signer" + "github.com/containers/image/v5/signature/sigstore" + "github.com/containers/image/v5/signature/sigstore/fulcio" + "github.com/containers/image/v5/signature/sigstore/rekor" +) + +// Options collects data that the caller should provide to NewSignerFromParameterFile. +// The caller should set all fields unless documented otherwise. +type Options struct { + PrivateKeyPassphrasePrompt func(keyFile string) (string, error) // A function to call to interactively prompt for a passphrase + Stdin io.Reader + Stdout io.Writer +} + +// NewSignerFromParameterFile returns a signature.Signer which creates sigstore signatures based a parameter file at the specified path. +// +// The caller must call Close() on the returned Signer. +func NewSignerFromParameterFile(path string, options *Options) (*signer.Signer, error) { + params, err := params.ParseFile(path) + if err != nil { + return nil, fmt.Errorf("setting up signing using parameter file %q: %w", path, err) + } + return newSignerFromParameterData(params, options) +} + +// newSignerFromParameterData returns a signature.Signer which creates sigstore signatures based on parameter file contents. +// +// The caller must call Close() on the returned Signer. +func newSignerFromParameterData(params *params.SigningParameterFile, options *Options) (*signer.Signer, error) { + opts := []sigstore.Option{} + if params.PrivateKeyFile != "" { + var getPassphrase func(keyFile string) (string, error) + switch { + case params.PrivateKeyPassphraseFile != "": + getPassphrase = func(_ string) (string, error) { + return cli.ReadPassphraseFile(params.PrivateKeyPassphraseFile) + } + case options.PrivateKeyPassphrasePrompt != nil: + getPassphrase = options.PrivateKeyPassphrasePrompt + default: // This shouldn’t happen, the caller is expected to set options.PrivateKeyPassphrasePrompt + return nil, fmt.Errorf("private key %s specified, but no way to get a passphrase", params.PrivateKeyFile) + } + passphrase, err := getPassphrase(params.PrivateKeyFile) + if err != nil { + return nil, err + } + opts = append(opts, sigstore.WithPrivateKeyFile(params.PrivateKeyFile, []byte(passphrase))) + } + + if params.Fulcio != nil { + fulcioOpt, err := fulcioOption(params.Fulcio, options) + if err != nil { + return nil, err + } + opts = append(opts, fulcioOpt) + } + + if params.RekorURL != "" { + rekorURL, err := url.Parse(params.RekorURL) + if err != nil { + return nil, fmt.Errorf("parsing rekorURL %q: %w", params.RekorURL, err) + } + opts = append(opts, rekor.WithRekor(rekorURL)) + } + + return sigstore.NewSigner(opts...) +} + +// fulcioOption returns a sigstore.Option for Fulcio use based on f. +func fulcioOption(f *params.SigningParameterFileFulcio, options *Options) (sigstore.Option, error) { + if f.FulcioURL == "" { + return nil, errors.New("missing fulcioURL") + } + fulcioURL, err := url.Parse(f.FulcioURL) + if err != nil { + return nil, fmt.Errorf("parsing fulcioURL %q: %w", f.FulcioURL, err) + } + + if f.OIDCMode == params.OIDCModeStaticToken { + if f.OIDCIDToken == "" { + return nil, errors.New("missing oidcToken") + } + return fulcio.WithFulcioAndPreexistingOIDCIDToken(fulcioURL, f.OIDCIDToken), nil + } + + if f.OIDCIssuerURL == "" { + return nil, errors.New("missing oidcIssuerURL") + } + oidcIssuerURL, err := url.Parse(f.OIDCIssuerURL) + if err != nil { + return nil, fmt.Errorf("parsing oidcIssuerURL %q: %w", f.OIDCIssuerURL, err) + } + switch f.OIDCMode { + case params.OIDCModeDeviceGrant: + return fulcio.WithFulcioAndDeviceAuthorizationGrantOIDC(fulcioURL, oidcIssuerURL, f.OIDCClientID, f.OIDCClientSecret, + options.Stdout), nil + case params.OIDCModeInteractive: + return fulcio.WithFulcioAndInteractiveOIDC(fulcioURL, oidcIssuerURL, f.OIDCClientID, f.OIDCClientSecret, + options.Stdin, options.Stdout), nil + case "": + return nil, errors.New("missing oidcMode") + case params.OIDCModeStaticToken: + return nil, errors.New("internal inconsistency: SigningParameterFileOIDCModeStaticToken was supposed to already be handled") + default: + return nil, fmt.Errorf("unknown oidcMode value %q", f.OIDCMode) + } +} diff --git a/vendor/github.com/containers/image/v5/pkg/compression/compression_test.go b/vendor/github.com/containers/image/v5/pkg/compression/compression_test.go new file mode 100644 index 00000000000..4eeff956e86 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/compression/compression_test.go @@ -0,0 +1,128 @@ +package compression + +import ( + "bytes" + "errors" + "io" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDetectCompression(t *testing.T) { + cases := []string{ + "fixtures/Hello.uncompressed", + "fixtures/Hello.gz", + "fixtures/Hello.bz2", + "fixtures/Hello.xz", + "fixtures/Hello.zst", + } + + // The original stream is preserved. + for _, c := range cases { + originalContents, err := os.ReadFile(c) + require.NoError(t, err, c) + + stream, err := os.Open(c) + require.NoError(t, err, c) + defer stream.Close() + + _, updatedStream, err := DetectCompression(stream) + require.NoError(t, err, c) + + updatedContents, err := io.ReadAll(updatedStream) + require.NoError(t, err, c) + assert.Equal(t, originalContents, updatedContents, c) + } + + // The correct decompressor is chosen, and the result is as expected. + for _, c := range cases { + stream, err := os.Open(c) + require.NoError(t, err, c) + defer stream.Close() + + decompressor, updatedStream, err := DetectCompression(stream) + require.NoError(t, err, c) + + var uncompressedStream io.Reader + if decompressor == nil { + uncompressedStream = updatedStream + } else { + s, err := decompressor(updatedStream) + require.NoError(t, err) + defer s.Close() + uncompressedStream = s + } + + uncompressedContents, err := io.ReadAll(uncompressedStream) + require.NoError(t, err, c) + assert.Equal(t, []byte("Hello"), uncompressedContents, c) + } + + // Empty input is handled reasonably. + decompressor, updatedStream, err := DetectCompression(bytes.NewReader([]byte{})) + require.NoError(t, err) + assert.Nil(t, decompressor) + updatedContents, err := io.ReadAll(updatedStream) + require.NoError(t, err) + assert.Equal(t, []byte{}, updatedContents) + + // Error reading input + reader, writer := io.Pipe() + defer reader.Close() + err = writer.CloseWithError(errors.New("Expected error reading input in DetectCompression")) + assert.NoError(t, err) + _, _, err = DetectCompression(reader) + assert.Error(t, err) +} + +func TestAutoDecompress(t *testing.T) { + cases := []struct { + filename string + isCompressed bool + }{ + {"fixtures/Hello.uncompressed", false}, + {"fixtures/Hello.gz", true}, + {"fixtures/Hello.bz2", true}, + {"fixtures/Hello.xz", true}, + } + + // The correct decompressor is chosen, and the result is as expected. + for _, c := range cases { + stream, err := os.Open(c.filename) + require.NoError(t, err, c.filename) + defer stream.Close() + + uncompressedStream, isCompressed, err := AutoDecompress(stream) + require.NoError(t, err, c.filename) + defer uncompressedStream.Close() + + assert.Equal(t, c.isCompressed, isCompressed) + + uncompressedContents, err := io.ReadAll(uncompressedStream) + require.NoError(t, err, c.filename) + assert.Equal(t, []byte("Hello"), uncompressedContents, c.filename) + } + + // Empty input is handled reasonably. + uncompressedStream, isCompressed, err := AutoDecompress(bytes.NewReader([]byte{})) + require.NoError(t, err) + assert.False(t, isCompressed) + uncompressedContents, err := io.ReadAll(uncompressedStream) + require.NoError(t, err) + assert.Equal(t, []byte{}, uncompressedContents) + + // Error initializing a decompressor (for a detected format) + _, _, err = AutoDecompress(bytes.NewReader([]byte{0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00})) + assert.Error(t, err) + + // Error reading input + reader, writer := io.Pipe() + defer reader.Close() + err = writer.CloseWithError(errors.New("Expected error reading input in AutoDecompress")) + require.NoError(t, err) + _, _, err = AutoDecompress(reader) + assert.Error(t, err) +} diff --git a/vendor/github.com/containers/image/v5/pkg/compression/fixtures/Hello.bz2 b/vendor/github.com/containers/image/v5/pkg/compression/fixtures/Hello.bz2 new file mode 100644 index 00000000000..e822f5e5e9e Binary files /dev/null and b/vendor/github.com/containers/image/v5/pkg/compression/fixtures/Hello.bz2 differ diff --git a/vendor/github.com/containers/image/v5/pkg/compression/fixtures/Hello.gz b/vendor/github.com/containers/image/v5/pkg/compression/fixtures/Hello.gz new file mode 100644 index 00000000000..22c895b7d17 Binary files /dev/null and b/vendor/github.com/containers/image/v5/pkg/compression/fixtures/Hello.gz differ diff --git a/vendor/github.com/containers/image/v5/pkg/compression/fixtures/Hello.uncompressed b/vendor/github.com/containers/image/v5/pkg/compression/fixtures/Hello.uncompressed new file mode 100644 index 00000000000..5ab2f8a4323 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/compression/fixtures/Hello.uncompressed @@ -0,0 +1 @@ +Hello \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/pkg/compression/fixtures/Hello.xz b/vendor/github.com/containers/image/v5/pkg/compression/fixtures/Hello.xz new file mode 100644 index 00000000000..6e9b0b6648f Binary files /dev/null and b/vendor/github.com/containers/image/v5/pkg/compression/fixtures/Hello.xz differ diff --git a/vendor/github.com/containers/image/v5/pkg/compression/fixtures/Hello.zst b/vendor/github.com/containers/image/v5/pkg/compression/fixtures/Hello.zst new file mode 100644 index 00000000000..02770a67dae Binary files /dev/null and b/vendor/github.com/containers/image/v5/pkg/compression/fixtures/Hello.zst differ diff --git a/vendor/github.com/containers/image/v5/pkg/docker/config/config.go b/vendor/github.com/containers/image/v5/pkg/docker/config/config.go index 2e79d0ffbc3..b987c580606 100644 --- a/vendor/github.com/containers/image/v5/pkg/docker/config/config.go +++ b/vendor/github.com/containers/image/v5/pkg/docker/config/config.go @@ -519,11 +519,12 @@ func getPathToAuthWithOS(sys *types.SystemContext, goOS string) (authPath, bool, if sys.LegacyFormatAuthFilePath != "" { return authPath{path: sys.LegacyFormatAuthFilePath, legacyFormat: true}, true, nil } - if sys.RootForImplicitAbsolutePaths != "" { + // Note: RootForImplicitAbsolutePaths should not affect paths starting with $HOME + if sys.RootForImplicitAbsolutePaths != "" && goOS == "linux" { return newAuthPathDefault(filepath.Join(sys.RootForImplicitAbsolutePaths, fmt.Sprintf(defaultPerUIDPathFormat, os.Getuid()))), false, nil } } - if goOS == "windows" || goOS == "darwin" { + if goOS != "linux" { return newAuthPathDefault(filepath.Join(homedir.Get(), nonLinuxAuthFilePath)), false, nil } diff --git a/vendor/github.com/containers/image/v5/pkg/docker/config/config_test.go b/vendor/github.com/containers/image/v5/pkg/docker/config/config_test.go new file mode 100644 index 00000000000..12975b74288 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/docker/config/config_test.go @@ -0,0 +1,898 @@ +package config + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetPathToAuth(t *testing.T) { + const linux = "linux" + const darwin = "darwin" + const freebsd = "freebsd" + + uid := fmt.Sprintf("%d", os.Getuid()) + // We don’t have to override the home directory for this because use of this path does not depend + // on any state of the filesystem. + darwinDefault := filepath.Join(os.Getenv("HOME"), ".config", "containers", "auth.json") + + tmpDir := t.TempDir() + + for caseIndex, c := range []struct { + sys *types.SystemContext + os string + xrd string + expected string + legacyFormat bool + expectedUserSpecified bool + }{ + // Default paths + {&types.SystemContext{}, linux, "", "/run/containers/" + uid + "/auth.json", false, false}, + {&types.SystemContext{}, darwin, "", darwinDefault, false, false}, + {&types.SystemContext{}, freebsd, "", darwinDefault, false, false}, + {nil, linux, "", "/run/containers/" + uid + "/auth.json", false, false}, + {nil, darwin, "", darwinDefault, false, false}, + {nil, freebsd, "", darwinDefault, false, false}, + // SystemContext overrides + {&types.SystemContext{AuthFilePath: "/absolute/path"}, linux, "", "/absolute/path", false, true}, + {&types.SystemContext{AuthFilePath: "/absolute/path"}, darwin, "", "/absolute/path", false, true}, + {&types.SystemContext{AuthFilePath: "/absolute/path"}, freebsd, "", "/absolute/path", false, true}, + {&types.SystemContext{LegacyFormatAuthFilePath: "/absolute/path"}, linux, "", "/absolute/path", true, true}, + {&types.SystemContext{LegacyFormatAuthFilePath: "/absolute/path"}, darwin, "", "/absolute/path", true, true}, + {&types.SystemContext{LegacyFormatAuthFilePath: "/absolute/path"}, freebsd, "", "/absolute/path", true, true}, + {&types.SystemContext{RootForImplicitAbsolutePaths: "/prefix"}, linux, "", "/prefix/run/containers/" + uid + "/auth.json", false, false}, + {&types.SystemContext{RootForImplicitAbsolutePaths: "/prefix"}, darwin, "", darwinDefault, false, false}, + {&types.SystemContext{RootForImplicitAbsolutePaths: "/prefix"}, freebsd, "", darwinDefault, false, false}, + // XDG_RUNTIME_DIR defined + {nil, linux, tmpDir, tmpDir + "/containers/auth.json", false, false}, + {nil, darwin, tmpDir, darwinDefault, false, false}, + {nil, freebsd, tmpDir, darwinDefault, false, false}, + {nil, linux, tmpDir + "/thisdoesnotexist", "", false, false}, + {nil, darwin, tmpDir + "/thisdoesnotexist", darwinDefault, false, false}, + {nil, freebsd, tmpDir + "/thisdoesnotexist", darwinDefault, false, false}, + } { + t.Run(fmt.Sprintf("%d", caseIndex), func(t *testing.T) { + // Always use t.Setenv() to ensure XDG_RUNTIME_DIR is restored to the original value after the test. + // Then, in cases where the test needs XDG_RUNTIME_DIR unset (not just set to empty), use a raw os.Unsetenv() + // to override the situation. (Sadly there isn’t a t.Unsetenv() as of Go 1.17.) + t.Setenv("XDG_RUNTIME_DIR", c.xrd) + if c.xrd == "" { + os.Unsetenv("XDG_RUNTIME_DIR") + } + res, userSpecified, err := getPathToAuthWithOS(c.sys, c.os) + if c.expected == "" { + assert.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, authPath{path: c.expected, legacyFormat: c.legacyFormat}, res) + assert.Equal(t, c.expectedUserSpecified, userSpecified) + } + }) + } +} + +func TestGetAuth(t *testing.T) { + tmpXDGRuntimeDir := t.TempDir() + t.Logf("using temporary XDG_RUNTIME_DIR directory: %q", tmpXDGRuntimeDir) + t.Setenv("XDG_RUNTIME_DIR", tmpXDGRuntimeDir) + + // override PATH for executing credHelper + curtDir, err := os.Getwd() + require.NoError(t, err) + origPath := os.Getenv("PATH") + newPath := fmt.Sprintf("%s:%s", filepath.Join(curtDir, "testdata"), origPath) + t.Setenv("PATH", newPath) + t.Logf("using PATH: %q", newPath) + + tmpHomeDir := t.TempDir() + t.Logf("using temporary home directory: %q", tmpHomeDir) + + configDir1 := filepath.Join(tmpXDGRuntimeDir, "containers") + if runtime.GOOS != "linux" { + configDir1 = filepath.Join(tmpHomeDir, ".config", "containers") + } + if err := os.MkdirAll(configDir1, 0700); err != nil { + t.Fatal(err) + } + configDir2 := filepath.Join(tmpHomeDir, ".docker") + if err := os.MkdirAll(configDir2, 0700); err != nil { + t.Fatal(err) + } + configPaths := [2]string{filepath.Join(configDir1, "auth.json"), filepath.Join(configDir2, "config.json")} + + for _, configPath := range configPaths { + for _, tc := range []struct { + name string + key string + path string + expected types.DockerAuthConfig + sys *types.SystemContext + }{ + { + name: "no auth config", + key: "index.docker.io", + expected: types.DockerAuthConfig{}, + }, + { + name: "empty hostname", + path: filepath.Join("testdata", "example.json"), + expected: types.DockerAuthConfig{}, + }, + { + name: "match one", + key: "example.org", + path: filepath.Join("testdata", "example.json"), + expected: types.DockerAuthConfig{Username: "example", Password: "org"}, + }, + { + name: "match none", + key: "registry.example.org", + path: filepath.Join("testdata", "example.json"), + expected: types.DockerAuthConfig{}, + }, + { + name: "match docker.io", + key: "docker.io", + path: filepath.Join("testdata", "full.json"), + expected: types.DockerAuthConfig{Username: "docker", Password: "io"}, + }, + { + name: "match docker.io normalized", + key: "docker.io", + path: filepath.Join("testdata", "abnormal.json"), + expected: types.DockerAuthConfig{Username: "index", Password: "docker.io"}, + }, + { + name: "normalize registry", + key: "normalize.example.org", + path: filepath.Join("testdata", "full.json"), + expected: types.DockerAuthConfig{Username: "normalize", Password: "example"}, + }, + { + name: "match localhost", + key: "localhost", + path: filepath.Join("testdata", "full.json"), + expected: types.DockerAuthConfig{Username: "local", Password: "host"}, + }, + { + name: "match ip", + key: "10.10.30.45:5000", + path: filepath.Join("testdata", "full.json"), + expected: types.DockerAuthConfig{Username: "10.10", Password: "30.45-5000"}, + }, + { + name: "match port", + key: "localhost:5000", + path: filepath.Join("testdata", "abnormal.json"), + expected: types.DockerAuthConfig{Username: "local", Password: "host-5000"}, + }, + { + name: "use system context", + key: "example.org", + path: filepath.Join("testdata", "example.json"), + expected: types.DockerAuthConfig{Username: "foo", Password: "bar"}, + sys: &types.SystemContext{ + DockerAuthConfig: &types.DockerAuthConfig{ + Username: "foo", + Password: "bar", + }, + }, + }, + { + name: "identity token", + key: "example.org", + path: filepath.Join("testdata", "example_identitytoken.json"), + expected: types.DockerAuthConfig{ + Username: "00000000-0000-0000-0000-000000000000", + Password: "", + IdentityToken: "some very long identity token", + }, + }, + { + name: "match none (empty.json)", + key: "localhost:5000", + path: filepath.Join("testdata", "empty.json"), + expected: types.DockerAuthConfig{}, + }, + { + name: "credhelper from registries.conf", + key: "registry-a.com", + sys: &types.SystemContext{ + SystemRegistriesConfPath: filepath.Join("testdata", "cred-helper.conf"), + SystemRegistriesConfDirPath: filepath.Join("testdata", "IdoNotExist"), + }, + expected: types.DockerAuthConfig{Username: "foo", Password: "bar"}, + }, + { + name: "identity token credhelper from registries.conf", + key: "registry-b.com", + sys: &types.SystemContext{ + SystemRegistriesConfPath: filepath.Join("testdata", "cred-helper.conf"), + SystemRegistriesConfDirPath: filepath.Join("testdata", "IdoNotExist"), + }, + expected: types.DockerAuthConfig{IdentityToken: "fizzbuzz"}, + }, + { + name: "match ref image", + key: "example.org/repo/image", + path: filepath.Join("testdata", "refpath.json"), + expected: types.DockerAuthConfig{Username: "example", Password: "org"}, + }, + { + name: "match ref repo", + key: "example.org/repo", + path: filepath.Join("testdata", "refpath.json"), + expected: types.DockerAuthConfig{Username: "example", Password: "org"}, + }, + { + name: "match ref host", + key: "example.org/image", + path: filepath.Join("testdata", "refpath.json"), + expected: types.DockerAuthConfig{Username: "local", Password: "host"}, + }, + // Test matching of docker.io/[library/] explicitly, to make sure the docker.io + // normalization behavior doesn’t affect the semantics. + { + name: "docker.io library repo match", + key: "docker.io/library/busybox", + path: filepath.Join("testdata", "refpath.json"), + expected: types.DockerAuthConfig{Username: "library", Password: "busybox"}, + }, + { + name: "docker.io library namespace match", + key: "docker.io/library/notbusybox", + path: filepath.Join("testdata", "refpath.json"), + expected: types.DockerAuthConfig{Username: "library", Password: "other"}, + }, + { // This tests that the docker.io/vendor key in auth file is not normalized to docker.io/library/vendor + name: "docker.io vendor repo match", + key: "docker.io/vendor/product", + path: filepath.Join("testdata", "refpath.json"), + expected: types.DockerAuthConfig{Username: "first", Password: "level"}, + }, + { // This tests that the docker.io/vendor key in the query is not normalized to docker.io/library/vendor. + name: "docker.io vendor namespace match", + key: "docker.io/vendor", + path: filepath.Join("testdata", "refpath.json"), + expected: types.DockerAuthConfig{Username: "first", Password: "level"}, + }, + { + name: "docker.io host-only match", + key: "docker.io/other-vendor/other-product", + path: filepath.Join("testdata", "refpath.json"), + expected: types.DockerAuthConfig{Username: "top", Password: "level"}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + if err := os.RemoveAll(configPath); err != nil { + t.Fatal(err) + } + + if tc.path != "" { + contents, err := os.ReadFile(tc.path) + if err != nil { + t.Fatal(err) + } + + if err := os.WriteFile(configPath, contents, 0640); err != nil { + t.Fatal(err) + } + } + + var sys *types.SystemContext + if tc.sys != nil { + sys = tc.sys + } + + auth, err := getCredentialsWithHomeDir(sys, tc.key, tmpHomeDir) + require.NoError(t, err) + assert.Equal(t, tc.expected, auth) + + // Verify the previous API also returns data consistent with the current one. + username, password, err := getAuthenticationWithHomeDir(sys, tc.key, tmpHomeDir) + if tc.expected.IdentityToken != "" { + assert.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tc.expected.Username, username) + assert.Equal(t, tc.expected.Password, password) + } + + require.NoError(t, os.RemoveAll(configPath)) + }) + } + } +} + +func TestGetAuthFromLegacyFile(t *testing.T) { + tmpDir := t.TempDir() + t.Logf("using temporary home directory: %q", tmpDir) + + configPath := filepath.Join(tmpDir, ".dockercfg") + contents, err := os.ReadFile(filepath.Join("testdata", "legacy.json")) + if err != nil { + t.Fatal(err) + } + + for _, tc := range []struct { + name string + hostname string + expected types.DockerAuthConfig + }{ + { + name: "ignore schema and path", + hostname: "localhost", + expected: types.DockerAuthConfig{ + Username: "local", + Password: "host-legacy", + }, + }, + { + name: "normalize registry", + hostname: "docker.io", + expected: types.DockerAuthConfig{ + Username: "docker", + Password: "io-legacy", + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + if err := os.WriteFile(configPath, contents, 0640); err != nil { + t.Fatal(err) + } + + auth, err := getCredentialsWithHomeDir(nil, tc.hostname, tmpDir) + require.NoError(t, err) + assert.Equal(t, tc.expected, auth) + + // Testing for previous APIs + username, password, err := getAuthenticationWithHomeDir(nil, tc.hostname, tmpDir) + require.NoError(t, err) + assert.Equal(t, tc.expected.Username, username) + assert.Equal(t, tc.expected.Password, password) + }) + } +} + +func TestGetAuthPreferNewConfig(t *testing.T) { + tmpDir := t.TempDir() + t.Logf("using temporary home directory: %q", tmpDir) + + configDir := filepath.Join(tmpDir, ".docker") + if err := os.Mkdir(configDir, 0750); err != nil { + t.Fatal(err) + } + + for _, data := range []struct { + source string + target string + }{ + { + source: filepath.Join("testdata", "full.json"), + target: filepath.Join(configDir, "config.json"), + }, + { + source: filepath.Join("testdata", "legacy.json"), + target: filepath.Join(tmpDir, ".dockercfg"), + }, + } { + contents, err := os.ReadFile(data.source) + if err != nil { + t.Fatal(err) + } + + if err := os.WriteFile(data.target, contents, 0640); err != nil { + t.Fatal(err) + } + } + + auth, err := getCredentialsWithHomeDir(nil, "docker.io", tmpDir) + assert.NoError(t, err) + assert.Equal(t, "docker", auth.Username) + assert.Equal(t, "io", auth.Password) +} + +func TestGetAuthFailsOnBadInput(t *testing.T) { + tmpXDGRuntimeDir := t.TempDir() + t.Logf("using temporary XDG_RUNTIME_DIR directory: %q", tmpXDGRuntimeDir) + t.Setenv("XDG_RUNTIME_DIR", tmpXDGRuntimeDir) + + tmpHomeDir := t.TempDir() + t.Logf("using temporary home directory: %q", tmpHomeDir) + + configDir := filepath.Join(tmpXDGRuntimeDir, "containers") + if runtime.GOOS != "linux" { + configDir = filepath.Join(tmpHomeDir, ".config", "containers") + } + if err := os.MkdirAll(configDir, 0750); err != nil { + t.Fatal(err) + } + configPath := filepath.Join(configDir, "auth.json") + + // no config file present + auth, err := getCredentialsWithHomeDir(nil, "index.docker.io", tmpHomeDir) + if err != nil { + t.Fatalf("got unexpected error: %#+v", err) + } + assert.Equal(t, types.DockerAuthConfig{}, auth) + + if err := os.WriteFile(configPath, []byte("Json rocks! Unless it doesn't."), 0640); err != nil { + t.Fatalf("failed to write file %q: %v", configPath, err) + } + _, err = getCredentialsWithHomeDir(nil, "index.docker.io", tmpHomeDir) + assert.ErrorContains(t, err, "unmarshaling JSON") + + // remove the invalid config file + os.RemoveAll(configPath) + // no config file present + auth, err = getCredentialsWithHomeDir(nil, "index.docker.io", tmpHomeDir) + if err != nil { + t.Fatalf("got unexpected error: %#+v", err) + } + assert.Equal(t, types.DockerAuthConfig{}, auth) + + configPath = filepath.Join(tmpHomeDir, ".dockercfg") + if err := os.WriteFile(configPath, []byte("I'm certainly not a json string."), 0640); err != nil { + t.Fatalf("failed to write file %q: %v", configPath, err) + } + _, err = getCredentialsWithHomeDir(nil, "index.docker.io", tmpHomeDir) + assert.ErrorContains(t, err, "unmarshaling JSON") +} + +func TestGetAllCredentials(t *testing.T) { + // Create a temporary authentication file. + tmpFile, err := os.CreateTemp("", "auth.json.") + require.NoError(t, err) + authFilePath := tmpFile.Name() + defer tmpFile.Close() + defer os.Remove(authFilePath) + // override PATH for executing credHelper + path, err := os.Getwd() + require.NoError(t, err) + origPath := os.Getenv("PATH") + newPath := fmt.Sprintf("%s:%s", filepath.Join(path, "testdata"), origPath) + t.Setenv("PATH", newPath) + t.Logf("using PATH: %q", newPath) + err = os.Chmod(filepath.Join(path, "testdata", "docker-credential-helper-registry"), os.ModePerm) + require.NoError(t, err) + sys := types.SystemContext{ + AuthFilePath: authFilePath, + SystemRegistriesConfPath: filepath.Join("testdata", "cred-helper-with-auth-files.conf"), + SystemRegistriesConfDirPath: filepath.Join("testdata", "IdoNotExist"), + } + + // Make sure that we can handle no-creds-found errors. + t.Run("no credentials found", func(t *testing.T) { + t.Setenv("DOCKER_CONFIG", filepath.Join(path, "testdata")) + authConfigs, err := GetAllCredentials(nil) + require.NoError(t, err) + require.Empty(t, authConfigs) + }) + + for _, data := range [][]struct { + writeKey string + expectedKey string + username string + password string + }{ + { // Basic operation, including a credential helper. + { + writeKey: "example.org", + expectedKey: "example.org", + username: "example-user", + password: "example-password", + }, + { + writeKey: "quay.io", + expectedKey: "quay.io", + username: "quay-user", + password: "quay-password", + }, + { + writeKey: "localhost:5000", + expectedKey: "localhost:5000", + username: "local-user", + password: "local-password", + }, + { + writeKey: "", + expectedKey: "registry-a.com", + username: "foo", + password: "bar", + }, + }, + { // docker.io normalization, both namespaced and not + { + writeKey: "docker.io/vendor", + expectedKey: "docker.io/vendor", + username: "u1", + password: "p1", + }, + { + writeKey: "index.docker.io", // Ideally we would even use a HTTPS URL + expectedKey: "docker.io", + username: "u2", + password: "p2", + }, + { + writeKey: "", + expectedKey: "registry-a.com", + username: "foo", + password: "bar", + }, + }, + } { + // Write the credentials to the authfile. + err := os.WriteFile(authFilePath, []byte{'{', '}'}, 0700) + require.NoError(t, err) + + for _, d := range data { + if d.writeKey == "" { + continue + } + err := SetAuthentication(&sys, d.writeKey, d.username, d.password) + require.NoError(t, err) + } + + // Now ask for all credentials and make sure that map includes all + // servers and the correct credentials. + authConfigs, err := GetAllCredentials(&sys) + require.NoError(t, err) + require.Equal(t, len(data), len(authConfigs)) + + for _, d := range data { + conf, exists := authConfigs[d.expectedKey] + require.True(t, exists, "%v", d) + require.Equal(t, d.username, conf.Username, "%v", d) + require.Equal(t, d.password, conf.Password, "%v", d) + } + } +} + +func TestAuthKeysForKey(t *testing.T) { + for _, tc := range []struct { + name, input string + expected []string + }{ + { + name: "a top-level repo", + input: "quay.io/image", + expected: []string{ + "quay.io/image", + "quay.io", + }, + }, + { + name: "a second-level repo", + input: "quay.io/user/image", + expected: []string{ + "quay.io/user/image", + "quay.io/user", + "quay.io", + }, + }, + { + name: "a deeply-nested repo", + input: "quay.io/a/b/c/d/image", + expected: []string{ + "quay.io/a/b/c/d/image", + "quay.io/a/b/c/d", + "quay.io/a/b/c", + "quay.io/a/b", + "quay.io/a", + "quay.io", + }, + }, + { + name: "docker.io library repo", + input: "docker.io/library/busybox", + expected: []string{ + "docker.io/library/busybox", + "docker.io/library", + "docker.io", + }, + }, + { + name: "docker.io non-library repo", + input: "docker.io/vendor/busybox", + expected: []string{ + "docker.io/vendor/busybox", + "docker.io/vendor", + "docker.io", + }, + }, + } { + result := authKeysForKey(tc.input) + require.Equal(t, tc.expected, result, tc.name) + } +} + +func TestSetCredentials(t *testing.T) { + const ( + usernamePrefix = "username-" + passwordPrefix = "password-" + ) + + for _, tc := range [][]string{ + {"quay.io"}, + {"quay.io/a/b/c/d/image"}, + { + "quay.io/a/b/c", + "quay.io/a/b", + "quay.io/a", + "quay.io", + "my-registry.local", + "my-registry.local", + }, + { + "docker.io", + "docker.io/vendor/product", + "docker.io/vendor", + "docker.io/library/busybox", + "docker.io/library", + }, + } { + tmpFile, err := os.CreateTemp("", "auth.json.set") + require.NoError(t, err) + defer os.RemoveAll(tmpFile.Name()) + + _, err = tmpFile.WriteString("{}") + require.NoError(t, err) + sys := &types.SystemContext{AuthFilePath: tmpFile.Name()} + + writtenCredentials := map[string]int{} + for i, input := range tc { + _, err := SetCredentials( + sys, + input, + usernamePrefix+fmt.Sprint(i), + passwordPrefix+fmt.Sprint(i), + ) + assert.NoError(t, err) + writtenCredentials[input] = i // Possibly overwriting a previous entry + } + + // Read the resulting file and verify it contains the expected keys + auth, err := newAuthPathDefault(tmpFile.Name()).parse() + require.NoError(t, err) + assert.Len(t, auth.AuthConfigs, len(writtenCredentials)) + // auth.AuthConfigs and writtenCredentials are both maps, i.e. their keys are unique; + // so A \subset B && len(A) == len(B) implies A == B + for key := range writtenCredentials { + assert.NotEmpty(t, auth.AuthConfigs[key].Auth) + } + + // Verify that the configuration is interpreted as expected + for key, i := range writtenCredentials { + expected := types.DockerAuthConfig{ + Username: usernamePrefix + fmt.Sprint(i), + Password: passwordPrefix + fmt.Sprint(i), + } + auth, err := GetCredentials(sys, key) + require.NoError(t, err) + assert.Equal(t, expected, auth) + ref, err := reference.ParseNamed(key) + // Full-registry keys and docker.io/top-level-namespace can't be read by GetCredentialsForRef; + // We have already tested that above, so ignore that; only verify that the two + // return consistent results if both are possible. + if err == nil { + auth, err := GetCredentialsForRef(sys, ref) + require.NoError(t, err) + assert.Equal(t, expected, auth, ref.String()) + } + } + } +} + +func TestRemoveAuthentication(t *testing.T) { + testAuth := dockerAuthConfig{Auth: "ZXhhbXBsZTpvcmc="} + for _, tc := range []struct { + config dockerConfigFile + inputs []string + shouldError bool + assert func(dockerConfigFile) + }{ + { + config: dockerConfigFile{ + AuthConfigs: map[string]dockerAuthConfig{ + "quay.io": testAuth, + }, + }, + inputs: []string{"quay.io"}, + assert: func(auth dockerConfigFile) { + assert.Len(t, auth.AuthConfigs, 0) + }, + }, + { + config: dockerConfigFile{ + AuthConfigs: map[string]dockerAuthConfig{ + "quay.io": testAuth, + }, + }, + inputs: []string{"quay.io/user/image"}, + shouldError: true, // not logged in + assert: func(auth dockerConfigFile) { + assert.Len(t, auth.AuthConfigs, 1) + assert.NotEmpty(t, auth.AuthConfigs["quay.io"].Auth) + }, + }, + { + config: dockerConfigFile{ + AuthConfigs: map[string]dockerAuthConfig{ + "quay.io": testAuth, + "my-registry.local": testAuth, + }, + }, + inputs: []string{"my-registry.local"}, + assert: func(auth dockerConfigFile) { + assert.Len(t, auth.AuthConfigs, 1) + assert.NotEmpty(t, auth.AuthConfigs["quay.io"].Auth) + }, + }, + { + config: dockerConfigFile{ + AuthConfigs: map[string]dockerAuthConfig{ + "quay.io/a/b/c": testAuth, + "quay.io/a/b": testAuth, + "quay.io/a": testAuth, + "quay.io": testAuth, + "my-registry.local": testAuth, + }, + }, + inputs: []string{ + "quay.io/a/b", + "quay.io", + "my-registry.local", + }, + assert: func(auth dockerConfigFile) { + assert.Len(t, auth.AuthConfigs, 2) + assert.NotEmpty(t, auth.AuthConfigs["quay.io/a/b/c"].Auth) + assert.NotEmpty(t, auth.AuthConfigs["quay.io/a"].Auth) + }, + }, + } { + content, err := json.Marshal(&tc.config) + require.NoError(t, err) + + tmpFile, err := os.CreateTemp("", "auth.json") + require.NoError(t, err) + defer os.RemoveAll(tmpFile.Name()) + + _, err = tmpFile.Write(content) + require.NoError(t, err) + + sys := &types.SystemContext{AuthFilePath: tmpFile.Name()} + + for _, input := range tc.inputs { + err := RemoveAuthentication(sys, input) + if tc.shouldError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + } + + auth, err := newAuthPathDefault(tmpFile.Name()).parse() + require.NoError(t, err) + + tc.assert(auth) + } +} + +func TestValidateKey(t *testing.T) { + // Invalid keys + for _, key := range []string{ + "https://my-registry.local", + "host/foo:tag", + "host/foo@digest", + "localhost:5000/repo:tag", + "localhost:5000/repo@digest", + } { + _, err := validateKey(key) + assert.Error(t, err, key) + } + + // Valid keys + for _, tc := range []struct { + key string + isNamespaced bool + }{ + {"my-registry.local", false}, + {"my-registry.local/path", true}, + {"quay.io/a/b/c/d", true}, + {"localhost:5000", false}, + {"localhost:5000/repo", true}, + } { + isNamespaced, err := validateKey(tc.key) + require.NoError(t, err, tc.key) + assert.Equal(t, tc.isNamespaced, isNamespaced, tc.key) + } +} + +func TestSetGetCredentials(t *testing.T) { + const ( + username = "username" + password = "password" + ) + + tmpDir := t.TempDir() + + for _, tc := range []struct { + name string + set string + get string + useLegacyFormat bool + shouldAuth bool + }{ + { + name: "Should match namespace", + set: "quay.io/foo", + get: "quay.io/foo/a", + shouldAuth: true, + }, + { + name: "Should match registry if repository provided", + set: "quay.io", + get: "quay.io/foo", + shouldAuth: true, + }, + { + name: "Should not match different repository", + set: "quay.io/foo", + get: "quay.io/bar", + shouldAuth: false, + }, + { + name: "Should match legacy registry entry (new API)", + set: "https://quay.io/v1/", + get: "quay.io/foo", + shouldAuth: true, + }, + { + name: "Should match legacy registry entry (legacy API)", + set: "https://quay.io/v1/", + get: "quay.io", + shouldAuth: true, + useLegacyFormat: true, + }, + } { + // Create a new empty SystemContext referring an empty auth.json + tmpFile, err := os.CreateTemp("", "auth.json-") + require.NoError(t, err) + defer os.RemoveAll(tmpFile.Name()) + + sys := &types.SystemContext{} + if tc.useLegacyFormat { + sys.LegacyFormatAuthFilePath = tmpFile.Name() + _, err = tmpFile.WriteString(fmt.Sprintf( + `{"%s":{"auth":"dXNlcm5hbWU6cGFzc3dvcmQ="}}`, tc.set, + )) + } else { + sys.AuthFilePath = tmpFile.Name() + _, err = tmpFile.WriteString(fmt.Sprintf( + `{"auths":{"%s":{"auth":"dXNlcm5hbWU6cGFzc3dvcmQ="}}}`, tc.set, + )) + } + require.NoError(t, err) + + // Try to authenticate against them + auth, err := getCredentialsWithHomeDir(sys, tc.get, tmpDir) + require.NoError(t, err) + + if tc.shouldAuth { + assert.Equal(t, username, auth.Username, tc.name) + assert.Equal(t, password, auth.Password, tc.name) + } else { + assert.Empty(t, auth.Username, tc.name) + assert.Empty(t, auth.Password, tc.name) + } + } +} diff --git a/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/abnormal.json b/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/abnormal.json new file mode 100644 index 00000000000..33afff20184 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/abnormal.json @@ -0,0 +1,22 @@ +{ + "auths": { + "example.org": { + "auth": "ZXhhbXBsZTpvcmc=" + }, + "https://index.docker.io/v1": { + "auth": "aW5kZXg6ZG9ja2VyLmlv" + }, + "https://127.0.0.1:5000": { + "auth": "MTI3LjA6MC4xLTUwMDA=" + }, + "http://localhost": { + "auth": "bG9jYWw6aG9zdA==" + }, + "https://localhost:5001": { + "auth": "bG9jYWw6aG9zdC01MDAx" + }, + "localhost:5000": { + "auth": "bG9jYWw6aG9zdC01MDAw" + } + } +} diff --git a/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/config.json b/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/config.json new file mode 100644 index 00000000000..c27ecfaf584 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/config.json @@ -0,0 +1,5 @@ +{ + "credHelpers" : { + "registry-no-creds.com" : "helper-registry" + } +} diff --git a/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/cred-helper-with-auth-files.conf b/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/cred-helper-with-auth-files.conf new file mode 100644 index 00000000000..51da42d195b --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/cred-helper-with-auth-files.conf @@ -0,0 +1 @@ +credential-helpers = [ "containers-auth.json", "helper-registry" ] diff --git a/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/cred-helper.conf b/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/cred-helper.conf new file mode 100644 index 00000000000..680faabb93a --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/cred-helper.conf @@ -0,0 +1 @@ +credential-helpers = [ "helper-registry" ] diff --git a/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/docker-credential-helper-registry b/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/docker-credential-helper-registry new file mode 100755 index 00000000000..d9dfa786a26 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/docker-credential-helper-registry @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +case "${1}" in + get) + read REGISTRY + case "${REGISTRY}" in + ("registry-a.com") echo "{\"ServerURL\":\"${REGISTRY}\",\"Username\":\"foo\",\"Secret\":\"bar\"}" ;; + ("registry-b.com") echo "{\"ServerURL\":\"${REGISTRY}\",\"Username\":\"\",\"Secret\":\"fizzbuzz\"}" ;; + ("registry-no-creds.com") echo "credentials not found in native keychain" && exit 1 ;; + (*) echo "{}" ;; + esac + exit 0 + ;; + store) + read UNUSED + exit 0 + ;; + list) + read UNUSED + echo "{\"registry-a.com\":\"foo\"}" + exit 0 + ;; + *) + echo "not implemented" + exit 1 + ;; +esac diff --git a/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/empty.json b/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/empty.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/empty.json @@ -0,0 +1 @@ +{} diff --git a/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/example.json b/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/example.json new file mode 100644 index 00000000000..58b9beb7bfc --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/example.json @@ -0,0 +1,7 @@ +{ + "auths": { + "example.org": { + "auth": "ZXhhbXBsZTpvcmc=" + } + } +} diff --git a/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/example_identitytoken.json b/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/example_identitytoken.json new file mode 100644 index 00000000000..bfcf489077a --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/example_identitytoken.json @@ -0,0 +1,8 @@ +{ + "auths": { + "example.org": { + "auth": "MDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAwOg==", + "identitytoken": "some very long identity token" + } + } +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/full.json b/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/full.json new file mode 100644 index 00000000000..041643ee5e6 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/full.json @@ -0,0 +1,28 @@ +{ + "auths": { + "example.org": { + "auth": "ZXhhbXBsZTpvcmc=" + }, + "index.docker.io": { + "auth": "aW5kZXg6ZG9ja2VyLmlv" + }, + "docker.io": { + "auth": "ZG9ja2VyOmlv" + }, + "localhost": { + "auth": "bG9jYWw6aG9zdA==" + }, + "localhost:5000": { + "auth": "bG9jYWw6aG9zdC01MDAw" + }, + "10.10.30.45": { + "auth": "MTAuMTA6MzAuNDU=" + }, + "10.10.30.45:5000": { + "auth": "MTAuMTA6MzAuNDUtNTAwMA==" + }, + "https://normalize.example.org/v1": { + "auth": "bm9ybWFsaXplOmV4YW1wbGU=" + } + } +} diff --git a/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/legacy.json b/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/legacy.json new file mode 100644 index 00000000000..479790a48cd --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/legacy.json @@ -0,0 +1,8 @@ +{ + "http://index.docker.io/v1": { + "auth": "ZG9ja2VyOmlvLWxlZ2FjeQ==" + }, + "https://localhost/v1": { + "auth": "bG9jYWw6aG9zdC1sZWdhY3k=" + } +} diff --git a/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/refpath.json b/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/refpath.json new file mode 100644 index 00000000000..729aa9ce93e --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/docker/config/testdata/refpath.json @@ -0,0 +1,10 @@ +{ + "auths": { + "example.org/repo": { "auth": "ZXhhbXBsZTpvcmc=" }, + "example.org": { "auth": "bG9jYWw6aG9zdA==" }, + "docker.io/vendor": { "auth": "Zmlyc3Q6bGV2ZWw="}, + "docker.io": { "auth": "dG9wOmxldmVs"}, + "docker.io/library/busybox": { "auth": "bGlicmFyeTpidXN5Ym94" }, + "docker.io/library": { "auth": "bGlicmFyeTpvdGhlcg==" } + } +} diff --git a/vendor/github.com/containers/image/v5/pkg/shortnames/shortnames_test.go b/vendor/github.com/containers/image/v5/pkg/shortnames/shortnames_test.go new file mode 100644 index 00000000000..78a9fbfa606 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/shortnames/shortnames_test.go @@ -0,0 +1,603 @@ +package shortnames + +import ( + "os" + "testing" + + "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/pkg/sysregistriesv2" + "github.com/containers/image/v5/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIsShortName(t *testing.T) { + tests := []struct { + input string + parseUnnormalizedShortName bool + mustFail bool + }{ + // SHORT NAMES + {"fedora", true, false}, + {"fedora:latest", true, false}, + {"library/fedora", true, false}, + {"library/fedora:latest", true, false}, + {"busybox@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", true, false}, + {"busybox:latest@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", true, false}, + // !SHORT NAMES + {"quay.io/fedora", false, false}, + {"docker.io/fedora", false, false}, + {"docker.io/library/fedora:latest", false, false}, + {"localhost/fedora", false, false}, + {"localhost:5000/fedora:latest", false, false}, + {"example.foo.this.may.be.garbage.but.maybe.not:1234/fedora:latest", false, false}, + {"docker.io/library/busybox@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", false, false}, + {"docker.io/library/busybox:latest@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", false, false}, + {"docker.io/fedora@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", false, false}, + // INVALID NAMES + {"", false, true}, + {"$$$", false, true}, + {"::", false, true}, + {"docker://quay.io/library/foo:bar", false, true}, + {" ", false, true}, + } + + for _, test := range tests { + res, _, err := parseUnnormalizedShortName(test.input) + if test.mustFail { + require.Error(t, err, "%q should not be parseable") + continue + } + require.NoError(t, err, "%q should be parseable") + assert.Equal(t, test.parseUnnormalizedShortName, res, "%q", test.input) + } +} + +func TestSplitUserInput(t *testing.T) { + tests := []struct { + input string + repo string + isTagged bool + isDigested bool + }{ + // Neither tags nor digests + {"fedora", "fedora", false, false}, + {"repo/fedora", "repo/fedora", false, false}, + {"registry.com/fedora", "registry.com/fedora", false, false}, + // Tags + {"fedora:tag", "fedora", true, false}, + {"repo/fedora:tag", "repo/fedora", true, false}, + {"registry.com/fedora:latest", "registry.com/fedora", true, false}, + // Digests + {"fedora@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", "fedora", false, true}, + {"repo/fedora@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", "repo/fedora", false, true}, + {"registry.com/fedora@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", "registry.com/fedora", false, true}, + } + + for _, test := range tests { + _, ref, err := parseUnnormalizedShortName(test.input) + require.NoError(t, err, "%v", test) + + isTagged, isDigested, shortNameRepo, tag, digest := splitUserInput(ref) + require.NotNil(t, shortNameRepo) + normalized := shortNameRepo.String() + assert.Equal(t, test.repo, normalized) + assert.Equal(t, test.isTagged, isTagged) + assert.Equal(t, test.isDigested, isDigested) + if isTagged { + normalized = normalized + ":" + tag + } else if isDigested { + normalized = normalized + "@" + digest.String() + } + assert.Equal(t, test.input, normalized) + } +} + +func TestResolve(t *testing.T) { + tmp, err := os.CreateTemp("", "aliases.conf") + require.NoError(t, err) + defer os.Remove(tmp.Name()) + + sys := &types.SystemContext{ + SystemRegistriesConfPath: "testdata/aliases.conf", + SystemRegistriesConfDirPath: "testdata/this-does-not-exist", + UserShortNameAliasConfPath: tmp.Name(), + } + + sysResolveToDockerHub := &types.SystemContext{ + SystemRegistriesConfPath: "testdata/aliases.conf", + SystemRegistriesConfDirPath: "testdata/this-does-not-exist", + UserShortNameAliasConfPath: tmp.Name(), + PodmanOnlyShortNamesIgnoreRegistriesConfAndForceDockerHub: true, + } + + _, err = sysregistriesv2.TryUpdatingCache(sys) + require.NoError(t, err) + + digest := "@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a" + + tests := []struct { + input, value, dockerHubValue string + }{ + { // alias + "docker", + "docker.io/library/foo:latest", + "docker.io/library/docker:latest", + }, + { // alias tagged + "docker:tag", + "docker.io/library/foo:tag", + "docker.io/library/docker:tag", + }, + { // alias digested + "docker" + digest, + "docker.io/library/foo" + digest, + "docker.io/library/docker" + digest, + }, + { // alias with repo + "quay/foo", + "quay.io/library/foo:latest", + "docker.io/quay/foo:latest", + }, + { // alias with repo tagged + "quay/foo:tag", + "quay.io/library/foo:tag", + "docker.io/quay/foo:tag", + }, + { // alias with repo digested + "quay/foo" + digest, + "quay.io/library/foo" + digest, + "docker.io/quay/foo" + digest, + }, + { // alias + "example", + "example.com/library/foo:latest", + "docker.io/library/example:latest", + }, + { // alias with tag + "example:tag", + "example.com/library/foo:tag", + "docker.io/library/example:tag", + }, + { // alias digested + "example" + digest, + "example.com/library/foo" + digest, + "docker.io/library/example" + digest, + }, + { // FQN + "registry.com/repo/image", + "registry.com/repo/image:latest", + "registry.com/repo/image:latest", + }, + { // FQN tagged + "registry.com/repo/image:tag", + "registry.com/repo/image:tag", + "registry.com/repo/image:tag", + }, + { // FQN digested + "registry.com/repo/image" + digest, + "registry.com/repo/image" + digest, + "registry.com/repo/image" + digest, + }, + } + + // All of them should resolve correctly. + for _, test := range tests { + resolved, err := Resolve(sys, test.input) + require.NoError(t, err, "%v", test) + require.NotNil(t, resolved) + require.Len(t, resolved.PullCandidates, 1) + assert.Equal(t, test.value, resolved.PullCandidates[0].Value.String()) + assert.False(t, resolved.PullCandidates[0].record) + } + + // Now another run with enforcing resolution to Docker Hub. + for _, test := range tests { + resolved, err := Resolve(sysResolveToDockerHub, test.input) + require.NoError(t, err, "%v", test) + require.NotNil(t, resolved) + require.Len(t, resolved.PullCandidates, 1) + assert.Equal(t, test.dockerHubValue, resolved.PullCandidates[0].Value.String()) + assert.False(t, resolved.PullCandidates[0].record) + } + + // Non-existent alias should return an error as no search registries + // are configured in the config. + resolved, err := Resolve(sys, "doesnotexist") + require.Error(t, err) + require.Nil(t, resolved) + + // It'll work though when enforcing resolving to Docker Hub. + resolved, err = Resolve(sysResolveToDockerHub, "doesnotexist") + require.NoError(t, err) + require.NotNil(t, resolved) + require.Len(t, resolved.PullCandidates, 1) + assert.Equal(t, "docker.io/library/doesnotexist:latest", resolved.PullCandidates[0].Value.String()) + assert.False(t, resolved.PullCandidates[0].record) + + // An empty name is not valid. + resolved, err = Resolve(sys, "") + require.Error(t, err) + require.Nil(t, resolved) + + // Invalid input. + resolved, err = Resolve(sys, "Invalid#$") + require.Error(t, err) + require.Nil(t, resolved) +} + +func toNamed(t *testing.T, input string, trim bool) reference.Named { + ref, err := reference.Parse(input) + require.NoError(t, err) + named := ref.(reference.Named) + require.NotNil(t, named) + + if trim { + named = reference.TrimNamed(named) + } + + return named +} + +func addAlias(t *testing.T, sys *types.SystemContext, name string, value string, mustFail bool) { + namedValue := toNamed(t, value, false) + + if mustFail { + require.Error(t, Add(sys, name, namedValue)) + } else { + require.NoError(t, Add(sys, name, namedValue)) + } +} + +func removeAlias(t *testing.T, sys *types.SystemContext, name string, mustFail bool, trim bool) { + namedName := toNamed(t, name, trim) + + if mustFail { + require.Error(t, Remove(sys, namedName.String())) + } else { + require.NoError(t, Remove(sys, namedName.String())) + } +} + +func TestResolveWithDropInConfigs(t *testing.T) { + tmp, err := os.CreateTemp("", "aliases.conf") + require.NoError(t, err) + defer os.Remove(tmp.Name()) + + sys := &types.SystemContext{ + SystemRegistriesConfPath: "testdata/aliases.conf", + SystemRegistriesConfDirPath: "testdata/registries.conf.d", + UserShortNameAliasConfPath: tmp.Name(), + } + + _, err = sysregistriesv2.TryUpdatingCache(sys) + require.NoError(t, err) + + tests := []struct { + name, value string + }{ + {"docker", "docker.io/library/config1:latest"}, // overridden by config1 + {"docker:tag", "docker.io/library/config1:tag"}, + { + "docker@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", + "docker.io/library/config1@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", + }, + {"quay/foo", "quay.io/library/foo:latest"}, + {"quay/foo:tag", "quay.io/library/foo:tag"}, + { + "quay/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", + "quay.io/library/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", + }, + {"config1", "config1.com/image:latest"}, + {"config1:tag", "config1.com/image:tag"}, + { + "config1@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", + "config1.com/image@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", + }, + {"barz", "barz.com/config2:latest"}, // from config1, overridden by config2 + {"barz:tag", "barz.com/config2:tag"}, + { + "barz@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", + "barz.com/config2@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", + }, + {"added1", "aliases.conf/added1:latest"}, // from Add() + {"added1:tag", "aliases.conf/added1:tag"}, + { + "added1@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", + "aliases.conf/added1@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", + }, + {"added2", "aliases.conf/added2:latest"}, // from Add() + {"added2:tag", "aliases.conf/added2:tag"}, + { + "added2@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", + "aliases.conf/added2@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", + }, + {"added3", "aliases.conf/added3:latest"}, // from Add() + {"added3:tag", "aliases.conf/added3:tag"}, + { + "added3@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", + "aliases.conf/added3@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", + }, + } + + addAlias(t, sys, "added1", "aliases.conf/added1", false) + addAlias(t, sys, "added2", "aliases.conf/added2", false) + addAlias(t, sys, "added3", "aliases.conf/added3", false) + + // Tags/digests are invalid! + addAlias(t, sys, "added3", "aliases.conf/added3:tag", true) + addAlias(t, sys, "added3:tag", "aliases.conf/added3", true) + addAlias(t, sys, "added3", "aliases.conf/added3@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", true) + addAlias(t, sys, "added3@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", "aliases.conf/added3", true) + + // All of them should resolve correctly. + for _, test := range tests { + resolved, err := Resolve(sys, test.name) + require.NoError(t, err) + require.NotNil(t, resolved) + require.Len(t, resolved.PullCandidates, 1) + assert.Equal(t, test.value, resolved.PullCandidates[0].Value.String()) + assert.False(t, resolved.PullCandidates[0].record) + } + + // config1 sets one search registry. + resolved, err := Resolve(sys, "doesnotexist") + require.NoError(t, err) + require.NotNil(t, resolved) + require.Len(t, resolved.PullCandidates, 1) + assert.Equal(t, "example-overwrite.com/doesnotexist:latest", resolved.PullCandidates[0].Value.String()) + + // An empty name is not valid. + resolved, err = Resolve(sys, "") + require.Error(t, err) + require.Nil(t, resolved) + + // Invalid input. + resolved, err = Resolve(sys, "Invalid#$") + require.Error(t, err) + require.Nil(t, resolved) + + // Fully-qualified input will be returned as is. + resolved, err = Resolve(sys, "quay.io/repo/fedora") + require.NoError(t, err) + require.NotNil(t, resolved) + require.Len(t, resolved.PullCandidates, 1) + assert.Equal(t, "quay.io/repo/fedora:latest", resolved.PullCandidates[0].Value.String()) + assert.False(t, resolved.PullCandidates[0].record) + + resolved, err = Resolve(sys, "localhost/repo/fedora:sometag") + require.NoError(t, err) + require.NotNil(t, resolved) + require.Len(t, resolved.PullCandidates, 1) + assert.Equal(t, "localhost/repo/fedora:sometag", resolved.PullCandidates[0].Value.String()) + assert.False(t, resolved.PullCandidates[0].record) + + // Now test removal. + + // Stored in aliases.conf, so we can remove it. + removeAlias(t, sys, "added1", false, false) + removeAlias(t, sys, "added2", false, false) + removeAlias(t, sys, "added3", false, false) + removeAlias(t, sys, "added2:tag", true, false) + removeAlias(t, sys, "added3@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", true, false) + + // Doesn't exist -> error. + removeAlias(t, sys, "added1", true, false) + removeAlias(t, sys, "added2", true, false) + removeAlias(t, sys, "added3", true, false) + + // Cannot remove entries from registries.conf files -> error. + removeAlias(t, sys, "docker", true, false) + removeAlias(t, sys, "docker", true, false) + removeAlias(t, sys, "docker", true, false) +} + +func TestResolveWithVaryingShortNameModes(t *testing.T) { + tmp, err := os.CreateTemp("", "aliases.conf") + require.NoError(t, err) + defer os.Remove(tmp.Name()) + + tests := []struct { + confPath string + mode types.ShortNameMode + name string + mustFail bool + numAliases int + }{ + // Invalid -> error + {"testdata/no-reg.conf", types.ShortNameModeInvalid, "repo/image", true, 0}, + {"testdata/one-reg.conf", types.ShortNameModeInvalid, "repo/image", true, 0}, + {"testdata/two-reg.conf", types.ShortNameModeInvalid, "repo/image", true, 0}, + // Permissive + match -> return alias + {"testdata/no-reg.conf", types.ShortNameModePermissive, "repo/image", false, 1}, + {"testdata/one-reg.conf", types.ShortNameModePermissive, "repo/image", false, 1}, + {"testdata/two-reg.conf", types.ShortNameModePermissive, "repo/image", false, 1}, + // Permissive + no match -> search (no tty) + {"testdata/no-reg.conf", types.ShortNameModePermissive, "doesnotexist", true, 0}, + {"testdata/one-reg.conf", types.ShortNameModePermissive, "doesnotexist", false, 1}, + {"testdata/two-reg.conf", types.ShortNameModePermissive, "doesnotexist", false, 2}, + // Disabled + match -> return alias + {"testdata/no-reg.conf", types.ShortNameModeDisabled, "repo/image", false, 1}, + {"testdata/one-reg.conf", types.ShortNameModeDisabled, "repo/image", false, 1}, + {"testdata/two-reg.conf", types.ShortNameModeDisabled, "repo/image", false, 1}, + // Disabled + no match -> search + {"testdata/no-reg.conf", types.ShortNameModeDisabled, "doesnotexist", true, 0}, + {"testdata/one-reg.conf", types.ShortNameModeDisabled, "doesnotexist", false, 1}, + {"testdata/two-reg.conf", types.ShortNameModeDisabled, "doesnotexist", false, 2}, + // Enforcing + match -> return alias + {"testdata/no-reg.conf", types.ShortNameModeEnforcing, "repo/image", false, 1}, + {"testdata/one-reg.conf", types.ShortNameModeEnforcing, "repo/image", false, 1}, + {"testdata/two-reg.conf", types.ShortNameModeEnforcing, "repo/image", false, 1}, + // Enforcing + no match -> error if search regs > 1 and no tty + {"testdata/no-reg.conf", types.ShortNameModeEnforcing, "doesnotexist", true, 0}, + {"testdata/one-reg.conf", types.ShortNameModeEnforcing, "doesnotexist", false, 1}, + {"testdata/two-reg.conf", types.ShortNameModeEnforcing, "doesnotexist", true, 0}, + } + + for _, test := range tests { + sys := &types.SystemContext{ + SystemRegistriesConfDirPath: "testdata/this-does-not-exist", + UserShortNameAliasConfPath: tmp.Name(), + // From test + SystemRegistriesConfPath: test.confPath, + ShortNameMode: &test.mode, + } + + _, err := sysregistriesv2.TryUpdatingCache(sys) + require.NoError(t, err) + + resolved, err := Resolve(sys, test.name) + if test.mustFail { + require.Error(t, err, "%v", test) + continue + } + require.NoError(t, err, "%v", test) + require.NotNil(t, resolved) + require.Len(t, resolved.PullCandidates, test.numAliases, "%v", test) + } +} + +func TestResolveAndRecord(t *testing.T) { + tmp, err := os.CreateTemp("", "aliases.conf") + require.NoError(t, err) + defer os.Remove(tmp.Name()) + + sys := &types.SystemContext{ + SystemRegistriesConfPath: "testdata/two-reg.conf", + SystemRegistriesConfDirPath: "testdata/this-does-not-exist", + UserShortNameAliasConfPath: tmp.Name(), + } + + _, err = sysregistriesv2.TryUpdatingCache(sys) + require.NoError(t, err) + + tests := []struct { + name string + expected []string + }{ + // No alias -> USRs + {"foo", []string{"quay.io/foo:latest", "registry.com/foo:latest"}}, + {"foo:tag", []string{"quay.io/foo:tag", "registry.com/foo:tag"}}, + {"foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", []string{"quay.io/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", "registry.com/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a"}}, + {"repo/foo", []string{"quay.io/repo/foo:latest", "registry.com/repo/foo:latest"}}, + {"repo/foo:tag", []string{"quay.io/repo/foo:tag", "registry.com/repo/foo:tag"}}, + {"repo/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", []string{"quay.io/repo/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", "registry.com/repo/foo@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a"}}, + // Alias + {"repo/image", []string{"quay.io/repo/image:latest"}}, + {"repo/image:tag", []string{"quay.io/repo/image:tag"}}, + {"repo/image@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a", []string{"quay.io/repo/image@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a"}}, + } + for _, test := range tests { + resolved, err := Resolve(sys, test.name) + require.NoError(t, err, "%v", test) + require.NotNil(t, resolved) + require.Len(t, resolved.PullCandidates, len(test.expected), "%v", test) + + for i, candidate := range resolved.PullCandidates { + require.Equal(t, test.expected[i], candidate.Value.String(), "%v", test) + + require.False(t, candidate.record, "%v", test) + candidate.record = true // make sure we can actually record + + // Record the alias, look it up another time and make + // sure there's only one match (i.e., the new alias) + // and that is has the expected value. + require.NoError(t, candidate.Record()) + + newResolved, err := Resolve(sys, test.name) + require.NoError(t, err, "%v", test) + require.Len(t, newResolved.PullCandidates, 1, "%v", test) + require.Equal(t, candidate.Value.String(), newResolved.PullCandidates[0].Value.String(), "%v", test) + + // Now remove the alias again. + removeAlias(t, sys, test.name, false, true) + + // Now set recording to false and try recording again. + candidate.record = false + require.NoError(t, candidate.Record()) + removeAlias(t, sys, test.name, true, true) // must error out now + } + } +} + +func TestResolveLocally(t *testing.T) { + tmp, err := os.CreateTemp("", "aliases.conf") + require.NoError(t, err) + defer os.Remove(tmp.Name()) + + sys := &types.SystemContext{ + SystemRegistriesConfPath: "testdata/two-reg.conf", + SystemRegistriesConfDirPath: "testdata/this-does-not-exist", + UserShortNameAliasConfPath: tmp.Name(), + } + sysResolveToDockerHub := &types.SystemContext{ + SystemRegistriesConfPath: "testdata/two-reg.conf", + SystemRegistriesConfDirPath: "testdata/this-does-not-exist", + UserShortNameAliasConfPath: tmp.Name(), + PodmanOnlyShortNamesIgnoreRegistriesConfAndForceDockerHub: true, + } + + digest := "@sha256:d366a4665ab44f0648d7a00ae3fae139d55e32f9712c67accd604bb55df9d05a" + + tests := []struct { + input string + expectedSys []string + expectedSysResolveToDockerHub string + }{ + { // alias match + "repo/image", + []string{"quay.io/repo/image:latest", "localhost/repo/image:latest", "quay.io/repo/image:latest", "registry.com/repo/image:latest"}, + "docker.io/repo/image:latest", + }, + { // no alias match + "foo", + []string{"localhost/foo:latest", "quay.io/foo:latest", "registry.com/foo:latest"}, + "docker.io/library/foo:latest", + }, + { // no alias match tagged + "foo:tag", + []string{"localhost/foo:tag", "quay.io/foo:tag", "registry.com/foo:tag"}, + "docker.io/library/foo:tag", + }, + { // no alias match digested + "foo" + digest, + []string{"localhost/foo" + digest, "quay.io/foo" + digest, "registry.com/foo" + digest}, + "docker.io/library/foo" + digest, + }, + { // localhost + "localhost/foo", + []string{"localhost/foo:latest"}, + "localhost/foo:latest", + }, + { // localhost tagged + "localhost/foo:tag", + []string{"localhost/foo:tag"}, + "localhost/foo:tag", + }, + { // localhost digested + "localhost/foo" + digest, + []string{"localhost/foo" + digest}, + "localhost/foo" + digest, + }, + { // non-localhost FQN + digest + "registry.com/repo/image" + digest, + []string{"registry.com/repo/image" + digest}, + "registry.com/repo/image" + digest, + }, + } + + for _, test := range tests { + aliases, err := ResolveLocally(sys, test.input) + require.NoError(t, err) + require.Len(t, aliases, len(test.expectedSys)) + for i := range aliases { + assert.Equal(t, test.expectedSys[i], aliases[i].String()) + } + + // Another run enforcing resolving to Docker Hub. + aliases, err = ResolveLocally(sysResolveToDockerHub, test.input) + require.NoError(t, err) + require.Len(t, aliases, 1) + assert.Equal(t, test.expectedSysResolveToDockerHub, aliases[0].String()) + } +} diff --git a/vendor/github.com/containers/image/v5/pkg/shortnames/testdata/aliases.conf b/vendor/github.com/containers/image/v5/pkg/shortnames/testdata/aliases.conf new file mode 100644 index 00000000000..cb05b275a3f --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/shortnames/testdata/aliases.conf @@ -0,0 +1,7 @@ +short-name-mode="enforcing" + +[aliases] +docker="docker.io/library/foo" +"quay/foo"="quay.io/library/foo" +example="example.com/library/foo" +empty="" diff --git a/vendor/github.com/containers/image/v5/pkg/shortnames/testdata/no-reg.conf b/vendor/github.com/containers/image/v5/pkg/shortnames/testdata/no-reg.conf new file mode 100644 index 00000000000..968f7ecbf93 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/shortnames/testdata/no-reg.conf @@ -0,0 +1,2 @@ +[aliases] +"repo/image"="quay.io/repo/image" diff --git a/vendor/github.com/containers/image/v5/pkg/shortnames/testdata/one-reg.conf b/vendor/github.com/containers/image/v5/pkg/shortnames/testdata/one-reg.conf new file mode 100644 index 00000000000..e9dfab2de40 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/shortnames/testdata/one-reg.conf @@ -0,0 +1,4 @@ +unqualified-search-registries=["quay.io"] + +[aliases] +"repo/image"="quay.io/repo/image" diff --git a/vendor/github.com/containers/image/v5/pkg/shortnames/testdata/registries.conf.d/config-1.conf b/vendor/github.com/containers/image/v5/pkg/shortnames/testdata/registries.conf.d/config-1.conf new file mode 100644 index 00000000000..f02e618a04d --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/shortnames/testdata/registries.conf.d/config-1.conf @@ -0,0 +1,9 @@ +unqualified-search-registries = ["example-overwrite.com"] + +[[registry]] +location = "1.com" + +[aliases] +docker="docker.io/library/config1" +config1="config1.com/image" +barz="barz.com/image/config1" diff --git a/vendor/github.com/containers/image/v5/pkg/shortnames/testdata/registries.conf.d/config-2.conf b/vendor/github.com/containers/image/v5/pkg/shortnames/testdata/registries.conf.d/config-2.conf new file mode 100644 index 00000000000..7ec82c75fff --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/shortnames/testdata/registries.conf.d/config-2.conf @@ -0,0 +1,14 @@ +short-name-mode="permissive" + +[[registry]] +location = "2.com" + +[[registry]] +location = "base.com" +blocked = true + +[aliases] +config2="config2.com/image" +barz="barz.com/config2" +added3="xxx.com/image" +example="" diff --git a/vendor/github.com/containers/image/v5/pkg/shortnames/testdata/registries.conf.d/config-3.conf b/vendor/github.com/containers/image/v5/pkg/shortnames/testdata/registries.conf.d/config-3.conf new file mode 100644 index 00000000000..e69de29bb2d diff --git a/vendor/github.com/containers/image/v5/pkg/shortnames/testdata/registries.conf.d/config-3.ignore b/vendor/github.com/containers/image/v5/pkg/shortnames/testdata/registries.conf.d/config-3.ignore new file mode 100644 index 00000000000..65866fd784f --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/shortnames/testdata/registries.conf.d/config-3.ignore @@ -0,0 +1,7 @@ +unqualified-search-registries = ["ignore-example-overwrite.com"] + +[[registry]] +location = "ignore-me-because-i-have-a-wrong-suffix.com" + +[aliases] +ignore="me because i have a wrong suffix" diff --git a/vendor/github.com/containers/image/v5/pkg/shortnames/testdata/two-reg.conf b/vendor/github.com/containers/image/v5/pkg/shortnames/testdata/two-reg.conf new file mode 100644 index 00000000000..2ed7829656e --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/shortnames/testdata/two-reg.conf @@ -0,0 +1,4 @@ +unqualified-search-registries=["quay.io", "registry.com"] + +[aliases] +"repo/image"="quay.io/repo/image" diff --git a/vendor/github.com/containers/image/v5/pkg/strslice/strslice_test.go b/vendor/github.com/containers/image/v5/pkg/strslice/strslice_test.go new file mode 100644 index 00000000000..0ef5f4bce20 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/strslice/strslice_test.go @@ -0,0 +1,86 @@ +package strslice + +import ( + "encoding/json" + "reflect" + "testing" +) + +func TestStrSliceMarshalJSON(t *testing.T) { + for _, testcase := range []struct { + input StrSlice + expected string + }{ + // MADNESS(stevvooe): No clue why nil would be "" but empty would be + // "null". Had to make a change here that may affect compatibility. + {input: nil, expected: "null"}, + {StrSlice{}, "[]"}, + {StrSlice{"/bin/sh", "-c", "echo"}, `["/bin/sh","-c","echo"]`}, + } { + data, err := json.Marshal(testcase.input) + if err != nil { + t.Fatal(err) + } + if string(data) != testcase.expected { + t.Fatalf("%#v: expected %v, got %v", testcase.input, testcase.expected, string(data)) + } + } +} + +func TestStrSliceUnmarshalJSON(t *testing.T) { + parts := map[string][]string{ + "": {"default", "values"}, + "[]": {}, + `["/bin/sh","-c","echo"]`: {"/bin/sh", "-c", "echo"}, + } + for json, expectedParts := range parts { + strs := StrSlice{"default", "values"} + if err := strs.UnmarshalJSON([]byte(json)); err != nil { + t.Fatal(err) + } + + actualParts := []string(strs) + if !reflect.DeepEqual(actualParts, expectedParts) { + t.Fatalf("%#v: expected %v, got %v", json, expectedParts, actualParts) + } + + } +} + +func TestStrSliceUnmarshalString(t *testing.T) { + var e StrSlice + echo, err := json.Marshal("echo") + if err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(echo, &e); err != nil { + t.Fatal(err) + } + + if len(e) != 1 { + t.Fatalf("expected 1 element after unmarshal: %q", e) + } + + if e[0] != "echo" { + t.Fatalf("expected `echo`, got: %q", e[0]) + } +} + +func TestStrSliceUnmarshalSlice(t *testing.T) { + var e StrSlice + echo, err := json.Marshal([]string{"echo"}) + if err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(echo, &e); err != nil { + t.Fatal(err) + } + + if len(e) != 1 { + t.Fatalf("expected 1 element after unmarshal: %q", e) + } + + if e[0] != "echo" { + t.Fatalf("expected `echo`, got: %q", e[0]) + } +} diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/shortnames_test.go b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/shortnames_test.go new file mode 100644 index 00000000000..7f295444a77 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/shortnames_test.go @@ -0,0 +1,298 @@ +package sysregistriesv2 + +import ( + "os" + "testing" + + "github.com/containers/image/v5/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestShortNameAliasConfNonempty(t *testing.T) { + for _, c := range []shortNameAliasConf{ + {}, + {Aliases: map[string]string{}}, + } { + copy := c // A shallow copy + res := copy.nonempty() + assert.False(t, res, c) + assert.Equal(t, c, copy, c) // Ensure the method did not change the original value + } + + res := (&shortNameAliasConf{}).nonempty() + assert.False(t, res) + for _, c := range []shortNameAliasConf{ + {Aliases: map[string]string{"a": "example.com/b"}}, + } { + copy := c // A shallow copy + res := copy.nonempty() + assert.True(t, res, c) + assert.Equal(t, c, copy, c) // Ensure the method did not change the original value + } +} + +func TestParseShortNameValue(t *testing.T) { + tests := []struct { + input string + valid bool + }{ + // VALID INPUT + {"docker.io/library/fedora", true}, + {"localhost/fedora", true}, + {"localhost:5000/fedora", true}, + {"localhost:5000/namespace/fedora", true}, + // INVALID INPUT + {"docker.io/library/fedora:latest", false}, // tag + {"docker.io/library/fedora@sha256:b87dd5f837112a9e1e9882963a6406387597698268c0ad371b187151a5dfe6bf", false}, // digest + {"fedora", false}, // short name + {"fedora:latest", false}, // short name + tag + {"library/fedora", false}, // no registry + {"library/fedora:latest", false}, // no registry + tag + {"$$4455%%", false}, // garbage + {"docker://foo", false}, // transports are not supported + {"docker-archive://foo", false}, // transports are not supported + {"", false}, // empty + } + + for _, test := range tests { + named, err := parseShortNameValue(test.input) + if test.valid { + require.NoError(t, err, "%q should be a valid alias", test.input) + assert.NotNil(t, named) + assert.Equal(t, test.input, named.String()) + } else { + require.Error(t, err, "%q should be an invalid alias", test.input) + assert.Nil(t, named) + } + } + + // Now make sure that docker.io references are normalized. + named, err := parseShortNameValue("docker.io/fedora") + require.NoError(t, err) + assert.NotNil(t, named) + assert.Equal(t, "docker.io/library/fedora", named.String()) + +} + +func TestValidateShortName(t *testing.T) { + tests := []struct { + input string + valid bool + }{ + // VALID INPUT + {"library/fedora", true}, + {"fedora", true}, + {"1234567489", true}, + // INVALID INPUT + {"docker.io/library/fedora:latest", false}, + {"docker.io/library/fedora@sha256:b87dd5f837112a9e1e9882963a6406387597698268c0ad371b187151a5dfe6bf", false}, // digest + {"fedora:latest", false}, + {"library/fedora:latest", false}, + {"$$4455%%", false}, + {"docker://foo", false}, + {"docker-archive://foo", false}, + {"", false}, + } + + for _, test := range tests { + err := validateShortName(test.input) + if test.valid { + require.NoError(t, err, "%q should be a valid alias", test.input) + } else { + require.Error(t, err, "%q should be an invalid alias", test.input) + } + } +} + +func TestResolveShortNameAlias(t *testing.T) { + tmp, err := os.CreateTemp("", "aliases.conf") + require.NoError(t, err) + defer os.Remove(tmp.Name()) + + sys := &types.SystemContext{ + SystemRegistriesConfPath: "testdata/aliases.conf", + SystemRegistriesConfDirPath: "testdata/this-does-not-exist", + UserShortNameAliasConfPath: tmp.Name(), + } + + InvalidateCache() + conf, err := tryUpdatingCache(sys, newConfigWrapper(sys)) + require.NoError(t, err) + assert.Len(t, conf.aliasCache.namedAliases, 4) + assert.Len(t, conf.partialV2.Aliases, 0) // This is an implementation detail, not an API guarantee. + + aliases := []struct { + name, value string + }{ + { + "docker", + "docker.io/library/foo", + }, + { + "quay/foo", + "quay.io/library/foo", + }, + { + "example", + "example.com/library/foo", + }, + } + + for _, alias := range aliases { + value, path, err := ResolveShortNameAlias(sys, alias.name) + require.NoError(t, err) + require.NotNil(t, value) + assert.Equal(t, alias.value, value.String()) + assert.Equal(t, "testdata/aliases.conf", path) + } + + // Non-existent alias. + value, path, err := ResolveShortNameAlias(sys, "idonotexist") + require.NoError(t, err) + assert.Nil(t, value) + assert.Equal(t, "", path) + + // Empty right-hand value (special case) -> does not resolve. + value, path, err = ResolveShortNameAlias(sys, "empty") + require.NoError(t, err) + assert.Nil(t, value) + assert.Equal(t, "testdata/aliases.conf", path) +} + +func TestAliasesWithDropInConfigs(t *testing.T) { + tmp, err := os.CreateTemp("", "aliases.conf") + require.NoError(t, err) + defer os.Remove(tmp.Name()) + + sys := &types.SystemContext{ + SystemRegistriesConfPath: "testdata/aliases.conf", + SystemRegistriesConfDirPath: "testdata/registries.conf.d", + UserShortNameAliasConfPath: tmp.Name(), + } + + InvalidateCache() + conf, err := tryUpdatingCache(sys, newConfigWrapper(sys)) + require.NoError(t, err) + assert.Len(t, conf.aliasCache.namedAliases, 8) + assert.Len(t, conf.partialV2.Aliases, 0) // This is an implementation detail, not an API guarantee. + + aliases := []struct { + name, value, config string + }{ + { + "docker", + "docker.io/library/config1", + "testdata/registries.conf.d/config-1.conf", + }, + { + "quay/foo", + "quay.io/library/foo", + "testdata/aliases.conf", + }, + { + "config1", + "config1.com/image", // from config1 + "testdata/registries.conf.d/config-1.conf", + }, + { + "barz", + "barz.com/config2", // from config1, overridden by config2 + "testdata/registries.conf.d/config-2.conf", + }, + { + "config2", + "config2.com/image", // from config2 + "testdata/registries.conf.d/config-2.conf", + }, + { + "added1", + "aliases.conf/added1", // from AddShortNameAlias + tmp.Name(), + }, + { + "added2", + "aliases.conf/added2", // from AddShortNameAlias + tmp.Name(), + }, + { + "added3", + "aliases.conf/added3", // from config2, overridden by AddShortNameAlias + tmp.Name(), + }, + } + + require.NoError(t, AddShortNameAlias(sys, "added1", "aliases.conf/added1")) + require.NoError(t, AddShortNameAlias(sys, "added2", "aliases.conf/added2")) + require.NoError(t, AddShortNameAlias(sys, "added3", "aliases.conf/added3")) + + for _, alias := range aliases { + value, path, err := ResolveShortNameAlias(sys, alias.name) + require.NoError(t, err) + require.NotNil(t, value, "%v", alias) + assert.Equal(t, alias.value, value.String()) + assert.Equal(t, alias.config, path) + } + + value, path, err := ResolveShortNameAlias(sys, "i/do/no/exist") + require.NoError(t, err) + assert.Nil(t, value) + assert.Equal(t, "", path) + + // Empty right-hand value (special case) -> does not resolve. + value, path, err = ResolveShortNameAlias(sys, "empty") // from aliases.conf, overridden by config2 + require.NoError(t, err) + assert.Nil(t, value) + assert.Equal(t, "testdata/aliases.conf", path) + + mode, err := GetShortNameMode(sys) + require.NoError(t, err) + assert.Equal(t, types.ShortNameModePermissive, mode) // from alias.conf, overridden by config2 + + // Now remove the aliases from the machine config. + require.NoError(t, RemoveShortNameAlias(sys, "added1")) + require.NoError(t, RemoveShortNameAlias(sys, "added2")) + require.NoError(t, RemoveShortNameAlias(sys, "added3")) + + // Make sure that 1 and 2 are gone. + for _, alias := range []string{"added1", "added2"} { + value, path, err := ResolveShortNameAlias(sys, alias) + require.NoError(t, err) + assert.Nil(t, value) + assert.Equal(t, "", path) + } + + // 3 is still present in config2 + value, path, err = ResolveShortNameAlias(sys, "added3") + require.NoError(t, err) + require.NotNil(t, value) + assert.Equal(t, "xxx.com/image", value.String()) + assert.Equal(t, "testdata/registries.conf.d/config-2.conf", path) + + require.Error(t, RemoveShortNameAlias(sys, "added3")) // we cannot remove it from config2 +} + +func TestInvalidAliases(t *testing.T) { + tmp, err := os.CreateTemp("", "aliases.conf") + require.NoError(t, err) + defer os.Remove(tmp.Name()) + + sys := &types.SystemContext{ + SystemRegistriesConfPath: "testdata/invalid-aliases.conf", + SystemRegistriesConfDirPath: "testdata/this-does-not-exist", + UserShortNameAliasConfPath: tmp.Name(), + } + + InvalidateCache() + _, err = TryUpdatingCache(sys) + require.Error(t, err) + + // We validate the alias value before loading existing configuration, + // so this tests the validation although the pre-existing configuration + // is invalid. + assert.Error(t, AddShortNameAlias(sys, "added1", "aliases")) + assert.Error(t, AddShortNameAlias(sys, "added2", "aliases.conf")) + assert.Error(t, AddShortNameAlias(sys, "added3", "")) + assert.Error(t, AddShortNameAlias(sys, "added3", " ")) + assert.Error(t, AddShortNameAlias(sys, "added3", "$$$")) +} diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/system_registries_v2_test.go b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/system_registries_v2_test.go new file mode 100644 index 00000000000..ca88c6ea781 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/system_registries_v2_test.go @@ -0,0 +1,975 @@ +package sysregistriesv2 + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var v1RegistriesConfEmptyTestData = []struct { + nonempty, hasSetField bool + v V1RegistriesConf +}{ + {nonempty: false, hasSetField: false, v: V1RegistriesConf{}}, + {nonempty: false, hasSetField: true, v: V1RegistriesConf{V1TOMLConfig{Search: V1TOMLregistries{Registries: []string{}}}}}, + {nonempty: false, hasSetField: true, v: V1RegistriesConf{V1TOMLConfig{Insecure: V1TOMLregistries{Registries: []string{}}}}}, + {nonempty: false, hasSetField: true, v: V1RegistriesConf{V1TOMLConfig{Block: V1TOMLregistries{Registries: []string{}}}}}, + {nonempty: true, hasSetField: true, v: V1RegistriesConf{V1TOMLConfig{Search: V1TOMLregistries{Registries: []string{"example.com"}}}}}, + {nonempty: true, hasSetField: true, v: V1RegistriesConf{V1TOMLConfig{Insecure: V1TOMLregistries{Registries: []string{"example.com"}}}}}, + {nonempty: true, hasSetField: true, v: V1RegistriesConf{V1TOMLConfig{Block: V1TOMLregistries{Registries: []string{"example.com"}}}}}, +} + +func TestV1RegistriesConfNonempty(t *testing.T) { + for _, c := range v1RegistriesConfEmptyTestData { + copy := c.v // A shallow copy + res := copy.Nonempty() + assert.Equal(t, c.nonempty, res, c.v) + assert.Equal(t, c.v, copy, c.v) // Ensure the method did not change the original value + } +} + +func TestV1RegistriesConfHasSetField(t *testing.T) { + for _, c := range v1RegistriesConfEmptyTestData { + copy := c.v // A shallow copy + res := copy.hasSetField() + assert.Equal(t, c.hasSetField, res, c.v) + assert.Equal(t, c.v, copy, c.v) // Ensure the method did not change the original value + } +} + +var v2RegistriesConfEmptyTestData = []struct { + nonempty, hasSetField bool + v V2RegistriesConf +}{ + {nonempty: false, hasSetField: false, v: V2RegistriesConf{}}, + {nonempty: false, hasSetField: true, v: V2RegistriesConf{Registries: []Registry{}}}, + {nonempty: false, hasSetField: true, v: V2RegistriesConf{UnqualifiedSearchRegistries: []string{}}}, + {nonempty: false, hasSetField: true, v: V2RegistriesConf{CredentialHelpers: []string{}}}, + {nonempty: false, hasSetField: true, v: V2RegistriesConf{shortNameAliasConf: shortNameAliasConf{Aliases: map[string]string{}}}}, + {nonempty: true, hasSetField: true, v: V2RegistriesConf{Registries: []Registry{{Prefix: "example.com"}}}}, + {nonempty: true, hasSetField: true, v: V2RegistriesConf{UnqualifiedSearchRegistries: []string{"example.com"}}}, + {nonempty: true, hasSetField: true, v: V2RegistriesConf{CredentialHelpers: []string{"a"}}}, + {nonempty: true, hasSetField: true, v: V2RegistriesConf{ShortNameMode: "enforcing"}}, + {nonempty: true, hasSetField: true, v: V2RegistriesConf{shortNameAliasConf: shortNameAliasConf{Aliases: map[string]string{"a": "example.com/b"}}}}, +} + +func TestV2RegistriesConfNonempty(t *testing.T) { + for _, c := range v2RegistriesConfEmptyTestData { + copy := c.v // A shallow copy + res := copy.Nonempty() + assert.Equal(t, c.nonempty, res, c.v) + assert.Equal(t, c.v, copy, c.v) // Ensure the method did not change the original value + } +} + +func TestV2RegistriesConfHasSetField(t *testing.T) { + for _, c := range v2RegistriesConfEmptyTestData { + copy := c.v // A shallow copy + res := copy.hasSetField() + assert.Equal(t, c.hasSetField, res, c.v) + assert.Equal(t, c.v, copy, c.v) // Ensure the method did not change the original value + } +} + +func TestParseLocation(t *testing.T) { + var err error + var location string + + // invalid locations + _, err = parseLocation("https://example.com") + assert.ErrorContains(t, err, "invalid location 'https://example.com': URI schemes are not supported") + + _, err = parseLocation("john.doe@example.com") + assert.Nil(t, err) + + // valid locations + location, err = parseLocation("example.com") + assert.Nil(t, err) + assert.Equal(t, "example.com", location) + + location, err = parseLocation("example.com/") // trailing slashes are stripped + assert.Nil(t, err) + assert.Equal(t, "example.com", location) + + location, err = parseLocation("example.com//////") // trailing slashes are stripped + assert.Nil(t, err) + assert.Equal(t, "example.com", location) + + location, err = parseLocation("example.com:5000/with/path") + assert.Nil(t, err) + assert.Equal(t, "example.com:5000/with/path", location) +} + +func TestEmptyConfig(t *testing.T) { + registries, err := GetRegistries(&types.SystemContext{ + SystemRegistriesConfPath: "testdata/empty.conf", + SystemRegistriesConfDirPath: "testdata/this-does-not-exist", + }) + assert.Nil(t, err) + assert.Equal(t, 0, len(registries)) + + // When SystemRegistriesConfPath is not explicitly specified (but RootForImplicitAbsolutePaths might be), missing file is treated + // the same as an empty one, without reporting an error. + nonexistentRoot, err := filepath.Abs("testdata/this-does-not-exist") + require.NoError(t, err) + registries, err = GetRegistries(&types.SystemContext{ + RootForImplicitAbsolutePaths: nonexistentRoot, + SystemRegistriesConfDirPath: "testdata/this-does-not-exist", + }) + assert.Nil(t, err) + assert.Equal(t, 0, len(registries)) +} + +func TestMirrors(t *testing.T) { + sys := &types.SystemContext{ + SystemRegistriesConfPath: "testdata/mirrors.conf", + SystemRegistriesConfDirPath: "testdata/this-does-not-exist", + } + + registries, err := GetRegistries(sys) + assert.Nil(t, err) + assert.Equal(t, 2, len(registries)) + + reg, err := FindRegistry(sys, "registry.com/image:tag") + assert.Nil(t, err) + assert.NotNil(t, reg) + assert.Equal(t, 2, len(reg.Mirrors)) + assert.Equal(t, "mirror-1.registry.com", reg.Mirrors[0].Location) + assert.False(t, reg.Mirrors[0].Insecure) + assert.Equal(t, "mirror-2.registry.com", reg.Mirrors[1].Location) + assert.True(t, reg.Mirrors[1].Insecure) +} + +func TestRefMatchingSubdomainPrefix(t *testing.T) { + for _, c := range []struct { + ref, prefix string + expected int + }{ + // Check for subdomain matches + {"docker.io", "*.io", len("docker.io")}, + {"docker.io/foo", "*.com", -1}, + {"example.com/foo", "*.co", -1}, + {"example.com/foo", "*.example.com", -1}, + //FIXME: Port Number matching needs to be revisited. + // https://github.com/containers/image/pull/1191#pullrequestreview-631869416 + //{"example.com:5000", "*.com", len("example.com")}, + //{"example.com:5000/foo", "*.com", len("example.com")}, + //{"sub.example.com:5000/foo", "*.example.com", len("sub.example.com")}, + //{"example.com:5000/foo/bar", "*.com", len("example.com")}, + //{"example.com:5000/foo/bar:baz", "*.com", len("example.com")}, + //{"example.com:5000/foo/bar/bbq:baz", "*.com", len("example.com")}, + //{"example.com:50000/foo", "*.example.com", -1}, + {"example.com/foo", "*.com", len("example.com")}, + {"example.com/foo:bar", "*.com", len("example.com")}, + {"example.com/foo/bar:baz", "*.com", len("example.com")}, + {"yet.another.example.com/foo", "**.example.com", -1}, + {"yet.another.example.com/foo", "***.another.example.com", -1}, + {"yet.another.example.com/foo", "**********.another.example.com", -1}, + {"yet.another.example.com/foo/bar", "**********.another.example.com", -1}, + {"yet.another.example.com/foo/bar", "*.another.example.com", len("yet.another.example.com")}, + {"another.example.com/namespace.com/foo/bar/bbq:baz", "*.example.com", len("another.example.com")}, + {"example.net/namespace-ends-in.com/foo/bar/bbq:baz", "*.com", -1}, + {"another.example.com/namespace.com/foo/bar/bbq:baz", "*.namespace.com", -1}, + {"sub.example.com/foo/bar", "*.com", len("sub.example.com")}, + {"sub.example.com/foo/bar", "*.example.com", len("sub.example.com")}, + {"another.sub.example.com/foo/bar/bbq:baz", "*.example.com", len("another.sub.example.com")}, + {"another.sub.example.com/foo/bar/bbq:baz", "*.sub.example.com", len("another.sub.example.com")}, + {"yet.another.example.com/foo/bar@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "*.example.com", len("yet.another.example.com")}, + {"yet.another.sub.example.com/foo/bar@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "*.sub.example.com", len("yet.another.sub.example.com")}, + } { + refLen := refMatchingSubdomainPrefix(c.ref, c.prefix) + assert.Equal(t, c.expected, refLen, fmt.Sprintf("%s vs. %s", c.ref, c.prefix)) + } +} + +func TestRefMatchingPrefix(t *testing.T) { + for _, c := range []struct { + ref, prefix string + expected int + }{ + // Prefix is a reference.Domain() value + {"docker.io", "docker.io", len("docker.io")}, + {"docker.io", "example.com", -1}, + {"example.com:5000", "example.com:5000", len("example.com:5000")}, + {"example.com:50000", "example.com:5000", -1}, + {"example.com:5000", "example.com", len("example.com")}, // FIXME FIXME This is unintended and undocumented, don't rely on this behavior + {"example.com/foo", "example.com", len("example.com")}, + {"example.com/foo/bar", "example.com", len("example.com")}, + {"example.com/foo/bar:baz", "example.com", len("example.com")}, + {"example.com/foo/bar@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "example.com", len("example.com")}, + // Prefix is a reference.Named.Name() value or a repo namespace + {"docker.io", "docker.io/library", -1}, + {"docker.io/library", "docker.io/library", len("docker.io/library")}, + {"example.com/library", "docker.io/library", -1}, + {"docker.io/libraryy", "docker.io/library", -1}, + {"docker.io/library/busybox", "docker.io/library", len("docker.io/library")}, + {"docker.io", "docker.io/library/busybox", -1}, + {"docker.io/library/busybox", "docker.io/library/busybox", len("docker.io/library/busybox")}, + {"example.com/library/busybox", "docker.io/library/busybox", -1}, + {"docker.io/library/busybox2", "docker.io/library/busybox", -1}, + // Prefix is a single image + {"example.com", "example.com/foo:bar", -1}, + {"example.com/foo", "example.com/foo:bar", -1}, + {"example.com/foo:bar", "example.com/foo:bar", len("example.com/foo:bar")}, + {"example.com/foo:bar2", "example.com/foo:bar", -1}, + {"example.com", "example.com/foo@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", -1}, + {"example.com/foo", "example.com/foo@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", -1}, + {"example.com/foo@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "example.com/foo@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + len("example.com/foo@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")}, + {"example.com/foo@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "example.com/foo@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", -1}, + // Prefix is invalid, but we shouldn’t crash. + // (Note that this is necessary only because loadConfigFile doesn’t reject single-character values outright, + // which it, in principle, could; a valid prefix/location must start with a host name, and host names + // that could ever match anything contain either a dot or a port number, due to docker.io normalization rules.) + {"example.com/foo", "*", -1}, + } { + prefixLen := refMatchingPrefix(c.ref, c.prefix) + assert.Equal(t, c.expected, prefixLen, fmt.Sprintf("%s vs. %s", c.ref, c.prefix)) + } +} + +func TestNewConfigWrapper(t *testing.T) { + const nondefaultPath = "/this/is/not/the/default/registries.conf" + const variableReference = "$HOME" + const rootPrefix = "/root/prefix" + tempHome := t.TempDir() + var userRegistriesFile = filepath.FromSlash(".config/containers/registries.conf") + userRegistriesFilePath := filepath.Join(tempHome, userRegistriesFile) + + for _, c := range []struct { + sys *types.SystemContext + userfilePresent bool + expected string + }{ + // The common case + {nil, false, systemRegistriesConfPath}, + // There is a context, but it does not override the path. + {&types.SystemContext{}, false, systemRegistriesConfPath}, + // Path overridden + {&types.SystemContext{SystemRegistriesConfPath: nondefaultPath}, false, nondefaultPath}, + // Root overridden + { + &types.SystemContext{RootForImplicitAbsolutePaths: rootPrefix}, + false, + filepath.Join(rootPrefix, systemRegistriesConfPath), + }, + // Root and path overrides present simultaneously, + { + &types.SystemContext{ + RootForImplicitAbsolutePaths: rootPrefix, + SystemRegistriesConfPath: nondefaultPath, + }, + false, + nondefaultPath, + }, + // User registries file overridden + {&types.SystemContext{}, true, userRegistriesFilePath}, + // Context and user User registries file preset simultaneously + {&types.SystemContext{SystemRegistriesConfPath: nondefaultPath}, true, nondefaultPath}, + // Root and user registries file overrides present simultaneously, + { + &types.SystemContext{ + RootForImplicitAbsolutePaths: rootPrefix, + SystemRegistriesConfPath: nondefaultPath, + }, + true, + nondefaultPath, + }, + // No environment expansion happens in the overridden paths + {&types.SystemContext{SystemRegistriesConfPath: variableReference}, false, variableReference}, + } { + if c.userfilePresent { + err := os.MkdirAll(filepath.Dir(userRegistriesFilePath), os.ModePerm) + require.NoError(t, err) + f, err := os.Create(userRegistriesFilePath) + require.NoError(t, err) + f.Close() + } else { + os.Remove(userRegistriesFilePath) + } + path := newConfigWrapperWithHomeDir(c.sys, tempHome).configPath + assert.Equal(t, c.expected, path) + } +} + +func TestFindRegistry(t *testing.T) { + sys := &types.SystemContext{ + SystemRegistriesConfPath: "testdata/find-registry.conf", + SystemRegistriesConfDirPath: "testdata/registries.conf.d", + } + + registries, err := GetRegistries(sys) + assert.Nil(t, err) + assert.Equal(t, 19, len(registries)) + + reg, err := FindRegistry(sys, "simple-prefix.com/foo/bar:latest") + assert.Nil(t, err) + assert.NotNil(t, reg) + assert.Equal(t, "simple-prefix.com", reg.Prefix) + assert.Equal(t, reg.Location, "registry.com:5000") + + // path match + reg, err = FindRegistry(sys, "simple-prefix.com/") + assert.Nil(t, err) + assert.NotNil(t, reg) + + // hostname match + reg, err = FindRegistry(sys, "simple-prefix.com") + assert.Nil(t, err) + assert.NotNil(t, reg) + + // subdomain prefix match + reg, err = FindRegistry(sys, "not.so.simple-prefix.com/") + assert.Nil(t, err) + assert.NotNil(t, reg) + assert.Equal(t, "subdomain-prefix.com", reg.Location) + + reg, err = FindRegistry(sys, "not.quite.simple-prefix.com/") + assert.Nil(t, err) + assert.NotNil(t, reg) + assert.Equal(t, "subdomain-prefix-2.com", reg.Location) + + reg, err = FindRegistry(sys, "not.quite.simple-prefix.com:5000/with/path/and/beyond:tag") + assert.Nil(t, err) + assert.NotNil(t, reg) + assert.Equal(t, "subdomain-prefix-2.com", reg.Location) + + // subdomain prefix match for *.not.quite.simple-prefix.com + // location field overridden by /registries.conf.d/subdomain-override-1.conf + reg, err = FindRegistry(sys, "really.not.quite.simple-prefix.com:5000/with/path/and/beyond:tag") + assert.Nil(t, err) + assert.NotNil(t, reg) + assert.Equal(t, "subdomain-prefix-1-overridden-by-dropin-location.com", reg.Location) + + // In this case, the override does NOT occur because the dropin + // prefix = "*.docker.com" which is not a match. + reg, err = FindRegistry(sys, "foo.docker.io:5000/omg/wtf/bbq:foo") + assert.Nil(t, err) + assert.NotNil(t, reg) + assert.Equal(t, "subdomain-prefix-2.com", reg.Location) + + // subdomain prefix match for *.bar.example.com + // location field overridden by /registries.conf.d/subdomain-override-3.conf + reg, err = FindRegistry(sys, "foo.bar.example.com:6000/omg/wtf/bbq@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + assert.Nil(t, err) + assert.NotNil(t, reg) + assert.Equal(t, "subdomain-prefix-3-overridden-by-dropin-location.com", reg.Location) + + // This case first matches with prefix = *.docker.io in find-registry.conf but + // there's a longer match with *.bar.docker.io which gets used + reg, err = FindRegistry(sys, "foo.bar.docker.io:5000/omg/wtf/bbq:foo") + assert.Nil(t, err) + assert.NotNil(t, reg) + assert.Equal(t, "subdomain-prefix-4.com", reg.Location) + + // This case first matches with prefix = *.example.com in find-registry.conf but + // there's a longer match with foo.bar.example.com:5000 which gets used + reg, err = FindRegistry(sys, "foo.bar.example.com:5000/omg/wtf/bbq:foo") + assert.Nil(t, err) + assert.NotNil(t, reg) + assert.Equal(t, "subdomain-prefix-5.com", reg.Location) + + // invalid match + reg, err = FindRegistry(sys, "simple-prefix.comx") + assert.Nil(t, err) + assert.Nil(t, reg) + + reg, err = FindRegistry(sys, "complex-prefix.com:4000/with/path/and/beyond:tag") + assert.Nil(t, err) + assert.NotNil(t, reg) + assert.Equal(t, "complex-prefix.com:4000/with/path", reg.Prefix) + assert.Equal(t, "another-registry.com:5000", reg.Location) + + reg, err = FindRegistry(sys, "no-prefix.com/foo:tag") + assert.Nil(t, err) + assert.NotNil(t, reg) + assert.Equal(t, "no-prefix.com", reg.Prefix) + assert.Equal(t, "no-prefix.com", reg.Location) + + reg, err = FindRegistry(sys, "empty-prefix.com/foo:tag") + assert.Nil(t, err) + assert.NotNil(t, reg) + assert.Equal(t, "empty-prefix.com", reg.Prefix) + assert.Equal(t, "empty-prefix.com", reg.Location) + + _, err = FindRegistry(&types.SystemContext{SystemRegistriesConfPath: "testdata/this-does-not-exist.conf"}, "example.com") + assert.Error(t, err) +} + +func assertRegistryLocationsEqual(t *testing.T, expected []string, regs []Registry) { + // verify the expected registries and their order + names := []string{} + for _, r := range regs { + names = append(names, r.Location) + } + assert.Equal(t, expected, names) +} + +func TestFindUnqualifiedSearchRegistries(t *testing.T) { + sys := &types.SystemContext{ + SystemRegistriesConfPath: "testdata/unqualified-search.conf", + SystemRegistriesConfDirPath: "testdata/this-does-not-exist", + } + + registries, err := GetRegistries(sys) + assert.Nil(t, err) + assert.Equal(t, 4, len(registries)) + + unqRegs, origin, err := UnqualifiedSearchRegistriesWithOrigin(sys) + assert.Nil(t, err) + assert.Equal(t, []string{"registry-a.com", "registry-c.com", "registry-d.com"}, unqRegs) + assert.Equal(t, "testdata/unqualified-search.conf", origin) + + _, err = UnqualifiedSearchRegistries(&types.SystemContext{ + SystemRegistriesConfPath: "testdata/invalid-search.conf", + SystemRegistriesConfDirPath: "testdata/this-does-not-exist", + }) + assert.Error(t, err) +} + +func TestInvalidV2Configs(t *testing.T) { + for _, c := range []struct{ path, errorSubstring string }{ + {"testdata/insecure-conflicts.conf", "registry 'registry.com' is defined multiple times with conflicting 'insecure' setting"}, + {"testdata/blocked-conflicts.conf", "registry 'registry.com' is defined multiple times with conflicting 'blocked' setting"}, + {"testdata/missing-mirror-location.conf", "invalid condition: mirror location is unset"}, + {"testdata/invalid-prefix.conf", "invalid location"}, + {"testdata/this-does-not-exist.conf", "no such file or directory"}, + } { + _, err := GetRegistries(&types.SystemContext{SystemRegistriesConfPath: c.path}) + assert.Error(t, err, c.path) + if c.errorSubstring != "" { + assert.ErrorContains(t, err, c.errorSubstring, c.path) + } + } +} + +func TestUnmarshalConfig(t *testing.T) { + registries, err := GetRegistries(&types.SystemContext{ + SystemRegistriesConfPath: "testdata/unmarshal.conf", + SystemRegistriesConfDirPath: "testdata/this-does-not-exist", + }) + assert.Nil(t, err) + assert.Equal(t, 4, len(registries)) +} + +func TestV1BackwardsCompatibility(t *testing.T) { + sys := &types.SystemContext{ + SystemRegistriesConfPath: "testdata/v1-compatibility.conf", + SystemRegistriesConfDirPath: "testdata/this-does-not-exist", + } + + registries, err := GetRegistries(sys) + assert.Nil(t, err) + assert.Equal(t, 4, len(registries)) + + unqRegs, err := UnqualifiedSearchRegistries(sys) + assert.Nil(t, err) + assert.Equal(t, []string{"registry-a.com", "registry-c.com", "registry-d.com"}, unqRegs) + + // check if merging works + reg, err := FindRegistry(sys, "registry-b.com/bar/foo/barfoo:latest") + assert.Nil(t, err) + assert.NotNil(t, reg) + assert.True(t, reg.Insecure) + assert.True(t, reg.Blocked) + + for _, c := range []string{"testdata/v1-invalid-block.conf", "testdata/v1-invalid-insecure.conf", "testdata/v1-invalid-search.conf"} { + _, err := GetRegistries(&types.SystemContext{ + SystemRegistriesConfPath: c, + SystemRegistriesConfDirPath: "testdata/this-does-not-exist", + }) + assert.Error(t, err, c) + } +} + +func TestMixingV1andV2(t *testing.T) { + for _, c := range []string{ + "testdata/mixing-v1-v2.conf", + "testdata/mixing-v1-v2-empty.conf", + } { + _, err := GetRegistries(&types.SystemContext{ + SystemRegistriesConfPath: c, + SystemRegistriesConfDirPath: "testdata/this-does-not-exist", + }) + assert.ErrorContains(t, err, "mixing sysregistry v1/v2 is not supported", c) + } +} + +func TestConfigCache(t *testing.T) { + configFile, err := os.CreateTemp("", "sysregistriesv2-test") + require.NoError(t, err) + defer os.Remove(configFile.Name()) + defer configFile.Close() + + err = os.WriteFile(configFile.Name(), []byte(` +[[registry]] +location = "registry.com" + +[[registry.mirror]] +location = "mirror-1.registry.com" + +[[registry.mirror]] +location = "mirror-2.registry.com" + + +[[registry]] +location = "blocked.registry.com" +blocked = true + + +[[registry]] +location = "insecure.registry.com" +insecure = true + + +[[registry]] +location = "untrusted.registry.com" +insecure = true`), 0600) + require.NoError(t, err) + + ctx := &types.SystemContext{SystemRegistriesConfPath: configFile.Name()} + + InvalidateCache() + registries, err := GetRegistries(ctx) + assert.Nil(t, err) + assert.Equal(t, 4, len(registries)) + + // empty the config, but use the same SystemContext to show that the + // previously specified registries are in the cache + err = os.WriteFile(configFile.Name(), []byte{}, 0600) + require.NoError(t, err) + registries, err = GetRegistries(ctx) + assert.Nil(t, err) + assert.Equal(t, 4, len(registries)) +} + +func TestInvalidateCache(t *testing.T) { + ctx := &types.SystemContext{SystemRegistriesConfPath: "testdata/invalidate-cache.conf"} + + InvalidateCache() + registries, err := GetRegistries(ctx) + assert.Nil(t, err) + assert.Equal(t, 4, len(registries)) + assertRegistryLocationsEqual(t, []string{"blocked.registry.com", "insecure.registry.com", "registry.com", "untrusted.registry.com"}, registries) + + // invalidate the cache, make sure it's empty and reload + InvalidateCache() + assert.Equal(t, 0, len(configCache)) + + registries, err = GetRegistries(ctx) + assert.Nil(t, err) + assert.Equal(t, 4, len(registries)) + assertRegistryLocationsEqual(t, []string{"blocked.registry.com", "insecure.registry.com", "registry.com", "untrusted.registry.com"}, registries) +} + +func toNamedRef(t *testing.T, ref string) reference.Named { + parsedRef, err := reference.ParseNamed(ref) + require.NoError(t, err) + return parsedRef +} + +func TestRewriteReferenceSuccess(t *testing.T) { + for _, c := range []struct{ inputRef, prefix, location, expected string }{ + // Standard use cases + {"example.com/image", "example.com", "example.com", "example.com/image"}, + {"example.com/image:latest", "example.com", "example.com", "example.com/image:latest"}, + {"example.com:5000/image", "example.com:5000", "example.com:5000", "example.com:5000/image"}, + {"example.com:5000/image:latest", "example.com:5000", "example.com:5000", "example.com:5000/image:latest"}, + // Separator test ('/', '@', ':') + {"example.com/foo@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "example.com", "example.com", + "example.com/foo@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {"example.com/foo/image:latest", "example.com/foo", "example.com", "example.com/image:latest"}, + {"example.com/foo/image:latest", "example.com/foo", "example.com/path", "example.com/path/image:latest"}, + // Docker examples + {"docker.io/library/image:latest", "docker.io", "docker.io", "docker.io/library/image:latest"}, + {"docker.io/library/image", "docker.io/library", "example.com", "example.com/image"}, + {"docker.io/library/image", "docker.io", "example.com", "example.com/library/image"}, + {"docker.io/library/prefix/image", "docker.io/library/prefix", "example.com", "example.com/image"}, + // Wildcard prefix examples + {"docker.io/namespace/image", "*.io", "example.com", "example.com/namespace/image"}, + {"docker.io/library/prefix/image", "*.io", "example.com", "example.com/library/prefix/image"}, + {"sub.example.io/library/prefix/image", "*.example.io", "example.com", "example.com/library/prefix/image"}, + {"another.sub.example.io:5000/library/prefix/image:latest", "*.sub.example.io", "example.com", "example.com:5000/library/prefix/image:latest"}, + {"foo.bar.io/ns1/ns2/ns3/ns4", "*.bar.io", "omg.bbq.com/roflmao", "omg.bbq.com/roflmao/ns1/ns2/ns3/ns4"}, + // Empty location with wildcard prefix examples. Essentially, no + // rewrite occurs and original reference is used as-is. + {"abc.internal.registry.com/foo:bar", "*.internal.registry.com", "", "abc.internal.registry.com/foo:bar"}, + {"blah.foo.bar.com/omg:bbq", "*.com", "", "blah.foo.bar.com/omg:bbq"}, + {"alien.vs.predator.foobar.io:5000/omg", "*.foobar.io", "", "alien.vs.predator.foobar.io:5000/omg"}, + {"alien.vs.predator.foobar.io:5000/foo@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "*.foobar.io", "", + "alien.vs.predator.foobar.io:5000/foo@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + {"alien.vs.predator.foobar.io:5000/omg:bbq", "*.foobar.io", "", "alien.vs.predator.foobar.io:5000/omg:bbq"}, + } { + ref := toNamedRef(t, c.inputRef) + testEndpoint := Endpoint{Location: c.location} + out, err := testEndpoint.rewriteReference(ref, c.prefix) + require.NoError(t, err) + assert.Equal(t, c.expected, out.String()) + } +} + +func TestRewriteReferenceFailedDuringParseNamed(t *testing.T) { + for _, c := range []struct{ inputRef, prefix, location string }{ + // Invalid reference format + {"example.com/foo/image:latest", "example.com/foo", "example.com/path/"}, + {"example.com/foo@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "example.com/foo", "example.com"}, + {"example.com:5000/image:latest", "example.com", ""}, + {"example.com:5000/image:latest", "example.com", "example.com:5000"}, + // Malformed prefix + {"example.com/foo/image:latest", "example.com//foo", "example.com/path"}, + {"example.com/image:latest", "image", "anotherimage"}, + {"example.com/foo/image:latest", "example.com/foo/", "example.com"}, + {"example.com/foo/image", "example.com/fo", "example.com/foo"}, + {"example.com/foo:latest", "example.com/fo", "example.com/foo"}, + {"example.com/foo@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "example.com/fo", "example.com/foo"}, + {"docker.io/library/image", "example.com", "example.com"}, + {"docker.io/library/image", "*.com", "example.com"}, + {"foo.docker.io/library/image", "*.example.com", "example.com/image"}, + {"foo.docker.io/library/image", "*.docker.com", "example.com/image"}, + } { + ref := toNamedRef(t, c.inputRef) + testEndpoint := Endpoint{Location: c.location} + out, err := testEndpoint.rewriteReference(ref, c.prefix) + assert.NotNil(t, err) + assert.Nil(t, out) + } +} + +func TestPullSourcesFromReference(t *testing.T) { + sys := &types.SystemContext{ + SystemRegistriesConfPath: "testdata/pull-sources-from-reference.conf", + SystemRegistriesConfDirPath: "testdata/this-does-not-exist", + } + registries, err := GetRegistries(sys) + require.NoError(t, err) + assert.Equal(t, 9, len(registries)) + + digest := "@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + tag := ":aaa" + for _, tc := range []struct { + matchedPrefix string + repo string + digestPrefixes []string + digestInsecure []bool + tagPrefixes []string + tagInsecure []bool + }{ + // Registry A allowing any kind of pull from mirrors + { + "registry-a.com/foo", + "image", + []string{"mirror-1.registry-a.com", "mirror-2.registry-a.com", "registry-a.com/bar"}, + []bool{false, true, false}, + []string{"mirror-1.registry-a.com", "mirror-2.registry-a.com", "registry-a.com/bar"}, + []bool{false, true, false}, + }, + // Registry B allowing digests pull only from mirrors + { + "registry-b.com/foo", + "image", + []string{"mirror-1.registry-b.com", "mirror-2.registry-b.com", "registry-b.com/bar"}, + []bool{false, false, false}, + []string{"registry-b.com/bar"}, + []bool{false}, + }, + // Registry A has mirrors allow any kind of pull + { + "registry-a.com/baz", + "image", + []string{"mirror-1.registry-a.com", "mirror-2.registry-a.com", "registry-a.com/bar"}, + []bool{false, true, false}, + []string{"mirror-1.registry-a.com", "mirror-2.registry-a.com", "registry-a.com/bar"}, + []bool{false, true, false}, + }, + // Registry B has mirrors allow digests pull only + { + "registry-b.com/baz", + "image", + []string{"mirror-1.registry-b.com", "mirror-2.registry-b.com", "registry-b.com/bar"}, + []bool{false, false, false}, + []string{"registry-b.com/bar"}, + []bool{false}, + }, + // Registry C has a mirror allows digest pull only and a mirror allows any kind of pull + { + "registry-c.com/baz", + "image", + []string{"mirror-1.registry-c.com", "mirror-2.registry-c.com", "registry-c.com/bar"}, + []bool{false, false, false}, + []string{"mirror-1.registry-c.com", "registry-c.com/bar"}, + []bool{false, false}, + }, + // Registry D set digest-only for registry level, allows only digest pulls + // Registry D has no digest-only set for mirrors table + { + "registry-d.com/baz", + "image", + []string{"mirror-1.registry-d.com", "mirror-2.registry-d.com", "registry-d.com/bar"}, + []bool{false, false, false}, + []string{"registry-d.com/bar"}, + []bool{false}, + }, + // Registry E has mirrors only allows tag pull + { + "registry-e.com/baz", + "image", + []string{"registry-e.com/bar"}, + []bool{false}, + []string{"mirror-1.registry-e.com", "mirror-2.registry-e.com", "registry-e.com/bar"}, + []bool{false, false, false}, + }, + // Registry F has one tag only mirror does not allow digest pull + { + "registry-f.com/baz", + "image", + []string{"mirror-1.registry-f.com", "registry-f.com/bar"}, + []bool{false, false}, + []string{"mirror-1.registry-f.com", "mirror-2.registry-f.com", "registry-f.com/bar"}, + []bool{false, false, false}, + }, + // Registry G has one digest-only pull and one tag only pull + { + "registry-g.com/baz", + "image", + []string{"mirror-1.registry-g.com", "mirror-3.registry-g.com", "mirror-4.registry-g.com", "registry-g.com/bar"}, + []bool{false, false, false, false}, + []string{"mirror-2.registry-g.com", "mirror-3.registry-g.com", "mirror-4.registry-g.com", "registry-g.com/bar"}, + []bool{false, false, false, false}, + }, + } { + // Digest + digestedRef := toNamedRef(t, fmt.Sprintf("%s/%s", tc.matchedPrefix, tc.repo)+digest) + registry, err := FindRegistry(sys, digestedRef.Name()) + require.NoError(t, err) + require.NotNil(t, registry) + pullSource, err := registry.PullSourcesFromReference(digestedRef) + require.NoError(t, err) + for i, p := range tc.digestPrefixes { + assert.Equal(t, p, pullSource[i].Endpoint.Location) + assert.Equal(t, fmt.Sprintf("%s/%s", p, tc.repo)+digest, pullSource[i].Reference.String()) + assert.Equal(t, tc.digestInsecure[i], pullSource[i].Endpoint.Insecure) + } + // Tag + taggedRef := toNamedRef(t, fmt.Sprintf("%s/%s", tc.matchedPrefix, tc.repo)+tag) + registry, err = FindRegistry(sys, taggedRef.Name()) + require.NoError(t, err) + require.NotNil(t, registry) + pullSource, err = registry.PullSourcesFromReference(taggedRef) + require.NoError(t, err) + for i, p := range tc.tagPrefixes { + assert.Equal(t, p, pullSource[i].Endpoint.Location) + assert.Equal(t, fmt.Sprintf("%s/%s", p, tc.repo)+tag, pullSource[i].Reference.String()) + assert.Equal(t, tc.tagInsecure[i], pullSource[i].Endpoint.Insecure) + } + } +} + +func TestInvalidMirrorConfig(t *testing.T) { + for _, tc := range []struct { + sys *types.SystemContext + expectErr string + }{ + { + sys: &types.SystemContext{ + SystemRegistriesConfPath: "testdata/invalid-config-level-mirror.conf", + SystemRegistriesConfDirPath: "testdata/this-does-not-exist", + }, + expectErr: fmt.Sprintf("pull-from-mirror must not be set for a non-mirror registry %q", "registry-a.com/foo"), + }, + { + sys: &types.SystemContext{ + SystemRegistriesConfPath: "testdata/invalid-conflict-mirror.conf", + SystemRegistriesConfDirPath: "testdata/this-does-not-exist", + }, + expectErr: fmt.Sprintf("cannot set mirror usage mirror-by-digest-only for the registry (%q) and pull-from-mirror for per-mirror (%q) at the same time", "registry-a.com/foo", "mirror-1.registry-a.com"), + }, + { + sys: &types.SystemContext{ + SystemRegistriesConfPath: "testdata/invalid-value-mirror.conf", + SystemRegistriesConfDirPath: "testdata/this-does-not-exist", + }, + expectErr: fmt.Sprintf("unsupported pull-from-mirror value %q for mirror %q", "notvalid", "mirror-1.registry-a.com"), + }, + } { + _, err := GetRegistries(tc.sys) + assert.ErrorContains(t, err, tc.expectErr) + } + +} + +func TestTryUpdatingCache(t *testing.T) { + ctx := &types.SystemContext{ + SystemRegistriesConfPath: "testdata/try-update-cache-valid.conf", + SystemRegistriesConfDirPath: "testdata/this-does-not-exist", + } + InvalidateCache() + registries, err := TryUpdatingCache(ctx) + assert.Nil(t, err) + assert.Equal(t, 1, len(registries.Registries)) + assert.Equal(t, 1, len(configCache)) + + ctxInvalid := &types.SystemContext{ + SystemRegistriesConfPath: "testdata/try-update-cache-invalid.conf", + SystemRegistriesConfDirPath: "testdata/this-does-not-exist", + } + registries, err = TryUpdatingCache(ctxInvalid) + assert.NotNil(t, err) + assert.Nil(t, registries) + assert.Equal(t, 1, len(configCache)) +} + +func TestRegistriesConfDirectory(t *testing.T) { + ctx := &types.SystemContext{ + SystemRegistriesConfPath: "testdata/base-for-registries.d.conf", + SystemRegistriesConfDirPath: "testdata/registries.conf.d", + } + + InvalidateCache() + registries, err := TryUpdatingCache(ctx) + require.NoError(t, err) + assert.NotNil(t, registries) + + assert.Equal(t, []string{"example-overwrite.com"}, registries.UnqualifiedSearchRegistries) + assert.Equal(t, 6, len(registries.Registries)) + assertRegistryLocationsEqual(t, []string{"subdomain-prefix-3-overridden-by-dropin-location.com", "subdomain-prefix-2-overridden-by-dropin-location.com", "subdomain-prefix-1-overridden-by-dropin-location.com", "1.com", "2.com", "base.com"}, registries.Registries) + + reg, err := FindRegistry(ctx, "base.com/test:latest") + require.NoError(t, err) + assert.True(t, reg.Blocked) + + usrs, origin, err := UnqualifiedSearchRegistriesWithOrigin(ctx) + require.NoError(t, err) + assert.Equal(t, []string{"example-overwrite.com"}, usrs) + assert.Equal(t, "testdata/registries.conf.d/config-1.conf", origin) + + // Test that unqualified-search-registries is merged correctly + usr, err := UnqualifiedSearchRegistries(&types.SystemContext{ + SystemRegistriesConfPath: "testdata/unqualified-search.conf", + SystemRegistriesConfDirPath: "testdata/registries.conf.d-usr1", + }) + require.NoError(t, err) + assert.Equal(t, []string{"registry-a.com", "registry-c.com", "registry-d.com"}, usr) // Nothing overrides the base file + + usr, err = UnqualifiedSearchRegistries(&types.SystemContext{ + SystemRegistriesConfPath: "testdata/unqualified-search.conf", + SystemRegistriesConfDirPath: "testdata/registries.conf.d-usr2", + }) + require.NoError(t, err) + assert.Equal(t, []string{}, usr) // Search overridden with an empty array +} + +func TestParseShortNameMode(t *testing.T) { + tests := []struct { + input string + result types.ShortNameMode + mustFail bool + }{ + {"disabled", types.ShortNameModeDisabled, false}, + {"enforcing", types.ShortNameModeEnforcing, false}, + {"permissive", types.ShortNameModePermissive, false}, + {"", -1, true}, + {"xxx", -1, true}, + } + + for _, test := range tests { + shortName, err := parseShortNameMode(test.input) + if test.mustFail { + assert.Error(t, err) + continue + } + require.NoError(t, err) + assert.Equal(t, test.result, shortName) + } +} + +func TestGetShortNameMode(t *testing.T) { + tests := []struct { + path string + mode types.ShortNameMode + mustFail bool + }{ + { + "testdata/aliases.conf", + types.ShortNameModeEnforcing, + false, + }, + { + "testdata/registries.conf.d/config-2.conf", + types.ShortNameModePermissive, + false, + }, + { + "testdata/registries.conf.d/config-3.conf", + types.ShortNameModePermissive, // empty -> default to permissive + false, + }, + { + "testdata/invalid-short-name-mode.conf", + -1, + true, + }, + } + + for _, test := range tests { + sys := &types.SystemContext{ + SystemRegistriesConfPath: test.path, + SystemRegistriesConfDirPath: "testdata/this-does-not-exist", + } + mode, err := GetShortNameMode(sys) + if test.mustFail { + assert.Error(t, err) + continue + } + require.NoError(t, err) + assert.Equal(t, test.mode, mode, "%s", test.path) + } +} + +func TestCredentialHelpers(t *testing.T) { + tests := []struct { + confPath string + confDirPath string + helpers []string + }{ + { + confPath: "testdata/cred-helper.conf", + confDirPath: "testdata/this-does-not-exist", + helpers: []string{"helper-1", "helper-2"}, + }, + { + confPath: "testdata/empty.conf", + confDirPath: "testdata/this-does-not-exist", + helpers: []string{"containers-auth.json"}, + }, + { + confPath: "testdata/cred-helper.conf", + confDirPath: "testdata/registries.conf.d-empty-helpers", + helpers: []string{"containers-auth.json"}, + }, + { + confPath: "testdata/cred-helper.conf", + confDirPath: "testdata/registries.conf.d", + helpers: []string{"dropin-1", "dropin-2"}, + }, + } + + for _, test := range tests { + ctx := &types.SystemContext{ + SystemRegistriesConfPath: test.confPath, + SystemRegistriesConfDirPath: test.confDirPath, + } + + helpers, err := CredentialHelpers(ctx) + require.NoError(t, err) + require.Equal(t, test.helpers, helpers, "%v", test) + } +} diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/aliases.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/aliases.conf new file mode 100644 index 00000000000..cb05b275a3f --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/aliases.conf @@ -0,0 +1,7 @@ +short-name-mode="enforcing" + +[aliases] +docker="docker.io/library/foo" +"quay/foo"="quay.io/library/foo" +example="example.com/library/foo" +empty="" diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/base-for-registries.d.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/base-for-registries.d.conf new file mode 100644 index 00000000000..f27ed071827 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/base-for-registries.d.conf @@ -0,0 +1,5 @@ +unqualified-search-registries = ["example.com"] + +[[registry]] +location = "base.com" +insecure = true \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/blocked-conflicts.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/blocked-conflicts.conf new file mode 100644 index 00000000000..5bf892542ed --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/blocked-conflicts.conf @@ -0,0 +1,13 @@ +[[registry]] +location = "registry.com" + +[[registry.mirror]] +location = "mirror-1.registry.com" + +[[registry.mirror]] +location = "mirror-2.registry.com" + + +[[registry]] +location = "registry.com" +blocked = true diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/cred-helper.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/cred-helper.conf new file mode 100644 index 00000000000..fbc5e97b337 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/cred-helper.conf @@ -0,0 +1,10 @@ +credential-helpers = ["helper-1", "helper-2"] +[[registry]] +location = "registry-a.com" + +[[registry]] +location = "registry-b.com" + +[[registry]] +location = "registry-c.com/foo" +prefix = "registry-c.com" diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/empty.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/empty.conf new file mode 100644 index 00000000000..e69de29bb2d diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/find-registry.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/find-registry.conf new file mode 100644 index 00000000000..0abb7d7c7b3 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/find-registry.conf @@ -0,0 +1,62 @@ +[[registry]] +location = "registry.com:5000" +prefix = "simple-prefix.com" + +[[registry]] +location = "another-registry.com:5000" +prefix = "complex-prefix.com:4000/with/path" + +[[registry]] +location = "registry.com:5000" +prefix = "another-registry.com" + +[[registry]] +location = "no-prefix.com" + +[[registry]] +location = "empty-prefix.com" +prefix = "" + +[[registry]] +location = "subdomain-prefix.com" +prefix = "*.so.simple-prefix.com" + +[[registry]] +location = "subdomain-prefix-2.com" +prefix = "*.simple-prefix.com" + +# For subdomain override using dropin registries.conf.d/subdomain-override-1.conf +[[registry]] +location = "subdomain-prefix-1.com" +prefix = "*.not.quite.simple-prefix.com" + +# For subdomain override failure using registries.conf.d/subdomain-override-2.conf +# with unmatched prefix = "*.example.com" +[[registry]] +location = "subdomain-prefix-2.com" +prefix = "*.docker.io" + +# For subdomain override using dropin registries.conf.d/subdomain-override-3.conf +[[registry]] +location = "subdomain-prefix-3.com" +prefix = "*.bar.example.com" + +# For longest wildcarded prefix match in comparison with "*.docker.io" +[[registry]] +location = "subdomain-prefix-4.com" +prefix = "*.bar.docker.io" + +# For longest prefix match in comparison with *.bar.example.com +[[registry]] +location = "subdomain-prefix-5.com" +prefix = "foo.bar.example.com:5000" + +# For empty location with wildcard prefix +[[registry]] +prefix="*.internal.registry.com" + +[[registry]] +prefix="*.com" + +[[registry]] +prefix="*.foobar.io" diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/insecure-conflicts.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/insecure-conflicts.conf new file mode 100644 index 00000000000..636819423af --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/insecure-conflicts.conf @@ -0,0 +1,13 @@ +[[registry]] +location = "registry.com" + +[[registry.mirror]] +location = "mirror-1.registry.com" + +[[registry.mirror]] +location = "mirror-2.registry.com" + + +[[registry]] +location = "registry.com" +insecure = true diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/invalid-aliases.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/invalid-aliases.conf new file mode 100644 index 00000000000..80530ca9883 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/invalid-aliases.conf @@ -0,0 +1,3 @@ +[aliases] +image1="quay.io/repo/image:1" +image2="image:1" diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/invalid-config-level-mirror.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/invalid-config-level-mirror.conf new file mode 100644 index 00000000000..cdcde7e3b25 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/invalid-config-level-mirror.conf @@ -0,0 +1,11 @@ +[[registry]] +prefix = "registry-a.com/foo" +location = "registry-a.com/bar" +pull-from-mirror = "digest-only" + +[[registry.mirror]] +location = "mirror-1.registry-a.com" + +[[registry.mirror]] +location = "mirror-2.registry-a.com" +insecure = true \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/invalid-conflict-mirror.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/invalid-conflict-mirror.conf new file mode 100644 index 00000000000..f710c1350be --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/invalid-conflict-mirror.conf @@ -0,0 +1,12 @@ +[[registry]] +prefix = "registry-a.com/foo" +location = "registry-a.com/bar" +mirror-by-digest-only = true + +[[registry.mirror]] +pull-from-mirror = "digest-only" +location = "mirror-1.registry-a.com" + +[[registry.mirror]] +location = "mirror-2.registry-a.com" +insecure = true \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/invalid-prefix.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/invalid-prefix.conf new file mode 100644 index 00000000000..0103d7ad08a --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/invalid-prefix.conf @@ -0,0 +1,18 @@ +[[registry]] +location = "registry.com:5000" +prefix = "http://schema-is-invalid.com" + +[[registry]] +location = "another-registry.com:5000" +prefix = "complex-prefix.com:4000/with/path" + +[[registry]] +location = "registry.com:5000" +prefix = "another-registry.com" + +[[registry]] +location = "no-prefix.com" + +[[registry]] +location = "empty-prefix.com" +prefix = "" diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/invalid-search.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/invalid-search.conf new file mode 100644 index 00000000000..c31fed19e98 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/invalid-search.conf @@ -0,0 +1 @@ +unqualified-search-registries = ["registry-a.com/namespace-is-forbidden"] diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/invalid-short-name-mode.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/invalid-short-name-mode.conf new file mode 100644 index 00000000000..acf2eb2013d --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/invalid-short-name-mode.conf @@ -0,0 +1 @@ +short-name-mode="invalid" diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/invalid-value-mirror.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/invalid-value-mirror.conf new file mode 100644 index 00000000000..bb0797bd3dc --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/invalid-value-mirror.conf @@ -0,0 +1,11 @@ +[[registry]] +prefix = "registry-a.com/foo" +location = "registry-a.com/bar" + +[[registry.mirror]] +pull-from-mirror = "notvalid" +location = "mirror-1.registry-a.com" + +[[registry.mirror]] +location = "mirror-2.registry-a.com" +insecure = true \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/invalidate-cache.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/invalidate-cache.conf new file mode 100644 index 00000000000..597796f42e3 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/invalidate-cache.conf @@ -0,0 +1,23 @@ +[[registry]] +location = "registry.com" + +[[registry.mirror]] +location = "mirror-1.registry.com" + +[[registry.mirror]] +location = "mirror-2.registry.com" + + +[[registry]] +location = "blocked.registry.com" +blocked = true + + +[[registry]] +location = "insecure.registry.com" +insecure = true + + +[[registry]] +location = "untrusted.registry.com" +insecure = true diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/mirrors.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/mirrors.conf new file mode 100644 index 00000000000..f1012f196a7 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/mirrors.conf @@ -0,0 +1,13 @@ +[[registry]] +location = "registry.com" + +[[registry.mirror]] +location = "mirror-1.registry.com" + +[[registry.mirror]] +location = "mirror-2.registry.com" +insecure = true + +[[registry]] +location = "blocked.registry.com" +blocked = true diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/missing-mirror-location.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/missing-mirror-location.conf new file mode 100644 index 00000000000..481574f981e --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/missing-mirror-location.conf @@ -0,0 +1,10 @@ +unqualified-search-registries = ["registry-a.com"] + +[[registry]] +location = "registry-a.com" + +[[registry]] +location = "registry-b.com" +[[registry.mirror]] +location = "mirror-b.com" +[[registry.mirror]] diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/mixing-v1-v2-empty.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/mixing-v1-v2-empty.conf new file mode 100644 index 00000000000..f049987b608 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/mixing-v1-v2-empty.conf @@ -0,0 +1,10 @@ +unqualified-search-registries = [] + +[registries.search] +registries = [] + +[registries.block] +registries = [] + +[registries.insecure] +registries = [] diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/mixing-v1-v2.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/mixing-v1-v2.conf new file mode 100644 index 00000000000..bd20b3822db --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/mixing-v1-v2.conf @@ -0,0 +1,19 @@ +unqualified-search-registries = ["registry-a.com", "registry-c.com"] + +[registries.search] +registries = ["registry-a.com", "registry-c.com"] + +[registries.block] +registries = ["registry-b.com"] + +[registries.insecure] +registries = ["registry-d.com", "registry-e.com", "registry-a.com"] + +[[registry]] +location = "registry-a.com" + +[[registry]] +location = "registry-b.com" + +[[registry]] +location = "registry-c.com" \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/pull-sources-from-reference.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/pull-sources-from-reference.conf new file mode 100644 index 00000000000..ee50fdc2fc2 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/pull-sources-from-reference.conf @@ -0,0 +1,108 @@ +[[registry]] +prefix = "registry-a.com/foo" +location = "registry-a.com/bar" + +[[registry.mirror]] +location = "mirror-1.registry-a.com" + +[[registry.mirror]] +location = "mirror-2.registry-a.com" +insecure = true + +[[registry]] +prefix = "registry-b.com/foo" +location = "registry-b.com/bar" +mirror-by-digest-only = true + +[[registry.mirror]] +location = "mirror-1.registry-b.com" + +[[registry.mirror]] +location = "mirror-2.registry-b.com" + +[[registry]] +prefix = "registry-a.com/baz" +location = "registry-a.com/bar" + +[[registry.mirror]] +location = "mirror-1.registry-a.com" + +[[registry.mirror]] +location = "mirror-2.registry-a.com" +insecure = true + +[[registry]] +prefix = "registry-b.com/baz" +location = "registry-b.com/bar" + +[[registry.mirror]] +pull-from-mirror = "digest-only" +location = "mirror-1.registry-b.com" + +[[registry.mirror]] +pull-from-mirror = "digest-only" +location = "mirror-2.registry-b.com" + +[[registry]] +prefix = "registry-c.com/baz" +location = "registry-c.com/bar" + +[[registry.mirror]] +location = "mirror-1.registry-c.com" + +[[registry.mirror]] +pull-from-mirror = "digest-only" +location = "mirror-2.registry-c.com" + +[[registry]] +prefix = "registry-d.com/baz" +location = "registry-d.com/bar" +mirror-by-digest-only = true + +[[registry.mirror]] +location = "mirror-1.registry-d.com" + +[[registry.mirror]] +location = "mirror-2.registry-d.com" + +[[registry]] +prefix = "registry-e.com/baz" +location = "registry-e.com/bar" + +[[registry.mirror]] +pull-from-mirror = "tag-only" +location = "mirror-1.registry-e.com" + +[[registry.mirror]] +pull-from-mirror = "tag-only" +location = "mirror-2.registry-e.com" + +[[registry]] +prefix = "registry-f.com/baz" +location = "registry-f.com/bar" + +[[registry.mirror]] +location = "mirror-1.registry-f.com" + +[[registry.mirror]] +pull-from-mirror = "tag-only" +location = "mirror-2.registry-f.com" + +[[registry]] +prefix = "registry-g.com/baz" +location = "registry-g.com/bar" + +[[registry.mirror]] +pull-from-mirror = "digest-only" +location = "mirror-1.registry-g.com" + +[[registry.mirror]] +pull-from-mirror = "tag-only" +location = "mirror-2.registry-g.com" + +[[registry.mirror]] +location = "mirror-3.registry-g.com" + +[[registry.mirror]] +pull-from-mirror = "all" +location = "mirror-4.registry-g.com" diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/registries.conf.d-empty-helpers/empty.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/registries.conf.d-empty-helpers/empty.conf new file mode 100644 index 00000000000..3be4372d107 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/registries.conf.d-empty-helpers/empty.conf @@ -0,0 +1 @@ +credential-helpers=[] diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/registries.conf.d-usr1/no-usr.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/registries.conf.d-usr1/no-usr.conf new file mode 100644 index 00000000000..913fa11f2f5 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/registries.conf.d-usr1/no-usr.conf @@ -0,0 +1 @@ +# unqualified-search-registries is not set diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/registries.conf.d-usr2/empty-usr.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/registries.conf.d-usr2/empty-usr.conf new file mode 100644 index 00000000000..0e4d167a3cf --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/registries.conf.d-usr2/empty-usr.conf @@ -0,0 +1 @@ +unqualified-search-registries = [] diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/registries.conf.d/config-1.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/registries.conf.d/config-1.conf new file mode 100644 index 00000000000..b52add558d9 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/registries.conf.d/config-1.conf @@ -0,0 +1,10 @@ +unqualified-search-registries = ["example-overwrite.com"] +credential-helpers = ["we", "will", "be", "overridden", "later"] + +[[registry]] +location = "1.com" + +[aliases] +docker="docker.io/library/config1" +config1="config1.com/image" +barz="barz.com/image/config1" diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/registries.conf.d/config-2.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/registries.conf.d/config-2.conf new file mode 100644 index 00000000000..695b80b7ca8 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/registries.conf.d/config-2.conf @@ -0,0 +1,19 @@ +short-name-mode="permissive" + +credential-helpers=["dropin-1", "dropin-2"] + +[[registry]] +location = "1.com" + +[[registry]] +location = "2.com" + +[[registry]] +location = "base.com" +blocked = true + +[aliases] +config2="config2.com/image" +barz="barz.com/config2" +added3="xxx.com/image" +example="" diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/registries.conf.d/config-3.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/registries.conf.d/config-3.conf new file mode 100644 index 00000000000..e69de29bb2d diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/registries.conf.d/config-3.ignore b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/registries.conf.d/config-3.ignore new file mode 100644 index 00000000000..65866fd784f --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/registries.conf.d/config-3.ignore @@ -0,0 +1,7 @@ +unqualified-search-registries = ["ignore-example-overwrite.com"] + +[[registry]] +location = "ignore-me-because-i-have-a-wrong-suffix.com" + +[aliases] +ignore="me because i have a wrong suffix" diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/registries.conf.d/subdomain-override-1.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/registries.conf.d/subdomain-override-1.conf new file mode 100644 index 00000000000..b98000c09c2 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/registries.conf.d/subdomain-override-1.conf @@ -0,0 +1,3 @@ +[[registry]] +location = "subdomain-prefix-1-overridden-by-dropin-location.com" +prefix = "*.not.quite.simple-prefix.com" diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/registries.conf.d/subdomain-override-2.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/registries.conf.d/subdomain-override-2.conf new file mode 100644 index 00000000000..87a10a148fb --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/registries.conf.d/subdomain-override-2.conf @@ -0,0 +1,3 @@ +[[registry]] +location = "subdomain-prefix-2-overridden-by-dropin-location.com" +prefix = "*.docker.com" diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/registries.conf.d/subdomain-override-3.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/registries.conf.d/subdomain-override-3.conf new file mode 100644 index 00000000000..f7daf16c6f0 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/registries.conf.d/subdomain-override-3.conf @@ -0,0 +1,3 @@ +[[registry]] +location = "subdomain-prefix-3-overridden-by-dropin-location.com" +prefix = "*.bar.example.com" diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/try-update-cache-invalid.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/try-update-cache-invalid.conf new file mode 100644 index 00000000000..9977a2836c1 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/try-update-cache-invalid.conf @@ -0,0 +1 @@ +invalid diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/try-update-cache-valid.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/try-update-cache-valid.conf new file mode 100644 index 00000000000..0a498afd4e0 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/try-update-cache-valid.conf @@ -0,0 +1,2 @@ +[[registry]] +location = "registry.com" diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/unmarshal.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/unmarshal.conf new file mode 100644 index 00000000000..597796f42e3 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/unmarshal.conf @@ -0,0 +1,23 @@ +[[registry]] +location = "registry.com" + +[[registry.mirror]] +location = "mirror-1.registry.com" + +[[registry.mirror]] +location = "mirror-2.registry.com" + + +[[registry]] +location = "blocked.registry.com" +blocked = true + + +[[registry]] +location = "insecure.registry.com" +insecure = true + + +[[registry]] +location = "untrusted.registry.com" +insecure = true diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/unqualified-search.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/unqualified-search.conf new file mode 100644 index 00000000000..c1137a0a8a1 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/unqualified-search.conf @@ -0,0 +1,13 @@ +unqualified-search-registries = ["registry-a.com", "registry-c.com", "registry-d.com"] + +[[registry]] +location = "registry-a.com" + +[[registry]] +location = "registry-b.com" + +[[registry]] +location = "registry-c.com" + +[[registry]] +location = "registry-d.com" diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/v1-compatibility.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/v1-compatibility.conf new file mode 100644 index 00000000000..c26ee83221c --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/v1-compatibility.conf @@ -0,0 +1,8 @@ +[registries.search] +registries = ["registry-a.com////", "registry-c.com", "registry-d.com"] + +[registries.block] +registries = ["registry-b.com"] + +[registries.insecure] +registries = ["registry-b.com////", "registry-d.com", "registry-e.com", "registry-a.com"] diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/v1-invalid-block.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/v1-invalid-block.conf new file mode 100644 index 00000000000..bcf92486054 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/v1-invalid-block.conf @@ -0,0 +1,8 @@ +[registries.search] +registries = ["registry-a.com////", "registry-c.com", "registry-d.com", "http://schema-is-invalid.com"] + +[registries.block] +registries = ["registry-b.com", "http://schema-is-invalid.com"] + +[registries.insecure] +registries = ["registry-b.com////", "registry-d.com", "registry-e.com", "registry-a.com"] diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/v1-invalid-insecure.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/v1-invalid-insecure.conf new file mode 100644 index 00000000000..d821fb1c5cb --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/v1-invalid-insecure.conf @@ -0,0 +1,8 @@ +[registries.search] +registries = ["registry-a.com////", "registry-c.com", "registry-d.com"] + +[registries.block] +registries = ["registry-b.com"] + +[registries.insecure] +registries = ["registry-b.com////", "registry-d.com", "registry-e.com", "registry-a.com", "http://schema-is-invalid.com"] diff --git a/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/v1-invalid-search.conf b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/v1-invalid-search.conf new file mode 100644 index 00000000000..a9cf15b4529 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/sysregistriesv2/testdata/v1-invalid-search.conf @@ -0,0 +1,8 @@ +[registries.search] +registries = ["registry-a.com////", "registry-c.com", "registry-d.com", "http://schema-is-invalid.com"] + +[registries.block] +registries = ["registry-b.com"] + +[registries.insecure] +registries = ["registry-b.com////", "registry-d.com", "registry-e.com", "registry-a.com"] diff --git a/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/full/ca-cert-1.crt b/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/full/ca-cert-1.crt new file mode 100644 index 00000000000..69151e31003 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/full/ca-cert-1.crt @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE+jCCAuKgAwIBAgIJAOwwZJiWYGNwMA0GCSqGSIb3DQEBCwUAMDExLzAtBgNV +BAMMJmNvbnRhaW5lcnMvaW1hZ2UgdGVzdCBDQSBjZXJ0aWZpY2F0ZSAxMCAXDTE4 +MDgyODE2MDQ0NVoYDzIxMTgwODA0MTYwNDQ1WjAxMS8wLQYDVQQDDCZjb250YWlu +ZXJzL2ltYWdlIHRlc3QgQ0EgY2VydGlmaWNhdGUgMTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBAMhS5F4PXlmXAtnxqdbxVRv/YYqKqg+/YV7y2hRuRZk2 +DVhcc2qCr80he6V9He9esAlGTAk/tLES4HasB27ry4ExZvAivNhf4VLSso8LANf6 +/mFDAK3+istdlZ4hb2f4eAKmKaCEB3GxB1EMxBWB8BiSZhzSLQfMaWBLOt5HKxNv +/7Ha2HOwQqCoqoKR6dg6nt9PV2VLuVsmgI6pKpn9CsXnsplak6hDzUtT71HH80De +GsZsfSApRB/iSWlJie80hDKyP5HK5//qFfRAhlrfdb7wuqrsjdafO4WYskVFtvJy +1eU2jmI/EPO83dWhyW/COiMJNHh+8IPYlDP8tCbm8tdGnqF+pZTe5hlXEXvwJwF0 +jxWlx6MhiDLX2T2Tq/ypOEsrAWFfRtKY+W1Hbm6w+i9vKKhpxkGFvg7Js/oKPif9 +QqKKY6bpERQG9tNZzpU+PcX3y0AyQU1mk4WmlF07p40U2lGddvXwUokEunbvSIKp +W3PINodekHuHdDVMA4bMS1SucJtp4MIPw2so83rfcwY0x2rc8ONWd97rJybqZtyf +DThWjnCUa/QDuAe2G2dVN3O6h0CZD1h9/faWecu7ppRN1x7HMfgjT5p1y+ZvObJS +fQr1AHZFx8lwRBBrAES0ygeuBIzgnCFo5pdeTE7cVbgCb1+5FeLiIhVXjZ9ZESJl +AgMBAAGjEzARMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBAKdM +SjM6m8+v/FwNboB+y0LC5Nu3BOLbSqo4V/OmNh94Lnk3gsGbJTEmNWfZ6Xa6svgx +IcR7ayZz/y2iXF45rbxk1b/UGPe4SR8YqO4z4NnGVjC37HAW1AtoYXsdQTQMjg39 +oGJSfRTn58BQbVj2DMYFK7hZhnn1a/9zQgz5wKlxEYqmF2UPHbERf0X6Nj8aj2gQ +rHmCx6erAKHDtqRA6WBIEC+01bdeq5Ds+i4x5E1XJtkugNY/MWrid1NXPaiWeJeh +HpebfxKXHqY8BKhTothBVJR5N+8YkJSFLCcSIrkZSvdu7vk8ZtuTKVHkjPPeCwKj +iIP0/SDLDE2FIH3VXpkuT4FutNkut8P3DAVVgpKEj3SyzSbPchrbVsYdRJk/iqLO +bRZot5V6W351U/GPvdyedpzykd9NWqVc1j624M8OXzrso48BKuhd2NlzeCHiev2d +VPKkYQzhxrHfQICbLgwVTf9BRPQDhjgzbXVzcEMQEt0eM9bFRhbunlPkcK6zSTeH +q6A2XEXuF4Y5f1azJNPX4x6RsPTRt1JmNUUOowcC+ilW3ALIlIQszzmUzKBaIVko +5A6Z2eih1Fj7AwzjnqErVGMhwHIzHkRc/NxE1s//oxwunqWCapduCHxvH7T5k6Dk +donX0wDmrj980cDo5+X8ZjjroGJvoraSl0QV2e+g +-----END CERTIFICATE----- diff --git a/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/full/ca-cert-2.crt b/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/full/ca-cert-2.crt new file mode 100644 index 00000000000..56580901c1a --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/full/ca-cert-2.crt @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE+jCCAuKgAwIBAgIJAPazFn6MWm78MA0GCSqGSIb3DQEBCwUAMDExLzAtBgNV +BAMMJmNvbnRhaW5lcnMvaW1hZ2UgdGVzdCBDQSBjZXJ0aWZpY2F0ZSAyMCAXDTE4 +MDgyODE2MDUyMVoYDzIxMTgwODA0MTYwNTIxWjAxMS8wLQYDVQQDDCZjb250YWlu +ZXJzL2ltYWdlIHRlc3QgQ0EgY2VydGlmaWNhdGUgMjCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBAKOEn+tOrG03S5LMxH/5LBCdQJ/JYVJCw9daPG1rEZSQ +RavX6NBZ+JhGg9Gb2d724y595UQ81IuiD6VIz8q0p1Pz09kW6AwQHLcMi64Xg9eN +AJ2SnEdpO2mYxydEpReWlkg82ZAgFHGMBUkie2Q07ascxo+MpXd8D4Q12uGVrjbA +BUcVz5xVarhX024hYgbuhvaItD2Bg1bwtZ9uAI6pKbIddkEW4rr4K3xLb5IitOk3 +Gr3Eg9e1ZIX+ZXbgGOgENlXZ4LouwK0aER9TjGIJ/KRTKZxbGG7lZhQ7ycamtjaX +Al1k4zhZ/OkEJF9lsFfQ1KUcPoG6leQw0hMR+6j3iNDHXs3rEhj4brgNIGK5ou0n +XKIaz0uaocYBnUPV0Bd3mHFuReouQIBFezoMGw2f9/SPq+aRwW5z0+xuMTJeDuX4 +J4mr7+Cm+vLLc3hdTOtw8+oHkKIHwVpC6VWEKXLDzfXCTGbq5+at2qR9edd1DVLc +o5wVKh31Oawd/0OxeUN+KRW+txBDLmpIsWxQqTt6S+qRhE3AoOO/3n8jzF/0G6LC +raLxIk/NCFaVMQYxK2PCVaQ/qJA5sNGsiHtcC/xmmKW/+0IGN1WOnJBWWCFoy/3s +CCXICmHEOT01aO3tTfLt67KyiDY9BCDhWTVtcrknturf/52qQAOuSXllwrIf+9QT +AgMBAAGjEzARMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBAB/G +UlpC8F1hq9t25fSA5Jf2P6lJXFX2dm+oJDR/dFHwj/tOU2ladVSae0mWzWnCsTyq +sCbNcdUlJ1X6rQoVFDeMsnooWFo52P56dNRlPCbboyd1mKBQZffR5ASbmMkdcDbM +ZYS5s6XXh80o9HoHKFF95XlBpnGY4GKuvm/LA6EikDcrcDrzwokfNJYEwjTKblh/ +QIEdnAeNNohpSIm4flDmJriUwnZxKjEzLcxPbII7kWJwwa/EAySxCfstGJ4KY6Cl +gE8a/bJ/R3t/g9ZrHyFxvKkVJ8ZdOE4KX3zbc/Hn2VVlL0/VBbenRuyt9DgSHY6I +ghPIpixxI35h4HcmDKD+3Ga6tKO3WNZgTLF66+4UdtvQ9XEGI3l1u9HqNsnYkapi +uzPHtIw7MZvNTj1ado4mun5k63Qgq0AsAF6Zffgvl7SNg7LyezMrs4+pythfvm8a +c/blO06Ue2hIasjFGDyYGVGXnv1xuQE8zgQ/Ye4e5VRoa0bNJqLPtnH3s9S6+i8E +sCtZYk4AZDMSRPE3jlUWdJ6yLEgdrqvtSuPZEX8sc0XnhPnM/OtzfGpRIID+IYVc +hqDdB6zpnJ6D4ficmWVRY+HLI3XLKHcTfg1JDfXHK9guNXTd+h1fwtk64XBhVLaf +YK5NHbrYyeuaY40cP2SqiDAhoSzzPpuronDjz4Nh +-----END CERTIFICATE----- diff --git a/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/full/client-cert-1.cert b/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/full/client-cert-1.cert new file mode 100644 index 00000000000..bbf4d637f60 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/full/client-cert-1.cert @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE6DCCAtACCQCF7Uo5GTGOezANBgkqhkiG9w0BAQsFADA1MTMwMQYDVQQDDCpj +b250YWluZXJzL2ltYWdlIHRlc3QgY2xpZW50IGNlcnRpZmljYXRlIDEwIBcNMTgw +ODI4MTUzNjI5WhgPMjExODA4MDQxNTM2MjlaMDUxMzAxBgNVBAMMKmNvbnRhaW5l +cnMvaW1hZ2UgdGVzdCBjbGllbnQgY2VydGlmaWNhdGUgMTCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAKEhMzeyArbV+VuNVP0ZeOrEDNzHooXY9yST0Ftl +blXifm8uFrCx+QytGJEFuM8K/Z1AR6MH0x6yXtYsqyCkM3+tFHlErdjm9Zxe+4Oh +e+3/NImmnyfMt7N7solyfoGm8RTu5/NhGCjUTVJxS5xwO5rg7UXNHNBSVVyW4VSQ +cAUs/j8zqajdAOP+3rE1A9rPLRZTUkuuLZqSPvlth2d1EMeQmvi2EUXoArQ1JKDm +FxdpeUJ7qoUyrGuvaSY9jTyvAaqRzBXA6bHZXUbeaCrB1feOY0NEmQdN0+knV/+s +an7MVpTrIRtsL+RraGCJX8hJFoTKcc7SiFKUK7Kv5KeqP6GQkv347HlXLZ9Ay+Do +b4bRyBu7tRnkoBlCtXSF+7MEFd82le0K9PRqqf4g4riSvPvWdTrvWxJEv1ntD8Mr +DRtBfw8MoMTdqTkrUCKzoHKzl2BHb9RzjOuRQpcT4tfNHn0UIn280CbDqzeNnqfp +x+1dXLTPiEVRz/BSekcjYcXjqrZPJZ6pm/f7IA041nPq4L/pzZH8NSyMf2K5CjRQ +P+ThhHqY2oYQRk73PezZVLFUk1hIuiuyQaoQqLplvNNzeXslyoY02UgjUYm4VHQz +ctHD5sETbNJO4oLm4hCVkGZUzogMd3Rki+f6TM0jL0LqCkWzPIsvkdVLrKk2xZOF +YPH1AgMBAAEwDQYJKoZIhvcNAQELBQADggIBAG2SYSh6nYy6BR8XpdprRUwyUmDt +v2Ee14ItrBFVjlyzJV+hVG9rjB4IUu+Xmj6J78m0y3ONrYS1bXfkIifJULVZFnBw +e3Y4l2ke1K0GaMZNvr8JhKB4uX9P/WL5kmYCV6F7YgFqCBr+YcQHRBHYbpVmkc1S +YFDzXmBPTI4OoYrNBHyjlF9zgLY40ZsL1Af/j6P11ufdNrqdiCJ6RcfCdsTMORd0 +H+xyCjIX54A+V5CWhUix53gQEuN8W6zPyRtRzV+zLX05bYIC7gYqQ+j6qvh6dEkF +zNr0YGLw2ecM/KLgocPPsaRGeAnXadnIP4Zt0YynAme0jSqYHK3JJD8qWZdj1QOd +bJ9twiO+4G+UC2cMZ/OaujVpHr8QjSppHEb+uw4mUqiQtzXBH42DjKeUZFA3MXbp +PWg8xmeuxS0uhb/j6Ookg9wREjcdb9dja7Ym/qslH5aix9CbULr4H6vllwMnFgiN +cKXuqupnvCihxVe2n1RHQetvgacOyMoi9/1AwJ6WLnHU+8KHNSdlxD9JrzYQ+WeZ +N82yqBZkKbmESj9BZuRT1Pl7y0qWAPmB9HiAr9A1LenoH/ZG2JBSCGiraUb6zxvg +Ros7TQYAh1C3dgdwyiISVvCblVQdn4nFbYCBwFWbPrpMM/PNKQ8Hmdj/2rRKPX6q +Ho8jfpXhdO8eMcbJ +-----END CERTIFICATE----- diff --git a/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/full/client-cert-1.key b/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/full/client-cert-1.key new file mode 100644 index 00000000000..6bbafc27535 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/full/client-cert-1.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQChITM3sgK21flb +jVT9GXjqxAzcx6KF2Pckk9BbZW5V4n5vLhawsfkMrRiRBbjPCv2dQEejB9Mesl7W +LKsgpDN/rRR5RK3Y5vWcXvuDoXvt/zSJpp8nzLeze7KJcn6BpvEU7ufzYRgo1E1S +cUuccDua4O1FzRzQUlVcluFUkHAFLP4/M6mo3QDj/t6xNQPazy0WU1JLri2akj75 +bYdndRDHkJr4thFF6AK0NSSg5hcXaXlCe6qFMqxrr2kmPY08rwGqkcwVwOmx2V1G +3mgqwdX3jmNDRJkHTdPpJ1f/rGp+zFaU6yEbbC/ka2hgiV/ISRaEynHO0ohSlCuy +r+Snqj+hkJL9+Ox5Vy2fQMvg6G+G0cgbu7UZ5KAZQrV0hfuzBBXfNpXtCvT0aqn+ +IOK4krz71nU671sSRL9Z7Q/DKw0bQX8PDKDE3ak5K1Ais6Bys5dgR2/Uc4zrkUKX +E+LXzR59FCJ9vNAmw6s3jZ6n6cftXVy0z4hFUc/wUnpHI2HF46q2TyWeqZv3+yAN +ONZz6uC/6c2R/DUsjH9iuQo0UD/k4YR6mNqGEEZO9z3s2VSxVJNYSLorskGqEKi6 +ZbzTc3l7JcqGNNlII1GJuFR0M3LRw+bBE2zSTuKC5uIQlZBmVM6IDHd0ZIvn+kzN +Iy9C6gpFszyLL5HVS6ypNsWThWDx9QIDAQABAoICABfPxTbk2pvUcT5DW5zvp7vh +7xitc+odzyAbq/3ltnGAwDiD8hx4oJpr9iZwVwhZ0nLrrep83Ik7yOITxdJUEbw7 +grwNFzfnocJTEw43zg202jnBYuHJ0hf3HsJLJkDYv+XdDHAeGCjofujBD3B18YkI +1merUSfEExNUxMXvdm59YLFMXDU1O811u8kqUYCDf2E+PPosKlceZ6oKsqjqVul3 +CD/bACB5kfS5qckRV7ZBAwd9KQz4GRzs1jgtfnLKVg+z7hoE4UREIBG6CmDPNmSY +KTkmqQq4SKm7A7kn7LECV4U4XjMkQMubx9gEVSBPFMHY/QqYeEbj2LVWYw8YDMEC +dWrAR9+kg5Y5LLlxTEHv3nJSAwWf2MmSxZGO3Gav9vxMv0+lUC90A874y61bnebf +ombV+WfrpVrSQd3UvDSwGxbKIdIsqz/6VTd1qrURIRNiL57JfbeQVo7p/cRjFsk1 +yzhPnFqVjgC3xt2bpzVMhFl1jovpk7YHctm305Oh1L5+O/OTILyuzVQOLVlmQlSh +69amtdWioUD+kuzAyxplue/achpk9ylB5/p2I5yHx7i4UOs8SVBHIgSi+6y8CMPi +1f9U8r+MbziLPP8fX4KqltZ15gMJRQ5yAjiaLOEq1ysvcdNxnSEHpDppN2XqDZXX +th5ma+zuN/Y5eXLouIv9AoIBAQDV8sG8lskAr6aOlPhWyPgrOSrIH0Lvcc2/+Pn+ +SkEry5HsZKXQhxbLb3WGGyVrocwv4RIRrsfqMGUN0HMbJbZLVTUTJ6uRDZe7zdP8 +FlY8q453XClOXxkXjtqg/ekstm7e0ZtsDXICzajDKZBt9iH1RtWshKlGncBE8w47 +gaMthbw8a2694ov34XC2A8AtrI/NOwXT1j2hhZ5oBVx8sDfNPgL/Tbklv90vWWQ8 +uTOlrQCekLvcdAH+SnyBhq/9sOKmsR8O+Cq8hrKeRyMCRe50612wO3V3w6Ug76R0 +4qH93hHH5fF/tT3ix4nUemjDC2fFt0dHBd8u0FvqzxFADuJTAoIBAQDAzMTLbKkq +FEY6tsXxDcv/Vd5AY7TjuXOe4hwner25KUfTSEpo+0KQGi4wZcUhC9edqea4trvu +8aTD2E1t7HwU9qC3MLfbCsoFTC3PuI9zZuBYBQ1QJSab5xaLCSKEg/Xf+uVcFSTv +lTRYHvAIUFQtH6A6xuGjOFSa3d0xbgOJYlGDSsdZ6eeEocpU86X6YEfJ4PU9RCXb +6XfFqxG4qBhEnJiGVaqNSFkrlyEScS5ZOZeNJfxsxaHtddopPsoZXKWeG5enPQ+S +i2BGZEVd73vgASYz8fqb9j8zwcUrL6ycSiWvWx2r94DPZ9DKIBzAKNF4X0dUkXn7 +NvRCloWYyGGXAoIBAC4AKw2scf1Wxv4DI74tKcbJXNx4dEDdfDqZgs7dElQjSfXE +0i4azZjiFyfFcG9K84cb4nGw3cUJsMCeoBEnM6HQ6T98GRRwEr7Li5e5CcOzs0cQ +psT2B4QcL//LeDYn54C7GqrJ3UijBXUo26f48uY0275jK65GPs/UXqwGvJoOFiH7 +i20CZ8vdSgmolsp7PtQGq8MXXlr7Ssrc1Dzu+qCDg8t236cxMJJ8quOvgl5N60Ms +pWfJ/Z+6TjjfR7nJLYJftOjYDQBKCd+kNe/IL3QuIw/ASQp+I2QIgpirHd9ivvn3 +A8zMoEjBVG49/4ZoTmChfo7TwV/kZs7+xJu8V4cCggEAMIsePa3lRfAG4r+kRRZZ +N7fyFrpEEFZgUy/RMOuJm3ZWXE9GVPp2fvvoh00IflKR/mOJ8RYpaXc0Kg9rJ3M0 +pr6WJqnAkQk9ZmoQ2s04aTEM6XyUJorEFtrvZSBurXjgTn3IhA/a1ev2Wv2nKCC5 +oQbmfAYZR9RmEPwttkVh5JR5u5n3aZ8oKM/nts4GC210hdL7TGq9MYquGnoBI0JH +ofbfvGsTXzFJKl2J+S/AIL0MeICI9wYnyDc1L89caVhXZuDBpV0Nb7NiETcr9APZ +Z8RCTy/tDpnIvgpaz43YYx2UMXu6k9LkftQ/0LSXKJbebvVma1eZo/PpOl7V0msZ +jwKCAQEAljJQnDUWiK73B1vMLjeEwuy/p86p43xCXGMW1rX6+AayYt3/ODNmKh8s +AhOF2Tl0dDJTBibJcpcoGCKm4nL3k9tO23U9omrBz5obRDl4To7l+ALv3x+tf+Fq +b/nVkZKhyUh99RneOjOfpEI6Cd4ffQkXA05/bFGdVVaMJ0yzeC8qQ/QGbyAwpZiL +c1e7Kju5uv+rT4czqKmQ+YsKpSM1Xjz9Mzoxs5E3OCdXSYsv7oo/sS1aXunQTZ6L +xv1M+F9YlCtgo8+1IdlYvcFb4WusAIDf3xjO1bDCvlYzv5JBKWtyO3BVKkfWzSx1 +yKIoxKzIpzNh5dHk6iIDjp1B/YU6fA== +-----END PRIVATE KEY----- diff --git a/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/full/client-cert-2.cert b/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/full/client-cert-2.cert new file mode 100644 index 00000000000..5047957ff6b --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/full/client-cert-2.cert @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE6DCCAtACCQCZd3nR9D1jPzANBgkqhkiG9w0BAQsFADA1MTMwMQYDVQQDDCpj +b250YWluZXJzL2ltYWdlIHRlc3QgY2xpZW50IGNlcnRpZmljYXRlIDIwIBcNMTgw +ODI4MTUzNzAyWhgPMjExODA4MDQxNTM3MDJaMDUxMzAxBgNVBAMMKmNvbnRhaW5l +cnMvaW1hZ2UgdGVzdCBjbGllbnQgY2VydGlmaWNhdGUgMjCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAPQITxwSAieNkNlDDcZUlGeiQmm7tEF9TKqCF06j +LvM+O8nA/DmNIIw84mAeuLH2/vv7jkS3Hz5MEj1duaiAAzhjRrcHTLj63ttcpomP +yc5OyW1GiuVYWKLKdZWRPaiSBeIV+uCGMaxvAzPivYiNz1GhYYt0Jf84OZm8oCZT +HEI/jNRp56nYQDIAW3dwf6LpQ2dyc3n1O2eysDp02AvWfIr9w8agUgMykWziN2ZX +c3fs/8UJ1Ta81YTuuuwTfEB2TtUmzxYql4WjxzXOL/rN7PqlwG7SZ1q6hoACP4K7 +v4tu7cXq79F16xHoKolJaIHnCznZrK3Hmp846/+x0VWn4Ic05f3qB89WSl7i6oa2 +pdgDE5bteSFtEhNXvzy+eobcKqhhqT3TF6oQiD9ufz2s5eTRJmu31sVUOW2+2LaZ +QipQu7G+awpqKh/k95GZNTXhWrDP1OQTtzYZui+l9Ri4lqM1LAlLb35zo6rqUj1A +HfyQrwADFzRQRY/zPijI8I8fnfChGLuW3q/WC0CyrTIkviE/JGY7XmmyT/KxABhY +rDCHWHXQ4nz14PulRBtvTJfMhHBcMM8guYzleeeGrcHX5y5IG7JR1QL+Y+h2zW0d +29RbusXKAkaN7SqlAGB8NIwU5x/Y9UWD0ar9IEXQT2bDkXHWTk2qvbAXwvqkv3r5 +qbB9AgMBAAEwDQYJKoZIhvcNAQELBQADggIBAD0T3FqFhuJ5OGbYaay5Iw5EKLS+ +njNIDNvsy9Q8+F1aa/F27KJfi8x8EQj1YabwOt1gS201jwJshC+AirrJUxdGBJqC +cVDPGeqY+zMKd+17ZgWKza81Y9qYBjx01v//Et5YYKmeS8q3xTsvbloJJ5D7vx5y +VcNwnO7yx5/IMDWCIAbw5j2BikILW0gMCfBK90o4z7Le7kPFLreLiUCfXYZjfbT7 +bT2v8Oy0OISVNLQjajxepK5+C9Qupaj5nL0GtTj37FOs6rulcWEWqX+kGXSctvrA +nuzcjGUkuQBOcMjEUaYRKLZ+Tghla4pFgJLrfKQgW+5Mahbnz3ehvzDc3LcScYCj +u0qyP+w5rW8/Tm9vE9QqwblUX7wZ4/zqTDSv5spdHi5x4Q77MomjDEfP83QnEAhg +Y4wixJBas64227rxJJQT30C2QcuwYMz4STQgjSGDPfomUr8tVPM8JcU4pq+fg7g9 +T0MvfNAWgMhUZllhxTntbHVbv43A2p3eEE0fuW5SOJyAvt9ROZMvQcDWJfw0TDey +dn9+Bz7CMbJvZAoV8P4Gm1+iuWZhUWBG7FdrXEzhbDh2GkSmd3jmUSISVA1G061b +5QVkW5NAr2jZrWhyTXiL0AFbD7QNKTJHma/IcRoJlxQ9rncIdgPLMNBbjridb4dn +PllRbfAWuBgV7vLu +-----END CERTIFICATE----- diff --git a/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/full/client-cert-2.key b/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/full/client-cert-2.key new file mode 100644 index 00000000000..998bc5e6136 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/full/client-cert-2.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQD0CE8cEgInjZDZ +Qw3GVJRnokJpu7RBfUyqghdOoy7zPjvJwPw5jSCMPOJgHrix9v77+45Etx8+TBI9 +XbmogAM4Y0a3B0y4+t7bXKaJj8nOTsltRorlWFiiynWVkT2okgXiFfrghjGsbwMz +4r2Ijc9RoWGLdCX/ODmZvKAmUxxCP4zUaeep2EAyAFt3cH+i6UNncnN59TtnsrA6 +dNgL1nyK/cPGoFIDMpFs4jdmV3N37P/FCdU2vNWE7rrsE3xAdk7VJs8WKpeFo8c1 +zi/6zez6pcBu0mdauoaAAj+Cu7+Lbu3F6u/RdesR6CqJSWiB5ws52aytx5qfOOv/ +sdFVp+CHNOX96gfPVkpe4uqGtqXYAxOW7XkhbRITV788vnqG3CqoYak90xeqEIg/ +bn89rOXk0SZrt9bFVDltvti2mUIqULuxvmsKaiof5PeRmTU14Vqwz9TkE7c2Gbov +pfUYuJajNSwJS29+c6Oq6lI9QB38kK8AAxc0UEWP8z4oyPCPH53woRi7lt6v1gtA +sq0yJL4hPyRmO15psk/ysQAYWKwwh1h10OJ89eD7pUQbb0yXzIRwXDDPILmM5Xnn +hq3B1+cuSBuyUdUC/mPods1tHdvUW7rFygJGje0qpQBgfDSMFOcf2PVFg9Gq/SBF +0E9mw5Fx1k5Nqr2wF8L6pL96+amwfQIDAQABAoICAENiu/2jV2b2p4XnBzm6B1Tq +sG4j/+2JnQ8EZ002sHNIvxfCK6P0vYUcFuK1+QhjjRul5i5BZuY7+VlqtSa7WqhK +ea9224/E7p8iYXZg9zf595MuRJJ6J9ekEn4foigXUnqQ2TsAs1zLNtYwCWhYyGPh +LV9lzkxAiV9Tf+G1V3tPsyLAr9DtvyRPhvJfZU9mHE/Hge2ucx36cMFjnHkAXFnB +IzuI77ykTRYFDYk8Is19607MnUzFLizMA1/HLatbP/+J/OtBDaBAjnTkH8mg7Yx1 +EsNHZHTgRt24QTNnhsgI9K2PZ3OunR/PvVc1px8f3rC11AIUuTS4ciqkPorplY0W +Wcgy9+saF2EL6Peb5zaWTAiCewrcI2ME9G6ytZtWovHzJqyE4iJMaQtLeczoaTk5 +lkl8r+zkAWGnxb6Xz1qd1UuAyXfZO3Ux7shxDJKxoQrgkHvAyEHkyBhJe3ghKElh +C+mGr1z15R+buL8daUC+SG2z0H9gTcYiOcOznGUhtOgR2EPalAC/xKEtYjsEHf3F +VtljEu29KPYcLokBFcI35Tij9KVfGJePMAEji8lLqrImwpPzsoPEdUVx7jvdy6gt +hCsgIo8Vpd/3D12Q4ul+E7ztsWX6wYoxIYGK2/KSAjd0fpla/UFwkrpSSnqkGLR0 +qhoxZz1158vuAtQCb7DdAoIBAQD8bHdBmWovRRywjPaLGPngVAkR4z5yo1OM9lRE +So6O9zyZvk7cJ4JQYpS3o1nXY9mw5YAG2qPrArXBIWwHq0vi7gkhng7gbmn/1j7o +ZrgEE5z//tt0Gl5Yqs35uiMcOlTrQ/1DsHWfJDw6udkQG5EwyrNRvcxaeXGx9dWc +Yzfq93WODSFWhzhgy0CG0q2Zj/VtgoO6gwkZI9KX9X7ELKyCfIqczqMYge5uxp0l +dTS6kNrhUACGCU5yBREKX/XwNxreFS6AuCG0KhBjwX3NeuBfKC2yGAOxr91Yz3l4 +o91CKFXDVXWtpvJCW/zIaeSfByqhgBYUpZPg4VniNqMheB87AoIBAQD3fWiLbidV +lRQ0O4jjAxtsUBKC95oxz4ipDerLlEAL7WA8h0eU05BLv8Nn5Ql5N7m/LTM6XWsg +cWMObk6UDS4N1pR7Sg1sPAlzK52x8CYLDswnYWjeejCP5yY8v8u3NCqCPuxvDlPp +0InR82xRunFEEG9ukC06krGDyMWCwaRbQRJBkRagBvUJA3/A+ZAC2vdN5WhPtZT3 +LT01T2Olk19URLgFTo1hia2o6L5cq7TXeNsZoNfLlaSV33/+fnYqb3F0SmDX+mOV +8zRV4bp9Qdc2vNkdzzC8s2EXr3UBl4miT/ovV0X5v+KPyfH2Rf/Hf0AoBhiHWRIO +NT9L912QFOOnAoIBABzXR8j2/mroOp7xfDnoYKSA9YhVrozqnGE+w+IJosAy/3mR +hPEikoEcwmE5CMrTXcwYbMhbst7nMF0gtHcr2z/Scriklo6ODw5sLEPheKT+mLGn +LOvXF3CKE361Bc3z3EAFRKq3PrkwKrGLCoIMpEou3s44IWE0/wiWThHQRFNUctoI +Jgb87DQjBPxilfM1v5UDlIl6708wCJ/ULOe9Mvi1wiCoe3oRXmzJxKrC+YNXiaq0 +uVqXNZ9RdOD6ld8cbLVzNhz+7Nro83ZyZS1VHM3CiXYPyFxE+8Vp7zcZge5NLX9k +BE4TBRsP55H+h2CkMPrC58L0KDFJjjuKgpkQYIECggEAb5scNBRepJdv3wYh+s2B +5lxNnMXvwRqntCTUhy25nCdVyLXwr8qgPaiihA2jMgjROMc3XNCX6K95Th6sTNOM +uyzTFK4WU4LXeBppKL71SPNJWVDyK7HKiHpioe6T/XAG42lg0cwSR1SFcipl5I1B +WsJWnfNikhFo/9bgStDsP0Ke6vZ0z4GTqpbrW6ivKrp336beXWOzY6wA+DNu6lIF +IUlD+xCrbRrbN1qNzdiY2rpjg7Em32YCLJv3alq5CvXqodiQx5Tgp9Re+4Opx6aT +WNncxzaR8eaqmDXFfAxMQufyGLswkSnZD6Kv/LEgYWSfF+13zkF6hPG6M5W/maPx +7QKCAQEA+EH/bZrfFIthC4pj2PZ7yR6AjvB0CUEc+s2Vk60kGCx7Ykl4OZfxvHHf +A/4890Lb1+lEEo7+mBqq3IOOWKKYz+K7a81G668X3KltmJd3twMWzDy9+RN2sGbY +ww3GQqS2B99N/E5e1N3TLm01aeqEUJqyYqsT0cx+vGQyiIqPF/1SLSqy0ZAl4ZrV +Zb+Zl/EJxi+wPYuax90HFgHRc1RAkN5YDicLlmiPCUWjx9gPN5LOXKMQmh7J7cs1 +n1YP3erz76BQA9q0dkJmHr54FXxkBaFR3SGvIjxjArkNSvl2GuT73kgfbVFfT5QB +Kesl5Q7sHOaJXry12QuWE5/Kj4absA== +-----END PRIVATE KEY----- diff --git a/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/missing-cert/client-cert-1.key b/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/missing-cert/client-cert-1.key new file mode 120000 index 00000000000..c8cf88db8a6 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/missing-cert/client-cert-1.key @@ -0,0 +1 @@ +../full/client-cert-1.key \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/missing-key/client-cert-1.cert b/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/missing-key/client-cert-1.cert new file mode 120000 index 00000000000..2c62e461110 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/missing-key/client-cert-1.cert @@ -0,0 +1 @@ +../full/client-cert-1.cert \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/unreadable-ca/unreadable.crt b/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/unreadable-ca/unreadable.crt new file mode 120000 index 00000000000..4cd6460f62b --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/unreadable-ca/unreadable.crt @@ -0,0 +1 @@ +/this/does/not/exist \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/unreadable-cert/client-cert-1.cert b/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/unreadable-cert/client-cert-1.cert new file mode 120000 index 00000000000..4cd6460f62b --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/unreadable-cert/client-cert-1.cert @@ -0,0 +1 @@ +/this/does/not/exist \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/unreadable-cert/client-cert-1.key b/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/unreadable-cert/client-cert-1.key new file mode 120000 index 00000000000..c8cf88db8a6 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/unreadable-cert/client-cert-1.key @@ -0,0 +1 @@ +../full/client-cert-1.key \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/unreadable-key/client-cert-1.cert b/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/unreadable-key/client-cert-1.cert new file mode 120000 index 00000000000..2c62e461110 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/unreadable-key/client-cert-1.cert @@ -0,0 +1 @@ +../full/client-cert-1.cert \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/unreadable-key/client-cert-1.key b/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/unreadable-key/client-cert-1.key new file mode 120000 index 00000000000..4cd6460f62b --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/testdata/unreadable-key/client-cert-1.key @@ -0,0 +1 @@ +/this/does/not/exist \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/tlsclientconfig_test.go b/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/tlsclientconfig_test.go new file mode 100644 index 00000000000..80278146429 --- /dev/null +++ b/vendor/github.com/containers/image/v5/pkg/tlsclientconfig/tlsclientconfig_test.go @@ -0,0 +1,136 @@ +package tlsclientconfig + +import ( + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "os" + "sort" + "testing" + + "github.com/containers/image/v5/internal/set" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSetupCertificates(t *testing.T) { + // Success + tlsc := tls.Config{} + err := SetupCertificates("testdata/full", &tlsc) + require.NoError(t, err) + require.NotNil(t, tlsc.RootCAs) + + // SystemCertPool is implemented natively, and .Subjects() does not + // return raw certificates, on some systems (as of Go 1.18, + // Windows, macOS, iOS); so, .Subjects() is deprecated. + // We still use .Subjects() in these tests, because they work + // acceptably even in the native case, and they work fine on Linux, + // which we care about the most. + + // For an unknown reason, with Go 1.18, as of Mar 15 2022, + // (golangci-lint) reports staticcheck SA1019 about using + // the deprecated .Subjects() function, but the //lint:ignore + // directives are ineffective (and cause extra warnings about + // pointless lint:ignore directives). So, use the big hammer + // of silencing staticcheck entirely; that should be removed + // as soon as practical. + + // On systems where SystemCertPool is not special-cased, RootCAs include SystemCertPool; + // On systems where SystemCertPool is special cased, this compares two empty sets + // and succeeds. + // There isn’t a plausible alternative to calling .Subjects() here. + loadedSubjectBytes := set.New[string]() + // lint:ignore SA1019 Receiving no data for system roots is acceptable. + for _, s := range tlsc.RootCAs.Subjects() { //nolint staticcheck: the lint:ignore directive is somehow not recognized (and causes an extra warning!) + loadedSubjectBytes.Add(string(s)) + } + systemCertPool, err := x509.SystemCertPool() + require.NoError(t, err) + // lint:ignore SA1019 Receiving no data for system roots is acceptable. + for _, s := range systemCertPool.Subjects() { //nolint staticcheck: the lint:ignore directive is somehow not recognized (and causes an extra warning!) + ok := loadedSubjectBytes.Contains(string(s)) + assert.True(t, ok) + } + + // RootCAs include our certificates. + // We could possibly test without .Subjects() this by validating certificates + // signed by our test CAs. + loadedSubjectCNs := set.New[string]() + // lint:ignore SA1019 We only care about non-system roots here. + for _, s := range tlsc.RootCAs.Subjects() { //nolint staticcheck: the lint:ignore directive is somehow not recognized (and causes an extra warning!) + subjectRDN := pkix.RDNSequence{} + rest, err := asn1.Unmarshal(s, &subjectRDN) + require.NoError(t, err) + require.Empty(t, rest) + subject := pkix.Name{} + subject.FillFromRDNSequence(&subjectRDN) + loadedSubjectCNs.Add(subject.CommonName) + } + ok := loadedSubjectCNs.Contains("containers/image test CA certificate 1") + assert.True(t, ok) + ok = loadedSubjectCNs.Contains("containers/image test CA certificate 2") + assert.True(t, ok) + // Certificates include our certificates + require.Len(t, tlsc.Certificates, 2) + names := []string{} + for _, c := range tlsc.Certificates { + require.Len(t, c.Certificate, 1) + parsed, err := x509.ParseCertificate(c.Certificate[0]) + require.NoError(t, err) + names = append(names, parsed.Subject.CommonName) + } + sort.Strings(names) + assert.Equal(t, []string{ + "containers/image test client certificate 1", + "containers/image test client certificate 2", + }, names) + + // Directory does not exist + tlsc = tls.Config{} + err = SetupCertificates("/this/does/not/exist", &tlsc) + require.NoError(t, err) + assert.Equal(t, &tls.Config{}, &tlsc) + + // Directory not accessible + unreadableDir := t.TempDir() + defer func() { + _ = os.Chmod(unreadableDir, 0700) + }() + err = os.Chmod(unreadableDir, 000) + require.NoError(t, err) + tlsc = tls.Config{} + err = SetupCertificates(unreadableDir, &tlsc) + assert.NoError(t, err) + assert.Equal(t, &tls.Config{}, &tlsc) + + // Other error reading the directory + tlsc = tls.Config{} + err = SetupCertificates("/dev/null/is/not/a/directory", &tlsc) + assert.Error(t, err) + + // Unreadable system cert pool untested + // Unreadable CA certificate + tlsc = tls.Config{} + err = SetupCertificates("testdata/unreadable-ca", &tlsc) + assert.NoError(t, err) + assert.Nil(t, tlsc.RootCAs) + + // Missing key file + tlsc = tls.Config{} + err = SetupCertificates("testdata/missing-key", &tlsc) + assert.Error(t, err) + // Missing certificate file + tlsc = tls.Config{} + err = SetupCertificates("testdata/missing-cert", &tlsc) + assert.Error(t, err) + + // Unreadable key file + tlsc = tls.Config{} + err = SetupCertificates("testdata/unreadable-key", &tlsc) + assert.Error(t, err) + // Unreadable certificate file + tlsc = tls.Config{} + err = SetupCertificates("testdata/unreadable-cert", &tlsc) + assert.Error(t, err) +} diff --git a/vendor/github.com/containers/image/v5/registries.conf b/vendor/github.com/containers/image/v5/registries.conf new file mode 100644 index 00000000000..d85befbf9e5 --- /dev/null +++ b/vendor/github.com/containers/image/v5/registries.conf @@ -0,0 +1,77 @@ +# For more information on this configuration file, see containers-registries.conf(5). +# +# NOTE: RISK OF USING UNQUALIFIED IMAGE NAMES +# We recommend always using fully qualified image names including the registry +# server (full dns name), namespace, image name, and tag +# (e.g., registry.redhat.io/ubi8/ubi:latest). Pulling by digest (i.e., +# quay.io/repository/name@digest) further eliminates the ambiguity of tags. +# When using short names, there is always an inherent risk that the image being +# pulled could be spoofed. For example, a user wants to pull an image named +# `foobar` from a registry and expects it to come from myregistry.com. If +# myregistry.com is not first in the search list, an attacker could place a +# different `foobar` image at a registry earlier in the search list. The user +# would accidentally pull and run the attacker's image and code rather than the +# intended content. We recommend only adding registries which are completely +# trusted (i.e., registries which don't allow unknown or anonymous users to +# create accounts with arbitrary names). This will prevent an image from being +# spoofed, squatted or otherwise made insecure. If it is necessary to use one +# of these registries, it should be added at the end of the list. +# +# # An array of host[:port] registries to try when pulling an unqualified image, in order. +# unqualified-search-registries = ["example.com"] +# +# [[registry]] +# # The "prefix" field is used to choose the relevant [[registry]] TOML table; +# # (only) the TOML table with the longest match for the input image name +# # (taking into account namespace/repo/tag/digest separators) is used. +# # +# # The prefix can also be of the form: *.example.com for wildcard subdomain +# # matching. +# # +# # If the prefix field is missing, it defaults to be the same as the "location" field. +# prefix = "example.com/foo" +# +# # If true, unencrypted HTTP as well as TLS connections with untrusted +# # certificates are allowed. +# insecure = false +# +# # If true, pulling images with matching names is forbidden. +# blocked = false +# +# # The physical location of the "prefix"-rooted namespace. +# # +# # By default, this is equal to "prefix" (in which case "prefix" can be omitted +# # and the [[registry]] TOML table can only specify "location"). +# # +# # Example: Given +# # prefix = "example.com/foo" +# # location = "internal-registry-for-example.net/bar" +# # requests for the image example.com/foo/myimage:latest will actually work with the +# # internal-registry-for-example.net/bar/myimage:latest image. +# +# # The location can be empty iff prefix is in a +# # wildcarded format: "*.example.com". In this case, the input reference will +# # be used as-is without any rewrite. +# location = internal-registry-for-example.com/bar" +# +# # (Possibly-partial) mirrors for the "prefix"-rooted namespace. +# # +# # The mirrors are attempted in the specified order; the first one that can be +# # contacted and contains the image will be used (and if none of the mirrors contains the image, +# # the primary location specified by the "registry.location" field, or using the unmodified +# # user-specified reference, is tried last). +# # +# # Each TOML table in the "mirror" array can contain the following fields, with the same semantics +# # as if specified in the [[registry]] TOML table directly: +# # - location +# # - insecure +# [[registry.mirror]] +# location = "example-mirror-0.local/mirror-for-foo" +# [[registry.mirror]] +# location = "example-mirror-1.local/mirrors/foo" +# insecure = true +# # Given the above, a pull of example.com/foo/image:latest will try: +# # 1. example-mirror-0.local/mirror-for-foo/image:latest +# # 2. example-mirror-1.local/mirrors/foo/image:latest +# # 3. internal-registry-for-example.net/bar/image:latest +# # in order, and use the first one that exists. diff --git a/vendor/github.com/containers/image/v5/sif/load_test.go b/vendor/github.com/containers/image/v5/sif/load_test.go new file mode 100644 index 00000000000..ee9bc7261e3 --- /dev/null +++ b/vendor/github.com/containers/image/v5/sif/load_test.go @@ -0,0 +1,58 @@ +package sif + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseDefFile(t *testing.T) { + for _, c := range []struct { + name string + input string + environment []string + runscript []string + }{ + {"Empty input", "", []string{}, []string{}}, + { + name: "Basic smoke test", + input: "Bootstrap: library\n" + + "%environment\n" + + " export FOO=world\n" + + " export BAR=baz\n" + + "%runscript\n" + + ` echo "Hello $FOO"` + "\n" + + " sleep 5\n" + + "%help\n" + + " Abandon all hope.\n", + environment: []string{"export FOO=world", "export BAR=baz"}, + runscript: []string{`echo "Hello $FOO"`, "sleep 5"}, + }, + { + name: "Trailing section marker", + input: "Bootstrap: library\n" + + "%environment\n" + + " export FOO=world\n" + + "%runscript", + environment: []string{"export FOO=world"}, + runscript: []string{}, + }, + } { + env, rs, err := parseDefFile(bytes.NewReader([]byte(c.input))) + require.NoError(t, err, c.name) + assert.Equal(t, c.environment, env, c.name) + assert.Equal(t, c.runscript, rs, c.name) + } +} + +func TestGenerateInjectedScript(t *testing.T) { + res := generateInjectedScript([]string{"export FOO=world", "export BAR=baz"}, + []string{`echo "Hello $FOO"`, "sleep 5"}) + assert.Equal(t, "#!/bin/bash\n"+ + "export FOO=world\n"+ + "export BAR=baz\n"+ + `echo "Hello $FOO"`+"\n"+ + "sleep 5\n", string(res)) +} diff --git a/vendor/github.com/containers/image/v5/sif/src_test.go b/vendor/github.com/containers/image/v5/sif/src_test.go new file mode 100644 index 00000000000..8402e23b1de --- /dev/null +++ b/vendor/github.com/containers/image/v5/sif/src_test.go @@ -0,0 +1,5 @@ +package sif + +import "github.com/containers/image/v5/internal/private" + +var _ private.ImageSource = (*sifImageSource)(nil) diff --git a/vendor/github.com/containers/image/v5/sif/transport_test.go b/vendor/github.com/containers/image/v5/sif/transport_test.go new file mode 100644 index 00000000000..8fe738fd8a4 --- /dev/null +++ b/vendor/github.com/containers/image/v5/sif/transport_test.go @@ -0,0 +1,175 @@ +package sif + +import ( + "context" + "os" + "path/filepath" + "testing" + + _ "github.com/containers/image/v5/internal/testing/explicitfilepath-tmpdir" + "github.com/containers/image/v5/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTransportName(t *testing.T) { + assert.Equal(t, "sif", Transport.Name()) +} + +func TestTransportParseReference(t *testing.T) { + testNewReference(t, Transport.ParseReference) +} + +func TestTransportValidatePolicyConfigurationScope(t *testing.T) { + for _, scope := range []string{ + "/etc/passwd", + "/this/does/not/exist", + } { + err := Transport.ValidatePolicyConfigurationScope(scope) + assert.NoError(t, err, scope) + } + + for _, scope := range []string{ + "relative/path", + "/double//slashes", + "/has/./dot", + "/has/dot/../dot", + "/trailing/slash/", + "/", + } { + err := Transport.ValidatePolicyConfigurationScope(scope) + assert.Error(t, err, scope) + } +} + +func TestNewReference(t *testing.T) { + testNewReference(t, NewReference) +} + +// testNewReference is a test shared for Transport.ParseReference and NewReference. +func testNewReference(t *testing.T, fn func(string) (types.ImageReference, error)) { + tmpDir := t.TempDir() + tmpFile := filepath.Join(tmpDir, "image.sif") + err := os.WriteFile(tmpFile, nil, 0600) + require.NoError(t, err) + + for _, file := range []string{ + "/dev/null", + tmpFile, + "relativepath", + tmpDir + "/thisdoesnotexist", + } { + ref, err := fn(file) + require.NoError(t, err, file) + sifRef, ok := ref.(sifReference) + require.True(t, ok) + assert.Equal(t, file, sifRef.file, file) + } + + _, err = fn(tmpDir + "/thisparentdoesnotexist/something") + assert.Error(t, err) +} + +// refToTempFile creates a temporary file and returns a reference to it. +// The caller should +// +// defer os.Remove(tmpFile) +func refToTempFile(t *testing.T) (ref types.ImageReference, tmpDir string) { + f, err := os.CreateTemp("", "sif-transport-test") + require.NoError(t, err) + tmpFile := f.Name() + err = f.Close() + require.NoError(t, err) + ref, err = NewReference(tmpFile) + require.NoError(t, err) + return ref, tmpFile +} + +func TestReferenceTransport(t *testing.T) { + ref, tmpFile := refToTempFile(t) + defer os.Remove(tmpFile) + assert.Equal(t, Transport, ref.Transport()) +} + +func TestReferenceStringWithinTransport(t *testing.T) { + ref, tmpFile := refToTempFile(t) + defer os.Remove(tmpFile) + assert.Equal(t, tmpFile, ref.StringWithinTransport()) +} + +func TestReferenceDockerReference(t *testing.T) { + ref, tmpFile := refToTempFile(t) + defer os.Remove(tmpFile) + assert.Nil(t, ref.DockerReference()) +} + +func TestReferencePolicyConfigurationIdentity(t *testing.T) { + ref, tmpFile := refToTempFile(t) + defer os.Remove(tmpFile) + + assert.Equal(t, tmpFile, ref.PolicyConfigurationIdentity()) + // A non-canonical path. Test just one, the various other cases are + // tested in explicitfilepath.ResolvePathToFullyExplicit. + ref, err := NewReference("/./" + tmpFile) + require.NoError(t, err) + assert.Equal(t, tmpFile, ref.PolicyConfigurationIdentity()) +} + +func TestReferencePolicyConfigurationNamespaces(t *testing.T) { + ref, tmpFile := refToTempFile(t) + defer os.Remove(tmpFile) + // We don't really know enough to make a full equality test here. + ns := ref.PolicyConfigurationNamespaces() + require.NotNil(t, ns) + assert.NotEmpty(t, ns) + assert.Equal(t, filepath.Dir(tmpFile), ns[0]) + + // Test with a known path where the directory should exist. Test just one non-canonical + // path, the various other cases are tested in explicitfilepath.ResolvePathToFullyExplicit. + for _, path := range []string{"/usr/share/probablydoesnotexist.sif", "/usr/share/././probablydoesnoexist.sif"} { + _, err := os.Lstat(filepath.Dir(path)) + require.NoError(t, err) + ref, err := NewReference(path) + require.NoError(t, err) + ns := ref.PolicyConfigurationNamespaces() + require.NotNil(t, ns) + assert.Equal(t, []string{"/usr/share", "/usr"}, ns) + } + + // "/" as a corner case. + ref, err := NewReference("/") + require.NoError(t, err) + assert.Equal(t, []string{}, ref.PolicyConfigurationNamespaces()) +} + +func TestReferenceNewImage(t *testing.T) { + ref, tmpFile := refToTempFile(t) + defer os.Remove(tmpFile) + // A pretty pointless smoke test for now; + // we don't want to require every developer of c/image to have fakeroot etc. around. + _, err := ref.NewImage(context.Background(), nil) + assert.Error(t, err) // Empty file is not valid +} + +func TestReferenceNewImageSource(t *testing.T) { + ref, tmpFile := refToTempFile(t) + defer os.Remove(tmpFile) + // A pretty pointless smoke test for now; + // we don't want to require every developer of c/image to have fakeroot etc. around. + _, err := ref.NewImageSource(context.Background(), nil) + assert.Error(t, err) // Empty file is not valid +} + +func TestReferenceNewImageDestination(t *testing.T) { + ref, tmpFile := refToTempFile(t) + defer os.Remove(tmpFile) + _, err := ref.NewImageDestination(context.Background(), nil) + assert.Error(t, err) +} + +func TestReferenceDeleteImage(t *testing.T) { + ref, tmpFile := refToTempFile(t) + defer os.Remove(tmpFile) + err := ref.DeleteImage(context.Background(), nil) + assert.Error(t, err) +} diff --git a/vendor/github.com/containers/image/v5/signature/docker_test.go b/vendor/github.com/containers/image/v5/signature/docker_test.go new file mode 100644 index 00000000000..fb2f52eaf2a --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/docker_test.go @@ -0,0 +1,236 @@ +package signature + +import ( + "os" + "testing" + + "github.com/containers/image/v5/internal/testing/gpgagent" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSignDockerManifest(t *testing.T) { + mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory) + require.NoError(t, err) + defer mech.Close() + + if err := mech.SupportsSigning(); err != nil { + t.Skipf("Signing not supported: %v", err) + } + + manifest, err := os.ReadFile("fixtures/image.manifest.json") + require.NoError(t, err) + + // Successful signing + signature, err := SignDockerManifest(manifest, TestImageSignatureReference, mech, TestKeyFingerprint) + require.NoError(t, err) + + verified, err := VerifyDockerManifestSignature(signature, manifest, TestImageSignatureReference, mech, TestKeyFingerprint) + assert.NoError(t, err) + assert.Equal(t, TestImageSignatureReference, verified.DockerReference) + assert.Equal(t, TestImageManifestDigest, verified.DockerManifestDigest) + + // Error computing Docker manifest + invalidManifest, err := os.ReadFile("fixtures/v2s1-invalid-signatures.manifest.json") + require.NoError(t, err) + _, err = SignDockerManifest(invalidManifest, TestImageSignatureReference, mech, TestKeyFingerprint) + assert.Error(t, err) + + // Error creating blob to sign + _, err = SignDockerManifest(manifest, "", mech, TestKeyFingerprint) + assert.Error(t, err) + + // Error signing + _, err = SignDockerManifest(manifest, TestImageSignatureReference, mech, "this fingerprint doesn't exist") + assert.Error(t, err) +} + +func TestSignDockerManifestWithPassphrase(t *testing.T) { + err := gpgagent.KillGPGAgent(testGPGHomeDirectory) + require.NoError(t, err) + + mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory) + require.NoError(t, err) + defer mech.Close() + + if err := mech.SupportsSigning(); err != nil { + t.Skipf("Signing not supported: %v", err) + } + + manifest, err := os.ReadFile("fixtures/image.manifest.json") + require.NoError(t, err) + + // Invalid passphrase + _, err = SignDockerManifestWithOptions(manifest, TestImageSignatureReference, mech, TestKeyFingerprintWithPassphrase, &SignOptions{Passphrase: TestPassphrase + "\n"}) + assert.ErrorContains(t, err, "invalid passphrase") + + // Wrong passphrase + _, err = SignDockerManifestWithOptions(manifest, TestImageSignatureReference, mech, TestKeyFingerprintWithPassphrase, &SignOptions{Passphrase: "wrong"}) + require.Error(t, err) + + // No passphrase + _, err = SignDockerManifestWithOptions(manifest, TestImageSignatureReference, mech, TestKeyFingerprintWithPassphrase, nil) + require.Error(t, err) + + // Successful signing + signature, err := SignDockerManifestWithOptions(manifest, TestImageSignatureReference, mech, TestKeyFingerprintWithPassphrase, &SignOptions{Passphrase: TestPassphrase}) + require.NoError(t, err) + + verified, err := VerifyDockerManifestSignature(signature, manifest, TestImageSignatureReference, mech, TestKeyFingerprintWithPassphrase) + assert.NoError(t, err) + assert.Equal(t, TestImageSignatureReference, verified.DockerReference) + assert.Equal(t, TestImageManifestDigest, verified.DockerManifestDigest) + + // Error computing Docker manifest + invalidManifest, err := os.ReadFile("fixtures/v2s1-invalid-signatures.manifest.json") + require.NoError(t, err) + _, err = SignDockerManifest(invalidManifest, TestImageSignatureReference, mech, TestKeyFingerprintWithPassphrase) + assert.Error(t, err) + + // Error creating blob to sign + _, err = SignDockerManifest(manifest, "", mech, TestKeyFingerprintWithPassphrase) + assert.Error(t, err) + + // Error signing + _, err = SignDockerManifest(manifest, TestImageSignatureReference, mech, "this fingerprint doesn't exist") + assert.Error(t, err) +} + +func TestVerifyDockerManifestSignature(t *testing.T) { + mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory) + require.NoError(t, err) + defer mech.Close() + manifest, err := os.ReadFile("fixtures/image.manifest.json") + require.NoError(t, err) + signature, err := os.ReadFile("fixtures/image.signature") + require.NoError(t, err) + + // Successful verification + sig, err := VerifyDockerManifestSignature(signature, manifest, TestImageSignatureReference, mech, TestKeyFingerprint) + require.NoError(t, err) + assert.Equal(t, TestImageSignatureReference, sig.DockerReference) + assert.Equal(t, TestImageManifestDigest, sig.DockerManifestDigest) + + // Verification using a different canonicalization of TestImageSignatureReference + sig, err = VerifyDockerManifestSignature(signature, manifest, "docker.io/"+TestImageSignatureReference, mech, TestKeyFingerprint) + require.NoError(t, err) + assert.Equal(t, TestImageSignatureReference, sig.DockerReference) + assert.Equal(t, TestImageManifestDigest, sig.DockerManifestDigest) + + // For extra paranoia, test that we return nil data on error. + + // Invalid docker reference on input + sig, err = VerifyDockerManifestSignature(signature, manifest, "UPPERCASEISINVALID", mech, TestKeyFingerprint) + assert.Error(t, err) + assert.Nil(t, sig) + + // Error computing Docker manifest + invalidManifest, err := os.ReadFile("fixtures/v2s1-invalid-signatures.manifest.json") + require.NoError(t, err) + sig, err = VerifyDockerManifestSignature(signature, invalidManifest, TestImageSignatureReference, mech, TestKeyFingerprint) + assert.Error(t, err) + assert.Nil(t, sig) + + // Error verifying signature + corruptSignature, err := os.ReadFile("fixtures/corrupt.signature") + require.NoError(t, err) + sig, err = VerifyDockerManifestSignature(corruptSignature, manifest, TestImageSignatureReference, mech, TestKeyFingerprint) + assert.Error(t, err) + assert.Nil(t, sig) + + // Key fingerprint mismatch + sig, err = VerifyDockerManifestSignature(signature, manifest, TestImageSignatureReference, mech, "unexpected fingerprint") + assert.Error(t, err) + assert.Nil(t, sig) + + // Invalid reference in the signature + invalidReferenceSignature, err := os.ReadFile("fixtures/invalid-reference.signature") + require.NoError(t, err) + sig, err = VerifyDockerManifestSignature(invalidReferenceSignature, manifest, TestImageSignatureReference, mech, TestKeyFingerprint) + assert.Error(t, err) + assert.Nil(t, sig) + + // Docker reference mismatch + sig, err = VerifyDockerManifestSignature(signature, manifest, "example.com/does-not/match", mech, TestKeyFingerprint) + assert.Error(t, err) + assert.Nil(t, sig) + + // Docker manifest digest mismatch + sig, err = VerifyDockerManifestSignature(signature, []byte("unexpected manifest"), TestImageSignatureReference, mech, TestKeyFingerprint) + assert.Error(t, err) + assert.Nil(t, sig) +} + +func TestVerifyImageManifestSignatureUsingKeyIdentityList(t *testing.T) { + mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory) + require.NoError(t, err) + defer mech.Close() + manifest, err := os.ReadFile("fixtures/image.manifest.json") + require.NoError(t, err) + signature, err := os.ReadFile("fixtures/image.signature") + require.NoError(t, err) + + // Successful verification + sig, keyIdentity, err := VerifyImageManifestSignatureUsingKeyIdentityList(signature, manifest, TestImageSignatureReference, mech, TestFingerprintListWithKey) + require.NoError(t, err) + assert.Equal(t, TestImageSignatureReference, sig.DockerReference) + assert.Equal(t, TestImageManifestDigest, sig.DockerManifestDigest) + assert.Equal(t, TestKeyFingerprint, keyIdentity) + + // Verification using a different canonicalization of TestImageSignatureReference + sig, keyIdentity, err = VerifyImageManifestSignatureUsingKeyIdentityList(signature, manifest, "docker.io/"+TestImageSignatureReference, mech, TestFingerprintListWithKey) + require.NoError(t, err) + assert.Equal(t, TestImageSignatureReference, sig.DockerReference) + assert.Equal(t, TestImageManifestDigest, sig.DockerManifestDigest) + assert.Equal(t, TestKeyFingerprint, keyIdentity) + + // For extra paranoia, test that we return nil data on error. + + // Invalid docker reference on input + sig, keyIdentity, err = VerifyImageManifestSignatureUsingKeyIdentityList(signature, manifest, "UPPERCASEISINVALID", mech, TestFingerprintListWithKey) + assert.Error(t, err) + assert.Nil(t, sig) + assert.Equal(t, "", keyIdentity) + + // Error computing Docker manifest + invalidManifest, err := os.ReadFile("fixtures/v2s1-invalid-signatures.manifest.json") + require.NoError(t, err) + sig, keyIdentity, err = VerifyImageManifestSignatureUsingKeyIdentityList(signature, invalidManifest, TestImageSignatureReference, mech, TestFingerprintListWithKey) + assert.Error(t, err) + assert.Nil(t, sig) + assert.Equal(t, "", keyIdentity) + + // Error verifying signature + corruptSignature, err := os.ReadFile("fixtures/corrupt.signature") + require.NoError(t, err) + sig, keyIdentity, err = VerifyImageManifestSignatureUsingKeyIdentityList(corruptSignature, manifest, TestImageSignatureReference, mech, TestFingerprintListWithKey) + assert.Error(t, err) + assert.Nil(t, sig) + assert.Equal(t, "", keyIdentity) + + // Key fingerprint mismatch + sig, keyIdentity, err = VerifyImageManifestSignatureUsingKeyIdentityList(signature, manifest, TestImageSignatureReference, mech, TestFingerprintListWithoutKey) + assert.Error(t, err) + assert.Nil(t, sig) + assert.Equal(t, "", keyIdentity) + + // Invalid reference in the signature + invalidReferenceSignature, err := os.ReadFile("fixtures/invalid-reference.signature") + require.NoError(t, err) + sig, keyIdentity, err = VerifyImageManifestSignatureUsingKeyIdentityList(invalidReferenceSignature, manifest, TestImageSignatureReference, mech, TestFingerprintListWithKey) + assert.Error(t, err) + assert.Nil(t, sig) + assert.Equal(t, "", keyIdentity) + + // Docker reference mismatch + sig, keyIdentity, err = VerifyImageManifestSignatureUsingKeyIdentityList(signature, manifest, "example.com/does-not/match", mech, TestFingerprintListWithKey) + assert.Error(t, err) + assert.Nil(t, sig) + assert.Equal(t, "", keyIdentity) + + // Docker manifest digest mismatch + sig, keyIdentity, err = VerifyImageManifestSignatureUsingKeyIdentityList(signature, []byte("unexpected manifest"), TestImageSignatureReference, mech, TestFingerprintListWithKey) + assert.Error(t, err) + assert.Nil(t, sig) + assert.Equal(t, "", keyIdentity) +} diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/.gitignore b/vendor/github.com/containers/image/v5/signature/fixtures/.gitignore new file mode 100644 index 00000000000..2772b97f7df --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/.gitignore @@ -0,0 +1,6 @@ +/*.gpg~ +/.gpg-v21-migrated +/private-keys-v1.d +/random_seed +/gnupg_spawn_agent_sentinel.lock +/.#* diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/corrupt.signature b/vendor/github.com/containers/image/v5/signature/fixtures/corrupt.signature new file mode 100644 index 00000000000..95c29087125 Binary files /dev/null and b/vendor/github.com/containers/image/v5/signature/fixtures/corrupt.signature differ diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/corrupt.signature-v3 b/vendor/github.com/containers/image/v5/signature/fixtures/corrupt.signature-v3 new file mode 100644 index 00000000000..51f986b961d Binary files /dev/null and b/vendor/github.com/containers/image/v5/signature/fixtures/corrupt.signature-v3 differ diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/cosign.pub b/vendor/github.com/containers/image/v5/signature/fixtures/cosign.pub new file mode 100644 index 00000000000..8dae995e3a2 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/cosign.pub @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFNLqFhf4fiN6o/glAuYnq2jYUeL0 +vRuLu/z39pmbVwS9ff5AYnlwaP9sxREajdLY9ynM6G1sy6AAmb7Z63TsLg== +-----END PUBLIC KEY----- diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/cosign2.pub b/vendor/github.com/containers/image/v5/signature/fixtures/cosign2.pub new file mode 100644 index 00000000000..ca46da8f297 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/cosign2.pub @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwOkOF9xpfG8ghueIhnZ66ooujwt1 ++ReV3HupgKnGFYnEh3Hh1YTg5L6kN1Yakkt5WltRoav8/R3hpCtUO3Rldw== +-----END PUBLIC KEY----- diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-fulcio-rekor-valid/manifest.json b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-fulcio-rekor-valid/manifest.json new file mode 100644 index 00000000000..b1ce3f65514 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-fulcio-rekor-valid/manifest.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 1508, + "digest": "sha256:3b0f78b718417dfa432fd8da26d0e3ac4ca3566d0825b0d2b056ccc4dd29e644" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 2568440, + "digest": "sha256:1df32bae7504a32024616c66017cd5df04dd98eaf150f8df45fffef2547a3c54" + } + ] +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-fulcio-rekor-valid/signature-1 b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-fulcio-rekor-valid/signature-1 new file mode 100644 index 00000000000..332e0f950e3 Binary files /dev/null and b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-fulcio-rekor-valid/signature-1 differ diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-key-rekor-valid/manifest.json b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-key-rekor-valid/manifest.json new file mode 100644 index 00000000000..b1ce3f65514 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-key-rekor-valid/manifest.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 1508, + "digest": "sha256:3b0f78b718417dfa432fd8da26d0e3ac4ca3566d0825b0d2b056ccc4dd29e644" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 2568440, + "digest": "sha256:1df32bae7504a32024616c66017cd5df04dd98eaf150f8df45fffef2547a3c54" + } + ] +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-key-rekor-valid/signature-1 b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-key-rekor-valid/signature-1 new file mode 100644 index 00000000000..befbf6c8793 Binary files /dev/null and b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-key-rekor-valid/signature-1 differ diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-manifest-digest-error/manifest.json b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-manifest-digest-error/manifest.json new file mode 120000 index 00000000000..3dee14b4a8d --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-manifest-digest-error/manifest.json @@ -0,0 +1 @@ +../v2s1-invalid-signatures.manifest.json \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-manifest-digest-error/signature-1 b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-manifest-digest-error/signature-1 new file mode 120000 index 00000000000..c5bec7e3c65 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-manifest-digest-error/signature-1 @@ -0,0 +1 @@ +../dir-img-cosign-valid/signature-1 \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-mixed/manifest.json b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-mixed/manifest.json new file mode 120000 index 00000000000..3c1dbaed06e --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-mixed/manifest.json @@ -0,0 +1 @@ +../dir-img-cosign-valid/manifest.json \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-mixed/signature-1 b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-mixed/signature-1 new file mode 120000 index 00000000000..0aee1d5636e --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-mixed/signature-1 @@ -0,0 +1 @@ +../unknown-cosign-key.signature \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-mixed/signature-2 b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-mixed/signature-2 new file mode 120000 index 00000000000..c5bec7e3c65 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-mixed/signature-2 @@ -0,0 +1 @@ +../dir-img-cosign-valid/signature-1 \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-modified-manifest/manifest.json b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-modified-manifest/manifest.json new file mode 100644 index 00000000000..40a935bf07a --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-modified-manifest/manifest.json @@ -0,0 +1,17 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 1512, + "digest": "sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 2896510, + "digest": "sha256:9d16cba9fb961d1aafec9542f2bf7cb64acfc55245f9e4eb5abecd4cdc38d749" + } + ], + "extra": "this manifest has been modified" +} diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-modified-manifest/signature-1 b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-modified-manifest/signature-1 new file mode 120000 index 00000000000..c5bec7e3c65 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-modified-manifest/signature-1 @@ -0,0 +1 @@ +../dir-img-cosign-valid/signature-1 \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-no-manifest/signature-1 b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-no-manifest/signature-1 new file mode 120000 index 00000000000..c5bec7e3c65 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-no-manifest/signature-1 @@ -0,0 +1 @@ +../dir-img-cosign-valid/signature-1 \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-other-attachment/manifest.json b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-other-attachment/manifest.json new file mode 120000 index 00000000000..3c1dbaed06e --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-other-attachment/manifest.json @@ -0,0 +1 @@ +../dir-img-cosign-valid/manifest.json \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-other-attachment/signature-1 b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-other-attachment/signature-1 new file mode 100644 index 00000000000..f809ba6858a Binary files /dev/null and b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-other-attachment/signature-1 differ diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-valid-2/manifest.json b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-valid-2/manifest.json new file mode 120000 index 00000000000..3c1dbaed06e --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-valid-2/manifest.json @@ -0,0 +1 @@ +../dir-img-cosign-valid/manifest.json \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-valid-2/signature-1 b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-valid-2/signature-1 new file mode 120000 index 00000000000..c5bec7e3c65 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-valid-2/signature-1 @@ -0,0 +1 @@ +../dir-img-cosign-valid/signature-1 \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-valid-2/signature-2 b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-valid-2/signature-2 new file mode 100644 index 00000000000..5a828ec1ae4 Binary files /dev/null and b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-valid-2/signature-2 differ diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-valid-with-tag/manifest.json b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-valid-with-tag/manifest.json new file mode 100644 index 00000000000..72fb41f656e --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-valid-with-tag/manifest.json @@ -0,0 +1 @@ +{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","size":1512,"digest":"sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4"},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","size":2896510,"digest":"sha256:9d16cba9fb961d1aafec9542f2bf7cb64acfc55245f9e4eb5abecd4cdc38d749"}]} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-valid-with-tag/signature-1 b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-valid-with-tag/signature-1 new file mode 100644 index 00000000000..efba75f7258 Binary files /dev/null and b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-valid-with-tag/signature-1 differ diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-valid/manifest.json b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-valid/manifest.json new file mode 100644 index 00000000000..72fb41f656e --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-valid/manifest.json @@ -0,0 +1 @@ +{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","size":1512,"digest":"sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4"},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","size":2896510,"digest":"sha256:9d16cba9fb961d1aafec9542f2bf7cb64acfc55245f9e4eb5abecd4cdc38d749"}]} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-valid/signature-1 b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-valid/signature-1 new file mode 100644 index 00000000000..46e7533f30e Binary files /dev/null and b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-cosign-valid/signature-1 differ diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-manifest-digest-error/manifest.json b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-manifest-digest-error/manifest.json new file mode 120000 index 00000000000..3dee14b4a8d --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-manifest-digest-error/manifest.json @@ -0,0 +1 @@ +../v2s1-invalid-signatures.manifest.json \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-manifest-digest-error/signature-1 b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-manifest-digest-error/signature-1 new file mode 120000 index 00000000000..f010fd4c41e --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-manifest-digest-error/signature-1 @@ -0,0 +1 @@ +../dir-img-valid/signature-1 \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-mixed/manifest.json b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-mixed/manifest.json new file mode 120000 index 00000000000..ff7d2ffadff --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-mixed/manifest.json @@ -0,0 +1 @@ +../dir-img-valid/manifest.json \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-mixed/signature-1 b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-mixed/signature-1 new file mode 120000 index 00000000000..b27cdc45853 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-mixed/signature-1 @@ -0,0 +1 @@ +../invalid-blob.signature \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-mixed/signature-2 b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-mixed/signature-2 new file mode 120000 index 00000000000..f010fd4c41e --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-mixed/signature-2 @@ -0,0 +1 @@ +../dir-img-valid/signature-1 \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-modified-manifest/manifest.json b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-modified-manifest/manifest.json new file mode 100644 index 00000000000..82fde3811ea --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-modified-manifest/manifest.json @@ -0,0 +1,27 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 7023, + "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 32654, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 16724, + "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 73109, + "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736" + } + ], + "extra": "this manifest has been modified" +} diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-modified-manifest/signature-1 b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-modified-manifest/signature-1 new file mode 120000 index 00000000000..f010fd4c41e --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-modified-manifest/signature-1 @@ -0,0 +1 @@ +../dir-img-valid/signature-1 \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-no-manifest/signature-1 b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-no-manifest/signature-1 new file mode 120000 index 00000000000..f010fd4c41e --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-no-manifest/signature-1 @@ -0,0 +1 @@ +../dir-img-valid/signature-1 \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-unsigned/manifest.json b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-unsigned/manifest.json new file mode 120000 index 00000000000..ff7d2ffadff --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-unsigned/manifest.json @@ -0,0 +1 @@ +../dir-img-valid/manifest.json \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-valid-2/manifest.json b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-valid-2/manifest.json new file mode 120000 index 00000000000..ff7d2ffadff --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-valid-2/manifest.json @@ -0,0 +1 @@ +../dir-img-valid/manifest.json \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-valid-2/signature-1 b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-valid-2/signature-1 new file mode 120000 index 00000000000..f010fd4c41e --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-valid-2/signature-1 @@ -0,0 +1 @@ +../dir-img-valid/signature-1 \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-valid-2/signature-2 b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-valid-2/signature-2 new file mode 100644 index 00000000000..dbba8f42281 Binary files /dev/null and b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-valid-2/signature-2 differ diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-valid/manifest.json b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-valid/manifest.json new file mode 120000 index 00000000000..c5bd25431f8 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-valid/manifest.json @@ -0,0 +1 @@ +../image.manifest.json \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-valid/signature-1 b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-valid/signature-1 new file mode 100644 index 00000000000..d0e18720d98 Binary files /dev/null and b/vendor/github.com/containers/image/v5/signature/fixtures/dir-img-valid/signature-1 differ diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/double.signature b/vendor/github.com/containers/image/v5/signature/fixtures/double.signature new file mode 100644 index 00000000000..76b17e2a1b8 Binary files /dev/null and b/vendor/github.com/containers/image/v5/signature/fixtures/double.signature differ diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/expired.signature b/vendor/github.com/containers/image/v5/signature/fixtures/expired.signature new file mode 100644 index 00000000000..c609c37927a Binary files /dev/null and b/vendor/github.com/containers/image/v5/signature/fixtures/expired.signature differ diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/fulcio-cert b/vendor/github.com/containers/image/v5/signature/fixtures/fulcio-cert new file mode 100644 index 00000000000..734b8bb2004 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/fulcio-cert @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICnTCCAiOgAwIBAgIUG45uaC2z8VvuOwzzm79RPfckoxwwCgYIKoZIzj0EAwMw +NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl +cm1lZGlhdGUwHhcNMjIxMjEyMTg0ODE4WhcNMjIxMjEyMTg1ODE4WjAAMFkwEwYH +KoZIzj0CAQYIKoZIzj0DAQcDQgAEJBurkhSRmHukHVh3VA8nYuSWqZ4ltafDwyWl +c3c9NaLovmu+NC4NiUtMLifL/P3nqedbnctKBuYmfISGiZlVyKOCAUIwggE+MA4G +A1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUAmvG +4K6lNk/sk/f4aiahjYRqR+cwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y +ZD8wHQYDVR0RAQH/BBMwEYEPbWl0ckByZWRoYXQuY29tMCwGCisGAQQBg78wAQEE +Hmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDCBiQYKKwYBBAHWeQIEAgR7 +BHkAdwB1AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6OAAABhQeqlwUA +AAQDAEYwRAIgVi2SvN5ReGYiOShY8LFSRG5D6oATKqHb6kN/DJh2rAUCIANS6Aqp +xaXiHwTHUr3xPSGa5i6hywmbbRC6N0kIeDM6MAoGCCqGSM49BAMDA2gAMGUCMCLu +cZtESIN0lm64Co/bZ68CCRkWlktX4mmJiRhKi9c9QD62C5SuZhc0vvo6MOKmTQIx +AIo/MozZeoU7rdNj0pZKuBeCmMqmJaDSsw19tKi/b1pEmuw+Sf2BI25GgIbKztG1 +9w== +-----END CERTIFICATE----- diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/fulcio-chain b/vendor/github.com/containers/image/v5/signature/fixtures/fulcio-chain new file mode 100644 index 00000000000..1c1e9f764b7 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/fulcio-chain @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMw +KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y +MjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3Jl +LmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0C +AQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV7 +7LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS +0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYB +BQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjp +KFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZI +zj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJR +nZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsP +mygUY7Ii2zbdCdliiow= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMw +KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y +MTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3Jl +LmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7 +XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxex +X69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92j +YzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRY +wB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQ +KsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCM +WP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9 +TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ +-----END CERTIFICATE----- \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/fulcio_v1.crt.pem b/vendor/github.com/containers/image/v5/signature/fixtures/fulcio_v1.crt.pem new file mode 100644 index 00000000000..3afc46bb6e1 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/fulcio_v1.crt.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMw +KjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0y +MTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3Jl +LmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7 +XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxex +X69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92j +YzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRY +wB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQ +KsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCM +WP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9 +TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ +-----END CERTIFICATE----- \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/image.manifest.json b/vendor/github.com/containers/image/v5/signature/fixtures/image.manifest.json new file mode 100644 index 00000000000..198da23f926 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/image.manifest.json @@ -0,0 +1,26 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 7023, + "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 32654, + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 16724, + "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 73109, + "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736" + } + ] +} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/image.signature b/vendor/github.com/containers/image/v5/signature/fixtures/image.signature new file mode 100644 index 00000000000..f8927212757 Binary files /dev/null and b/vendor/github.com/containers/image/v5/signature/fixtures/image.signature differ diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/invalid-blob.signature b/vendor/github.com/containers/image/v5/signature/fixtures/invalid-blob.signature new file mode 100644 index 00000000000..c8db18cac28 Binary files /dev/null and b/vendor/github.com/containers/image/v5/signature/fixtures/invalid-blob.signature differ diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/invalid-blob.signature-v3 b/vendor/github.com/containers/image/v5/signature/fixtures/invalid-blob.signature-v3 new file mode 100644 index 00000000000..246bdd9a8e5 Binary files /dev/null and b/vendor/github.com/containers/image/v5/signature/fixtures/invalid-blob.signature-v3 differ diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/invalid-reference.signature b/vendor/github.com/containers/image/v5/signature/fixtures/invalid-reference.signature new file mode 100644 index 00000000000..0236598301e Binary files /dev/null and b/vendor/github.com/containers/image/v5/signature/fixtures/invalid-reference.signature differ diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/no-optional-fields.signature b/vendor/github.com/containers/image/v5/signature/fixtures/no-optional-fields.signature new file mode 100644 index 00000000000..482ae3acf0d Binary files /dev/null and b/vendor/github.com/containers/image/v5/signature/fixtures/no-optional-fields.signature differ diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/policy.json b/vendor/github.com/containers/image/v5/signature/fixtures/policy.json new file mode 100644 index 00000000000..22d68d8d1d6 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/policy.json @@ -0,0 +1,160 @@ +{ + "default": [ + { + "type": "reject" + } + ], + "transports": { + "dir": { + "": [ + { + "type": "insecureAcceptAnything" + } + ] + }, + "docker": { + "example.com/playground": [ + { + "type": "insecureAcceptAnything" + } + ], + "example.com/production": [ + { + "type": "signedBy", + "keyType": "GPGKeys", + "keyPath": "/keys/employee-gpg-keyring" + } + ], + "example.com/hardened": [ + { + "type": "signedBy", + "keyType": "GPGKeys", + "keyPath": "/keys/employee-gpg-keyring", + "signedIdentity": { + "type": "matchRepository" + } + }, + { + "type": "signedBy", + "keyType": "signedByGPGKeys", + "keyPath": "/keys/public-key-signing-gpg-keyring", + "signedIdentity": { + "type": "matchExact" + } + }, + { + "type": "signedBaseLayer", + "baseLayerIdentity": { + "type": "exactRepository", + "dockerRepository": "registry.access.redhat.com/rhel7/rhel" + } + } + ], + "example.com/hardened-x509": [ + { + "type": "signedBy", + "keyType": "X509Certificates", + "keyPath": "/keys/employee-cert-file", + "signedIdentity": { + "type": "matchRepository" + } + }, + { + "type": "signedBy", + "keyType": "signedByX509CAs", + "keyPath": "/keys/public-key-signing-ca-file" + } + ], + "registry.access.redhat.com": [ + { + "type": "signedBy", + "keyType": "signedByGPGKeys", + "keyPath": "/keys/RH-key-signing-key-gpg-keyring", + "signedIdentity": { + "type": "matchRepoDigestOrExact" + } + } + ], + "registry.redhat.io/beta": [ + { + "type": "signedBy", + "keyType": "GPGKeys", + "keyPaths": ["/keys/RH-production-signing-key-gpg-keyring", "/keys/RH-beta-signing-key-gpg-keyring"] + } + ], + "private-mirror:5000/vendor-mirror": [ + { + "type": "signedBy", + "keyType": "GPGKeys", + "keyPath": "/keys/vendor-gpg-keyring", + "signedIdentity": { + "type": "remapIdentity", + "prefix": "private-mirror:5000/vendor-mirror", + "signedPrefix": "vendor.example.com" + } + } + ], + "*.access.redhat.com": [ + { + "type": "signedBy", + "keyType": "signedByGPGKeys", + "keyPath": "/keys/RH-key-signing-key-gpg-keyring", + "signedIdentity": { + "type": "matchRepoDigestOrExact" + } + } + ], + "*.redhat.com": [ + { + "type": "signedBy", + "keyType": "signedByGPGKeys", + "keyPath": "/keys/RH-key-signing-key-gpg-keyring", + "signedIdentity": { + "type": "matchRepoDigestOrExact" + } + } + ], + "*.com": [ + { + "type": "signedBy", + "keyType": "signedByGPGKeys", + "keyPath": "/keys/RH-key-signing-key-gpg-keyring", + "signedIdentity": { + "type": "matchRepoDigestOrExact" + } + } + ], + "bogus/key-data-example": [ + { + "type": "signedBy", + "keyType": "signedByGPGKeys", + "keyData": "bm9uc2Vuc2U=" + } + ], + "bogus/signed-identity-example": [ + { + "type": "signedBaseLayer", + "baseLayerIdentity": { + "type": "exactReference", + "dockerReference": "registry.access.redhat.com/rhel7/rhel:latest" + } + } + ], + "example.com/sigstore/key-data-example": [ + { + "type": "sigstoreSigned", + "keyData": "bm9uc2Vuc2U=" + } + ], + "example.com/sigstore/key-path-example": [ + { + "type": "sigstoreSigned", + "keyPath": "/keys/public-key", + "signedIdentity": { + "type": "matchRepository" + } + } + ] + } + } +} diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/public-key-1.gpg b/vendor/github.com/containers/image/v5/signature/fixtures/public-key-1.gpg new file mode 100644 index 00000000000..c97f2e0add6 Binary files /dev/null and b/vendor/github.com/containers/image/v5/signature/fixtures/public-key-1.gpg differ diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/public-key-2.gpg b/vendor/github.com/containers/image/v5/signature/fixtures/public-key-2.gpg new file mode 100644 index 00000000000..c15d06b4ae5 Binary files /dev/null and b/vendor/github.com/containers/image/v5/signature/fixtures/public-key-2.gpg differ diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/public-key.gpg b/vendor/github.com/containers/image/v5/signature/fixtures/public-key.gpg new file mode 100644 index 00000000000..46901d58dbb --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/public-key.gpg @@ -0,0 +1,19 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1 + +mI0EVurzqQEEAL3qkFq4K2URtSWVDYnQUNA9HdM9sqS2eAWfqUFMrkD5f+oN+LBL +tPyaE5GNLA0vXY7nHAM2TeM8ijZ/eMP17Raj64JL8GhCymL3wn2jNvb9XaF0R0s6 +H0IaRPPu45A3SnxLwm4Orc/9Z7/UxtYjKSg9xOaTiVPzJgaf5Vm4J4ApABEBAAG0 +EnNrb3BlbyB0ZXN0aW5nIGtleYi4BBMBAgAiBQJW6vOpAhsDBgsJCAcDAgYVCAIJ +CgsEFgIDAQIeAQIXgAAKCRDbcvIYi7RsyBbOBACgJFiKDlQ1UyvsNmGqJ7D0OpbS +1OppJlradKgZXyfahFswhFI+7ZREvELLHbinq3dBy5cLXRWzQKdJZNHknSN5Tjf2 +0ipVBQuqpcBo+dnKiG4zH6fhTri7yeTZksIDfsqlI6FXDOdKLUSnahagEBn4yU+x +jHPvZk5SuuZv56A45biNBFbq86kBBADIC/9CsAlOmRALuYUmkhcqEjuFwn3wKz2d +IBjzgvro7zcVNNCgxQfMEjcUsvEh5cx13G3QQHcwOKy3M6Bv6VMhfZjd+1P1el4P +0fJS8GFmhWRBknMN8jFsgyohQeouQ798RFFv94KszfStNnr/ae8oao5URmoUXSCa +/MdUxn0YKwARAQABiJ8EGAECAAkFAlbq86kCGwwACgkQ23LyGIu0bMjUywQAq0dn +lUpDNSoLTcpNWuVvHQ7c/qmnE4TyiSLiRiAywdEWA6gMiyhUUucuGsEhMFP1WX1k +UNwArZ6UG7BDOUsvngP7jKGNqyUOQrq1s/r8D+0MrJGOWErGLlfttO2WeoijECkI +5qm8cXzAra3Xf/Z3VjxYTKSnNu37LtZkakdTdYE= +=tJAt +-----END PGP PUBLIC KEY BLOCK----- diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/pubring.gpg b/vendor/github.com/containers/image/v5/signature/fixtures/pubring.gpg new file mode 100644 index 00000000000..9e7712b2986 Binary files /dev/null and b/vendor/github.com/containers/image/v5/signature/fixtures/pubring.gpg differ diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/rekor-payload b/vendor/github.com/containers/image/v5/signature/fixtures/rekor-payload new file mode 120000 index 00000000000..04a25636bd6 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/rekor-payload @@ -0,0 +1 @@ +../internal/testdata/rekor-payload \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/rekor-set b/vendor/github.com/containers/image/v5/signature/fixtures/rekor-set new file mode 120000 index 00000000000..5cd8159dfc3 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/rekor-set @@ -0,0 +1 @@ +../internal/testdata/rekor-set \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/rekor-sig b/vendor/github.com/containers/image/v5/signature/fixtures/rekor-sig new file mode 120000 index 00000000000..16b66d7bb87 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/rekor-sig @@ -0,0 +1 @@ +../internal/testdata/rekor-sig \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/rekor.pub b/vendor/github.com/containers/image/v5/signature/fixtures/rekor.pub new file mode 120000 index 00000000000..ac451b9d9ad --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/rekor.pub @@ -0,0 +1 @@ +../internal/testdata/rekor.pub \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/secring.gpg b/vendor/github.com/containers/image/v5/signature/fixtures/secring.gpg new file mode 100644 index 00000000000..1c274998cda Binary files /dev/null and b/vendor/github.com/containers/image/v5/signature/fixtures/secring.gpg differ diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/some-rsa-key.pub b/vendor/github.com/containers/image/v5/signature/fixtures/some-rsa-key.pub new file mode 100644 index 00000000000..1d56ec17b54 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/some-rsa-key.pub @@ -0,0 +1,14 @@ +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1bofelEsr9ZYEDlNZN+Z +K4LbdXi1grcUYGk9S7UsaZV4ovq++j5yYAiM2mWBxcmD65pokyanLEz/0ya9zSez +C1dY337ecoRj7OgLzrMJarKJu3xDYe/jCylQZCke7bobsIahi00i6sU0Feh94ULt +aSiBaOEZzYvbE6QBsfQWwH2EC/Ch0Dsy6zpZQxnsEGJkpr/ed9UYwUlgaGAouL4N +A5flQvDNWyKNMBsosrTHE+yRYiouAytszJ1WzMQrR8n3ngsMaJtM5FZClyT1FbB5 +YnvoFcTUqVQqMFCK6DBQvMp5mOBHAdWaHXTAP9cKFK73CdQwHigP3ayXRyq/Npjd +tSpk1AW12Ctkiu9jIX1upGpwJCz0nevsXm5CL2jdxbzDPxIHV4w4kHxt6CcCWJ3V +DSqTIV8xGMyRahUsmhnRXeRSF0UaQdzI9ZPKWW20bYjZ3qJawO3YhJZUwxZKiAm6 +Px2nd4lVpJXZLZ/DJ7eXJ0rWiwC2Y3C+1FlXdWtbocersg6a7oW/VNe+unVznHWm +N0GSp1IobRsvP6t5ITIJiQguROl8PVNS7Wu4vzDndy1cH4NBCyrQopQLJ29U51S4 +2tqYlSlJReWjvSidCSZDa9/YXU9LZqVWmkSKwMcCKCRACIYOLxFZJjsxj5hOIOkT +4EeUfiK04GAV1QKVloZ9b2sCAwEAAQ== +-----END PUBLIC KEY----- diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/trustdb.gpg b/vendor/github.com/containers/image/v5/signature/fixtures/trustdb.gpg new file mode 100644 index 00000000000..fca2002faab Binary files /dev/null and b/vendor/github.com/containers/image/v5/signature/fixtures/trustdb.gpg differ diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/unknown-cosign-key.signature b/vendor/github.com/containers/image/v5/signature/fixtures/unknown-cosign-key.signature new file mode 100644 index 00000000000..e05ba51fd83 Binary files /dev/null and b/vendor/github.com/containers/image/v5/signature/fixtures/unknown-cosign-key.signature differ diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/unknown-key.signature b/vendor/github.com/containers/image/v5/signature/fixtures/unknown-key.signature new file mode 100644 index 00000000000..393ace4a92b Binary files /dev/null and b/vendor/github.com/containers/image/v5/signature/fixtures/unknown-key.signature differ diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/unknown-key.signature-v3 b/vendor/github.com/containers/image/v5/signature/fixtures/unknown-key.signature-v3 new file mode 100644 index 00000000000..67f429b0d82 Binary files /dev/null and b/vendor/github.com/containers/image/v5/signature/fixtures/unknown-key.signature-v3 differ diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/unsigned-encrypted.signature b/vendor/github.com/containers/image/v5/signature/fixtures/unsigned-encrypted.signature new file mode 100644 index 00000000000..7da65dec261 Binary files /dev/null and b/vendor/github.com/containers/image/v5/signature/fixtures/unsigned-encrypted.signature differ diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/unsigned-literal.signature b/vendor/github.com/containers/image/v5/signature/fixtures/unsigned-literal.signature new file mode 100644 index 00000000000..9b660cb214e Binary files /dev/null and b/vendor/github.com/containers/image/v5/signature/fixtures/unsigned-literal.signature differ diff --git a/vendor/github.com/containers/image/v5/signature/fixtures/v2s1-invalid-signatures.manifest.json b/vendor/github.com/containers/image/v5/signature/fixtures/v2s1-invalid-signatures.manifest.json new file mode 100644 index 00000000000..96def402320 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures/v2s1-invalid-signatures.manifest.json @@ -0,0 +1,11 @@ +{ + "schemaVersion": 1, + "name": "mitr/busybox", + "tag": "latest", + "architecture": "amd64", + "fsLayers": [ + ], + "history": [ + ], + "signatures": 1 +} diff --git a/vendor/github.com/containers/image/v5/signature/fixtures_info_test.go b/vendor/github.com/containers/image/v5/signature/fixtures_info_test.go new file mode 100644 index 00000000000..e370bbc9b12 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fixtures_info_test.go @@ -0,0 +1,29 @@ +package signature + +import "github.com/opencontainers/go-digest" + +const ( + // TestImageManifestDigest is the Docker manifest digest of "image.manifest.json" + TestImageManifestDigest = digest.Digest("sha256:20bf21ed457b390829cdbeec8795a7bea1626991fda603e0d01b4e7f60427e55") + // TestImageSignatureReference is the Docker image reference signed in "image.signature" + TestImageSignatureReference = "testing/manifest" + // TestKeyFingerprint is the fingerprint of the private key in this directory. + TestKeyFingerprint = "1D8230F6CDB6A06716E414C1DB72F2188BB46CC8" + // TestOtherFingerprint1 is a random fingerprint. + TestOtherFingerprint1 = "0123456789ABCDEF0123456789ABCDEF01234567" + // TestOtherFingerprint2 is a random fingerprint. + TestOtherFingerprint2 = "DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF" + // TestKeyShortID is the short ID of the private key in this directory. + TestKeyShortID = "DB72F2188BB46CC8" + // TestKeyFingerprintWithPassphrase is the fingerprint of the private key with passphrase in this directory. + TestKeyFingerprintWithPassphrase = "E3EB7611D815211F141946B5B0CDE60B42557346" + // TestPassphrase is the passphrase for TestKeyFingerprintWithPassphrase. + TestPassphrase = "WithPassphrase123" +) + +var ( + // TestFingerprintListWithKey slice of multiple fingerprints including the fingerprint of the private key in this directory. + TestFingerprintListWithKey = []string{TestKeyFingerprint, TestOtherFingerprint1, TestOtherFingerprint2} + // TestFingerprintListWithoutKey slice of multiple fingerprints not including the fingerprint of the private key in this directory. + TestFingerprintListWithoutKey = []string{TestOtherFingerprint1, TestOtherFingerprint2} +) diff --git a/vendor/github.com/containers/image/v5/signature/fulcio_cert_test.go b/vendor/github.com/containers/image/v5/signature/fulcio_cert_test.go new file mode 100644 index 00000000000..d30731ccfda --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/fulcio_cert_test.go @@ -0,0 +1,475 @@ +package signature + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/pem" + "os" + "testing" + "time" + + "github.com/sigstore/fulcio/pkg/certificate" + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// assert that crypto.PublicKey matches the on in certPEM. +func assertPublicKeyMatchesCert(t *testing.T, certPEM []byte, pk crypto.PublicKey) { + pkInterface, ok := pk.(interface { + Equal(x crypto.PublicKey) bool + }) + require.True(t, ok) + certs, err := cryptoutils.UnmarshalCertificatesFromPEM(certPEM) + require.NoError(t, err) + require.Len(t, certs, 1) + equal := pkInterface.Equal(certs[0].PublicKey) + assert.True(t, equal) +} + +func TestFulcioTrustRootValidate(t *testing.T) { + certs := x509.NewCertPool() // Empty is valid enough for our purposes. + + for _, tr := range []fulcioTrustRoot{ + { + caCertificates: certs, + oidcIssuer: "", + subjectEmail: "email", + }, + { + caCertificates: certs, + oidcIssuer: "issuer", + subjectEmail: "", + }, + } { + err := tr.validate() + assert.Error(t, err) + } + + tr := fulcioTrustRoot{ + caCertificates: certs, + oidcIssuer: "issuer", + subjectEmail: "email", + } + err := tr.validate() + assert.NoError(t, err) +} + +// oidIssuerV1Ext creates an certificate.OIDIssuer extension +func oidIssuerV1Ext(value string) pkix.Extension { + return pkix.Extension{ + Id: certificate.OIDIssuer, //nolint:staticcheck // This is deprecated, but we must continue to accept it. + Value: []byte(value), + } +} + +// asn1MarshalTest is asn1.MarshalWithParams that must not fail +func asn1MarshalTest(t *testing.T, value any, params string) []byte { + bytes, err := asn1.MarshalWithParams(value, params) + require.NoError(t, err) + return bytes +} + +// oidIssuerV2Ext creates an certificate.OIDIssuerV2 extension +func oidIssuerV2Ext(t *testing.T, value string) pkix.Extension { + return pkix.Extension{ + Id: certificate.OIDIssuerV2, + Value: asn1MarshalTest(t, value, "utf8"), + } +} + +func TestFulcioIssuerInCertificate(t *testing.T) { + referenceTime := time.Now() + fulcioExtensions, err := certificate.Extensions{Issuer: "https://github.com/login/oauth"}.Render() + require.NoError(t, err) + for _, c := range []struct { + name string + extensions []pkix.Extension + errorFragment string + expected string + }{ + { + name: "Missing issuer", + extensions: nil, + errorFragment: "Fulcio certificate is missing the issuer extension", + }, + { + name: "Duplicate issuer v1 extension", + extensions: []pkix.Extension{ + oidIssuerV1Ext("https://github.com/login/oauth"), + oidIssuerV1Ext("this does not match"), + }, + // Match both our message and the Go 1.19 message: "certificate contains duplicate extensions" + errorFragment: "duplicate", + }, + { + name: "Duplicate issuer v2 extension", + extensions: []pkix.Extension{ + oidIssuerV2Ext(t, "https://github.com/login/oauth"), + oidIssuerV2Ext(t, "this does not match"), + }, + // Match both our message and the Go 1.19 message: "certificate contains duplicate extensions" + errorFragment: "duplicate", + }, + { + name: "Completely invalid issuer v2 extension - error parsing", + extensions: []pkix.Extension{ + { + Id: certificate.OIDIssuerV2, + Value: asn1MarshalTest(t, 1, ""), // not a string type + }, + }, + errorFragment: "invalid ASN.1 in OIDC issuer v2 extension: asn1: structure error", + }, + { + name: "Completely invalid issuer v2 extension - trailing data", + extensions: []pkix.Extension{ + { + Id: certificate.OIDIssuerV2, + Value: append(asn1MarshalTest(t, "https://", "utf8"), asn1MarshalTest(t, "example.com", "utf8")...), + }, + }, + errorFragment: "invalid ASN.1 in OIDC issuer v2 extension, trailing data", + }, + { + name: "One valid issuer v1", + extensions: []pkix.Extension{oidIssuerV1Ext("https://github.com/login/oauth")}, + expected: "https://github.com/login/oauth", + }, + { + name: "One valid issuer v2", + extensions: []pkix.Extension{oidIssuerV2Ext(t, "https://github.com/login/oauth")}, + expected: "https://github.com/login/oauth", + }, + { + name: "Inconsistent issuer v1 and v2", + extensions: []pkix.Extension{ + oidIssuerV1Ext("https://github.com/login/oauth"), + oidIssuerV2Ext(t, "this does not match"), + }, + errorFragment: "inconsistent OIDC issuer extension values", + }, + { + name: "Both issuer v1 and v2", + extensions: []pkix.Extension{ + oidIssuerV1Ext("https://github.com/login/oauth"), + oidIssuerV2Ext(t, "https://github.com/login/oauth"), + }, + expected: "https://github.com/login/oauth", + }, + { + name: "Fulcio interoperability", + extensions: fulcioExtensions, + expected: "https://github.com/login/oauth", + }, + } { + testLeafKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err, c.name) + testLeafSN, err := cryptoutils.GenerateSerialNumber() + require.NoError(t, err, c.name) + testLeafContents := x509.Certificate{ + SerialNumber: testLeafSN, + Subject: pkix.Name{CommonName: "leaf"}, + NotBefore: referenceTime.Add(-1 * time.Minute), + NotAfter: referenceTime.Add(1 * time.Hour), + ExtraExtensions: c.extensions, + EmailAddresses: []string{"test-user@example.com"}, + } + // To be fairly representative, we do generate and parse a _real_ certificate, but we just use a self-signed certificate instead + // of bothering with a CA. + testLeafCert, err := x509.CreateCertificate(rand.Reader, &testLeafContents, &testLeafContents, testLeafKey.Public(), testLeafKey) + require.NoError(t, err, c.name) + testLeafPEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: testLeafCert, + }) + + parsedLeafCerts, err := cryptoutils.UnmarshalCertificatesFromPEM(testLeafPEM) + if err != nil { + require.NotEqual(t, "", c.errorFragment) + assert.ErrorContains(t, err, c.errorFragment, c.name) + } else { + require.Len(t, parsedLeafCerts, 1) + parsedLeafCert := parsedLeafCerts[0] + + res, err := fulcioIssuerInCertificate(parsedLeafCert) + if c.errorFragment == "" { + require.NoError(t, err, c.name) + assert.Equal(t, c.expected, res) + } else { + assert.ErrorContains(t, err, c.errorFragment, c.name) + assert.Equal(t, "", res) + } + } + } +} + +func TestFulcioTrustRootVerifyFulcioCertificateAtTime(t *testing.T) { + fulcioCACertificates := x509.NewCertPool() + fulcioCABundlePEM, err := os.ReadFile("fixtures/fulcio_v1.crt.pem") + require.NoError(t, err) + ok := fulcioCACertificates.AppendCertsFromPEM(fulcioCABundlePEM) + require.True(t, ok) + fulcioCertBytes, err := os.ReadFile("fixtures/fulcio-cert") + require.NoError(t, err) + fulcioChainBytes, err := os.ReadFile("fixtures/fulcio-chain") + require.NoError(t, err) + + // A successful verification + tr := fulcioTrustRoot{ + caCertificates: fulcioCACertificates, + oidcIssuer: "https://github.com/login/oauth", + subjectEmail: "mitr@redhat.com", + } + pk, err := tr.verifyFulcioCertificateAtTime(time.Unix(1670870899, 0), fulcioCertBytes, fulcioChainBytes) + require.NoError(t, err) + assertPublicKeyMatchesCert(t, fulcioCertBytes, pk) + + // Invalid intermediate certificates + pk, err = tr.verifyFulcioCertificateAtTime(time.Unix(1670870899, 0), fulcioCertBytes, []byte("not a certificate")) + assert.Error(t, err) + assert.Nil(t, pk) + + // No intermediate certificates: verification fails as is … + pk, err = tr.verifyFulcioCertificateAtTime(time.Unix(1670870899, 0), fulcioCertBytes, []byte{}) + assert.Error(t, err) + assert.Nil(t, pk) + // … but succeeds if we add the intermediate certificates to the root of trust + intermediateCertPool := x509.NewCertPool() + ok = intermediateCertPool.AppendCertsFromPEM(fulcioChainBytes) + require.True(t, ok) + trWithIntermediates := fulcioTrustRoot{ + caCertificates: intermediateCertPool, + oidcIssuer: "https://github.com/login/oauth", + subjectEmail: "mitr@redhat.com", + } + pk, err = trWithIntermediates.verifyFulcioCertificateAtTime(time.Unix(1670870899, 0), fulcioCertBytes, []byte{}) + require.NoError(t, err) + assertPublicKeyMatchesCert(t, fulcioCertBytes, pk) + + // Invalid leaf certificate + for _, c := range [][]byte{ + []byte("not a certificate"), + {}, // Empty + bytes.Repeat(fulcioCertBytes, 2), // More than one certificate + } { + pk, err := tr.verifyFulcioCertificateAtTime(time.Unix(1670870899, 0), c, fulcioChainBytes) + assert.Error(t, err) + assert.Nil(t, pk) + } + + // Unexpected relevantTime + for _, tm := range []time.Time{ + time.Date(2022, time.December, 12, 18, 48, 17, 0, time.UTC), + time.Date(2022, time.December, 12, 18, 58, 19, 0, time.UTC), + } { + pk, err := tr.verifyFulcioCertificateAtTime(tm, fulcioCertBytes, fulcioChainBytes) + assert.Error(t, err) + assert.Nil(t, pk) + } + + referenceTime := time.Now() + testCAKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + testCASN, err := cryptoutils.GenerateSerialNumber() + require.NoError(t, err) + testCAContents := x509.Certificate{ + SerialNumber: testCASN, + Subject: pkix.Name{CommonName: "root CA"}, + NotBefore: referenceTime.Add(-1 * time.Minute), + NotAfter: referenceTime.Add(1 * time.Hour), + BasicConstraintsValid: true, + IsCA: true, + } + testCACertBytes, err := x509.CreateCertificate(rand.Reader, &testCAContents, &testCAContents, + testCAKey.Public(), testCAKey) + require.NoError(t, err) + testCACert, err := x509.ParseCertificate(testCACertBytes) + require.NoError(t, err) + testCACertPool := x509.NewCertPool() + testCACertPool.AddCert(testCACert) + + for _, c := range []struct { + name string + fn func(cert *x509.Certificate) + errorFragment string + }{ + { + // OtherName SAN element, with none of the Go-parsed SAN elements present, + // should not be a reason to reject the certificate entirely; + // but we don’t actually support matching it, so this basically tests that the code + // gets far enough to do subject matching. + name: "OtherName in SAN", + fn: func(cert *x509.Certificate) { + // Setting SAN in ExtraExtensions causes EmailAddresses to be ignored, + // so we need to construct the whole SAN manually. + sansBytes, err := asn1.Marshal([]asn1.RawValue{ + { + Class: 2, + Tag: 0, + IsCompound: false, + Bytes: []byte("otherName"), + }, + }) + require.NoError(t, err) + cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{ + Id: cryptoutils.SANOID, + Critical: true, + Value: sansBytes, + }) + }, + errorFragment: "Required email test-user@example.com not found", + }, + { // Other completely unrecognized critical extensions still cause failures + name: "Unhandled critical extension", + fn: func(cert *x509.Certificate) { + cert.ExtraExtensions = append(cert.ExtraExtensions, pkix.Extension{ + Id: asn1.ObjectIdentifier{2, 99999, 99998, 99997, 99996}, + Critical: true, + Value: []byte("whatever"), + }) + }, + errorFragment: "unhandled critical extension", + }, + { + name: "Missing issuer", + fn: func(cert *x509.Certificate) { + cert.ExtraExtensions = nil // Remove the issuer extension + }, + errorFragment: "Fulcio certificate is missing the issuer extension", + }, + { + name: "Duplicate issuer extension", + fn: func(cert *x509.Certificate) { + cert.ExtraExtensions = append([]pkix.Extension{oidIssuerV1Ext("this does not match")}, cert.ExtraExtensions...) + }, + // Match both our message and the Go 1.19 message: "certificate contains duplicate extensions" + errorFragment: "duplicate", + }, + { + name: "Issuer mismatch", + fn: func(cert *x509.Certificate) { + cert.ExtraExtensions = []pkix.Extension{oidIssuerV1Ext("this does not match")} + }, + errorFragment: "Unexpected Fulcio OIDC issuer", + }, + { + name: "Missing subject email", + fn: func(cert *x509.Certificate) { + cert.EmailAddresses = nil + }, + errorFragment: "Required email test-user@example.com not found", + }, + { + name: "Multiple emails, one matches", + fn: func(cert *x509.Certificate) { + cert.EmailAddresses = []string{"a@example.com", "test-user@example.com", "c@example.com"} + }, + errorFragment: "", + }, + { + name: "Email mismatch", + fn: func(cert *x509.Certificate) { + cert.EmailAddresses = []string{"a@example.com"} + }, + errorFragment: "Required email test-user@example.com not found", + }, + { + name: "Multiple emails, no matches", + fn: func(cert *x509.Certificate) { + cert.EmailAddresses = []string{"a@example.com", "b@example.com", "c@example.com"} + }, + errorFragment: "Required email test-user@example.com not found", + }, + } { + testLeafKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err, c.name) + testLeafSN, err := cryptoutils.GenerateSerialNumber() + require.NoError(t, err, c.name) + testLeafContents := x509.Certificate{ + SerialNumber: testLeafSN, + Subject: pkix.Name{CommonName: "leaf"}, + NotBefore: referenceTime.Add(-1 * time.Minute), + NotAfter: referenceTime.Add(1 * time.Hour), + ExtraExtensions: []pkix.Extension{oidIssuerV1Ext("https://github.com/login/oauth")}, + EmailAddresses: []string{"test-user@example.com"}, + } + c.fn(&testLeafContents) + testLeafCert, err := x509.CreateCertificate(rand.Reader, &testLeafContents, testCACert, testLeafKey.Public(), testCAKey) + require.NoError(t, err, c.name) + tr := fulcioTrustRoot{ + caCertificates: testCACertPool, + oidcIssuer: "https://github.com/login/oauth", + subjectEmail: "test-user@example.com", + } + testLeafPEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: testLeafCert, + }) + pk, err := tr.verifyFulcioCertificateAtTime(referenceTime, testLeafPEM, []byte{}) + if c.errorFragment == "" { + require.NoError(t, err, c.name) + assertPublicKeyMatchesCert(t, testLeafPEM, pk) + } else { + assert.ErrorContains(t, err, c.errorFragment, c.name) + assert.Nil(t, pk, c.name) + } + } +} + +func TestVerifyRekorFulcio(t *testing.T) { + caCertificates := x509.NewCertPool() + fulcioCABundlePEM, err := os.ReadFile("fixtures/fulcio_v1.crt.pem") + require.NoError(t, err) + ok := caCertificates.AppendCertsFromPEM(fulcioCABundlePEM) + require.True(t, ok) + certBytes, err := os.ReadFile("fixtures/fulcio-cert") + require.NoError(t, err) + chainBytes, err := os.ReadFile("fixtures/fulcio-chain") + require.NoError(t, err) + rekorKeyPEM, err := os.ReadFile("fixtures/rekor.pub") + require.NoError(t, err) + rekorKey, err := cryptoutils.UnmarshalPEMToPublicKey(rekorKeyPEM) + require.NoError(t, err) + rekorKeyECDSA, ok := rekorKey.(*ecdsa.PublicKey) + require.True(t, ok) + setBytes, err := os.ReadFile("fixtures/rekor-set") + require.NoError(t, err) + sigBase64, err := os.ReadFile("fixtures/rekor-sig") + require.NoError(t, err) + payloadBytes, err := os.ReadFile("fixtures/rekor-payload") + require.NoError(t, err) + + // Success + pk, err := verifyRekorFulcio(rekorKeyECDSA, &fulcioTrustRoot{ + caCertificates: caCertificates, + oidcIssuer: "https://github.com/login/oauth", + subjectEmail: "mitr@redhat.com", + }, setBytes, certBytes, chainBytes, string(sigBase64), payloadBytes) + require.NoError(t, err) + assertPublicKeyMatchesCert(t, certBytes, pk) + + // Rekor failure + pk, err = verifyRekorFulcio(rekorKeyECDSA, &fulcioTrustRoot{ + caCertificates: caCertificates, + oidcIssuer: "https://github.com/login/oauth", + subjectEmail: "mitr@redhat.com", + }, setBytes, certBytes, chainBytes, string(sigBase64), []byte("this payload does not match")) + assert.Error(t, err) + assert.Nil(t, pk) + + // Fulcio failure + pk, err = verifyRekorFulcio(rekorKeyECDSA, &fulcioTrustRoot{ + caCertificates: caCertificates, + oidcIssuer: "https://github.com/login/oauth", + subjectEmail: "this-does-not-match@example.com", + }, setBytes, certBytes, chainBytes, string(sigBase64), payloadBytes) + assert.Error(t, err) + assert.Nil(t, pk) +} diff --git a/vendor/github.com/containers/image/v5/signature/internal/errors_test.go b/vendor/github.com/containers/image/v5/signature/internal/errors_test.go new file mode 100644 index 00000000000..ee243d30bc2 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/internal/errors_test.go @@ -0,0 +1,14 @@ +package internal + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestInvalidSignatureError(t *testing.T) { + // A stupid test just to keep code coverage + s := "test" + err := NewInvalidSignatureError(s) + assert.Equal(t, s, err.Error()) +} diff --git a/vendor/github.com/containers/image/v5/signature/internal/fixtures_info_test.go b/vendor/github.com/containers/image/v5/signature/internal/fixtures_info_test.go new file mode 100644 index 00000000000..8da249dbcdc --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/internal/fixtures_info_test.go @@ -0,0 +1,15 @@ +package internal + +import "github.com/opencontainers/go-digest" + +const ( + // TestImageManifestDigest is the Docker manifest digest of "image.manifest.json" + TestImageManifestDigest = digest.Digest("sha256:20bf21ed457b390829cdbeec8795a7bea1626991fda603e0d01b4e7f60427e55") + // TestImageSignatureReference is the Docker image reference signed in "image.signature" + TestImageSignatureReference = "testing/manifest" + + // TestSigstoreManifestDigest is the manifest digest of "valid.signature" + TestSigstoreManifestDigest = digest.Digest("sha256:634a8f35b5f16dcf4aaa0822adc0b1964bb786fca12f6831de8ddc45e5986a00") + // TestSigstoreSignatureReference is the Docker reference signed in "valid.signature" + TestSigstoreSignatureReference = "192.168.64.2:5000/cosign-signed-single-sample" +) diff --git a/vendor/github.com/containers/image/v5/signature/internal/json_test.go b/vendor/github.com/containers/image/v5/signature/internal/json_test.go new file mode 100644 index 00000000000..f3a6ff01989 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/internal/json_test.go @@ -0,0 +1,138 @@ +package internal + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type mSA map[string]any // To minimize typing the long name + +// implementsUnmarshalJSON is a minimalistic type used to detect that +// paranoidUnmarshalJSONObject uses the json.Unmarshaler interface of resolved +// pointers. +type implementsUnmarshalJSON bool + +// Compile-time check that Policy implements json.Unmarshaler. +var _ json.Unmarshaler = (*implementsUnmarshalJSON)(nil) + +func (dest *implementsUnmarshalJSON) UnmarshalJSON(data []byte) error { + _ = data // We don't care, not really. + *dest = true // Mark handler as called + return nil +} + +func TestParanoidUnmarshalJSONObject(t *testing.T) { + type testStruct struct { + A string + B int + } + ts := testStruct{} + var unmarshalJSONCalled implementsUnmarshalJSON + tsResolver := func(key string) any { + switch key { + case "a": + return &ts.A + case "b": + return &ts.B + case "implementsUnmarshalJSON": + return &unmarshalJSONCalled + default: + return nil + } + } + + // Empty object + ts = testStruct{} + err := ParanoidUnmarshalJSONObject([]byte(`{}`), tsResolver) + require.NoError(t, err) + assert.Equal(t, testStruct{}, ts) + + // Success + ts = testStruct{} + err = ParanoidUnmarshalJSONObject([]byte(`{"a":"x", "b":2}`), tsResolver) + require.NoError(t, err) + assert.Equal(t, testStruct{A: "x", B: 2}, ts) + + // json.Unmarshaler is used for decoding values + ts = testStruct{} + unmarshalJSONCalled = implementsUnmarshalJSON(false) + err = ParanoidUnmarshalJSONObject([]byte(`{"implementsUnmarshalJSON":true}`), tsResolver) + require.NoError(t, err) + assert.Equal(t, unmarshalJSONCalled, implementsUnmarshalJSON(true)) + + // Various kinds of invalid input + for _, input := range []string{ + ``, // Empty input + `&`, // Entirely invalid JSON + `1`, // Not an object + `{&}`, // Invalid key JSON + `{1:1}`, // Key not a string + `{"b":1, "b":1}`, // Duplicate key + `{"thisdoesnotexist":1}`, // Key rejected by resolver + `{"a":&}`, // Invalid value JSON + `{"a":1}`, // Type mismatch + `{"a":"value"}{}`, // Extra data after object + } { + ts = testStruct{} + err := ParanoidUnmarshalJSONObject([]byte(input), tsResolver) + assert.Error(t, err, input) + } +} + +func TestParanoidUnmarshalJSONObjectExactFields(t *testing.T) { + var stringValue string + var float64Value float64 + var rawValue json.RawMessage + var unmarshallCalled implementsUnmarshalJSON + exactFields := map[string]any{ + "string": &stringValue, + "float64": &float64Value, + "raw": &rawValue, + "unmarshaller": &unmarshallCalled, + } + + // Empty object + err := ParanoidUnmarshalJSONObjectExactFields([]byte(`{}`), map[string]any{}) + require.NoError(t, err) + + // Success + err = ParanoidUnmarshalJSONObjectExactFields([]byte(`{"string": "a", "float64": 3.5, "raw": {"a":"b"}, "unmarshaller": true}`), exactFields) + require.NoError(t, err) + assert.Equal(t, "a", stringValue) + assert.Equal(t, 3.5, float64Value) + assert.Equal(t, json.RawMessage(`{"a":"b"}`), rawValue) + assert.Equal(t, implementsUnmarshalJSON(true), unmarshallCalled) + + // Various kinds of invalid input + for _, input := range []string{ + ``, // Empty input + `&`, // Entirely invalid JSON + `1`, // Not an object + `{&}`, // Invalid key JSON + `{1:1}`, // Key not a string + `{"string": "a", "string": "a", "float64": 3.5, "raw": {"a":"b"}, "unmarshaller": true}`, // Duplicate key + `{"string": "a", "float64": 3.5, "raw": {"a":"b"}, "unmarshaller": true, "thisisunknown", 1}`, // Unknown key + `{"string": &, "float64": 3.5, "raw": {"a":"b"}, "unmarshaller": true}`, // Invalid value JSON + `{"string": 1, "float64": 3.5, "raw": {"a":"b"}, "unmarshaller": true}`, // Type mismatch + `{"string": "a", "float64": 3.5, "raw": {"a":"b"}, "unmarshaller": true}{}`, // Extra data after object + } { + err := ParanoidUnmarshalJSONObjectExactFields([]byte(input), exactFields) + assert.Error(t, err, input) + } +} + +// Return the result of modifying validJSON with fn +func modifiedJSON(t *testing.T, validJSON []byte, modifyFn func(mSA)) []byte { + var tmp mSA + err := json.Unmarshal(validJSON, &tmp) + require.NoError(t, err) + + modifyFn(tmp) + + modifiedJSON, err := json.Marshal(tmp) + require.NoError(t, err) + return modifiedJSON +} diff --git a/vendor/github.com/containers/image/v5/signature/internal/rekor_set_test.go b/vendor/github.com/containers/image/v5/signature/internal/rekor_set_test.go new file mode 100644 index 00000000000..0cc8483d4ae --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/internal/rekor_set_test.go @@ -0,0 +1,400 @@ +package internal + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/sha512" + "encoding/base64" + "encoding/hex" + "encoding/json" + "os" + "testing" + "time" + + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/sigstore/pkg/cryptoutils" + sigstoreSignature "github.com/sigstore/sigstore/pkg/signature" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Verify that input can be unmarshaled as an UntrustedRekorSET. +func successfullyUnmarshalUntrustedRekorSET(t *testing.T, input []byte) UntrustedRekorSET { + var s UntrustedRekorSET + err := json.Unmarshal(input, &s) + require.NoError(t, err, string(input)) + + return s +} + +// Verify that input can't be unmarshaled as an UntrustedRekorSET. +func assertUnmarshalUntrustedRekorSETFails(t *testing.T, input []byte) { + var s UntrustedRekorSET + err := json.Unmarshal(input, &s) + assert.Error(t, err, string(input)) +} + +func TestUntrustedRekorSETUnmarshalJSON(t *testing.T) { + // Invalid input. Note that json.Unmarshal is guaranteed to validate input before calling our + // UnmarshalJSON implementation; so test that first, then test our error handling for completeness. + assertUnmarshalUntrustedRekorSETFails(t, []byte("&")) + var s UntrustedRekorSET + err := s.UnmarshalJSON([]byte("&")) + assert.Error(t, err) + + // Not an object + assertUnmarshalUntrustedRekorSETFails(t, []byte("1")) + + // Start with a valid JSON. + validSET := UntrustedRekorSET{ + UntrustedSignedEntryTimestamp: []byte("signedTimestamp#@!"), + UntrustedPayload: json.RawMessage(`["payload#@!"]`), + } + validJSON, err := json.Marshal(validSET) + require.NoError(t, err) + + // Success + s = successfullyUnmarshalUntrustedRekorSET(t, validJSON) + assert.Equal(t, validSET, s) + + // A /usr/bin/cosign-generated payload is handled correctly + setBytes, err := os.ReadFile("testdata/rekor-set") + require.NoError(t, err) + s = successfullyUnmarshalUntrustedRekorSET(t, setBytes) + expectedSET, err := base64.StdEncoding.DecodeString(`MEYCIQDdeujdGLpMTgFdew9wsSJ3WF7olX9PawgzGeX2RmJd8QIhAPxGJf+HjUFVpQc0hgPaUSK8LsONJ08fZFEBVKDeLj4S`) + require.NoError(t, err) + assert.Equal(t, UntrustedRekorSET{ + UntrustedSignedEntryTimestamp: expectedSET, + UntrustedPayload: []byte(`{"body":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIwZmQxZTk4MzJjYzVhNWY1MDJlODAwZmU5Y2RlZWZiZDMxMzYyZGYxNmZlOGMyMjUwZDMwOGFlYTNmYjFmYzY5In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJRUpjOTZlMDQxVkFoS0EwM1N2ZkNZYldvZElNSVFQeUF0V3lEUDRGblBxcEFpQWFJUzYwRWpoUkRoU2Fub0Zzb0l5OGZLcXFLZVc1cHMvdExYU0dwYXlpMmc9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTnVWRU5EUVdsUFowRjNTVUpCWjBsVlJ6UTFkV0ZETW5vNFZuWjFUM2Q2ZW0wM09WSlFabU5yYjNoM2QwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEplRTFxUlhsTlZHY3dUMFJGTkZkb1kwNU5ha2w0VFdwRmVVMVVaekZQUkVVMFYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZLUW5WeWEyaFRVbTFJZFd0SVZtZ3pWa0U0YmxsMVUxZHhXalJzZEdGbVJIZDVWMndLWXpOak9VNWhURzkyYlhVclRrTTBUbWxWZEUxTWFXWk1MMUF6Ym5GbFpHSnVZM1JMUW5WWmJXWkpVMGRwV214V2VVdFBRMEZWU1hkblowVXJUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZCYlhaSENqUkxObXhPYXk5emF5OW1OR0ZwWVdocVdWSnhVaXRqZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBoUldVUldVakJTUVZGSUwwSkNUWGRGV1VWUVlsZHNNR05yUW5sYVYxSnZXVmhSZFZreU9YUk5RM2RIUTJselIwRlJVVUpuTnpoM1FWRkZSUXBJYldnd1pFaENlazlwT0haYU1td3dZVWhXYVV4dFRuWmlVemx6WWpKa2NHSnBPWFpaV0ZZd1lVUkRRbWxSV1V0TGQxbENRa0ZJVjJWUlNVVkJaMUkzQ2tKSWEwRmtkMEl4UVU0d09VMUhja2Q0ZUVWNVdYaHJaVWhLYkc1T2QwdHBVMncyTkROcWVYUXZOR1ZMWTI5QmRrdGxOazlCUVVGQ2FGRmxjV3gzVlVFS1FVRlJSRUZGV1hkU1FVbG5WbWt5VTNaT05WSmxSMWxwVDFOb1dUaE1SbE5TUnpWRU5tOUJWRXR4U0dJMmEwNHZSRXBvTW5KQlZVTkpRVTVUTmtGeGNBcDRZVmhwU0hkVVNGVnlNM2hRVTBkaE5XazJhSGwzYldKaVVrTTJUakJyU1dWRVRUWk5RVzlIUTBOeFIxTk5ORGxDUVUxRVFUSm5RVTFIVlVOTlEweDFDbU5hZEVWVFNVNHdiRzAyTkVOdkwySmFOamhEUTFKclYyeHJkRmcwYlcxS2FWSm9TMms1WXpsUlJEWXlRelZUZFZwb1l6QjJkbTgyVFU5TGJWUlJTWGdLUVVsdkwwMXZlbHBsYjFVM2NtUk9hakJ3V2t0MVFtVkRiVTF4YlVwaFJGTnpkekU1ZEV0cEwySXhjRVZ0ZFhjclUyWXlRa2t5TlVkblNXSkxlblJITVFvNWR6MDlDaTB0TFMwdFJVNUVJRU5GVWxSSlJrbERRVlJGTFMwdExTMEsifX19fQ==","integratedTime":1670870899,"logIndex":8949589,"logID":"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"}`), + }, s) + + // Various ways to corrupt the JSON + breakFns := []func(mSA){ + // A top-level field is missing + func(v mSA) { delete(v, "SignedEntryTimestamp") }, + func(v mSA) { delete(v, "Payload") }, + // Extra top-level sub-object + func(v mSA) { v["unexpected"] = 1 }, + // "SignedEntryTimestamp" not a string + func(v mSA) { v["critical"] = 1 }, + // "Payload" not an object + func(v mSA) { v["optional"] = 1 }, + } + for _, fn := range breakFns { + testJSON := modifiedJSON(t, validJSON, fn) + assertUnmarshalUntrustedRekorSETFails(t, testJSON) + } +} + +// Verify that input can be unmarshaled as an UntrustedRekorPayload. +func successfullyUnmarshalUntrustedRekorPayload(t *testing.T, input []byte) UntrustedRekorPayload { + var s UntrustedRekorPayload + err := json.Unmarshal(input, &s) + require.NoError(t, err, string(input)) + + return s +} + +// Verify that input can't be unmarshaled as an UntrustedRekorPayload. +func assertUnmarshalUntrustedRekorPayloadFails(t *testing.T, input []byte) { + var s UntrustedRekorPayload + err := json.Unmarshal(input, &s) + assert.Error(t, err, string(input)) +} + +func TestUntrustedRekorPayloadUnmarshalJSON(t *testing.T) { + // Invalid input. Note that json.Unmarshal is guaranteed to validate input before calling our + // UnmarshalJSON implementation; so test that first, then test our error handling for completeness. + assertUnmarshalUntrustedRekorPayloadFails(t, []byte("&")) + var p UntrustedRekorPayload + err := p.UnmarshalJSON([]byte("&")) + assert.Error(t, err) + + // Not an object + assertUnmarshalUntrustedRekorPayloadFails(t, []byte("1")) + + // Start with a valid JSON. + validPayload := UntrustedRekorPayload{ + Body: []byte(`["json"]`), + IntegratedTime: 1, + LogIndex: 2, + LogID: "abc", + } + validJSON, err := validPayload.MarshalJSON() + require.NoError(t, err) + + // Success + p = successfullyUnmarshalUntrustedRekorPayload(t, validJSON) + assert.Equal(t, validPayload, p) + + // A /usr/bin/cosign-generated payload is handled correctly + setBytes, err := os.ReadFile("testdata/rekor-set") + require.NoError(t, err) + s := successfullyUnmarshalUntrustedRekorSET(t, setBytes) + p = successfullyUnmarshalUntrustedRekorPayload(t, s.UntrustedPayload) + expectedBody, err := base64.StdEncoding.DecodeString(`eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIwZmQxZTk4MzJjYzVhNWY1MDJlODAwZmU5Y2RlZWZiZDMxMzYyZGYxNmZlOGMyMjUwZDMwOGFlYTNmYjFmYzY5In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJRUpjOTZlMDQxVkFoS0EwM1N2ZkNZYldvZElNSVFQeUF0V3lEUDRGblBxcEFpQWFJUzYwRWpoUkRoU2Fub0Zzb0l5OGZLcXFLZVc1cHMvdExYU0dwYXlpMmc9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTnVWRU5EUVdsUFowRjNTVUpCWjBsVlJ6UTFkV0ZETW5vNFZuWjFUM2Q2ZW0wM09WSlFabU5yYjNoM2QwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEplRTFxUlhsTlZHY3dUMFJGTkZkb1kwNU5ha2w0VFdwRmVVMVVaekZQUkVVMFYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZLUW5WeWEyaFRVbTFJZFd0SVZtZ3pWa0U0YmxsMVUxZHhXalJzZEdGbVJIZDVWMndLWXpOak9VNWhURzkyYlhVclRrTTBUbWxWZEUxTWFXWk1MMUF6Ym5GbFpHSnVZM1JMUW5WWmJXWkpVMGRwV214V2VVdFBRMEZWU1hkblowVXJUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZCYlhaSENqUkxObXhPYXk5emF5OW1OR0ZwWVdocVdWSnhVaXRqZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBoUldVUldVakJTUVZGSUwwSkNUWGRGV1VWUVlsZHNNR05yUW5sYVYxSnZXVmhSZFZreU9YUk5RM2RIUTJselIwRlJVVUpuTnpoM1FWRkZSUXBJYldnd1pFaENlazlwT0haYU1td3dZVWhXYVV4dFRuWmlVemx6WWpKa2NHSnBPWFpaV0ZZd1lVUkRRbWxSV1V0TGQxbENRa0ZJVjJWUlNVVkJaMUkzQ2tKSWEwRmtkMEl4UVU0d09VMUhja2Q0ZUVWNVdYaHJaVWhLYkc1T2QwdHBVMncyTkROcWVYUXZOR1ZMWTI5QmRrdGxOazlCUVVGQ2FGRmxjV3gzVlVFS1FVRlJSRUZGV1hkU1FVbG5WbWt5VTNaT05WSmxSMWxwVDFOb1dUaE1SbE5TUnpWRU5tOUJWRXR4U0dJMmEwNHZSRXBvTW5KQlZVTkpRVTVUTmtGeGNBcDRZVmhwU0hkVVNGVnlNM2hRVTBkaE5XazJhSGwzYldKaVVrTTJUakJyU1dWRVRUWk5RVzlIUTBOeFIxTk5ORGxDUVUxRVFUSm5RVTFIVlVOTlEweDFDbU5hZEVWVFNVNHdiRzAyTkVOdkwySmFOamhEUTFKclYyeHJkRmcwYlcxS2FWSm9TMms1WXpsUlJEWXlRelZUZFZwb1l6QjJkbTgyVFU5TGJWUlJTWGdLUVVsdkwwMXZlbHBsYjFVM2NtUk9hakJ3V2t0MVFtVkRiVTF4YlVwaFJGTnpkekU1ZEV0cEwySXhjRVZ0ZFhjclUyWXlRa2t5TlVkblNXSkxlblJITVFvNWR6MDlDaTB0TFMwdFJVNUVJRU5GVWxSSlJrbERRVlJGTFMwdExTMEsifX19fQ==`) + require.NoError(t, err) + assert.Equal(t, UntrustedRekorPayload{ + Body: expectedBody, + IntegratedTime: 1670870899, + LogIndex: 8949589, + LogID: "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d", + }, p) + + // Various ways to corrupt the JSON + breakFns := []func(mSA){ + // A top-level field is missing + func(v mSA) { delete(v, "body") }, + func(v mSA) { delete(v, "integratedTime") }, + func(v mSA) { delete(v, "logIndex") }, + func(v mSA) { delete(v, "logID") }, + // Extra top-level sub-object + func(v mSA) { v["unexpected"] = 1 }, + // "body" not a string + func(v mSA) { v["body"] = 1 }, + // "integratedTime" not an integer + func(v mSA) { v["integratedTime"] = "hello" }, + // "logIndex" not an integer + func(v mSA) { v["logIndex"] = "hello" }, + // "logID" not a string + func(v mSA) { v["logID"] = 1 }, + } + for _, fn := range breakFns { + testJSON := modifiedJSON(t, validJSON, fn) + assertUnmarshalUntrustedRekorPayloadFails(t, testJSON) + } +} + +func TestVerifyRekorSET(t *testing.T) { + cosignRekorKeyPEM, err := os.ReadFile("testdata/rekor.pub") + require.NoError(t, err) + cosignRekorKey, err := cryptoutils.UnmarshalPEMToPublicKey(cosignRekorKeyPEM) + require.NoError(t, err) + cosignRekorKeyECDSA, ok := cosignRekorKey.(*ecdsa.PublicKey) + require.True(t, ok) + cosignSETBytes, err := os.ReadFile("testdata/rekor-set") + require.NoError(t, err) + cosignCertBytes, err := os.ReadFile("testdata/rekor-cert") + require.NoError(t, err) + cosignSigBase64, err := os.ReadFile("testdata/rekor-sig") + require.NoError(t, err) + cosignPayloadBytes, err := os.ReadFile("testdata/rekor-payload") + require.NoError(t, err) + + // Successful verification + tm, err := VerifyRekorSET(cosignRekorKeyECDSA, cosignSETBytes, cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes) + require.NoError(t, err) + assert.Equal(t, time.Unix(1670870899, 0), tm) + + // For extra paranoia, test that we return a zero time on error. + + // A completely invalid SET. + tm, err = VerifyRekorSET(cosignRekorKeyECDSA, []byte{}, cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes) + assert.Error(t, err) + assert.Zero(t, tm) + + tm, err = VerifyRekorSET(cosignRekorKeyECDSA, []byte("invalid signature"), cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes) + assert.Error(t, err) + assert.Zero(t, tm) + + testKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + testSigner, err := sigstoreSignature.LoadECDSASigner(testKey, crypto.SHA256) + require.NoError(t, err) + + // JSON canonicalization fails: + // This payload is invalid because it has duplicate fields. + // Right now, that particular failure (unlike more blatantly invalid JSON) is allowed + // by json.Marshal, but detected by jsoncanonicalizer.Transform. + invalidPayload := []byte(`{"logIndex":1, "integratedTime":2,"body":"abc","logID":"def","body":"ABC"}`) + invalidPayloadSig, err := testSigner.SignMessage(bytes.NewReader(invalidPayload)) + require.NoError(t, err) + invalidSET, err := json.Marshal(UntrustedRekorSET{ + UntrustedSignedEntryTimestamp: invalidPayloadSig, + UntrustedPayload: json.RawMessage(invalidPayload), + }) + require.NoError(t, err) + tm, err = VerifyRekorSET(&testKey.PublicKey, invalidSET, cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes) + assert.Error(t, err) + assert.Zero(t, tm) + + // Cryptographic verification fails (a mismatched public key) + tm, err = VerifyRekorSET(&testKey.PublicKey, cosignSETBytes, cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes) + assert.Error(t, err) + assert.Zero(t, tm) + + // Parsing UntrustedRekorPayload fails + invalidPayload = []byte(`{}`) + invalidPayloadSig, err = testSigner.SignMessage(bytes.NewReader(invalidPayload)) + require.NoError(t, err) + invalidSET, err = json.Marshal(UntrustedRekorSET{ + UntrustedSignedEntryTimestamp: invalidPayloadSig, + UntrustedPayload: json.RawMessage(invalidPayload), + }) + require.NoError(t, err) + tm, err = VerifyRekorSET(&testKey.PublicKey, invalidSET, cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes) + assert.Error(t, err) + assert.Zero(t, tm) + + // A correctly signed UntrustedRekorPayload is invalid + cosignPayloadSHA256 := sha256.Sum256(cosignPayloadBytes) + cosignSigBytes, err := base64.StdEncoding.DecodeString(string(cosignSigBase64)) + require.NoError(t, err) + validHashedRekord := models.Hashedrekord{ + APIVersion: swag.String(HashedRekordV001APIVersion), + Spec: models.HashedrekordV001Schema{ + Data: &models.HashedrekordV001SchemaData{ + Hash: &models.HashedrekordV001SchemaDataHash{ + Algorithm: swag.String(models.HashedrekordV001SchemaDataHashAlgorithmSha256), + Value: swag.String(hex.EncodeToString(cosignPayloadSHA256[:])), + }, + }, + Signature: &models.HashedrekordV001SchemaSignature{ + Content: strfmt.Base64(cosignSigBytes), + PublicKey: &models.HashedrekordV001SchemaSignaturePublicKey{ + Content: strfmt.Base64(cosignCertBytes), + }, + }, + }, + } + validHashedRekordJSON, err := json.Marshal(validHashedRekord) + require.NoError(t, err) + for _, fn := range []func(mSA){ + // A Hashedrekord field is missing + func(v mSA) { delete(v, "apiVersion") }, + func(v mSA) { delete(v, "kind") }, // "kind" is not visible in the type definition, but required by the implementation + func(v mSA) { delete(v, "spec") }, + // This, along with many other extra fields, is currently accepted. That is NOT an API commitment. + // func(v mSA) { v["unexpected"] = 1 }, // Extra top-level field: + // Invalid apiVersion + func(v mSA) { v["apiVersion"] = nil }, + func(v mSA) { v["apiVersion"] = 1 }, + func(v mSA) { v["apiVersion"] = mSA{} }, + func(v mSA) { v["apiVersion"] = "99.0.99" }, + // Invalid kind + func(v mSA) { v["kind"] = nil }, + func(v mSA) { v["kind"] = 1 }, + func(v mSA) { v["kind"] = "notHashedRekord" }, + // Invalid spec + func(v mSA) { v["spec"] = nil }, + func(v mSA) { v["spec"] = 1 }, + // A HashedRekordV001Schema field is missing + func(v mSA) { delete(x(v, "spec"), "data") }, + func(v mSA) { delete(x(v, "spec"), "signature") }, + // Invalid spec.data + func(v mSA) { x(v, "spec")["data"] = nil }, + func(v mSA) { x(v, "spec")["data"] = 1 }, + // Missing spec.data.hash + func(v mSA) { delete(x(v, "spec", "data"), "hash") }, + // Invalid spec.data.hash + func(v mSA) { x(v, "spec", "data")["hash"] = nil }, + func(v mSA) { x(v, "spec", "data")["hash"] = 1 }, + // A spec.data.hash field is missing + func(v mSA) { delete(x(v, "spec", "data", "hash"), "algorithm") }, + func(v mSA) { delete(x(v, "spec", "data", "hash"), "value") }, + // Invalid spec.data.hash.algorithm + func(v mSA) { x(v, "spec", "data", "hash")["algorithm"] = nil }, + func(v mSA) { x(v, "spec", "data", "hash")["algorithm"] = 1 }, + // Invalid spec.data.hash.value + func(v mSA) { x(v, "spec", "data", "hash")["value"] = nil }, + func(v mSA) { x(v, "spec", "data", "hash")["value"] = 1 }, + func(v mSA) { // An odd number of hexadecimal digits + x(v, "spec", "data", "hash")["value"] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + // spec.data.hash does not match + func(v mSA) { + x(v, "spec", "data", "hash")["value"] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + // A non-sha256 hash + func(v mSA) { + x(v, "spec", "data", "hash")["algorithm"] = "sha512" + h := sha512.Sum512(cosignPayloadBytes) + x(v, "spec", "data", "hash")["value"] = hex.EncodeToString(h[:]) + }, + // Invalid spec.signature + func(v mSA) { x(v, "spec")["signature"] = nil }, + func(v mSA) { x(v, "spec")["signature"] = 1 }, + // A spec.signature field is missing + func(v mSA) { delete(x(v, "spec", "signature"), "content") }, + func(v mSA) { delete(x(v, "spec", "signature"), "publicKey") }, + // Invalid spec.signature.content + func(v mSA) { x(v, "spec", "signature")["content"] = nil }, + func(v mSA) { x(v, "spec", "signature")["content"] = 1 }, + func(v mSA) { x(v, "spec", "signature")["content"] = "" }, + func(v mSA) { x(v, "spec", "signature")["content"] = "+" }, // Invalid base64 + // spec.signature.content does not match + func(v mSA) { + x(v, "spec", "signature")["content"] = base64.StdEncoding.EncodeToString([]byte("does not match")) + }, + // Invalid spec.signature.publicKey + func(v mSA) { x(v, "spec", "signature")["publicKey"] = nil }, + func(v mSA) { x(v, "spec", "signature")["publicKey"] = 1 }, + // Missing spec.signature.publicKey.content + func(v mSA) { delete(x(v, "spec", "signature", "publicKey"), "content") }, + // Invalid spec.signature.publicKey.content + func(v mSA) { x(v, "spec", "signature", "publicKey")["content"] = nil }, + func(v mSA) { x(v, "spec", "signature", "publicKey")["content"] = 1 }, + func(v mSA) { x(v, "spec", "signature", "publicKey")["content"] = "" }, + func(v mSA) { x(v, "spec", "signature", "publicKey")["content"] = "+" }, // Invalid base64 + func(v mSA) { + x(v, "spec", "signature", "publicKey")["content"] = base64.StdEncoding.EncodeToString([]byte("not PEM")) + }, + func(v mSA) { // Multiple PEM blocks + x(v, "spec", "signature", "publicKey")["content"] = base64.StdEncoding.EncodeToString(bytes.Repeat(cosignCertBytes, 2)) + }, + // spec.signature.publicKey.content does not match + func(v mSA) { + otherKey, err := testSigner.PublicKey() + require.NoError(t, err) + otherPEM, err := cryptoutils.MarshalPublicKeyToPEM(otherKey) + require.NoError(t, err) + x(v, "spec", "signature", "publicKey")["content"] = base64.StdEncoding.EncodeToString(otherPEM) + }, + } { + testHashedRekordJSON := modifiedJSON(t, validHashedRekordJSON, fn) + testPayload, err := json.Marshal(UntrustedRekorPayload{ + Body: testHashedRekordJSON, + IntegratedTime: 1, + LogIndex: 2, + LogID: "logID", + }) + require.NoError(t, err) + testPayloadSig, err := testSigner.SignMessage(bytes.NewReader(testPayload)) + require.NoError(t, err) + testSET, err := json.Marshal(UntrustedRekorSET{ + UntrustedSignedEntryTimestamp: testPayloadSig, + UntrustedPayload: json.RawMessage(testPayload), + }) + require.NoError(t, err) + tm, err = VerifyRekorSET(&testKey.PublicKey, testSET, cosignCertBytes, string(cosignSigBase64), cosignPayloadBytes) + assert.Error(t, err) + assert.Zero(t, tm) + } + + // Invalid unverifiedBase64Signature parameter + truncatedBase64 := cosignSigBase64 + truncatedBase64 = truncatedBase64[:len(truncatedBase64)-1] + tm, err = VerifyRekorSET(cosignRekorKeyECDSA, cosignSETBytes, cosignCertBytes, + string(truncatedBase64), cosignPayloadBytes) + assert.Error(t, err) + assert.Zero(t, tm) + + // Invalid unverifiedKeyOrCertBytes + for _, c := range [][]byte{ + nil, + {}, + []byte("this is not PEM"), + bytes.Repeat(cosignCertBytes, 2), + } { + tm, err = VerifyRekorSET(cosignRekorKeyECDSA, cosignSETBytes, c, + string(cosignSigBase64), cosignPayloadBytes) + assert.Error(t, err) + assert.Zero(t, tm) + } +} diff --git a/vendor/github.com/containers/image/v5/signature/internal/sigstore_payload_test.go b/vendor/github.com/containers/image/v5/signature/internal/sigstore_payload_test.go new file mode 100644 index 00000000000..eaee202a340 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/internal/sigstore_payload_test.go @@ -0,0 +1,328 @@ +package internal + +import ( + "encoding/base64" + "encoding/json" + "errors" + "os" + "testing" + "time" + + "github.com/containers/image/v5/internal/signature" + "github.com/containers/image/v5/version" + digest "github.com/opencontainers/go-digest" + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// A short-hand way to get a JSON object field value or panic. No error handling done, we know +// what we are working with, a panic in a test is good enough, and fitting test cases on a single line +// is a priority. +func x(m mSA, fields ...string) mSA { + for _, field := range fields { + // Not .(mSA) because type assertion of an unnamed type to a named type always fails (the types + // are not "identical"), but the assignment is fine because they are "assignable". + m = m[field].(map[string]any) + } + return m +} + +func TestNewUntrustedSigstorePayload(t *testing.T) { + timeBefore := time.Now() + sig := NewUntrustedSigstorePayload(TestImageManifestDigest, TestImageSignatureReference) + assert.Equal(t, TestImageManifestDigest, sig.untrustedDockerManifestDigest) + assert.Equal(t, TestImageSignatureReference, sig.untrustedDockerReference) + require.NotNil(t, sig.untrustedCreatorID) + assert.Equal(t, "containers/image "+version.Version, *sig.untrustedCreatorID) + require.NotNil(t, sig.untrustedTimestamp) + timeAfter := time.Now() + assert.True(t, timeBefore.Unix() <= *sig.untrustedTimestamp) + assert.True(t, *sig.untrustedTimestamp <= timeAfter.Unix()) +} + +func TestUntrustedSigstorePayloadMarshalJSON(t *testing.T) { + // Empty string values + s := NewUntrustedSigstorePayload("", "_") + _, err := s.MarshalJSON() + assert.Error(t, err) + s = NewUntrustedSigstorePayload("_", "") + _, err = s.MarshalJSON() + assert.Error(t, err) + + // Success + // Use intermediate variables for these values so that we can take their addresses. + creatorID := "CREATOR" + timestamp := int64(1484683104) + for _, c := range []struct { + input UntrustedSigstorePayload + expected string + }{ + { + UntrustedSigstorePayload{ + untrustedDockerManifestDigest: "digest!@#", + untrustedDockerReference: "reference#@!", + untrustedCreatorID: &creatorID, + untrustedTimestamp: ×tamp, + }, + "{\"critical\":{\"identity\":{\"docker-reference\":\"reference#@!\"},\"image\":{\"docker-manifest-digest\":\"digest!@#\"},\"type\":\"cosign container image signature\"},\"optional\":{\"creator\":\"CREATOR\",\"timestamp\":1484683104}}", + }, + { + UntrustedSigstorePayload{ + untrustedDockerManifestDigest: "digest!@#", + untrustedDockerReference: "reference#@!", + }, + "{\"critical\":{\"identity\":{\"docker-reference\":\"reference#@!\"},\"image\":{\"docker-manifest-digest\":\"digest!@#\"},\"type\":\"cosign container image signature\"},\"optional\":{}}", + }, + } { + marshaled, err := c.input.MarshalJSON() + require.NoError(t, err) + assert.Equal(t, []byte(c.expected), marshaled) + + // Also call MarshalJSON through the JSON package. + marshaled, err = json.Marshal(c.input) + assert.NoError(t, err) + assert.Equal(t, []byte(c.expected), marshaled) + } +} + +// Verify that input can be unmarshaled as an UntrustedSigstorePayload. +func successfullyUnmarshalUntrustedSigstorePayload(t *testing.T, input []byte) UntrustedSigstorePayload { + var s UntrustedSigstorePayload + err := json.Unmarshal(input, &s) + require.NoError(t, err, string(input)) + + return s +} + +// Verify that input can't be unmarshaled as an UntrustedSigstorePayload. +func assertUnmarshalUntrustedSigstorePayloadFails(t *testing.T, input []byte) { + var s UntrustedSigstorePayload + err := json.Unmarshal(input, &s) + assert.Error(t, err, string(input)) +} + +func TestUntrustedSigstorePayloadUnmarshalJSON(t *testing.T) { + // Invalid input. Note that json.Unmarshal is guaranteed to validate input before calling our + // UnmarshalJSON implementation; so test that first, then test our error handling for completeness. + assertUnmarshalUntrustedSigstorePayloadFails(t, []byte("&")) + var s UntrustedSigstorePayload + err := s.UnmarshalJSON([]byte("&")) + assert.Error(t, err) + + // Not an object + assertUnmarshalUntrustedSigstorePayloadFails(t, []byte("1")) + + // Start with a valid JSON. + validSig := NewUntrustedSigstorePayload("digest!@#", "reference#@!") + validJSON, err := validSig.MarshalJSON() + require.NoError(t, err) + + // Success + s = successfullyUnmarshalUntrustedSigstorePayload(t, validJSON) + assert.Equal(t, validSig, s) + + // A /usr/bin/cosign-generated payload is handled correctly + s = successfullyUnmarshalUntrustedSigstorePayload(t, []byte(`{"critical":{"identity":{"docker-reference":"192.168.64.2:5000/cosign-signed-multi"},"image":{"docker-manifest-digest":"sha256:43955d6857268cc948ae9b370b221091057de83c4962da0826f9a2bdc9bd6b44"},"type":"cosign container image signature"},"optional":null}`)) + assert.Equal(t, UntrustedSigstorePayload{ + untrustedDockerManifestDigest: "sha256:43955d6857268cc948ae9b370b221091057de83c4962da0826f9a2bdc9bd6b44", + untrustedDockerReference: "192.168.64.2:5000/cosign-signed-multi", + untrustedCreatorID: nil, + untrustedTimestamp: nil, + }, s) + + // Various ways to corrupt the JSON + breakFns := []func(mSA){ + // A top-level field is missing + func(v mSA) { delete(v, "critical") }, + func(v mSA) { delete(v, "optional") }, + // Extra top-level sub-object + func(v mSA) { v["unexpected"] = 1 }, + // "critical" not an object + func(v mSA) { v["critical"] = 1 }, + // "optional" not an object + func(v mSA) { v["optional"] = 1 }, + // A field of "critical" is missing + func(v mSA) { delete(x(v, "critical"), "type") }, + func(v mSA) { delete(x(v, "critical"), "image") }, + func(v mSA) { delete(x(v, "critical"), "identity") }, + // Extra field of "critical" + func(v mSA) { x(v, "critical")["unexpected"] = 1 }, + // Invalid "type" + func(v mSA) { x(v, "critical")["type"] = 1 }, + func(v mSA) { x(v, "critical")["type"] = "unexpected" }, + // Invalid "image" object + func(v mSA) { x(v, "critical")["image"] = 1 }, + func(v mSA) { delete(x(v, "critical", "image"), "docker-manifest-digest") }, + func(v mSA) { x(v, "critical", "image")["unexpected"] = 1 }, + // Invalid "docker-manifest-digest" + func(v mSA) { x(v, "critical", "image")["docker-manifest-digest"] = 1 }, + // Invalid "identity" object + func(v mSA) { x(v, "critical")["identity"] = 1 }, + func(v mSA) { delete(x(v, "critical", "identity"), "docker-reference") }, + func(v mSA) { x(v, "critical", "identity")["unexpected"] = 1 }, + // Invalid "docker-reference" + func(v mSA) { x(v, "critical", "identity")["docker-reference"] = 1 }, + // Invalid "creator" + func(v mSA) { x(v, "optional")["creator"] = 1 }, + // Invalid "timestamp" + func(v mSA) { x(v, "optional")["timestamp"] = "unexpected" }, + func(v mSA) { x(v, "optional")["timestamp"] = 0.5 }, // Fractional input + } + for _, fn := range breakFns { + testJSON := modifiedJSON(t, validJSON, fn) + assertUnmarshalUntrustedSigstorePayloadFails(t, testJSON) + } + + // Modifications to unrecognized fields in "optional" are allowed and ignored + allowedModificationFns := []func(mSA){ + // Add an optional field + func(v mSA) { x(v, "optional")["unexpected"] = 1 }, + } + for _, fn := range allowedModificationFns { + testJSON := modifiedJSON(t, validJSON, fn) + s := successfullyUnmarshalUntrustedSigstorePayload(t, testJSON) + assert.Equal(t, validSig, s) + } + + // Optional fields can be missing + validSig = UntrustedSigstorePayload{ + untrustedDockerManifestDigest: "digest!@#", + untrustedDockerReference: "reference#@!", + untrustedCreatorID: nil, + untrustedTimestamp: nil, + } + validJSON, err = validSig.MarshalJSON() + require.NoError(t, err) + s = successfullyUnmarshalUntrustedSigstorePayload(t, validJSON) + assert.Equal(t, validSig, s) +} + +func TestVerifySigstorePayload(t *testing.T) { + publicKeyPEM, err := os.ReadFile("./testdata/cosign.pub") + require.NoError(t, err) + publicKey, err := cryptoutils.UnmarshalPEMToPublicKey(publicKeyPEM) + require.NoError(t, err) + + type acceptanceData struct { + signedDockerReference string + signedDockerManifestDigest digest.Digest + } + var wanted, recorded acceptanceData + // recordingRules are a plausible SigstorePayloadAcceptanceRules implementations, but equally + // importantly record that we are passing the correct values to the rule callbacks. + recordingRules := SigstorePayloadAcceptanceRules{ + ValidateSignedDockerReference: func(signedDockerReference string) error { + recorded.signedDockerReference = signedDockerReference + if signedDockerReference != wanted.signedDockerReference { + return errors.New("signedDockerReference mismatch") + } + return nil + }, + ValidateSignedDockerManifestDigest: func(signedDockerManifestDigest digest.Digest) error { + recorded.signedDockerManifestDigest = signedDockerManifestDigest + if signedDockerManifestDigest != wanted.signedDockerManifestDigest { + return errors.New("signedDockerManifestDigest mismatch") + } + return nil + }, + } + + sigBlob, err := os.ReadFile("./testdata/valid.signature") + require.NoError(t, err) + genericSig, err := signature.FromBlob(sigBlob) + require.NoError(t, err) + sigstoreSig, ok := genericSig.(signature.Sigstore) + require.True(t, ok) + cryptoBase64Sig, ok := sigstoreSig.UntrustedAnnotations()[signature.SigstoreSignatureAnnotationKey] + require.True(t, ok) + signatureData := acceptanceData{ + signedDockerReference: TestSigstoreSignatureReference, + signedDockerManifestDigest: TestSigstoreManifestDigest, + } + + // Successful verification + wanted = signatureData + recorded = acceptanceData{} + res, err := VerifySigstorePayload(publicKey, sigstoreSig.UntrustedPayload(), cryptoBase64Sig, recordingRules) + require.NoError(t, err) + assert.Equal(t, res, &UntrustedSigstorePayload{ + untrustedDockerManifestDigest: TestSigstoreManifestDigest, + untrustedDockerReference: TestSigstoreSignatureReference, + untrustedCreatorID: nil, + untrustedTimestamp: nil, + }) + assert.Equal(t, signatureData, recorded) + + // For extra paranoia, test that we return a nil signature object on error. + + // Invalid verifier + recorded = acceptanceData{} + invalidPublicKey := struct{}{} // crypto.PublicKey is, for some reason, just an any, so this is acceptable. + res, err = VerifySigstorePayload(invalidPublicKey, sigstoreSig.UntrustedPayload(), cryptoBase64Sig, recordingRules) + assert.Error(t, err) + assert.Nil(t, res) + assert.Equal(t, acceptanceData{}, recorded) + + // Invalid base64 encoding + for _, invalidBase64Sig := range []string{ + "&", // Invalid base64 characters + cryptoBase64Sig + "=", // Extra padding + cryptoBase64Sig[:len(cryptoBase64Sig)-1], // Truncated base64 data + } { + recorded = acceptanceData{} + res, err = VerifySigstorePayload(publicKey, sigstoreSig.UntrustedPayload(), invalidBase64Sig, recordingRules) + assert.Error(t, err) + assert.Nil(t, res) + assert.Equal(t, acceptanceData{}, recorded) + } + + // Invalid signature + validSignatureBytes, err := base64.StdEncoding.DecodeString(cryptoBase64Sig) + require.NoError(t, err) + for _, invalidSig := range [][]byte{ + {}, // Empty signature + []byte("invalid signature"), + append(validSignatureBytes, validSignatureBytes...), + } { + recorded = acceptanceData{} + res, err = VerifySigstorePayload(publicKey, sigstoreSig.UntrustedPayload(), base64.StdEncoding.EncodeToString(invalidSig), recordingRules) + assert.Error(t, err) + assert.Nil(t, res) + assert.Equal(t, acceptanceData{}, recorded) + } + + // Valid signature of non-JSON + recorded = acceptanceData{} + res, err = VerifySigstorePayload(publicKey, []byte("&"), "MEUCIARnnxZQPALBfqkB4aNAYXad79Qs6VehcrgIeZ8p7I2FAiEAzq2HXwXlz1iJeh+ucUR3L0zpjynQk6Rk0+/gXYp49RU=", recordingRules) + assert.Error(t, err) + assert.Nil(t, res) + assert.Equal(t, acceptanceData{}, recorded) + + // Valid signature of an unacceptable JSON + recorded = acceptanceData{} + res, err = VerifySigstorePayload(publicKey, []byte("{}"), "MEUCIQDkySOBGxastVP0+koTA33NH5hXjwosFau4rxTPN6g48QIgb7eWKkGqfEpHMM3aT4xiqyP/170jEkdFuciuwN4mux4=", recordingRules) + assert.Error(t, err) + assert.Nil(t, res) + assert.Equal(t, acceptanceData{}, recorded) + + // Valid signature with a wrong manifest digest: asked for signedDockerManifestDigest + wanted = signatureData + wanted.signedDockerManifestDigest = "invalid digest" + recorded = acceptanceData{} + res, err = VerifySigstorePayload(publicKey, sigstoreSig.UntrustedPayload(), cryptoBase64Sig, recordingRules) + assert.Error(t, err) + assert.Nil(t, res) + assert.Equal(t, acceptanceData{ + signedDockerManifestDigest: signatureData.signedDockerManifestDigest, + }, recorded) + + // Valid signature with a wrong image reference + wanted = signatureData + wanted.signedDockerReference = "unexpected docker reference" + recorded = acceptanceData{} + res, err = VerifySigstorePayload(publicKey, sigstoreSig.UntrustedPayload(), cryptoBase64Sig, recordingRules) + assert.Error(t, err) + assert.Nil(t, res) + assert.Equal(t, signatureData, recorded) +} diff --git a/vendor/github.com/containers/image/v5/signature/internal/testdata/cosign.pub b/vendor/github.com/containers/image/v5/signature/internal/testdata/cosign.pub new file mode 120000 index 00000000000..96b28799348 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/internal/testdata/cosign.pub @@ -0,0 +1 @@ +../../fixtures/cosign.pub \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/internal/testdata/rekor-cert b/vendor/github.com/containers/image/v5/signature/internal/testdata/rekor-cert new file mode 100644 index 00000000000..734b8bb2004 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/internal/testdata/rekor-cert @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICnTCCAiOgAwIBAgIUG45uaC2z8VvuOwzzm79RPfckoxwwCgYIKoZIzj0EAwMw +NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl +cm1lZGlhdGUwHhcNMjIxMjEyMTg0ODE4WhcNMjIxMjEyMTg1ODE4WjAAMFkwEwYH +KoZIzj0CAQYIKoZIzj0DAQcDQgAEJBurkhSRmHukHVh3VA8nYuSWqZ4ltafDwyWl +c3c9NaLovmu+NC4NiUtMLifL/P3nqedbnctKBuYmfISGiZlVyKOCAUIwggE+MA4G +A1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUAmvG +4K6lNk/sk/f4aiahjYRqR+cwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y +ZD8wHQYDVR0RAQH/BBMwEYEPbWl0ckByZWRoYXQuY29tMCwGCisGAQQBg78wAQEE +Hmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDCBiQYKKwYBBAHWeQIEAgR7 +BHkAdwB1AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6OAAABhQeqlwUA +AAQDAEYwRAIgVi2SvN5ReGYiOShY8LFSRG5D6oATKqHb6kN/DJh2rAUCIANS6Aqp +xaXiHwTHUr3xPSGa5i6hywmbbRC6N0kIeDM6MAoGCCqGSM49BAMDA2gAMGUCMCLu +cZtESIN0lm64Co/bZ68CCRkWlktX4mmJiRhKi9c9QD62C5SuZhc0vvo6MOKmTQIx +AIo/MozZeoU7rdNj0pZKuBeCmMqmJaDSsw19tKi/b1pEmuw+Sf2BI25GgIbKztG1 +9w== +-----END CERTIFICATE----- diff --git a/vendor/github.com/containers/image/v5/signature/internal/testdata/rekor-payload b/vendor/github.com/containers/image/v5/signature/internal/testdata/rekor-payload new file mode 100644 index 00000000000..b3cf6c40d69 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/internal/testdata/rekor-payload @@ -0,0 +1 @@ +{"critical":{"identity":{"docker-reference":"192.168.64.2:5000/test-repo-2100826901021538654/alpine"},"image":{"docker-manifest-digest":"sha256:fa93b01658e3a5a1686dc3ae55f170d8de487006fb53a28efcd12ab0710a2e5f"},"type":"cosign container image signature"},"optional":null} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/internal/testdata/rekor-set b/vendor/github.com/containers/image/v5/signature/internal/testdata/rekor-set new file mode 100644 index 00000000000..04ca1e5487c --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/internal/testdata/rekor-set @@ -0,0 +1 @@ +{"SignedEntryTimestamp":"MEYCIQDdeujdGLpMTgFdew9wsSJ3WF7olX9PawgzGeX2RmJd8QIhAPxGJf+HjUFVpQc0hgPaUSK8LsONJ08fZFEBVKDeLj4S","Payload":{"body":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIwZmQxZTk4MzJjYzVhNWY1MDJlODAwZmU5Y2RlZWZiZDMxMzYyZGYxNmZlOGMyMjUwZDMwOGFlYTNmYjFmYzY5In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJRUpjOTZlMDQxVkFoS0EwM1N2ZkNZYldvZElNSVFQeUF0V3lEUDRGblBxcEFpQWFJUzYwRWpoUkRoU2Fub0Zzb0l5OGZLcXFLZVc1cHMvdExYU0dwYXlpMmc9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTnVWRU5EUVdsUFowRjNTVUpCWjBsVlJ6UTFkV0ZETW5vNFZuWjFUM2Q2ZW0wM09WSlFabU5yYjNoM2QwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEplRTFxUlhsTlZHY3dUMFJGTkZkb1kwNU5ha2w0VFdwRmVVMVVaekZQUkVVMFYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZLUW5WeWEyaFRVbTFJZFd0SVZtZ3pWa0U0YmxsMVUxZHhXalJzZEdGbVJIZDVWMndLWXpOak9VNWhURzkyYlhVclRrTTBUbWxWZEUxTWFXWk1MMUF6Ym5GbFpHSnVZM1JMUW5WWmJXWkpVMGRwV214V2VVdFBRMEZWU1hkblowVXJUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZCYlhaSENqUkxObXhPYXk5emF5OW1OR0ZwWVdocVdWSnhVaXRqZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBoUldVUldVakJTUVZGSUwwSkNUWGRGV1VWUVlsZHNNR05yUW5sYVYxSnZXVmhSZFZreU9YUk5RM2RIUTJselIwRlJVVUpuTnpoM1FWRkZSUXBJYldnd1pFaENlazlwT0haYU1td3dZVWhXYVV4dFRuWmlVemx6WWpKa2NHSnBPWFpaV0ZZd1lVUkRRbWxSV1V0TGQxbENRa0ZJVjJWUlNVVkJaMUkzQ2tKSWEwRmtkMEl4UVU0d09VMUhja2Q0ZUVWNVdYaHJaVWhLYkc1T2QwdHBVMncyTkROcWVYUXZOR1ZMWTI5QmRrdGxOazlCUVVGQ2FGRmxjV3gzVlVFS1FVRlJSRUZGV1hkU1FVbG5WbWt5VTNaT05WSmxSMWxwVDFOb1dUaE1SbE5TUnpWRU5tOUJWRXR4U0dJMmEwNHZSRXBvTW5KQlZVTkpRVTVUTmtGeGNBcDRZVmhwU0hkVVNGVnlNM2hRVTBkaE5XazJhSGwzYldKaVVrTTJUakJyU1dWRVRUWk5RVzlIUTBOeFIxTk5ORGxDUVUxRVFUSm5RVTFIVlVOTlEweDFDbU5hZEVWVFNVNHdiRzAyTkVOdkwySmFOamhEUTFKclYyeHJkRmcwYlcxS2FWSm9TMms1WXpsUlJEWXlRelZUZFZwb1l6QjJkbTgyVFU5TGJWUlJTWGdLUVVsdkwwMXZlbHBsYjFVM2NtUk9hakJ3V2t0MVFtVkRiVTF4YlVwaFJGTnpkekU1ZEV0cEwySXhjRVZ0ZFhjclUyWXlRa2t5TlVkblNXSkxlblJITVFvNWR6MDlDaTB0TFMwdFJVNUVJRU5GVWxSSlJrbERRVlJGTFMwdExTMEsifX19fQ==","integratedTime":1670870899,"logIndex":8949589,"logID":"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"}} \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/internal/testdata/rekor-sig b/vendor/github.com/containers/image/v5/signature/internal/testdata/rekor-sig new file mode 100644 index 00000000000..5735a2341a6 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/internal/testdata/rekor-sig @@ -0,0 +1 @@ +MEQCIEJc96e041VAhKA03SvfCYbWodIMIQPyAtWyDP4FnPqpAiAaIS60EjhRDhSanoFsoIy8fKqqKeW5ps/tLXSGpayi2g== \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/internal/testdata/rekor.pub b/vendor/github.com/containers/image/v5/signature/internal/testdata/rekor.pub new file mode 100644 index 00000000000..050ef601497 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/internal/testdata/rekor.pub @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwr +kBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw== +-----END PUBLIC KEY----- diff --git a/vendor/github.com/containers/image/v5/signature/internal/testdata/valid.signature b/vendor/github.com/containers/image/v5/signature/internal/testdata/valid.signature new file mode 120000 index 00000000000..b720f8c4d0b --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/internal/testdata/valid.signature @@ -0,0 +1 @@ +../../fixtures/dir-img-cosign-valid/signature-1 \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/mechanism_gpgme_test.go b/vendor/github.com/containers/image/v5/signature/mechanism_gpgme_test.go new file mode 100644 index 00000000000..82ca9989373 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/mechanism_gpgme_test.go @@ -0,0 +1,49 @@ +//go:build !containers_image_openpgp +// +build !containers_image_openpgp + +package signature + +import ( + "os" + "testing" + + "github.com/containers/image/v5/internal/testing/gpgagent" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Ensure we don’t leave around GPG agent processes. +func TestMain(m *testing.M) { + code := m.Run() + if err := gpgagent.KillGPGAgent(testGPGHomeDirectory); err != nil { + logrus.Warnf("Error killing GPG agent: %v", err) + } + os.Exit(code) +} + +func TestGPGMESigningMechanismClose(t *testing.T) { + // Closing an ephemeral mechanism removes the directory. + // (The non-ephemeral case is tested in the common TestGPGSigningMechanismClose) + mech, _, err := NewEphemeralGPGSigningMechanism([]byte{}) + require.NoError(t, err) + gpgMech, ok := mech.(*gpgmeSigningMechanism) + require.True(t, ok) + dir := gpgMech.ephemeralDir + assert.NotEmpty(t, dir) + _, err = os.Lstat(dir) + require.NoError(t, err) + err = mech.Close() + assert.NoError(t, err) + _, err = os.Lstat(dir) + require.Error(t, err) + assert.True(t, os.IsNotExist(err)) +} + +func TestGPGMESigningMechanismSupportsSigning(t *testing.T) { + mech, _, err := NewEphemeralGPGSigningMechanism([]byte{}) + require.NoError(t, err) + defer mech.Close() + err = mech.SupportsSigning() + assert.NoError(t, err) +} diff --git a/vendor/github.com/containers/image/v5/signature/mechanism_openpgp_test.go b/vendor/github.com/containers/image/v5/signature/mechanism_openpgp_test.go new file mode 100644 index 00000000000..b57eb705b69 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/mechanism_openpgp_test.go @@ -0,0 +1,29 @@ +//go:build containers_image_openpgp +// +build containers_image_openpgp + +package signature + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestOpenpgpSigningMechanismSupportsSigning(t *testing.T) { + mech, _, err := NewEphemeralGPGSigningMechanism([]byte{}) + require.NoError(t, err) + defer mech.Close() + err = mech.SupportsSigning() + assert.Error(t, err) + assert.IsType(t, SigningNotSupportedError(""), err) +} + +func TestOpenpgpSigningMechanismSign(t *testing.T) { + mech, _, err := NewEphemeralGPGSigningMechanism([]byte{}) + require.NoError(t, err) + defer mech.Close() + _, err = mech.Sign([]byte{}, TestKeyFingerprint) + assert.Error(t, err) + assert.IsType(t, SigningNotSupportedError(""), err) +} diff --git a/vendor/github.com/containers/image/v5/signature/mechanism_test.go b/vendor/github.com/containers/image/v5/signature/mechanism_test.go new file mode 100644 index 00000000000..ef67db6b991 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/mechanism_test.go @@ -0,0 +1,330 @@ +package signature + +// These tests are expected to pass unmodified for _both_ mechanism_gpgme.go and mechanism_openpgp.go. + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + testGPGHomeDirectory = "./fixtures" +) + +// Many of the tests use two fixtures: V4 signature packets (*.signature), and V3 signature packets (*.signature-v3) + +// fixtureVariants loads V3 and V4 signature fixture variants based on the v4 fixture path, and returns a map which makes it easy to test both. +func fixtureVariants(t *testing.T, v4Path string) map[string][]byte { + v4, err := os.ReadFile(v4Path) + require.NoError(t, err) + v3Path := v4Path + "-v3" + v3, err := os.ReadFile(v3Path) + require.NoError(t, err) + return map[string][]byte{v4Path: v4, v3Path: v3} +} + +func TestSigningNotSupportedError(t *testing.T) { + // A stupid test just to keep code coverage + s := "test" + err := SigningNotSupportedError(s) + assert.Equal(t, s, err.Error()) +} + +func TestNewGPGSigningMechanism(t *testing.T) { + // A dumb test just for code coverage. We test more with newGPGSigningMechanismInDirectory(). + mech, err := NewGPGSigningMechanism() + assert.NoError(t, err) + mech.Close() +} + +func TestNewGPGSigningMechanismInDirectory(t *testing.T) { + // A dumb test just for code coverage. + mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory) + assert.NoError(t, err) + mech.Close() + // The various GPG failure cases are not obviously easy to reach. + + // Test that using the default directory (presumably in user’s home) + // cannot use TestKeyFingerprint. + signatures := fixtureVariants(t, "./fixtures/invalid-blob.signature") + mech, err = newGPGSigningMechanismInDirectory("") + require.NoError(t, err) + defer mech.Close() + for version, signature := range signatures { + _, _, err := mech.Verify(signature) + assert.Error(t, err, version) + } + + // Similarly, using a newly created empty directory makes TestKeyFingerprint + // unavailable + emptyDir := t.TempDir() + mech, err = newGPGSigningMechanismInDirectory(emptyDir) + require.NoError(t, err) + defer mech.Close() + for version, signature := range signatures { + _, _, err := mech.Verify(signature) + assert.Error(t, err, version) + } + + // If pubring.gpg is unreadable in the directory, either initializing + // the mechanism fails (with openpgp), or it succeeds (sadly, gpgme) and + // later verification fails. + unreadableDir := t.TempDir() + f, err := os.OpenFile(filepath.Join(unreadableDir, "pubring.gpg"), os.O_RDONLY|os.O_CREATE, 0000) + require.NoError(t, err) + f.Close() + mech, err = newGPGSigningMechanismInDirectory(unreadableDir) + if err == nil { + defer mech.Close() + for version, signature := range signatures { + _, _, err := mech.Verify(signature) + assert.Error(t, err, version) + } + } + + // Setting the directory parameter to testGPGHomeDirectory makes the key available. + mech, err = newGPGSigningMechanismInDirectory(testGPGHomeDirectory) + require.NoError(t, err) + defer mech.Close() + for version, signature := range signatures { + _, _, err := mech.Verify(signature) + assert.NoError(t, err, version) + } + + // If we use the default directory mechanism, GNUPGHOME is respected. + t.Setenv("GNUPGHOME", testGPGHomeDirectory) + mech, err = newGPGSigningMechanismInDirectory("") + require.NoError(t, err) + defer mech.Close() + for version, signature := range signatures { + _, _, err := mech.Verify(signature) + assert.NoError(t, err, version) + } +} + +func TestNewEphemeralGPGSigningMechanism(t *testing.T) { + // Empty input: This is accepted anyway by GPG, just returns no keys. + mech, keyIdentities, err := NewEphemeralGPGSigningMechanism([]byte{}) + require.NoError(t, err) + defer mech.Close() + assert.Empty(t, keyIdentities) + // Try validating a signature when the key is unknown. + signatures := fixtureVariants(t, "./fixtures/invalid-blob.signature") + for version, signature := range signatures { + _, _, err := mech.Verify(signature) + require.Error(t, err, version) + } + + // Successful import + keyBlob, err := os.ReadFile("./fixtures/public-key.gpg") + require.NoError(t, err) + mech, keyIdentities, err = NewEphemeralGPGSigningMechanism(keyBlob) + require.NoError(t, err) + defer mech.Close() + assert.Equal(t, []string{TestKeyFingerprint}, keyIdentities) + // After import, the signature should validate. + for version, signature := range signatures { + content, signingFingerprint, err := mech.Verify(signature) + require.NoError(t, err, version) + assert.Equal(t, []byte("This is not JSON\n"), content, version) + assert.Equal(t, TestKeyFingerprint, signingFingerprint, version) + } + + // Two keys in a keyring: Read the binary-format pubring.gpg, and concatenate it twice. + // (Using two copies of public-key.gpg, in the ASCII-armored format, works with + // gpgmeSigningMechanism but not openpgpSigningMechanism.) + keyBlob, err = os.ReadFile("./fixtures/pubring.gpg") + require.NoError(t, err) + mech, keyIdentities, err = NewEphemeralGPGSigningMechanism(bytes.Join([][]byte{keyBlob, keyBlob}, nil)) + require.NoError(t, err) + defer mech.Close() + assert.Equal(t, []string{TestKeyFingerprint, TestKeyFingerprintWithPassphrase, TestKeyFingerprint, TestKeyFingerprintWithPassphrase}, keyIdentities) + + // Two keys from two blobs: + keyBlob1, err := os.ReadFile("./fixtures/public-key-1.gpg") + require.NoError(t, err) + keyBlob2, err := os.ReadFile("./fixtures/public-key-2.gpg") + require.NoError(t, err) + mech, keyIdentities, err = newEphemeralGPGSigningMechanism([][]byte{keyBlob1, keyBlob2}) + require.NoError(t, err) + defer mech.Close() + assert.Equal(t, []string{TestKeyFingerprint, TestKeyFingerprintWithPassphrase}, keyIdentities) + + // Invalid input: This is, sadly, accepted anyway by GPG, just returns no keys. + // For openpgpSigningMechanism we can detect this and fail. + mech, keyIdentities, err = NewEphemeralGPGSigningMechanism([]byte("This is invalid")) + assert.True(t, err != nil || len(keyIdentities) == 0) + if err == nil { + mech.Close() + } + assert.Empty(t, keyIdentities) + // The various GPG/GPGME failures cases are not obviously easy to reach. +} + +func TestGPGSigningMechanismClose(t *testing.T) { + // Closing a non-ephemeral mechanism does not remove anything in the directory. + mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory) + require.NoError(t, err) + err = mech.Close() + assert.NoError(t, err) + _, err = os.Lstat(testGPGHomeDirectory) + assert.NoError(t, err) + _, err = os.Lstat(filepath.Join(testGPGHomeDirectory, "pubring.gpg")) + assert.NoError(t, err) +} + +func TestGPGSigningMechanismSign(t *testing.T) { + mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory) + require.NoError(t, err) + defer mech.Close() + + if err := mech.SupportsSigning(); err != nil { + t.Skipf("Signing not supported: %v", err) + } + + // Successful signing + content := []byte("content") + signature, err := mech.Sign(content, TestKeyFingerprint) + require.NoError(t, err) + + signedContent, signingFingerprint, err := mech.Verify(signature) + require.NoError(t, err) + assert.EqualValues(t, content, signedContent) + assert.Equal(t, TestKeyFingerprint, signingFingerprint) + + // Error signing + _, err = mech.Sign(content, "this fingerprint doesn't exist") + assert.Error(t, err) + // The various GPG/GPGME failures cases are not obviously easy to reach. +} + +func assertSigningError(t *testing.T, content []byte, fingerprint string, err error, msgAndArgs ...any) { + assert.Error(t, err, msgAndArgs...) + assert.Nil(t, content, msgAndArgs...) + assert.Empty(t, fingerprint, msgAndArgs...) +} + +func TestGPGSigningMechanismVerify(t *testing.T) { + mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory) + require.NoError(t, err) + defer mech.Close() + + // Successful verification + signatures := fixtureVariants(t, "./fixtures/invalid-blob.signature") + for variant, signature := range signatures { + content, signingFingerprint, err := mech.Verify(signature) + require.NoError(t, err, variant) + assert.Equal(t, []byte("This is not JSON\n"), content, variant) + assert.Equal(t, TestKeyFingerprint, signingFingerprint, variant) + } + + // For extra paranoia, test that we return nil data on error. + + // Completely invalid signature. + content, signingFingerprint, err := mech.Verify([]byte{}) + assertSigningError(t, content, signingFingerprint, err) + + content, signingFingerprint, err = mech.Verify([]byte("invalid signature")) + assertSigningError(t, content, signingFingerprint, err) + + // Literal packet, not a signature + signature, err := os.ReadFile("./fixtures/unsigned-literal.signature") // Not fixtureVariants, the “literal data” packet does not have V3/V4 versions. + require.NoError(t, err) + content, signingFingerprint, err = mech.Verify(signature) + assertSigningError(t, content, signingFingerprint, err) + + // Encrypted data, not a signature. + signature, err = os.ReadFile("./fixtures/unsigned-encrypted.signature") // Not fixtureVariants, the “public-key encrypted session key” does not have V3/V4 versions. + require.NoError(t, err) + content, signingFingerprint, err = mech.Verify(signature) + assertSigningError(t, content, signingFingerprint, err) + + // FIXME? Is there a way to create a multi-signature so that gpgme_op_verify returns multiple signatures? + + // Expired signature + signature, err = os.ReadFile("./fixtures/expired.signature") // Not fixtureVariants, V3 signature packets don’t support expiration. + require.NoError(t, err) + content, signingFingerprint, err = mech.Verify(signature) + assertSigningError(t, content, signingFingerprint, err) + + // Corrupt signature + signatures = fixtureVariants(t, "./fixtures/corrupt.signature") + for version, signature := range signatures { + content, signingFingerprint, err := mech.Verify(signature) + assertSigningError(t, content, signingFingerprint, err, version) + } + + // Valid signature with an unknown key + signatures = fixtureVariants(t, "./fixtures/unknown-key.signature") + for version, signature := range signatures { + content, signingFingerprint, err := mech.Verify(signature) + assertSigningError(t, content, signingFingerprint, err, version) + } + + // The various GPG/GPGME failures cases are not obviously easy to reach. +} + +func TestGPGSigningMechanismUntrustedSignatureContents(t *testing.T) { + mech, _, err := NewEphemeralGPGSigningMechanism([]byte{}) + require.NoError(t, err) + defer mech.Close() + + // A valid signature + signatures := fixtureVariants(t, "./fixtures/invalid-blob.signature") + for version, signature := range signatures { + content, shortKeyID, err := mech.UntrustedSignatureContents(signature) + require.NoError(t, err, version) + assert.Equal(t, []byte("This is not JSON\n"), content, version) + assert.Equal(t, TestKeyShortID, shortKeyID, version) + } + + // Completely invalid signature. + _, _, err = mech.UntrustedSignatureContents([]byte{}) + assert.Error(t, err) + + _, _, err = mech.UntrustedSignatureContents([]byte("invalid signature")) + assert.Error(t, err) + + // Literal packet, not a signature + signature, err := os.ReadFile("./fixtures/unsigned-literal.signature") // Not fixtureVariants, the “literal data” packet does not have V3/V4 versions. + require.NoError(t, err) + _, _, err = mech.UntrustedSignatureContents(signature) + assert.Error(t, err) + + // Encrypted data, not a signature. + signature, err = os.ReadFile("./fixtures/unsigned-encrypted.signature") // Not fixtureVariants, the “public-key encrypted session key” does not have V3/V4 versions. + require.NoError(t, err) + _, _, err = mech.UntrustedSignatureContents(signature) + assert.Error(t, err) + + // Expired signature + signature, err = os.ReadFile("./fixtures/expired.signature") // Not fixtureVariants, V3 signature packets don’t support expiration. + require.NoError(t, err) + content, shortKeyID, err := mech.UntrustedSignatureContents(signature) + require.NoError(t, err) + assert.Equal(t, []byte("This signature is expired.\n"), content) + assert.Equal(t, TestKeyShortID, shortKeyID) + + // Corrupt signature + signatures = fixtureVariants(t, "./fixtures/corrupt.signature") + for version, signature := range signatures { + content, shortKeyID, err := mech.UntrustedSignatureContents(signature) + require.NoError(t, err, version) + assert.Equal(t, []byte(`{"critical":{"identity":{"docker-reference":"testing/manifest"},"image":{"docker-manifest-digest":"sha256:20bf21ed457b390829cdbeec8795a7bea1626991fda603e0d01b4e7f60427e55"},"type":"atomic container signature"},"optional":{"creator":"atomic ","timestamp":1458239713}}`), content, version) + assert.Equal(t, TestKeyShortID, shortKeyID, version) + } + + // Valid signature with an unknown key + signatures = fixtureVariants(t, "./fixtures/unknown-key.signature") + for version, signature := range signatures { + content, shortKeyID, err := mech.UntrustedSignatureContents(signature) + require.NoError(t, err, version) + assert.Equal(t, []byte(`{"critical":{"identity":{"docker-reference":"testing/manifest"},"image":{"docker-manifest-digest":"sha256:20bf21ed457b390829cdbeec8795a7bea1626991fda603e0d01b4e7f60427e55"},"type":"atomic container signature"},"optional":{"creator":"atomic 0.1.13-dev","timestamp":1464633474}}`), content, version) + assert.Equal(t, "BB75E91990DF8F7E", shortKeyID, version) + } +} diff --git a/vendor/github.com/containers/image/v5/signature/policy_config_sigstore_test.go b/vendor/github.com/containers/image/v5/signature/policy_config_sigstore_test.go new file mode 100644 index 00000000000..d71ae5f895c --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/policy_config_sigstore_test.go @@ -0,0 +1,502 @@ +package signature + +import ( + "encoding/json" + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// xNewPRSigstoreSigned is like NewPRSigstoreSigned, except it must not fail. +func xNewPRSigstoreSigned(options ...PRSigstoreSignedOption) PolicyRequirement { + pr, err := NewPRSigstoreSigned(options...) + if err != nil { + panic("xNewPRSigstoreSigned failed") + } + return pr +} + +func TestNewPRSigstoreSigned(t *testing.T) { + const testKeyPath = "/foo/bar" + testKeyData := []byte("abc") + testFulcio, err := NewPRSigstoreSignedFulcio( + PRSigstoreSignedFulcioWithCAPath("fixtures/fulcio_v1.crt.pem"), + PRSigstoreSignedFulcioWithOIDCIssuer("https://github.com/login/oauth"), + PRSigstoreSignedFulcioWithSubjectEmail("mitr@redhat.com"), + ) + require.NoError(t, err) + const testRekorKeyPath = "/foo/baz" + testRekorKeyData := []byte("def") + testIdentity := NewPRMMatchRepoDigestOrExact() + + // Success: combinatoric combinations of key source and Rekor uses + for _, c := range []struct { + options []PRSigstoreSignedOption + requiresRekor bool + expected prSigstoreSigned + }{ + { + options: []PRSigstoreSignedOption{ + PRSigstoreSignedWithKeyPath(testKeyPath), + PRSigstoreSignedWithSignedIdentity(testIdentity), + }, + expected: prSigstoreSigned{ + prCommon: prCommon{prTypeSigstoreSigned}, + KeyPath: testKeyPath, + KeyData: nil, + Fulcio: nil, + SignedIdentity: testIdentity, + }, + }, + { + options: []PRSigstoreSignedOption{ + PRSigstoreSignedWithKeyData(testKeyData), + PRSigstoreSignedWithSignedIdentity(testIdentity), + }, + expected: prSigstoreSigned{ + prCommon: prCommon{prTypeSigstoreSigned}, + KeyPath: "", + KeyData: testKeyData, + Fulcio: nil, + SignedIdentity: testIdentity, + }, + }, + { + options: []PRSigstoreSignedOption{ + PRSigstoreSignedWithFulcio(testFulcio), + PRSigstoreSignedWithSignedIdentity(testIdentity), + }, + requiresRekor: true, + expected: prSigstoreSigned{ + prCommon: prCommon{prTypeSigstoreSigned}, + KeyPath: "", + KeyData: nil, + Fulcio: testFulcio, + SignedIdentity: testIdentity, + }, + }, + } { + for _, c2 := range []struct { + rekorOptions []PRSigstoreSignedOption + rekorExpected prSigstoreSigned + }{ + { // No Rekor + rekorOptions: []PRSigstoreSignedOption{}, + rekorExpected: prSigstoreSigned{}, + }, + { + rekorOptions: []PRSigstoreSignedOption{ + PRSigstoreSignedWithRekorPublicKeyPath(testRekorKeyPath), + }, + rekorExpected: prSigstoreSigned{ + RekorPublicKeyPath: testRekorKeyPath, + }, + }, + { + rekorOptions: []PRSigstoreSignedOption{ + PRSigstoreSignedWithRekorPublicKeyData(testRekorKeyData), + }, + rekorExpected: prSigstoreSigned{ + RekorPublicKeyData: testRekorKeyData, + }, + }, + } { + // Special-case this rejected combination: + if c.requiresRekor && len(c2.rekorOptions) == 0 { + continue + } + pr, err := newPRSigstoreSigned(append(c.options, c2.rekorOptions...)...) + require.NoError(t, err) + expected := c.expected // A shallow copy + expected.RekorPublicKeyPath = c2.rekorExpected.RekorPublicKeyPath + expected.RekorPublicKeyData = c2.rekorExpected.RekorPublicKeyData + assert.Equal(t, &expected, pr) + } + } + + testFulcio2, err := NewPRSigstoreSignedFulcio( + PRSigstoreSignedFulcioWithCAPath("fixtures/fulcio_v1.crt.pem"), + PRSigstoreSignedFulcioWithOIDCIssuer("https://github.com/login/oauth"), + PRSigstoreSignedFulcioWithSubjectEmail("test-user@example.com"), + ) + require.NoError(t, err) + for _, c := range [][]PRSigstoreSignedOption{ + {}, // None of keyPath nor keyData, fulcio specified + { // Both keyPath and keyData specified + PRSigstoreSignedWithKeyPath(testKeyPath), + PRSigstoreSignedWithKeyData(testKeyData), + PRSigstoreSignedWithSignedIdentity(testIdentity), + }, + { // both keyPath and fulcio specified + PRSigstoreSignedWithKeyPath(testKeyPath), + PRSigstoreSignedWithFulcio(testFulcio), + PRSigstoreSignedWithRekorPublicKeyPath(testRekorKeyPath), + PRSigstoreSignedWithSignedIdentity(testIdentity), + }, + { // both keyData and fulcio specified + PRSigstoreSignedWithKeyData(testKeyData), + PRSigstoreSignedWithFulcio(testFulcio), + PRSigstoreSignedWithRekorPublicKeyPath(testRekorKeyPath), + PRSigstoreSignedWithSignedIdentity(testIdentity), + }, + { // Duplicate keyPath + PRSigstoreSignedWithKeyPath(testKeyPath), + PRSigstoreSignedWithKeyPath(testKeyPath + "1"), + PRSigstoreSignedWithSignedIdentity(testIdentity), + }, + { // Duplicate keyData + PRSigstoreSignedWithKeyData(testKeyData), + PRSigstoreSignedWithKeyData([]byte("def")), + PRSigstoreSignedWithSignedIdentity(testIdentity), + }, + { // Duplicate fulcio + PRSigstoreSignedWithFulcio(testFulcio), + PRSigstoreSignedWithFulcio(testFulcio2), + PRSigstoreSignedWithRekorPublicKeyPath(testRekorKeyPath), + PRSigstoreSignedWithSignedIdentity(testIdentity), + }, + { // fulcio without Rekor + PRSigstoreSignedWithFulcio(testFulcio), + PRSigstoreSignedWithSignedIdentity(testIdentity), + }, + { // Both rekorKeyPath and rekorKeyData specified + PRSigstoreSignedWithKeyPath(testKeyPath), + PRSigstoreSignedWithRekorPublicKeyPath(testRekorKeyPath), + PRSigstoreSignedWithRekorPublicKeyData(testRekorKeyData), + PRSigstoreSignedWithSignedIdentity(testIdentity), + }, + { // Duplicate rekorKeyPath + PRSigstoreSignedWithKeyPath(testKeyPath), + PRSigstoreSignedWithRekorPublicKeyPath(testRekorKeyPath), + PRSigstoreSignedWithRekorPublicKeyPath(testRekorKeyPath + "1"), + PRSigstoreSignedWithSignedIdentity(testIdentity), + }, + { // Duplicate keyData + PRSigstoreSignedWithKeyPath(testKeyPath), + PRSigstoreSignedWithRekorPublicKeyData(testRekorKeyData), + PRSigstoreSignedWithRekorPublicKeyData([]byte("def")), + PRSigstoreSignedWithSignedIdentity(testIdentity), + }, + { // Missing signedIdentity + PRSigstoreSignedWithKeyPath(testKeyPath), + }, + { // Duplicate signedIdentity} + PRSigstoreSignedWithKeyPath(testKeyPath), + PRSigstoreSignedWithSignedIdentity(testIdentity), + PRSigstoreSignedWithSignedIdentity(newPRMMatchRepository()), + }, + } { + _, err = newPRSigstoreSigned(c...) + assert.Error(t, err) + } +} + +func TestNewPRSigstoreSignedKeyPath(t *testing.T) { + const testPath = "/foo/bar" + signedIdentity := NewPRMMatchRepoDigestOrExact() + _pr, err := NewPRSigstoreSignedKeyPath(testPath, signedIdentity) + require.NoError(t, err) + pr, ok := _pr.(*prSigstoreSigned) + require.True(t, ok) + assert.Equal(t, &prSigstoreSigned{ + prCommon: prCommon{Type: prTypeSigstoreSigned}, + KeyPath: testPath, + SignedIdentity: NewPRMMatchRepoDigestOrExact(), + }, pr) +} + +func TestNewPRSigstoreSignedKeyData(t *testing.T) { + testData := []byte("abc") + signedIdentity := NewPRMMatchRepoDigestOrExact() + _pr, err := NewPRSigstoreSignedKeyData(testData, signedIdentity) + require.NoError(t, err) + pr, ok := _pr.(*prSigstoreSigned) + require.True(t, ok) + assert.Equal(t, &prSigstoreSigned{ + prCommon: prCommon{Type: prTypeSigstoreSigned}, + KeyData: testData, + SignedIdentity: NewPRMMatchRepoDigestOrExact(), + }, pr) +} + +// Return the result of modifying validJSON with fn and unmarshaling it into *pr +func tryUnmarshalModifiedSigstoreSigned(t *testing.T, pr *prSigstoreSigned, validJSON []byte, modifyFn func(mSA)) error { + var tmp mSA + err := json.Unmarshal(validJSON, &tmp) + require.NoError(t, err) + + modifyFn(tmp) + + *pr = prSigstoreSigned{} + return jsonUnmarshalFromObject(t, tmp, &pr) +} + +func TestPRSigstoreSignedUnmarshalJSON(t *testing.T) { + keyDataTests := policyJSONUmarshallerTests[PolicyRequirement]{ + newDest: func() json.Unmarshaler { return &prSigstoreSigned{} }, + newValidObject: func() (PolicyRequirement, error) { + return NewPRSigstoreSignedKeyData([]byte("abc"), NewPRMMatchRepoDigestOrExact()) + }, + otherJSONParser: newPolicyRequirementFromJSON, + breakFns: []func(mSA){ + // The "type" field is missing + func(v mSA) { delete(v, "type") }, + // Wrong "type" field + func(v mSA) { v["type"] = 1 }, + func(v mSA) { v["type"] = "this is invalid" }, + // Extra top-level sub-object + func(v mSA) { v["unexpected"] = 1 }, + // All of "keyPath" and "keyData", and "fulcio" is missing + func(v mSA) { delete(v, "keyData") }, + // Both "keyPath" and "keyData" is present + func(v mSA) { v["keyPath"] = "/foo/bar" }, + // Both "keyData" and "fulcio" is present + func(v mSA) { + v["fulcio"] = mSA{ + "caPath": "/foo/baz", + "oidcIssuer": "https://example.com", + "subjectEmail": "test@example.com", + } + }, + // Invalid "keyPath" field + func(v mSA) { delete(v, "keyData"); v["keyPath"] = 1 }, + // Invalid "keyData" field + func(v mSA) { v["keyData"] = 1 }, + func(v mSA) { v["keyData"] = "this is invalid base64" }, + // Invalid "fulcio" field + func(v mSA) { v["fulcio"] = 1 }, + func(v mSA) { v["fulcio"] = mSA{} }, + // "fulcio" is explicit nil + func(v mSA) { v["fulcio"] = nil }, + // Both "rekorKeyPath" and "rekorKeyData" is present + func(v mSA) { + v["rekorPublicKeyPath"] = "/foo/baz" + v["rekorPublicKeyData"] = "" + }, + // Invalid "rekorPublicKeyPath" field + func(v mSA) { v["rekorPublicKeyPath"] = 1 }, + // Invalid "rekorPublicKeyData" field + func(v mSA) { v["rekorPublicKeyData"] = 1 }, + func(v mSA) { v["rekorPublicKeyData"] = "this is invalid base64" }, + // Invalid "signedIdentity" field + func(v mSA) { v["signedIdentity"] = "this is invalid" }, + // "signedIdentity" an explicit nil + func(v mSA) { v["signedIdentity"] = nil }, + }, + duplicateFields: []string{"type", "keyData", "signedIdentity"}, + } + keyDataTests.run(t) + // Test keyPath-specific duplicate fields + policyJSONUmarshallerTests[PolicyRequirement]{ + newDest: func() json.Unmarshaler { return &prSigstoreSigned{} }, + newValidObject: func() (PolicyRequirement, error) { + return NewPRSigstoreSignedKeyPath("/foo/bar", NewPRMMatchRepoDigestOrExact()) + }, + otherJSONParser: newPolicyRequirementFromJSON, + duplicateFields: []string{"type", "keyPath", "signedIdentity"}, + }.run(t) + // Test Fulcio and rekorPublicKeyPath duplicate fields + testFulcio, err := NewPRSigstoreSignedFulcio( + PRSigstoreSignedFulcioWithCAPath("fixtures/fulcio_v1.crt.pem"), + PRSigstoreSignedFulcioWithOIDCIssuer("https://github.com/login/oauth"), + PRSigstoreSignedFulcioWithSubjectEmail("mitr@redhat.com"), + ) + require.NoError(t, err) + policyJSONUmarshallerTests[PolicyRequirement]{ + newDest: func() json.Unmarshaler { return &prSigstoreSigned{} }, + newValidObject: func() (PolicyRequirement, error) { + return NewPRSigstoreSigned( + PRSigstoreSignedWithFulcio(testFulcio), + PRSigstoreSignedWithRekorPublicKeyPath("/foo/rekor"), + PRSigstoreSignedWithSignedIdentity(NewPRMMatchRepoDigestOrExact()), + ) + }, + otherJSONParser: newPolicyRequirementFromJSON, + duplicateFields: []string{"type", "fulcio", "rekorPublicKeyPath", "signedIdentity"}, + }.run(t) + // Test rekorPublicKeyData duplicate fields + policyJSONUmarshallerTests[PolicyRequirement]{ + newDest: func() json.Unmarshaler { return &prSigstoreSigned{} }, + newValidObject: func() (PolicyRequirement, error) { + return NewPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("/foo/bar"), + PRSigstoreSignedWithRekorPublicKeyData([]byte("foo")), + PRSigstoreSignedWithSignedIdentity(NewPRMMatchRepoDigestOrExact()), + ) + }, + otherJSONParser: newPolicyRequirementFromJSON, + duplicateFields: []string{"type", "keyPath", "rekorPublicKeyData", "signedIdentity"}, + }.run(t) + + var pr prSigstoreSigned + + // Start with a valid JSON. + _, validJSON := keyDataTests.validObjectAndJSON(t) + + // Various allowed modifications to the requirement + allowedModificationFns := []func(mSA){ + // Delete the signedIdentity field + func(v mSA) { delete(v, "signedIdentity") }, + } + for _, fn := range allowedModificationFns { + err := tryUnmarshalModifiedSigstoreSigned(t, &pr, validJSON, fn) + require.NoError(t, err) + } + + // Various ways to set signedIdentity to the default value + signedIdentityDefaultFns := []func(mSA){ + // Set signedIdentity to the default explicitly + func(v mSA) { v["signedIdentity"] = NewPRMMatchRepoDigestOrExact() }, + // Delete the signedIdentity field + func(v mSA) { delete(v, "signedIdentity") }, + } + for _, fn := range signedIdentityDefaultFns { + err := tryUnmarshalModifiedSigstoreSigned(t, &pr, validJSON, fn) + require.NoError(t, err) + assert.Equal(t, NewPRMMatchRepoDigestOrExact(), pr.SignedIdentity) + } +} + +func TestNewPRSigstoreSignedFulcio(t *testing.T) { + const testCAPath = "/foo/bar" + testCAData := []byte("abc") + const testOIDCIssuer = "https://example.com" + const testSubjectEmail = "test@example.com" + + // Success: + for _, c := range []struct { + options []PRSigstoreSignedFulcioOption + expected prSigstoreSignedFulcio + }{ + { + options: []PRSigstoreSignedFulcioOption{ + PRSigstoreSignedFulcioWithCAPath(testCAPath), + PRSigstoreSignedFulcioWithOIDCIssuer(testOIDCIssuer), + PRSigstoreSignedFulcioWithSubjectEmail(testSubjectEmail), + }, + expected: prSigstoreSignedFulcio{ + CAPath: testCAPath, + OIDCIssuer: testOIDCIssuer, + SubjectEmail: testSubjectEmail, + }, + }, + { + options: []PRSigstoreSignedFulcioOption{ + PRSigstoreSignedFulcioWithCAData(testCAData), + PRSigstoreSignedFulcioWithOIDCIssuer(testOIDCIssuer), + PRSigstoreSignedFulcioWithSubjectEmail(testSubjectEmail), + }, + expected: prSigstoreSignedFulcio{ + CAData: testCAData, + OIDCIssuer: testOIDCIssuer, + SubjectEmail: testSubjectEmail, + }, + }, + } { + pr, err := newPRSigstoreSignedFulcio(c.options...) + require.NoError(t, err) + assert.Equal(t, &c.expected, pr) + } + + for _, c := range [][]PRSigstoreSignedFulcioOption{ + { // Neither caPath nor caData specified + PRSigstoreSignedFulcioWithOIDCIssuer(testOIDCIssuer), + PRSigstoreSignedFulcioWithSubjectEmail(testSubjectEmail), + }, + { // Both caPath and caData specified + PRSigstoreSignedFulcioWithCAPath(testCAPath), + PRSigstoreSignedFulcioWithCAData(testCAData), + PRSigstoreSignedFulcioWithOIDCIssuer(testOIDCIssuer), + PRSigstoreSignedFulcioWithSubjectEmail(testSubjectEmail), + }, + { // Duplicate caPath + PRSigstoreSignedFulcioWithCAPath(testCAPath), + PRSigstoreSignedFulcioWithCAPath(testCAPath + "1"), + PRSigstoreSignedFulcioWithOIDCIssuer(testOIDCIssuer), + PRSigstoreSignedFulcioWithSubjectEmail(testSubjectEmail), + }, + { // Duplicate caData + PRSigstoreSignedFulcioWithCAData(testCAData), + PRSigstoreSignedFulcioWithCAData([]byte("def")), + PRSigstoreSignedFulcioWithOIDCIssuer(testOIDCIssuer), + PRSigstoreSignedFulcioWithSubjectEmail(testSubjectEmail), + }, + { // Missing oidcIssuer + PRSigstoreSignedFulcioWithCAPath(testCAPath), + PRSigstoreSignedFulcioWithSubjectEmail(testSubjectEmail), + }, + { // Duplicate oidcIssuer + PRSigstoreSignedFulcioWithCAPath(testCAPath), + PRSigstoreSignedFulcioWithOIDCIssuer(testOIDCIssuer), + PRSigstoreSignedFulcioWithOIDCIssuer(testOIDCIssuer + "1"), + PRSigstoreSignedFulcioWithSubjectEmail(testSubjectEmail), + }, + { // Missing subjectEmail + PRSigstoreSignedFulcioWithCAPath(testCAPath), + PRSigstoreSignedFulcioWithOIDCIssuer(testOIDCIssuer), + }, + { // Duplicate subjectEmail + PRSigstoreSignedFulcioWithCAPath(testCAPath), + PRSigstoreSignedFulcioWithOIDCIssuer(testOIDCIssuer), + PRSigstoreSignedFulcioWithSubjectEmail(testSubjectEmail), + PRSigstoreSignedFulcioWithSubjectEmail("1" + testSubjectEmail), + }, + } { + _, err := newPRSigstoreSignedFulcio(c...) + logrus.Errorf("%#v", err) + assert.Error(t, err) + } +} + +func TestPRSigstoreSignedFulcioUnmarshalJSON(t *testing.T) { + policyJSONUmarshallerTests[PRSigstoreSignedFulcio]{ + newDest: func() json.Unmarshaler { return &prSigstoreSignedFulcio{} }, + newValidObject: func() (PRSigstoreSignedFulcio, error) { + return NewPRSigstoreSignedFulcio( + PRSigstoreSignedFulcioWithCAPath("fixtures/fulcio_v1.crt.pem"), + PRSigstoreSignedFulcioWithOIDCIssuer("https://github.com/login/oauth"), + PRSigstoreSignedFulcioWithSubjectEmail("mitr@redhat.com"), + ) + }, + otherJSONParser: nil, + breakFns: []func(mSA){ + // Extra top-level sub-object + func(v mSA) { v["unexpected"] = 1 }, + // Both of "caPath" and "caData" are missing + func(v mSA) { delete(v, "caPath") }, + // Both "caPath" and "caData" is present + func(v mSA) { v["caData"] = "" }, + // Invalid "caPath" field + func(v mSA) { v["caPath"] = 1 }, + // Invalid "oidcIssuer" field + func(v mSA) { v["oidcIssuer"] = 1 }, + // "oidcIssuer" is missing + func(v mSA) { delete(v, "oidcIssuer") }, + // Invalid "subjectEmail" field + func(v mSA) { v["subjectEmail"] = 1 }, + // "subjectEmail" is missing + func(v mSA) { delete(v, "subjectEmail") }, + }, + duplicateFields: []string{"caPath", "oidcIssuer", "subjectEmail"}, + }.run(t) + // Test caData specifics + policyJSONUmarshallerTests[PRSigstoreSignedFulcio]{ + newDest: func() json.Unmarshaler { return &prSigstoreSignedFulcio{} }, + newValidObject: func() (PRSigstoreSignedFulcio, error) { + return NewPRSigstoreSignedFulcio( + PRSigstoreSignedFulcioWithCAData([]byte("abc")), + PRSigstoreSignedFulcioWithOIDCIssuer("https://github.com/login/oauth"), + PRSigstoreSignedFulcioWithSubjectEmail("mitr@redhat.com"), + ) + }, + otherJSONParser: nil, + breakFns: []func(mSA){ + // Invalid "caData" field + func(v mSA) { v["caData"] = 1 }, + func(v mSA) { v["caData"] = "this is invalid base64" }, + }, + duplicateFields: []string{"caData", "oidcIssuer", "subjectEmail"}, + }.run(t) +} diff --git a/vendor/github.com/containers/image/v5/signature/policy_config_test.go b/vendor/github.com/containers/image/v5/signature/policy_config_test.go new file mode 100644 index 00000000000..1af6dc4604a --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/policy_config_test.go @@ -0,0 +1,1341 @@ +package signature + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/containers/image/v5/directory" + "github.com/containers/image/v5/docker" + "golang.org/x/exp/maps" + + // this import is needed where we use the "atomic" transport in TestPolicyUnmarshalJSON + _ "github.com/containers/image/v5/openshift" + "github.com/containers/image/v5/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type mSA map[string]any // To minimize typing the long name + +// A short-hand way to get a JSON object field value or panic. No error handling done, we know +// what we are working with, a panic in a test is good enough, and fitting test cases on a single line +// is a priority. +func x(m mSA, fields ...string) mSA { + for _, field := range fields { + // Not .(mSA) because type assertion of an unnamed type to a named type always fails (the types + // are not "identical"), but the assignment is fine because they are "assignable". + m = m[field].(map[string]any) + } + return m +} + +// policyFixtureContents is a data structure equal to the contents of "fixtures/policy.json" +var policyFixtureContents = &Policy{ + Default: PolicyRequirements{NewPRReject()}, + Transports: map[string]PolicyTransportScopes{ + "dir": { + "": PolicyRequirements{NewPRInsecureAcceptAnything()}, + }, + "docker": { + "example.com/playground": { + NewPRInsecureAcceptAnything(), + }, + "example.com/production": { + xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, + "/keys/employee-gpg-keyring", + NewPRMMatchRepoDigestOrExact()), + }, + "example.com/hardened": { + xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, + "/keys/employee-gpg-keyring", + NewPRMMatchRepository()), + xNewPRSignedByKeyPath(SBKeyTypeSignedByGPGKeys, + "/keys/public-key-signing-gpg-keyring", + NewPRMMatchExact()), + xNewPRSignedBaseLayer(xNewPRMExactRepository("registry.access.redhat.com/rhel7/rhel")), + }, + "example.com/hardened-x509": { + xNewPRSignedByKeyPath(SBKeyTypeX509Certificates, + "/keys/employee-cert-file", + NewPRMMatchRepository()), + xNewPRSignedByKeyPath(SBKeyTypeSignedByX509CAs, + "/keys/public-key-signing-ca-file", + NewPRMMatchRepoDigestOrExact()), + }, + "registry.access.redhat.com": { + xNewPRSignedByKeyPath(SBKeyTypeSignedByGPGKeys, + "/keys/RH-key-signing-key-gpg-keyring", + NewPRMMatchRepoDigestOrExact()), + }, + "registry.redhat.io/beta": { + xNewPRSignedByKeyPaths(SBKeyTypeGPGKeys, + []string{"/keys/RH-production-signing-key-gpg-keyring", "/keys/RH-beta-signing-key-gpg-keyring"}, + newPRMMatchRepoDigestOrExact()), + }, + "private-mirror:5000/vendor-mirror": { + xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, + "/keys/vendor-gpg-keyring", + xNewPRMRemapIdentity("private-mirror:5000/vendor-mirror", "vendor.example.com")), + }, + "*.access.redhat.com": { + xNewPRSignedByKeyPath(SBKeyTypeSignedByGPGKeys, + "/keys/RH-key-signing-key-gpg-keyring", + NewPRMMatchRepoDigestOrExact()), + }, + "*.redhat.com": { + xNewPRSignedByKeyPath(SBKeyTypeSignedByGPGKeys, + "/keys/RH-key-signing-key-gpg-keyring", + NewPRMMatchRepoDigestOrExact()), + }, + "*.com": { + xNewPRSignedByKeyPath(SBKeyTypeSignedByGPGKeys, + "/keys/RH-key-signing-key-gpg-keyring", + NewPRMMatchRepoDigestOrExact()), + }, + "bogus/key-data-example": { + xNewPRSignedByKeyData(SBKeyTypeSignedByGPGKeys, + []byte("nonsense"), + NewPRMMatchRepoDigestOrExact()), + }, + "bogus/signed-identity-example": { + xNewPRSignedBaseLayer(xNewPRMExactReference("registry.access.redhat.com/rhel7/rhel:latest")), + }, + "example.com/sigstore/key-data-example": { + xNewPRSigstoreSigned( + PRSigstoreSignedWithKeyData([]byte("nonsense")), + PRSigstoreSignedWithSignedIdentity(NewPRMMatchRepoDigestOrExact()), + ), + }, + "example.com/sigstore/key-path-example": { + xNewPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("/keys/public-key"), + PRSigstoreSignedWithSignedIdentity(NewPRMMatchRepository()), + ), + }, + }, + }, +} + +func TestInvalidPolicyFormatError(t *testing.T) { + // A stupid test just to keep code coverage + s := "test" + err := InvalidPolicyFormatError(s) + assert.Equal(t, s, err.Error()) +} + +func TestDefaultPolicy(t *testing.T) { + // We can't test the actual systemDefaultPolicyPath, so override. + // TestDefaultPolicyPath below tests that we handle the overrides and defaults + // correctly. + + // Success + policy, err := DefaultPolicy(&types.SystemContext{SignaturePolicyPath: "./fixtures/policy.json"}) + require.NoError(t, err) + assert.Equal(t, policyFixtureContents, policy) + + for _, path := range []string{ + "/this/does/not/exist", // Error reading file + "/dev/null", // A failure case; most are tested in the individual method unit tests. + } { + policy, err := DefaultPolicy(&types.SystemContext{SignaturePolicyPath: path}) + assert.Error(t, err) + assert.Nil(t, policy) + } +} + +func TestDefaultPolicyPath(t *testing.T) { + const nondefaultPath = "/this/is/not/the/default/path.json" + const variableReference = "$HOME" + const rootPrefix = "/root/prefix" + tempHome := t.TempDir() + userDefaultPolicyPath := filepath.Join(tempHome, userPolicyFile) + + for _, c := range []struct { + sys *types.SystemContext + userfilePresent bool + expected string + }{ + // The common case + {nil, false, systemDefaultPolicyPath}, + // There is a context, but it does not override the path. + {&types.SystemContext{}, false, systemDefaultPolicyPath}, + // Path overridden + {&types.SystemContext{SignaturePolicyPath: nondefaultPath}, false, nondefaultPath}, + // Root overridden + { + &types.SystemContext{RootForImplicitAbsolutePaths: rootPrefix}, + false, + filepath.Join(rootPrefix, systemDefaultPolicyPath), + }, + // Empty context and user policy present + {&types.SystemContext{}, true, userDefaultPolicyPath}, + // Only user policy present + {nil, true, userDefaultPolicyPath}, + // Context signature path and user policy present + { + &types.SystemContext{ + SignaturePolicyPath: nondefaultPath, + }, + true, + nondefaultPath, + }, + // Root and user policy present + { + &types.SystemContext{ + RootForImplicitAbsolutePaths: rootPrefix, + }, + true, + userDefaultPolicyPath, + }, + // Context and user policy file preset simultaneously + { + &types.SystemContext{ + RootForImplicitAbsolutePaths: rootPrefix, + SignaturePolicyPath: nondefaultPath, + }, + true, + nondefaultPath, + }, + // Root and path overrides present simultaneously, + { + &types.SystemContext{ + RootForImplicitAbsolutePaths: rootPrefix, + SignaturePolicyPath: nondefaultPath, + }, + false, + nondefaultPath, + }, + // No environment expansion happens in the overridden paths + {&types.SystemContext{SignaturePolicyPath: variableReference}, false, variableReference}, + } { + if c.userfilePresent { + err := os.MkdirAll(filepath.Dir(userDefaultPolicyPath), os.ModePerm) + require.NoError(t, err) + f, err := os.Create(userDefaultPolicyPath) + require.NoError(t, err) + f.Close() + } else { + os.Remove(userDefaultPolicyPath) + } + path := defaultPolicyPathWithHomeDir(c.sys, tempHome) + assert.Equal(t, c.expected, path) + } +} + +func TestNewPolicyFromFile(t *testing.T) { + // Success + policy, err := NewPolicyFromFile("./fixtures/policy.json") + require.NoError(t, err) + assert.Equal(t, policyFixtureContents, policy) + + // Error reading file + _, err = NewPolicyFromFile("/this/does/not/exist") + assert.Error(t, err) + + // A failure case; most are tested in the individual method unit tests. + _, err = NewPolicyFromFile("/dev/null") + require.Error(t, err) + var formatError InvalidPolicyFormatError + assert.ErrorAs(t, err, &formatError) +} + +func TestNewPolicyFromBytes(t *testing.T) { + // Success + bytes, err := os.ReadFile("./fixtures/policy.json") + require.NoError(t, err) + policy, err := NewPolicyFromBytes(bytes) + require.NoError(t, err) + assert.Equal(t, policyFixtureContents, policy) + + // A failure case; most are tested in the individual method unit tests. + _, err = NewPolicyFromBytes([]byte("")) + require.Error(t, err) + assert.IsType(t, InvalidPolicyFormatError(""), err) +} + +// FIXME? There is quite a bit of duplication below. Factor some of it out? + +// jsonUnmarshalFromObject is like json.Unmarshal(), but the input is an arbitrary object +// that is JSON-marshalled first (as a convenient way to create an invalid/unusual JSON input) +func jsonUnmarshalFromObject(t *testing.T, object any, dest any) error { + testJSON, err := json.Marshal(object) + require.NoError(t, err) + return json.Unmarshal(testJSON, dest) +} + +// assertJSONUnmarshalFromObjectFails checks that unmarshaling the JSON-marshaled version +// of an arbitrary object (as a convenient way to create an invalid/unusual JSON input) into +// dest fails. +func assertJSONUnmarshalFromObjectFails(t *testing.T, object any, dest any) { + err := jsonUnmarshalFromObject(t, object, dest) + assert.Error(t, err) +} + +// testInvalidJSONInput verifies that obviously invalid input is rejected for dest. +func testInvalidJSONInput(t *testing.T, dest json.Unmarshaler) { + // Invalid input. Note that json.Unmarshal is guaranteed to validate input before calling our + // UnmarshalJSON implementation; so test that first, then test our error handling for completeness. + err := json.Unmarshal([]byte("&"), dest) + assert.Error(t, err) + err = dest.UnmarshalJSON([]byte("&")) + assert.Error(t, err) + + // Not an object/array/string + err = json.Unmarshal([]byte("1"), dest) + assert.Error(t, err) +} + +// addExtraJSONMember adds an additional member "$name": $extra, +// possibly with a duplicate name, to encoded. +// Errors, if any, are reported through t. +func addExtraJSONMember(t *testing.T, encoded []byte, name string, extra any) []byte { + extraJSON, err := json.Marshal(extra) + require.NoError(t, err) + + require.True(t, bytes.HasSuffix(encoded, []byte("}"))) + preservedLen := len(encoded) - 1 + + res := bytes.Join([][]byte{encoded[:preservedLen], []byte(`,"`), []byte(name), []byte(`":`), extraJSON, []byte("}")}, nil) + // Verify that the result is valid JSON, as a sanity check that we are actually triggering + // the “duplicate member” case in the caller. + var raw map[string]any + err = json.Unmarshal(res, &raw) + require.NoError(t, err) + return res +} + +// policyJSONUnmarshallerTests formalizes the repeated structure of the JSON unmarshaller +// tests in this file, and allows sharing the test implementation. +type policyJSONUmarshallerTests[T any] struct { + newDest func() json.Unmarshaler // Create a new json.Unmarshaler to test against + newValidObject func() (T, error) // A function that generates a valid object, used as a base for other tests + otherJSONParser func([]byte) (T, error) // Another function that must accept the result of encoding validObject + invalidObjects []mSA // mSA values that are invalid for this unmarshaller; a simpler alternative to breakFns + breakFns []func(mSA) // Functions that edit a mSA from newValidObject() to make it invalid + duplicateFields []string // Names of fields in the return value of newValidObject() that should not be duplicated +} + +// validObjectAndJSON returns an object created by d.newValidObject() and its JSON representation. +func (d policyJSONUmarshallerTests[T]) validObjectAndJSON(t *testing.T) (T, []byte) { + validObject, err := d.newValidObject() + require.NoError(t, err) + validJSON, err := json.Marshal(validObject) + require.NoError(t, err) + return validObject, validJSON +} + +func (d policyJSONUmarshallerTests[T]) run(t *testing.T) { + dest := d.newDest() + testInvalidJSONInput(t, dest) + + validObject, validJSON := d.validObjectAndJSON(t) + + // Success + dest = d.newDest() + err := json.Unmarshal(validJSON, dest) + require.NoError(t, err) + assert.Equal(t, validObject, dest) + + // otherJSONParser recognizes this data + if d.otherJSONParser != nil { + other, err := d.otherJSONParser(validJSON) + require.NoError(t, err) + assert.Equal(t, validObject, other) + } + + // Invalid JSON objects + for _, invalid := range d.invalidObjects { + dest := d.newDest() + assertJSONUnmarshalFromObjectFails(t, invalid, dest) + } + // Various ways to corrupt the JSON + for index, fn := range d.breakFns { + t.Run(fmt.Sprintf("breakFns[%d]", index), func(t *testing.T) { + var tmp mSA + err := json.Unmarshal(validJSON, &tmp) + require.NoError(t, err) + + fn(tmp) + + dest := d.newDest() + assertJSONUnmarshalFromObjectFails(t, tmp, dest) + }) + } + + // Duplicated fields + for _, field := range d.duplicateFields { + var tmp mSA + err := json.Unmarshal(validJSON, &tmp) + require.NoError(t, err) + + testJSON := addExtraJSONMember(t, validJSON, field, tmp[field]) + + dest := d.newDest() + err = json.Unmarshal(testJSON, dest) + assert.Error(t, err) + } +} + +// xNewPRSignedByKeyPath is like NewPRSignedByKeyPath, except it must not fail. +func xNewPRSignedByKeyPath(keyType sbKeyType, keyPath string, signedIdentity PolicyReferenceMatch) PolicyRequirement { + pr, err := NewPRSignedByKeyPath(keyType, keyPath, signedIdentity) + if err != nil { + panic("xNewPRSignedByKeyPath failed") + } + return pr +} + +// xNewPRSignedByKeyPaths is like NewPRSignedByKeyPaths, except it must not fail. +func xNewPRSignedByKeyPaths(keyType sbKeyType, keyPaths []string, signedIdentity PolicyReferenceMatch) PolicyRequirement { + pr, err := NewPRSignedByKeyPaths(keyType, keyPaths, signedIdentity) + if err != nil { + panic("xNewPRSignedByKeyPaths failed") + } + return pr +} + +// xNewPRSignedByKeyData is like NewPRSignedByKeyData, except it must not fail. +func xNewPRSignedByKeyData(keyType sbKeyType, keyData []byte, signedIdentity PolicyReferenceMatch) PolicyRequirement { + pr, err := NewPRSignedByKeyData(keyType, keyData, signedIdentity) + if err != nil { + panic("xNewPRSignedByKeyData failed") + } + return pr +} + +func TestPolicyUnmarshalJSON(t *testing.T) { + tests := policyJSONUmarshallerTests[*Policy]{ + newDest: func() json.Unmarshaler { return &Policy{} }, + newValidObject: func() (*Policy, error) { + return &Policy{ + Default: []PolicyRequirement{ + xNewPRSignedByKeyData(SBKeyTypeGPGKeys, []byte("abc"), NewPRMMatchRepoDigestOrExact()), + }, + Transports: map[string]PolicyTransportScopes{ + "docker": { + "docker.io/library/busybox": []PolicyRequirement{ + xNewPRSignedByKeyData(SBKeyTypeGPGKeys, []byte("def"), NewPRMMatchRepoDigestOrExact()), + }, + "registry.access.redhat.com": []PolicyRequirement{ + xNewPRSignedByKeyData(SBKeyTypeSignedByGPGKeys, []byte("RH"), NewPRMMatchRepository()), + }, + }, + "atomic": { + "registry.access.redhat.com/rhel7": []PolicyRequirement{ + xNewPRSignedByKeyData(SBKeyTypeSignedByGPGKeys, []byte("RHatomic"), NewPRMMatchRepository()), + }, + }, + "unknown": { + "registry.access.redhat.com/rhel7": []PolicyRequirement{ + xNewPRSignedByKeyData(SBKeyTypeSignedByGPGKeys, []byte("RHatomic"), NewPRMMatchRepository()), + }, + }, + }, + }, nil + }, + otherJSONParser: nil, + breakFns: []func(mSA){ + // The "default" field is missing + func(v mSA) { delete(v, "default") }, + // Extra top-level sub-object + func(v mSA) { v["unexpected"] = 1 }, + // "default" not an array + func(v mSA) { v["default"] = 1 }, + func(v mSA) { v["default"] = mSA{} }, + // "transports" not an object + func(v mSA) { v["transports"] = 1 }, + func(v mSA) { v["transports"] = []string{} }, + // "default" is an invalid PolicyRequirements + func(v mSA) { v["default"] = PolicyRequirements{} }, + }, + duplicateFields: []string{"default", "transports"}, + } + tests.run(t) + + // Various allowed modifications to the policy + _, validJSON := tests.validObjectAndJSON(t) + allowedModificationFns := []func(mSA){ + // Delete the map of transport-specific scopes + func(v mSA) { delete(v, "transports") }, + // Use an empty map of transport-specific scopes + func(v mSA) { v["transports"] = map[string]PolicyTransportScopes{} }, + } + for _, fn := range allowedModificationFns { + var tmp mSA + err := json.Unmarshal(validJSON, &tmp) + require.NoError(t, err) + + fn(tmp) + + p := Policy{} + err = jsonUnmarshalFromObject(t, tmp, &p) + assert.NoError(t, err) + } +} + +func TestPolicyTransportScopesUnmarshalJSON(t *testing.T) { + // Start with a valid JSON. + validPTS := PolicyTransportScopes{ + "": []PolicyRequirement{ + xNewPRSignedByKeyData(SBKeyTypeGPGKeys, []byte("global"), NewPRMMatchRepoDigestOrExact()), + }, + } + + // Nothing can be unmarshaled directly into PolicyTransportScopes + pts := PolicyTransportScopes{} + assertJSONUnmarshalFromObjectFails(t, validPTS, &pts) +} + +// Return the result of modifying validJSON with fn and unmarshaling it into *pts +// using transport. +func tryUnmarshalModifiedPTS(t *testing.T, pts *PolicyTransportScopes, transport types.ImageTransport, + validJSON []byte, modifyFn func(mSA)) error { + var tmp mSA + err := json.Unmarshal(validJSON, &tmp) + require.NoError(t, err) + + modifyFn(tmp) + + *pts = PolicyTransportScopes{} + dest := policyTransportScopesWithTransport{ + transport: transport, + dest: pts, + } + return jsonUnmarshalFromObject(t, tmp, &dest) +} + +func TestPolicyTransportScopesWithTransportUnmarshalJSON(t *testing.T) { + var pts PolicyTransportScopes + + dest := policyTransportScopesWithTransport{ + transport: docker.Transport, + dest: &pts, + } + testInvalidJSONInput(t, &dest) + + // Start with a valid JSON. + validPTS := PolicyTransportScopes{ + "docker.io/library/busybox": []PolicyRequirement{ + xNewPRSignedByKeyData(SBKeyTypeGPGKeys, []byte("def"), NewPRMMatchRepoDigestOrExact()), + }, + "registry.access.redhat.com": []PolicyRequirement{ + xNewPRSignedByKeyData(SBKeyTypeSignedByGPGKeys, []byte("RH"), NewPRMMatchRepository()), + }, + "": []PolicyRequirement{ + xNewPRSignedByKeyData(SBKeyTypeGPGKeys, []byte("global"), NewPRMMatchRepoDigestOrExact()), + }, + } + validJSON, err := json.Marshal(validPTS) + require.NoError(t, err) + + // Success + pts = PolicyTransportScopes{} + dest = policyTransportScopesWithTransport{ + transport: docker.Transport, + dest: &pts, + } + err = json.Unmarshal(validJSON, &dest) + require.NoError(t, err) + assert.Equal(t, validPTS, pts) + + // Various ways to corrupt the JSON + breakFns := []func(mSA){ + // A scope is not an array + func(v mSA) { v["docker.io/library/busybox"] = 1 }, + func(v mSA) { v["docker.io/library/busybox"] = mSA{} }, + func(v mSA) { v[""] = 1 }, + func(v mSA) { v[""] = mSA{} }, + // A scope is an invalid PolicyRequirements + func(v mSA) { v["docker.io/library/busybox"] = PolicyRequirements{} }, + func(v mSA) { v[""] = PolicyRequirements{} }, + } + for _, fn := range breakFns { + err = tryUnmarshalModifiedPTS(t, &pts, docker.Transport, validJSON, fn) + assert.Error(t, err) + } + + // Duplicated fields + for _, field := range []string{"docker.io/library/busybox", ""} { + var tmp mSA + err := json.Unmarshal(validJSON, &tmp) + require.NoError(t, err) + + testJSON := addExtraJSONMember(t, validJSON, field, tmp[field]) + + pts = PolicyTransportScopes{} + dest := policyTransportScopesWithTransport{ + transport: docker.Transport, + dest: &pts, + } + err = json.Unmarshal(testJSON, &dest) + assert.Error(t, err) + } + + // Scope rejected by transport the Docker scopes we use as valid are rejected by directory.Transport + // as relative paths. + err = tryUnmarshalModifiedPTS(t, &pts, directory.Transport, validJSON, + func(v mSA) {}) + assert.Error(t, err) + + // Various allowed modifications to the policy + allowedModificationFns := []func(mSA){ + // The "" scope is missing + func(v mSA) { delete(v, "") }, + // The policy is completely empty + func(v mSA) { maps.Clear(v) }, + } + for _, fn := range allowedModificationFns { + err = tryUnmarshalModifiedPTS(t, &pts, docker.Transport, validJSON, fn) + require.NoError(t, err) + } +} + +func TestPolicyRequirementsUnmarshalJSON(t *testing.T) { + policyJSONUmarshallerTests[*PolicyRequirements]{ + newDest: func() json.Unmarshaler { return &PolicyRequirements{} }, + newValidObject: func() (*PolicyRequirements, error) { + return &PolicyRequirements{ + xNewPRSignedByKeyData(SBKeyTypeGPGKeys, []byte("def"), NewPRMMatchRepoDigestOrExact()), + xNewPRSignedByKeyData(SBKeyTypeSignedByGPGKeys, []byte("RH"), NewPRMMatchRepository()), + }, nil + }, + otherJSONParser: nil, + }.run(t) + + // This would be inconvenient to integrate into policyJSONUnmarshallerTests.invalidObjects + // because all other users are easier to express as mSA. + for _, invalid := range [][]any{ + // No requirements + {}, + // A member is not an object + {1}, + // A member has an invalid type + {prSignedBy{prCommon: prCommon{Type: "this is invalid"}}}, + // A member has a valid type but invalid contents + {prSignedBy{ + prCommon: prCommon{Type: prTypeSignedBy}, + KeyType: "this is invalid", + }}, + } { + reqs := PolicyRequirements{} + assertJSONUnmarshalFromObjectFails(t, invalid, &reqs) + } +} + +func TestNewPolicyRequirementFromJSON(t *testing.T) { + // Sample success. Others tested in the individual PolicyRequirement.UnmarshalJSON implementations. + validReq := NewPRInsecureAcceptAnything() + validJSON, err := json.Marshal(validReq) + require.NoError(t, err) + req, err := newPolicyRequirementFromJSON(validJSON) + require.NoError(t, err) + assert.Equal(t, validReq, req) + + // Invalid + for _, invalid := range []any{ + // Not an object + 1, + // Missing type + prCommon{}, + // Invalid type + prCommon{Type: "this is invalid"}, + // Valid type but invalid contents + prSignedBy{ + prCommon: prCommon{Type: prTypeSignedBy}, + KeyType: "this is invalid", + }, + } { + testJSON, err := json.Marshal(invalid) + require.NoError(t, err) + + _, err = newPolicyRequirementFromJSON(testJSON) + assert.Error(t, err, string(testJSON)) + } +} + +func TestNewPRInsecureAcceptAnything(t *testing.T) { + _pr := NewPRInsecureAcceptAnything() + pr, ok := _pr.(*prInsecureAcceptAnything) + require.True(t, ok) + assert.Equal(t, &prInsecureAcceptAnything{prCommon{prTypeInsecureAcceptAnything}}, pr) +} + +func TestPRInsecureAcceptAnythingUnmarshalJSON(t *testing.T) { + policyJSONUmarshallerTests[PolicyRequirement]{ + newDest: func() json.Unmarshaler { return &prInsecureAcceptAnything{} }, + newValidObject: func() (PolicyRequirement, error) { + return NewPRInsecureAcceptAnything(), nil + }, + otherJSONParser: newPolicyRequirementFromJSON, + invalidObjects: []mSA{ + // Missing "type" field + {}, + // Wrong "type" field + {"type": 1}, + {"type": "this is invalid"}, + // Extra fields + { + "type": string(prTypeInsecureAcceptAnything), + "unknown": "foo", + }, + }, + duplicateFields: []string{"type"}, + }.run(t) +} + +func TestNewPRReject(t *testing.T) { + _pr := NewPRReject() + pr, ok := _pr.(*prReject) + require.True(t, ok) + assert.Equal(t, &prReject{prCommon{prTypeReject}}, pr) +} + +func TestPRRejectUnmarshalJSON(t *testing.T) { + policyJSONUmarshallerTests[PolicyRequirement]{ + newDest: func() json.Unmarshaler { return &prReject{} }, + newValidObject: func() (PolicyRequirement, error) { + return NewPRReject(), nil + }, + otherJSONParser: newPolicyRequirementFromJSON, + invalidObjects: []mSA{ + // Missing "type" field + {}, + // Wrong "type" field + {"type": 1}, + {"type": "this is invalid"}, + // Extra fields + { + "type": string(prTypeReject), + "unknown": "foo", + }, + }, + duplicateFields: []string{"type"}, + }.run(t) +} + +func TestNewPRSignedBy(t *testing.T) { + const testPath = "/foo/bar" + testPaths := []string{"/path/1", "/path/2"} + testData := []byte("abc") + testIdentity := NewPRMMatchRepoDigestOrExact() + + // Success + pr, err := newPRSignedBy(SBKeyTypeGPGKeys, testPath, nil, nil, testIdentity) + require.NoError(t, err) + assert.Equal(t, &prSignedBy{ + prCommon: prCommon{prTypeSignedBy}, + KeyType: SBKeyTypeGPGKeys, + KeyPath: testPath, + KeyPaths: nil, + KeyData: nil, + SignedIdentity: testIdentity, + }, pr) + pr, err = newPRSignedBy(SBKeyTypeGPGKeys, "", testPaths, nil, testIdentity) + require.NoError(t, err) + assert.Equal(t, &prSignedBy{ + prCommon: prCommon{prTypeSignedBy}, + KeyType: SBKeyTypeGPGKeys, + KeyPath: "", + KeyPaths: testPaths, + KeyData: nil, + SignedIdentity: testIdentity, + }, pr) + pr, err = newPRSignedBy(SBKeyTypeGPGKeys, "", nil, testData, testIdentity) + require.NoError(t, err) + assert.Equal(t, &prSignedBy{ + prCommon: prCommon{prTypeSignedBy}, + KeyType: SBKeyTypeGPGKeys, + KeyPath: "", + KeyPaths: nil, + KeyData: testData, + SignedIdentity: testIdentity, + }, pr) + + // Invalid keyType + _, err = newPRSignedBy(sbKeyType(""), testPath, nil, nil, testIdentity) + assert.Error(t, err) + _, err = newPRSignedBy(sbKeyType("this is invalid"), testPath, nil, nil, testIdentity) + assert.Error(t, err) + + // Invalid keyPath/keyPaths/keyData combinations + _, err = newPRSignedBy(SBKeyTypeGPGKeys, testPath, testPaths, testData, testIdentity) + assert.Error(t, err) + _, err = newPRSignedBy(SBKeyTypeGPGKeys, testPath, testPaths, nil, testIdentity) + assert.Error(t, err) + _, err = newPRSignedBy(SBKeyTypeGPGKeys, testPath, nil, testData, testIdentity) + assert.Error(t, err) + _, err = newPRSignedBy(SBKeyTypeGPGKeys, "", testPaths, testData, testIdentity) + assert.Error(t, err) + _, err = newPRSignedBy(SBKeyTypeGPGKeys, "", nil, nil, testIdentity) + assert.Error(t, err) + + // Invalid signedIdentity + _, err = newPRSignedBy(SBKeyTypeGPGKeys, testPath, nil, nil, nil) + assert.Error(t, err) +} + +func TestNewPRSignedByKeyPath(t *testing.T) { + const testPath = "/foo/bar" + _pr, err := NewPRSignedByKeyPath(SBKeyTypeGPGKeys, testPath, NewPRMMatchRepoDigestOrExact()) + require.NoError(t, err) + pr, ok := _pr.(*prSignedBy) + require.True(t, ok) + assert.Equal(t, testPath, pr.KeyPath) + // Failure cases tested in TestNewPRSignedBy. +} + +func TestNewPRSignedByKeyPaths(t *testing.T) { + testPaths := []string{"/path/1", "/path/2"} + _pr, err := NewPRSignedByKeyPaths(SBKeyTypeGPGKeys, testPaths, NewPRMMatchRepoDigestOrExact()) + require.NoError(t, err) + pr, ok := _pr.(*prSignedBy) + require.True(t, ok) + assert.Equal(t, testPaths, pr.KeyPaths) + // Failure cases tested in TestNewPRSignedBy. +} + +func TestNewPRSignedByKeyData(t *testing.T) { + testData := []byte("abc") + _pr, err := NewPRSignedByKeyData(SBKeyTypeGPGKeys, testData, NewPRMMatchRepoDigestOrExact()) + require.NoError(t, err) + pr, ok := _pr.(*prSignedBy) + require.True(t, ok) + assert.Equal(t, testData, pr.KeyData) + // Failure cases tested in TestNewPRSignedBy. +} + +// Return the result of modifying validJSON with fn and unmarshaling it into *pr +func tryUnmarshalModifiedSignedBy(t *testing.T, pr *prSignedBy, validJSON []byte, modifyFn func(mSA)) error { + var tmp mSA + err := json.Unmarshal(validJSON, &tmp) + require.NoError(t, err) + + modifyFn(tmp) + + *pr = prSignedBy{} + return jsonUnmarshalFromObject(t, tmp, &pr) +} + +func TestPRSignedByUnmarshalJSON(t *testing.T) { + keyDataTests := policyJSONUmarshallerTests[PolicyRequirement]{ + newDest: func() json.Unmarshaler { return &prSignedBy{} }, + newValidObject: func() (PolicyRequirement, error) { + return NewPRSignedByKeyData(SBKeyTypeGPGKeys, []byte("abc"), NewPRMMatchRepoDigestOrExact()) + }, + otherJSONParser: newPolicyRequirementFromJSON, + breakFns: []func(mSA){ + // The "type" field is missing + func(v mSA) { delete(v, "type") }, + // Wrong "type" field + func(v mSA) { v["type"] = 1 }, + func(v mSA) { v["type"] = "this is invalid" }, + // Extra top-level sub-object + func(v mSA) { v["unexpected"] = 1 }, + // The "keyType" field is missing + func(v mSA) { delete(v, "keyType") }, + // Invalid "keyType" field + func(v mSA) { v["keyType"] = "this is invalid" }, + // All three of "keyPath", "keyPaths" and "keyData" are missing + func(v mSA) { delete(v, "keyData") }, + // All three of "keyPath", "keyPaths" and "keyData" are present + func(v mSA) { v["keyPath"] = "/foo/bar"; v["keyPaths"] = []string{"/1", "/2"} }, + // Two of "keyPath", "keyPaths" and "keyData" are present + func(v mSA) { v["keyPath"] = "/foo/bar"; v["keyPaths"] = []string{"/1", "/2"}; delete(v, "keyData") }, + func(v mSA) { v["keyPath"] = "/foo/bar" }, + func(v mSA) { v["keyPaths"] = []string{"/1", "/2"} }, + // Invalid "keyPath" field + func(v mSA) { delete(v, "keyData"); v["keyPath"] = 1 }, + // Invalid "keyPaths" field + func(v mSA) { delete(v, "keyData"); v["keyPaths"] = 1 }, + func(v mSA) { delete(v, "keyData"); v["keyPaths"] = []int{1} }, + // Invalid "keyData" field + func(v mSA) { v["keyData"] = 1 }, + func(v mSA) { v["keyData"] = "this is invalid base64" }, + // Invalid "signedIdentity" field + func(v mSA) { v["signedIdentity"] = "this is invalid" }, + // "signedIdentity" an explicit nil + func(v mSA) { v["signedIdentity"] = nil }, + }, + duplicateFields: []string{"type", "keyType", "keyData", "signedIdentity"}, + } + keyDataTests.run(t) + // Test the keyPath-specific aspects + policyJSONUmarshallerTests[PolicyRequirement]{ + newDest: func() json.Unmarshaler { return &prSignedBy{} }, + newValidObject: func() (PolicyRequirement, error) { + return NewPRSignedByKeyPath(SBKeyTypeGPGKeys, "/foo/bar", NewPRMMatchRepoDigestOrExact()) + }, + otherJSONParser: newPolicyRequirementFromJSON, + duplicateFields: []string{"type", "keyType", "keyPath", "signedIdentity"}, + }.run(t) + // Test the keyPaths-specific aspects + policyJSONUmarshallerTests[PolicyRequirement]{ + newDest: func() json.Unmarshaler { return &prSignedBy{} }, + newValidObject: func() (PolicyRequirement, error) { + return NewPRSignedByKeyPaths(SBKeyTypeGPGKeys, []string{"/1", "/2"}, NewPRMMatchRepoDigestOrExact()) + }, + otherJSONParser: newPolicyRequirementFromJSON, + duplicateFields: []string{"type", "keyType", "keyPaths", "signedIdentity"}, + }.run(t) + + var pr prSignedBy + + // Start with a valid JSON. + _, validJSON := keyDataTests.validObjectAndJSON(t) + + // Various allowed modifications to the requirement + allowedModificationFns := []func(mSA){ + // Delete the signedIdentity field + func(v mSA) { delete(v, "signedIdentity") }, + } + for _, fn := range allowedModificationFns { + err := tryUnmarshalModifiedSignedBy(t, &pr, validJSON, fn) + require.NoError(t, err) + } + + // Various ways to set signedIdentity to the default value + signedIdentityDefaultFns := []func(mSA){ + // Set signedIdentity to the default explicitly + func(v mSA) { v["signedIdentity"] = NewPRMMatchRepoDigestOrExact() }, + // Delete the signedIdentity field + func(v mSA) { delete(v, "signedIdentity") }, + } + for _, fn := range signedIdentityDefaultFns { + err := tryUnmarshalModifiedSignedBy(t, &pr, validJSON, fn) + require.NoError(t, err) + assert.Equal(t, NewPRMMatchRepoDigestOrExact(), pr.SignedIdentity) + } +} + +func TestSBKeyTypeIsValid(t *testing.T) { + // Valid values + for _, s := range []sbKeyType{ + SBKeyTypeGPGKeys, + SBKeyTypeSignedByGPGKeys, + SBKeyTypeX509Certificates, + SBKeyTypeSignedByX509CAs, + } { + assert.True(t, s.IsValid()) + } + + // Invalid values + for _, s := range []string{"", "this is invalid"} { + assert.False(t, sbKeyType(s).IsValid()) + } +} + +func TestSBKeyTypeUnmarshalJSON(t *testing.T) { + var kt sbKeyType + + testInvalidJSONInput(t, &kt) + + // Valid values. + for _, v := range []sbKeyType{ + SBKeyTypeGPGKeys, + SBKeyTypeSignedByGPGKeys, + SBKeyTypeX509Certificates, + SBKeyTypeSignedByX509CAs, + } { + kt = sbKeyType("") + err := json.Unmarshal([]byte(`"`+string(v)+`"`), &kt) + assert.NoError(t, err) + } + + // Invalid values + kt = sbKeyType("") + err := json.Unmarshal([]byte(`""`), &kt) + assert.Error(t, err) + + kt = sbKeyType("") + err = json.Unmarshal([]byte(`"this is invalid"`), &kt) + assert.Error(t, err) +} + +// NewPRSignedBaseLayer is like NewPRSignedBaseLayer, except it must not fail. +func xNewPRSignedBaseLayer(baseLayerIdentity PolicyReferenceMatch) PolicyRequirement { + pr, err := NewPRSignedBaseLayer(baseLayerIdentity) + if err != nil { + panic("xNewPRSignedBaseLayer failed") + } + return pr +} + +func TestNewPRSignedBaseLayer(t *testing.T) { + testBLI := NewPRMMatchExact() + + // Success + _pr, err := NewPRSignedBaseLayer(testBLI) + require.NoError(t, err) + pr, ok := _pr.(*prSignedBaseLayer) + require.True(t, ok) + assert.Equal(t, &prSignedBaseLayer{ + prCommon: prCommon{prTypeSignedBaseLayer}, + BaseLayerIdentity: testBLI, + }, pr) + + // Invalid baseLayerIdentity + _, err = NewPRSignedBaseLayer(nil) + assert.Error(t, err) +} + +func TestPRSignedBaseLayerUnmarshalJSON(t *testing.T) { + policyJSONUmarshallerTests[PolicyRequirement]{ + newDest: func() json.Unmarshaler { return &prSignedBaseLayer{} }, + newValidObject: func() (PolicyRequirement, error) { + baseIdentity, err := NewPRMExactReference("registry.access.redhat.com/rhel7/rhel:7.2.3") + require.NoError(t, err) + return NewPRSignedBaseLayer(baseIdentity) + }, + otherJSONParser: newPolicyRequirementFromJSON, + breakFns: []func(mSA){ + // The "type" field is missing + func(v mSA) { delete(v, "type") }, + // Wrong "type" field + func(v mSA) { v["type"] = 1 }, + func(v mSA) { v["type"] = "this is invalid" }, + // Extra top-level sub-object + func(v mSA) { v["unexpected"] = 1 }, + // The "baseLayerIdentity" field is missing + func(v mSA) { delete(v, "baseLayerIdentity") }, + // Invalid "baseLayerIdentity" field + func(v mSA) { v["baseLayerIdentity"] = "this is invalid" }, + // Invalid "baseLayerIdentity" an explicit nil + func(v mSA) { v["baseLayerIdentity"] = nil }, + }, + duplicateFields: []string{"type", "baseLayerIdentity"}, + }.run(t) +} + +func TestNewPolicyReferenceMatchFromJSON(t *testing.T) { + // Sample success. Others tested in the individual PolicyReferenceMatch.UnmarshalJSON implementations. + validPRM := NewPRMMatchRepoDigestOrExact() + validJSON, err := json.Marshal(validPRM) + require.NoError(t, err) + prm, err := newPolicyReferenceMatchFromJSON(validJSON) + require.NoError(t, err) + assert.Equal(t, validPRM, prm) + + // Invalid + for _, invalid := range []any{ + // Not an object + 1, + // Missing type + prmCommon{}, + // Invalid type + prmCommon{Type: "this is invalid"}, + // Valid type but invalid contents + prmExactReference{ + prmCommon: prmCommon{Type: prmTypeExactReference}, + DockerReference: "", + }, + } { + testJSON, err := json.Marshal(invalid) + require.NoError(t, err) + + _, err = newPolicyReferenceMatchFromJSON(testJSON) + assert.Error(t, err, string(testJSON)) + } +} + +func TestNewPRMMatchExact(t *testing.T) { + _prm := NewPRMMatchExact() + prm, ok := _prm.(*prmMatchExact) + require.True(t, ok) + assert.Equal(t, &prmMatchExact{prmCommon{prmTypeMatchExact}}, prm) +} + +func TestPRMMatchExactUnmarshalJSON(t *testing.T) { + policyJSONUmarshallerTests[PolicyReferenceMatch]{ + newDest: func() json.Unmarshaler { return &prmMatchExact{} }, + newValidObject: func() (PolicyReferenceMatch, error) { + return NewPRMMatchExact(), nil + }, + otherJSONParser: newPolicyReferenceMatchFromJSON, + invalidObjects: []mSA{ + // Missing "type" field + {}, + // Wrong "type" field + {"type": 1}, + {"type": "this is invalid"}, + // Extra fields + { + "type": string(prmTypeMatchExact), + "unknown": "foo", + }, + }, + duplicateFields: []string{"type"}, + }.run(t) +} + +func TestNewPRMMatchRepoDigestOrExact(t *testing.T) { + _prm := NewPRMMatchRepoDigestOrExact() + prm, ok := _prm.(*prmMatchRepoDigestOrExact) + require.True(t, ok) + assert.Equal(t, &prmMatchRepoDigestOrExact{prmCommon{prmTypeMatchRepoDigestOrExact}}, prm) +} + +func TestPRMMatchRepoDigestOrExactUnmarshalJSON(t *testing.T) { + policyJSONUmarshallerTests[PolicyReferenceMatch]{ + newDest: func() json.Unmarshaler { return &prmMatchRepoDigestOrExact{} }, + newValidObject: func() (PolicyReferenceMatch, error) { + return NewPRMMatchRepoDigestOrExact(), nil + }, + otherJSONParser: newPolicyReferenceMatchFromJSON, + invalidObjects: []mSA{ + // Missing "type" field + {}, + // Wrong "type" field + {"type": 1}, + {"type": "this is invalid"}, + // Extra fields + { + "type": string(prmTypeMatchRepoDigestOrExact), + "unknown": "foo", + }, + }, + duplicateFields: []string{"type"}, + }.run(t) +} + +func TestNewPRMMatchRepository(t *testing.T) { + _prm := NewPRMMatchRepository() + prm, ok := _prm.(*prmMatchRepository) + require.True(t, ok) + assert.Equal(t, &prmMatchRepository{prmCommon{prmTypeMatchRepository}}, prm) +} + +func TestPRMMatchRepositoryUnmarshalJSON(t *testing.T) { + policyJSONUmarshallerTests[PolicyReferenceMatch]{ + newDest: func() json.Unmarshaler { return &prmMatchRepository{} }, + newValidObject: func() (PolicyReferenceMatch, error) { + return NewPRMMatchRepository(), nil + }, + otherJSONParser: newPolicyReferenceMatchFromJSON, + invalidObjects: []mSA{ + // Missing "type" field + {}, + // Wrong "type" field + {"type": 1}, + {"type": "this is invalid"}, + // Extra fields + { + "type": string(prmTypeMatchRepository), + "unknown": "foo", + }, + }, + duplicateFields: []string{"type"}, + }.run(t) +} + +// xNewPRMExactReference is like NewPRMExactReference, except it must not fail. +func xNewPRMExactReference(dockerReference string) PolicyReferenceMatch { + pr, err := NewPRMExactReference(dockerReference) + if err != nil { + panic("xNewPRMExactReference failed") + } + return pr +} + +func TestNewPRMExactReference(t *testing.T) { + const testDR = "library/busybox:latest" + + // Success + _prm, err := NewPRMExactReference(testDR) + require.NoError(t, err) + prm, ok := _prm.(*prmExactReference) + require.True(t, ok) + assert.Equal(t, &prmExactReference{ + prmCommon: prmCommon{prmTypeExactReference}, + DockerReference: testDR, + }, prm) + + // Invalid dockerReference + _, err = NewPRMExactReference("") + assert.Error(t, err) + // Uppercase is invalid in Docker reference components. + _, err = NewPRMExactReference("INVALIDUPPERCASE:latest") + assert.Error(t, err) + // Missing tag + _, err = NewPRMExactReference("library/busybox") + assert.Error(t, err) +} + +func TestPRMExactReferenceUnmarshalJSON(t *testing.T) { + policyJSONUmarshallerTests[PolicyReferenceMatch]{ + newDest: func() json.Unmarshaler { return &prmExactReference{} }, + newValidObject: func() (PolicyReferenceMatch, error) { + return NewPRMExactReference("library/busybox:latest") + }, + otherJSONParser: newPolicyReferenceMatchFromJSON, + breakFns: []func(mSA){ + // The "type" field is missing + func(v mSA) { delete(v, "type") }, + // Wrong "type" field + func(v mSA) { v["type"] = 1 }, + func(v mSA) { v["type"] = "this is invalid" }, + // Extra top-level sub-object + func(v mSA) { v["unexpected"] = 1 }, + // The "dockerReference" field is missing + func(v mSA) { delete(v, "dockerReference") }, + // Invalid "dockerReference" field + func(v mSA) { v["dockerReference"] = 1 }, + }, + duplicateFields: []string{"type", "dockerReference"}, + }.run(t) +} + +// xNewPRMExactRepository is like NewPRMExactRepository, except it must not fail. +func xNewPRMExactRepository(dockerRepository string) PolicyReferenceMatch { + pr, err := NewPRMExactRepository(dockerRepository) + if err != nil { + panic("xNewPRMExactRepository failed") + } + return pr +} + +func TestNewPRMExactRepository(t *testing.T) { + const testDR = "library/busybox:latest" + + // Success + _prm, err := NewPRMExactRepository(testDR) + require.NoError(t, err) + prm, ok := _prm.(*prmExactRepository) + require.True(t, ok) + assert.Equal(t, &prmExactRepository{ + prmCommon: prmCommon{prmTypeExactRepository}, + DockerRepository: testDR, + }, prm) + + // Invalid dockerRepository + _, err = NewPRMExactRepository("") + assert.Error(t, err) + // Uppercase is invalid in Docker reference components. + _, err = NewPRMExactRepository("INVALIDUPPERCASE") + assert.Error(t, err) +} + +func TestPRMExactRepositoryUnmarshalJSON(t *testing.T) { + policyJSONUmarshallerTests[PolicyReferenceMatch]{ + newDest: func() json.Unmarshaler { return &prmExactRepository{} }, + newValidObject: func() (PolicyReferenceMatch, error) { + return NewPRMExactRepository("library/busybox:latest") + }, + otherJSONParser: newPolicyReferenceMatchFromJSON, + breakFns: []func(mSA){ + // The "type" field is missing + func(v mSA) { delete(v, "type") }, + // Wrong "type" field + func(v mSA) { v["type"] = 1 }, + func(v mSA) { v["type"] = "this is invalid" }, + // Extra top-level sub-object + func(v mSA) { v["unexpected"] = 1 }, + // The "dockerRepository" field is missing + func(v mSA) { delete(v, "dockerRepository") }, + // Invalid "dockerRepository" field + func(v mSA) { v["dockerRepository"] = 1 }, + }, + duplicateFields: []string{"type", "dockerRepository"}, + }.run(t) +} + +func TestValidateIdentityRemappingPrefix(t *testing.T) { + for _, s := range []string{ + "localhost", + "example.com", + "example.com:80", + "example.com/repo", + "example.com/ns1/ns2/ns3/repo.with.dots-dashes_underscores", + "example.com:80/ns1/ns2/ns3/repo.with.dots-dashes_underscores", + // NOTE: These values are invalid, do not actually work, and may be rejected by this function + // and in NewPRMRemapIdentity in the future. + "shortname", + "ns/shortname", + } { + err := validateIdentityRemappingPrefix(s) + assert.NoError(t, err, s) + } + + for _, s := range []string{ + "", + "repo_with_underscores", // Not a valid DNS name, at least per docker/reference + "example.com/", + "example.com/UPPERCASEISINVALID", + "example.com/repo/", + "example.com/repo:tag", + "example.com/repo@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "example.com/repo:tag@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + } { + err := validateIdentityRemappingPrefix(s) + assert.Error(t, err, s) + } +} + +// xNewPRMRemapIdentity is like NewPRMRemapIdentity, except it must not fail. +func xNewPRMRemapIdentity(prefix, signedPrefix string) PolicyReferenceMatch { + pr, err := NewPRMRemapIdentity(prefix, signedPrefix) + if err != nil { + panic("xNewPRMRemapIdentity failed") + } + return pr +} + +func TestNewPRMRemapIdentity(t *testing.T) { + const testPrefix = "example.com/docker-library" + const testSignedPrefix = "docker.io/library" + + // Success + _prm, err := NewPRMRemapIdentity(testPrefix, testSignedPrefix) + require.NoError(t, err) + prm, ok := _prm.(*prmRemapIdentity) + require.True(t, ok) + assert.Equal(t, &prmRemapIdentity{ + prmCommon: prmCommon{prmTypeRemapIdentity}, + Prefix: testPrefix, + SignedPrefix: testSignedPrefix, + }, prm) + + // Invalid prefix + _, err = NewPRMRemapIdentity("", testSignedPrefix) + assert.Error(t, err) + _, err = NewPRMRemapIdentity("example.com/UPPERCASEISINVALID", testSignedPrefix) + assert.Error(t, err) + // Invalid signedPrefix + _, err = NewPRMRemapIdentity(testPrefix, "") + assert.Error(t, err) + _, err = NewPRMRemapIdentity(testPrefix, "example.com/UPPERCASEISINVALID") + assert.Error(t, err) +} + +func TestPRMRemapIdentityUnmarshalJSON(t *testing.T) { + policyJSONUmarshallerTests[PolicyReferenceMatch]{ + newDest: func() json.Unmarshaler { return &prmRemapIdentity{} }, + newValidObject: func() (PolicyReferenceMatch, error) { + return NewPRMRemapIdentity("example.com/docker-library", "docker.io/library") + }, + otherJSONParser: newPolicyReferenceMatchFromJSON, + breakFns: []func(mSA){ + // The "type" field is missing + func(v mSA) { delete(v, "type") }, + // Wrong "type" field + func(v mSA) { v["type"] = 1 }, + func(v mSA) { v["type"] = "this is invalid" }, + // Extra top-level sub-object + func(v mSA) { v["unexpected"] = 1 }, + // The "prefix" field is missing + func(v mSA) { delete(v, "prefix") }, + // Invalid "prefix" field + func(v mSA) { v["prefix"] = 1 }, + func(v mSA) { v["prefix"] = "this is invalid" }, + // The "signedPrefix" field is missing + func(v mSA) { delete(v, "signedPrefix") }, + // Invalid "signedPrefix" field + func(v mSA) { v["signedPrefix"] = 1 }, + func(v mSA) { v["signedPrefix"] = "this is invalid" }, + }, + duplicateFields: []string{"type", "prefix", "signedPrefix"}, + }.run(t) +} diff --git a/vendor/github.com/containers/image/v5/signature/policy_eval_baselayer_test.go b/vendor/github.com/containers/image/v5/signature/policy_eval_baselayer_test.go new file mode 100644 index 00000000000..8898cf81675 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/policy_eval_baselayer_test.go @@ -0,0 +1,25 @@ +package signature + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPRSignedBaseLayerIsSignatureAuthorAccepted(t *testing.T) { + pr, err := NewPRSignedBaseLayer(NewPRMMatchRepository()) + require.NoError(t, err) + // Pass nil pointers to, kind of, test that the return value does not depend on the parameters. + sar, parsedSig, err := pr.isSignatureAuthorAccepted(context.Background(), nil, nil) + assertSARUnknown(t, sar, parsedSig, err) +} + +func TestPRSignedBaseLayerIsRunningImageAllowed(t *testing.T) { + // This will obviously need to change after signedBaseLayer is implemented. + pr, err := NewPRSignedBaseLayer(NewPRMMatchRepository()) + require.NoError(t, err) + // Pass a nil pointer to, kind of, test that the return value does not depend on the image. + res, err := pr.isRunningImageAllowed(context.Background(), nil) + assertRunningRejectedPolicyRequirement(t, res, err) +} diff --git a/vendor/github.com/containers/image/v5/signature/policy_eval_signedby_test.go b/vendor/github.com/containers/image/v5/signature/policy_eval_signedby_test.go new file mode 100644 index 00000000000..0e9bb83f73d --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/policy_eval_signedby_test.go @@ -0,0 +1,280 @@ +package signature + +import ( + "context" + "os" + "path" + "testing" + + "github.com/containers/image/v5/directory" + "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/internal/image" + "github.com/containers/image/v5/internal/imagesource" + "github.com/containers/image/v5/internal/private" + "github.com/containers/image/v5/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// dirImageMock returns a private.UnparsedImage for a directory, claiming a specified dockerReference. +func dirImageMock(t *testing.T, dir, dockerReference string) private.UnparsedImage { + ref, err := reference.ParseNormalizedNamed(dockerReference) + require.NoError(t, err) + return dirImageMockWithRef(t, dir, refImageReferenceMock{ref: ref}) +} + +// dirImageMockWithRef returns a private.UnparsedImage for a directory, claiming a specified ref. +func dirImageMockWithRef(t *testing.T, dir string, ref types.ImageReference) private.UnparsedImage { + srcRef, err := directory.NewReference(dir) + require.NoError(t, err) + src, err := srcRef.NewImageSource(context.Background(), nil) + require.NoError(t, err) + t.Cleanup(func() { + err := src.Close() + require.NoError(t, err) + }) + return image.UnparsedInstance(&dirImageSourceMock{ + ImageSource: imagesource.FromPublic(src), + ref: ref, + }, nil) +} + +// dirImageSourceMock inherits dirImageSource, but overrides its Reference method. +type dirImageSourceMock struct { + private.ImageSource + ref types.ImageReference +} + +func (d *dirImageSourceMock) Reference() types.ImageReference { + return d.ref +} + +func TestPRSignedByIsSignatureAuthorAccepted(t *testing.T) { + ktGPG := SBKeyTypeGPGKeys + prm := NewPRMMatchExact() + testImage := dirImageMock(t, "fixtures/dir-img-valid", "testing/manifest:latest") + testImageSig, err := os.ReadFile("fixtures/dir-img-valid/signature-1") + require.NoError(t, err) + keyData, err := os.ReadFile("fixtures/public-key.gpg") + require.NoError(t, err) + + // Successful validation, with KeyPath, KeyPaths and KeyData. + for _, fn := range []func() (PolicyRequirement, error){ + func() (PolicyRequirement, error) { + return NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm) + }, + // Test the files in both orders, to make sure the correct public keys accepted in either position. + func() (PolicyRequirement, error) { + return NewPRSignedByKeyPaths(ktGPG, []string{"fixtures/public-key-1.gpg", "fixtures/public-key-1.gpg"}, prm) + }, + func() (PolicyRequirement, error) { + return NewPRSignedByKeyPaths(ktGPG, []string{"fixtures/public-key-2.gpg", "fixtures/public-key-1.gpg"}, prm) + }, + func() (PolicyRequirement, error) { + return NewPRSignedByKeyData(ktGPG, keyData, prm) + }, + } { + pr, err := fn() + require.NoError(t, err) + sar, parsedSig, err := pr.isSignatureAuthorAccepted(context.Background(), testImage, testImageSig) + assertSARAccepted(t, sar, parsedSig, err, Signature{ + DockerManifestDigest: TestImageManifestDigest, + DockerReference: "testing/manifest:latest", + }) + } + + // Unimplemented and invalid KeyType values + for _, keyType := range []sbKeyType{SBKeyTypeSignedByGPGKeys, + SBKeyTypeX509Certificates, + SBKeyTypeSignedByX509CAs, + sbKeyType("This is invalid"), + } { + // Do not use NewPRSignedByKeyData, because it would reject invalid values. + pr := &prSignedBy{ + KeyType: keyType, + KeyData: keyData, + SignedIdentity: prm, + } + // Pass nil pointers to, kind of, test that the return value does not depend on the parameters. + sar, parsedSig, err := pr.isSignatureAuthorAccepted(context.Background(), nil, nil) + assertSARRejected(t, sar, parsedSig, err) + } + + // Invalid KeyPath/KeyPaths/KeyData combinations. + for _, fn := range []func() (PolicyRequirement, error){ + // Two or more of KeyPath, KeyPaths and KeyData set. Do not use NewPRSignedBy*, because it would reject this. + func() (PolicyRequirement, error) { + return &prSignedBy{KeyType: ktGPG, KeyPath: "fixtures/public-key.gpg", KeyPaths: []string{"fixtures/public-key-1.gpg", "fixtures/public-key-2.gpg"}, KeyData: keyData, SignedIdentity: prm}, nil + }, + func() (PolicyRequirement, error) { + return &prSignedBy{KeyType: ktGPG, KeyPath: "fixtures/public-key.gpg", KeyPaths: []string{"fixtures/public-key-1.gpg", "fixtures/public-key-2.gpg"}, SignedIdentity: prm}, nil + }, + func() (PolicyRequirement, error) { + return &prSignedBy{KeyType: ktGPG, KeyPath: "fixtures/public-key.gpg", KeyData: keyData, SignedIdentity: prm}, nil + }, + func() (PolicyRequirement, error) { + return &prSignedBy{KeyType: ktGPG, KeyPaths: []string{"fixtures/public-key-1.gpg", "fixtures/public-key-2.gpg"}, KeyData: keyData, SignedIdentity: prm}, nil + }, + // None of KeyPath, KeyPaths and KeyData set. Do not use NewPRSignedBy*, because it would reject this. + func() (PolicyRequirement, error) { + return &prSignedBy{KeyType: ktGPG, SignedIdentity: prm}, nil + }, + func() (PolicyRequirement, error) { // Invalid KeyPath + return NewPRSignedByKeyPath(ktGPG, "/this/does/not/exist", prm) + }, + func() (PolicyRequirement, error) { // Invalid KeyPaths + return NewPRSignedByKeyPaths(ktGPG, []string{"/this/does/not/exist"}, prm) + }, + func() (PolicyRequirement, error) { // One of the KeyPaths is invalid + return NewPRSignedByKeyPaths(ktGPG, []string{"fixtures/public-key.gpg", "/this/does/not/exist"}, prm) + }, + } { + pr, err := fn() + require.NoError(t, err) + // Pass nil pointers to, kind of, test that the return value does not depend on the parameters. + sar, parsedSig, err := pr.isSignatureAuthorAccepted(context.Background(), nil, nil) + assertSARRejected(t, sar, parsedSig, err) + } + + // Errors initializing the temporary GPG directory and mechanism are not obviously easy to reach. + + // KeyData has no public keys. + pr, err := NewPRSignedByKeyData(ktGPG, []byte{}, prm) + require.NoError(t, err) + // Pass nil pointers to, kind of, test that the return value does not depend on the parameters. + sar, parsedSig, err := pr.isSignatureAuthorAccepted(context.Background(), nil, nil) + assertSARRejectedPolicyRequirement(t, sar, parsedSig, err) + + // A signature which does not GPG verify + pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm) + require.NoError(t, err) + // Pass a nil pointer to, kind of, test that the return value does not depend on the image parameter. + sar, parsedSig, err = pr.isSignatureAuthorAccepted(context.Background(), nil, []byte("invalid signature")) + assertSARRejected(t, sar, parsedSig, err) + + // A valid signature using an unknown key. + // (This is (currently?) rejected through the "mech.Verify fails" path, not the "!identityFound" path, + // because we use a temporary directory and only import the trusted keys.) + pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm) + require.NoError(t, err) + sig, err := os.ReadFile("fixtures/unknown-key.signature") + require.NoError(t, err) + // Pass a nil pointer to, kind of, test that the return value does not depend on the image parameter.. + sar, parsedSig, err = pr.isSignatureAuthorAccepted(context.Background(), nil, sig) + assertSARRejected(t, sar, parsedSig, err) + + // A valid signature of an invalid JSON. + pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm) + require.NoError(t, err) + sig, err = os.ReadFile("fixtures/invalid-blob.signature") + require.NoError(t, err) + // Pass a nil pointer to, kind of, test that the return value does not depend on the image parameter.. + sar, parsedSig, err = pr.isSignatureAuthorAccepted(context.Background(), nil, sig) + assertSARRejected(t, sar, parsedSig, err) + assert.IsType(t, InvalidSignatureError{}, err) + + // A valid signature with a rejected identity. + nonmatchingPRM, err := NewPRMExactReference("this/doesnt:match") + require.NoError(t, err) + pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", nonmatchingPRM) + require.NoError(t, err) + sar, parsedSig, err = pr.isSignatureAuthorAccepted(context.Background(), testImage, testImageSig) + assertSARRejectedPolicyRequirement(t, sar, parsedSig, err) + + // Error reading image manifest + image := dirImageMock(t, "fixtures/dir-img-no-manifest", "testing/manifest:latest") + sig, err = os.ReadFile("fixtures/dir-img-no-manifest/signature-1") + require.NoError(t, err) + pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm) + require.NoError(t, err) + sar, parsedSig, err = pr.isSignatureAuthorAccepted(context.Background(), image, sig) + assertSARRejected(t, sar, parsedSig, err) + + // Error computing manifest digest + image = dirImageMock(t, "fixtures/dir-img-manifest-digest-error", "testing/manifest:latest") + sig, err = os.ReadFile("fixtures/dir-img-manifest-digest-error/signature-1") + require.NoError(t, err) + pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm) + require.NoError(t, err) + sar, parsedSig, err = pr.isSignatureAuthorAccepted(context.Background(), image, sig) + assertSARRejected(t, sar, parsedSig, err) + + // A valid signature with a non-matching manifest + image = dirImageMock(t, "fixtures/dir-img-modified-manifest", "testing/manifest:latest") + sig, err = os.ReadFile("fixtures/dir-img-modified-manifest/signature-1") + require.NoError(t, err) + pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm) + require.NoError(t, err) + sar, parsedSig, err = pr.isSignatureAuthorAccepted(context.Background(), image, sig) + assertSARRejectedPolicyRequirement(t, sar, parsedSig, err) +} + +// createInvalidSigDir creates a directory suitable for dirImageMock, in which image.Signatures() +// fails. +func createInvalidSigDir(t *testing.T) string { + dir := t.TempDir() + err := os.WriteFile(path.Join(dir, "manifest.json"), []byte("{}"), 0644) + require.NoError(t, err) + // Creating a 000-permissions file would work for unprivileged accounts, but root (in particular, + // in the Docker container we use for testing) would still have access. So, create a symlink + // pointing to itself, to cause an ELOOP. (Note that a symlink pointing to a nonexistent file would be treated + // just like a nonexistent signature file, and not an error.) + err = os.Symlink("signature-1", path.Join(dir, "signature-1")) + require.NoError(t, err) + return dir +} + +func TestPRSignedByIsRunningImageAllowed(t *testing.T) { + ktGPG := SBKeyTypeGPGKeys + prm := NewPRMMatchExact() + + // A simple success case: single valid signature. + image := dirImageMock(t, "fixtures/dir-img-valid", "testing/manifest:latest") + pr, err := NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm) + require.NoError(t, err) + allowed, err := pr.isRunningImageAllowed(context.Background(), image) + assertRunningAllowed(t, allowed, err) + + // Error reading signatures + invalidSigDir := createInvalidSigDir(t) + image = dirImageMock(t, invalidSigDir, "testing/manifest:latest") + pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm) + require.NoError(t, err) + allowed, err = pr.isRunningImageAllowed(context.Background(), image) + assertRunningRejected(t, allowed, err) + + // No signatures + image = dirImageMock(t, "fixtures/dir-img-unsigned", "testing/manifest:latest") + pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm) + require.NoError(t, err) + allowed, err = pr.isRunningImageAllowed(context.Background(), image) + assertRunningRejectedPolicyRequirement(t, allowed, err) + + // 1 invalid signature: use dir-img-valid, but a non-matching Docker reference + image = dirImageMock(t, "fixtures/dir-img-valid", "testing/manifest:notlatest") + pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm) + require.NoError(t, err) + allowed, err = pr.isRunningImageAllowed(context.Background(), image) + assertRunningRejectedPolicyRequirement(t, allowed, err) + + // 2 valid signatures + image = dirImageMock(t, "fixtures/dir-img-valid-2", "testing/manifest:latest") + pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm) + require.NoError(t, err) + allowed, err = pr.isRunningImageAllowed(context.Background(), image) + assertRunningAllowed(t, allowed, err) + + // One invalid, one valid signature (in this order) + image = dirImageMock(t, "fixtures/dir-img-mixed", "testing/manifest:latest") + pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm) + require.NoError(t, err) + allowed, err = pr.isRunningImageAllowed(context.Background(), image) + assertRunningAllowed(t, allowed, err) + + // 2 invalid signatures: use dir-img-valid-2, but a non-matching Docker reference + image = dirImageMock(t, "fixtures/dir-img-valid-2", "testing/manifest:notlatest") + pr, err = NewPRSignedByKeyPath(ktGPG, "fixtures/public-key.gpg", prm) + require.NoError(t, err) + allowed, err = pr.isRunningImageAllowed(context.Background(), image) + assertRunningRejectedPolicyRequirement(t, allowed, err) +} diff --git a/vendor/github.com/containers/image/v5/signature/policy_eval_sigstore_test.go b/vendor/github.com/containers/image/v5/signature/policy_eval_sigstore_test.go new file mode 100644 index 00000000000..f4dd11368e9 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/policy_eval_sigstore_test.go @@ -0,0 +1,681 @@ +// Policy evaluation for prCosignSigned. + +package signature + +import ( + "context" + "encoding/base64" + "os" + "testing" + + "github.com/containers/image/v5/internal/signature" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPRSigstoreSignedFulcioPrepareTrustRoot(t *testing.T) { + const testCAPath = "fixtures/fulcio_v1.crt.pem" + testCAData, err := os.ReadFile(testCAPath) + require.NoError(t, err) + const testOIDCIssuer = "https://example.com" + testSubjectEmail := "test@example.com" + + // Success + for _, c := range [][]PRSigstoreSignedFulcioOption{ + { + PRSigstoreSignedFulcioWithCAPath(testCAPath), + PRSigstoreSignedFulcioWithOIDCIssuer(testOIDCIssuer), + PRSigstoreSignedFulcioWithSubjectEmail(testSubjectEmail), + }, + { + PRSigstoreSignedFulcioWithCAData(testCAData), + PRSigstoreSignedFulcioWithOIDCIssuer(testOIDCIssuer), + PRSigstoreSignedFulcioWithSubjectEmail(testSubjectEmail), + }, + } { + f, err := newPRSigstoreSignedFulcio(c...) + require.NoError(t, err) + res, err := f.prepareTrustRoot() + require.NoError(t, err) + assert.NotNil(t, res.caCertificates) // Doing a better test seems hard; we would need to compare .Subjects with a DER encoding. + assert.Equal(t, testOIDCIssuer, res.oidcIssuer) + assert.Equal(t, testSubjectEmail, res.subjectEmail) + } + + // Failure + for _, f := range []prSigstoreSignedFulcio{ // Use a prSigstoreSignedFulcio because these configurations should be rejected by NewPRSigstoreSignedFulcio. + { // Neither CAPath nor CAData specified + OIDCIssuer: testOIDCIssuer, + SubjectEmail: testSubjectEmail, + }, + { // Both CAPath and CAData specified + CAPath: testCAPath, + CAData: testCAData, + OIDCIssuer: testOIDCIssuer, + SubjectEmail: testSubjectEmail, + }, + { // Invalid CAPath + CAPath: "fixtures/image.signature", + OIDCIssuer: testOIDCIssuer, + SubjectEmail: testSubjectEmail, + }, + { // Unusable CAPath + CAPath: "fixtures/this/does/not/exist", + OIDCIssuer: testOIDCIssuer, + SubjectEmail: testSubjectEmail, + }, + { // Invalid CAData + CAData: []byte("invalid"), + OIDCIssuer: testOIDCIssuer, + SubjectEmail: testSubjectEmail, + }, + { // Missing OIDCIssuer + CAPath: testCAPath, + SubjectEmail: testSubjectEmail, + }, + { // Missing SubjectEmail + CAPath: testCAPath, + OIDCIssuer: testOIDCIssuer, + }, + } { + _, err := f.prepareTrustRoot() + assert.Error(t, err) + } +} + +func TestPRSigstoreSignedPrepareTrustRoot(t *testing.T) { + const testKeyPath = "fixtures/cosign.pub" + testKeyData, err := os.ReadFile(testKeyPath) + require.NoError(t, err) + testFulcio, err := NewPRSigstoreSignedFulcio( + PRSigstoreSignedFulcioWithCAPath("fixtures/fulcio_v1.crt.pem"), + PRSigstoreSignedFulcioWithOIDCIssuer("https://github.com/login/oauth"), + PRSigstoreSignedFulcioWithSubjectEmail("mitr@redhat.com"), + ) + require.NoError(t, err) + const testRekorPublicKeyPath = "fixtures/rekor.pub" + testRekorPublicKeyData, err := os.ReadFile(testRekorPublicKeyPath) + require.NoError(t, err) + testIdentity := newPRMMatchRepoDigestOrExact() + testIdentityOption := PRSigstoreSignedWithSignedIdentity(testIdentity) + + // Success with public key + for _, c := range [][]PRSigstoreSignedOption{ + { + PRSigstoreSignedWithKeyPath(testKeyPath), + testIdentityOption, + }, + { + PRSigstoreSignedWithKeyData(testKeyData), + testIdentityOption, + }, + } { + pr, err := newPRSigstoreSigned(c...) + require.NoError(t, err) + res, err := pr.prepareTrustRoot() + require.NoError(t, err) + assert.NotNil(t, res.publicKey) + assert.Nil(t, res.fulcio) + assert.Nil(t, res.rekorPublicKey) + } + // Success with Fulcio + pr, err := newPRSigstoreSigned( + PRSigstoreSignedWithFulcio(testFulcio), + PRSigstoreSignedWithRekorPublicKeyData(testRekorPublicKeyData), + testIdentityOption, + ) + require.NoError(t, err) + res, err := pr.prepareTrustRoot() + require.NoError(t, err) + assert.Nil(t, res.publicKey) + assert.NotNil(t, res.fulcio) + assert.NotNil(t, res.rekorPublicKey) + // Success with Rekor public key + for _, c := range [][]PRSigstoreSignedOption{ + { + PRSigstoreSignedWithKeyData(testKeyData), + PRSigstoreSignedWithRekorPublicKeyPath(testRekorPublicKeyPath), + testIdentityOption, + }, + { + PRSigstoreSignedWithKeyData(testKeyData), + PRSigstoreSignedWithRekorPublicKeyData(testRekorPublicKeyData), + testIdentityOption, + }, + } { + pr, err := newPRSigstoreSigned(c...) + require.NoError(t, err) + res, err := pr.prepareTrustRoot() + require.NoError(t, err) + assert.NotNil(t, res.publicKey) + assert.Nil(t, res.fulcio) + assert.NotNil(t, res.rekorPublicKey) + } + + // Failure + for _, pr := range []prSigstoreSigned{ // Use a prSigstoreSigned because these configurations should be rejected by NewPRSigstoreSigned. + { // Both KeyPath and KeyData specified + KeyPath: testKeyPath, + KeyData: testKeyData, + SignedIdentity: testIdentity, + }, + { // Invalid public key path + KeyPath: "fixtures/image.signature", + SignedIdentity: testIdentity, + }, + { // Unusable public key path + KeyPath: "fixtures/this/does/not/exist", + SignedIdentity: testIdentity, + }, + { // Invalid public key data + KeyData: []byte("this is invalid"), + SignedIdentity: testIdentity, + }, + { // Invalid Fulcio configuration + Fulcio: &prSigstoreSignedFulcio{}, + RekorPublicKeyData: testKeyData, + SignedIdentity: testIdentity, + }, + { // Both RekorPublicKeyPath and RekorPublicKeyData specified + KeyData: testKeyData, + RekorPublicKeyPath: testRekorPublicKeyPath, + RekorPublicKeyData: testRekorPublicKeyData, + SignedIdentity: testIdentity, + }, + { // Invalid Rekor public key path + KeyData: testKeyData, + RekorPublicKeyPath: "fixtures/image.signature", + SignedIdentity: testIdentity, + }, + { // Invalid Rekor public key data + KeyData: testKeyData, + RekorPublicKeyData: []byte("this is invalid"), + SignedIdentity: testIdentity, + }, + { // Rekor public key is not ECDSA + KeyData: testKeyData, + RekorPublicKeyPath: "fixtures/some-rsa-key.pub", + SignedIdentity: testIdentity, + }, + } { + _, err = pr.prepareTrustRoot() + assert.Error(t, err) + } +} + +func TestPRSigstoreSignedIsSignatureAuthorAccepted(t *testing.T) { + // Currently, this fails even with a correctly signed image. + prm := NewPRMMatchRepository() // We prefer to test with a Cosign-created signature for interoperability, and that doesn’t work with matchExact. + testImage := dirImageMock(t, "fixtures/dir-img-cosign-valid", "192.168.64.2:5000/cosign-signed-single-sample") + testImageSigBlob, err := os.ReadFile("fixtures/dir-img-cosign-valid/signature-1") + require.NoError(t, err) + + // Successful validation, with KeyData and KeyPath + pr, err := newPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + sar, parsedSig, err := pr.isSignatureAuthorAccepted(context.Background(), testImage, testImageSigBlob) + assertSARRejected(t, sar, parsedSig, err) +} + +// sigstoreSignatureFromFile returns a signature.Sigstore loaded from path. +func sigstoreSignatureFromFile(t *testing.T, path string) signature.Sigstore { + blob, err := os.ReadFile(path) + require.NoError(t, err) + genericSig, err := signature.FromBlob(blob) + require.NoError(t, err) + sig, ok := genericSig.(signature.Sigstore) + require.True(t, ok) + return sig +} + +// sigstoreSignatureWithoutAnnotation returns a signature.Sigstore based on template +// that is missing the specified annotation. +func sigstoreSignatureWithoutAnnotation(t *testing.T, template signature.Sigstore, annotation string) signature.Sigstore { + annotations := template.UntrustedAnnotations() // This returns a copy that is safe to modify. + require.Contains(t, annotations, annotation) + delete(annotations, annotation) + return signature.SigstoreFromComponents(template.UntrustedMIMEType(), template.UntrustedPayload(), annotations) +} + +// sigstoreSignatureWithModifiedAnnotation returns a signature.Sigstore based on template +// where the specified annotation is replaced +func sigstoreSignatureWithModifiedAnnotation(template signature.Sigstore, annotation, value string) signature.Sigstore { + annotations := template.UntrustedAnnotations() // This returns a copy that is safe to modify. + annotations[annotation] = value + return signature.SigstoreFromComponents(template.UntrustedMIMEType(), template.UntrustedPayload(), annotations) +} + +func TestPRrSigstoreSignedIsSignatureAccepted(t *testing.T) { + assertAccepted := func(sar signatureAcceptanceResult, err error) { + assert.Equal(t, sarAccepted, sar) + assert.NoError(t, err) + } + assertRejected := func(sar signatureAcceptanceResult, err error) { + logrus.Errorf("%v", err) + assert.Equal(t, sarRejected, sar) + assert.Error(t, err) + } + + prm := NewPRMMatchRepository() // We prefer to test with a Cosign-created signature to ensure interoperability, and that doesn’t work with matchExact. matchExact is tested later. + testKeyImage := dirImageMock(t, "fixtures/dir-img-cosign-valid", "192.168.64.2:5000/cosign-signed-single-sample") + testKeyImageSig := sigstoreSignatureFromFile(t, "fixtures/dir-img-cosign-valid/signature-1") + testKeyRekorImage := dirImageMock(t, "fixtures/dir-img-cosign-key-rekor-valid", "192.168.64.2:5000/cosign-signed/key-1") + testKeyRekorImageSig := sigstoreSignatureFromFile(t, "fixtures/dir-img-cosign-key-rekor-valid/signature-1") + testFulcioRekorImage := dirImageMock(t, "fixtures/dir-img-cosign-fulcio-rekor-valid", "192.168.64.2:5000/cosign-signed/fulcio-rekor-1") + testFulcioRekorImageSig := sigstoreSignatureFromFile(t, "fixtures/dir-img-cosign-fulcio-rekor-valid/signature-1") + keyData, err := os.ReadFile("fixtures/cosign.pub") + require.NoError(t, err) + + // prepareTrustRoot fails + pr := &prSigstoreSigned{ + KeyPath: "fixtures/cosign.pub", + KeyData: keyData, + SignedIdentity: prm, + } + // Pass a nil pointer to, kind of, test that the return value does not depend on the image. + sar, err := pr.isSignatureAccepted(context.Background(), nil, testKeyImageSig) + assertRejected(sar, err) + + // Signature has no cryptographic signature + pr, err = newPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + // Pass a nil pointer to, kind of, test that the return value does not depend on the image. + sar, err = pr.isSignatureAccepted(context.Background(), nil, + signature.SigstoreFromComponents(testKeyImageSig.UntrustedMIMEType(), testKeyImageSig.UntrustedPayload(), nil)) + assertRejected(sar, err) + + // Neither a public key nor Fulcio is specified + pr = &prSigstoreSigned{ + SignedIdentity: prm, + } + // Pass a nil pointer to, kind of, test that the return value does not depend on the image. + sar, err = pr.isSignatureAccepted(context.Background(), nil, testKeyImageSig) + assertRejected(sar, err) + + // Both a public key and Fulcio is specified + fulcio, err := NewPRSigstoreSignedFulcio( + PRSigstoreSignedFulcioWithCAPath("fixtures/fulcio_v1.crt.pem"), + PRSigstoreSignedFulcioWithOIDCIssuer("https://github.com/login/oauth"), + PRSigstoreSignedFulcioWithSubjectEmail("mitr@redhat.com"), + ) + require.NoError(t, err) + pr = &prSigstoreSigned{ + KeyPath: "fixtures/cosign.pub", + Fulcio: fulcio, + SignedIdentity: prm, + } + // Pass a nil pointer to, kind of, test that the return value does not depend on the image. + sar, err = pr.isSignatureAccepted(context.Background(), nil, testKeyImageSig) + assertRejected(sar, err) + + // Successful key+Rekor use + pr, err = newPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign2.pub"), + PRSigstoreSignedWithRekorPublicKeyPath("fixtures/rekor.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + sar, err = pr.isSignatureAccepted(context.Background(), testKeyRekorImage, testKeyRekorImageSig) + assertAccepted(sar, err) + + // key+Rekor, missing Rekor SET annotation + sar, err = pr.isSignatureAccepted(context.Background(), nil, + sigstoreSignatureWithoutAnnotation(t, testKeyRekorImageSig, signature.SigstoreSETAnnotationKey)) + assertRejected(sar, err) + // Actual Rekor logic is unit-tested elsewhere, but smoke-test the basics: + // key+Rekor: Invalid Rekor SET + // Pass a nil pointer to, kind of, test that the return value does not depend on the image. + sar, err = pr.isSignatureAccepted(context.Background(), nil, + sigstoreSignatureWithModifiedAnnotation(testKeyRekorImageSig, signature.SigstoreSETAnnotationKey, + "this is not a valid SET")) + assertRejected(sar, err) + // Fulcio: A Rekor SET which we don’t accept (one of many reasons) + pr2, err := newPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign2.pub"), + PRSigstoreSignedWithRekorPublicKeyPath("fixtures/cosign.pub"), // not rekor.pub = a key mismatch + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + // Pass a nil pointer to, kind of, test that the return value does not depend on the image. + sar, err = pr2.isSignatureAccepted(context.Background(), nil, testKeyRekorImageSig) + assertRejected(sar, err) + + // Successful Fulcio certificate use + fulcio, err = NewPRSigstoreSignedFulcio( + PRSigstoreSignedFulcioWithCAPath("fixtures/fulcio_v1.crt.pem"), + PRSigstoreSignedFulcioWithOIDCIssuer("https://github.com/login/oauth"), + PRSigstoreSignedFulcioWithSubjectEmail("mitr@redhat.com"), + ) + require.NoError(t, err) + pr, err = newPRSigstoreSigned( + PRSigstoreSignedWithFulcio(fulcio), + PRSigstoreSignedWithRekorPublicKeyPath("fixtures/rekor.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + sar, err = pr.isSignatureAccepted(context.Background(), testFulcioRekorImage, + testFulcioRekorImageSig) + assertAccepted(sar, err) + + // Fulcio, no Rekor requirement + pr2 = &prSigstoreSigned{ + Fulcio: fulcio, + SignedIdentity: prm, + } + // Pass a nil pointer to, kind of, test that the return value does not depend on the image. + sar, err = pr2.isSignatureAccepted(context.Background(), nil, + sigstoreSignatureWithoutAnnotation(t, testFulcioRekorImageSig, signature.SigstoreSETAnnotationKey)) + assertRejected(sar, err) + // Fulcio, missing Rekor SET annotation + // Pass a nil pointer to, kind of, test that the return value does not depend on the image. + sar, err = pr.isSignatureAccepted(context.Background(), nil, + sigstoreSignatureWithoutAnnotation(t, testFulcioRekorImageSig, signature.SigstoreSETAnnotationKey)) + assertRejected(sar, err) + // Fulcio, missing certificate annotation + // Pass a nil pointer to, kind of, test that the return value does not depend on the image. + sar, err = pr.isSignatureAccepted(context.Background(), nil, + sigstoreSignatureWithoutAnnotation(t, testFulcioRekorImageSig, signature.SigstoreCertificateAnnotationKey)) + assertRejected(sar, err) + // Fulcio: missing certificate chain annotation causes the Cosign-issued signature to be rejected + // because there is no path to the trusted CA + // Pass a nil pointer to, kind of, test that the return value does not depend on the image. + sar, err = pr.isSignatureAccepted(context.Background(), nil, + sigstoreSignatureWithoutAnnotation(t, testFulcioRekorImageSig, signature.SigstoreIntermediateCertificateChainAnnotationKey)) + assertRejected(sar, err) + // … but a signature without the intermediate annotation is fine if the issuer is directly trusted + // (which we handle by trusing the intermediates) + fulcio2, err := NewPRSigstoreSignedFulcio( + PRSigstoreSignedFulcioWithCAData([]byte(testFulcioRekorImageSig.UntrustedAnnotations()[signature.SigstoreIntermediateCertificateChainAnnotationKey])), + PRSigstoreSignedFulcioWithOIDCIssuer("https://github.com/login/oauth"), + PRSigstoreSignedFulcioWithSubjectEmail("mitr@redhat.com"), + ) + require.NoError(t, err) + pr2, err = newPRSigstoreSigned( + PRSigstoreSignedWithFulcio(fulcio2), + PRSigstoreSignedWithRekorPublicKeyPath("fixtures/rekor.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + sar, err = pr2.isSignatureAccepted(context.Background(), testFulcioRekorImage, + sigstoreSignatureWithoutAnnotation(t, testFulcioRekorImageSig, signature.SigstoreIntermediateCertificateChainAnnotationKey)) + assertAccepted(sar, err) + // Actual Fulcio and Rekor logic is unit-tested elsewhere, but smoke-test the basics: + // Fulcio: Invalid Fulcio certificate + // Pass a nil pointer to, kind of, test that the return value does not depend on the image. + sar, err = pr.isSignatureAccepted(context.Background(), nil, + sigstoreSignatureWithModifiedAnnotation(testFulcioRekorImageSig, signature.SigstoreCertificateAnnotationKey, + "this is not a valid certificate")) + assertRejected(sar, err) + // Fulcio: A Fulcio certificate which we don’t accept (one of many reasons) + fulcio2, err = NewPRSigstoreSignedFulcio( + PRSigstoreSignedFulcioWithCAPath("fixtures/fulcio_v1.crt.pem"), + PRSigstoreSignedFulcioWithOIDCIssuer("https://github.com/login/oauth"), + PRSigstoreSignedFulcioWithSubjectEmail("this-does-not-match@example.com"), + ) + require.NoError(t, err) + pr2, err = newPRSigstoreSigned( + PRSigstoreSignedWithFulcio(fulcio2), + PRSigstoreSignedWithRekorPublicKeyPath("fixtures/rekor.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + // Pass a nil pointer to, kind of, test that the return value does not depend on the image. + sar, err = pr2.isSignatureAccepted(context.Background(), nil, testFulcioRekorImageSig) + assertRejected(sar, err) + // Fulcio: Invalid Rekor SET + // Pass a nil pointer to, kind of, test that the return value does not depend on the image. + sar, err = pr.isSignatureAccepted(context.Background(), nil, + sigstoreSignatureWithModifiedAnnotation(testFulcioRekorImageSig, signature.SigstoreSETAnnotationKey, + "this is not a valid SET")) + assertRejected(sar, err) + // Fulcio: A Rekor SET which we don’t accept (one of many reasons) + pr2, err = newPRSigstoreSigned( + PRSigstoreSignedWithFulcio(fulcio), + PRSigstoreSignedWithRekorPublicKeyPath("fixtures/cosign.pub"), // not rekor.pub = a key mismatch + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + // Pass a nil pointer to, kind of, test that the return value does not depend on the image. + sar, err = pr2.isSignatureAccepted(context.Background(), nil, testFulcioRekorImageSig) + assertRejected(sar, err) + + // Successful validation, with KeyData and KeyPath + pr, err = newPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + sar, err = pr.isSignatureAccepted(context.Background(), testKeyImage, testKeyImageSig) + assertAccepted(sar, err) + + pr, err = newPRSigstoreSigned( + PRSigstoreSignedWithKeyData(keyData), + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + sar, err = pr.isSignatureAccepted(context.Background(), testKeyImage, testKeyImageSig) + assertAccepted(sar, err) + + // A signature which does not verify + pr, err = newPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + // Pass a nil pointer to, kind of, test that the return value does not depend on the image. + sar, err = pr.isSignatureAccepted(context.Background(), nil, + signature.SigstoreFromComponents(testKeyImageSig.UntrustedMIMEType(), testKeyImageSig.UntrustedPayload(), map[string]string{ + signature.SigstoreSignatureAnnotationKey: base64.StdEncoding.EncodeToString([]byte("invalid signature")), + })) + assertRejected(sar, err) + + // A valid signature using an unknown key. + pr, err = newPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + // Pass a nil pointer to, kind of, test that the return value does not depend on the image. + sar, err = pr.isSignatureAccepted(context.Background(), nil, sigstoreSignatureFromFile(t, "fixtures/unknown-cosign-key.signature")) + assertRejected(sar, err) + + // A valid signature with a rejected identity. + nonmatchingPRM, err := NewPRMExactReference("this/doesnt:match") + require.NoError(t, err) + pr, err = newPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(nonmatchingPRM), + ) + require.NoError(t, err) + sar, err = pr.isSignatureAccepted(context.Background(), testKeyImage, testKeyImageSig) + assertRejected(sar, err) + + // Error reading image manifest + image := dirImageMock(t, "fixtures/dir-img-cosign-no-manifest", "192.168.64.2:5000/cosign-signed-single-sample") + pr, err = newPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + sar, err = pr.isSignatureAccepted(context.Background(), image, sigstoreSignatureFromFile(t, "fixtures/dir-img-cosign-no-manifest/signature-1")) + assertRejected(sar, err) + + // Error computing manifest digest + image = dirImageMock(t, "fixtures/dir-img-cosign-manifest-digest-error", "192.168.64.2:5000/cosign-signed-single-sample") + pr, err = newPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + sar, err = pr.isSignatureAccepted(context.Background(), image, sigstoreSignatureFromFile(t, "fixtures/dir-img-cosign-manifest-digest-error/signature-1")) + assertRejected(sar, err) + + // A valid signature with a non-matching manifest + image = dirImageMock(t, "fixtures/dir-img-cosign-modified-manifest", "192.168.64.2:5000/cosign-signed-single-sample") + pr, err = newPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + sar, err = pr.isSignatureAccepted(context.Background(), image, sigstoreSignatureFromFile(t, "fixtures/dir-img-cosign-modified-manifest/signature-1")) + assertRejected(sar, err) + + // Minimally check that the prmMatchExact also works as expected: + // - Signatures with a matching tag work + image = dirImageMock(t, "fixtures/dir-img-cosign-valid-with-tag", "192.168.64.2:5000/skopeo-signed:tag") + pr, err = newPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(NewPRMMatchExact()), + ) + require.NoError(t, err) + sar, err = pr.isSignatureAccepted(context.Background(), image, sigstoreSignatureFromFile(t, "fixtures/dir-img-cosign-valid-with-tag/signature-1")) + assertAccepted(sar, err) + // - Signatures with a non-matching tag are rejected + image = dirImageMock(t, "fixtures/dir-img-cosign-valid-with-tag", "192.168.64.2:5000/skopeo-signed:othertag") + pr, err = newPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(NewPRMMatchExact()), + ) + require.NoError(t, err) + sar, err = pr.isSignatureAccepted(context.Background(), image, sigstoreSignatureFromFile(t, "fixtures/dir-img-cosign-valid-with-tag/signature-1")) + assertRejected(sar, err) + // - Cosign-created signatures are rejected + pr, err = newPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(NewPRMMatchExact()), + ) + require.NoError(t, err) + sar, err = pr.isSignatureAccepted(context.Background(), testKeyImage, testKeyImageSig) + assertRejected(sar, err) +} + +func TestPRSigstoreSignedIsRunningImageAllowed(t *testing.T) { + prm := NewPRMMatchRepository() // We prefer to test with a Cosign-created signature to ensure interoperability, and that doesn’t work with matchExact. matchExact is tested later. + + // A simple success case: single valid signature. + image := dirImageMock(t, "fixtures/dir-img-cosign-valid", "192.168.64.2:5000/cosign-signed-single-sample") + pr, err := NewPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + allowed, err := pr.isRunningImageAllowed(context.Background(), image) + assertRunningAllowed(t, allowed, err) + + // Error reading signatures + invalidSigDir := createInvalidSigDir(t) + image = dirImageMock(t, invalidSigDir, "192.168.64.2:5000/cosign-signed-single-sample") + pr, err = NewPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + allowed, err = pr.isRunningImageAllowed(context.Background(), image) + assertRunningRejected(t, allowed, err) + + // No signatures + image = dirImageMock(t, "fixtures/dir-img-unsigned", "testing/manifest:latest") + pr, err = NewPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + allowed, err = pr.isRunningImageAllowed(context.Background(), image) + assertRunningRejected(t, allowed, err) + + // Only non-sigstore signatures + image = dirImageMock(t, "fixtures/dir-img-valid", "testing/manifest:latest") + pr, err = NewPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + allowed, err = pr.isRunningImageAllowed(context.Background(), image) + assertRunningRejected(t, allowed, err) + + // Only non-signature sigstore attachments + image = dirImageMock(t, "fixtures/dir-img-cosign-other-attachment", "testing/manifest:latest") + pr, err = NewPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + allowed, err = pr.isRunningImageAllowed(context.Background(), image) + assertRunningRejected(t, allowed, err) + + // 1 invalid signature: use dir-img-valid, but a non-matching Docker reference + image = dirImageMock(t, "fixtures/dir-img-cosign-valid", "testing/manifest:notlatest") + pr, err = NewPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + allowed, err = pr.isRunningImageAllowed(context.Background(), image) + assertRunningRejectedPolicyRequirement(t, allowed, err) + + // 2 valid signatures + image = dirImageMock(t, "fixtures/dir-img-cosign-valid-2", "192.168.64.2:5000/cosign-signed-single-sample") + pr, err = NewPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + allowed, err = pr.isRunningImageAllowed(context.Background(), image) + assertRunningAllowed(t, allowed, err) + + // One invalid, one valid signature (in this order) + image = dirImageMock(t, "fixtures/dir-img-cosign-mixed", "192.168.64.2:5000/cosign-signed-single-sample") + pr, err = NewPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + allowed, err = pr.isRunningImageAllowed(context.Background(), image) + assertRunningAllowed(t, allowed, err) + + // 2 invalid signajtures: use dir-img-cosign-valid-2, but a non-matching Docker reference + image = dirImageMock(t, "fixtures/dir-img-cosign-valid-2", "this/doesnt:match") + pr, err = NewPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(prm), + ) + require.NoError(t, err) + allowed, err = pr.isRunningImageAllowed(context.Background(), image) + assertRunningRejectedPolicyRequirement(t, allowed, err) + + // Minimally check that the prmMatchExact also works as expected: + // - Signatures with a matching tag work + image = dirImageMock(t, "fixtures/dir-img-cosign-valid-with-tag", "192.168.64.2:5000/skopeo-signed:tag") + pr, err = NewPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(NewPRMMatchExact()), + ) + require.NoError(t, err) + allowed, err = pr.isRunningImageAllowed(context.Background(), image) + assertRunningAllowed(t, allowed, err) + // - Signatures with a non-matching tag are rejected + image = dirImageMock(t, "fixtures/dir-img-cosign-valid-with-tag", "192.168.64.2:5000/skopeo-signed:othertag") + pr, err = NewPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(NewPRMMatchExact()), + ) + require.NoError(t, err) + allowed, err = pr.isRunningImageAllowed(context.Background(), image) + assertRunningRejectedPolicyRequirement(t, allowed, err) + // - Cosign-created signatures are rejected + image = dirImageMock(t, "fixtures/dir-img-cosign-valid", "192.168.64.2:5000/cosign-signed-single-sample") + pr, err = NewPRSigstoreSigned( + PRSigstoreSignedWithKeyPath("fixtures/cosign.pub"), + PRSigstoreSignedWithSignedIdentity(NewPRMMatchExact()), + ) + require.NoError(t, err) + allowed, err = pr.isRunningImageAllowed(context.Background(), image) + assertRunningRejectedPolicyRequirement(t, allowed, err) +} diff --git a/vendor/github.com/containers/image/v5/signature/policy_eval_simple_test.go b/vendor/github.com/containers/image/v5/signature/policy_eval_simple_test.go new file mode 100644 index 00000000000..3249090b563 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/policy_eval_simple_test.go @@ -0,0 +1,57 @@ +package signature + +import ( + "context" + "testing" + + "github.com/containers/image/v5/internal/testing/mocks" + "github.com/containers/image/v5/types" +) + +// nameOnlyImageMock is a mock of private.UnparsedImage which only allows transports.ImageName to work +type nameOnlyImageMock struct { + mocks.ForbiddenUnparsedImage +} + +func (nameOnlyImageMock) Reference() types.ImageReference { + return nameOnlyImageReferenceMock{s: "== StringWithinTransport mock"} +} + +// nameOnlyImageReferenceMock is a mock of types.ImageReference which only allows transports.ImageName to work, returning self. +type nameOnlyImageReferenceMock struct { + mocks.ForbiddenImageReference + s string +} + +func (ref nameOnlyImageReferenceMock) Transport() types.ImageTransport { + return mocks.NameImageTransport("== Transport mock") +} +func (ref nameOnlyImageReferenceMock) StringWithinTransport() string { + return ref.s +} + +func TestPRInsecureAcceptAnythingIsSignatureAuthorAccepted(t *testing.T) { + pr := NewPRInsecureAcceptAnything() + // Pass nil signature to, kind of, test that the return value does not depend on it. + sar, parsedSig, err := pr.isSignatureAuthorAccepted(context.Background(), nameOnlyImageMock{}, nil) + assertSARUnknown(t, sar, parsedSig, err) +} + +func TestPRInsecureAcceptAnythingIsRunningImageAllowed(t *testing.T) { + pr := NewPRInsecureAcceptAnything() + res, err := pr.isRunningImageAllowed(context.Background(), nameOnlyImageMock{}) + assertRunningAllowed(t, res, err) +} + +func TestPRRejectIsSignatureAuthorAccepted(t *testing.T) { + pr := NewPRReject() + // Pass nil signature to, kind of, test that the return value does not depend on it. + sar, parsedSig, err := pr.isSignatureAuthorAccepted(context.Background(), nameOnlyImageMock{}, nil) + assertSARRejectedPolicyRequirement(t, sar, parsedSig, err) +} + +func TestPRRejectIsRunningImageAllowed(t *testing.T) { + pr := NewPRReject() + res, err := pr.isRunningImageAllowed(context.Background(), nameOnlyImageMock{}) + assertRunningRejectedPolicyRequirement(t, res, err) +} diff --git a/vendor/github.com/containers/image/v5/signature/policy_eval_test.go b/vendor/github.com/containers/image/v5/signature/policy_eval_test.go new file mode 100644 index 00000000000..af3c3e4aec0 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/policy_eval_test.go @@ -0,0 +1,499 @@ +package signature + +import ( + "context" + "fmt" + "testing" + + "github.com/containers/image/v5/docker" + "github.com/containers/image/v5/docker/policyconfiguration" + "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/internal/private" + "github.com/containers/image/v5/internal/testing/mocks" + "github.com/containers/image/v5/transports" + "github.com/containers/image/v5/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPolicyRequirementError(t *testing.T) { + // A stupid test just to keep code coverage + s := "test" + err := PolicyRequirementError(s) + assert.Equal(t, s, err.Error()) +} + +func TestPolicyContextChangeState(t *testing.T) { + pc, err := NewPolicyContext(&Policy{Default: PolicyRequirements{NewPRReject()}}) + require.NoError(t, err) + defer func() { + err := pc.Destroy() + require.NoError(t, err) + }() + + require.Equal(t, pcReady, pc.state) + err = pc.changeState(pcReady, pcInUse) + require.NoError(t, err) + + err = pc.changeState(pcReady, pcInUse) + require.Error(t, err) + + // Return state to pcReady to allow pc.Destroy to clean up. + err = pc.changeState(pcInUse, pcReady) + require.NoError(t, err) +} + +func TestPolicyContextNewDestroy(t *testing.T) { + pc, err := NewPolicyContext(&Policy{Default: PolicyRequirements{NewPRReject()}}) + require.NoError(t, err) + assert.Equal(t, pcReady, pc.state) + + err = pc.Destroy() + require.NoError(t, err) + assert.Equal(t, pcDestroyed, pc.state) + + // Trying to destroy when not pcReady + pc, err = NewPolicyContext(&Policy{Default: PolicyRequirements{NewPRReject()}}) + require.NoError(t, err) + err = pc.changeState(pcReady, pcInUse) + require.NoError(t, err) + err = pc.Destroy() + require.Error(t, err) + assert.Equal(t, pcInUse, pc.state) // The state, and hopefully nothing else, has changed. + + err = pc.changeState(pcInUse, pcReady) + require.NoError(t, err) + err = pc.Destroy() + assert.NoError(t, err) +} + +// pcImageReferenceMock is a mock of types.ImageReference which returns itself in DockerReference +// and handles PolicyConfigurationIdentity and PolicyConfigurationReference consistently. +type pcImageReferenceMock struct { + mocks.ForbiddenImageReference + transportName string + ref reference.Named +} + +func (ref pcImageReferenceMock) Transport() types.ImageTransport { + return mocks.NameImageTransport(ref.transportName) +} +func (ref pcImageReferenceMock) StringWithinTransport() string { + // We use this in error messages, so sadly we must return something. + return "== StringWithinTransport mock" +} +func (ref pcImageReferenceMock) DockerReference() reference.Named { + return ref.ref +} +func (ref pcImageReferenceMock) PolicyConfigurationIdentity() string { + res, err := policyconfiguration.DockerReferenceIdentity(ref.ref) + if res == "" || err != nil { + panic(fmt.Sprintf("Internal inconsistency: policyconfiguration.DockerReferenceIdentity returned %#v, %v", res, err)) + } + return res +} +func (ref pcImageReferenceMock) PolicyConfigurationNamespaces() []string { + if ref.ref == nil { + panic("unexpected call to a mock function") + } + return policyconfiguration.DockerReferenceNamespaces(ref.ref) +} + +func TestPolicyContextRequirementsForImageRefNotRegisteredTransport(t *testing.T) { + transports.Delete("docker") + assert.Nil(t, transports.Get("docker")) + + defer func() { + assert.Nil(t, transports.Get("docker")) + transports.Register(docker.Transport) + assert.NotNil(t, transports.Get("docker")) + }() + + pr := []PolicyRequirement{ + xNewPRSignedByKeyData(SBKeyTypeSignedByGPGKeys, []byte("RH"), NewPRMMatchRepository()), + } + policy := &Policy{ + Default: PolicyRequirements{NewPRReject()}, + Transports: map[string]PolicyTransportScopes{ + "docker": { + "registry.access.redhat.com": pr, + }, + }, + } + pc, err := NewPolicyContext(policy) + require.NoError(t, err) + ref, err := reference.ParseNormalizedNamed("registry.access.redhat.com/rhel7:latest") + require.NoError(t, err) + reqs := pc.requirementsForImageRef(pcImageReferenceMock{transportName: "docker", ref: ref}) + assert.True(t, &(reqs[0]) == &(pr[0])) + assert.True(t, len(reqs) == len(pr)) + +} + +func TestPolicyContextRequirementsForImageRef(t *testing.T) { + ktGPG := SBKeyTypeGPGKeys + prm := NewPRMMatchRepoDigestOrExact() + + policy := &Policy{ + Default: PolicyRequirements{NewPRReject()}, + Transports: map[string]PolicyTransportScopes{}, + } + // Just put _something_ into the PolicyTransportScopes map for the keys we care about, and make it pairwise + // distinct so that we can compare the values and show them when debugging the tests. + for _, t := range []struct{ transport, scope string }{ + {"docker", ""}, + {"docker", "unmatched"}, + {"docker", "deep.com"}, + {"docker", "*.very.deep.com"}, + {"docker", "*.deep.com"}, + {"docker", "deep.com/n1"}, + {"docker", "deep.com/n1/n2"}, + {"docker", "deep.com/n1/n2/n3"}, + {"docker", "deep.com/n1/n2/n3/repo"}, + {"docker", "deep.com/n1/n2/n3/repo:tag2"}, + {"atomic", "unmatched"}, + } { + if _, ok := policy.Transports[t.transport]; !ok { + policy.Transports[t.transport] = PolicyTransportScopes{} + } + policy.Transports[t.transport][t.scope] = PolicyRequirements{xNewPRSignedByKeyData(ktGPG, []byte(t.transport+t.scope), prm)} + } + + pc, err := NewPolicyContext(policy) + require.NoError(t, err) + + for _, c := range []struct{ inputTransport, input, matchedTransport, matched string }{ + // Full match + {"docker", "deep.com/n1/n2/n3/repo:tag2", "docker", "deep.com/n1/n2/n3/repo:tag2"}, + // Namespace matches + {"docker", "deep.com/n1/n2/n3/repo:nottag2", "docker", "deep.com/n1/n2/n3/repo"}, + {"docker", "deep.com/n1/n2/n3/notrepo:tag2", "docker", "deep.com/n1/n2/n3"}, + {"docker", "deep.com/n1/n2/notn3/repo:tag2", "docker", "deep.com/n1/n2"}, + {"docker", "deep.com/n1/notn2/n3/repo:tag2", "docker", "deep.com/n1"}, + // Host name match + {"docker", "deep.com/notn1/n2/n3/repo:tag2", "docker", "deep.com"}, + // Sub domain match + {"docker", "very.deep.com/n1/n2/n3/repo:tag2", "docker", "*.deep.com"}, + {"docker", "not.very.deep.com/n1/n2/n3/repo:tag2", "docker", "*.very.deep.com"}, + // Default + {"docker", "this.does-not/match:anything", "docker", ""}, + // No match within a matched transport which doesn't have a "" scope + {"atomic", "this.does-not/match:anything", "", ""}, + // No configuration available for this transport at all + {"dir", "what/ever", "", ""}, // "what/ever" is not a valid scope for the real "dir" transport, but we only need it to be a valid reference.Named. + } { + var expected PolicyRequirements + if c.matchedTransport != "" { + e, ok := policy.Transports[c.matchedTransport][c.matched] + require.True(t, ok, fmt.Sprintf("case %s:%s: expected reqs not found", c.inputTransport, c.input)) + expected = e + } else { + expected = policy.Default + } + + ref, err := reference.ParseNormalizedNamed(c.input) + require.NoError(t, err) + reqs := pc.requirementsForImageRef(pcImageReferenceMock{transportName: c.inputTransport, ref: ref}) + comment := fmt.Sprintf("case %s:%s: %#v", c.inputTransport, c.input, reqs[0]) + // Do not use assert.Equal, which would do a deep contents comparison; we want to compare + // the pointers. Also, == does not work on slices; so test that the slices start at the + // same element and have the same length. + assert.True(t, &(reqs[0]) == &(expected[0]), comment) + assert.True(t, len(reqs) == len(expected), comment) + } +} + +// pcImageMock returns a private.UnparsedImage for a directory, claiming a specified dockerReference and implementing PolicyConfigurationIdentity/PolicyConfigurationNamespaces. +func pcImageMock(t *testing.T, dir, dockerReference string) private.UnparsedImage { + ref, err := reference.ParseNormalizedNamed(dockerReference) + require.NoError(t, err) + return dirImageMockWithRef(t, dir, pcImageReferenceMock{transportName: "docker", ref: ref}) +} + +func TestPolicyContextGetSignaturesWithAcceptedAuthor(t *testing.T) { + expectedSig := &Signature{ + DockerManifestDigest: TestImageManifestDigest, + DockerReference: "testing/manifest:latest", + } + + pc, err := NewPolicyContext(&Policy{ + Default: PolicyRequirements{NewPRReject()}, + Transports: map[string]PolicyTransportScopes{ + "docker": { + "docker.io/testing/manifest:latest": { + xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchExact()), + }, + "docker.io/testing/manifest:twoAccepts": { + xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchRepository()), + xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchRepository()), + }, + "docker.io/testing/manifest:acceptReject": { + xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchRepository()), + NewPRReject(), + }, + "docker.io/testing/manifest:acceptUnknown": { + xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchRepository()), + xNewPRSignedBaseLayer(NewPRMMatchRepository()), + }, + "docker.io/testing/manifest:rejectUnknown": { + NewPRReject(), + xNewPRSignedBaseLayer(NewPRMMatchRepository()), + }, + "docker.io/testing/manifest:unknown": { + xNewPRSignedBaseLayer(NewPRMMatchRepository()), + }, + "docker.io/testing/manifest:unknown2": { + NewPRInsecureAcceptAnything(), + }, + "docker.io/testing/manifest:invalidEmptyRequirements": {}, + }, + }, + }) + require.NoError(t, err) + defer func() { + err := pc.Destroy() + require.NoError(t, err) + }() + + // Success + img := pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:latest") + sigs, err := pc.GetSignaturesWithAcceptedAuthor(context.Background(), img) + require.NoError(t, err) + assert.Equal(t, []*Signature{expectedSig}, sigs) + + // Two signatures + // FIXME? Use really different signatures for this? + img = pcImageMock(t, "fixtures/dir-img-valid-2", "testing/manifest:latest") + sigs, err = pc.GetSignaturesWithAcceptedAuthor(context.Background(), img) + require.NoError(t, err) + assert.Equal(t, []*Signature{expectedSig, expectedSig}, sigs) + + // No signatures + img = pcImageMock(t, "fixtures/dir-img-unsigned", "testing/manifest:latest") + sigs, err = pc.GetSignaturesWithAcceptedAuthor(context.Background(), img) + require.NoError(t, err) + assert.Empty(t, sigs) + + // Only invalid signatures + img = pcImageMock(t, "fixtures/dir-img-modified-manifest", "testing/manifest:latest") + sigs, err = pc.GetSignaturesWithAcceptedAuthor(context.Background(), img) + require.NoError(t, err) + assert.Empty(t, sigs) + + // 1 invalid, 1 valid signature (in this order) + img = pcImageMock(t, "fixtures/dir-img-mixed", "testing/manifest:latest") + sigs, err = pc.GetSignaturesWithAcceptedAuthor(context.Background(), img) + require.NoError(t, err) + assert.Equal(t, []*Signature{expectedSig}, sigs) + + // Two sarAccepted results for one signature + img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:twoAccepts") + sigs, err = pc.GetSignaturesWithAcceptedAuthor(context.Background(), img) + require.NoError(t, err) + assert.Equal(t, []*Signature{expectedSig}, sigs) + + // sarAccepted+sarRejected for a signature + img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:acceptReject") + sigs, err = pc.GetSignaturesWithAcceptedAuthor(context.Background(), img) + require.NoError(t, err) + assert.Empty(t, sigs) + + // sarAccepted+sarUnknown for a signature + img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:acceptUnknown") + sigs, err = pc.GetSignaturesWithAcceptedAuthor(context.Background(), img) + require.NoError(t, err) + assert.Equal(t, []*Signature{expectedSig}, sigs) + + // sarRejected+sarUnknown for a signature + img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:rejectUnknown") + sigs, err = pc.GetSignaturesWithAcceptedAuthor(context.Background(), img) + require.NoError(t, err) + assert.Empty(t, sigs) + + // sarUnknown only + img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:unknown") + sigs, err = pc.GetSignaturesWithAcceptedAuthor(context.Background(), img) + require.NoError(t, err) + assert.Empty(t, sigs) + + img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:unknown2") + sigs, err = pc.GetSignaturesWithAcceptedAuthor(context.Background(), img) + require.NoError(t, err) + assert.Empty(t, sigs) + + // Empty list of requirements (invalid) + img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:invalidEmptyRequirements") + sigs, err = pc.GetSignaturesWithAcceptedAuthor(context.Background(), img) + require.NoError(t, err) + assert.Empty(t, sigs) + + // Failures: Make sure we return nil sigs. + + // Unexpected state (context already destroyed) + destroyedPC, err := NewPolicyContext(pc.Policy) + require.NoError(t, err) + err = destroyedPC.Destroy() + require.NoError(t, err) + img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:latest") + sigs, err = destroyedPC.GetSignaturesWithAcceptedAuthor(context.Background(), img) + assert.Error(t, err) + assert.Nil(t, sigs) + // Not testing the pcInUse->pcReady transition, that would require custom PolicyRequirement + // implementations meddling with the state, or threads. This is for catching trivial programmer + // mistakes only, anyway. + + // Error reading signatures. + invalidSigDir := createInvalidSigDir(t) + img = pcImageMock(t, invalidSigDir, "testing/manifest:latest") + sigs, err = pc.GetSignaturesWithAcceptedAuthor(context.Background(), img) + assert.Error(t, err) + assert.Nil(t, sigs) +} + +func TestPolicyContextIsRunningImageAllowed(t *testing.T) { + pc, err := NewPolicyContext(&Policy{ + Default: PolicyRequirements{NewPRReject()}, + Transports: map[string]PolicyTransportScopes{ + "docker": { + "docker.io/testing/manifest:latest": { + xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchExact()), + }, + "docker.io/testing/manifest:twoAllows": { + xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchRepository()), + xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchRepository()), + }, + "docker.io/testing/manifest:allowDeny": { + xNewPRSignedByKeyPath(SBKeyTypeGPGKeys, "fixtures/public-key.gpg", NewPRMMatchRepository()), + NewPRReject(), + }, + "docker.io/testing/manifest:reject": { + NewPRReject(), + }, + "docker.io/testing/manifest:acceptAnything": { + NewPRInsecureAcceptAnything(), + }, + "docker.io/testing/manifest:invalidEmptyRequirements": {}, + }, + }, + }) + require.NoError(t, err) + defer func() { + err := pc.Destroy() + require.NoError(t, err) + }() + + // Success + img := pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:latest") + res, err := pc.IsRunningImageAllowed(context.Background(), img) + assertRunningAllowed(t, res, err) + + // Two signatures + // FIXME? Use really different signatures for this? + img = pcImageMock(t, "fixtures/dir-img-valid-2", "testing/manifest:latest") + res, err = pc.IsRunningImageAllowed(context.Background(), img) + assertRunningAllowed(t, res, err) + + // No signatures + img = pcImageMock(t, "fixtures/dir-img-unsigned", "testing/manifest:latest") + res, err = pc.IsRunningImageAllowed(context.Background(), img) + assertRunningRejectedPolicyRequirement(t, res, err) + + // Only invalid signatures + img = pcImageMock(t, "fixtures/dir-img-modified-manifest", "testing/manifest:latest") + res, err = pc.IsRunningImageAllowed(context.Background(), img) + assertRunningRejectedPolicyRequirement(t, res, err) + + // 1 invalid, 1 valid signature (in this order) + img = pcImageMock(t, "fixtures/dir-img-mixed", "testing/manifest:latest") + res, err = pc.IsRunningImageAllowed(context.Background(), img) + assertRunningAllowed(t, res, err) + + // Two allowed results + img = pcImageMock(t, "fixtures/dir-img-mixed", "testing/manifest:twoAllows") + res, err = pc.IsRunningImageAllowed(context.Background(), img) + assertRunningAllowed(t, res, err) + + // Allow + deny results + img = pcImageMock(t, "fixtures/dir-img-mixed", "testing/manifest:allowDeny") + res, err = pc.IsRunningImageAllowed(context.Background(), img) + assertRunningRejectedPolicyRequirement(t, res, err) + + // prReject works + img = pcImageMock(t, "fixtures/dir-img-mixed", "testing/manifest:reject") + res, err = pc.IsRunningImageAllowed(context.Background(), img) + assertRunningRejectedPolicyRequirement(t, res, err) + + // prInsecureAcceptAnything works + img = pcImageMock(t, "fixtures/dir-img-mixed", "testing/manifest:acceptAnything") + res, err = pc.IsRunningImageAllowed(context.Background(), img) + assertRunningAllowed(t, res, err) + + // Empty list of requirements (invalid) + img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:invalidEmptyRequirements") + res, err = pc.IsRunningImageAllowed(context.Background(), img) + assertRunningRejectedPolicyRequirement(t, res, err) + + // Unexpected state (context already destroyed) + destroyedPC, err := NewPolicyContext(pc.Policy) + require.NoError(t, err) + err = destroyedPC.Destroy() + require.NoError(t, err) + img = pcImageMock(t, "fixtures/dir-img-valid", "testing/manifest:latest") + res, err = destroyedPC.IsRunningImageAllowed(context.Background(), img) + assertRunningRejected(t, res, err) + // Not testing the pcInUse->pcReady transition, that would require custom PolicyRequirement + // implementations meddling with the state, or threads. This is for catching trivial programmer + // mistakes only, anyway. +} + +// Helpers for validating PolicyRequirement.isSignatureAuthorAccepted results: + +// assertSARRejected verifies that isSignatureAuthorAccepted returns a consistent sarRejected result +// with the expected signature. +func assertSARAccepted(t *testing.T, sar signatureAcceptanceResult, parsedSig *Signature, err error, expectedSig Signature) { + assert.Equal(t, sarAccepted, sar) + assert.Equal(t, &expectedSig, parsedSig) + assert.NoError(t, err) +} + +// assertSARRejected verifies that isSignatureAuthorAccepted returns a consistent sarRejected result. +func assertSARRejected(t *testing.T, sar signatureAcceptanceResult, parsedSig *Signature, err error) { + assert.Equal(t, sarRejected, sar) + assert.Nil(t, parsedSig) + assert.Error(t, err) +} + +// assertSARRejectedPolicyRequirement verifies that isSignatureAuthorAccepted returns a consistent sarRejected result, +// and that the returned error is a PolicyRequirementError.. +func assertSARRejectedPolicyRequirement(t *testing.T, sar signatureAcceptanceResult, parsedSig *Signature, err error) { + assertSARRejected(t, sar, parsedSig, err) + assert.IsType(t, PolicyRequirementError(""), err) +} + +// assertSARRejected verifies that isSignatureAuthorAccepted returns a consistent sarUnknown result. +func assertSARUnknown(t *testing.T, sar signatureAcceptanceResult, parsedSig *Signature, err error) { + assert.Equal(t, sarUnknown, sar) + assert.Nil(t, parsedSig) + assert.NoError(t, err) +} + +// Helpers for validating PolicyRequirement.isRunningImageAllowed results: + +// assertRunningAllowed verifies that isRunningImageAllowed returns a consistent true result +func assertRunningAllowed(t *testing.T, allowed bool, err error) { + assert.Equal(t, true, allowed) + assert.NoError(t, err) +} + +// assertRunningRejected verifies that isRunningImageAllowed returns a consistent false result +func assertRunningRejected(t *testing.T, allowed bool, err error) { + assert.Equal(t, false, allowed) + assert.Error(t, err) +} + +// assertRunningRejectedPolicyRequirement verifies that isRunningImageAllowed returns a consistent false result +// and that the returned error is a PolicyRequirementError. +func assertRunningRejectedPolicyRequirement(t *testing.T, allowed bool, err error) { + assertRunningRejected(t, allowed, err) + assert.IsType(t, PolicyRequirementError(""), err) +} diff --git a/vendor/github.com/containers/image/v5/signature/policy_reference_match_test.go b/vendor/github.com/containers/image/v5/signature/policy_reference_match_test.go new file mode 100644 index 00000000000..8707732ee31 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/policy_reference_match_test.go @@ -0,0 +1,556 @@ +package signature + +import ( + "fmt" + "testing" + + "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/internal/testing/mocks" + "github.com/containers/image/v5/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + fullRHELRef = "registry.access.redhat.com/rhel7/rhel:7.2.3" + untaggedRHELRef = "registry.access.redhat.com/rhel7/rhel" + digestSuffix = "@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + digestSuffixOther = "@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +) + +func TestParseImageAndDockerReference(t *testing.T) { + const ( + ok1 = "busybox" + ok2 = fullRHELRef + bad1 = "UPPERCASE_IS_INVALID_IN_DOCKER_REFERENCES" + bad2 = "" + ) + // Success + ref, err := reference.ParseNormalizedNamed(ok1) + require.NoError(t, err) + r1, r2, err := parseImageAndDockerReference(refImageMock{ref: ref}, ok2) + require.NoError(t, err) + assert.Equal(t, ok1, reference.FamiliarString(r1)) + assert.Equal(t, ok2, reference.FamiliarString(r2)) + + // Unidentified images are rejected. + _, _, err = parseImageAndDockerReference(refImageMock{ref: nil}, ok2) + require.Error(t, err) + assert.IsType(t, PolicyRequirementError(""), err) + + // Failures + for _, refs := range [][]string{ + {bad1, ok2}, + {ok1, bad2}, + {bad1, bad2}, + } { + ref, err := reference.ParseNormalizedNamed(refs[0]) + if err == nil { + _, _, err := parseImageAndDockerReference(refImageMock{ref: ref}, refs[1]) + assert.Error(t, err) + } + } +} + +// refImageMock is a mock of private.UnparsedImage which returns itself in Reference().DockerReference. +type refImageMock struct { + mocks.ForbiddenUnparsedImage + ref reference.Named +} + +func (ref refImageMock) Reference() types.ImageReference { + return refImageReferenceMock{ref: ref.ref} +} + +// refImageReferenceMock is a mock of types.ImageReference which returns itself in DockerReference. +type refImageReferenceMock struct { + mocks.ForbiddenImageReference + ref reference.Named +} + +func (ref refImageReferenceMock) Transport() types.ImageTransport { + return mocks.NameImageTransport("== Transport mock") +} +func (ref refImageReferenceMock) StringWithinTransport() string { + // We use this in error messages, so sadly we must return something. But right now we do so only when DockerReference is nil, so restrict to that. + if ref.ref == nil { + return "== StringWithinTransport for an image with no Docker support" + } + panic("unexpected call to a mock function") +} +func (ref refImageReferenceMock) DockerReference() reference.Named { + return ref.ref +} + +type prmSymmetricTableTest struct { + refA, refB string + result bool +} + +// Test cases for exact reference match. The behavior is supposed to be symmetric. +var prmExactMatchTestTable = []prmSymmetricTableTest{ + // Success, simple matches + {"busybox:latest", "busybox:latest", true}, + {fullRHELRef, fullRHELRef, true}, + {"busybox" + digestSuffix, "busybox" + digestSuffix, true}, // NOTE: This is not documented; signing digests is not recommended at this time. + // Non-canonical reference format is canonicalized + {"library/busybox:latest", "busybox:latest", true}, + {"docker.io/library/busybox:latest", "busybox:latest", true}, + {"library/busybox" + digestSuffix, "busybox" + digestSuffix, true}, + // Mismatch + {"busybox:latest", "busybox:notlatest", false}, + {"busybox:latest", "notbusybox:latest", false}, + {"busybox:latest", "hostname/library/busybox:notlatest", false}, + {"hostname/library/busybox:latest", "busybox:notlatest", false}, + {"busybox:latest", fullRHELRef, false}, + {"busybox" + digestSuffix, "notbusybox" + digestSuffix, false}, + {"busybox:latest", "busybox" + digestSuffix, false}, + {"busybox" + digestSuffix, "busybox" + digestSuffixOther, false}, + // NameOnly references + {"busybox", "busybox:latest", false}, + {"busybox", "busybox" + digestSuffix, false}, + {"busybox", "busybox", false}, + // References with both tags and digests: We match them exactly (requiring BOTH to match) + // NOTE: Again, this is not documented behavior; the recommendation is to sign tags, not digests, and then tag-and-digest references won’t match the signed identity. + {"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffix, true}, + {"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffixOther, false}, + {"busybox:latest" + digestSuffix, "busybox:notlatest" + digestSuffix, false}, + {"busybox:latest" + digestSuffix, "busybox" + digestSuffix, false}, + {"busybox:latest" + digestSuffix, "busybox:latest", false}, + // Invalid format + {"UPPERCASE_IS_INVALID_IN_DOCKER_REFERENCES", "busybox:latest", false}, + {"", "UPPERCASE_IS_INVALID_IN_DOCKER_REFERENCES", false}, + // Even if they are exactly equal, invalid values are rejected. + {"INVALID", "INVALID", false}, +} + +// Test cases for repository-only reference match. The behavior is supposed to be symmetric. +var prmRepositoryMatchTestTable = []prmSymmetricTableTest{ + // Success, simple matches + {"busybox:latest", "busybox:latest", true}, + {fullRHELRef, fullRHELRef, true}, + {"busybox" + digestSuffix, "busybox" + digestSuffix, true}, // NOTE: This is not documented; signing digests is not recommended at this time. + // Non-canonical reference format is canonicalized + {"library/busybox:latest", "busybox:latest", true}, + {"docker.io/library/busybox:latest", "busybox:latest", true}, + {"library/busybox" + digestSuffix, "busybox" + digestSuffix, true}, + // The same as above, but with mismatching tags + {"busybox:latest", "busybox:notlatest", true}, + {fullRHELRef + "tagsuffix", fullRHELRef, true}, + {"library/busybox:latest", "busybox:notlatest", true}, + {"busybox:latest", "library/busybox:notlatest", true}, + {"docker.io/library/busybox:notlatest", "busybox:latest", true}, + {"busybox:notlatest", "docker.io/library/busybox:latest", true}, + {"busybox:latest", "busybox" + digestSuffix, true}, + {"busybox" + digestSuffix, "busybox" + digestSuffixOther, true}, // Even this is accepted here. (This could more reasonably happen with two different digest algorithms.) + // The same as above, but with defaulted tags (which can happen with /usr/bin/cosign) + {"busybox", "busybox:notlatest", true}, + {fullRHELRef, untaggedRHELRef, true}, + {"busybox", "busybox" + digestSuffix, true}, + {"library/busybox", "busybox", true}, + {"docker.io/library/busybox", "busybox", true}, + // Mismatch + {"busybox:latest", "notbusybox:latest", false}, + {"hostname/library/busybox:latest", "busybox:notlatest", false}, + {"busybox:latest", fullRHELRef, false}, + {"busybox" + digestSuffix, "notbusybox" + digestSuffix, false}, + // References with both tags and digests: We ignore both anyway. + {"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffix, true}, + {"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffixOther, true}, + {"busybox:latest" + digestSuffix, "busybox:notlatest" + digestSuffix, true}, + {"busybox:latest" + digestSuffix, "busybox" + digestSuffix, true}, + {"busybox:latest" + digestSuffix, "busybox:latest", true}, + // Invalid format + {"UPPERCASE_IS_INVALID_IN_DOCKER_REFERENCES", "busybox:latest", false}, + {"", "UPPERCASE_IS_INVALID_IN_DOCKER_REFERENCES", false}, + // Even if they are exactly equal, invalid values are rejected. + {"INVALID", "INVALID", false}, +} + +// Test cases for matchRepoDigestOrExact +var matchRepoDigestOrExactTestTable = []struct { + imageRef, sigRef string + result bool +}{ + // Tag mismatch + {"busybox:latest", "busybox:notlatest", false}, + {fullRHELRef + "tagsuffix", fullRHELRef, false}, + {"library/busybox:latest", "busybox:notlatest", false}, + {"busybox:latest", "library/busybox:notlatest", false}, + {"docker.io/library/busybox:notlatest", "busybox:latest", false}, + {"busybox:notlatest", "docker.io/library/busybox:latest", false}, + // NameOnly references + {"busybox", "busybox:latest", false}, + {"busybox:latest", "busybox", false}, + {"busybox", "busybox" + digestSuffix, false}, + {"busybox" + digestSuffix, "busybox", false}, + {fullRHELRef, untaggedRHELRef, false}, + {"busybox", "busybox", false}, + // Tag references only accept signatures with matching tags. + {"busybox:latest", "busybox" + digestSuffix, false}, + // Digest references accept any signature with matching repository. + {"busybox" + digestSuffix, "busybox:latest", true}, + {"busybox" + digestSuffix, "busybox" + digestSuffixOther, true}, // Even this is accepted here. (This could more reasonably happen with two different digest algorithms.) + // References with both tags and digests: We match them exactly (requiring BOTH to match). + {"busybox:latest" + digestSuffix, "busybox:latest", false}, + {"busybox:latest" + digestSuffix, "busybox:notlatest", false}, + {"busybox:latest", "busybox:latest" + digestSuffix, false}, + {"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffixOther, false}, + {"busybox:latest" + digestSuffix, "busybox:notlatest" + digestSuffixOther, false}, +} + +func testImageAndSig(t *testing.T, prm PolicyReferenceMatch, imageRef, sigRef string, result bool) { + // This assumes that all ways to obtain a reference.Named perform equivalent validation, + // and therefore values refused by reference.ParseNormalizedNamed can not happen in practice. + parsedImageRef, err := reference.ParseNormalizedNamed(imageRef) + require.NoError(t, err) + res := prm.matchesDockerReference(refImageMock{ref: parsedImageRef}, sigRef) + assert.Equal(t, result, res, fmt.Sprintf("%s vs. %s", imageRef, sigRef)) +} + +// testPossiblyInvalidImageAndSig is a variant of testImageAndSig +// that does not fail if the imageRef is invalid (which should never happen in practice, +// but makes testing of symmetrical properties using shared tables easier) +func testPossiblyInvalidImageAndSig(t *testing.T, prm PolicyReferenceMatch, imageRef, sigRef string, result bool) { + // This assumes that all ways to obtain a reference.Named perform equivalent validation, + // and therefore values refused by reference.ParseNormalizedNamed can not happen in practice. + _, err := reference.ParseNormalizedNamed(imageRef) + if err != nil { + return + } + testImageAndSig(t, prm, imageRef, sigRef, result) +} + +func TestMatchRepoDigestOrExactReferenceValues(t *testing.T) { + // prmMatchRepoDigestOrExact is a middle ground between prmMatchExact and prmMatchRepository: + // It accepts anything prmMatchExact accepts,… + for _, test := range prmExactMatchTestTable { + if test.result == true { + refA, errA := reference.ParseNormalizedNamed(test.refA) + refB, errB := reference.ParseNormalizedNamed(test.refB) + if errA == nil && errB == nil { + res1 := matchRepoDigestOrExactReferenceValues(refA, refB) + assert.Equal(t, test.result, res1) + res2 := matchRepoDigestOrExactReferenceValues(refB, refA) + assert.Equal(t, test.result, res2) + } + } + } + // … and it rejects everything prmMatchRepository rejects. + for _, test := range prmRepositoryMatchTestTable { + if test.result == false { + refA, errA := reference.ParseNormalizedNamed(test.refA) + refB, errB := reference.ParseNormalizedNamed(test.refB) + if errA == nil && errB == nil { + res1 := matchRepoDigestOrExactReferenceValues(refA, refB) + assert.Equal(t, test.result, res1) + res2 := matchRepoDigestOrExactReferenceValues(refB, refA) + assert.Equal(t, test.result, res2) + } + } + } + + // The other cases, possibly asymmetrical: + for _, test := range matchRepoDigestOrExactTestTable { + imageRef, err := reference.ParseNormalizedNamed(test.imageRef) + require.NoError(t, err) + sigRef, err := reference.ParseNormalizedNamed(test.sigRef) + require.NoError(t, err) + res := matchRepoDigestOrExactReferenceValues(imageRef, sigRef) + assert.Equal(t, test.result, res) + } +} + +func TestPRMMatchExactMatchesDockerReference(t *testing.T) { + prm := NewPRMMatchExact() + for _, test := range prmExactMatchTestTable { + testPossiblyInvalidImageAndSig(t, prm, test.refA, test.refB, test.result) + testPossiblyInvalidImageAndSig(t, prm, test.refB, test.refA, test.result) + } + // Even if they are signed with an empty string as a reference, unidentified images are rejected. + res := prm.matchesDockerReference(refImageMock{ref: nil}, "") + assert.False(t, res, `unidentified vs. ""`) +} + +func TestPRMMatchRepoDigestOrExactMatchesDockerReference(t *testing.T) { + prm := NewPRMMatchRepoDigestOrExact() + + // prmMatchRepoDigestOrExact is a middle ground between prmMatchExact and prmMatchRepository: + // It accepts anything prmMatchExact accepts,… + for _, test := range prmExactMatchTestTable { + if test.result == true { + testPossiblyInvalidImageAndSig(t, prm, test.refA, test.refB, test.result) + testPossiblyInvalidImageAndSig(t, prm, test.refB, test.refA, test.result) + } + } + // … and it rejects everything prmMatchRepository rejects. + for _, test := range prmRepositoryMatchTestTable { + if test.result == false { + testPossiblyInvalidImageAndSig(t, prm, test.refA, test.refB, test.result) + testPossiblyInvalidImageAndSig(t, prm, test.refB, test.refA, test.result) + } + } + + // The other cases, possibly asymmetrical: + for _, test := range matchRepoDigestOrExactTestTable { + testImageAndSig(t, prm, test.imageRef, test.sigRef, test.result) + } +} + +func TestPRMMatchRepositoryMatchesDockerReference(t *testing.T) { + prm := NewPRMMatchRepository() + for _, test := range prmRepositoryMatchTestTable { + testPossiblyInvalidImageAndSig(t, prm, test.refA, test.refB, test.result) + testPossiblyInvalidImageAndSig(t, prm, test.refB, test.refA, test.result) + } + // Even if they are signed with an empty string as a reference, unidentified images are rejected. + res := prm.matchesDockerReference(refImageMock{ref: nil}, "") + assert.False(t, res, `unidentified vs. ""`) +} + +func TestParseDockerReferences(t *testing.T) { + const ( + ok1 = "busybox" + ok2 = fullRHELRef + bad1 = "UPPERCASE_IS_INVALID_IN_DOCKER_REFERENCES" + bad2 = "" + ) + + // Success + r1, r2, err := parseDockerReferences(ok1, ok2) + require.NoError(t, err) + assert.Equal(t, ok1, reference.FamiliarString(r1)) + assert.Equal(t, ok2, reference.FamiliarString(r2)) + + // Failures + for _, refs := range [][]string{ + {bad1, ok2}, + {ok1, bad2}, + {bad1, bad2}, + } { + _, _, err := parseDockerReferences(refs[0], refs[1]) + assert.Error(t, err) + } +} + +func testExactPRMAndSig(t *testing.T, prmFactory func(string) PolicyReferenceMatch, imageRef, sigRef string, result bool) { + prm := prmFactory(imageRef) + res := prm.matchesDockerReference(mocks.ForbiddenUnparsedImage{}, sigRef) + assert.Equal(t, result, res, fmt.Sprintf("%s vs. %s", imageRef, sigRef)) +} + +func prmExactReferenceFactory(ref string) PolicyReferenceMatch { + // Do not use NewPRMExactReference, we want to also test the case with an invalid DockerReference, + // even though NewPRMExactReference should never let it happen. + return &prmExactReference{DockerReference: ref} +} + +func TestPRMExactReferenceMatchesDockerReference(t *testing.T) { + for _, test := range prmExactMatchTestTable { + testExactPRMAndSig(t, prmExactReferenceFactory, test.refA, test.refB, test.result) + testExactPRMAndSig(t, prmExactReferenceFactory, test.refB, test.refA, test.result) + } +} + +func prmExactRepositoryFactory(ref string) PolicyReferenceMatch { + // Do not use NewPRMExactRepository, we want to also test the case with an invalid DockerReference, + // even though NewPRMExactRepository should never let it happen. + return &prmExactRepository{DockerRepository: ref} +} + +func TestPRMExactRepositoryMatchesDockerReference(t *testing.T) { + for _, test := range prmRepositoryMatchTestTable { + testExactPRMAndSig(t, prmExactRepositoryFactory, test.refA, test.refB, test.result) + testExactPRMAndSig(t, prmExactRepositoryFactory, test.refB, test.refA, test.result) + } +} + +func TestPRMRemapIdentityRefMatchesPrefix(t *testing.T) { + for _, c := range []struct { + ref, prefix string + expected bool + }{ + // Prefix is a reference.Domain() value + {"docker.io/image", "docker.io", true}, + {"docker.io/image", "example.com", false}, + {"example.com:5000/image", "example.com:5000", true}, + {"example.com:50000/image", "example.com:5000", false}, + {"example.com:5000/image", "example.com", false}, + {"example.com/foo", "example.com", true}, + {"example.com/foo/bar", "example.com", true}, + {"example.com/foo/bar:baz", "example.com", true}, + {"example.com/foo/bar" + digestSuffix, "example.com", true}, + // Prefix is a reference.Named.Name() value or a repo namespace + {"docker.io/ns/image", "docker.io/library", false}, + {"example.com/library", "docker.io/library", false}, + {"docker.io/libraryy/image", "docker.io/library", false}, + {"docker.io/library/busybox", "docker.io/library", true}, + {"example.com/ns/image", "example.com/ns", true}, + {"example.com/ns2/image", "example.com/ns", false}, + {"example.com/n2/image", "example.com/ns", false}, + {"example.com", "example.com/library/busybox", false}, + {"example.com:5000/ns/image", "example.com/ns", false}, + {"example.com/ns/image", "example.com:5000/ns", false}, + {"docker.io/library/busybox", "docker.io/library/busybox", true}, + {"example.com/library/busybox", "docker.io/library/busybox", false}, + {"docker.io/library/busybox2", "docker.io/library/busybox", false}, + {"example.com/ns/image", "example.com/ns/image", true}, + {"example.com/ns/imag2", "example.com/ns/image", false}, + {"example.com/ns/imagee", "example.com/ns/image", false}, + {"example.com:5000/ns/image", "example.com/ns/image", false}, + {"example.com/ns/image", "example.com:5000/ns/image", false}, + {"example.com/ns/image:tag", "example.com/ns/image", true}, + {"example.com/ns/image" + digestSuffix, "example.com/ns/image", true}, + {"example.com/ns/image:tag" + digestSuffix, "example.com/ns/image", true}, + } { + prm, err := newPRMRemapIdentity(c.prefix, "docker.io/library/signed-prefix") + require.NoError(t, err, c.prefix) + ref, err := reference.ParseNormalizedNamed(c.ref) + require.NoError(t, err, c.ref) + res := prm.refMatchesPrefix(ref) + assert.Equal(t, c.expected, res, fmt.Sprintf("%s vs. %s", c.ref, c.prefix)) + } +} + +func TestPRMRemapIdentityRemapReferencePrefix(t *testing.T) { + for _, c := range []struct{ prefix, signedPrefix, ref, expected string }{ + // Match sanity checking, primarily tested in TestPRMRefMatchesPrefix + {"mirror.example", "vendor.example", "mirror.example/ns/image:tag", "vendor.example/ns/image:tag"}, + {"mirror.example", "vendor.example", "different.com/ns/image:tag", "different.com/ns/image:tag"}, + {"mirror.example/ns", "vendor.example/vendor-ns", "mirror.example/different-ns/image:tag", "mirror.example/different-ns/image:tag"}, + {"docker.io", "not-docker-signed.example/ns", "busybox", "not-docker-signed.example/ns/library/busybox"}, + // Rewrites work as expected + {"mirror.example", "vendor.example", "mirror.example/ns/image:tag", "vendor.example/ns/image:tag"}, + {"example.com/mirror", "example.com/vendor", "example.com/mirror/image:tag", "example.com/vendor/image:tag"}, + {"example.com/ns/mirror", "example.com/ns/vendor", "example.com/ns/mirror:tag", "example.com/ns/vendor:tag"}, + {"mirror.example", "vendor.example", "prefixmirror.example/ns/image:tag", "prefixmirror.example/ns/image:tag"}, + {"docker.io", "not-docker-signed.example", "busybox", "not-docker-signed.example/library/busybox"}, + {"docker.io/library", "not-docker-signed.example/ns", "busybox", "not-docker-signed.example/ns/busybox"}, + {"docker.io/library/busybox", "not-docker-signed.example/ns/notbusybox", "busybox", "not-docker-signed.example/ns/notbusybox"}, + // On match, tag/digest is preserved + {"mirror.example", "vendor.example", "mirror.example/image", "vendor.example/image"}, // This one should not actually happen, testing for completeness + {"mirror.example", "vendor.example", "mirror.example/image:tag", "vendor.example/image:tag"}, + {"mirror.example", "vendor.example", "mirror.example/image" + digestSuffix, "vendor.example/image" + digestSuffix}, + {"mirror.example", "vendor.example", "mirror.example/image:tag" + digestSuffix, "vendor.example/image:tag" + digestSuffix}, + // Rewrite creating an invalid reference + {"mirror.example/ns/image", "vendor.example:5000", "mirror.example/ns/image:tag", ""}, + // Rewrite creating a valid reference string in short format, which would imply a docker.io prefix and is rejected + {"mirror.example/ns/image", "vendor.example:5000", "mirror.example/ns/image" + digestSuffix, ""}, // vendor.example:5000@digest + {"mirror.example/ns/image", "notlocalhost", "mirror.example/ns/image:tag", ""}, // notlocalhost:tag + } { + testName := fmt.Sprintf("%#v", c) + prm, err := newPRMRemapIdentity(c.prefix, c.signedPrefix) + require.NoError(t, err, testName) + ref, err := reference.ParseNormalizedNamed(c.ref) + require.NoError(t, err, testName) + res, err := prm.remapReferencePrefix(ref) + if c.expected == "" { + assert.Error(t, err, testName) + } else { + require.NoError(t, err, testName) + assert.Equal(t, c.expected, res.String(), testName) + } + } +} + +// modifiedString returns some string that is different from the input, +// consistent across calls with the same input; +// in particular it just replaces the first letter. +func modifiedString(t *testing.T, input string) string { + c := input[0] + switch { + case c >= 'a' && c <= 'y': + c++ + case c == 'z': + c = 'a' + default: + require.Fail(t, "unimplemented leading character '%c'", c) + } + return string(c) + input[1:] +} + +// prmRemapIdentityMRDOETestCase is a helper for TestPRMRemapIdentityMatchesDockerReference, +// verifying that the behavior is consistent with prmMatchRepoDigestOrExact, +// while still smoke-testing the rewriting behavior. +// The test succeeds if imageRefString is invalid and ignoreInvalidImageRef. +func prmRemapIdentityMRDOETestCase(t *testing.T, ignoreInvalidImageRef bool, imageRef, sigRef string, result bool) { + parsedImageRef, err := reference.ParseNormalizedNamed(imageRef) + if ignoreInvalidImageRef && err != nil { + return + } + require.NoError(t, err) + + // No rewriting happens. + prm, err := NewPRMRemapIdentity("never-causes-a-rewrite.example", "never-causes-a-rewrite.example") + require.NoError(t, err) + testImageAndSig(t, prm, imageRef, sigRef, result) + + // Rewrite imageRef + domain := reference.Domain(parsedImageRef) + prm, err = NewPRMRemapIdentity(modifiedString(t, domain), domain) + require.NoError(t, err) + modifiedImageRef, err := reference.ParseNormalizedNamed(modifiedString(t, parsedImageRef.String())) + require.NoError(t, err) + testImageAndSig(t, prm, modifiedImageRef.String(), sigRef, result) +} + +func TestPRMRemapIdentityMatchesDockerReference(t *testing.T) { + // Basic sanity checks. More detailed testing is done in TestPRMRemapIdentityRemapReferencePrefix + // and TestMatchRepoDigestOrExactReferenceValues. + for _, c := range []struct { + prefix, signedPrefix, imageRef, sigRef string + result bool + }{ + // No match rewriting + {"does-not-match.com", "does-not-match.rewritten", "busybox:latest", "busybox:latest", true}, + {"does-not-match.com", "does-not-match.rewritten", "busybox:latest", "notbusybox:latest", false}, + // Match rewriting non-docker + {"mirror.example", "public.com", "mirror.example/busybox:1", "public.com/busybox:1", true}, + {"mirror.example", "public.com", "mirror.example/busybox:1", "public.com/busybox:not1", false}, + // Rewriting to docker.io + {"mirror.example", "docker.io/library", "mirror.example/busybox:latest", "busybox:latest", true}, + {"mirror.example", "docker.io/library", "mirror.example/alpine:latest", "busybox:latest", false}, + // Rewriting from docker.io + {"docker.io/library", "original.com", "copied:latest", "original.com/copied:latest", true}, + {"docker.io/library", "original.com", "copied:latest", "original.com/ns/copied:latest", false}, + // Invalid object: prefix is not a host name + {"busybox", "example.com/busybox", "busybox:latest", "example.com/busybox:latest", false}, + // Invalid object: signedPrefix is not a host name + {"docker.io/library/busybox", "busybox", "docker.io/library/busybox:latest", "busybox:latest", false}, + // Invalid object: invalid prefix + {"UPPERCASE", "example.com", "example.com/foo:latest", "example.com/foo:latest", true}, // Happens to work, not an API promise + {"example.com", "UPPERCASE", "example.com/foo:latest", "UPPERCASE/foo:latest", false}, + } { + // Do not use NewPRMRemapIdentity, we want to also test the cases with invalid values, + // even though NewPRMExactReference should never let it happen. + prm := &prmRemapIdentity{Prefix: c.prefix, SignedPrefix: c.signedPrefix} + testImageAndSig(t, prm, c.imageRef, c.sigRef, c.result) + } + // Even if they are signed with an empty string as a reference, unidentified images are rejected. + prm, err := NewPRMRemapIdentity("docker.io", "docker.io") + require.NoError(t, err) + res := prm.matchesDockerReference(refImageMock{ref: nil}, "") + assert.False(t, res, `unidentified vs. ""`) + + // Verify that the behavior is otherwise the same as for prmMatchRepoDigestOrExact: + // prmMatchRepoDigestOrExact is a middle ground between prmMatchExact and prmMatchRepository: + // It accepts anything prmMatchExact accepts,… + for _, test := range prmExactMatchTestTable { + if test.result == true { + prmRemapIdentityMRDOETestCase(t, true, test.refA, test.refB, test.result) + prmRemapIdentityMRDOETestCase(t, true, test.refB, test.refA, test.result) + } + } + // … and it rejects everything prmMatchRepository rejects. + for _, test := range prmRepositoryMatchTestTable { + if test.result == false { + prmRemapIdentityMRDOETestCase(t, true, test.refA, test.refB, test.result) + prmRemapIdentityMRDOETestCase(t, true, test.refB, test.refA, test.result) + } + } + + // The other cases, possibly asymmetrical: + for _, test := range matchRepoDigestOrExactTestTable { + prmRemapIdentityMRDOETestCase(t, false, test.imageRef, test.sigRef, test.result) + } +} diff --git a/vendor/github.com/containers/image/v5/signature/sigstore/fulcio/fulcio.go b/vendor/github.com/containers/image/v5/signature/sigstore/fulcio/fulcio.go new file mode 100644 index 00000000000..0e6746abb36 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/sigstore/fulcio/fulcio.go @@ -0,0 +1,155 @@ +package fulcio + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "fmt" + "io" + "net/url" + + "github.com/containers/image/v5/internal/useragent" + "github.com/containers/image/v5/signature/sigstore/internal" + "github.com/sigstore/fulcio/pkg/api" + "github.com/sigstore/sigstore/pkg/oauth" + "github.com/sigstore/sigstore/pkg/oauthflow" + sigstoreSignature "github.com/sigstore/sigstore/pkg/signature" + "github.com/sirupsen/logrus" + "golang.org/x/oauth2" +) + +// setupSignerWithFulcio updates s with a certificate generated by fulcioURL based on oidcIDToken +func setupSignerWithFulcio(s *internal.SigstoreSigner, fulcioURL *url.URL, oidcIDToken *oauthflow.OIDCIDToken) error { + // ECDSA-P256 is the only interoperable algorithm per + // https://github.com/sigstore/cosign/blob/main/specs/SIGNATURE_SPEC.md#signature-schemes . + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return fmt.Errorf("generating short-term private key: %w", err) + } + keyAlgorithm := "ecdsa" + // SHA-256 is opencontainers/go-digest.Canonical, thus the algorithm to use here as well per + // https://github.com/sigstore/cosign/blob/main/specs/SIGNATURE_SPEC.md#hashing-algorithms + signer, err := sigstoreSignature.LoadECDSASigner(privateKey, crypto.SHA256) + if err != nil { + return fmt.Errorf("initializing short-term private key: %w", err) + } + s.PrivateKey = signer + + logrus.Debugf("Requesting a certificate from Fulcio at %s", fulcioURL.Redacted()) + fulcioClient := api.NewClient(fulcioURL, api.WithUserAgent(useragent.DefaultUserAgent)) + // Sign the email address as part of the request + h := sha256.Sum256([]byte(oidcIDToken.Subject)) + keyOwnershipProof, err := ecdsa.SignASN1(rand.Reader, privateKey, h[:]) + if err != nil { + return fmt.Errorf("Error signing key ownership proof: %w", err) + } + publicKeyBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey) + if err != nil { + return fmt.Errorf("converting public key to ASN.1: %w", err) + } + // Note that unlike most OAuth2 uses, this passes the ID token, not an access token. + // This is only secure if every Fulcio server has an individual client ID value + // = fulcioOIDCClientID, distinct from other Fulcio servers, + // that is embedded into the ID token’s "aud" field. + resp, err := fulcioClient.SigningCert(api.CertificateRequest{ + PublicKey: api.Key{ + Content: publicKeyBytes, + Algorithm: keyAlgorithm, + }, + SignedEmailAddress: keyOwnershipProof, + }, oidcIDToken.RawString) + if err != nil { + return fmt.Errorf("obtaining certificate from Fulcio: %w", err) + } + s.FulcioGeneratedCertificate = resp.CertPEM + s.FulcioGeneratedCertificateChain = resp.ChainPEM + // Cosign goes through an unmarshal/marshal roundtrip for Fulcio-generated certificates, let’s not do that. + s.SigningKeyOrCert = resp.CertPEM + return nil +} + +// WithFulcioAndPreexistingOIDCIDToken sets up signing to use a short-lived key and a Fulcio-issued certificate +// based on a caller-provided OIDC ID token. +func WithFulcioAndPreexistingOIDCIDToken(fulcioURL *url.URL, oidcIDToken string) internal.Option { + return func(s *internal.SigstoreSigner) error { + if s.PrivateKey != nil { + return fmt.Errorf("multiple private key sources specified when preparing to create sigstore signatures") + } + + // This adds dependencies even just to parse the token. We could possibly reimplement that, and split this variant + // into a subpackage without the OIDC dependencies… but really, is this going to be used in significantly different situations + // than the two interactive OIDC authentication workflows? + // + // Are there any widely used tools to manually obtain an ID token? Why would there be? + // For long-term usage, users provisioning a static OIDC credential might just as well provision an already-generated certificate + // or something like that. + logrus.Debugf("Using a statically-provided OIDC token") + staticTokenGetter := oauthflow.StaticTokenGetter{RawToken: oidcIDToken} + oidcIDToken, err := staticTokenGetter.GetIDToken(nil, oauth2.Config{}) + if err != nil { + return fmt.Errorf("parsing OIDC token: %w", err) + } + + return setupSignerWithFulcio(s, fulcioURL, oidcIDToken) + } +} + +// WithFulcioAndDeviceAuthorizationGrantOIDC sets up signing to use a short-lived key and a Fulcio-issued certificate +// based on an OIDC ID token obtained using a device authorization grant (RFC 8628). +// +// interactiveOutput must be directly accessible to a human user in real time (i.e. not be just a log file). +func WithFulcioAndDeviceAuthorizationGrantOIDC(fulcioURL *url.URL, oidcIssuerURL *url.URL, oidcClientID, oidcClientSecret string, + interactiveOutput io.Writer) internal.Option { + return func(s *internal.SigstoreSigner) error { + if s.PrivateKey != nil { + return fmt.Errorf("multiple private key sources specified when preparing to create sigstore signatures") + } + + logrus.Debugf("Starting OIDC device flow for issuer %s", oidcIssuerURL.Redacted()) + tokenGetter := oauthflow.NewDeviceFlowTokenGetterForIssuer(oidcIssuerURL.String()) + tokenGetter.MessagePrinter = func(s string) { + fmt.Fprintln(interactiveOutput, s) + } + oidcIDToken, err := oauthflow.OIDConnect(oidcIssuerURL.String(), oidcClientID, oidcClientSecret, "", tokenGetter) + if err != nil { + return fmt.Errorf("Error authenticating with OIDC: %w", err) + } + + return setupSignerWithFulcio(s, fulcioURL, oidcIDToken) + } +} + +// WithFulcioAndInterativeOIDC sets up signing to use a short-lived key and a Fulcio-issued certificate +// based on an interactively-obtained OIDC ID token. +// The token is obtained +// - directly using a browser, listening on localhost, automatically opening a browser to the OIDC issuer, +// to be redirected on localhost. (I.e. the current environment must allow launching a browser that connect back to the current process; +// either or both may be impossible in a container or a remote VM). +// - or by instructing the user to manually open a browser, obtain the OIDC code, and interactively input it as text. +// +// interactiveInput and interactiveOutput must both be directly operable by a human user in real time (i.e. not be just a log file). +func WithFulcioAndInteractiveOIDC(fulcioURL *url.URL, oidcIssuerURL *url.URL, oidcClientID, oidcClientSecret string, + interactiveInput io.Reader, interactiveOutput io.Writer) internal.Option { + return func(s *internal.SigstoreSigner) error { + if s.PrivateKey != nil { + return fmt.Errorf("multiple private key sources specified when preparing to create sigstore signatures") + } + + logrus.Debugf("Starting interactive OIDC authentication for issuer %s", oidcIssuerURL.Redacted()) + // This is intended to match oauthflow.DefaultIDTokenGetter, overriding only input/output + tokenGetter := &oauthflow.InteractiveIDTokenGetter{ + HTMLPage: oauth.InteractiveSuccessHTML, + Input: interactiveInput, + Output: interactiveOutput, + } + oidcIDToken, err := oauthflow.OIDConnect(oidcIssuerURL.String(), oidcClientID, oidcClientSecret, "", tokenGetter) + if err != nil { + return fmt.Errorf("Error authenticating with OIDC: %w", err) + } + + return setupSignerWithFulcio(s, fulcioURL, oidcIDToken) + } +} diff --git a/vendor/github.com/containers/image/v5/signature/sigstore/generate_test.go b/vendor/github.com/containers/image/v5/signature/sigstore/generate_test.go new file mode 100644 index 00000000000..7861a92401c --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/sigstore/generate_test.go @@ -0,0 +1,64 @@ +package sigstore + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/containers/image/v5/docker/reference" + "github.com/containers/image/v5/internal/signature" + internalSigner "github.com/containers/image/v5/internal/signer" + "github.com/containers/image/v5/manifest" + "github.com/containers/image/v5/signature/internal" + "github.com/opencontainers/go-digest" + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenerateKeyPair(t *testing.T) { + // Test that generation is possible, and the key can be used for signing. + testManifest := []byte("{}") + testDockerReference, err := reference.ParseNormalizedNamed("example.com/foo:notlatest") + require.NoError(t, err) + + passphrase := []byte("some passphrase") + keyPair, err := GenerateKeyPair(passphrase) + require.NoError(t, err) + + tmpDir := t.TempDir() + privateKeyFile := filepath.Join(tmpDir, "private.key") + err = os.WriteFile(privateKeyFile, keyPair.PrivateKey, 0600) + require.NoError(t, err) + + signer, err := NewSigner(WithPrivateKeyFile(privateKeyFile, passphrase)) + require.NoError(t, err) + sig0, err := internalSigner.SignImageManifest(context.Background(), signer, testManifest, testDockerReference) + require.NoError(t, err) + sig, ok := sig0.(signature.Sigstore) + require.True(t, ok) + + // It would be even more elegant to invoke the higher-level prSigstoreSigned code, + // but that is private. + publicKey, err := cryptoutils.UnmarshalPEMToPublicKey(keyPair.PublicKey) + require.NoError(t, err) + + _, err = internal.VerifySigstorePayload(publicKey, sig.UntrustedPayload(), + sig.UntrustedAnnotations()[signature.SigstoreSignatureAnnotationKey], + internal.SigstorePayloadAcceptanceRules{ + ValidateSignedDockerReference: func(ref string) error { + assert.Equal(t, "example.com/foo:notlatest", ref) + return nil + }, + ValidateSignedDockerManifestDigest: func(digest digest.Digest) error { + matches, err := manifest.MatchesDigest(testManifest, digest) + require.NoError(t, err) + assert.True(t, matches) + return nil + }, + }) + assert.NoError(t, err) + + // The failure paths are not obviously easy to reach. +} diff --git a/vendor/github.com/containers/image/v5/signature/sigstore/rekor/leveled_logger.go b/vendor/github.com/containers/image/v5/signature/sigstore/rekor/leveled_logger.go new file mode 100644 index 00000000000..f240d8cab8d --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/sigstore/rekor/leveled_logger.go @@ -0,0 +1,52 @@ +package rekor + +import ( + "fmt" + + "github.com/hashicorp/go-retryablehttp" + "github.com/sirupsen/logrus" +) + +// leveledLogger adapts our use of logrus to the expected go-retryablehttp.LeveledLogger interface. +type leveledLogger struct { + logger *logrus.Logger +} + +func leveledLoggerForLogrus(logger *logrus.Logger) retryablehttp.LeveledLogger { + return &leveledLogger{logger: logger} +} + +// log is the actual conversion implementation +func (l *leveledLogger) log(level logrus.Level, msg string, keysAndValues []any) { + fields := logrus.Fields{} + for i := 0; i < len(keysAndValues)-1; i += 2 { + key := keysAndValues[i] + keyString, isString := key.(string) + if !isString { + // It seems attractive to panic() here, but we might already be in a failure state, so let’s not make it worse + keyString = fmt.Sprintf("[Invalid LeveledLogger key %#v]", key) + } + fields[keyString] = keysAndValues[i+1] + } + l.logger.WithFields(fields).Log(level, msg) +} + +// Debug implements retryablehttp.LeveledLogger +func (l *leveledLogger) Debug(msg string, keysAndValues ...any) { + l.log(logrus.DebugLevel, msg, keysAndValues) +} + +// Error implements retryablehttp.LeveledLogger +func (l *leveledLogger) Error(msg string, keysAndValues ...any) { + l.log(logrus.ErrorLevel, msg, keysAndValues) +} + +// Info implements retryablehttp.LeveledLogger +func (l *leveledLogger) Info(msg string, keysAndValues ...any) { + l.log(logrus.InfoLevel, msg, keysAndValues) +} + +// Warn implements retryablehttp.LeveledLogger +func (l *leveledLogger) Warn(msg string, keysAndValues ...any) { + l.log(logrus.WarnLevel, msg, keysAndValues) +} diff --git a/vendor/github.com/containers/image/v5/signature/sigstore/rekor/rekor.go b/vendor/github.com/containers/image/v5/signature/sigstore/rekor/rekor.go new file mode 100644 index 00000000000..0236f0aabb5 --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/sigstore/rekor/rekor.go @@ -0,0 +1,160 @@ +package rekor + +import ( + "context" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "net/url" + "strings" + + "github.com/containers/image/v5/signature/internal" + signerInternal "github.com/containers/image/v5/signature/sigstore/internal" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + rekor "github.com/sigstore/rekor/pkg/client" + "github.com/sigstore/rekor/pkg/generated/client" + "github.com/sigstore/rekor/pkg/generated/client/entries" + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sirupsen/logrus" +) + +// WithRekor asks the generated signature to be uploaded to the specified Rekor server, +// and to include a log inclusion proof in the signature. +func WithRekor(rekorURL *url.URL) signerInternal.Option { + return func(s *signerInternal.SigstoreSigner) error { + logrus.Debugf("Using Rekor server at %s", rekorURL.Redacted()) + client, err := rekor.GetRekorClient(rekorURL.String(), + rekor.WithLogger(leveledLoggerForLogrus(logrus.StandardLogger()))) + if err != nil { + return fmt.Errorf("creating Rekor client: %w", err) + } + u := uploader{ + client: client, + } + s.RekorUploader = u.uploadKeyOrCert + return nil + } +} + +// uploader wraps a Rekor client, basically so that we can set RekorUploader to a method instead of an one-off closure. +type uploader struct { + client *client.Rekor +} + +// rekorEntryToSET converts a Rekor log entry into a sigstore “signed entry timestamp”. +func rekorEntryToSET(entry *models.LogEntryAnon) (internal.UntrustedRekorSET, error) { + // We could plausibly call entry.Validate() here; that mostly just uses unnecessary reflection instead of direct == nil checks. + // Right now the only extra validation .Validate() does is *entry.LogIndex >= 0 and a regex check on *entry.LogID; + // we don’t particularly care about either of these (notably signature verification only uses the Body value). + if entry.Verification == nil || entry.IntegratedTime == nil || entry.LogIndex == nil || entry.LogID == nil { + return internal.UntrustedRekorSET{}, fmt.Errorf("invalid Rekor entry (missing data): %#v", *entry) + } + bodyBase64, ok := entry.Body.(string) + if !ok { + return internal.UntrustedRekorSET{}, fmt.Errorf("unexpected Rekor entry body type: %#v", entry.Body) + } + body, err := base64.StdEncoding.DecodeString(bodyBase64) + if err != nil { + return internal.UntrustedRekorSET{}, fmt.Errorf("error parsing Rekor entry body: %w", err) + } + payloadJSON, err := internal.UntrustedRekorPayload{ + Body: body, + IntegratedTime: *entry.IntegratedTime, + LogIndex: *entry.LogIndex, + LogID: *entry.LogID, + }.MarshalJSON() + if err != nil { + return internal.UntrustedRekorSET{}, err + } + + return internal.UntrustedRekorSET{ + UntrustedSignedEntryTimestamp: entry.Verification.SignedEntryTimestamp, + UntrustedPayload: payloadJSON, + }, nil +} + +// uploadEntry ensures proposedEntry exists in Rekor (usually uploading it), and returns the resulting log entry. +func (u *uploader) uploadEntry(ctx context.Context, proposedEntry models.ProposedEntry) (models.LogEntry, error) { + params := entries.NewCreateLogEntryParamsWithContext(ctx) + params.SetProposedEntry(proposedEntry) + logrus.Debugf("Calling Rekor's CreateLogEntry") + resp, err := u.client.Entries.CreateLogEntry(params) + if err != nil { + // In ordinary operation, we should not get duplicate entries, because our payload contains a timestamp, + // so it is supposed to be unique; and the default key format, ECDSA p256, also contains a nonce. + // But conflicts can fairly easily happen during debugging and experimentation, so it pays to handle this. + var conflictErr *entries.CreateLogEntryConflict + if errors.As(err, &conflictErr) && conflictErr.Location != "" { + location := conflictErr.Location.String() + logrus.Debugf("CreateLogEntry reported a conflict, location = %s", location) + // We might be able to just GET the returned Location, but let’s use the generated API client. + // OTOH that requires us to hard-code the URI structure… + uuidDelimiter := strings.LastIndexByte(location, '/') + if uuidDelimiter != -1 { // Otherwise the URI is unexpected, and fall through to the bottom + uuid := location[uuidDelimiter+1:] + logrus.Debugf("Calling Rekor's NewGetLogEntryByUUIDParamsWithContext") + params2 := entries.NewGetLogEntryByUUIDParamsWithContext(ctx) + params2.SetEntryUUID(uuid) + resp2, err := u.client.Entries.GetLogEntryByUUID(params2) + if err != nil { + return nil, fmt.Errorf("Error re-loading previously-created log entry with UUID %s: %w", uuid, err) + } + return resp2.GetPayload(), nil + } + } + return nil, fmt.Errorf("Error uploading a log entry: %w", err) + } + return resp.GetPayload(), nil +} + +// uploadKeyOrCert integrates this code into sigstore/internal.Signer. +// Given components of the created signature, it returns a SET that should be added to the signature. +func (u *uploader) uploadKeyOrCert(ctx context.Context, keyOrCertBytes []byte, signatureBytes []byte, payloadBytes []byte) ([]byte, error) { + payloadHash := sha256.Sum256(payloadBytes) // HashedRecord only accepts SHA-256 + proposedEntry := models.Hashedrekord{ + APIVersion: swag.String(internal.HashedRekordV001APIVersion), + Spec: models.HashedrekordV001Schema{ + Data: &models.HashedrekordV001SchemaData{ + Hash: &models.HashedrekordV001SchemaDataHash{ + Algorithm: swag.String(models.HashedrekordV001SchemaDataHashAlgorithmSha256), + Value: swag.String(hex.EncodeToString(payloadHash[:])), + }, + }, + Signature: &models.HashedrekordV001SchemaSignature{ + Content: strfmt.Base64(signatureBytes), + PublicKey: &models.HashedrekordV001SchemaSignaturePublicKey{ + Content: strfmt.Base64(keyOrCertBytes), + }, + }, + }, + } + + uploadedPayload, err := u.uploadEntry(ctx, &proposedEntry) + if err != nil { + return nil, err + } + + if len(uploadedPayload) != 1 { + return nil, fmt.Errorf("expected 1 Rekor entry, got %d", len(uploadedPayload)) + } + var storedEntry *models.LogEntryAnon + // This “loop” extracts the single value from the uploadedPayload map. + for _, p := range uploadedPayload { + storedEntry = &p + break + } + + rekorBundle, err := rekorEntryToSET(storedEntry) + if err != nil { + return nil, err + } + rekorSETBytes, err := json.Marshal(rekorBundle) + if err != nil { + return nil, err + } + return rekorSETBytes, nil +} diff --git a/vendor/github.com/containers/image/v5/signature/simple_test.go b/vendor/github.com/containers/image/v5/signature/simple_test.go new file mode 100644 index 00000000000..313343e955a --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/simple_test.go @@ -0,0 +1,405 @@ +package signature + +import ( + "encoding/json" + "errors" + "os" + "path/filepath" + "testing" + "time" + + "github.com/containers/image/v5/version" + "github.com/opencontainers/go-digest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/xeipuuv/gojsonschema" +) + +func TestNewUntrustedSignature(t *testing.T) { + timeBefore := time.Now() + sig := newUntrustedSignature(TestImageManifestDigest, TestImageSignatureReference) + assert.Equal(t, TestImageManifestDigest, sig.untrustedDockerManifestDigest) + assert.Equal(t, TestImageSignatureReference, sig.untrustedDockerReference) + require.NotNil(t, sig.untrustedCreatorID) + assert.Equal(t, "atomic "+version.Version, *sig.untrustedCreatorID) + require.NotNil(t, sig.untrustedTimestamp) + timeAfter := time.Now() + assert.True(t, timeBefore.Unix() <= *sig.untrustedTimestamp) + assert.True(t, *sig.untrustedTimestamp <= timeAfter.Unix()) +} + +func TestMarshalJSON(t *testing.T) { + // Empty string values + s := newUntrustedSignature("", "_") + _, err := s.MarshalJSON() + assert.Error(t, err) + s = newUntrustedSignature("_", "") + _, err = s.MarshalJSON() + assert.Error(t, err) + + // Success + // Use intermediate variables for these values so that we can take their addresses. + creatorID := "CREATOR" + timestamp := int64(1484683104) + for _, c := range []struct { + input untrustedSignature + expected string + }{ + { + untrustedSignature{ + untrustedDockerManifestDigest: "digest!@#", + untrustedDockerReference: "reference#@!", + untrustedCreatorID: &creatorID, + untrustedTimestamp: ×tamp, + }, + "{\"critical\":{\"identity\":{\"docker-reference\":\"reference#@!\"},\"image\":{\"docker-manifest-digest\":\"digest!@#\"},\"type\":\"atomic container signature\"},\"optional\":{\"creator\":\"CREATOR\",\"timestamp\":1484683104}}", + }, + { + untrustedSignature{ + untrustedDockerManifestDigest: "digest!@#", + untrustedDockerReference: "reference#@!", + }, + "{\"critical\":{\"identity\":{\"docker-reference\":\"reference#@!\"},\"image\":{\"docker-manifest-digest\":\"digest!@#\"},\"type\":\"atomic container signature\"},\"optional\":{}}", + }, + } { + marshaled, err := c.input.MarshalJSON() + require.NoError(t, err) + assert.Equal(t, []byte(c.expected), marshaled) + + // Also call MarshalJSON through the JSON package. + marshaled, err = json.Marshal(c.input) + assert.NoError(t, err) + assert.Equal(t, []byte(c.expected), marshaled) + } +} + +// Return the result of modifying validJSON with fn +func modifiedJSON(t *testing.T, validJSON []byte, modifyFn func(mSA)) []byte { + var tmp mSA + err := json.Unmarshal(validJSON, &tmp) + require.NoError(t, err) + + modifyFn(tmp) + + modifiedJSON, err := json.Marshal(tmp) + require.NoError(t, err) + return modifiedJSON +} + +// Verify that input can be unmarshaled as an untrustedSignature, and that it passes JSON schema validation, and return the unmarshaled untrustedSignature. +func successfullyUnmarshalUntrustedSignature(t *testing.T, schemaLoader gojsonschema.JSONLoader, input []byte) untrustedSignature { + inputString := string(input) + + var s untrustedSignature + err := json.Unmarshal(input, &s) + require.NoError(t, err, inputString) + + res, err := gojsonschema.Validate(schemaLoader, gojsonschema.NewStringLoader(inputString)) + assert.True(t, err == nil, inputString) + assert.True(t, res.Valid(), inputString) + + return s +} + +// Verify that input can't be unmarshaled as an untrusted signature, and that it fails JSON schema validation. +func assertUnmarshalUntrustedSignatureFails(t *testing.T, schemaLoader gojsonschema.JSONLoader, input []byte) { + inputString := string(input) + + var s untrustedSignature + err := json.Unmarshal(input, &s) + assert.Error(t, err, inputString) + + res, err := gojsonschema.Validate(schemaLoader, gojsonschema.NewStringLoader(inputString)) + assert.True(t, err != nil || !res.Valid(), inputString) +} + +func TestUnmarshalJSON(t *testing.T) { + // NOTE: The schema at schemaPath is NOT authoritative; docs/atomic-signature.json and the code is, rather! + // The schemaPath references are not testing that the code follows the behavior declared by the schema, + // they are testing that the schema follows the behavior of the code! + schemaPath, err := filepath.Abs("../docs/atomic-signature-embedded-json.json") + require.NoError(t, err) + schemaLoader := gojsonschema.NewReferenceLoader("file://" + schemaPath) + + // Invalid input. Note that json.Unmarshal is guaranteed to validate input before calling our + // UnmarshalJSON implementation; so test that first, then test our error handling for completeness. + assertUnmarshalUntrustedSignatureFails(t, schemaLoader, []byte("&")) + var s untrustedSignature + err = s.UnmarshalJSON([]byte("&")) + assert.Error(t, err) + + // Not an object + assertUnmarshalUntrustedSignatureFails(t, schemaLoader, []byte("1")) + + // Start with a valid JSON. + validSig := newUntrustedSignature("digest!@#", "reference#@!") + validJSON, err := validSig.MarshalJSON() + require.NoError(t, err) + + // Success + s = successfullyUnmarshalUntrustedSignature(t, schemaLoader, validJSON) + assert.Equal(t, validSig, s) + + // Various ways to corrupt the JSON + breakFns := []func(mSA){ + // A top-level field is missing + func(v mSA) { delete(v, "critical") }, + func(v mSA) { delete(v, "optional") }, + // Extra top-level sub-object + func(v mSA) { v["unexpected"] = 1 }, + // "critical" not an object + func(v mSA) { v["critical"] = 1 }, + // "optional" not an object + func(v mSA) { v["optional"] = 1 }, + // A field of "critical" is missing + func(v mSA) { delete(x(v, "critical"), "type") }, + func(v mSA) { delete(x(v, "critical"), "image") }, + func(v mSA) { delete(x(v, "critical"), "identity") }, + // Extra field of "critical" + func(v mSA) { x(v, "critical")["unexpected"] = 1 }, + // Invalid "type" + func(v mSA) { x(v, "critical")["type"] = 1 }, + func(v mSA) { x(v, "critical")["type"] = "unexpected" }, + // Invalid "image" object + func(v mSA) { x(v, "critical")["image"] = 1 }, + func(v mSA) { delete(x(v, "critical", "image"), "docker-manifest-digest") }, + func(v mSA) { x(v, "critical", "image")["unexpected"] = 1 }, + // Invalid "docker-manifest-digest" + func(v mSA) { x(v, "critical", "image")["docker-manifest-digest"] = 1 }, + // Invalid "identity" object + func(v mSA) { x(v, "critical")["identity"] = 1 }, + func(v mSA) { delete(x(v, "critical", "identity"), "docker-reference") }, + func(v mSA) { x(v, "critical", "identity")["unexpected"] = 1 }, + // Invalid "docker-reference" + func(v mSA) { x(v, "critical", "identity")["docker-reference"] = 1 }, + // Invalid "creator" + func(v mSA) { x(v, "optional")["creator"] = 1 }, + // Invalid "timestamp" + func(v mSA) { x(v, "optional")["timestamp"] = "unexpected" }, + func(v mSA) { x(v, "optional")["timestamp"] = 0.5 }, // Fractional input + } + for _, fn := range breakFns { + testJSON := modifiedJSON(t, validJSON, fn) + assertUnmarshalUntrustedSignatureFails(t, schemaLoader, testJSON) + } + + // Modifications to unrecognized fields in "optional" are allowed and ignored + allowedModificationFns := []func(mSA){ + // Add an optional field + func(v mSA) { x(v, "optional")["unexpected"] = 1 }, + } + for _, fn := range allowedModificationFns { + testJSON := modifiedJSON(t, validJSON, fn) + s := successfullyUnmarshalUntrustedSignature(t, schemaLoader, testJSON) + assert.Equal(t, validSig, s) + } + + // Optional fields can be missing + validSig = untrustedSignature{ + untrustedDockerManifestDigest: "digest!@#", + untrustedDockerReference: "reference#@!", + untrustedCreatorID: nil, + untrustedTimestamp: nil, + } + validJSON, err = validSig.MarshalJSON() + require.NoError(t, err) + s = successfullyUnmarshalUntrustedSignature(t, schemaLoader, validJSON) + assert.Equal(t, validSig, s) +} + +func TestSign(t *testing.T) { + mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory) + require.NoError(t, err) + defer mech.Close() + + if err := mech.SupportsSigning(); err != nil { + t.Skipf("Signing not supported: %v", err) + } + + sig := newUntrustedSignature("digest!@#", "reference#@!") + + // Successful signing + signature, err := sig.sign(mech, TestKeyFingerprint, "") + require.NoError(t, err) + + verified, err := verifyAndExtractSignature(mech, signature, signatureAcceptanceRules{ + validateKeyIdentity: func(keyIdentity string) error { + if keyIdentity != TestKeyFingerprint { + return errors.New("Unexpected keyIdentity") + } + return nil + }, + validateSignedDockerReference: func(signedDockerReference string) error { + if signedDockerReference != sig.untrustedDockerReference { + return errors.New("Unexpected signedDockerReference") + } + return nil + }, + validateSignedDockerManifestDigest: func(signedDockerManifestDigest digest.Digest) error { + if signedDockerManifestDigest != sig.untrustedDockerManifestDigest { + return errors.New("Unexpected signedDockerManifestDigest") + } + return nil + }, + }) + require.NoError(t, err) + + assert.Equal(t, sig.untrustedDockerManifestDigest, verified.DockerManifestDigest) + assert.Equal(t, sig.untrustedDockerReference, verified.DockerReference) + + // Error creating blob to sign + _, err = untrustedSignature{}.sign(mech, TestKeyFingerprint, "") + assert.Error(t, err) + + // Error signing + _, err = sig.sign(mech, "this fingerprint doesn't exist", "") + assert.Error(t, err) +} + +func TestVerifyAndExtractSignature(t *testing.T) { + mech, err := newGPGSigningMechanismInDirectory(testGPGHomeDirectory) + require.NoError(t, err) + defer mech.Close() + + type triple struct { + keyIdentity string + signedDockerReference string + signedDockerManifestDigest digest.Digest + } + var wanted, recorded triple + // recordingRules are a plausible signatureAcceptanceRules implementations, but equally + // importantly record that we are passing the correct values to the rule callbacks. + recordingRules := signatureAcceptanceRules{ + validateKeyIdentity: func(keyIdentity string) error { + recorded.keyIdentity = keyIdentity + if keyIdentity != wanted.keyIdentity { + return errors.New("keyIdentity mismatch") + } + return nil + }, + validateSignedDockerReference: func(signedDockerReference string) error { + recorded.signedDockerReference = signedDockerReference + if signedDockerReference != wanted.signedDockerReference { + return errors.New("signedDockerReference mismatch") + } + return nil + }, + validateSignedDockerManifestDigest: func(signedDockerManifestDigest digest.Digest) error { + recorded.signedDockerManifestDigest = signedDockerManifestDigest + if signedDockerManifestDigest != wanted.signedDockerManifestDigest { + return errors.New("signedDockerManifestDigest mismatch") + } + return nil + }, + } + + signature, err := os.ReadFile("./fixtures/image.signature") + require.NoError(t, err) + signatureData := triple{ + keyIdentity: TestKeyFingerprint, + signedDockerReference: TestImageSignatureReference, + signedDockerManifestDigest: TestImageManifestDigest, + } + + // Successful verification + wanted = signatureData + recorded = triple{} + sig, err := verifyAndExtractSignature(mech, signature, recordingRules) + require.NoError(t, err) + assert.Equal(t, TestImageSignatureReference, sig.DockerReference) + assert.Equal(t, TestImageManifestDigest, sig.DockerManifestDigest) + assert.Equal(t, signatureData, recorded) + + // For extra paranoia, test that we return a nil signature object on error. + + // Completely invalid signature. + recorded = triple{} + sig, err = verifyAndExtractSignature(mech, []byte{}, recordingRules) + assert.Error(t, err) + assert.Nil(t, sig) + assert.Equal(t, triple{}, recorded) + + recorded = triple{} + sig, err = verifyAndExtractSignature(mech, []byte("invalid signature"), recordingRules) + assert.Error(t, err) + assert.Nil(t, sig) + assert.Equal(t, triple{}, recorded) + + // Valid signature of non-JSON: asked for keyIdentity, only + invalidBlobSignature, err := os.ReadFile("./fixtures/invalid-blob.signature") + require.NoError(t, err) + recorded = triple{} + sig, err = verifyAndExtractSignature(mech, invalidBlobSignature, recordingRules) + assert.Error(t, err) + assert.Nil(t, sig) + assert.Equal(t, triple{keyIdentity: signatureData.keyIdentity}, recorded) + + // Valid signature with a wrong key: asked for keyIdentity, only + wanted = signatureData + wanted.keyIdentity = "unexpected fingerprint" + recorded = triple{} + sig, err = verifyAndExtractSignature(mech, signature, recordingRules) + assert.Error(t, err) + assert.Nil(t, sig) + assert.Equal(t, triple{keyIdentity: signatureData.keyIdentity}, recorded) + + // Valid signature with a wrong manifest digest: asked for keyIdentity and signedDockerManifestDigest + wanted = signatureData + wanted.signedDockerManifestDigest = "invalid digest" + recorded = triple{} + sig, err = verifyAndExtractSignature(mech, signature, recordingRules) + assert.Error(t, err) + assert.Nil(t, sig) + assert.Equal(t, triple{ + keyIdentity: signatureData.keyIdentity, + signedDockerManifestDigest: signatureData.signedDockerManifestDigest, + }, recorded) + + // Valid signature with a wrong image reference + wanted = signatureData + wanted.signedDockerReference = "unexpected docker reference" + recorded = triple{} + sig, err = verifyAndExtractSignature(mech, signature, recordingRules) + assert.Error(t, err) + assert.Nil(t, sig) + assert.Equal(t, signatureData, recorded) +} + +func TestGetUntrustedSignatureInformationWithoutVerifying(t *testing.T) { + signature, err := os.ReadFile("./fixtures/image.signature") + require.NoError(t, err) + // Successful parsing, all optional fields present + info, err := GetUntrustedSignatureInformationWithoutVerifying(signature) + require.NoError(t, err) + assert.Equal(t, TestImageSignatureReference, info.UntrustedDockerReference) + assert.Equal(t, TestImageManifestDigest, info.UntrustedDockerManifestDigest) + assert.NotNil(t, info.UntrustedCreatorID) + assert.Equal(t, "atomic ", *info.UntrustedCreatorID) + assert.NotNil(t, info.UntrustedTimestamp) + assert.Equal(t, time.Unix(1458239713, 0), *info.UntrustedTimestamp) + assert.Equal(t, TestKeyShortID, info.UntrustedShortKeyIdentifier) + // Successful parsing, no optional fields present + signature, err = os.ReadFile("./fixtures/no-optional-fields.signature") + require.NoError(t, err) + // Successful parsing + info, err = GetUntrustedSignatureInformationWithoutVerifying(signature) + require.NoError(t, err) + assert.Equal(t, TestImageSignatureReference, info.UntrustedDockerReference) + assert.Equal(t, TestImageManifestDigest, info.UntrustedDockerManifestDigest) + assert.Nil(t, info.UntrustedCreatorID) + assert.Nil(t, info.UntrustedTimestamp) + assert.Equal(t, TestKeyShortID, info.UntrustedShortKeyIdentifier) + + // Completely invalid signature. + _, err = GetUntrustedSignatureInformationWithoutVerifying([]byte{}) + assert.Error(t, err) + + _, err = GetUntrustedSignatureInformationWithoutVerifying([]byte("invalid signature")) + assert.Error(t, err) + + // Valid signature of non-JSON + invalidBlobSignature, err := os.ReadFile("./fixtures/invalid-blob.signature") + require.NoError(t, err) + _, err = GetUntrustedSignatureInformationWithoutVerifying(invalidBlobSignature) + assert.Error(t, err) +} diff --git a/vendor/github.com/containers/image/v5/signature/simplesigning/signer_test.go b/vendor/github.com/containers/image/v5/signature/simplesigning/signer_test.go new file mode 100644 index 00000000000..0246c13c5ae --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/simplesigning/signer_test.go @@ -0,0 +1,235 @@ +package simplesigning + +import ( + "context" + "os" + "testing" + + "github.com/containers/image/v5/docker/reference" + internalSig "github.com/containers/image/v5/internal/signature" + internalSigner "github.com/containers/image/v5/internal/signer" + "github.com/containers/image/v5/internal/testing/gpgagent" + "github.com/containers/image/v5/signature" + "github.com/opencontainers/go-digest" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + // testImageManifestDigest is the Docker manifest digest of "image.manifest.json" + testImageManifestDigest = digest.Digest("sha256:20bf21ed457b390829cdbeec8795a7bea1626991fda603e0d01b4e7f60427e55") + testGPGHomeDirectory = "./testdata" + // TestKeyFingerprint is the fingerprint of the private key in testGPGHomeDirectory. + testKeyFingerprint = "1D8230F6CDB6A06716E414C1DB72F2188BB46CC8" + // testKeyFingerprintWithPassphrase is the fingerprint of the private key with passphrase in testGPGHomeDirectory. + testKeyFingerprintWithPassphrase = "E3EB7611D815211F141946B5B0CDE60B42557346" + // testPassphrase is the passphrase for testKeyFingerprintWithPassphrase. + testPassphrase = "WithPassphrase123" +) + +// Ensure we don’t leave around GPG agent processes. +func TestMain(m *testing.M) { + code := m.Run() + if err := gpgagent.KillGPGAgent(testGPGHomeDirectory); err != nil { + logrus.Warnf("Error killing GPG agent: %v", err) + } + os.Exit(code) +} + +func TestNewSigner(t *testing.T) { + t.Setenv("GNUPGHOME", testGPGHomeDirectory) + + mech, err := signature.NewGPGSigningMechanism() + require.NoError(t, err) + defer mech.Close() + if err := mech.SupportsSigning(); err != nil { + t.Skipf("Signing not supported: %v", err) + } + + // An option causes an error + _, err = NewSigner(WithKeyFingerprint(testKeyFingerprintWithPassphrase), WithPassphrase("\n")) + assert.Error(t, err) + + // WithKeyFingerprint is missing + _, err = NewSigner(WithPassphrase("something")) + assert.Error(t, err) + + // A smoke test + s, err := NewSigner(WithKeyFingerprint(testKeyFingerprint)) + require.NoError(t, err) + err = s.Close() + assert.NoError(t, err) +} + +func TestSimpleSignerProgressMessage(t *testing.T) { + t.Setenv("GNUPGHOME", testGPGHomeDirectory) + + mech, err := signature.NewGPGSigningMechanism() + require.NoError(t, err) + defer mech.Close() + if err := mech.SupportsSigning(); err != nil { + t.Skipf("Signing not supported: %v", err) + } + + // Just a smoke test + s, err := NewSigner(WithKeyFingerprint(testKeyFingerprint)) + require.NoError(t, err) + defer func() { + err = s.Close() + assert.NoError(t, err) + }() + + _ = internalSigner.ProgressMessage(s) +} + +func TestSimpleSignerSignImageManifest(t *testing.T) { + t.Setenv("GNUPGHOME", testGPGHomeDirectory) + + mech, err := signature.NewGPGSigningMechanism() + require.NoError(t, err) + defer mech.Close() + if err := mech.SupportsSigning(); err != nil { + t.Skipf("Signing not supported: %v", err) + } + + err = gpgagent.KillGPGAgent(testGPGHomeDirectory) + require.NoError(t, err) + + manifest, err := os.ReadFile("../fixtures/image.manifest.json") + require.NoError(t, err) + testImageSignatureReference, err := reference.ParseNormalizedNamed("example.com/testing/manifest:notlatest") + require.NoError(t, err) + + // Failures to sign need to be tested in two parts: First the failures that involve the wrong passphrase, then failures that + // should manifest even with a valid passphrase or unlocked key (because the GPG agent is caching unlocked keys). + // Alternatively, we could be caling gpgagent.KillGPGAgent() all the time... + type failingCase struct { + name string + opts []Option + // NOTE: We DO NOT promise that things that don't fail during NewSigner won't start failing there. + // Actually we’d prefer failures to be identified early. This field only records current expected behavior, not the _desired_ end state. + creationFails bool + creationErrorContains string + manifest []byte + ref reference.Named + } + testFailure := func(c failingCase) { + s, err := NewSigner(c.opts...) + if c.creationFails { + assert.Error(t, err, c.name) + if c.creationErrorContains != "" { + assert.ErrorContains(t, err, c.creationErrorContains, c.name) + } + } else { + require.NoError(t, err, c.name) + defer s.Close() + + m := manifest + if c.manifest != nil { + m = c.manifest + } + _, err = internalSigner.SignImageManifest(context.Background(), s, m, c.ref) + assert.Error(t, err, c.name) + } + } + for _, c := range []failingCase{ + { + name: "Invalid passphrase", + opts: []Option{ + WithKeyFingerprint(testKeyFingerprintWithPassphrase), + WithPassphrase(testPassphrase + "\n"), + }, + creationFails: true, + creationErrorContains: "invalid passphrase", + ref: testImageSignatureReference, + }, + { + name: "Wrong passphrase", + opts: []Option{ + WithKeyFingerprint(testKeyFingerprintWithPassphrase), + WithPassphrase("wrong"), + }, + ref: testImageSignatureReference, + }, + { + name: "No passphrase", + opts: []Option{WithKeyFingerprint(testKeyFingerprintWithPassphrase)}, + ref: testImageSignatureReference, + }, + } { + testFailure(c) + } + + // Successful signing + for _, c := range []struct { + name string + fingerprint string + opts []Option + }{ + { + name: "No passphrase", + fingerprint: testKeyFingerprint, + }, + { + name: "With passphrase", + fingerprint: testKeyFingerprintWithPassphrase, + opts: []Option{WithPassphrase(testPassphrase)}, + }, + } { + s, err := NewSigner(append([]Option{WithKeyFingerprint(c.fingerprint)}, c.opts...)...) + require.NoError(t, err, c.name) + defer s.Close() + + sig, err := internalSigner.SignImageManifest(context.Background(), s, manifest, testImageSignatureReference) + require.NoError(t, err, c.name) + simpleSig, ok := sig.(internalSig.SimpleSigning) + require.True(t, ok) + + // FIXME FIXME: gpgme_op_sign with a passphrase succeeds, but somehow confuses the GPGME internal state + // so that gpgme_op_verify below never completes (it polls on an already closed FD). + // That’s probably a GPGME bug, and needs investigating and fixing, but it isn’t related to this “signer” implementation. + if len(c.opts) == 0 { + mech, err := signature.NewGPGSigningMechanism() + require.NoError(t, err) + defer mech.Close() + + verified, err := signature.VerifyDockerManifestSignature(simpleSig.UntrustedSignature(), manifest, testImageSignatureReference.String(), mech, c.fingerprint) + require.NoError(t, err) + assert.Equal(t, testImageSignatureReference.String(), verified.DockerReference) + assert.Equal(t, testImageManifestDigest, verified.DockerManifestDigest) + } + } + + invalidManifest, err := os.ReadFile("../fixtures/v2s1-invalid-signatures.manifest.json") + require.NoError(t, err) + invalidReference, err := reference.ParseNormalizedNamed("no-tag") + require.NoError(t, err) + for _, c := range []failingCase{ + { + name: "No key to sign with", + opts: nil, + creationFails: true, + }, + { + name: "Error computing Docker manifest", + opts: []Option{WithKeyFingerprint(testKeyFingerprint)}, + manifest: invalidManifest, + ref: testImageSignatureReference, + }, + { + name: "Invalid reference", + opts: []Option{WithKeyFingerprint(testKeyFingerprint)}, + ref: invalidReference, + }, + { + name: "Error signing", + opts: []Option{ + WithKeyFingerprint("this fingerprint doesn't exist"), + }, + ref: testImageSignatureReference, + }, + } { + testFailure(c) + } +} diff --git a/vendor/github.com/containers/image/v5/signature/simplesigning/testdata/.gitignore b/vendor/github.com/containers/image/v5/signature/simplesigning/testdata/.gitignore new file mode 100644 index 00000000000..2772b97f7df --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/simplesigning/testdata/.gitignore @@ -0,0 +1,6 @@ +/*.gpg~ +/.gpg-v21-migrated +/private-keys-v1.d +/random_seed +/gnupg_spawn_agent_sentinel.lock +/.#* diff --git a/vendor/github.com/containers/image/v5/signature/simplesigning/testdata/pubring.gpg b/vendor/github.com/containers/image/v5/signature/simplesigning/testdata/pubring.gpg new file mode 120000 index 00000000000..be16a533e9d --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/simplesigning/testdata/pubring.gpg @@ -0,0 +1 @@ +../../fixtures/pubring.gpg \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/simplesigning/testdata/secring.gpg b/vendor/github.com/containers/image/v5/signature/simplesigning/testdata/secring.gpg new file mode 120000 index 00000000000..f8f8c2ba55e --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/simplesigning/testdata/secring.gpg @@ -0,0 +1 @@ +../../fixtures/secring.gpg \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/signature/simplesigning/testdata/trustdb.gpg b/vendor/github.com/containers/image/v5/signature/simplesigning/testdata/trustdb.gpg new file mode 120000 index 00000000000..e22f33aac9c --- /dev/null +++ b/vendor/github.com/containers/image/v5/signature/simplesigning/testdata/trustdb.gpg @@ -0,0 +1 @@ +../../fixtures/trustdb.gpg \ No newline at end of file diff --git a/vendor/github.com/containers/image/v5/storage/storage_dest.go b/vendor/github.com/containers/image/v5/storage/storage_dest.go index 576d510cc5b..7bbbf1752cd 100644 --- a/vendor/github.com/containers/image/v5/storage/storage_dest.go +++ b/vendor/github.com/containers/image/v5/storage/storage_dest.go @@ -307,6 +307,9 @@ func (s *storageImageDestination) PutBlobPartial(ctx context.Context, chunkAcces // If the blob has been successfully reused, returns (true, info, nil). // If the transport can not reuse the requested blob, TryReusingBlob returns (false, {}, nil); it returns a non-nil error only on an unexpected failure. func (s *storageImageDestination) TryReusingBlobWithOptions(ctx context.Context, blobinfo types.BlobInfo, options private.TryReusingBlobOptions) (bool, private.ReusedBlob, error) { + if !impl.OriginalBlobMatchesRequiredCompression(options) { + return false, private.ReusedBlob{}, nil + } reused, info, err := s.tryReusingBlobAsPending(blobinfo.Digest, blobinfo.Size, &options) if err != nil || !reused || options.LayerIndex == nil { return reused, info, err diff --git a/vendor/github.com/containers/image/v5/storage/storage_reference_test.go b/vendor/github.com/containers/image/v5/storage/storage_reference_test.go new file mode 100644 index 00000000000..6fa7d0e7130 --- /dev/null +++ b/vendor/github.com/containers/image/v5/storage/storage_reference_test.go @@ -0,0 +1,144 @@ +//go:build !containers_image_storage_stub +// +build !containers_image_storage_stub + +package storage + +import ( + "fmt" + "strings" + "testing" + + "github.com/containers/image/v5/docker/reference" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewReference(t *testing.T) { + newStore(t) + st, ok := Transport.(*storageTransport) + require.True(t, ok) + // Success is tested throughout; test only the failure + _, err := newReference(*st, nil, "") + assert.Error(t, err) + _, err = newReference(*st, nil, "ab") + assert.Error(t, err) + ref, err := reference.ParseNormalizedNamed("busybox") + require.NoError(t, err) + _, err = newReference(*st, ref, "") + assert.Error(t, err) +} + +func TestStorageReferenceTransport(t *testing.T) { + newStore(t) + ref, err := Transport.ParseReference("busybox") + require.NoError(t, err) + transport := ref.Transport() + st, ok := transport.(*storageTransport) + require.True(t, ok) + assert.Equal(t, *(Transport.(*storageTransport)), *st) +} + +// A common list of reference formats to test for the various ImageReference methods. +var validReferenceTestCases = []struct { + input, dockerRef, canonical string + namespaces []string +}{ + { + "busybox", "docker.io/library/busybox:latest", "docker.io/library/busybox:latest", + []string{"docker.io/library/busybox", "docker.io/library", "docker.io"}, + }, + { + "example.com/myns/ns2/busybox:notlatest", "example.com/myns/ns2/busybox:notlatest", "example.com/myns/ns2/busybox:notlatest", + []string{"example.com/myns/ns2/busybox", "example.com/myns/ns2", "example.com/myns", "example.com"}, + }, + { + "@" + sha256digestHex, "", "@" + sha256digestHex, + []string{}, + }, + { + "busybox@" + sha256digestHex, "docker.io/library/busybox:latest", "docker.io/library/busybox:latest@" + sha256digestHex, + []string{"docker.io/library/busybox:latest", "docker.io/library/busybox", "docker.io/library", "docker.io"}, + }, + { + "busybox@sha256:" + sha256digestHex, "docker.io/library/busybox@sha256:" + sha256digestHex, "docker.io/library/busybox@sha256:" + sha256digestHex, + []string{"docker.io/library/busybox", "docker.io/library", "docker.io"}, + }, + { + "busybox:notlatest@" + sha256digestHex, "docker.io/library/busybox:notlatest", "docker.io/library/busybox:notlatest@" + sha256digestHex, + []string{"docker.io/library/busybox:notlatest", "docker.io/library/busybox", "docker.io/library", "docker.io"}, + }, + { + "busybox:notlatest@sha256:" + sha256digestHex, "docker.io/library/busybox:notlatest@sha256:" + sha256digestHex, "docker.io/library/busybox:notlatest@sha256:" + sha256digestHex, + []string{"docker.io/library/busybox:notlatest", "docker.io/library/busybox", "docker.io/library", "docker.io"}, + }, + { + "busybox@" + sha256Digest2 + "@" + sha256digestHex, "docker.io/library/busybox@" + sha256Digest2, "docker.io/library/busybox@" + sha256Digest2 + "@" + sha256digestHex, + []string{"docker.io/library/busybox@" + sha256Digest2, "docker.io/library/busybox", "docker.io/library", "docker.io"}, + }, + { + "busybox:notlatest@" + sha256Digest2 + "@" + sha256digestHex, "docker.io/library/busybox:notlatest@" + sha256Digest2, "docker.io/library/busybox:notlatest@" + sha256Digest2 + "@" + sha256digestHex, + []string{"docker.io/library/busybox:notlatest@" + sha256Digest2, "docker.io/library/busybox:notlatest", "docker.io/library/busybox", "docker.io/library", "docker.io"}, + }, +} + +func TestStorageReferenceDockerReference(t *testing.T) { + newStore(t) + for _, c := range validReferenceTestCases { + ref, err := Transport.ParseReference(c.input) + require.NoError(t, err, c.input) + if c.dockerRef != "" { + dr := ref.DockerReference() + require.NotNil(t, dr, c.input) + assert.Equal(t, c.dockerRef, dr.String(), c.input) + } else { + dr := ref.DockerReference() + assert.Nil(t, dr, c.input) + } + } +} + +func TestStorageReferenceStringWithinTransport(t *testing.T) { + store := newStore(t) + optionsList := "" + options := store.GraphOptions() + if len(options) > 0 { + optionsList = ":" + strings.Join(options, ",") + } + storeSpec := fmt.Sprintf("[%s@%s+%s%s]", store.GraphDriverName(), store.GraphRoot(), store.RunRoot(), optionsList) + + for _, c := range validReferenceTestCases { + ref, err := Transport.ParseReference(c.input) + require.NoError(t, err, c.input) + assert.Equal(t, storeSpec+c.canonical, ref.StringWithinTransport(), c.input) + } +} + +func TestStorageReferencePolicyConfigurationIdentity(t *testing.T) { + store := newStore(t) + storeSpec := fmt.Sprintf("[%s@%s]", store.GraphDriverName(), store.GraphRoot()) + + for _, c := range validReferenceTestCases { + ref, err := Transport.ParseReference(c.input) + require.NoError(t, err, c.input) + assert.Equal(t, storeSpec+c.canonical, ref.PolicyConfigurationIdentity(), c.input) + } +} + +func TestStorageReferencePolicyConfigurationNamespaces(t *testing.T) { + store := newStore(t) + storeSpec := fmt.Sprintf("[%s@%s]", store.GraphDriverName(), store.GraphRoot()) + + for _, c := range validReferenceTestCases { + ref, err := Transport.ParseReference(c.input) + require.NoError(t, err, c.input) + expectedNS := []string{} + for _, ns := range c.namespaces { + expectedNS = append(expectedNS, storeSpec+ns) + } + expectedNS = append(expectedNS, storeSpec) + expectedNS = append(expectedNS, fmt.Sprintf("[%s]", store.GraphRoot())) + assert.Equal(t, expectedNS, ref.PolicyConfigurationNamespaces(), c.input) + } +} + +// NewImage, NewImageSource, NewImageDestination, DeleteImage tested in storage_test.go diff --git a/vendor/github.com/containers/image/v5/storage/storage_src_test.go b/vendor/github.com/containers/image/v5/storage/storage_src_test.go new file mode 100644 index 00000000000..adbcfe98ebc --- /dev/null +++ b/vendor/github.com/containers/image/v5/storage/storage_src_test.go @@ -0,0 +1,43 @@ +package storage + +import ( + "testing" + + "github.com/containers/image/v5/manifest" + "github.com/containers/image/v5/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBuildLayerInfosForCopy(t *testing.T) { + manifestInfos := []manifest.LayerInfo{ + {BlobInfo: types.BlobInfo{Digest: "sha256:6a5a5368e0c2d3e5909184fa28ddfd56072e7ff3ee9a945876f7eee5896ef5bb", Size: -1}, EmptyLayer: false}, + {BlobInfo: types.BlobInfo{Digest: "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", Size: -1}, EmptyLayer: true}, + {BlobInfo: types.BlobInfo{Digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Size: -1}, EmptyLayer: true}, + {BlobInfo: types.BlobInfo{Digest: "sha256:1bbf5d58d24c47512e234a5623474acf65ae00d4d1414272a893204f44cc680c", Size: -1}, EmptyLayer: false}, + {BlobInfo: types.BlobInfo{Digest: "sha256:5555555555555555555555555555555555555555555555555555555555555555", Size: -1}, EmptyLayer: true}, + } + physicalInfos := []types.BlobInfo{ + {Digest: "sha256:1111111111111111111111111111111111111111111111111111111111111111", Size: 111, MediaType: manifest.DockerV2Schema2LayerMediaType}, + {Digest: "sha256:2222222222222222222222222222222222222222222222222222222222222222", Size: 222, MediaType: manifest.DockerV2Schema2LayerMediaType}, + } + + // Success + res, err := buildLayerInfosForCopy(manifestInfos, physicalInfos) + require.NoError(t, err) + assert.Equal(t, []types.BlobInfo{ + {Digest: "sha256:1111111111111111111111111111111111111111111111111111111111111111", Size: 111, MediaType: manifest.DockerV2Schema2LayerMediaType}, + {Digest: "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", Size: 32}, + {Digest: "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", Size: 32}, + {Digest: "sha256:2222222222222222222222222222222222222222222222222222222222222222", Size: 222, MediaType: manifest.DockerV2Schema2LayerMediaType}, + {Digest: "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", Size: 32}, + }, res) + + // PhysicalInfos too short + _, err = buildLayerInfosForCopy(manifestInfos, physicalInfos[:len(physicalInfos)-1]) + assert.Error(t, err) + + // PhysicalInfos too long + _, err = buildLayerInfosForCopy(manifestInfos, append(physicalInfos, physicalInfos[0])) + assert.Error(t, err) +} diff --git a/vendor/github.com/containers/image/v5/storage/storage_test.go b/vendor/github.com/containers/image/v5/storage/storage_test.go new file mode 100644 index 00000000000..247f0d4469a --- /dev/null +++ b/vendor/github.com/containers/image/v5/storage/storage_test.go @@ -0,0 +1,1136 @@ +//go:build !containers_image_storage_stub +// +build !containers_image_storage_stub + +package storage + +import ( + "archive/tar" + "bytes" + "context" + "crypto/rand" + "crypto/sha256" + "errors" + "flag" + "fmt" + "io" + "os" + "path/filepath" + "reflect" + "strings" + "sync" + "testing" + "time" + + imanifest "github.com/containers/image/v5/internal/manifest" + "github.com/containers/image/v5/internal/private" + "github.com/containers/image/v5/pkg/blobinfocache/memory" + "github.com/containers/image/v5/types" + "github.com/containers/storage" + "github.com/containers/storage/pkg/archive" + "github.com/containers/storage/pkg/idtools" + "github.com/containers/storage/pkg/ioutils" + "github.com/containers/storage/pkg/reexec" + ddigest "github.com/opencontainers/go-digest" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" + "golang.org/x/exp/slices" +) + +var ( + _ types.ImageDestination = &storageImageDestination{} + _ private.ImageDestination = (*storageImageDestination)(nil) + _ types.ImageSource = &storageImageSource{} + _ private.ImageSource = (*storageImageSource)(nil) + _ types.ImageReference = &storageReference{} + _ types.ImageTransport = &storageTransport{} +) + +const ( + layerSize = 12345 +) + +func TestMain(m *testing.M) { + if reexec.Init() { + return + } + debug := false + flag.BoolVar(&debug, "debug", false, "print debug statements") + flag.Parse() + if debug { + logrus.SetLevel(logrus.DebugLevel) + } + os.Exit(m.Run()) +} + +func newStoreWithGraphDriverOptions(t *testing.T, options []string) storage.Store { + wd := t.TempDir() + run := filepath.Join(wd, "run") + root := filepath.Join(wd, "root") + Transport.SetDefaultUIDMap([]idtools.IDMap{{ + ContainerID: 0, + HostID: os.Getuid(), + Size: 1, + }}) + Transport.SetDefaultGIDMap([]idtools.IDMap{{ + ContainerID: 0, + HostID: os.Getgid(), + Size: 1, + }}) + store, err := storage.GetStore(storage.StoreOptions{ + RunRoot: run, + GraphRoot: root, + GraphDriverName: "vfs", + GraphDriverOptions: options, + UIDMap: Transport.DefaultUIDMap(), + GIDMap: Transport.DefaultGIDMap(), + }) + if err != nil { + t.Fatal(err) + } + Transport.SetStore(store) + return store +} + +func newStore(t *testing.T) storage.Store { + return newStoreWithGraphDriverOptions(t, []string{}) +} + +func TestParse(t *testing.T) { + store := newStore(t) + + ref, err := Transport.ParseReference("test") + if err != nil { + t.Fatalf("ParseReference(%q) returned error %v", "test", err) + } + if ref == nil { + t.Fatalf("ParseReference returned nil reference") + } + + ref, err = Transport.ParseStoreReference(store, "test") + if err != nil { + t.Fatalf("ParseStoreReference(%q) returned error %v", "test", err) + } + + strRef := ref.StringWithinTransport() + ref, err = Transport.ParseReference(strRef) + if err != nil { + t.Fatalf("ParseReference(%q) returned error: %v", strRef, err) + } + if ref == nil { + t.Fatalf("ParseReference(%q) returned nil reference", strRef) + } + + transport := storageTransport{ + store: store, + defaultUIDMap: Transport.(*storageTransport).defaultUIDMap, + defaultGIDMap: Transport.(*storageTransport).defaultGIDMap, + } + _references := []storageReference{ + { + named: ref.(*storageReference).named, + id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + transport: transport, + }, + { + named: ref.(*storageReference).named, + transport: transport, + }, + { + id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + transport: transport, + }, + { + named: ref.DockerReference(), + transport: transport, + }, + } + for _, reference := range _references { + s := reference.StringWithinTransport() + ref, err := Transport.ParseStoreReference(store, s) + if err != nil { + t.Fatalf("ParseReference(%q) returned error: %v", strRef, err) + } + if ref.id != reference.id { + t.Fatalf("ParseReference(%q) failed to extract ID", s) + } + if reference.named == nil { + if ref.named != nil { + t.Fatalf("ParseReference(%q) set non-nil named", s) + } + } else { + if ref.named.String() != reference.named.String() { + t.Fatalf("ParseReference(%q) failed to extract reference (%q!=%q)", s, ref.named.String(), reference.named.String()) + } + } + } +} + +func TestParseWithGraphDriverOptions(t *testing.T) { + optionLists := [][]string{ + {}, + {"vfs.ignore_chown_errors=true"}, + {"vfs.ignore_chown_errors=false"}, + } + for _, optionList := range optionLists { + store := newStoreWithGraphDriverOptions(t, optionList) + ref, err := Transport.ParseStoreReference(store, "test") + if err != nil { + t.Fatalf("ParseStoreReference(%q, graph driver options %v) returned error %v", "test", optionList, err) + } + if ref == nil { + t.Fatalf("ParseStoreReference returned nil reference") + } + spec := ref.StringWithinTransport() + ref2, err := Transport.ParseReference(spec) + if err != nil { + t.Fatalf("ParseReference(%q) returned error %v", "test", err) + } + if ref == nil { + t.Fatalf("ParseReference returned nil reference") + } + sref, ok := ref2.(*storageReference) + if !ok { + t.Fatalf("ParseReference returned a reference from transport %s, not one of ours", ref2.Transport().Name()) + } + parsedOptions := sref.transport.store.GraphOptions() + if !slices.Equal(parsedOptions, optionList) { + t.Fatalf("Mismatched options: %#v and %#v", optionList, parsedOptions) + } + } +} + +func systemContext() *types.SystemContext { + return &types.SystemContext{} +} + +func makeLayer(t *testing.T, compression archive.Compression) (ddigest.Digest, int64, int64, []byte) { + var cwriter io.WriteCloser + var uncompressed *ioutils.WriteCounter + var twriter *tar.Writer + preader, pwriter := io.Pipe() + tbuffer := bytes.Buffer{} + if compression != archive.Uncompressed { + compressor, err := archive.CompressStream(pwriter, compression) + if err != nil { + t.Fatalf("Error compressing layer: %v", err) + } + cwriter = compressor + uncompressed = ioutils.NewWriteCounter(cwriter) + } else { + uncompressed = ioutils.NewWriteCounter(pwriter) + } + twriter = tar.NewWriter(uncompressed) + buf := make([]byte, layerSize) + n, err := rand.Read(buf) + if err != nil { + t.Fatalf("Error reading tar data: %v", err) + } + if n != len(buf) { + t.Fatalf("Short read reading tar data: %d < %d", n, len(buf)) + } + for i := 1024; i < 2048; i++ { + buf[i] = 0 + } + + wg := sync.WaitGroup{} + errs := make(chan error) + wg.Add(1) + go func() { + defer pwriter.Close() + if cwriter != nil { + defer cwriter.Close() + } + defer twriter.Close() + err := twriter.WriteHeader(&tar.Header{ + Name: "/random-single-file", + Mode: 0600, + Size: int64(len(buf)), + ModTime: time.Now(), + AccessTime: time.Now(), + ChangeTime: time.Now(), + Typeflag: tar.TypeReg, + }) + if err != nil { + errs <- fmt.Errorf("Error writing tar header: %v", err) + } + n, err := twriter.Write(buf) + if err != nil { + errs <- fmt.Errorf("Error writing tar header: %v", err) + } + if n != len(buf) { + errs <- fmt.Errorf("Short write writing tar header: %d < %d", n, len(buf)) + } + err = twriter.Flush() + if err != nil { + errs <- fmt.Errorf("Error flushing output to tar archive: %v", err) + } + }() + go func() { + wg.Wait() + close(errs) + }() + for err := range errs { + if err != nil { + t.Fatal(err) + } + } + + _, err = io.Copy(&tbuffer, preader) + if err != nil { + t.Fatalf("Error reading layer tar: %v", err) + } + sum := ddigest.SHA256.FromBytes(tbuffer.Bytes()) + return sum, uncompressed.Count, int64(tbuffer.Len()), tbuffer.Bytes() +} + +func TestWriteRead(t *testing.T) { + if os.Geteuid() != 0 { + t.Skip("TestWriteRead requires root privileges") + } + + config := `{"config":{"labels":{}},"created":"2006-01-02T15:04:05Z"}` + sum := ddigest.SHA256.FromBytes([]byte(config)) + configInfo := types.BlobInfo{ + Digest: sum, + Size: int64(len(config)), + } + manifests := []string{ + //`{ + // "schemaVersion": 2, + // "mediaType": "application/vnd.oci.image.manifest.v1+json", + // "config": { + // "mediaType": "application/vnd.oci.image.serialization.config.v1+json", + // "size": %cs, + // "digest": "%ch" + // }, + // "layers": [ + // { + // "mediaType": "application/vnd.oci.image.serialization.rootfs.tar.gzip", + // "digest": "%lh", + // "size": %ls + // } + // ] + //}`, + `{ + "schemaVersion": 1, + "name": "test", + "tag": "latest", + "architecture": "amd64", + "fsLayers": [ + { + "blobSum": "%lh" + } + ], + "history": [ + { + "v1Compatibility": "{\"id\":\"%li\",\"created\":\"2016-03-03T11:29:44.222098366Z\",\"container\":\"\",\"container_config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":[\"/bin/sh\"],\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{}},\"docker_version\":\"1.8.2-fc22\",\"author\":\"\\\"William Temple \\u003cwtemple at redhat dot com\\u003e\\\"\",\"config\":{\"Hostname\":\"56f0fe1dfc95\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"ExposedPorts\":null,\"PublishService\":\"\",\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":null,\"Cmd\":null,\"Image\":\"\",\"Volumes\":null,\"VolumeDriver\":\"\",\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":null,\"Labels\":{}},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":%ls}" + } + ] + }`, + `{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": %cs, + "digest": "%ch" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "%lh", + "size": %ls + } + ] + }`, + } + signatures := [][]byte{ + []byte("Signature A"), + []byte("Signature B"), + } + newStore(t) + ref, err := Transport.ParseReference("test") + if err != nil { + t.Fatalf("ParseReference(%q) returned error %v", "test", err) + } + if ref == nil { + t.Fatalf("ParseReference returned nil reference") + } + cache := memory.New() + + for _, manifestFmt := range manifests { + dest, err := ref.NewImageDestination(context.Background(), systemContext()) + if err != nil { + t.Fatalf("NewImageDestination(%q) returned error %v", ref.StringWithinTransport(), err) + } + if dest == nil { + t.Fatalf("NewImageDestination(%q) returned no destination", ref.StringWithinTransport()) + } + if dest.Reference().StringWithinTransport() != ref.StringWithinTransport() { + t.Fatalf("NewImageDestination(%q) changed the reference to %q", ref.StringWithinTransport(), dest.Reference().StringWithinTransport()) + } + t.Logf("supported manifest MIME types: %v", dest.SupportedManifestMIMETypes()) + if err := dest.SupportsSignatures(context.Background()); err != nil { + t.Fatalf("Destination image doesn't support signatures: %v", err) + } + t.Logf("compress layers: %v", dest.DesiredLayerCompression()) + compression := archive.Uncompressed + if dest.DesiredLayerCompression() == types.Compress { + compression = archive.Gzip + } + digest, decompressedSize, size, blob := makeLayer(t, compression) + if _, err := dest.PutBlob(context.Background(), bytes.NewReader(blob), types.BlobInfo{ + Size: size, + Digest: digest, + }, cache, false); err != nil { + t.Fatalf("Error saving randomly-generated layer to destination: %v", err) + } + t.Logf("Wrote randomly-generated layer %q (%d/%d bytes) to destination", digest, size, decompressedSize) + if _, err := dest.PutBlob(context.Background(), strings.NewReader(config), configInfo, cache, false); err != nil { + t.Fatalf("Error saving config to destination: %v", err) + } + manifest := strings.ReplaceAll(manifestFmt, "%lh", digest.String()) + manifest = strings.ReplaceAll(manifest, "%ch", configInfo.Digest.String()) + manifest = strings.ReplaceAll(manifest, "%ls", fmt.Sprintf("%d", size)) + manifest = strings.ReplaceAll(manifest, "%cs", fmt.Sprintf("%d", configInfo.Size)) + li := digest.Hex() + manifest = strings.ReplaceAll(manifest, "%li", li) + manifest = strings.ReplaceAll(manifest, "%ci", sum.Hex()) + t.Logf("this manifest is %q", manifest) + if err := dest.PutManifest(context.Background(), []byte(manifest), nil); err != nil { + t.Fatalf("Error saving manifest to destination: %v", err) + } + if err := dest.PutSignatures(context.Background(), signatures, nil); err != nil { + t.Fatalf("Error saving signatures to destination: %v", err) + } + unparsedToplevel := unparsedImage{ + imageReference: nil, + manifestBytes: []byte(manifest), + manifestType: imanifest.GuessMIMEType([]byte(manifest)), + signatures: signatures, + } + if err := dest.Commit(context.Background(), &unparsedToplevel); err != nil { + t.Fatalf("Error committing changes to destination: %v", err) + } + dest.Close() + + img, err := ref.NewImage(context.Background(), systemContext()) + if err != nil { + t.Fatalf("NewImage(%q) returned error %v", ref.StringWithinTransport(), err) + } + imageConfigInfo := img.ConfigInfo() + if imageConfigInfo.Digest != "" { + blob, err := img.ConfigBlob(context.Background()) + if err != nil { + t.Fatalf("image %q claimed there was a config blob, but couldn't produce it: %v", ref.StringWithinTransport(), err) + } + sum := ddigest.SHA256.FromBytes(blob) + if sum != configInfo.Digest { + t.Fatalf("image config blob digest for %q doesn't match", ref.StringWithinTransport()) + } + if int64(len(blob)) != configInfo.Size { + t.Fatalf("image config size for %q changed from %d to %d", ref.StringWithinTransport(), configInfo.Size, len(blob)) + } + } + layerInfos := img.LayerInfos() + if layerInfos == nil { + t.Fatalf("image for %q returned empty layer list", ref.StringWithinTransport()) + } + imageInfo, err := img.Inspect(context.Background()) + if err != nil { + t.Fatalf("Inspect(%q) returned error %v", ref.StringWithinTransport(), err) + } + if imageInfo.Created.IsZero() { + t.Fatalf("Image %q claims to have been created at time 0", ref.StringWithinTransport()) + } + + src, err := ref.NewImageSource(context.Background(), systemContext()) + if err != nil { + t.Fatalf("NewImageSource(%q) returned error %v", ref.StringWithinTransport(), err) + } + if src == nil { + t.Fatalf("NewImageSource(%q) returned no source", ref.StringWithinTransport()) + } + // Note that we would strip a digest here, but not a tag. + if src.Reference().StringWithinTransport() != ref.StringWithinTransport() { + // As long as it's only the addition of an ID suffix, that's okay. + if !strings.HasPrefix(src.Reference().StringWithinTransport(), ref.StringWithinTransport()+"@") { + t.Fatalf("NewImageSource(%q) changed the reference to %q", ref.StringWithinTransport(), src.Reference().StringWithinTransport()) + } + } + _, manifestType, err := src.GetManifest(context.Background(), nil) + if err != nil { + t.Fatalf("GetManifest(%q) returned error %v", ref.StringWithinTransport(), err) + } + t.Logf("this manifest's type appears to be %q", manifestType) + sum, err = imanifest.Digest([]byte(manifest)) + if err != nil { + t.Fatalf("manifest.Digest() returned error %v", err) + } + retrieved, _, err := src.GetManifest(context.Background(), &sum) + if err != nil { + t.Fatalf("GetManifest(%q) with an instanceDigest is supposed to succeed", ref.StringWithinTransport()) + } + if string(retrieved) != manifest { + t.Fatalf("GetManifest(%q) with an instanceDigest retrieved a different manifest", ref.StringWithinTransport()) + } + sigs, err := src.GetSignatures(context.Background(), nil) + if err != nil { + t.Fatalf("GetSignatures(%q) returned error %v", ref.StringWithinTransport(), err) + } + if len(sigs) < len(signatures) { + t.Fatalf("Lost %d signatures", len(signatures)-len(sigs)) + } + if len(sigs) > len(signatures) { + t.Fatalf("Gained %d signatures", len(sigs)-len(signatures)) + } + for i := range sigs { + if !bytes.Equal(sigs[i], signatures[i]) { + t.Fatalf("Signature %d was corrupted", i) + } + } + sigs2, err := src.GetSignatures(context.Background(), &sum) + if err != nil { + t.Fatalf("GetSignatures(%q) with instance %s returned error %v", ref.StringWithinTransport(), sum.String(), err) + } + if !reflect.DeepEqual(sigs, sigs2) { + t.Fatalf("GetSignatures(%q) with instance %s returned a different result", ref.StringWithinTransport(), sum.String()) + } + for _, layerInfo := range layerInfos { + buf := bytes.Buffer{} + layer, size, err := src.GetBlob(context.Background(), layerInfo, cache) + if err != nil { + t.Fatalf("Error reading layer %q from %q", layerInfo.Digest, ref.StringWithinTransport()) + } + t.Logf("Decompressing blob %q, blob size = %d, layerInfo.Size = %d bytes", layerInfo.Digest, size, layerInfo.Size) + hasher := sha256.New() + compressed := ioutils.NewWriteCounter(hasher) + countedLayer := io.TeeReader(layer, compressed) + decompressed, err := archive.DecompressStream(countedLayer) + if err != nil { + t.Fatalf("Error decompressing layer %q from %q", layerInfo.Digest, ref.StringWithinTransport()) + } + n, err := io.Copy(&buf, decompressed) + require.NoError(t, err) + layer.Close() + if layerInfo.Size >= 0 && compressed.Count != layerInfo.Size { + t.Fatalf("Blob size is different than expected: %d != %d, read %d", compressed.Count, layerInfo.Size, n) + } + if size >= 0 && compressed.Count != size { + t.Fatalf("Blob size mismatch: %d != %d, read %d", compressed.Count, size, n) + } + sum := hasher.Sum(nil) + if ddigest.NewDigestFromBytes(ddigest.SHA256, sum) != layerInfo.Digest { + t.Fatalf("Layer blob digest for %q doesn't match", ref.StringWithinTransport()) + } + } + src.Close() + img.Close() + err = ref.DeleteImage(context.Background(), systemContext()) + if err != nil { + t.Fatalf("DeleteImage(%q) returned error %v", ref.StringWithinTransport(), err) + } + } +} + +func TestDuplicateName(t *testing.T) { + if os.Geteuid() != 0 { + t.Skip("TestDuplicateName requires root privileges") + } + + newStore(t) + cache := memory.New() + + ref, err := Transport.ParseReference("test") + if err != nil { + t.Fatalf("ParseReference(%q) returned error %v", "test", err) + } + if ref == nil { + t.Fatalf("ParseReference returned nil reference") + } + + dest, err := ref.NewImageDestination(context.Background(), systemContext()) + if err != nil { + t.Fatalf("NewImageDestination(%q, first pass) returned error %v", ref.StringWithinTransport(), err) + } + if dest == nil { + t.Fatalf("NewImageDestination(%q, first pass) returned no destination", ref.StringWithinTransport()) + } + digest, _, size, blob := makeLayer(t, archive.Uncompressed) + if _, err := dest.PutBlob(context.Background(), bytes.NewReader(blob), types.BlobInfo{ + Size: size, + Digest: digest, + }, cache, false); err != nil { + t.Fatalf("Error saving randomly-generated layer to destination, first pass: %v", err) + } + manifest := fmt.Sprintf(` + { + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "%s", + "size": %d + } + ] + } + `, digest, size) + if err := dest.PutManifest(context.Background(), []byte(manifest), nil); err != nil { + t.Fatalf("Error storing manifest to destination: %v", err) + } + unparsedToplevel := unparsedImage{ + imageReference: nil, + manifestBytes: []byte(manifest), + manifestType: imanifest.GuessMIMEType([]byte(manifest)), + signatures: nil, + } + if err := dest.Commit(context.Background(), &unparsedToplevel); err != nil { + t.Fatalf("Error committing changes to destination, first pass: %v", err) + } + dest.Close() + + dest, err = ref.NewImageDestination(context.Background(), systemContext()) + if err != nil { + t.Fatalf("NewImageDestination(%q, second pass) returned error %v", ref.StringWithinTransport(), err) + } + if dest == nil { + t.Fatalf("NewImageDestination(%q, second pass) returned no destination", ref.StringWithinTransport()) + } + digest, _, size, blob = makeLayer(t, archive.Gzip) + if _, err := dest.PutBlob(context.Background(), bytes.NewReader(blob), types.BlobInfo{ + Size: size, + Digest: digest, + }, cache, false); err != nil { + t.Fatalf("Error saving randomly-generated layer to destination, second pass: %v", err) + } + manifest = fmt.Sprintf(` + { + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "%s", + "size": %d + } + ] + } + `, digest, size) + if err := dest.PutManifest(context.Background(), []byte(manifest), nil); err != nil { + t.Fatalf("Error storing manifest to destination: %v", err) + } + unparsedToplevel = unparsedImage{ + imageReference: nil, + manifestBytes: []byte(manifest), + manifestType: imanifest.GuessMIMEType([]byte(manifest)), + signatures: nil, + } + if err := dest.Commit(context.Background(), &unparsedToplevel); err != nil { + t.Fatalf("Error committing changes to destination, second pass: %v", err) + } + dest.Close() +} + +func TestDuplicateID(t *testing.T) { + if os.Geteuid() != 0 { + t.Skip("TestDuplicateID requires root privileges") + } + + newStore(t) + cache := memory.New() + + ref, err := Transport.ParseReference("@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + if err != nil { + t.Fatalf("ParseReference(%q) returned error %v", "test", err) + } + if ref == nil { + t.Fatalf("ParseReference returned nil reference") + } + + dest, err := ref.NewImageDestination(context.Background(), systemContext()) + if err != nil { + t.Fatalf("NewImageDestination(%q, first pass) returned error %v", ref.StringWithinTransport(), err) + } + if dest == nil { + t.Fatalf("NewImageDestination(%q, first pass) returned no destination", ref.StringWithinTransport()) + } + digest, _, size, blob := makeLayer(t, archive.Gzip) + if _, err := dest.PutBlob(context.Background(), bytes.NewReader(blob), types.BlobInfo{ + Size: size, + Digest: digest, + }, cache, false); err != nil { + t.Fatalf("Error saving randomly-generated layer to destination, first pass: %v", err) + } + manifest := fmt.Sprintf(` + { + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "%s", + "size": %d + } + ] + } + `, digest, size) + if err := dest.PutManifest(context.Background(), []byte(manifest), nil); err != nil { + t.Fatalf("Error storing manifest to destination: %v", err) + } + unparsedToplevel := unparsedImage{ + imageReference: nil, + manifestBytes: []byte(manifest), + manifestType: imanifest.GuessMIMEType([]byte(manifest)), + signatures: nil, + } + if err := dest.Commit(context.Background(), &unparsedToplevel); err != nil { + t.Fatalf("Error committing changes to destination, first pass: %v", err) + } + dest.Close() + + dest, err = ref.NewImageDestination(context.Background(), systemContext()) + if err != nil { + t.Fatalf("NewImageDestination(%q, second pass) returned error %v", ref.StringWithinTransport(), err) + } + if dest == nil { + t.Fatalf("NewImageDestination(%q, second pass) returned no destination", ref.StringWithinTransport()) + } + digest, _, size, blob = makeLayer(t, archive.Gzip) + if _, err := dest.PutBlob(context.Background(), bytes.NewReader(blob), types.BlobInfo{ + Size: size, + Digest: digest, + }, cache, false); err != nil { + t.Fatalf("Error saving randomly-generated layer to destination, second pass: %v", err) + } + manifest = fmt.Sprintf(` + { + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "%s", + "size": %d + } + ] + } + `, digest, size) + if err := dest.PutManifest(context.Background(), []byte(manifest), nil); err != nil { + t.Fatalf("Error storing manifest to destination: %v", err) + } + unparsedToplevel = unparsedImage{ + imageReference: nil, + manifestBytes: []byte(manifest), + manifestType: imanifest.GuessMIMEType([]byte(manifest)), + signatures: nil, + } + if err := dest.Commit(context.Background(), &unparsedToplevel); !errors.Is(err, storage.ErrDuplicateID) { + if err != nil { + t.Fatalf("Wrong error committing changes to destination, second pass: %v", err) + } + t.Fatal("Incorrectly succeeded committing changes to destination, second pass: no error") + } + dest.Close() +} + +func TestDuplicateNameID(t *testing.T) { + if os.Geteuid() != 0 { + t.Skip("TestDuplicateNameID requires root privileges") + } + + newStore(t) + cache := memory.New() + + ref, err := Transport.ParseReference("test@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + if err != nil { + t.Fatalf("ParseReference(%q) returned error %v", "test", err) + } + if ref == nil { + t.Fatalf("ParseReference returned nil reference") + } + + dest, err := ref.NewImageDestination(context.Background(), systemContext()) + if err != nil { + t.Fatalf("NewImageDestination(%q, first pass) returned error %v", ref.StringWithinTransport(), err) + } + if dest == nil { + t.Fatalf("NewImageDestination(%q, first pass) returned no destination", ref.StringWithinTransport()) + } + digest, _, size, blob := makeLayer(t, archive.Gzip) + if _, err := dest.PutBlob(context.Background(), bytes.NewReader(blob), types.BlobInfo{ + Size: size, + Digest: digest, + }, cache, false); err != nil { + t.Fatalf("Error saving randomly-generated layer to destination, first pass: %v", err) + } + manifest := fmt.Sprintf(` + { + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "%s", + "size": %d + } + ] + } + `, digest, size) + if err := dest.PutManifest(context.Background(), []byte(manifest), nil); err != nil { + t.Fatalf("Error storing manifest to destination: %v", err) + } + unparsedToplevel := unparsedImage{ + imageReference: nil, + manifestBytes: []byte(manifest), + manifestType: imanifest.GuessMIMEType([]byte(manifest)), + signatures: nil, + } + if err := dest.Commit(context.Background(), &unparsedToplevel); err != nil { + t.Fatalf("Error committing changes to destination, first pass: %v", err) + } + dest.Close() + + dest, err = ref.NewImageDestination(context.Background(), systemContext()) + if err != nil { + t.Fatalf("NewImageDestination(%q, second pass) returned error %v", ref.StringWithinTransport(), err) + } + if dest == nil { + t.Fatalf("NewImageDestination(%q, second pass) returned no destination", ref.StringWithinTransport()) + } + digest, _, size, blob = makeLayer(t, archive.Gzip) + if _, err := dest.PutBlob(context.Background(), bytes.NewReader(blob), types.BlobInfo{ + Size: size, + Digest: digest, + }, cache, false); err != nil { + t.Fatalf("Error saving randomly-generated layer to destination, second pass: %v", err) + } + manifest = fmt.Sprintf(` + { + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "%s", + "size": %d + } + ] + } + `, digest, size) + if err := dest.PutManifest(context.Background(), []byte(manifest), nil); err != nil { + t.Fatalf("Error storing manifest to destination: %v", err) + } + unparsedToplevel = unparsedImage{ + imageReference: nil, + manifestBytes: []byte(manifest), + manifestType: imanifest.GuessMIMEType([]byte(manifest)), + signatures: nil, + } + if err := dest.Commit(context.Background(), &unparsedToplevel); !errors.Is(err, storage.ErrDuplicateID) { + if err != nil { + t.Fatalf("Wrong error committing changes to destination, second pass: %v", err) + } + t.Fatal("Incorrectly succeeded committing changes to destination, second pass: no error") + } + dest.Close() +} + +func TestNamespaces(t *testing.T) { + newStore(t) + + ref, err := Transport.ParseReference("test@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + if err != nil { + t.Fatalf("ParseReference(%q) returned error %v", "test", err) + } + if ref == nil { + t.Fatalf("ParseReference returned nil reference") + } + + namespaces := ref.PolicyConfigurationNamespaces() + for _, namespace := range namespaces { + t.Logf("namespace: %q", namespace) + err = Transport.ValidatePolicyConfigurationScope(namespace) + if ref == nil { + t.Fatalf("ValidatePolicyConfigurationScope(%q) returned error: %v", namespace, err) + } + } + namespace := ref.StringWithinTransport() + t.Logf("ref: %q", namespace) + err = Transport.ValidatePolicyConfigurationScope(namespace) + if err != nil { + t.Fatalf("ValidatePolicyConfigurationScope(%q) returned error: %v", namespace, err) + } + for _, namespace := range []string{ + "@beefee", + ":miracle", + ":miracle@beefee", + "@beefee:miracle", + } { + t.Logf("invalid ref: %q", namespace) + err = Transport.ValidatePolicyConfigurationScope(namespace) + if err == nil { + t.Fatalf("ValidatePolicyConfigurationScope(%q) should have failed", namespace) + } + } +} + +func TestSize(t *testing.T) { + if os.Geteuid() != 0 { + t.Skip("TestSize requires root privileges") + } + + config := `{"config":{"labels":{}},"created":"2006-01-02T15:04:05Z"}` + sum := ddigest.SHA256.FromBytes([]byte(config)) + configInfo := types.BlobInfo{ + Digest: sum, + Size: int64(len(config)), + } + + newStore(t) + cache := memory.New() + + ref, err := Transport.ParseReference("test") + if err != nil { + t.Fatalf("ParseReference(%q) returned error %v", "test", err) + } + if ref == nil { + t.Fatalf("ParseReference returned nil reference") + } + + dest, err := ref.NewImageDestination(context.Background(), systemContext()) + if err != nil { + t.Fatalf("NewImageDestination(%q) returned error %v", ref.StringWithinTransport(), err) + } + if dest == nil { + t.Fatalf("NewImageDestination(%q) returned no destination", ref.StringWithinTransport()) + } + if _, err := dest.PutBlob(context.Background(), strings.NewReader(config), configInfo, cache, false); err != nil { + t.Fatalf("Error saving config to destination: %v", err) + } + digest1, usize1, size1, blob := makeLayer(t, archive.Gzip) + if _, err := dest.PutBlob(context.Background(), bytes.NewReader(blob), types.BlobInfo{ + Size: size1, + Digest: digest1, + }, cache, false); err != nil { + t.Fatalf("Error saving randomly-generated layer 1 to destination: %v", err) + } + digest2, usize2, size2, blob := makeLayer(t, archive.Gzip) + if _, err := dest.PutBlob(context.Background(), bytes.NewReader(blob), types.BlobInfo{ + Size: size2, + Digest: digest2, + }, cache, false); err != nil { + t.Fatalf("Error saving randomly-generated layer 2 to destination: %v", err) + } + manifest := fmt.Sprintf(` + { + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": %d, + "digest": "%s" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "%s", + "size": %d + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "%s", + "size": %d + } + ] + } + `, configInfo.Size, configInfo.Digest, digest1, size1, digest2, size2) + if err := dest.PutManifest(context.Background(), []byte(manifest), nil); err != nil { + t.Fatalf("Error storing manifest to destination: %v", err) + } + unparsedToplevel := unparsedImage{ + imageReference: nil, + manifestBytes: []byte(manifest), + manifestType: imanifest.GuessMIMEType([]byte(manifest)), + signatures: nil, + } + if err := dest.Commit(context.Background(), &unparsedToplevel); err != nil { + t.Fatalf("Error committing changes to destination: %v", err) + } + dest.Close() + + img, err := ref.NewImage(context.Background(), systemContext()) + if err != nil { + t.Fatalf("NewImage(%q) returned error %v", ref.StringWithinTransport(), err) + } + usize, err := img.Size() + if usize == -1 || err != nil { + t.Fatalf("Error calculating image size: %v", err) + } + if int(usize) != len(config)+int(usize1)+int(usize2)+2*len(manifest) { + t.Fatalf("Unexpected image size: %d != %d + %d + %d + %d (%d)", usize, len(config), usize1, usize2, len(manifest), len(config)+int(usize1)+int(usize2)+2*len(manifest)) + } + img.Close() +} + +func TestDuplicateBlob(t *testing.T) { + if os.Geteuid() != 0 { + t.Skip("TestDuplicateBlob requires root privileges") + } + + config := `{"config":{"labels":{}},"created":"2006-01-02T15:04:05Z"}` + sum := ddigest.SHA256.FromBytes([]byte(config)) + configInfo := types.BlobInfo{ + Digest: sum, + Size: int64(len(config)), + } + + newStore(t) + cache := memory.New() + + ref, err := Transport.ParseReference("test") + if err != nil { + t.Fatalf("ParseReference(%q) returned error %v", "test", err) + } + if ref == nil { + t.Fatalf("ParseReference returned nil reference") + } + + dest, err := ref.NewImageDestination(context.Background(), systemContext()) + if err != nil { + t.Fatalf("NewImageDestination(%q) returned error %v", ref.StringWithinTransport(), err) + } + if dest == nil { + t.Fatalf("NewImageDestination(%q) returned no destination", ref.StringWithinTransport()) + } + digest1, _, size1, blob1 := makeLayer(t, archive.Gzip) + if _, err := dest.PutBlob(context.Background(), bytes.NewReader(blob1), types.BlobInfo{ + Size: size1, + Digest: digest1, + }, cache, false); err != nil { + t.Fatalf("Error saving randomly-generated layer 1 to destination (first copy): %v", err) + } + digest2, _, size2, blob2 := makeLayer(t, archive.Gzip) + if _, err := dest.PutBlob(context.Background(), bytes.NewReader(blob2), types.BlobInfo{ + Size: size2, + Digest: digest2, + }, cache, false); err != nil { + t.Fatalf("Error saving randomly-generated layer 2 to destination (first copy): %v", err) + } + if _, err := dest.PutBlob(context.Background(), bytes.NewReader(blob1), types.BlobInfo{ + Size: size1, + Digest: digest1, + }, cache, false); err != nil { + t.Fatalf("Error saving randomly-generated layer 1 to destination (second copy): %v", err) + } + if _, err := dest.PutBlob(context.Background(), bytes.NewReader(blob2), types.BlobInfo{ + Size: size2, + Digest: digest2, + }, cache, false); err != nil { + t.Fatalf("Error saving randomly-generated layer 2 to destination (second copy): %v", err) + } + manifest := fmt.Sprintf(` + { + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": %d, + "digest": "%s" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "%s", + "size": %d + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "%s", + "size": %d + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "%s", + "size": %d + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "digest": "%s", + "size": %d + } + ] + } + `, configInfo.Size, configInfo.Digest, digest1, size1, digest2, size2, digest1, size1, digest2, size2) + if err := dest.PutManifest(context.Background(), []byte(manifest), nil); err != nil { + t.Fatalf("Error storing manifest to destination: %v", err) + } + unparsedToplevel := unparsedImage{ + imageReference: nil, + manifestBytes: []byte(manifest), + manifestType: imanifest.GuessMIMEType([]byte(manifest)), + signatures: nil, + } + if err := dest.Commit(context.Background(), &unparsedToplevel); err != nil { + t.Fatalf("Error committing changes to destination: %v", err) + } + dest.Close() + + img, err := ref.NewImage(context.Background(), systemContext()) + if err != nil { + t.Fatalf("NewImage(%q) returned error %v", ref.StringWithinTransport(), err) + } + src, err := ref.NewImageSource(context.Background(), systemContext()) + if err != nil { + t.Fatalf("NewImageSource(%q) returned error %v", ref.StringWithinTransport(), err) + } + source, ok := src.(*storageImageSource) + if !ok { + t.Fatalf("ImageSource is not a storage image") + } + layers := []string{} + layersInfo, err := img.LayerInfosForCopy(context.Background()) + if err != nil { + t.Fatalf("LayerInfosForCopy() returned error %v", err) + } + for _, layerInfo := range layersInfo { + digestLayers, _ := source.imageRef.transport.store.LayersByUncompressedDigest(layerInfo.Digest) + rc, _, layerID, err := source.getBlobAndLayerID(layerInfo.Digest, digestLayers) + if err != nil { + t.Fatalf("getBlobAndLayerID(%q) returned error %v", layerInfo.Digest, err) + } + _, err = io.Copy(io.Discard, rc) + require.NoError(t, err) + rc.Close() + layers = append(layers, layerID) + } + if len(layers) != 4 { + t.Fatalf("Incorrect number of layers: %d", len(layers)) + } + for i, layerID := range layers { + for j, otherID := range layers { + if i != j && layerID == otherID { + t.Fatalf("Layer IDs are not unique: %v", layers) + } + } + } + src.Close() + img.Close() +} + +type unparsedImage struct { + imageReference types.ImageReference + manifestBytes []byte + manifestType string + signatures [][]byte +} + +func (u *unparsedImage) Reference() types.ImageReference { + return u.imageReference +} +func (u *unparsedImage) Manifest(context.Context) ([]byte, string, error) { + return u.manifestBytes, u.manifestType, nil +} +func (u *unparsedImage) Signatures(context.Context) ([][]byte, error) { + return u.signatures, nil +} diff --git a/vendor/github.com/containers/image/v5/storage/storage_transport_test.go b/vendor/github.com/containers/image/v5/storage/storage_transport_test.go new file mode 100644 index 00000000000..091806c6e28 --- /dev/null +++ b/vendor/github.com/containers/image/v5/storage/storage_transport_test.go @@ -0,0 +1,187 @@ +//go:build !containers_image_storage_stub +// +build !containers_image_storage_stub + +package storage + +import ( + "fmt" + "testing" + + "github.com/containers/image/v5/docker/reference" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + sha256digestHex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + sha256Digest2 = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +) + +func TestTransportName(t *testing.T) { + assert.Equal(t, "containers-storage", Transport.Name()) +} + +func TestTransportSetGetStore(t *testing.T) { + Transport.SetStore(nil) + res := Transport.GetStoreIfSet() + assert.Nil(t, res) + store := newStore(t) // Calls SetStore + res = Transport.GetStoreIfSet() + assert.Equal(t, store, res) + Transport.SetStore(nil) +} + +func TestTransportParseStoreReference(t *testing.T) { + const digest3 = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + + store := newStore(t) + + Transport.SetStore(nil) + for _, c := range []struct{ input, expectedRef, expectedID string }{ + {"", "", ""}, // Empty input + // Handling of the store prefix + // FIXME? Should we be silently discarding input like this? + {"[unterminated", "", ""}, // Unterminated store specifier + {"[garbage]busybox", "docker.io/library/busybox:latest", ""}, // Store specifier is overridden by the store we pass to ParseStoreReference + + {"UPPERCASEISINVALID", "", ""}, // Invalid single-component name + {"sha256:" + sha256digestHex, "docker.io/library/sha256:" + sha256digestHex, ""}, // Valid single-component name; the hex part is not an ID unless it has a "@" prefix, so it looks like a tag + // FIXME: This test is now incorrect, this should not fail _if the image ID matches_ + {sha256digestHex, "", ""}, // Invalid single-component ID; not an ID without a "@" prefix, so it's parsed as a name, but names aren't allowed to look like IDs + {"@" + sha256digestHex, "", sha256digestHex}, // Valid single-component ID + {"@sha256:" + sha256digestHex, "", ""}, // Invalid un-named @digest + // "aaaa", either a valid image ID prefix, or a short form of docker.io/library/aaaa, untested + {"sha256:ab", "docker.io/library/sha256:ab", ""}, // Valid single-component name, explicit tag + {"busybox", "docker.io/library/busybox:latest", ""}, // Valid single-component name, implicit tag + {"busybox:notlatest", "docker.io/library/busybox:notlatest", ""}, // Valid single-component name, explicit tag + {"docker.io/library/busybox:notlatest", "docker.io/library/busybox:notlatest", ""}, // Valid single-component name, everything explicit + + {"UPPERCASEISINVALID@" + sha256digestHex, "", ""}, // Invalid name in name@digestOrID + {"busybox@ab", "", ""}, // Invalid ID in name@digestOrID + {"busybox@", "", ""}, // Empty ID in name@digestOrID + {"busybox@sha256:ab", "", ""}, // Invalid digest in name@digestOrID + {"busybox@sha256:" + sha256digestHex, "docker.io/library/busybox@sha256:" + sha256digestHex, ""}, // Valid name@digest, no tag + {"busybox@" + sha256digestHex, "docker.io/library/busybox:latest", sha256digestHex}, // Valid name@ID, implicit tag + // "busybox@aaaa", a valid image ID prefix, untested + {"busybox:notlatest@" + sha256digestHex, "docker.io/library/busybox:notlatest", sha256digestHex}, // Valid name@ID, explicit tag + {"docker.io/library/busybox:notlatest@" + sha256digestHex, "docker.io/library/busybox:notlatest", sha256digestHex}, // Valid name@ID, everything explicit + {"docker.io/library/busybox:notlatest@" + sha256Digest2, "docker.io/library/busybox:notlatest@" + sha256Digest2, ""}, // Valid name:tag@digest, everything explicit + + {"busybox@sha256:" + sha256digestHex + "@ab", "", ""}, // Invalid ID in name@digest@ID + {"busybox@ab@" + sha256digestHex, "", ""}, // Invalid digest in name@digest@ID + {"busybox@@" + sha256digestHex, "", ""}, // Invalid digest in name@digest@ID + {"busybox@" + sha256Digest2 + "@" + sha256digestHex, "docker.io/library/busybox@" + sha256Digest2, sha256digestHex}, // name@digest@ID + {"docker.io/library/busybox@" + sha256Digest2 + "@" + sha256digestHex, "docker.io/library/busybox@" + sha256Digest2, sha256digestHex}, // name@digest@ID, everything explicit + {"docker.io/library/busybox:notlatest@sha256:" + sha256digestHex + "@" + sha256digestHex, "docker.io/library/busybox:notlatest@sha256:" + sha256digestHex, sha256digestHex}, // name:tag@digest@ID, everything explicit + // "busybox@sha256:"+sha256digestHex+"@aaaa", a valid image ID prefix, untested + {"busybox:notlatest@" + sha256Digest2 + "@" + digest3 + "@" + sha256digestHex, "", ""}, // name@digest@ID, with name containing another digest + } { + storageRef, err := Transport.ParseStoreReference(store, c.input) + if c.expectedRef == "" && c.expectedID == "" { + assert.Error(t, err, c.input) + } else { + require.NoError(t, err, c.input) + assert.Equal(t, store, storageRef.transport.store, c.input) + if c.expectedRef == "" { + assert.Nil(t, storageRef.named, c.input) + } else { + dockerRef, err := reference.ParseNormalizedNamed(c.expectedRef) + require.NoError(t, err) + require.NotNil(t, storageRef.named, c.input) + assert.Equal(t, dockerRef.String(), storageRef.named.String()) + } + assert.Equal(t, c.expectedID, storageRef.id, c.input) + } + } +} + +func TestTransportParseReference(t *testing.T) { + store := newStore(t) + driver := store.GraphDriverName() + root := store.GraphRoot() + + for _, c := range []struct{ prefix, expectedDriver, expectedRoot, expectedRunRoot string }{ + {"", driver, root, ""}, // Implicit store location prefix + {"[unterminated", "", "", ""}, // Unterminated store specifier + {"[]", "", "", ""}, // Empty store specifier + {"[relative/path]", "", "", ""}, // Non-absolute graph root path + {"[" + driver + "@relative/path]", "", "", ""}, // Non-absolute graph root path + {"[@" + root + "suffix2]", "", "", ""}, // Empty graph driver + {"[" + driver + "@]", "", "", ""}, // Empty root path + {"[thisisunknown@" + root + "suffix2]", "", "", ""}, // Unknown graph driver + {"[" + root + "suffix1]", "", "", ""}, // A valid root path, but no run dir + {"[" + driver + "@" + root + "suffix3+relative/path]", "", "", ""}, // Non-absolute run dir + {"[" + driver + "@" + root + "suffix3+" + root + "suffix4]", + driver, + root + "suffix3", + root + "suffix4"}, // A valid root@graph+run set + {"[" + driver + "@" + root + "suffix3+" + root + "suffix4:options,options,options]", + driver, + root + "suffix3", + root + "suffix4"}, // A valid root@graph+run+options set + } { + t.Logf("parsing %q", c.prefix+"busybox") + ref, err := Transport.ParseReference(c.prefix + "busybox") + if c.expectedDriver == "" { + assert.Error(t, err, c.prefix) + } else { + require.NoError(t, err, c.prefix) + storageRef, ok := ref.(*storageReference) + require.True(t, ok, c.prefix) + assert.Equal(t, c.expectedDriver, storageRef.transport.store.GraphDriverName(), c.prefix) + assert.Equal(t, c.expectedRoot, storageRef.transport.store.GraphRoot(), c.prefix) + if c.expectedRunRoot != "" { + assert.Equal(t, c.expectedRunRoot, storageRef.transport.store.RunRoot(), c.prefix) + } + } + } +} + +func TestTransportValidatePolicyConfigurationScope(t *testing.T) { + store := newStore(t) + driver := store.GraphDriverName() + root := store.GraphRoot() + storeSpec := fmt.Sprintf("[%s@%s]", driver, root) // As computed in PolicyConfigurationNamespaces + + // Valid inputs + for _, scope := range []string{ + "[" + root + "suffix1]", // driverlessStoreSpec in PolicyConfigurationNamespaces + "[" + driver + "@" + root + "suffix3]", // storeSpec in PolicyConfigurationNamespaces + storeSpec + "@" + sha256digestHex, // ID only + storeSpec + "docker.io", // Host name only + storeSpec + "docker.io/library", // A repository namespace + storeSpec + "docker.io/library/busybox", // A repository name + storeSpec + "docker.io/library/busybox:notlatest", // name:tag + storeSpec + "docker.io/library/busybox:notlatest@" + sha256digestHex, // name@ID + storeSpec + "docker.io/library/busybox@" + sha256Digest2, // name@digest + storeSpec + "docker.io/library/busybox@" + sha256Digest2 + "@" + sha256digestHex, // name@digest@ID + storeSpec + "docker.io/library/busybox:notlatest@" + sha256Digest2, // name:tag@digest + storeSpec + "docker.io/library/busybox:notlatest@" + sha256Digest2 + "@" + sha256digestHex, // name:tag@digest@ID + } { + err := Transport.ValidatePolicyConfigurationScope(scope) + assert.NoError(t, err, scope) + } + + // Invalid inputs + for _, scope := range []string{ + "busybox", // Unprefixed reference + "[unterminated", // Unterminated store specifier + "[]", // Empty store specifier + "[relative/path]", // Non-absolute graph root path + "[" + driver + "@relative/path]", // Non-absolute graph root path + // "[thisisunknown@" + root + "suffix2]", // Unknown graph driver FIXME: validate against storage.ListGraphDrivers() once that's available + storeSpec + "@", // An incomplete two-component name + + storeSpec + "docker.io/library/busybox@sha256:ab", // Invalid digest in name@digest + storeSpec + "docker.io/library/busybox@ab", // Invalid ID in name@ID + storeSpec + "docker.io/library/busybox@", // Empty ID/digest in name@ID + storeSpec + "docker.io/library/busybox@@" + sha256digestHex, // Empty digest in name@digest@ID + storeSpec + "docker.io/library/busybox@ab@" + sha256digestHex, // Invalid digest in name@digest@ID + storeSpec + "docker.io/library/busybox@sha256:ab@" + sha256digestHex, // Invalid digest in name@digest@ID + storeSpec + "docker.io/library/busybox@" + sha256Digest2 + "@", // Empty ID in name@digest@ID + storeSpec + "docker.io/library/busybox@" + sha256Digest2 + "@ab", // Invalid ID in name@digest@ID + } { + err := Transport.ValidatePolicyConfigurationScope(scope) + assert.Error(t, err, scope) + } +} diff --git a/vendor/github.com/containers/image/v5/tarball/tarball_src_test.go b/vendor/github.com/containers/image/v5/tarball/tarball_src_test.go new file mode 100644 index 00000000000..5d9f48cf6f5 --- /dev/null +++ b/vendor/github.com/containers/image/v5/tarball/tarball_src_test.go @@ -0,0 +1,5 @@ +package tarball + +import "github.com/containers/image/v5/internal/private" + +var _ private.ImageSource = (*tarballImageSource)(nil) diff --git a/vendor/github.com/containers/image/v5/transports/alltransports/alltransports_test.go b/vendor/github.com/containers/image/v5/transports/alltransports/alltransports_test.go new file mode 100644 index 00000000000..1a5d17753ab --- /dev/null +++ b/vendor/github.com/containers/image/v5/transports/alltransports/alltransports_test.go @@ -0,0 +1,63 @@ +package alltransports + +import ( + "testing" + + "github.com/containers/image/v5/directory" + "github.com/containers/image/v5/transports" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseImageName(t *testing.T) { + // This primarily tests error handling, TestImageNameHandling is a table-driven + // test for the expected values. + for _, name := range []string{ + "", // Empty + "busybox", // No transport name + ":busybox", // Empty transport name + "docker:", // Empty transport reference + } { + _, err := ParseImageName(name) + assert.Error(t, err, name) + } +} + +// A table-driven test summarizing the various transports' behavior. +func TestImageNameHandling(t *testing.T) { + // Always registered transports + for _, c := range []struct{ transport, input, roundtrip string }{ + {"dir", "/etc", "/etc"}, + {"docker", "//busybox", "//busybox:latest"}, + {"docker", "//busybox:notlatest", "//busybox:notlatest"}, // This also tests handling of multiple ":" characters + {"docker-archive", "/var/lib/oci/busybox.tar:busybox:latest", "/var/lib/oci/busybox.tar:docker.io/library/busybox:latest"}, + {"docker-archive", "busybox.tar:busybox:latest", "busybox.tar:docker.io/library/busybox:latest"}, + {"oci", "/etc:someimage", "/etc:someimage"}, + {"oci", "/etc:someimage:mytag", "/etc:someimage:mytag"}, + {"oci-archive", "/etc:someimage", "/etc:someimage"}, + {"oci-archive", "/etc:someimage:mytag", "/etc:someimage:mytag"}, + // "atomic" not tested here because it depends on per-user configuration for the default cluster. + // "containers-storage" not tested here because it needs to initialize various directories on the fs. + } { + fullInput := c.transport + ":" + c.input + ref, err := ParseImageName(fullInput) + require.NoError(t, err, fullInput) + s := transports.ImageName(ref) + assert.Equal(t, c.transport+":"+c.roundtrip, s, fullInput) + } + + // Possibly stubbed-out transports: Only verify that something is registered. + for _, c := range []string{"docker-daemon", "ostree"} { + transport := transports.Get(c) + assert.NotNil(t, transport, c) + } +} + +func TestTransportFromImageName(t *testing.T) { + dirTransport := TransportFromImageName("dir:/tmp/test") + assert.Equal(t, dirTransport.Name(), directory.Transport.Name()) + unknownTransport := TransportFromImageName("unknown:ref:test") + assert.Equal(t, unknownTransport, nil) + invalidName := TransportFromImageName("unknown") + assert.Equal(t, invalidName, nil) +} diff --git a/vendor/github.com/containers/image/v5/transports/stub_test.go b/vendor/github.com/containers/image/v5/transports/stub_test.go new file mode 100644 index 00000000000..f181a1ace90 --- /dev/null +++ b/vendor/github.com/containers/image/v5/transports/stub_test.go @@ -0,0 +1,18 @@ +package transports + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestStubTransport(t *testing.T) { + const name = "whatever" + + s := NewStubTransport(name) + assert.Equal(t, name, s.Name()) + _, err := s.ParseReference("this is rejected regardless of content") + assert.Error(t, err) + err = s.ValidatePolicyConfigurationScope("this is accepted regardless of content") + assert.NoError(t, err) +} diff --git a/vendor/github.com/containers/image/v5/version/version.go b/vendor/github.com/containers/image/v5/version/version.go index 3c8fc094d0c..c270910b0f0 100644 --- a/vendor/github.com/containers/image/v5/version/version.go +++ b/vendor/github.com/containers/image/v5/version/version.go @@ -6,12 +6,12 @@ const ( // VersionMajor is for an API incompatible changes VersionMajor = 5 // VersionMinor is for functionality in a backwards-compatible manner - VersionMinor = 26 + VersionMinor = 27 // VersionPatch is for backwards-compatible bug fixes - VersionPatch = 1 + VersionPatch = 0 // VersionDev indicates development branch. Releases will be empty string. - VersionDev = "" + VersionDev = "-dev" ) // Version is the specification version that the package types support.