diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index ac2ff6ae..d5bffed6 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -62,6 +62,10 @@ jobs: GIT_REPOSITORY_NAME: cluster-stacks GIT_ACCESS_TOKEN: ${{ secrets.GIT_ACCESS_TOKEN }} ENCODED_CLOUDS_YAML: ${{ secrets.ENCODED_CLOUDS_YAML }} + OCI_REGISTRY: registry.scs.community + OCI_REPOSITORY: registry.scs.community/csctl-oci/openstack + OCI_USERNAME: ${{ secrets.OCI_USERNAME }} + OCI_PASSWORD: ${{ secrets.OCI_PASSWORD }} run: make test-integration # - name: Create Report diff --git a/Makefile b/Makefile index eb1dbdc9..3d0f715e 100644 --- a/Makefile +++ b/Makefile @@ -494,7 +494,7 @@ generate-modules-ci: generate-modules KUBEBUILDER_ASSETS ?= $(shell $(SETUP_ENVTEST) use --use-env --bin-dir $(abspath $(TOOLS_BIN_DIR)) -p path $(KUBEBUILDER_ENVTEST_KUBERNETES_VERSION)) .PHONY: test-integration ## Run integration tests -test-integration: test-integration-github test-integration-openstack +test-integration: test-integration-openstack test-integration-github test-integration-oci echo done .PHONY: test-unit @@ -513,6 +513,12 @@ test-integration-github: $(SETUP_ENVTEST) $(GOTESTSUM) KUBEBUILDER_ASSETS="$(KUBEBUILDER_ASSETS)" $(GOTESTSUM) --junitfile=.coverage/junit.xml --format testname -- -mod=vendor \ -covermode=atomic -coverprofile=.coverage/cover.out -p=1 ./internal/test/integration/github/... +.PHONY: test-integration-oci +test-integration-oci: $(SETUP_ENVTEST) $(GOTESTSUM) + @mkdir -p $(shell pwd)/.coverage + KUBEBUILDER_ASSETS="$(KUBEBUILDER_ASSETS)" $(GOTESTSUM) --junitfile=.coverage/junit.xml --format testname -- -mod=vendor \ + -covermode=atomic -coverprofile=.coverage/cover.out -p=1 ./internal/test/integration/oci/... + .PHONY: test-integration-openstack test-integration-openstack: $(SETUP_ENVTEST) $(GOTESTSUM) @mkdir -p $(shell pwd)/.coverage diff --git a/cmd/main.go b/cmd/main.go index 64f3703a..01d74470 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -19,6 +19,7 @@ package main // Import packages including all Kubernetes client auth plugins: k8s.io/client-go/plugin/pkg/client/auth. import ( + "errors" "flag" "os" "time" @@ -26,6 +27,7 @@ import ( "github.com/SovereignCloudStack/cluster-stack-operator/pkg/assetsclient" "github.com/SovereignCloudStack/cluster-stack-operator/pkg/assetsclient/fake" "github.com/SovereignCloudStack/cluster-stack-operator/pkg/assetsclient/github" + "github.com/SovereignCloudStack/cluster-stack-operator/pkg/assetsclient/oci" apiv1alpha1 "github.com/SovereignCloudStack/cluster-stack-provider-openstack/api/v1alpha1" "github.com/SovereignCloudStack/cluster-stack-provider-openstack/internal/controller" "k8s.io/apimachinery/pkg/runtime" @@ -54,6 +56,7 @@ var ( releaseDir string imageImportTimeout int localMode bool + source string metricsAddr string enableLeaderElection bool probeAddr string @@ -71,6 +74,7 @@ func main() { flag.StringVar(&releaseDir, "release-dir", "/tmp/downloads/", "Specify release directory for cluster-stack releases") flag.IntVar(&imageImportTimeout, "image-import-timeout", 0, "Maximum time in minutes that you allow cspo to import image. If image-import-timeout <= 0, cspo waits forever.") flag.BoolVar(&localMode, "local", false, "Enable local mode where no release assets will be downloaded from a remote repository. Useful for implementing cluster stacks.") + flag.StringVar(&source, "source", "github", "Specifies the source from which release assets would be downloaded. Allowed sources are 'github' and 'oci'") opts := zap.Options{ Development: true, @@ -105,7 +109,15 @@ func main() { if localMode { assetsClientFactory = fake.NewFactory() } else { - assetsClientFactory = github.NewFactory() + switch source { + case "oci": + assetsClientFactory = oci.NewFactory() + case "github": + assetsClientFactory = github.NewFactory() + default: + setupLog.Error(errors.New("invalid asset source"), "no valid source specified, allowed sources are 'github' and 'oci'") + os.Exit(1) + } } if err = (&controller.OpenStackClusterStackReleaseReconciler{ diff --git a/config/manager/credentials.yaml b/config/manager/credentials.yaml index c8211f0a..6b7a14a8 100644 --- a/config/manager/credentials.yaml +++ b/config/manager/credentials.yaml @@ -9,3 +9,6 @@ data: git-org-name: ${GIT_ORG_NAME_B64:=""} git-repo-name: ${GIT_REPOSITORY_NAME_B64:=""} git-access-token: ${GIT_ACCESS_TOKEN_B64:=""} + oci-registry: ${OCI_REGISTRY_B64:=""} + oci-repository: ${OCI_REPOSITORY_B64:=""} + oci-access-token: ${OCI_ACCESS_TOKEN_B64:=""} diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index fc2e8f5b..7f93ad6d 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -62,6 +62,21 @@ spec: secretKeyRef: name: cspo-cluster-stack-variables key: git-access-token + - name: OCI_REGISTRY + valueFrom: + secretKeyRef: + name: cso-cluster-stack-variables + key: oci-registry + - name: OCI_REPOSITORY + valueFrom: + secretKeyRef: + name: cso-cluster-stack-variables + key: oci-repository + - name: OCI_ACCESS_TOKEN + valueFrom: + secretKeyRef: + name: cso-cluster-stack-variables + key: oci-access-token image: controller:latest name: manager securityContext: diff --git a/go.mod b/go.mod index b100c57c..24fd376e 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.21.6 toolchain go1.22.1 require ( - github.com/SovereignCloudStack/cluster-stack-operator v0.1.0-alpha.6 + github.com/SovereignCloudStack/cluster-stack-operator v0.1.0-alpha.6.0.20240828081204-d479eab19273 github.com/gophercloud/gophercloud/v2 v2.0.0-beta.2 github.com/gophercloud/utils/v2 v2.0.0-20240305212012-b57aefba4cdb github.com/onsi/ginkgo/v2 v2.19.0 @@ -60,6 +60,8 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.18.0 // indirect @@ -75,6 +77,7 @@ require ( golang.org/x/mod v0.18.0 // indirect golang.org/x/net v0.25.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.20.0 // indirect golang.org/x/term v0.20.0 // indirect golang.org/x/text v0.15.0 // indirect @@ -89,6 +92,7 @@ require ( k8s.io/component-base v0.29.3 // indirect k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect k8s.io/utils v0.0.0-20231127182322-b307cd553661 // indirect + oras.land/oras-go/v2 v2.5.0 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect ) diff --git a/go.sum b/go.sum index b5592b71..17a17ffa 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,8 @@ github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA= github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= -github.com/SovereignCloudStack/cluster-stack-operator v0.1.0-alpha.6 h1:n+QpmV8C+rZZ5+nvA5PytpWWnD5Di/Xcyzdqih5jnzw= -github.com/SovereignCloudStack/cluster-stack-operator v0.1.0-alpha.6/go.mod h1:9tPLUQFLeoDb+LWErQYqfbIj+9TIOy9E4qyQOxmTHik= +github.com/SovereignCloudStack/cluster-stack-operator v0.1.0-alpha.6.0.20240828081204-d479eab19273 h1:HtnEWP9sKVxm8gQrN+93kh+SyfXMzbdKk/p/xLKttoA= +github.com/SovereignCloudStack/cluster-stack-operator v0.1.0-alpha.6.0.20240828081204-d479eab19273/go.mod h1:eERjYBSOckgJ6Z2bUW1RxR3dh6bRVxUq6LWIHkIKu/k= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= @@ -63,8 +63,8 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/cel-go v0.19.0 h1:vVgaZoHPBDd1lXCYGQOh5A06L4EtuIfmqQ/qnSXSKiU= -github.com/google/cel-go v0.19.0/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg= +github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84= +github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -130,6 +130,8 @@ github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= 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= @@ -268,6 +270,8 @@ k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/A k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= k8s.io/utils v0.0.0-20231127182322-b307cd553661 h1:FepOBzJ0GXm8t0su67ln2wAZjbQ6RxQGZDnzuLcrUTI= k8s.io/utils v0.0.0-20231127182322-b307cd553661/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +oras.land/oras-go/v2 v2.5.0 h1:o8Me9kLY74Vp5uw07QXPiitjsw7qNXi8Twd+19Zf02c= +oras.land/oras-go/v2 v2.5.0/go.mod h1:z4eisnLP530vwIOUOJeBIj0aGI0L1C3d53atvCBqZHg= sigs.k8s.io/cluster-api v1.7.2 h1:bRE8zoao7ajuLC0HijqfZVcubKQCPlZ04HMgcA53FGE= sigs.k8s.io/cluster-api v1.7.2/go.mod h1:V9ZhKLvQtsDODwjXOKgbitjyCmC71yMBwDcMyNNIov0= sigs.k8s.io/cluster-api-provider-openstack v0.9.0 h1:ScwZIfT1kI88+qMzeO7ppMP9DvEzrfLHuYPg2p1mcho= diff --git a/internal/test/integration/oci/integration_suite_test.go b/internal/test/integration/oci/integration_suite_test.go new file mode 100644 index 00000000..a3e85850 --- /dev/null +++ b/internal/test/integration/oci/integration_suite_test.go @@ -0,0 +1,64 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package oci + +import ( + "testing" + "time" + + ociclient "github.com/SovereignCloudStack/cluster-stack-operator/pkg/assetsclient/oci" + "github.com/SovereignCloudStack/cluster-stack-provider-openstack/internal/controller" + "github.com/SovereignCloudStack/cluster-stack-provider-openstack/internal/test/helpers" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + ctrl "sigs.k8s.io/controller-runtime" +) + +const ( + timeout = time.Second * 10 + interval = 1000 * time.Millisecond + releaseDir = "/tmp/downloads" +) + +func TestControllers(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Controller Suite") +} + +var ( + ctx = ctrl.SetupSignalHandler() + testEnv *helpers.TestEnvironment +) + +var _ = BeforeSuite(func() { + testEnv = helpers.NewTestEnvironment() + Expect((&controller.OpenStackClusterStackReleaseReconciler{ + Client: testEnv.Manager.GetClient(), + AssetsClientFactory: ociclient.NewFactory(), + ReleaseDirectory: releaseDir, + }).SetupWithManager(testEnv.Manager)).To(Succeed()) + + go func() { + defer GinkgoRecover() + Expect(testEnv.StartManager(ctx)).To(Succeed()) + }() + <-testEnv.Manager.Elected() +}) + +var _ = AfterSuite(func() { + Expect(testEnv.Stop()).To(Succeed()) +}) diff --git a/internal/test/integration/oci/integration_test.go b/internal/test/integration/oci/integration_test.go new file mode 100644 index 00000000..8bf73560 --- /dev/null +++ b/internal/test/integration/oci/integration_test.go @@ -0,0 +1,109 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package oci + +import ( + "encoding/base64" + "os" + + "github.com/SovereignCloudStack/cluster-stack-operator/pkg/test/utils" + cspov1alpha1 "github.com/SovereignCloudStack/cluster-stack-provider-openstack/api/v1alpha1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + apiv1alpha7 "sigs.k8s.io/cluster-api-provider-openstack/api/v1alpha7" + clusterv1beta1 "sigs.k8s.io/cluster-api/api/v1beta1" +) + +var _ = Describe("OpenStackClusterStackReleaseReconciler", func() { + Context("test", func() { + var ( + openStackClusterStackRelease *cspov1alpha1.OpenStackClusterStackRelease + testNs *corev1.Namespace + openstackClusterStackReleaseKey types.NamespacedName + ) + + BeforeEach(func() { + var err error + testNs, err = testEnv.CreateNamespace(ctx, "oscsr-integration") + Expect(err).NotTo(HaveOccurred()) + + openstackClusterStackReleaseKey = types.NamespacedName{Name: "openstack-scs-1-28-v0-sha-umng5ie", Namespace: testNs.Name} + + cloudsYAMLBase64 := os.Getenv("ENCODED_CLOUDS_YAML") + cloudsYAMLData, err := base64.StdEncoding.DecodeString(cloudsYAMLBase64) + Expect(err).NotTo(HaveOccurred()) + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "supersecret", + Namespace: testNs.Name, + }, + Data: map[string][]byte{ + "clouds.yaml": cloudsYAMLData, + }, + } + Expect(testEnv.Create(ctx, secret)).To(Succeed()) + + openStackClusterStackRelease = &cspov1alpha1.OpenStackClusterStackRelease{ + TypeMeta: metav1.TypeMeta{ + Kind: "OpenStackClusterStackRelease", + APIVersion: "infrastructure.clusterstack.x-k8s.io", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "openstack-scs-1-28-v0-sha-umng5ie", + Namespace: testNs.Name, + }, + Spec: cspov1alpha1.OpenStackClusterStackReleaseSpec{ + IdentityRef: &apiv1alpha7.OpenStackIdentityReference{ + Kind: "Secret", + Name: "supersecret", + }, + }, + Status: cspov1alpha1.OpenStackClusterStackReleaseStatus{ + Ready: false, + Conditions: clusterv1beta1.Conditions{}, + }, + } + Expect(testEnv.Create(ctx, openStackClusterStackRelease)).To(Succeed()) + }) + + AfterEach(func() { + Expect(os.RemoveAll(releaseDir)).To(Succeed()) + Eventually(func() error { + return testEnv.Cleanup(ctx, testNs, openStackClusterStackRelease) + }, timeout, interval).Should(BeNil()) + }) + + It("creates the OpenStackNodeImageRelease object", func() { + openstackNodeImageReleaseName := types.NamespacedName{Name: "openstack-scs-1-28-ubuntu-capi-image-v1.28.11-v0-sha-umng5ie", Namespace: testNs.Name} + Eventually(func() error { + var openStackNodeImageRelease cspov1alpha1.OpenStackNodeImageRelease + return testEnv.Get(ctx, openstackNodeImageReleaseName, &openStackNodeImageRelease) + }, timeout, interval).Should(BeNil()) + }) + + It("sets ClusterStackReleaseAssetsReadyCondition condition once OpenStackClusterStackRelease object is created", func() { + Eventually(func() bool { + var openStackClusterStackRelease cspov1alpha1.OpenStackClusterStackRelease + return utils.IsPresentAndTrue(ctx, testEnv.Client, openstackClusterStackReleaseKey, &openStackClusterStackRelease, cspov1alpha1.ClusterStackReleaseAssetsReadyCondition) + }, timeout, interval).Should(BeTrue()) + }) + }) +}) diff --git a/internal/test/integration/openstack/controller_suite_test.go b/internal/test/integration/openstack/controller_suite_test.go index 149ea99b..5524664e 100644 --- a/internal/test/integration/openstack/controller_suite_test.go +++ b/internal/test/integration/openstack/controller_suite_test.go @@ -20,7 +20,6 @@ import ( "testing" "time" - githubclient "github.com/SovereignCloudStack/cluster-stack-operator/pkg/assetsclient/github" "github.com/SovereignCloudStack/cluster-stack-provider-openstack/internal/controller" "github.com/SovereignCloudStack/cluster-stack-provider-openstack/internal/test/helpers" . "github.com/onsi/ginkgo/v2" @@ -50,7 +49,7 @@ var _ = BeforeSuite(func() { Expect((&controller.OpenStackClusterStackReleaseReconciler{ Client: testEnv.Manager.GetClient(), - AssetsClientFactory: githubclient.NewFactory(), + AssetsClientFactory: testEnv.AssetsClientFactory, ReleaseDirectory: "./../../../../test/releases", }).SetupWithManager(testEnv.Manager)).To(Succeed()) diff --git a/vendor/github.com/SovereignCloudStack/cluster-stack-operator/pkg/assetsclient/oci/client.go b/vendor/github.com/SovereignCloudStack/cluster-stack-operator/pkg/assetsclient/oci/client.go new file mode 100644 index 00000000..d7218d63 --- /dev/null +++ b/vendor/github.com/SovereignCloudStack/cluster-stack-operator/pkg/assetsclient/oci/client.go @@ -0,0 +1,99 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package oci + +import ( + "context" + "errors" + "fmt" + + "github.com/SovereignCloudStack/cluster-stack-operator/pkg/assetsclient" + "oras.land/oras-go/v2" + "oras.land/oras-go/v2/content/file" + "oras.land/oras-go/v2/registry" + "oras.land/oras-go/v2/registry/remote" + "oras.land/oras-go/v2/registry/remote/auth" +) + +type ociClient struct { + repository *remote.Repository +} + +type factory struct{} + +// NewFactory returns a new factory for OCI clients. +func NewFactory() assetsclient.Factory { + return &factory{} +} + +var _ = assetsclient.Factory(&factory{}) + +var _ = assetsclient.Client(&ociClient{}) + +func (*factory) NewClient(ctx context.Context) (assetsclient.Client, error) { + _ = ctx + config, err := newOCIConfig() + if err != nil { + return nil, fmt.Errorf("failed to create OCI config: %w", err) + } + + client := auth.Client{ + Credential: auth.StaticCredential(config.registry, auth.Credential{ + AccessToken: config.accessToken, + Username: config.username, + Password: config.password, + }), + } + + repository, err := remote.NewRepository(config.repository) + if err != nil { + return nil, fmt.Errorf("failed to create OCI client to remote repository %s: %w", config.repository, err) + } + + repository.Client = &client + return &ociClient{repository: repository}, nil +} + +func (c *ociClient) ListRelease(ctx context.Context) ([]string, error) { + tags, err := registry.Tags(ctx, c.repository) + if err != nil { + return nil, fmt.Errorf("failed to list tags: %w", err) + } + + return tags, nil +} + +func (c *ociClient) DownloadReleaseAssets(ctx context.Context, tag, path string) (reterr error) { + dest, err := file.New(path) + if err != nil { + return fmt.Errorf("failed to create file store: %w", err) + } + + defer func() { + err := dest.Close() + if err != nil { + reterr = errors.Join(reterr, err) + } + }() + + _, err = oras.Copy(ctx, c.repository, tag, dest, tag, oras.DefaultCopyOptions) + if err != nil { + return fmt.Errorf("failed to copy repository artifacts to path %s: %w", path, err) + } + + return nil +} diff --git a/vendor/github.com/SovereignCloudStack/cluster-stack-operator/pkg/assetsclient/oci/credentials.go b/vendor/github.com/SovereignCloudStack/cluster-stack-operator/pkg/assetsclient/oci/credentials.go new file mode 100644 index 00000000..74edecd9 --- /dev/null +++ b/vendor/github.com/SovereignCloudStack/cluster-stack-operator/pkg/assetsclient/oci/credentials.go @@ -0,0 +1,76 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package oci provides utilities for comunicating with the OCI registry. +package oci + +import ( + "encoding/base64" + "fmt" + "os" +) + +const ( + envOCIRegistry = "OCI_REGISTRY" + envOCIRepository = "OCI_REPOSITORY" + envOCIAccessToken = "OCI_ACCESS_TOKEN" + envOCIUsername = "OCI_USERNAME" + envOCIPassword = "OCI_PASSWORD" +) + +type ociConfig struct { + registry string + repository string + accessToken string + username string + password string +} + +func newOCIConfig() (ociConfig, error) { + var config ociConfig + + val := os.Getenv(envOCIRegistry) + if val == "" { + return ociConfig{}, fmt.Errorf("environment variable %s is not set", envOCIRegistry) + } + config.registry = val + + val = os.Getenv(envOCIRepository) + if val == "" { + return ociConfig{}, fmt.Errorf("environment variable %s is not set", envOCIRepository) + } + config.repository = val + + val = os.Getenv(envOCIAccessToken) + if val != "" { + base64AccessToken := base64.StdEncoding.EncodeToString([]byte(val)) + config.accessToken = base64AccessToken + } else { + val = os.Getenv(envOCIUsername) + if val == "" { + return ociConfig{}, fmt.Errorf("environment variable %s is not set", envOCIUsername) + } + config.username = val + + val = os.Getenv(envOCIPassword) + if val == "" { + return ociConfig{}, fmt.Errorf("environment variable %s is not set", envOCIPassword) + } + config.password = val + } + + return config, nil +} diff --git a/vendor/github.com/opencontainers/go-digest/.mailmap b/vendor/github.com/opencontainers/go-digest/.mailmap new file mode 100644 index 00000000..eaf8b2f9 --- /dev/null +++ b/vendor/github.com/opencontainers/go-digest/.mailmap @@ -0,0 +1,4 @@ +Aaron Lehmann +Derek McGowan +Stephen J Day +Haibing Zhou diff --git a/vendor/github.com/opencontainers/go-digest/.pullapprove.yml b/vendor/github.com/opencontainers/go-digest/.pullapprove.yml new file mode 100644 index 00000000..b6165f83 --- /dev/null +++ b/vendor/github.com/opencontainers/go-digest/.pullapprove.yml @@ -0,0 +1,28 @@ +version: 2 + +requirements: + signed_off_by: + required: true + +always_pending: + title_regex: '^WIP' + explanation: 'Work in progress...' + +group_defaults: + required: 2 + approve_by_comment: + enabled: true + approve_regex: '^LGTM' + reject_regex: '^Rejected' + reset_on_push: + enabled: true + author_approval: + ignored: true + conditions: + branches: + - master + +groups: + go-digest: + teams: + - go-digest-maintainers diff --git a/vendor/github.com/opencontainers/go-digest/.travis.yml b/vendor/github.com/opencontainers/go-digest/.travis.yml new file mode 100644 index 00000000..5775f885 --- /dev/null +++ b/vendor/github.com/opencontainers/go-digest/.travis.yml @@ -0,0 +1,5 @@ +language: go +go: + - 1.12.x + - 1.13.x + - master diff --git a/vendor/github.com/opencontainers/go-digest/CONTRIBUTING.md b/vendor/github.com/opencontainers/go-digest/CONTRIBUTING.md new file mode 100644 index 00000000..e4d962ac --- /dev/null +++ b/vendor/github.com/opencontainers/go-digest/CONTRIBUTING.md @@ -0,0 +1,72 @@ +# Contributing to Docker open source projects + +Want to hack on this project? Awesome! Here are instructions to get you started. + +This project is a part of the [Docker](https://www.docker.com) project, and follows +the same rules and principles. If you're already familiar with the way +Docker does things, you'll feel right at home. + +Otherwise, go read Docker's +[contributions guidelines](https://github.com/docker/docker/blob/master/CONTRIBUTING.md), +[issue triaging](https://github.com/docker/docker/blob/master/project/ISSUE-TRIAGE.md), +[review process](https://github.com/docker/docker/blob/master/project/REVIEWING.md) and +[branches and tags](https://github.com/docker/docker/blob/master/project/BRANCHES-AND-TAGS.md). + +For an in-depth description of our contribution process, visit the +contributors guide: [Understand how to contribute](https://docs.docker.com/opensource/workflow/make-a-contribution/) + +### Sign your work + +The sign-off is a simple 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 pretty simple: if you can certify +the below (from [developercertificate.org](http://developercertificate.org/)): + +``` +Developer Certificate of Origin +Version 1.1 + +Copyright (C) 2004, 2006 The Linux Foundation and its contributors. +1 Letterman Drive +Suite D4700 +San Francisco, CA, 94129 + +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`. diff --git a/vendor/github.com/opencontainers/go-digest/LICENSE b/vendor/github.com/opencontainers/go-digest/LICENSE new file mode 100644 index 00000000..3ac8ab64 --- /dev/null +++ b/vendor/github.com/opencontainers/go-digest/LICENSE @@ -0,0 +1,192 @@ + + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2019, 2020 OCI Contributors + Copyright 2016 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/opencontainers/go-digest/LICENSE.docs b/vendor/github.com/opencontainers/go-digest/LICENSE.docs new file mode 100644 index 00000000..e26cd4fc --- /dev/null +++ b/vendor/github.com/opencontainers/go-digest/LICENSE.docs @@ -0,0 +1,425 @@ +Attribution-ShareAlike 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More_considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution-ShareAlike 4.0 International Public +License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution-ShareAlike 4.0 International Public License ("Public +License"). To the extent this Public License may be interpreted as a +contract, You are granted the Licensed Rights in consideration of Your +acceptance of these terms and conditions, and the Licensor grants You +such rights in consideration of benefits the Licensor receives from +making the Licensed Material available under these terms and +conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. BY-SA Compatible License means a license listed at + creativecommons.org/compatiblelicenses, approved by Creative + Commons as essentially the equivalent of this Public License. + + d. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + e. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + f. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + g. License Elements means the license attributes listed in the name + of a Creative Commons Public License. The License Elements of this + Public License are Attribution and ShareAlike. + + h. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + i. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + j. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + k. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + l. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + m. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part; and + + b. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. Additional offer from the Licensor -- Adapted Material. + Every recipient of Adapted Material from You + automatically receives an offer from the Licensor to + exercise the Licensed Rights in the Adapted Material + under the conditions of the Adapter's License You apply. + + c. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + b. ShareAlike. + + In addition to the conditions in Section 3(a), if You Share + Adapted Material You produce, the following conditions also apply. + + 1. The Adapter's License You apply must be a Creative Commons + license with the same License Elements, this version or + later, or a BY-SA Compatible License. + + 2. You must include the text of, or the URI or hyperlink to, the + Adapter's License You apply. You may satisfy this condition + in any reasonable manner based on the medium, means, and + context in which You Share Adapted Material. + + 3. You may not offer or impose any additional or different terms + or conditions on, or apply any Effective Technological + Measures to, Adapted Material that restrict exercise of the + rights granted under the Adapter's License You apply. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material, + + including for purposes of Section 3(b); and + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + + +======================================================================= + +Creative Commons is not a party to its public licenses. +Notwithstanding, Creative Commons may elect to apply one of its public +licenses to material it publishes and in those instances will be +considered the "Licensor." Except for the limited purpose of indicating +that material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the public +licenses. + +Creative Commons may be contacted at creativecommons.org. diff --git a/vendor/github.com/opencontainers/go-digest/MAINTAINERS b/vendor/github.com/opencontainers/go-digest/MAINTAINERS new file mode 100644 index 00000000..843b1b20 --- /dev/null +++ b/vendor/github.com/opencontainers/go-digest/MAINTAINERS @@ -0,0 +1,5 @@ +Derek McGowan (@dmcgowan) +Stephen Day (@stevvooe) +Vincent Batts (@vbatts) +Akihiro Suda (@AkihiroSuda) +Sebastiaan van Stijn (@thaJeztah) diff --git a/vendor/github.com/opencontainers/go-digest/README.md b/vendor/github.com/opencontainers/go-digest/README.md new file mode 100644 index 00000000..a1128720 --- /dev/null +++ b/vendor/github.com/opencontainers/go-digest/README.md @@ -0,0 +1,96 @@ +# go-digest + +[![GoDoc](https://godoc.org/github.com/opencontainers/go-digest?status.svg)](https://godoc.org/github.com/opencontainers/go-digest) [![Go Report Card](https://goreportcard.com/badge/github.com/opencontainers/go-digest)](https://goreportcard.com/report/github.com/opencontainers/go-digest) [![Build Status](https://travis-ci.org/opencontainers/go-digest.svg?branch=master)](https://travis-ci.org/opencontainers/go-digest) + +Common digest package used across the container ecosystem. + +Please see the [godoc](https://godoc.org/github.com/opencontainers/go-digest) for more information. + +# What is a digest? + +A digest is just a [hash](https://en.wikipedia.org/wiki/Hash_function). + +The most common use case for a digest is to create a content identifier for use in [Content Addressable Storage](https://en.wikipedia.org/wiki/Content-addressable_storage) systems: + +```go +id := digest.FromBytes([]byte("my content")) +``` + +In the example above, the id can be used to uniquely identify the byte slice "my content". +This allows two disparate applications to agree on a verifiable identifier without having to trust one another. + +An identifying digest can be verified, as follows: + +```go +if id != digest.FromBytes([]byte("my content")) { + return errors.New("the content has changed!") +} +``` + +A `Verifier` type can be used to handle cases where an `io.Reader` makes more sense: + +```go +rd := getContent() +verifier := id.Verifier() +io.Copy(verifier, rd) + +if !verifier.Verified() { + return errors.New("the content has changed!") +} +``` + +Using [Merkle DAGs](https://en.wikipedia.org/wiki/Merkle_tree), this can power a rich, safe, content distribution system. + +# Usage + +While the [godoc](https://godoc.org/github.com/opencontainers/go-digest) is considered the best resource, a few important items need to be called out when using this package. + +1. Make sure to import the hash implementations into your application or the package will panic. + You should have something like the following in the main (or other entrypoint) of your application: + + ```go + import ( + _ "crypto/sha256" + _ "crypto/sha512" + ) + ``` + This may seem inconvenient but it allows you replace the hash + implementations with others, such as https://github.com/stevvooe/resumable. + +2. Even though `digest.Digest` may be assemblable as a string, _always_ verify your input with `digest.Parse` or use `Digest.Validate` when accepting untrusted input. + While there are measures to avoid common problems, this will ensure you have valid digests in the rest of your application. + +3. While alternative encodings of hash values (digests) are possible (for example, base64), this package deals exclusively with hex-encoded digests. + +# Stability + +The Go API, at this stage, is considered stable, unless otherwise noted. + +As always, before using a package export, read the [godoc](https://godoc.org/github.com/opencontainers/go-digest). + +# Contributing + +This package is considered fairly complete. +It has been in production in thousands (millions?) of deployments and is fairly battle-hardened. +New additions will be met with skepticism. +If you think there is a missing feature, please file a bug clearly describing the problem and the alternatives you tried before submitting a PR. + +## Code of Conduct + +Participation in the OpenContainers community is governed by [OpenContainer's Code of Conduct][code-of-conduct]. + +## Security + +If you find an issue, please follow the [security][security] protocol to report it. + +# Copyright and license + +Copyright © 2019, 2020 OCI Contributors +Copyright © 2016 Docker, Inc. +All rights reserved, except as follows. +Code is released under the [Apache 2.0 license](LICENSE). +This `README.md` file and the [`CONTRIBUTING.md`](CONTRIBUTING.md) file are licensed under the Creative Commons Attribution 4.0 International License under the terms and conditions set forth in the file [`LICENSE.docs`](LICENSE.docs). +You may obtain a duplicate copy of the same license, titled CC BY-SA 4.0, at http://creativecommons.org/licenses/by-sa/4.0/. + +[security]: https://github.com/opencontainers/org/blob/master/security +[code-of-conduct]: https://github.com/opencontainers/org/blob/master/CODE_OF_CONDUCT.md diff --git a/vendor/github.com/opencontainers/go-digest/algorithm.go b/vendor/github.com/opencontainers/go-digest/algorithm.go new file mode 100644 index 00000000..490951dc --- /dev/null +++ b/vendor/github.com/opencontainers/go-digest/algorithm.go @@ -0,0 +1,193 @@ +// Copyright 2019, 2020 OCI Contributors +// Copyright 2017 Docker, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package digest + +import ( + "crypto" + "fmt" + "hash" + "io" + "regexp" +) + +// Algorithm identifies and implementation of a digester by an identifier. +// Note the that this defines both the hash algorithm used and the string +// encoding. +type Algorithm string + +// supported digest types +const ( + SHA256 Algorithm = "sha256" // sha256 with hex encoding (lower case only) + SHA384 Algorithm = "sha384" // sha384 with hex encoding (lower case only) + SHA512 Algorithm = "sha512" // sha512 with hex encoding (lower case only) + + // Canonical is the primary digest algorithm used with the distribution + // project. Other digests may be used but this one is the primary storage + // digest. + Canonical = SHA256 +) + +var ( + // TODO(stevvooe): Follow the pattern of the standard crypto package for + // registration of digests. Effectively, we are a registerable set and + // common symbol access. + + // algorithms maps values to hash.Hash implementations. Other algorithms + // may be available but they cannot be calculated by the digest package. + algorithms = map[Algorithm]crypto.Hash{ + SHA256: crypto.SHA256, + SHA384: crypto.SHA384, + SHA512: crypto.SHA512, + } + + // anchoredEncodedRegexps contains anchored regular expressions for hex-encoded digests. + // Note that /A-F/ disallowed. + anchoredEncodedRegexps = map[Algorithm]*regexp.Regexp{ + SHA256: regexp.MustCompile(`^[a-f0-9]{64}$`), + SHA384: regexp.MustCompile(`^[a-f0-9]{96}$`), + SHA512: regexp.MustCompile(`^[a-f0-9]{128}$`), + } +) + +// Available returns true if the digest type is available for use. If this +// returns false, Digester and Hash will return nil. +func (a Algorithm) Available() bool { + h, ok := algorithms[a] + if !ok { + return false + } + + // check availability of the hash, as well + return h.Available() +} + +func (a Algorithm) String() string { + return string(a) +} + +// Size returns number of bytes returned by the hash. +func (a Algorithm) Size() int { + h, ok := algorithms[a] + if !ok { + return 0 + } + return h.Size() +} + +// Set implemented to allow use of Algorithm as a command line flag. +func (a *Algorithm) Set(value string) error { + if value == "" { + *a = Canonical + } else { + // just do a type conversion, support is queried with Available. + *a = Algorithm(value) + } + + if !a.Available() { + return ErrDigestUnsupported + } + + return nil +} + +// Digester returns a new digester for the specified algorithm. If the algorithm +// does not have a digester implementation, nil will be returned. This can be +// checked by calling Available before calling Digester. +func (a Algorithm) Digester() Digester { + return &digester{ + alg: a, + hash: a.Hash(), + } +} + +// Hash returns a new hash as used by the algorithm. If not available, the +// method will panic. Check Algorithm.Available() before calling. +func (a Algorithm) Hash() hash.Hash { + if !a.Available() { + // Empty algorithm string is invalid + if a == "" { + panic(fmt.Sprintf("empty digest algorithm, validate before calling Algorithm.Hash()")) + } + + // NOTE(stevvooe): A missing hash is usually a programming error that + // must be resolved at compile time. We don't import in the digest + // package to allow users to choose their hash implementation (such as + // when using stevvooe/resumable or a hardware accelerated package). + // + // Applications that may want to resolve the hash at runtime should + // call Algorithm.Available before call Algorithm.Hash(). + panic(fmt.Sprintf("%v not available (make sure it is imported)", a)) + } + + return algorithms[a].New() +} + +// Encode encodes the raw bytes of a digest, typically from a hash.Hash, into +// the encoded portion of the digest. +func (a Algorithm) Encode(d []byte) string { + // TODO(stevvooe): Currently, all algorithms use a hex encoding. When we + // add support for back registration, we can modify this accordingly. + return fmt.Sprintf("%x", d) +} + +// FromReader returns the digest of the reader using the algorithm. +func (a Algorithm) FromReader(rd io.Reader) (Digest, error) { + digester := a.Digester() + + if _, err := io.Copy(digester.Hash(), rd); err != nil { + return "", err + } + + return digester.Digest(), nil +} + +// FromBytes digests the input and returns a Digest. +func (a Algorithm) FromBytes(p []byte) Digest { + digester := a.Digester() + + if _, err := digester.Hash().Write(p); err != nil { + // Writes to a Hash should never fail. None of the existing + // hash implementations in the stdlib or hashes vendored + // here can return errors from Write. Having a panic in this + // condition instead of having FromBytes return an error value + // avoids unnecessary error handling paths in all callers. + panic("write to hash function returned error: " + err.Error()) + } + + return digester.Digest() +} + +// FromString digests the string input and returns a Digest. +func (a Algorithm) FromString(s string) Digest { + return a.FromBytes([]byte(s)) +} + +// Validate validates the encoded portion string +func (a Algorithm) Validate(encoded string) error { + r, ok := anchoredEncodedRegexps[a] + if !ok { + return ErrDigestUnsupported + } + // Digests much always be hex-encoded, ensuring that their hex portion will + // always be size*2 + if a.Size()*2 != len(encoded) { + return ErrDigestInvalidLength + } + if r.MatchString(encoded) { + return nil + } + return ErrDigestInvalidFormat +} diff --git a/vendor/github.com/opencontainers/go-digest/digest.go b/vendor/github.com/opencontainers/go-digest/digest.go new file mode 100644 index 00000000..518b5e71 --- /dev/null +++ b/vendor/github.com/opencontainers/go-digest/digest.go @@ -0,0 +1,157 @@ +// Copyright 2019, 2020 OCI Contributors +// Copyright 2017 Docker, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package digest + +import ( + "fmt" + "hash" + "io" + "regexp" + "strings" +) + +// Digest allows simple protection of hex formatted digest strings, prefixed +// by their algorithm. Strings of type Digest have some guarantee of being in +// the correct format and it provides quick access to the components of a +// digest string. +// +// The following is an example of the contents of Digest types: +// +// sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc +// +// This allows to abstract the digest behind this type and work only in those +// terms. +type Digest string + +// NewDigest returns a Digest from alg and a hash.Hash object. +func NewDigest(alg Algorithm, h hash.Hash) Digest { + return NewDigestFromBytes(alg, h.Sum(nil)) +} + +// NewDigestFromBytes returns a new digest from the byte contents of p. +// Typically, this can come from hash.Hash.Sum(...) or xxx.SumXXX(...) +// functions. This is also useful for rebuilding digests from binary +// serializations. +func NewDigestFromBytes(alg Algorithm, p []byte) Digest { + return NewDigestFromEncoded(alg, alg.Encode(p)) +} + +// NewDigestFromHex is deprecated. Please use NewDigestFromEncoded. +func NewDigestFromHex(alg, hex string) Digest { + return NewDigestFromEncoded(Algorithm(alg), hex) +} + +// NewDigestFromEncoded returns a Digest from alg and the encoded digest. +func NewDigestFromEncoded(alg Algorithm, encoded string) Digest { + return Digest(fmt.Sprintf("%s:%s", alg, encoded)) +} + +// DigestRegexp matches valid digest types. +var DigestRegexp = regexp.MustCompile(`[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[a-zA-Z0-9=_-]+`) + +// DigestRegexpAnchored matches valid digest types, anchored to the start and end of the match. +var DigestRegexpAnchored = regexp.MustCompile(`^` + DigestRegexp.String() + `$`) + +var ( + // ErrDigestInvalidFormat returned when digest format invalid. + ErrDigestInvalidFormat = fmt.Errorf("invalid checksum digest format") + + // ErrDigestInvalidLength returned when digest has invalid length. + ErrDigestInvalidLength = fmt.Errorf("invalid checksum digest length") + + // ErrDigestUnsupported returned when the digest algorithm is unsupported. + ErrDigestUnsupported = fmt.Errorf("unsupported digest algorithm") +) + +// Parse parses s and returns the validated digest object. An error will +// be returned if the format is invalid. +func Parse(s string) (Digest, error) { + d := Digest(s) + return d, d.Validate() +} + +// FromReader consumes the content of rd until io.EOF, returning canonical digest. +func FromReader(rd io.Reader) (Digest, error) { + return Canonical.FromReader(rd) +} + +// FromBytes digests the input and returns a Digest. +func FromBytes(p []byte) Digest { + return Canonical.FromBytes(p) +} + +// FromString digests the input and returns a Digest. +func FromString(s string) Digest { + return Canonical.FromString(s) +} + +// Validate checks that the contents of d is a valid digest, returning an +// error if not. +func (d Digest) Validate() error { + s := string(d) + i := strings.Index(s, ":") + if i <= 0 || i+1 == len(s) { + return ErrDigestInvalidFormat + } + algorithm, encoded := Algorithm(s[:i]), s[i+1:] + if !algorithm.Available() { + if !DigestRegexpAnchored.MatchString(s) { + return ErrDigestInvalidFormat + } + return ErrDigestUnsupported + } + return algorithm.Validate(encoded) +} + +// Algorithm returns the algorithm portion of the digest. This will panic if +// the underlying digest is not in a valid format. +func (d Digest) Algorithm() Algorithm { + return Algorithm(d[:d.sepIndex()]) +} + +// Verifier returns a writer object that can be used to verify a stream of +// content against the digest. If the digest is invalid, the method will panic. +func (d Digest) Verifier() Verifier { + return hashVerifier{ + hash: d.Algorithm().Hash(), + digest: d, + } +} + +// Encoded returns the encoded portion of the digest. This will panic if the +// underlying digest is not in a valid format. +func (d Digest) Encoded() string { + return string(d[d.sepIndex()+1:]) +} + +// Hex is deprecated. Please use Digest.Encoded. +func (d Digest) Hex() string { + return d.Encoded() +} + +func (d Digest) String() string { + return string(d) +} + +func (d Digest) sepIndex() int { + i := strings.Index(string(d), ":") + + if i < 0 { + panic(fmt.Sprintf("no ':' separator in digest %q", d)) + } + + return i +} diff --git a/vendor/github.com/opencontainers/go-digest/digester.go b/vendor/github.com/opencontainers/go-digest/digester.go new file mode 100644 index 00000000..ede90775 --- /dev/null +++ b/vendor/github.com/opencontainers/go-digest/digester.go @@ -0,0 +1,40 @@ +// Copyright 2019, 2020 OCI Contributors +// Copyright 2017 Docker, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package digest + +import "hash" + +// Digester calculates the digest of written data. Writes should go directly +// to the return value of Hash, while calling Digest will return the current +// value of the digest. +type Digester interface { + Hash() hash.Hash // provides direct access to underlying hash instance. + Digest() Digest +} + +// digester provides a simple digester definition that embeds a hasher. +type digester struct { + alg Algorithm + hash hash.Hash +} + +func (d *digester) Hash() hash.Hash { + return d.hash +} + +func (d *digester) Digest() Digest { + return NewDigest(d.alg, d.hash) +} diff --git a/vendor/github.com/opencontainers/go-digest/doc.go b/vendor/github.com/opencontainers/go-digest/doc.go new file mode 100644 index 00000000..83d3a936 --- /dev/null +++ b/vendor/github.com/opencontainers/go-digest/doc.go @@ -0,0 +1,62 @@ +// Copyright 2019, 2020 OCI Contributors +// Copyright 2017 Docker, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package digest provides a generalized type to opaquely represent message +// digests and their operations within the registry. The Digest type is +// designed to serve as a flexible identifier in a content-addressable system. +// More importantly, it provides tools and wrappers to work with +// hash.Hash-based digests with little effort. +// +// Basics +// +// The format of a digest is simply a string with two parts, dubbed the +// "algorithm" and the "digest", separated by a colon: +// +// : +// +// An example of a sha256 digest representation follows: +// +// sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc +// +// The "algorithm" portion defines both the hashing algorithm used to calculate +// the digest and the encoding of the resulting digest, which defaults to "hex" +// if not otherwise specified. Currently, all supported algorithms have their +// digests encoded in hex strings. +// +// In the example above, the string "sha256" is the algorithm and the hex bytes +// are the "digest". +// +// Because the Digest type is simply a string, once a valid Digest is +// obtained, comparisons are cheap, quick and simple to express with the +// standard equality operator. +// +// Verification +// +// The main benefit of using the Digest type is simple verification against a +// given digest. The Verifier interface, modeled after the stdlib hash.Hash +// interface, provides a common write sink for digest verification. After +// writing is complete, calling the Verifier.Verified method will indicate +// whether or not the stream of bytes matches the target digest. +// +// Missing Features +// +// In addition to the above, we intend to add the following features to this +// package: +// +// 1. A Digester type that supports write sink digest calculation. +// +// 2. Suspend and resume of ongoing digest calculations to support efficient digest verification in the registry. +// +package digest diff --git a/vendor/github.com/opencontainers/go-digest/verifiers.go b/vendor/github.com/opencontainers/go-digest/verifiers.go new file mode 100644 index 00000000..afef506f --- /dev/null +++ b/vendor/github.com/opencontainers/go-digest/verifiers.go @@ -0,0 +1,46 @@ +// Copyright 2019, 2020 OCI Contributors +// Copyright 2017 Docker, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package digest + +import ( + "hash" + "io" +) + +// Verifier presents a general verification interface to be used with message +// digests and other byte stream verifications. Users instantiate a Verifier +// from one of the various methods, write the data under test to it then check +// the result with the Verified method. +type Verifier interface { + io.Writer + + // Verified will return true if the content written to Verifier matches + // the digest. + Verified() bool +} + +type hashVerifier struct { + digest Digest + hash hash.Hash +} + +func (hv hashVerifier) Write(p []byte) (n int, err error) { + return hv.hash.Write(p) +} + +func (hv hashVerifier) Verified() bool { + return hv.digest == NewDigest(hv.digest.Algorithm(), hv.hash) +} diff --git a/vendor/github.com/opencontainers/image-spec/LICENSE b/vendor/github.com/opencontainers/image-spec/LICENSE new file mode 100644 index 00000000..9fdc20fd --- /dev/null +++ b/vendor/github.com/opencontainers/image-spec/LICENSE @@ -0,0 +1,191 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2016 The Linux Foundation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/opencontainers/image-spec/specs-go/v1/annotations.go b/vendor/github.com/opencontainers/image-spec/specs-go/v1/annotations.go new file mode 100644 index 00000000..581cf7cd --- /dev/null +++ b/vendor/github.com/opencontainers/image-spec/specs-go/v1/annotations.go @@ -0,0 +1,62 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +const ( + // AnnotationCreated is the annotation key for the date and time on which the image was built (date-time string as defined by RFC 3339). + AnnotationCreated = "org.opencontainers.image.created" + + // AnnotationAuthors is the annotation key for the contact details of the people or organization responsible for the image (freeform string). + AnnotationAuthors = "org.opencontainers.image.authors" + + // AnnotationURL is the annotation key for the URL to find more information on the image. + AnnotationURL = "org.opencontainers.image.url" + + // AnnotationDocumentation is the annotation key for the URL to get documentation on the image. + AnnotationDocumentation = "org.opencontainers.image.documentation" + + // AnnotationSource is the annotation key for the URL to get source code for building the image. + AnnotationSource = "org.opencontainers.image.source" + + // AnnotationVersion is the annotation key for the version of the packaged software. + // The version MAY match a label or tag in the source code repository. + // The version MAY be Semantic versioning-compatible. + AnnotationVersion = "org.opencontainers.image.version" + + // AnnotationRevision is the annotation key for the source control revision identifier for the packaged software. + AnnotationRevision = "org.opencontainers.image.revision" + + // AnnotationVendor is the annotation key for the name of the distributing entity, organization or individual. + AnnotationVendor = "org.opencontainers.image.vendor" + + // AnnotationLicenses is the annotation key for the license(s) under which contained software is distributed as an SPDX License Expression. + AnnotationLicenses = "org.opencontainers.image.licenses" + + // AnnotationRefName is the annotation key for the name of the reference for a target. + // SHOULD only be considered valid when on descriptors on `index.json` within image layout. + AnnotationRefName = "org.opencontainers.image.ref.name" + + // AnnotationTitle is the annotation key for the human-readable title of the image. + AnnotationTitle = "org.opencontainers.image.title" + + // AnnotationDescription is the annotation key for the human-readable description of the software packaged in the image. + AnnotationDescription = "org.opencontainers.image.description" + + // AnnotationBaseImageDigest is the annotation key for the digest of the image's base image. + AnnotationBaseImageDigest = "org.opencontainers.image.base.digest" + + // AnnotationBaseImageName is the annotation key for the image reference of the image's base image. + AnnotationBaseImageName = "org.opencontainers.image.base.name" +) diff --git a/vendor/github.com/opencontainers/image-spec/specs-go/v1/config.go b/vendor/github.com/opencontainers/image-spec/specs-go/v1/config.go new file mode 100644 index 00000000..36b0aeb8 --- /dev/null +++ b/vendor/github.com/opencontainers/image-spec/specs-go/v1/config.go @@ -0,0 +1,111 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "time" + + digest "github.com/opencontainers/go-digest" +) + +// ImageConfig defines the execution parameters which should be used as a base when running a container using an image. +type ImageConfig struct { + // User defines the username or UID which the process in the container should run as. + User string `json:"User,omitempty"` + + // ExposedPorts a set of ports to expose from a container running this image. + ExposedPorts map[string]struct{} `json:"ExposedPorts,omitempty"` + + // Env is a list of environment variables to be used in a container. + Env []string `json:"Env,omitempty"` + + // Entrypoint defines a list of arguments to use as the command to execute when the container starts. + Entrypoint []string `json:"Entrypoint,omitempty"` + + // Cmd defines the default arguments to the entrypoint of the container. + Cmd []string `json:"Cmd,omitempty"` + + // Volumes is a set of directories describing where the process is likely write data specific to a container instance. + Volumes map[string]struct{} `json:"Volumes,omitempty"` + + // WorkingDir sets the current working directory of the entrypoint process in the container. + WorkingDir string `json:"WorkingDir,omitempty"` + + // Labels contains arbitrary metadata for the container. + Labels map[string]string `json:"Labels,omitempty"` + + // StopSignal contains the system call signal that will be sent to the container to exit. + StopSignal string `json:"StopSignal,omitempty"` + + // ArgsEscaped + // + // Deprecated: This field is present only for legacy compatibility with + // Docker and should not be used by new image builders. It is used by Docker + // for Windows images to indicate that the `Entrypoint` or `Cmd` or both, + // contains only a single element array, that is a pre-escaped, and combined + // into a single string `CommandLine`. If `true` the value in `Entrypoint` or + // `Cmd` should be used as-is to avoid double escaping. + // https://github.com/opencontainers/image-spec/pull/892 + ArgsEscaped bool `json:"ArgsEscaped,omitempty"` +} + +// RootFS describes a layer content addresses +type RootFS struct { + // Type is the type of the rootfs. + Type string `json:"type"` + + // DiffIDs is an array of layer content hashes (DiffIDs), in order from bottom-most to top-most. + DiffIDs []digest.Digest `json:"diff_ids"` +} + +// History describes the history of a layer. +type History struct { + // Created is the combined date and time at which the layer was created, formatted as defined by RFC 3339, section 5.6. + Created *time.Time `json:"created,omitempty"` + + // CreatedBy is the command which created the layer. + CreatedBy string `json:"created_by,omitempty"` + + // Author is the author of the build point. + Author string `json:"author,omitempty"` + + // Comment is a custom message set when creating the layer. + Comment string `json:"comment,omitempty"` + + // EmptyLayer is used to mark if the history item created a filesystem diff. + EmptyLayer bool `json:"empty_layer,omitempty"` +} + +// Image is the JSON structure which describes some basic information about the image. +// This provides the `application/vnd.oci.image.config.v1+json` mediatype when marshalled to JSON. +type Image struct { + // Created is the combined date and time at which the image was created, formatted as defined by RFC 3339, section 5.6. + Created *time.Time `json:"created,omitempty"` + + // Author defines the name and/or email address of the person or entity which created and is responsible for maintaining the image. + Author string `json:"author,omitempty"` + + // Platform describes the platform which the image in the manifest runs on. + Platform + + // Config defines the execution parameters which should be used as a base when running a container using the image. + Config ImageConfig `json:"config,omitempty"` + + // RootFS references the layer content addresses used by the image. + RootFS RootFS `json:"rootfs"` + + // History describes the history of each layer. + History []History `json:"history,omitempty"` +} diff --git a/vendor/github.com/opencontainers/image-spec/specs-go/v1/descriptor.go b/vendor/github.com/opencontainers/image-spec/specs-go/v1/descriptor.go new file mode 100644 index 00000000..1881b118 --- /dev/null +++ b/vendor/github.com/opencontainers/image-spec/specs-go/v1/descriptor.go @@ -0,0 +1,80 @@ +// Copyright 2016-2022 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import digest "github.com/opencontainers/go-digest" + +// Descriptor describes the disposition of targeted content. +// This structure provides `application/vnd.oci.descriptor.v1+json` mediatype +// when marshalled to JSON. +type Descriptor struct { + // MediaType is the media type of the object this schema refers to. + MediaType string `json:"mediaType"` + + // Digest is the digest of the targeted content. + Digest digest.Digest `json:"digest"` + + // Size specifies the size in bytes of the blob. + Size int64 `json:"size"` + + // URLs specifies a list of URLs from which this object MAY be downloaded + URLs []string `json:"urls,omitempty"` + + // Annotations contains arbitrary metadata relating to the targeted content. + Annotations map[string]string `json:"annotations,omitempty"` + + // Data is an embedding of the targeted content. This is encoded as a base64 + // string when marshalled to JSON (automatically, by encoding/json). If + // present, Data can be used directly to avoid fetching the targeted content. + Data []byte `json:"data,omitempty"` + + // Platform describes the platform which the image in the manifest runs on. + // + // This should only be used when referring to a manifest. + Platform *Platform `json:"platform,omitempty"` + + // ArtifactType is the IANA media type of this artifact. + ArtifactType string `json:"artifactType,omitempty"` +} + +// Platform describes the platform which the image in the manifest runs on. +type Platform struct { + // Architecture field specifies the CPU architecture, for example + // `amd64` or `ppc64le`. + Architecture string `json:"architecture"` + + // OS specifies the operating system, for example `linux` or `windows`. + OS string `json:"os"` + + // OSVersion is an optional field specifying the operating system + // version, for example on Windows `10.0.14393.1066`. + OSVersion string `json:"os.version,omitempty"` + + // OSFeatures is an optional field specifying an array of strings, + // each listing a required OS feature (for example on Windows `win32k`). + OSFeatures []string `json:"os.features,omitempty"` + + // Variant is an optional field specifying a variant of the CPU, for + // example `v7` to specify ARMv7 when architecture is `arm`. + Variant string `json:"variant,omitempty"` +} + +// DescriptorEmptyJSON is the descriptor of a blob with content of `{}`. +var DescriptorEmptyJSON = Descriptor{ + MediaType: MediaTypeEmptyJSON, + Digest: `sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a`, + Size: 2, + Data: []byte(`{}`), +} diff --git a/vendor/github.com/opencontainers/image-spec/specs-go/v1/index.go b/vendor/github.com/opencontainers/image-spec/specs-go/v1/index.go new file mode 100644 index 00000000..e2bed9d4 --- /dev/null +++ b/vendor/github.com/opencontainers/image-spec/specs-go/v1/index.go @@ -0,0 +1,38 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import "github.com/opencontainers/image-spec/specs-go" + +// Index references manifests for various platforms. +// This structure provides `application/vnd.oci.image.index.v1+json` mediatype when marshalled to JSON. +type Index struct { + specs.Versioned + + // MediaType specifies the type of this document data structure e.g. `application/vnd.oci.image.index.v1+json` + MediaType string `json:"mediaType,omitempty"` + + // ArtifactType specifies the IANA media type of artifact when the manifest is used for an artifact. + ArtifactType string `json:"artifactType,omitempty"` + + // Manifests references platform specific manifests. + Manifests []Descriptor `json:"manifests"` + + // Subject is an optional link from the image manifest to another manifest forming an association between the image manifest and the other manifest. + Subject *Descriptor `json:"subject,omitempty"` + + // Annotations contains arbitrary metadata for the image index. + Annotations map[string]string `json:"annotations,omitempty"` +} diff --git a/vendor/github.com/opencontainers/image-spec/specs-go/v1/layout.go b/vendor/github.com/opencontainers/image-spec/specs-go/v1/layout.go new file mode 100644 index 00000000..c5503cb3 --- /dev/null +++ b/vendor/github.com/opencontainers/image-spec/specs-go/v1/layout.go @@ -0,0 +1,32 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +const ( + // ImageLayoutFile is the file name containing ImageLayout in an OCI Image Layout + ImageLayoutFile = "oci-layout" + // ImageLayoutVersion is the version of ImageLayout + ImageLayoutVersion = "1.0.0" + // ImageIndexFile is the file name of the entry point for references and descriptors in an OCI Image Layout + ImageIndexFile = "index.json" + // ImageBlobsDir is the directory name containing content addressable blobs in an OCI Image Layout + ImageBlobsDir = "blobs" +) + +// ImageLayout is the structure in the "oci-layout" file, found in the root +// of an OCI Image-layout directory. +type ImageLayout struct { + Version string `json:"imageLayoutVersion"` +} diff --git a/vendor/github.com/opencontainers/image-spec/specs-go/v1/manifest.go b/vendor/github.com/opencontainers/image-spec/specs-go/v1/manifest.go new file mode 100644 index 00000000..26fec52a --- /dev/null +++ b/vendor/github.com/opencontainers/image-spec/specs-go/v1/manifest.go @@ -0,0 +1,41 @@ +// Copyright 2016-2022 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import "github.com/opencontainers/image-spec/specs-go" + +// Manifest provides `application/vnd.oci.image.manifest.v1+json` mediatype structure when marshalled to JSON. +type Manifest struct { + specs.Versioned + + // MediaType specifies the type of this document data structure e.g. `application/vnd.oci.image.manifest.v1+json` + MediaType string `json:"mediaType,omitempty"` + + // ArtifactType specifies the IANA media type of artifact when the manifest is used for an artifact. + ArtifactType string `json:"artifactType,omitempty"` + + // Config references a configuration object for a container, by digest. + // The referenced configuration object is a JSON blob that the runtime uses to set up the container. + Config Descriptor `json:"config"` + + // Layers is an indexed list of layers referenced by the manifest. + Layers []Descriptor `json:"layers"` + + // Subject is an optional link from the image manifest to another manifest forming an association between the image manifest and the other manifest. + Subject *Descriptor `json:"subject,omitempty"` + + // Annotations contains arbitrary metadata for the image manifest. + Annotations map[string]string `json:"annotations,omitempty"` +} diff --git a/vendor/github.com/opencontainers/image-spec/specs-go/v1/mediatype.go b/vendor/github.com/opencontainers/image-spec/specs-go/v1/mediatype.go new file mode 100644 index 00000000..ce8313e7 --- /dev/null +++ b/vendor/github.com/opencontainers/image-spec/specs-go/v1/mediatype.go @@ -0,0 +1,85 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +const ( + // MediaTypeDescriptor specifies the media type for a content descriptor. + MediaTypeDescriptor = "application/vnd.oci.descriptor.v1+json" + + // MediaTypeLayoutHeader specifies the media type for the oci-layout. + MediaTypeLayoutHeader = "application/vnd.oci.layout.header.v1+json" + + // MediaTypeImageIndex specifies the media type for an image index. + MediaTypeImageIndex = "application/vnd.oci.image.index.v1+json" + + // MediaTypeImageManifest specifies the media type for an image manifest. + MediaTypeImageManifest = "application/vnd.oci.image.manifest.v1+json" + + // MediaTypeImageConfig specifies the media type for the image configuration. + MediaTypeImageConfig = "application/vnd.oci.image.config.v1+json" + + // MediaTypeEmptyJSON specifies the media type for an unused blob containing the value "{}". + MediaTypeEmptyJSON = "application/vnd.oci.empty.v1+json" +) + +const ( + // MediaTypeImageLayer is the media type used for layers referenced by the manifest. + MediaTypeImageLayer = "application/vnd.oci.image.layer.v1.tar" + + // MediaTypeImageLayerGzip is the media type used for gzipped layers + // referenced by the manifest. + MediaTypeImageLayerGzip = "application/vnd.oci.image.layer.v1.tar+gzip" + + // MediaTypeImageLayerZstd is the media type used for zstd compressed + // layers referenced by the manifest. + MediaTypeImageLayerZstd = "application/vnd.oci.image.layer.v1.tar+zstd" +) + +// Non-distributable layer media-types. +// +// Deprecated: Non-distributable layers are deprecated, and not recommended +// for future use. Implementations SHOULD NOT produce new non-distributable +// layers. +// https://github.com/opencontainers/image-spec/pull/965 +const ( + // MediaTypeImageLayerNonDistributable is the media type for layers referenced by + // the manifest but with distribution restrictions. + // + // Deprecated: Non-distributable layers are deprecated, and not recommended + // for future use. Implementations SHOULD NOT produce new non-distributable + // layers. + // https://github.com/opencontainers/image-spec/pull/965 + MediaTypeImageLayerNonDistributable = "application/vnd.oci.image.layer.nondistributable.v1.tar" + + // MediaTypeImageLayerNonDistributableGzip is the media type for + // gzipped layers referenced by the manifest but with distribution + // restrictions. + // + // Deprecated: Non-distributable layers are deprecated, and not recommended + // for future use. Implementations SHOULD NOT produce new non-distributable + // layers. + // https://github.com/opencontainers/image-spec/pull/965 + MediaTypeImageLayerNonDistributableGzip = "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip" + + // MediaTypeImageLayerNonDistributableZstd is the media type for zstd + // compressed layers referenced by the manifest but with distribution + // restrictions. + // + // Deprecated: Non-distributable layers are deprecated, and not recommended + // for future use. Implementations SHOULD NOT produce new non-distributable + // layers. + // https://github.com/opencontainers/image-spec/pull/965 + MediaTypeImageLayerNonDistributableZstd = "application/vnd.oci.image.layer.nondistributable.v1.tar+zstd" +) diff --git a/vendor/github.com/opencontainers/image-spec/specs-go/version.go b/vendor/github.com/opencontainers/image-spec/specs-go/version.go new file mode 100644 index 00000000..7069ae44 --- /dev/null +++ b/vendor/github.com/opencontainers/image-spec/specs-go/version.go @@ -0,0 +1,32 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package specs + +import "fmt" + +const ( + // VersionMajor is for an API incompatible changes + VersionMajor = 1 + // VersionMinor is for functionality in a backwards-compatible manner + VersionMinor = 1 + // VersionPatch is for backwards-compatible bug fixes + VersionPatch = 0 + + // VersionDev indicates development branch. Releases will be empty string. + VersionDev = "" +) + +// Version is the specification version that the package types support. +var Version = fmt.Sprintf("%d.%d.%d%s", VersionMajor, VersionMinor, VersionPatch, VersionDev) diff --git a/vendor/github.com/opencontainers/image-spec/specs-go/versioned.go b/vendor/github.com/opencontainers/image-spec/specs-go/versioned.go new file mode 100644 index 00000000..58a1510f --- /dev/null +++ b/vendor/github.com/opencontainers/image-spec/specs-go/versioned.go @@ -0,0 +1,23 @@ +// Copyright 2016 The Linux Foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package specs + +// Versioned provides a struct with the manifest schemaVersion and mediaType. +// Incoming content with unknown schema version can be decoded against this +// struct to check the version. +type Versioned struct { + // SchemaVersion is the image manifest schema that this image follows + SchemaVersion int `json:"schemaVersion"` +} diff --git a/vendor/golang.org/x/sync/LICENSE b/vendor/golang.org/x/sync/LICENSE new file mode 100644 index 00000000..6a66aea5 --- /dev/null +++ b/vendor/golang.org/x/sync/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/golang.org/x/sync/PATENTS b/vendor/golang.org/x/sync/PATENTS new file mode 100644 index 00000000..73309904 --- /dev/null +++ b/vendor/golang.org/x/sync/PATENTS @@ -0,0 +1,22 @@ +Additional IP Rights Grant (Patents) + +"This implementation" means the copyrightable works distributed by +Google as part of the Go project. + +Google hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) +patent license to make, have made, use, offer to sell, sell, import, +transfer and otherwise run, modify and propagate the contents of this +implementation of Go, where such license applies only to those patent +claims, both currently owned or controlled by Google and acquired in +the future, licensable by Google that are necessarily infringed by this +implementation of Go. This grant does not include claims that would be +infringed only as a consequence of further modification of this +implementation. If you or your agent or exclusive licensee institute or +order or agree to the institution of patent litigation against any +entity (including a cross-claim or counterclaim in a lawsuit) alleging +that this implementation of Go or any code incorporated within this +implementation of Go constitutes direct or contributory patent +infringement, or inducement of patent infringement, then any patent +rights granted to you under this License for this implementation of Go +shall terminate as of the date such litigation is filed. diff --git a/vendor/golang.org/x/sync/errgroup/errgroup.go b/vendor/golang.org/x/sync/errgroup/errgroup.go new file mode 100644 index 00000000..948a3ee6 --- /dev/null +++ b/vendor/golang.org/x/sync/errgroup/errgroup.go @@ -0,0 +1,135 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package errgroup provides synchronization, error propagation, and Context +// cancelation for groups of goroutines working on subtasks of a common task. +// +// [errgroup.Group] is related to [sync.WaitGroup] but adds handling of tasks +// returning errors. +package errgroup + +import ( + "context" + "fmt" + "sync" +) + +type token struct{} + +// A Group is a collection of goroutines working on subtasks that are part of +// the same overall task. +// +// A zero Group is valid, has no limit on the number of active goroutines, +// and does not cancel on error. +type Group struct { + cancel func(error) + + wg sync.WaitGroup + + sem chan token + + errOnce sync.Once + err error +} + +func (g *Group) done() { + if g.sem != nil { + <-g.sem + } + g.wg.Done() +} + +// WithContext returns a new Group and an associated Context derived from ctx. +// +// The derived Context is canceled the first time a function passed to Go +// returns a non-nil error or the first time Wait returns, whichever occurs +// first. +func WithContext(ctx context.Context) (*Group, context.Context) { + ctx, cancel := withCancelCause(ctx) + return &Group{cancel: cancel}, ctx +} + +// Wait blocks until all function calls from the Go method have returned, then +// returns the first non-nil error (if any) from them. +func (g *Group) Wait() error { + g.wg.Wait() + if g.cancel != nil { + g.cancel(g.err) + } + return g.err +} + +// Go calls the given function in a new goroutine. +// It blocks until the new goroutine can be added without the number of +// active goroutines in the group exceeding the configured limit. +// +// The first call to return a non-nil error cancels the group's context, if the +// group was created by calling WithContext. The error will be returned by Wait. +func (g *Group) Go(f func() error) { + if g.sem != nil { + g.sem <- token{} + } + + g.wg.Add(1) + go func() { + defer g.done() + + if err := f(); err != nil { + g.errOnce.Do(func() { + g.err = err + if g.cancel != nil { + g.cancel(g.err) + } + }) + } + }() +} + +// TryGo calls the given function in a new goroutine only if the number of +// active goroutines in the group is currently below the configured limit. +// +// The return value reports whether the goroutine was started. +func (g *Group) TryGo(f func() error) bool { + if g.sem != nil { + select { + case g.sem <- token{}: + // Note: this allows barging iff channels in general allow barging. + default: + return false + } + } + + g.wg.Add(1) + go func() { + defer g.done() + + if err := f(); err != nil { + g.errOnce.Do(func() { + g.err = err + if g.cancel != nil { + g.cancel(g.err) + } + }) + } + }() + return true +} + +// SetLimit limits the number of active goroutines in this group to at most n. +// A negative value indicates no limit. +// +// Any subsequent call to the Go method will block until it can add an active +// goroutine without exceeding the configured limit. +// +// The limit must not be modified while any goroutines in the group are active. +func (g *Group) SetLimit(n int) { + if n < 0 { + g.sem = nil + return + } + if len(g.sem) != 0 { + panic(fmt.Errorf("errgroup: modify limit while %v goroutines in the group are still active", len(g.sem))) + } + g.sem = make(chan token, n) +} diff --git a/vendor/golang.org/x/sync/errgroup/go120.go b/vendor/golang.org/x/sync/errgroup/go120.go new file mode 100644 index 00000000..f93c740b --- /dev/null +++ b/vendor/golang.org/x/sync/errgroup/go120.go @@ -0,0 +1,13 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.20 + +package errgroup + +import "context" + +func withCancelCause(parent context.Context) (context.Context, func(error)) { + return context.WithCancelCause(parent) +} diff --git a/vendor/golang.org/x/sync/errgroup/pre_go120.go b/vendor/golang.org/x/sync/errgroup/pre_go120.go new file mode 100644 index 00000000..88ce3343 --- /dev/null +++ b/vendor/golang.org/x/sync/errgroup/pre_go120.go @@ -0,0 +1,14 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !go1.20 + +package errgroup + +import "context" + +func withCancelCause(parent context.Context) (context.Context, func(error)) { + ctx, cancel := context.WithCancel(parent) + return ctx, func(error) { cancel() } +} diff --git a/vendor/golang.org/x/sync/semaphore/semaphore.go b/vendor/golang.org/x/sync/semaphore/semaphore.go new file mode 100644 index 00000000..b618162a --- /dev/null +++ b/vendor/golang.org/x/sync/semaphore/semaphore.go @@ -0,0 +1,160 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package semaphore provides a weighted semaphore implementation. +package semaphore // import "golang.org/x/sync/semaphore" + +import ( + "container/list" + "context" + "sync" +) + +type waiter struct { + n int64 + ready chan<- struct{} // Closed when semaphore acquired. +} + +// NewWeighted creates a new weighted semaphore with the given +// maximum combined weight for concurrent access. +func NewWeighted(n int64) *Weighted { + w := &Weighted{size: n} + return w +} + +// Weighted provides a way to bound concurrent access to a resource. +// The callers can request access with a given weight. +type Weighted struct { + size int64 + cur int64 + mu sync.Mutex + waiters list.List +} + +// Acquire acquires the semaphore with a weight of n, blocking until resources +// are available or ctx is done. On success, returns nil. On failure, returns +// ctx.Err() and leaves the semaphore unchanged. +func (s *Weighted) Acquire(ctx context.Context, n int64) error { + done := ctx.Done() + + s.mu.Lock() + select { + case <-done: + // ctx becoming done has "happened before" acquiring the semaphore, + // whether it became done before the call began or while we were + // waiting for the mutex. We prefer to fail even if we could acquire + // the mutex without blocking. + s.mu.Unlock() + return ctx.Err() + default: + } + if s.size-s.cur >= n && s.waiters.Len() == 0 { + // Since we hold s.mu and haven't synchronized since checking done, if + // ctx becomes done before we return here, it becoming done must have + // "happened concurrently" with this call - it cannot "happen before" + // we return in this branch. So, we're ok to always acquire here. + s.cur += n + s.mu.Unlock() + return nil + } + + if n > s.size { + // Don't make other Acquire calls block on one that's doomed to fail. + s.mu.Unlock() + <-done + return ctx.Err() + } + + ready := make(chan struct{}) + w := waiter{n: n, ready: ready} + elem := s.waiters.PushBack(w) + s.mu.Unlock() + + select { + case <-done: + s.mu.Lock() + select { + case <-ready: + // Acquired the semaphore after we were canceled. + // Pretend we didn't and put the tokens back. + s.cur -= n + s.notifyWaiters() + default: + isFront := s.waiters.Front() == elem + s.waiters.Remove(elem) + // If we're at the front and there're extra tokens left, notify other waiters. + if isFront && s.size > s.cur { + s.notifyWaiters() + } + } + s.mu.Unlock() + return ctx.Err() + + case <-ready: + // Acquired the semaphore. Check that ctx isn't already done. + // We check the done channel instead of calling ctx.Err because we + // already have the channel, and ctx.Err is O(n) with the nesting + // depth of ctx. + select { + case <-done: + s.Release(n) + return ctx.Err() + default: + } + return nil + } +} + +// TryAcquire acquires the semaphore with a weight of n without blocking. +// On success, returns true. On failure, returns false and leaves the semaphore unchanged. +func (s *Weighted) TryAcquire(n int64) bool { + s.mu.Lock() + success := s.size-s.cur >= n && s.waiters.Len() == 0 + if success { + s.cur += n + } + s.mu.Unlock() + return success +} + +// Release releases the semaphore with a weight of n. +func (s *Weighted) Release(n int64) { + s.mu.Lock() + s.cur -= n + if s.cur < 0 { + s.mu.Unlock() + panic("semaphore: released more than held") + } + s.notifyWaiters() + s.mu.Unlock() +} + +func (s *Weighted) notifyWaiters() { + for { + next := s.waiters.Front() + if next == nil { + break // No more waiters blocked. + } + + w := next.Value.(waiter) + if s.size-s.cur < w.n { + // Not enough tokens for the next waiter. We could keep going (to try to + // find a waiter with a smaller request), but under load that could cause + // starvation for large requests; instead, we leave all remaining waiters + // blocked. + // + // Consider a semaphore used as a read-write lock, with N tokens, N + // readers, and one writer. Each reader can Acquire(1) to obtain a read + // lock. The writer can Acquire(N) to obtain a write lock, excluding all + // of the readers. If we allow the readers to jump ahead in the queue, + // the writer will starve — there is always one token available for every + // reader. + break + } + + s.cur += w.n + s.waiters.Remove(next) + close(w.ready) + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 129fb88e..a08a9303 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -18,12 +18,13 @@ github.com/ProtonMail/go-crypto/openpgp/internal/ecc github.com/ProtonMail/go-crypto/openpgp/internal/encoding github.com/ProtonMail/go-crypto/openpgp/packet github.com/ProtonMail/go-crypto/openpgp/s2k -# github.com/SovereignCloudStack/cluster-stack-operator v0.1.0-alpha.6 +# github.com/SovereignCloudStack/cluster-stack-operator v0.1.0-alpha.6.0.20240828081204-d479eab19273 ## explicit; go 1.21 github.com/SovereignCloudStack/cluster-stack-operator/pkg/assetsclient github.com/SovereignCloudStack/cluster-stack-operator/pkg/assetsclient/fake github.com/SovereignCloudStack/cluster-stack-operator/pkg/assetsclient/github github.com/SovereignCloudStack/cluster-stack-operator/pkg/assetsclient/mocks +github.com/SovereignCloudStack/cluster-stack-operator/pkg/assetsclient/oci github.com/SovereignCloudStack/cluster-stack-operator/pkg/clusterstack github.com/SovereignCloudStack/cluster-stack-operator/pkg/kubernetesversion github.com/SovereignCloudStack/cluster-stack-operator/pkg/release @@ -234,6 +235,13 @@ github.com/onsi/gomega/matchers/support/goraph/edge github.com/onsi/gomega/matchers/support/goraph/node github.com/onsi/gomega/matchers/support/goraph/util github.com/onsi/gomega/types +# github.com/opencontainers/go-digest v1.0.0 +## explicit; go 1.13 +github.com/opencontainers/go-digest +# github.com/opencontainers/image-spec v1.1.0 +## explicit; go 1.18 +github.com/opencontainers/image-spec/specs-go +github.com/opencontainers/image-spec/specs-go/v1 # github.com/pkg/errors v0.9.1 ## explicit github.com/pkg/errors @@ -311,6 +319,10 @@ golang.org/x/net/idna ## explicit; go 1.18 golang.org/x/oauth2 golang.org/x/oauth2/internal +# golang.org/x/sync v0.7.0 +## explicit; go 1.18 +golang.org/x/sync/errgroup +golang.org/x/sync/semaphore # golang.org/x/sys v0.20.0 ## explicit; go 1.18 golang.org/x/sys/cpu @@ -698,6 +710,34 @@ k8s.io/utils/pointer k8s.io/utils/ptr k8s.io/utils/strings/slices k8s.io/utils/trace +# oras.land/oras-go/v2 v2.5.0 +## explicit; go 1.21 +oras.land/oras-go/v2 +oras.land/oras-go/v2/content +oras.land/oras-go/v2/content/file +oras.land/oras-go/v2/errdef +oras.land/oras-go/v2/internal/cas +oras.land/oras-go/v2/internal/container/set +oras.land/oras-go/v2/internal/copyutil +oras.land/oras-go/v2/internal/descriptor +oras.land/oras-go/v2/internal/docker +oras.land/oras-go/v2/internal/graph +oras.land/oras-go/v2/internal/httputil +oras.land/oras-go/v2/internal/interfaces +oras.land/oras-go/v2/internal/ioutil +oras.land/oras-go/v2/internal/manifestutil +oras.land/oras-go/v2/internal/platform +oras.land/oras-go/v2/internal/registryutil +oras.land/oras-go/v2/internal/resolver +oras.land/oras-go/v2/internal/spec +oras.land/oras-go/v2/internal/status +oras.land/oras-go/v2/internal/syncutil +oras.land/oras-go/v2/registry +oras.land/oras-go/v2/registry/remote +oras.land/oras-go/v2/registry/remote/auth +oras.land/oras-go/v2/registry/remote/errcode +oras.land/oras-go/v2/registry/remote/internal/errutil +oras.land/oras-go/v2/registry/remote/retry # sigs.k8s.io/cluster-api v1.7.2 ## explicit; go 1.21 sigs.k8s.io/cluster-api/api/v1beta1 diff --git a/vendor/oras.land/oras-go/v2/.gitignore b/vendor/oras.land/oras-go/v2/.gitignore new file mode 100644 index 00000000..400a0ea0 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/.gitignore @@ -0,0 +1,41 @@ +# Copyright The ORAS Authors. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# VS Code +.vscode +debug + +# Jetbrains +.idea + +# Custom +coverage.txt +bin/ +dist/ +*.tar.gz +vendor/ +_dist/ +.cover diff --git a/vendor/oras.land/oras-go/v2/CODEOWNERS b/vendor/oras.land/oras-go/v2/CODEOWNERS new file mode 100644 index 00000000..45a68a31 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/CODEOWNERS @@ -0,0 +1,2 @@ +# Derived from OWNERS.md +* @sajayantony @shizhMSFT @stevelasker @Wwwsylvia diff --git a/vendor/oras.land/oras-go/v2/CODE_OF_CONDUCT.md b/vendor/oras.land/oras-go/v2/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..49579ab8 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/CODE_OF_CONDUCT.md @@ -0,0 +1,3 @@ +# Code of Conduct + +OCI Registry As Storage (ORAS) follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). diff --git a/vendor/oras.land/oras-go/v2/LICENSE b/vendor/oras.land/oras-go/v2/LICENSE new file mode 100644 index 00000000..a67d1693 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2021 ORAS Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/oras.land/oras-go/v2/MIGRATION_GUIDE.md b/vendor/oras.land/oras-go/v2/MIGRATION_GUIDE.md new file mode 100644 index 00000000..b9292f14 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/MIGRATION_GUIDE.md @@ -0,0 +1,45 @@ +# Migration Guide + +In version `v2`, ORAS Go library has been completely refreshed with: + +- More unified interfaces +- Notably fewer dependencies +- Higher test coverage +- Better documentation + +**Besides, ORAS Go `v2` is now a registry client.** + +## Major Changes in `v2` + +- Moves `content.FileStore` to [file.Store](https://pkg.go.dev/oras.land/oras-go/v2/content/file#Store) +- Moves `content.OCIStore` to [oci.Store](https://pkg.go.dev/oras.land/oras-go/v2/content/oci#Store) +- Moves `content.MemoryStore` to [memory.Store](https://pkg.go.dev/oras.land/oras-go/v2/content/memory#Store) +- Provides [SDK](https://pkg.go.dev/oras.land/oras-go/v2/registry/remote) to interact with OCI-compliant and Docker-compliant registries +- Supports [Copy](https://pkg.go.dev/oras.land/oras-go/v2#Copy) with more flexible options +- Supports [Extended Copy](https://pkg.go.dev/oras.land/oras-go/v2#ExtendedCopy) with options *(experimental)* +- No longer supports `docker.Login` and `docker.Logout` (removes the dependency on `docker`); instead, provides authentication through [auth.Client](https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/auth#Client) + +Documentation and examples are available at [pkg.go.dev](https://pkg.go.dev/oras.land/oras-go/v2). + +## Migrating from `v1` to `v2` + +1. Get the `v2` package + + ```sh + go get oras.land/oras-go/v2 + ``` + +2. Import and use the `v2` package + + ```go + import "oras.land/oras-go/v2" + ``` + +3. Run + + ```sh + go mod tidy + ``` + +Since breaking changes are introduced in `v2`, code refactoring is required for migrating from `v1` to `v2`. +The migration can be done in an iterative fashion, as `v1` and `v2` can be imported and used at the same time. diff --git a/vendor/oras.land/oras-go/v2/Makefile b/vendor/oras.land/oras-go/v2/Makefile new file mode 100644 index 00000000..bc671e44 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/Makefile @@ -0,0 +1,38 @@ +# Copyright The ORAS Authors. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +.PHONY: test +test: vendor check-encoding + go test -race -v -coverprofile=coverage.txt -covermode=atomic ./... + +.PHONY: covhtml +covhtml: + open .cover/coverage.html + +.PHONY: clean +clean: + git status --ignored --short | grep '^!! ' | sed 's/!! //' | xargs rm -rf + +.PHONY: check-encoding +check-encoding: + ! find . -not -path "./vendor/*" -name "*.go" -type f -exec file "{}" ";" | grep CRLF + ! find scripts -name "*.sh" -type f -exec file "{}" ";" | grep CRLF + +.PHONY: fix-encoding +fix-encoding: + find . -not -path "./vendor/*" -name "*.go" -type f -exec sed -i -e "s/\r//g" {} + + find scripts -name "*.sh" -type f -exec sed -i -e "s/\r//g" {} + + +.PHONY: vendor +vendor: + go mod vendor diff --git a/vendor/oras.land/oras-go/v2/OWNERS.md b/vendor/oras.land/oras-go/v2/OWNERS.md new file mode 100644 index 00000000..402c4a97 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/OWNERS.md @@ -0,0 +1,11 @@ +# Owners + +Owners: + - Sajay Antony (@sajayantony) + - Shiwei Zhang (@shizhMSFT) + - Steve Lasker (@stevelasker) + - Sylvia Lei (@Wwwsylvia) + +Emeritus: + - Avi Deitcher (@deitch) + - Josh Dolitsky (@jdolitsky) diff --git a/vendor/oras.land/oras-go/v2/README.md b/vendor/oras.land/oras-go/v2/README.md new file mode 100644 index 00000000..7c3013c7 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/README.md @@ -0,0 +1,58 @@ +# ORAS Go library + +

+banner +

+ +## Project status + +### Versioning + +The ORAS Go library follows [Semantic Versioning](https://semver.org/), where breaking changes are reserved for MAJOR releases, and MINOR and PATCH releases must be 100% backwards compatible. + +### v2: stable + +[![Build Status](https://github.com/oras-project/oras-go/actions/workflows/build.yml/badge.svg?event=push&branch=main)](https://github.com/oras-project/oras-go/actions/workflows/build.yml?query=workflow%3Abuild+event%3Apush+branch%3Amain) +[![codecov](https://codecov.io/gh/oras-project/oras-go/branch/main/graph/badge.svg)](https://codecov.io/gh/oras-project/oras-go) +[![Go Report Card](https://goreportcard.com/badge/oras.land/oras-go/v2)](https://goreportcard.com/report/oras.land/oras-go/v2) +[![Go Reference](https://pkg.go.dev/badge/oras.land/oras-go/v2.svg)](https://pkg.go.dev/oras.land/oras-go/v2) + +The version `2` is actively developed in the [`main`](https://github.com/oras-project/oras-go/tree/main) branch with all new features. + +> [!Note] +> The `main` branch follows [Go's Security Policy](https://github.com/golang/go/security/policy) and supports the two latest versions of Go (currently `1.21` and `1.22`). + +Examples for common use cases can be found below: + +- [Copy examples](https://pkg.go.dev/oras.land/oras-go/v2#pkg-examples) +- [Registry interaction examples](https://pkg.go.dev/oras.land/oras-go/v2/registry#pkg-examples) +- [Repository interaction examples](https://pkg.go.dev/oras.land/oras-go/v2/registry/remote#pkg-examples) +- [Authentication examples](https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/auth#pkg-examples) + +If you are seeking latest changes, you should use the [`main`](https://github.com/oras-project/oras-go/tree/main) branch (or a specific commit hash) over a tagged version when including the ORAS Go library in your project's `go.mod`. +The Go Reference for the `main` branch is available [here](https://pkg.go.dev/oras.land/oras-go/v2@main). + +To migrate from `v1` to `v2`, see [MIGRATION_GUIDE.md](./MIGRATION_GUIDE.md). + +### v1: stable + +[![Build Status](https://github.com/oras-project/oras-go/actions/workflows/build.yml/badge.svg?event=push&branch=v1)](https://github.com/oras-project/oras-go/actions/workflows/build.yml?query=workflow%3Abuild+event%3Apush+branch%3Av1) +[![Go Report Card](https://goreportcard.com/badge/oras.land/oras-go)](https://goreportcard.com/report/oras.land/oras-go) +[![Go Reference](https://pkg.go.dev/badge/oras.land/oras-go.svg)](https://pkg.go.dev/oras.land/oras-go) + +As there are various stable projects depending on the ORAS Go library `v1`, the +[`v1`](https://github.com/oras-project/oras-go/tree/v1) branch +is maintained for API stability, dependency updates, and security patches. +All `v1.*` releases are based upon this branch. + +Since `v1` is in a maintenance state, you are highly encouraged +to use releases with major version `2` for new features. + +## Docs + +- [oras.land/client_libraries/go](https://oras.land/docs/Client_Libraries/go): Documentation for the ORAS Go library +- [Reviewing guide](https://github.com/oras-project/community/blob/main/REVIEWING.md): All reviewers must read the reviewing guide and agree to follow the project review guidelines. + +## Code of Conduct + +This project has adopted the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for further details. diff --git a/vendor/oras.land/oras-go/v2/SECURITY.md b/vendor/oras.land/oras-go/v2/SECURITY.md new file mode 100644 index 00000000..ffefe341 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/SECURITY.md @@ -0,0 +1,3 @@ +# Security Policy + +Please follow the [security policy](https://oras.land/docs/community/reporting_security_concerns) to report a security vulnerability or concern. diff --git a/vendor/oras.land/oras-go/v2/content.go b/vendor/oras.land/oras-go/v2/content.go new file mode 100644 index 00000000..b8bf2638 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/content.go @@ -0,0 +1,411 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package oras + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/content" + "oras.land/oras-go/v2/errdef" + "oras.land/oras-go/v2/internal/cas" + "oras.land/oras-go/v2/internal/docker" + "oras.land/oras-go/v2/internal/interfaces" + "oras.land/oras-go/v2/internal/platform" + "oras.land/oras-go/v2/internal/syncutil" + "oras.land/oras-go/v2/registry" + "oras.land/oras-go/v2/registry/remote/auth" +) + +const ( + // defaultTagConcurrency is the default concurrency of tagging. + defaultTagConcurrency int = 5 // This value is consistent with dockerd + + // defaultTagNMaxMetadataBytes is the default value of + // TagNOptions.MaxMetadataBytes. + defaultTagNMaxMetadataBytes int64 = 4 * 1024 * 1024 // 4 MiB + + // defaultResolveMaxMetadataBytes is the default value of + // ResolveOptions.MaxMetadataBytes. + defaultResolveMaxMetadataBytes int64 = 4 * 1024 * 1024 // 4 MiB + + // defaultMaxBytes is the default value of FetchBytesOptions.MaxBytes. + defaultMaxBytes int64 = 4 * 1024 * 1024 // 4 MiB +) + +// DefaultTagNOptions provides the default TagNOptions. +var DefaultTagNOptions TagNOptions + +// TagNOptions contains parameters for [oras.TagN]. +type TagNOptions struct { + // Concurrency limits the maximum number of concurrent tag tasks. + // If less than or equal to 0, a default (currently 5) is used. + Concurrency int + + // MaxMetadataBytes limits the maximum size of metadata that can be cached + // in the memory. + // If less than or equal to 0, a default (currently 4 MiB) is used. + MaxMetadataBytes int64 +} + +// TagN tags the descriptor identified by srcReference with dstReferences. +func TagN(ctx context.Context, target Target, srcReference string, dstReferences []string, opts TagNOptions) (ocispec.Descriptor, error) { + switch len(dstReferences) { + case 0: + return ocispec.Descriptor{}, fmt.Errorf("dstReferences cannot be empty: %w", errdef.ErrMissingReference) + case 1: + return Tag(ctx, target, srcReference, dstReferences[0]) + } + + if opts.Concurrency <= 0 { + opts.Concurrency = defaultTagConcurrency + } + if opts.MaxMetadataBytes <= 0 { + opts.MaxMetadataBytes = defaultTagNMaxMetadataBytes + } + + _, isRefFetcher := target.(registry.ReferenceFetcher) + _, isRefPusher := target.(registry.ReferencePusher) + if isRefFetcher && isRefPusher { + if repo, ok := target.(interfaces.ReferenceParser); ok { + // add scope hints to minimize the number of auth requests + ref, err := repo.ParseReference(srcReference) + if err != nil { + return ocispec.Descriptor{}, err + } + ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull, auth.ActionPush) + } + + desc, contentBytes, err := FetchBytes(ctx, target, srcReference, FetchBytesOptions{ + MaxBytes: opts.MaxMetadataBytes, + }) + if err != nil { + if errors.Is(err, errdef.ErrSizeExceedsLimit) { + err = fmt.Errorf( + "content size %v exceeds MaxMetadataBytes %v: %w", + desc.Size, + opts.MaxMetadataBytes, + errdef.ErrSizeExceedsLimit) + } + return ocispec.Descriptor{}, err + } + + if err := tagBytesN(ctx, target, desc, contentBytes, dstReferences, TagBytesNOptions{ + Concurrency: opts.Concurrency, + }); err != nil { + return ocispec.Descriptor{}, err + } + return desc, nil + } + + desc, err := target.Resolve(ctx, srcReference) + if err != nil { + return ocispec.Descriptor{}, err + } + eg, egCtx := syncutil.LimitGroup(ctx, opts.Concurrency) + for _, dstRef := range dstReferences { + eg.Go(func(dst string) func() error { + return func() error { + if err := target.Tag(egCtx, desc, dst); err != nil { + return fmt.Errorf("failed to tag %s as %s: %w", srcReference, dst, err) + } + return nil + } + }(dstRef)) + } + + if err := eg.Wait(); err != nil { + return ocispec.Descriptor{}, err + } + return desc, nil +} + +// Tag tags the descriptor identified by src with dst. +func Tag(ctx context.Context, target Target, src, dst string) (ocispec.Descriptor, error) { + refFetcher, okFetch := target.(registry.ReferenceFetcher) + refPusher, okPush := target.(registry.ReferencePusher) + if okFetch && okPush { + if repo, ok := target.(interfaces.ReferenceParser); ok { + // add scope hints to minimize the number of auth requests + ref, err := repo.ParseReference(src) + if err != nil { + return ocispec.Descriptor{}, err + } + ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull, auth.ActionPush) + } + desc, rc, err := refFetcher.FetchReference(ctx, src) + if err != nil { + return ocispec.Descriptor{}, err + } + defer rc.Close() + if err := refPusher.PushReference(ctx, desc, rc, dst); err != nil { + return ocispec.Descriptor{}, err + } + return desc, nil + } + + desc, err := target.Resolve(ctx, src) + if err != nil { + return ocispec.Descriptor{}, err + } + if err := target.Tag(ctx, desc, dst); err != nil { + return ocispec.Descriptor{}, err + } + return desc, nil +} + +// DefaultResolveOptions provides the default ResolveOptions. +var DefaultResolveOptions ResolveOptions + +// ResolveOptions contains parameters for [oras.Resolve]. +type ResolveOptions struct { + // TargetPlatform ensures the resolved content matches the target platform + // if the node is a manifest, or selects the first resolved content that + // matches the target platform if the node is a manifest list. + TargetPlatform *ocispec.Platform + + // MaxMetadataBytes limits the maximum size of metadata that can be cached + // in the memory. + // If less than or equal to 0, a default (currently 4 MiB) is used. + MaxMetadataBytes int64 +} + +// Resolve resolves a descriptor with provided reference from the target. +func Resolve(ctx context.Context, target ReadOnlyTarget, reference string, opts ResolveOptions) (ocispec.Descriptor, error) { + if opts.TargetPlatform == nil { + return target.Resolve(ctx, reference) + } + return resolve(ctx, target, nil, reference, opts) +} + +// resolve resolves a descriptor with provided reference from the target, with +// specified caching. +func resolve(ctx context.Context, target ReadOnlyTarget, proxy *cas.Proxy, reference string, opts ResolveOptions) (ocispec.Descriptor, error) { + if opts.MaxMetadataBytes <= 0 { + opts.MaxMetadataBytes = defaultResolveMaxMetadataBytes + } + + if refFetcher, ok := target.(registry.ReferenceFetcher); ok { + // optimize performance for ReferenceFetcher targets + desc, rc, err := refFetcher.FetchReference(ctx, reference) + if err != nil { + return ocispec.Descriptor{}, err + } + defer rc.Close() + + switch desc.MediaType { + case docker.MediaTypeManifestList, ocispec.MediaTypeImageIndex, + docker.MediaTypeManifest, ocispec.MediaTypeImageManifest: + // cache the fetched content + if desc.Size > opts.MaxMetadataBytes { + return ocispec.Descriptor{}, fmt.Errorf( + "content size %v exceeds MaxMetadataBytes %v: %w", + desc.Size, + opts.MaxMetadataBytes, + errdef.ErrSizeExceedsLimit) + } + if proxy == nil { + proxy = cas.NewProxyWithLimit(target, cas.NewMemory(), opts.MaxMetadataBytes) + } + if err := proxy.Cache.Push(ctx, desc, rc); err != nil { + return ocispec.Descriptor{}, err + } + // stop caching as SelectManifest may fetch a config blob + proxy.StopCaching = true + return platform.SelectManifest(ctx, proxy, desc, opts.TargetPlatform) + default: + return ocispec.Descriptor{}, fmt.Errorf("%s: %s: %w", desc.Digest, desc.MediaType, errdef.ErrUnsupported) + } + } + + desc, err := target.Resolve(ctx, reference) + if err != nil { + return ocispec.Descriptor{}, err + } + return platform.SelectManifest(ctx, target, desc, opts.TargetPlatform) +} + +// DefaultFetchOptions provides the default FetchOptions. +var DefaultFetchOptions FetchOptions + +// FetchOptions contains parameters for [oras.Fetch]. +type FetchOptions struct { + // ResolveOptions contains parameters for resolving reference. + ResolveOptions +} + +// Fetch fetches the content identified by the reference. +func Fetch(ctx context.Context, target ReadOnlyTarget, reference string, opts FetchOptions) (ocispec.Descriptor, io.ReadCloser, error) { + if opts.TargetPlatform == nil { + if refFetcher, ok := target.(registry.ReferenceFetcher); ok { + return refFetcher.FetchReference(ctx, reference) + } + + desc, err := target.Resolve(ctx, reference) + if err != nil { + return ocispec.Descriptor{}, nil, err + } + rc, err := target.Fetch(ctx, desc) + if err != nil { + return ocispec.Descriptor{}, nil, err + } + return desc, rc, nil + } + + if opts.MaxMetadataBytes <= 0 { + opts.MaxMetadataBytes = defaultResolveMaxMetadataBytes + } + proxy := cas.NewProxyWithLimit(target, cas.NewMemory(), opts.MaxMetadataBytes) + desc, err := resolve(ctx, target, proxy, reference, opts.ResolveOptions) + if err != nil { + return ocispec.Descriptor{}, nil, err + } + // if the content exists in cache, fetch it from cache + // otherwise fetch without caching + proxy.StopCaching = true + rc, err := proxy.Fetch(ctx, desc) + if err != nil { + return ocispec.Descriptor{}, nil, err + } + return desc, rc, nil +} + +// DefaultFetchBytesOptions provides the default FetchBytesOptions. +var DefaultFetchBytesOptions FetchBytesOptions + +// FetchBytesOptions contains parameters for [oras.FetchBytes]. +type FetchBytesOptions struct { + // FetchOptions contains parameters for fetching content. + FetchOptions + // MaxBytes limits the maximum size of the fetched content bytes. + // If less than or equal to 0, a default (currently 4 MiB) is used. + MaxBytes int64 +} + +// FetchBytes fetches the content bytes identified by the reference. +func FetchBytes(ctx context.Context, target ReadOnlyTarget, reference string, opts FetchBytesOptions) (ocispec.Descriptor, []byte, error) { + if opts.MaxBytes <= 0 { + opts.MaxBytes = defaultMaxBytes + } + + desc, rc, err := Fetch(ctx, target, reference, opts.FetchOptions) + if err != nil { + return ocispec.Descriptor{}, nil, err + } + defer rc.Close() + + if desc.Size > opts.MaxBytes { + return ocispec.Descriptor{}, nil, fmt.Errorf( + "content size %v exceeds MaxBytes %v: %w", + desc.Size, + opts.MaxBytes, + errdef.ErrSizeExceedsLimit) + } + bytes, err := content.ReadAll(rc, desc) + if err != nil { + return ocispec.Descriptor{}, nil, err + } + + return desc, bytes, nil +} + +// PushBytes describes the contentBytes using the given mediaType and pushes it. +// If mediaType is not specified, "application/octet-stream" is used. +func PushBytes(ctx context.Context, pusher content.Pusher, mediaType string, contentBytes []byte) (ocispec.Descriptor, error) { + desc := content.NewDescriptorFromBytes(mediaType, contentBytes) + r := bytes.NewReader(contentBytes) + if err := pusher.Push(ctx, desc, r); err != nil { + return ocispec.Descriptor{}, err + } + + return desc, nil +} + +// DefaultTagBytesNOptions provides the default TagBytesNOptions. +var DefaultTagBytesNOptions TagBytesNOptions + +// TagBytesNOptions contains parameters for [oras.TagBytesN]. +type TagBytesNOptions struct { + // Concurrency limits the maximum number of concurrent tag tasks. + // If less than or equal to 0, a default (currently 5) is used. + Concurrency int +} + +// TagBytesN describes the contentBytes using the given mediaType, pushes it, +// and tag it with the given references. +// If mediaType is not specified, "application/octet-stream" is used. +func TagBytesN(ctx context.Context, target Target, mediaType string, contentBytes []byte, references []string, opts TagBytesNOptions) (ocispec.Descriptor, error) { + if len(references) == 0 { + return PushBytes(ctx, target, mediaType, contentBytes) + } + + desc := content.NewDescriptorFromBytes(mediaType, contentBytes) + if opts.Concurrency <= 0 { + opts.Concurrency = defaultTagConcurrency + } + + if err := tagBytesN(ctx, target, desc, contentBytes, references, opts); err != nil { + return ocispec.Descriptor{}, err + } + return desc, nil +} + +// tagBytesN pushes the contentBytes using the given desc, and tag it with the +// given references. +func tagBytesN(ctx context.Context, target Target, desc ocispec.Descriptor, contentBytes []byte, references []string, opts TagBytesNOptions) error { + eg, egCtx := syncutil.LimitGroup(ctx, opts.Concurrency) + if refPusher, ok := target.(registry.ReferencePusher); ok { + for _, reference := range references { + eg.Go(func(ref string) func() error { + return func() error { + r := bytes.NewReader(contentBytes) + if err := refPusher.PushReference(egCtx, desc, r, ref); err != nil && !errors.Is(err, errdef.ErrAlreadyExists) { + return fmt.Errorf("failed to tag %s: %w", ref, err) + } + return nil + } + }(reference)) + } + } else { + r := bytes.NewReader(contentBytes) + if err := target.Push(ctx, desc, r); err != nil && !errors.Is(err, errdef.ErrAlreadyExists) { + return fmt.Errorf("failed to push content: %w", err) + } + for _, reference := range references { + eg.Go(func(ref string) func() error { + return func() error { + if err := target.Tag(egCtx, desc, ref); err != nil { + return fmt.Errorf("failed to tag %s: %w", ref, err) + } + return nil + } + }(reference)) + } + } + + return eg.Wait() +} + +// TagBytes describes the contentBytes using the given mediaType, pushes it, +// and tag it with the given reference. +// If mediaType is not specified, "application/octet-stream" is used. +func TagBytes(ctx context.Context, target Target, mediaType string, contentBytes []byte, reference string) (ocispec.Descriptor, error) { + return TagBytesN(ctx, target, mediaType, contentBytes, []string{reference}, DefaultTagBytesNOptions) +} diff --git a/vendor/oras.land/oras-go/v2/content/descriptor.go b/vendor/oras.land/oras-go/v2/content/descriptor.go new file mode 100644 index 00000000..8e6c25de --- /dev/null +++ b/vendor/oras.land/oras-go/v2/content/descriptor.go @@ -0,0 +1,40 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package content + +import ( + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/internal/descriptor" +) + +// NewDescriptorFromBytes returns a descriptor, given the content and media type. +// If no media type is specified, "application/octet-stream" will be used. +func NewDescriptorFromBytes(mediaType string, content []byte) ocispec.Descriptor { + if mediaType == "" { + mediaType = descriptor.DefaultMediaType + } + return ocispec.Descriptor{ + MediaType: mediaType, + Digest: digest.FromBytes(content), + Size: int64(len(content)), + } +} + +// Equal returns true if two descriptors point to the same content. +func Equal(a, b ocispec.Descriptor) bool { + return a.Size == b.Size && a.Digest == b.Digest && a.MediaType == b.MediaType +} diff --git a/vendor/oras.land/oras-go/v2/content/file/errors.go b/vendor/oras.land/oras-go/v2/content/file/errors.go new file mode 100644 index 00000000..36c35d4d --- /dev/null +++ b/vendor/oras.land/oras-go/v2/content/file/errors.go @@ -0,0 +1,28 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package file + +import "errors" + +var ( + ErrMissingName = errors.New("missing name") + ErrDuplicateName = errors.New("duplicate name") + ErrPathTraversalDisallowed = errors.New("path traversal disallowed") + ErrOverwriteDisallowed = errors.New("overwrite disallowed") + ErrStoreClosed = errors.New("store already closed") +) + +var errSkipUnnamed = errors.New("unnamed descriptor skipped") diff --git a/vendor/oras.land/oras-go/v2/content/file/file.go b/vendor/oras.land/oras-go/v2/content/file/file.go new file mode 100644 index 00000000..3f1e8c08 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/content/file/file.go @@ -0,0 +1,684 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at +http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package file provides implementation of a content store based on file system. +package file + +import ( + "compress/gzip" + "context" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "sync" + "sync/atomic" + + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/content" + "oras.land/oras-go/v2/errdef" + "oras.land/oras-go/v2/internal/cas" + "oras.land/oras-go/v2/internal/graph" + "oras.land/oras-go/v2/internal/ioutil" + "oras.land/oras-go/v2/internal/resolver" +) + +// bufPool is a pool of byte buffers that can be reused for copying content +// between files. +var bufPool = sync.Pool{ + New: func() interface{} { + // the buffer size should be larger than or equal to 128 KiB + // for performance considerations. + // we choose 1 MiB here so there will be less disk I/O. + buffer := make([]byte, 1<<20) // buffer size = 1 MiB + return &buffer + }, +} + +const ( + // AnnotationDigest is the annotation key for the digest of the uncompressed content. + AnnotationDigest = "io.deis.oras.content.digest" + // AnnotationUnpack is the annotation key for indication of unpacking. + AnnotationUnpack = "io.deis.oras.content.unpack" + // defaultBlobMediaType specifies the default blob media type. + defaultBlobMediaType = ocispec.MediaTypeImageLayer + // defaultBlobDirMediaType specifies the default blob directory media type. + defaultBlobDirMediaType = ocispec.MediaTypeImageLayerGzip + // defaultFallbackPushSizeLimit specifies the default size limit for pushing no-name contents. + defaultFallbackPushSizeLimit = 1 << 22 // 4 MiB +) + +// Store represents a file system based store, which implements `oras.Target`. +// +// In the file store, the contents described by names are location-addressed +// by file paths. Meanwhile, the file paths are mapped to a virtual CAS +// where all metadata are stored in the memory. +// +// The contents that are not described by names are stored in a fallback storage, +// which is a limited memory CAS by default. +// As all the metadata are stored in the memory, the file store +// cannot be restored from the file system. +// +// After use, the file store needs to be closed by calling the [Store.Close] function. +// The file store cannot be used after being closed. +type Store struct { + // TarReproducible controls if the tarballs generated + // for the added directories are reproducible. + // When specified, some metadata such as change time + // will be removed from the files in the tarballs. Default value: false. + TarReproducible bool + // AllowPathTraversalOnWrite controls if path traversal is allowed + // when writing files. When specified, writing files + // outside the working directory will be allowed. Default value: false. + AllowPathTraversalOnWrite bool + // DisableOverwrite controls if push operations can overwrite existing files. + // When specified, saving files to existing paths will be disabled. + // Default value: false. + DisableOverwrite bool + // ForceCAS controls if files with same content but different names are + // deduped after push operations. When a DAG is copied between CAS + // targets, nodes are deduped by content. By default, file store restores + // deduped successor files after a node is copied. This may result in two + // files with identical content. If this is not the desired behavior, + // ForceCAS can be specified to enforce CAS style dedup. + // Default value: false. + ForceCAS bool + // IgnoreNoName controls if push operations should ignore descriptors + // without a name. When specified, corresponding content will be discarded. + // Otherwise, content will be saved to a fallback storage. + // A typical scenario is pulling an arbitrary artifact masqueraded as OCI + // image to file store. This option can be specified to discard unnamed + // manifest and config file, while leaving only named layer files. + // Default value: false. + IgnoreNoName bool + // SkipUnpack controls if push operations should skip unpacking files. This + // value overrides the [AnnotationUnpack]. + // Default value: false. + SkipUnpack bool + + workingDir string // the working directory of the file store + closed int32 // if the store is closed - 0: false, 1: true. + digestToPath sync.Map // map[digest.Digest]string + nameToStatus sync.Map // map[string]*nameStatus + tmpFiles sync.Map // map[string]bool + + fallbackStorage content.Storage + resolver content.TagResolver + graph *graph.Memory +} + +// nameStatus contains a flag indicating if a name exists, +// and a RWMutex protecting it. +type nameStatus struct { + sync.RWMutex + exists bool +} + +// New creates a file store, using a default limited memory CAS +// as the fallback storage for contents without names. +// When pushing content without names, the size of content being pushed +// cannot exceed the default size limit: 4 MiB. +func New(workingDir string) (*Store, error) { + return NewWithFallbackLimit(workingDir, defaultFallbackPushSizeLimit) +} + +// NewWithFallbackLimit creates a file store, using a default +// limited memory CAS as the fallback storage for contents without names. +// When pushing content without names, the size of content being pushed +// cannot exceed the size limit specified by the `limit` parameter. +func NewWithFallbackLimit(workingDir string, limit int64) (*Store, error) { + m := cas.NewMemory() + ls := content.LimitStorage(m, limit) + return NewWithFallbackStorage(workingDir, ls) +} + +// NewWithFallbackStorage creates a file store, +// using the provided fallback storage for contents without names. +func NewWithFallbackStorage(workingDir string, fallbackStorage content.Storage) (*Store, error) { + workingDirAbs, err := filepath.Abs(workingDir) + if err != nil { + return nil, fmt.Errorf("failed to resolve absolute path for %s: %w", workingDir, err) + } + + return &Store{ + workingDir: workingDirAbs, + fallbackStorage: fallbackStorage, + resolver: resolver.NewMemory(), + graph: graph.NewMemory(), + }, nil +} + +// Close closes the file store and cleans up all the temporary files used by it. +// The store cannot be used after being closed. +// This function is not go-routine safe. +func (s *Store) Close() error { + if s.isClosedSet() { + return nil + } + s.setClosed() + + var errs []string + s.tmpFiles.Range(func(name, _ interface{}) bool { + if err := os.Remove(name.(string)); err != nil { + errs = append(errs, err.Error()) + } + return true + }) + + if len(errs) > 0 { + return errors.New(strings.Join(errs, "; ")) + } + return nil +} + +// Fetch fetches the content identified by the descriptor. +func (s *Store) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) { + if s.isClosedSet() { + return nil, ErrStoreClosed + } + + // if the target has name, check if the name exists. + name := target.Annotations[ocispec.AnnotationTitle] + if name != "" && !s.nameExists(name) { + return nil, fmt.Errorf("%s: %s: %w", name, target.MediaType, errdef.ErrNotFound) + } + + // check if the content exists in the store + val, exists := s.digestToPath.Load(target.Digest) + if exists { + path := val.(string) + + fp, err := os.Open(path) + if err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("%s: %s: %w", target.Digest, target.MediaType, errdef.ErrNotFound) + } + return nil, err + } + + return fp, nil + } + + // if the content does not exist in the store, + // then fall back to the fallback storage. + return s.fallbackStorage.Fetch(ctx, target) +} + +// Push pushes the content, matching the expected descriptor. +// If name is not specified in the descriptor, the content will be pushed to +// the fallback storage by default, or will be discarded when +// Store.IgnoreNoName is true. +func (s *Store) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error { + if s.isClosedSet() { + return ErrStoreClosed + } + + if err := s.push(ctx, expected, content); err != nil { + if errors.Is(err, errSkipUnnamed) { + return nil + } + return err + } + + if !s.ForceCAS { + if err := s.restoreDuplicates(ctx, expected); err != nil { + return fmt.Errorf("failed to restore duplicated file: %w", err) + } + } + + return s.graph.Index(ctx, s, expected) +} + +// push pushes the content, matching the expected descriptor. +// If name is not specified in the descriptor, the content will be pushed to +// the fallback storage by default, or will be discarded when +// Store.IgnoreNoName is true. +func (s *Store) push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error { + name := expected.Annotations[ocispec.AnnotationTitle] + if name == "" { + if s.IgnoreNoName { + return errSkipUnnamed + } + return s.fallbackStorage.Push(ctx, expected, content) + } + + // check the status of the name + status := s.status(name) + status.Lock() + defer status.Unlock() + + if status.exists { + return fmt.Errorf("%s: %w", name, ErrDuplicateName) + } + + target, err := s.resolveWritePath(name) + if err != nil { + return fmt.Errorf("failed to resolve path for writing: %w", err) + } + + if needUnpack := expected.Annotations[AnnotationUnpack]; needUnpack == "true" && !s.SkipUnpack { + err = s.pushDir(name, target, expected, content) + } else { + err = s.pushFile(target, expected, content) + } + if err != nil { + return err + } + + // update the name status as existed + status.exists = true + return nil +} + +// restoreDuplicates restores successor files with same content but different +// names. +// See Store.ForceCAS for more info. +func (s *Store) restoreDuplicates(ctx context.Context, desc ocispec.Descriptor) error { + successors, err := content.Successors(ctx, s, desc) + if err != nil { + return err + } + for _, successor := range successors { + name := successor.Annotations[ocispec.AnnotationTitle] + if name == "" || s.nameExists(name) { + continue + } + if err := func() error { + desc := ocispec.Descriptor{ + MediaType: successor.MediaType, + Digest: successor.Digest, + Size: successor.Size, + } + rc, err := s.Fetch(ctx, desc) + if err != nil { + return fmt.Errorf("%q: %s: %w", name, desc.MediaType, err) + } + defer rc.Close() + if err := s.push(ctx, successor, rc); err != nil { + return fmt.Errorf("%q: %s: %w", name, desc.MediaType, err) + } + return nil + }(); err != nil { + switch { + case errors.Is(err, errdef.ErrNotFound): + // allow pushing manifests before blobs + case errors.Is(err, ErrDuplicateName): + // in case multiple goroutines are pushing or restoring the same + // named content, the error is ignored + default: + return err + } + } + } + return nil +} + +// Exists returns true if the described content exists. +func (s *Store) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) { + if s.isClosedSet() { + return false, ErrStoreClosed + } + + // if the target has name, check if the name exists. + name := target.Annotations[ocispec.AnnotationTitle] + if name != "" && !s.nameExists(name) { + return false, nil + } + + // check if the content exists in the store + _, exists := s.digestToPath.Load(target.Digest) + if exists { + return true, nil + } + + // if the content does not exist in the store, + // then fall back to the fallback storage. + return s.fallbackStorage.Exists(ctx, target) +} + +// Resolve resolves a reference to a descriptor. +func (s *Store) Resolve(ctx context.Context, ref string) (ocispec.Descriptor, error) { + if s.isClosedSet() { + return ocispec.Descriptor{}, ErrStoreClosed + } + + if ref == "" { + return ocispec.Descriptor{}, errdef.ErrMissingReference + } + + return s.resolver.Resolve(ctx, ref) +} + +// Tag tags a descriptor with a reference string. +func (s *Store) Tag(ctx context.Context, desc ocispec.Descriptor, ref string) error { + if s.isClosedSet() { + return ErrStoreClosed + } + + if ref == "" { + return errdef.ErrMissingReference + } + + exists, err := s.Exists(ctx, desc) + if err != nil { + return err + } + if !exists { + return fmt.Errorf("%s: %s: %w", desc.Digest, desc.MediaType, errdef.ErrNotFound) + } + + return s.resolver.Tag(ctx, desc, ref) +} + +// Predecessors returns the nodes directly pointing to the current node. +// Predecessors returns nil without error if the node does not exists in the +// store. +func (s *Store) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) { + if s.isClosedSet() { + return nil, ErrStoreClosed + } + + return s.graph.Predecessors(ctx, node) +} + +// Add adds a file into the file store. +func (s *Store) Add(_ context.Context, name, mediaType, path string) (ocispec.Descriptor, error) { + if s.isClosedSet() { + return ocispec.Descriptor{}, ErrStoreClosed + } + + if name == "" { + return ocispec.Descriptor{}, ErrMissingName + } + + // check the status of the name + status := s.status(name) + status.Lock() + defer status.Unlock() + + if status.exists { + return ocispec.Descriptor{}, fmt.Errorf("%s: %w", name, ErrDuplicateName) + } + + if path == "" { + path = name + } + path = s.absPath(path) + + fi, err := os.Stat(path) + if err != nil { + return ocispec.Descriptor{}, fmt.Errorf("failed to stat %s: %w", path, err) + } + + // generate descriptor + var desc ocispec.Descriptor + if fi.IsDir() { + desc, err = s.descriptorFromDir(name, mediaType, path) + } else { + desc, err = s.descriptorFromFile(fi, mediaType, path) + } + if err != nil { + return ocispec.Descriptor{}, fmt.Errorf("failed to generate descriptor from %s: %w", path, err) + } + + if desc.Annotations == nil { + desc.Annotations = make(map[string]string) + } + desc.Annotations[ocispec.AnnotationTitle] = name + + // update the name status as existed + status.exists = true + return desc, nil +} + +// saveFile saves content matching the descriptor to the given file. +func (s *Store) saveFile(fp *os.File, expected ocispec.Descriptor, content io.Reader) (err error) { + defer func() { + closeErr := fp.Close() + if err == nil { + err = closeErr + } + }() + path := fp.Name() + + buf := bufPool.Get().(*[]byte) + defer bufPool.Put(buf) + if err := ioutil.CopyBuffer(fp, content, *buf, expected); err != nil { + return fmt.Errorf("failed to copy content to %s: %w", path, err) + } + + s.digestToPath.Store(expected.Digest, path) + return nil +} + +// pushFile saves content matching the descriptor to the target path. +func (s *Store) pushFile(target string, expected ocispec.Descriptor, content io.Reader) error { + if err := ensureDir(filepath.Dir(target)); err != nil { + return fmt.Errorf("failed to ensure directories of the target path: %w", err) + } + + fp, err := os.Create(target) + if err != nil { + return fmt.Errorf("failed to create file %s: %w", target, err) + } + + return s.saveFile(fp, expected, content) +} + +// pushDir saves content matching the descriptor to the target directory. +func (s *Store) pushDir(name, target string, expected ocispec.Descriptor, content io.Reader) (err error) { + if err := ensureDir(target); err != nil { + return fmt.Errorf("failed to ensure directories of the target path: %w", err) + } + + gz, err := s.tempFile() + if err != nil { + return err + } + + gzPath := gz.Name() + // the digest of the gz is verified while saving + if err := s.saveFile(gz, expected, content); err != nil { + return fmt.Errorf("failed to save gzip to %s: %w", gzPath, err) + } + + checksum := expected.Annotations[AnnotationDigest] + buf := bufPool.Get().(*[]byte) + defer bufPool.Put(buf) + if err := extractTarGzip(target, name, gzPath, checksum, *buf); err != nil { + return fmt.Errorf("failed to extract tar to %s: %w", target, err) + } + return nil +} + +// descriptorFromDir generates descriptor from the given directory. +func (s *Store) descriptorFromDir(name, mediaType, dir string) (desc ocispec.Descriptor, err error) { + // make a temp file to store the gzip + gz, err := s.tempFile() + if err != nil { + return ocispec.Descriptor{}, err + } + defer func() { + closeErr := gz.Close() + if err == nil { + err = closeErr + } + }() + + // compress the directory + gzDigester := digest.Canonical.Digester() + gzw := gzip.NewWriter(io.MultiWriter(gz, gzDigester.Hash())) + defer func() { + closeErr := gzw.Close() + if err == nil { + err = closeErr + } + }() + + tarDigester := digest.Canonical.Digester() + tw := io.MultiWriter(gzw, tarDigester.Hash()) + buf := bufPool.Get().(*[]byte) + defer bufPool.Put(buf) + if err := tarDirectory(dir, name, tw, s.TarReproducible, *buf); err != nil { + return ocispec.Descriptor{}, fmt.Errorf("failed to tar %s: %w", dir, err) + } + + // flush all + if err := gzw.Close(); err != nil { + return ocispec.Descriptor{}, err + } + if err := gz.Sync(); err != nil { + return ocispec.Descriptor{}, err + } + + fi, err := gz.Stat() + if err != nil { + return ocispec.Descriptor{}, err + } + + // map gzip digest to gzip path + gzDigest := gzDigester.Digest() + s.digestToPath.Store(gzDigest, gz.Name()) + + // generate descriptor + if mediaType == "" { + mediaType = defaultBlobDirMediaType + } + + return ocispec.Descriptor{ + MediaType: mediaType, + Digest: gzDigest, // digest for the compressed content + Size: fi.Size(), + Annotations: map[string]string{ + AnnotationDigest: tarDigester.Digest().String(), // digest fot the uncompressed content + AnnotationUnpack: "true", // the content needs to be unpacked + }, + }, nil +} + +// descriptorFromFile generates descriptor from the given file. +func (s *Store) descriptorFromFile(fi os.FileInfo, mediaType, path string) (desc ocispec.Descriptor, err error) { + fp, err := os.Open(path) + if err != nil { + return ocispec.Descriptor{}, err + } + defer func() { + closeErr := fp.Close() + if err == nil { + err = closeErr + } + }() + + dgst, err := digest.FromReader(fp) + if err != nil { + return ocispec.Descriptor{}, err + } + // map digest to file path + s.digestToPath.Store(dgst, path) + + // generate descriptor + if mediaType == "" { + mediaType = defaultBlobMediaType + } + + return ocispec.Descriptor{ + MediaType: mediaType, + Digest: dgst, + Size: fi.Size(), + }, nil +} + +// resolveWritePath resolves the path to write for the given name. +func (s *Store) resolveWritePath(name string) (string, error) { + path := s.absPath(name) + if !s.AllowPathTraversalOnWrite { + base, err := filepath.Abs(s.workingDir) + if err != nil { + return "", err + } + target, err := filepath.Abs(path) + if err != nil { + return "", err + } + rel, err := filepath.Rel(base, target) + if err != nil { + return "", ErrPathTraversalDisallowed + } + rel = filepath.ToSlash(rel) + if strings.HasPrefix(rel, "../") || rel == ".." { + return "", ErrPathTraversalDisallowed + } + } + if s.DisableOverwrite { + if _, err := os.Stat(path); err == nil { + return "", ErrOverwriteDisallowed + } else if !os.IsNotExist(err) { + return "", err + } + } + return path, nil +} + +// status returns the nameStatus for the given name. +func (s *Store) status(name string) *nameStatus { + v, _ := s.nameToStatus.LoadOrStore(name, &nameStatus{sync.RWMutex{}, false}) + status := v.(*nameStatus) + return status +} + +// nameExists returns if the given name exists in the file store. +func (s *Store) nameExists(name string) bool { + status := s.status(name) + status.RLock() + defer status.RUnlock() + + return status.exists +} + +// tempFile creates a temp file with the file name format "oras_file_randomString", +// and returns the pointer to the temp file. +func (s *Store) tempFile() (*os.File, error) { + tmp, err := os.CreateTemp("", "oras_file_*") + if err != nil { + return nil, err + } + + s.tmpFiles.Store(tmp.Name(), true) + return tmp, nil +} + +// absPath returns the absolute path of the path. +func (s *Store) absPath(path string) string { + if filepath.IsAbs(path) { + return path + } + return filepath.Join(s.workingDir, path) +} + +// isClosedSet returns true if the `closed` flag is set, otherwise returns false. +func (s *Store) isClosedSet() bool { + return atomic.LoadInt32(&s.closed) == 1 +} + +// setClosed sets the `closed` flag. +func (s *Store) setClosed() { + atomic.StoreInt32(&s.closed, 1) +} + +// ensureDir ensures the directories of the path exists. +func ensureDir(path string) error { + return os.MkdirAll(path, 0777) +} diff --git a/vendor/oras.land/oras-go/v2/content/file/utils.go b/vendor/oras.land/oras-go/v2/content/file/utils.go new file mode 100644 index 00000000..c42013d8 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/content/file/utils.go @@ -0,0 +1,261 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package file + +import ( + "archive/tar" + "compress/gzip" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "time" + + "github.com/opencontainers/go-digest" +) + +// tarDirectory walks the directory specified by path, and tar those files with a new +// path prefix. +func tarDirectory(root, prefix string, w io.Writer, removeTimes bool, buf []byte) (err error) { + tw := tar.NewWriter(w) + defer func() { + closeErr := tw.Close() + if err == nil { + err = closeErr + } + }() + + return filepath.Walk(root, func(path string, info os.FileInfo, err error) (returnErr error) { + if err != nil { + return err + } + + // Rename path + name, err := filepath.Rel(root, path) + if err != nil { + return err + } + name = filepath.Join(prefix, name) + name = filepath.ToSlash(name) + + // Generate header + var link string + mode := info.Mode() + if mode&os.ModeSymlink != 0 { + if link, err = os.Readlink(path); err != nil { + return err + } + } + header, err := tar.FileInfoHeader(info, link) + if err != nil { + return fmt.Errorf("%s: %w", path, err) + } + header.Name = name + header.Uid = 0 + header.Gid = 0 + header.Uname = "" + header.Gname = "" + + if removeTimes { + header.ModTime = time.Time{} + header.AccessTime = time.Time{} + header.ChangeTime = time.Time{} + } + + // Write file + if err := tw.WriteHeader(header); err != nil { + return fmt.Errorf("tar: %w", err) + } + if mode.IsRegular() { + fp, err := os.Open(path) + if err != nil { + return err + } + defer func() { + closeErr := fp.Close() + if returnErr == nil { + returnErr = closeErr + } + }() + + if _, err := io.CopyBuffer(tw, fp, buf); err != nil { + return fmt.Errorf("failed to copy to %s: %w", path, err) + } + } + + return nil + }) +} + +// extractTarGzip decompresses the gzip +// and extracts tar file to a directory specified by the `dir` parameter. +func extractTarGzip(dir, prefix, filename, checksum string, buf []byte) (err error) { + fp, err := os.Open(filename) + if err != nil { + return err + } + defer func() { + closeErr := fp.Close() + if err == nil { + err = closeErr + } + }() + + gzr, err := gzip.NewReader(fp) + if err != nil { + return err + } + defer func() { + closeErr := gzr.Close() + if err == nil { + err = closeErr + } + }() + + var r io.Reader = gzr + var verifier digest.Verifier + if checksum != "" { + if digest, err := digest.Parse(checksum); err == nil { + verifier = digest.Verifier() + r = io.TeeReader(r, verifier) + } + } + if err := extractTarDirectory(dir, prefix, r, buf); err != nil { + return err + } + if verifier != nil && !verifier.Verified() { + return errors.New("content digest mismatch") + } + return nil +} + +// extractTarDirectory extracts tar file to a directory specified by the `dir` +// parameter. The file name prefix is ensured to be the string specified by the +// `prefix` parameter and is trimmed. +func extractTarDirectory(dir, prefix string, r io.Reader, buf []byte) error { + tr := tar.NewReader(r) + for { + header, err := tr.Next() + if err != nil { + if err == io.EOF { + return nil + } + return err + } + + // Name check + name := header.Name + path, err := ensureBasePath(dir, prefix, name) + if err != nil { + return err + } + path = filepath.Join(dir, path) + + // Create content + switch header.Typeflag { + case tar.TypeReg: + err = writeFile(path, tr, header.FileInfo().Mode(), buf) + case tar.TypeDir: + err = os.MkdirAll(path, header.FileInfo().Mode()) + case tar.TypeLink: + var target string + if target, err = ensureLinkPath(dir, prefix, path, header.Linkname); err == nil { + err = os.Link(target, path) + } + case tar.TypeSymlink: + var target string + if target, err = ensureLinkPath(dir, prefix, path, header.Linkname); err == nil { + err = os.Symlink(target, path) + } + default: + continue // Non-regular files are skipped + } + if err != nil { + return err + } + + // Change access time and modification time if possible (error ignored) + os.Chtimes(path, header.AccessTime, header.ModTime) + } +} + +// ensureBasePath ensures the target path is in the base path, +// returning its relative path to the base path. +// target can be either an absolute path or a relative path. +func ensureBasePath(baseAbs, baseRel, target string) (string, error) { + base := baseRel + if filepath.IsAbs(target) { + // ensure base and target are consistent + base = baseAbs + } + path, err := filepath.Rel(base, target) + if err != nil { + return "", err + } + cleanPath := filepath.ToSlash(filepath.Clean(path)) + if cleanPath == ".." || strings.HasPrefix(cleanPath, "../") { + return "", fmt.Errorf("%q is outside of %q", target, baseRel) + } + + // No symbolic link allowed in the relative path + dir := filepath.Dir(path) + for dir != "." { + if info, err := os.Lstat(filepath.Join(baseAbs, dir)); err != nil { + if !os.IsNotExist(err) { + return "", err + } + } else if info.Mode()&os.ModeSymlink != 0 { + return "", fmt.Errorf("no symbolic link allowed between %q and %q", baseRel, target) + } + dir = filepath.Dir(dir) + } + + return path, nil +} + +// ensureLinkPath ensures the target path pointed by the link is in the base +// path. It returns target path if validated. +func ensureLinkPath(baseAbs, baseRel, link, target string) (string, error) { + // resolve link + path := target + if !filepath.IsAbs(target) { + path = filepath.Join(filepath.Dir(link), target) + } + // ensure path is under baseAbs or baseRel + if _, err := ensureBasePath(baseAbs, baseRel, path); err != nil { + return "", err + } + return target, nil +} + +// writeFile writes content to the file specified by the `path` parameter. +func writeFile(path string, r io.Reader, perm os.FileMode, buf []byte) (err error) { + file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm) + if err != nil { + return err + } + defer func() { + closeErr := file.Close() + if err == nil { + err = closeErr + } + }() + + _, err = io.CopyBuffer(file, r, buf) + return err +} diff --git a/vendor/oras.land/oras-go/v2/content/graph.go b/vendor/oras.land/oras-go/v2/content/graph.go new file mode 100644 index 00000000..9ae83728 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/content/graph.go @@ -0,0 +1,122 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package content + +import ( + "context" + "encoding/json" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/internal/docker" + "oras.land/oras-go/v2/internal/spec" +) + +// PredecessorFinder finds out the nodes directly pointing to a given node of a +// directed acyclic graph. +// In other words, returns the "parents" of the current descriptor. +// PredecessorFinder is an extension of Storage. +type PredecessorFinder interface { + // Predecessors returns the nodes directly pointing to the current node. + Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) +} + +// GraphStorage represents a CAS that supports direct predecessor node finding. +type GraphStorage interface { + Storage + PredecessorFinder +} + +// ReadOnlyGraphStorage represents a read-only GraphStorage. +type ReadOnlyGraphStorage interface { + ReadOnlyStorage + PredecessorFinder +} + +// Successors returns the nodes directly pointed by the current node. +// In other words, returns the "children" of the current descriptor. +func Successors(ctx context.Context, fetcher Fetcher, node ocispec.Descriptor) ([]ocispec.Descriptor, error) { + switch node.MediaType { + case docker.MediaTypeManifest: + content, err := FetchAll(ctx, fetcher, node) + if err != nil { + return nil, err + } + // OCI manifest schema can be used to marshal docker manifest + var manifest ocispec.Manifest + if err := json.Unmarshal(content, &manifest); err != nil { + return nil, err + } + return append([]ocispec.Descriptor{manifest.Config}, manifest.Layers...), nil + case ocispec.MediaTypeImageManifest: + content, err := FetchAll(ctx, fetcher, node) + if err != nil { + return nil, err + } + var manifest ocispec.Manifest + if err := json.Unmarshal(content, &manifest); err != nil { + return nil, err + } + var nodes []ocispec.Descriptor + if manifest.Subject != nil { + nodes = append(nodes, *manifest.Subject) + } + nodes = append(nodes, manifest.Config) + return append(nodes, manifest.Layers...), nil + case docker.MediaTypeManifestList: + content, err := FetchAll(ctx, fetcher, node) + if err != nil { + return nil, err + } + + // OCI manifest index schema can be used to marshal docker manifest list + var index ocispec.Index + if err := json.Unmarshal(content, &index); err != nil { + return nil, err + } + return index.Manifests, nil + case ocispec.MediaTypeImageIndex: + content, err := FetchAll(ctx, fetcher, node) + if err != nil { + return nil, err + } + + var index ocispec.Index + if err := json.Unmarshal(content, &index); err != nil { + return nil, err + } + var nodes []ocispec.Descriptor + if index.Subject != nil { + nodes = append(nodes, *index.Subject) + } + return append(nodes, index.Manifests...), nil + case spec.MediaTypeArtifactManifest: + content, err := FetchAll(ctx, fetcher, node) + if err != nil { + return nil, err + } + + var manifest spec.Artifact + if err := json.Unmarshal(content, &manifest); err != nil { + return nil, err + } + var nodes []ocispec.Descriptor + if manifest.Subject != nil { + nodes = append(nodes, *manifest.Subject) + } + return append(nodes, manifest.Blobs...), nil + } + return nil, nil +} diff --git a/vendor/oras.land/oras-go/v2/content/limitedstorage.go b/vendor/oras.land/oras-go/v2/content/limitedstorage.go new file mode 100644 index 00000000..9a6df2f8 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/content/limitedstorage.go @@ -0,0 +1,50 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package content + +import ( + "context" + "fmt" + "io" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/errdef" +) + +// LimitedStorage represents a CAS with a push size limit. +type LimitedStorage struct { + Storage // underlying storage + PushLimit int64 // max size for push +} + +// Push pushes the content, matching the expected descriptor. +// The size of the content cannot exceed the push size limit. +func (ls *LimitedStorage) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error { + if expected.Size > ls.PushLimit { + return fmt.Errorf( + "content size %v exceeds push size limit %v: %w", + expected.Size, + ls.PushLimit, + errdef.ErrSizeExceedsLimit) + } + + return ls.Storage.Push(ctx, expected, io.LimitReader(content, expected.Size)) +} + +// LimitStorage returns a storage with a push size limit. +func LimitStorage(s Storage, n int64) *LimitedStorage { + return &LimitedStorage{s, n} +} diff --git a/vendor/oras.land/oras-go/v2/content/reader.go b/vendor/oras.land/oras-go/v2/content/reader.go new file mode 100644 index 00000000..e575378e --- /dev/null +++ b/vendor/oras.land/oras-go/v2/content/reader.go @@ -0,0 +1,144 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package content + +import ( + "errors" + "fmt" + "io" + + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +var ( + // ErrInvalidDescriptorSize is returned by ReadAll() when + // the descriptor has an invalid size. + ErrInvalidDescriptorSize = errors.New("invalid descriptor size") + + // ErrMismatchedDigest is returned by ReadAll() when + // the descriptor has an invalid digest. + ErrMismatchedDigest = errors.New("mismatched digest") + + // ErrTrailingData is returned by ReadAll() when + // there exists trailing data unread when the read terminates. + ErrTrailingData = errors.New("trailing data") +) + +var ( + // errEarlyVerify is returned by VerifyReader.Verify() when + // Verify() is called before completing reading the entire content blob. + errEarlyVerify = errors.New("early verify") +) + +// VerifyReader reads the content described by its descriptor and verifies +// against its size and digest. +type VerifyReader struct { + base *io.LimitedReader + verifier digest.Verifier + verified bool + err error +} + +// Read reads up to len(p) bytes into p. It returns the number of bytes +// read (0 <= n <= len(p)) and any error encountered. +func (vr *VerifyReader) Read(p []byte) (n int, err error) { + if vr.err != nil { + return 0, vr.err + } + + n, err = vr.base.Read(p) + if err != nil { + if err == io.EOF && vr.base.N > 0 { + err = io.ErrUnexpectedEOF + } + vr.err = err + } + return +} + +// Verify checks for remaining unread content and verifies the read content against the digest +func (vr *VerifyReader) Verify() error { + if vr.verified { + return nil + } + if vr.err == nil { + if vr.base.N > 0 { + return errEarlyVerify + } + } else if vr.err != io.EOF { + return vr.err + } + + if err := ensureEOF(vr.base.R); err != nil { + vr.err = err + return vr.err + } + if !vr.verifier.Verified() { + vr.err = ErrMismatchedDigest + return vr.err + } + + vr.verified = true + vr.err = io.EOF + return nil +} + +// NewVerifyReader wraps r for reading content with verification against desc. +func NewVerifyReader(r io.Reader, desc ocispec.Descriptor) *VerifyReader { + verifier := desc.Digest.Verifier() + lr := &io.LimitedReader{ + R: io.TeeReader(r, verifier), + N: desc.Size, + } + return &VerifyReader{ + base: lr, + verifier: verifier, + } +} + +// ReadAll safely reads the content described by the descriptor. +// The read content is verified against the size and the digest +// using a VerifyReader. +func ReadAll(r io.Reader, desc ocispec.Descriptor) ([]byte, error) { + if desc.Size < 0 { + return nil, ErrInvalidDescriptorSize + } + buf := make([]byte, desc.Size) + + vr := NewVerifyReader(r, desc) + if n, err := io.ReadFull(vr, buf); err != nil { + if errors.Is(err, io.ErrUnexpectedEOF) { + return nil, fmt.Errorf("read failed: expected content size of %d, got %d, for digest %s: %w", desc.Size, n, desc.Digest.String(), err) + } + return nil, fmt.Errorf("read failed: %w", err) + } + if err := vr.Verify(); err != nil { + return nil, err + } + return buf, nil +} + +// ensureEOF ensures the read operation ends with an EOF and no +// trailing data is present. +func ensureEOF(r io.Reader) error { + var peek [1]byte + _, err := io.ReadFull(r, peek[:]) + if err != io.EOF { + return ErrTrailingData + } + return nil +} diff --git a/vendor/oras.land/oras-go/v2/content/resolver.go b/vendor/oras.land/oras-go/v2/content/resolver.go new file mode 100644 index 00000000..bc0fd8df --- /dev/null +++ b/vendor/oras.land/oras-go/v2/content/resolver.go @@ -0,0 +1,47 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package content provides implementations to access content stores. +package content + +import ( + "context" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +// Resolver resolves reference tags. +type Resolver interface { + // Resolve resolves a reference to a descriptor. + Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) +} + +// Tagger tags reference tags. +type Tagger interface { + // Tag tags a descriptor with a reference string. + Tag(ctx context.Context, desc ocispec.Descriptor, reference string) error +} + +// TagResolver provides reference tag indexing services. +type TagResolver interface { + Tagger + Resolver +} + +// Untagger untags reference tags. +type Untagger interface { + // Untag untags the given reference string. + Untag(ctx context.Context, reference string) error +} diff --git a/vendor/oras.land/oras-go/v2/content/storage.go b/vendor/oras.land/oras-go/v2/content/storage.go new file mode 100644 index 00000000..47c95d87 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/content/storage.go @@ -0,0 +1,80 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package content + +import ( + "context" + "io" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +// Fetcher fetches content. +type Fetcher interface { + // Fetch fetches the content identified by the descriptor. + Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) +} + +// Pusher pushes content. +type Pusher interface { + // Push pushes the content, matching the expected descriptor. + // Reader is preferred to Writer so that the suitable buffer size can be + // chosen by the underlying implementation. Furthermore, the implementation + // can also do reflection on the Reader for more advanced I/O optimization. + Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error +} + +// Storage represents a content-addressable storage (CAS) where contents are +// accessed via Descriptors. +// The storage is designed to handle blobs of large sizes. +type Storage interface { + ReadOnlyStorage + Pusher +} + +// ReadOnlyStorage represents a read-only Storage. +type ReadOnlyStorage interface { + Fetcher + + // Exists returns true if the described content exists. + Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) +} + +// Deleter removes content. +// Deleter is an extension of Storage. +type Deleter interface { + // Delete removes the content identified by the descriptor. + Delete(ctx context.Context, target ocispec.Descriptor) error +} + +// FetchAll safely fetches the content described by the descriptor. +// The fetched content is verified against the size and the digest. +func FetchAll(ctx context.Context, fetcher Fetcher, desc ocispec.Descriptor) ([]byte, error) { + rc, err := fetcher.Fetch(ctx, desc) + if err != nil { + return nil, err + } + defer rc.Close() + return ReadAll(rc, desc) +} + +// FetcherFunc is the basic Fetch method defined in Fetcher. +type FetcherFunc func(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) + +// Fetch performs Fetch operation by the FetcherFunc. +func (fn FetcherFunc) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) { + return fn(ctx, target) +} diff --git a/vendor/oras.land/oras-go/v2/copy.go b/vendor/oras.land/oras-go/v2/copy.go new file mode 100644 index 00000000..2f131a8c --- /dev/null +++ b/vendor/oras.land/oras-go/v2/copy.go @@ -0,0 +1,516 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package oras + +import ( + "context" + "errors" + "fmt" + "io" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "golang.org/x/sync/semaphore" + "oras.land/oras-go/v2/content" + "oras.land/oras-go/v2/errdef" + "oras.land/oras-go/v2/internal/cas" + "oras.land/oras-go/v2/internal/descriptor" + "oras.land/oras-go/v2/internal/platform" + "oras.land/oras-go/v2/internal/registryutil" + "oras.land/oras-go/v2/internal/status" + "oras.land/oras-go/v2/internal/syncutil" + "oras.land/oras-go/v2/registry" +) + +// defaultConcurrency is the default value of CopyGraphOptions.Concurrency. +const defaultConcurrency int = 3 // This value is consistent with dockerd and containerd. + +// SkipNode signals to stop copying a node. When returned from PreCopy the blob must exist in the target. +// This can be used to signal that a blob has been made available in the target repository by "Mount()" or some other technique. +var SkipNode = errors.New("skip node") + +// DefaultCopyOptions provides the default CopyOptions. +var DefaultCopyOptions CopyOptions = CopyOptions{ + CopyGraphOptions: DefaultCopyGraphOptions, +} + +// CopyOptions contains parameters for [oras.Copy]. +type CopyOptions struct { + CopyGraphOptions + // MapRoot maps the resolved root node to a desired root node for copy. + // When MapRoot is provided, the descriptor resolved from the source + // reference will be passed to MapRoot, and the mapped descriptor will be + // used as the root node for copy. + MapRoot func(ctx context.Context, src content.ReadOnlyStorage, root ocispec.Descriptor) (ocispec.Descriptor, error) +} + +// WithTargetPlatform configures opts.MapRoot to select the manifest whose +// platform matches the given platform. When MapRoot is provided, the platform +// selection will be applied on the mapped root node. +// - If the given platform is nil, no platform selection will be applied. +// - If the root node is a manifest, it will remain the same if platform +// matches, otherwise ErrNotFound will be returned. +// - If the root node is a manifest list, it will be mapped to the first +// matching manifest if exists, otherwise ErrNotFound will be returned. +// - Otherwise ErrUnsupported will be returned. +func (opts *CopyOptions) WithTargetPlatform(p *ocispec.Platform) { + if p == nil { + return + } + mapRoot := opts.MapRoot + opts.MapRoot = func(ctx context.Context, src content.ReadOnlyStorage, root ocispec.Descriptor) (desc ocispec.Descriptor, err error) { + if mapRoot != nil { + if root, err = mapRoot(ctx, src, root); err != nil { + return ocispec.Descriptor{}, err + } + } + return platform.SelectManifest(ctx, src, root, p) + } +} + +// defaultCopyMaxMetadataBytes is the default value of +// CopyGraphOptions.MaxMetadataBytes. +const defaultCopyMaxMetadataBytes int64 = 4 * 1024 * 1024 // 4 MiB + +// DefaultCopyGraphOptions provides the default CopyGraphOptions. +var DefaultCopyGraphOptions CopyGraphOptions + +// CopyGraphOptions contains parameters for [oras.CopyGraph]. +type CopyGraphOptions struct { + // Concurrency limits the maximum number of concurrent copy tasks. + // If less than or equal to 0, a default (currently 3) is used. + Concurrency int + // MaxMetadataBytes limits the maximum size of the metadata that can be + // cached in the memory. + // If less than or equal to 0, a default (currently 4 MiB) is used. + MaxMetadataBytes int64 + // PreCopy handles the current descriptor before it is copied. PreCopy can + // return a SkipNode to signal that desc should be skipped when it already + // exists in the target. + PreCopy func(ctx context.Context, desc ocispec.Descriptor) error + // PostCopy handles the current descriptor after it is copied. + PostCopy func(ctx context.Context, desc ocispec.Descriptor) error + // OnCopySkipped will be called when the sub-DAG rooted by the current node + // is skipped. + OnCopySkipped func(ctx context.Context, desc ocispec.Descriptor) error + // MountFrom returns the candidate repositories that desc may be mounted from. + // The OCI references will be tried in turn. If mounting fails on all of them, + // then it falls back to a copy. + MountFrom func(ctx context.Context, desc ocispec.Descriptor) ([]string, error) + // OnMounted will be invoked when desc is mounted. + OnMounted func(ctx context.Context, desc ocispec.Descriptor) error + // FindSuccessors finds the successors of the current node. + // fetcher provides cached access to the source storage, and is suitable + // for fetching non-leaf nodes like manifests. Since anything fetched from + // fetcher will be cached in the memory, it is recommended to use original + // source storage to fetch large blobs. + // If FindSuccessors is nil, content.Successors will be used. + FindSuccessors func(ctx context.Context, fetcher content.Fetcher, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) +} + +// Copy copies a rooted directed acyclic graph (DAG) with the tagged root node +// in the source Target to the destination Target. +// The destination reference will be the same as the source reference if the +// destination reference is left blank. +// +// Returns the descriptor of the root node on successful copy. +func Copy(ctx context.Context, src ReadOnlyTarget, srcRef string, dst Target, dstRef string, opts CopyOptions) (ocispec.Descriptor, error) { + if src == nil { + return ocispec.Descriptor{}, errors.New("nil source target") + } + if dst == nil { + return ocispec.Descriptor{}, errors.New("nil destination target") + } + if dstRef == "" { + dstRef = srcRef + } + + // use caching proxy on non-leaf nodes + if opts.MaxMetadataBytes <= 0 { + opts.MaxMetadataBytes = defaultCopyMaxMetadataBytes + } + proxy := cas.NewProxyWithLimit(src, cas.NewMemory(), opts.MaxMetadataBytes) + root, err := resolveRoot(ctx, src, srcRef, proxy) + if err != nil { + return ocispec.Descriptor{}, fmt.Errorf("failed to resolve %s: %w", srcRef, err) + } + + if opts.MapRoot != nil { + proxy.StopCaching = true + root, err = opts.MapRoot(ctx, proxy, root) + if err != nil { + return ocispec.Descriptor{}, err + } + proxy.StopCaching = false + } + + if err := prepareCopy(ctx, dst, dstRef, proxy, root, &opts); err != nil { + return ocispec.Descriptor{}, err + } + + if err := copyGraph(ctx, src, dst, root, proxy, nil, nil, opts.CopyGraphOptions); err != nil { + return ocispec.Descriptor{}, err + } + + return root, nil +} + +// CopyGraph copies a rooted directed acyclic graph (DAG) from the source CAS to +// the destination CAS. +func CopyGraph(ctx context.Context, src content.ReadOnlyStorage, dst content.Storage, root ocispec.Descriptor, opts CopyGraphOptions) error { + return copyGraph(ctx, src, dst, root, nil, nil, nil, opts) +} + +// copyGraph copies a rooted directed acyclic graph (DAG) from the source CAS to +// the destination CAS with specified caching, concurrency limiter and tracker. +func copyGraph(ctx context.Context, src content.ReadOnlyStorage, dst content.Storage, root ocispec.Descriptor, + proxy *cas.Proxy, limiter *semaphore.Weighted, tracker *status.Tracker, opts CopyGraphOptions) error { + if proxy == nil { + // use caching proxy on non-leaf nodes + if opts.MaxMetadataBytes <= 0 { + opts.MaxMetadataBytes = defaultCopyMaxMetadataBytes + } + proxy = cas.NewProxyWithLimit(src, cas.NewMemory(), opts.MaxMetadataBytes) + } + if limiter == nil { + // if Concurrency is not set or invalid, use the default concurrency + if opts.Concurrency <= 0 { + opts.Concurrency = defaultConcurrency + } + limiter = semaphore.NewWeighted(int64(opts.Concurrency)) + } + if tracker == nil { + // track content status + tracker = status.NewTracker() + } + // if FindSuccessors is not provided, use the default one + if opts.FindSuccessors == nil { + opts.FindSuccessors = content.Successors + } + + // traverse the graph + var fn syncutil.GoFunc[ocispec.Descriptor] + fn = func(ctx context.Context, region *syncutil.LimitedRegion, desc ocispec.Descriptor) (err error) { + // skip the descriptor if other go routine is working on it + done, committed := tracker.TryCommit(desc) + if !committed { + return nil + } + defer func() { + if err == nil { + // mark the content as done on success + close(done) + } + }() + + // skip if a rooted sub-DAG exists + exists, err := dst.Exists(ctx, desc) + if err != nil { + return err + } + if exists { + if opts.OnCopySkipped != nil { + if err := opts.OnCopySkipped(ctx, desc); err != nil { + return err + } + } + return nil + } + + // find successors while non-leaf nodes will be fetched and cached + successors, err := opts.FindSuccessors(ctx, proxy, desc) + if err != nil { + return err + } + successors = removeForeignLayers(successors) + + if len(successors) != 0 { + // for non-leaf nodes, process successors and wait for them to complete + region.End() + if err := syncutil.Go(ctx, limiter, fn, successors...); err != nil { + return err + } + for _, node := range successors { + done, committed := tracker.TryCommit(node) + if committed { + return fmt.Errorf("%s: %s: successor not committed", desc.Digest, node.Digest) + } + select { + case <-done: + case <-ctx.Done(): + return ctx.Err() + } + } + if err := region.Start(); err != nil { + return err + } + } + + exists, err = proxy.Cache.Exists(ctx, desc) + if err != nil { + return err + } + if exists { + return copyNode(ctx, proxy.Cache, dst, desc, opts) + } + return mountOrCopyNode(ctx, src, dst, desc, opts) + } + + return syncutil.Go(ctx, limiter, fn, root) +} + +// mountOrCopyNode tries to mount the node, if not falls back to copying. +func mountOrCopyNode(ctx context.Context, src content.ReadOnlyStorage, dst content.Storage, desc ocispec.Descriptor, opts CopyGraphOptions) error { + // Need MountFrom and it must be a blob + if opts.MountFrom == nil || descriptor.IsManifest(desc) { + return copyNode(ctx, src, dst, desc, opts) + } + + mounter, ok := dst.(registry.Mounter) + if !ok { + // mounting is not supported by the destination + return copyNode(ctx, src, dst, desc, opts) + } + + sourceRepositories, err := opts.MountFrom(ctx, desc) + if err != nil { + // Technically this error is not fatal, we can still attempt to copy the node + // But for consistency with the other callbacks we bail out. + return err + } + + if len(sourceRepositories) == 0 { + return copyNode(ctx, src, dst, desc, opts) + } + + skipSource := errors.New("skip source") + for i, sourceRepository := range sourceRepositories { + // try mounting this source repository + var mountFailed bool + getContent := func() (io.ReadCloser, error) { + // the invocation of getContent indicates that mounting has failed + mountFailed = true + + if i < len(sourceRepositories)-1 { + // If this is not the last one, skip this source and try next one + // We want to return an error that we will test for from mounter.Mount() + return nil, skipSource + } + // this is the last iteration so we need to actually get the content and do the copy + // but first call the PreCopy function + if opts.PreCopy != nil { + if err := opts.PreCopy(ctx, desc); err != nil { + return nil, err + } + } + return src.Fetch(ctx, desc) + } + + // Mount or copy + if err := mounter.Mount(ctx, desc, sourceRepository, getContent); err != nil && !errors.Is(err, skipSource) { + return err + } + + if !mountFailed { + // mounted, success + if opts.OnMounted != nil { + if err := opts.OnMounted(ctx, desc); err != nil { + return err + } + } + return nil + } + } + + // we copied it + if opts.PostCopy != nil { + if err := opts.PostCopy(ctx, desc); err != nil { + return err + } + } + + return nil +} + +// doCopyNode copies a single content from the source CAS to the destination CAS. +func doCopyNode(ctx context.Context, src content.ReadOnlyStorage, dst content.Storage, desc ocispec.Descriptor) error { + rc, err := src.Fetch(ctx, desc) + if err != nil { + return err + } + defer rc.Close() + err = dst.Push(ctx, desc, rc) + if err != nil && !errors.Is(err, errdef.ErrAlreadyExists) { + return err + } + return nil +} + +// copyNode copies a single content from the source CAS to the destination CAS, +// and apply the given options. +func copyNode(ctx context.Context, src content.ReadOnlyStorage, dst content.Storage, desc ocispec.Descriptor, opts CopyGraphOptions) error { + if opts.PreCopy != nil { + if err := opts.PreCopy(ctx, desc); err != nil { + if err == SkipNode { + return nil + } + return err + } + } + + if err := doCopyNode(ctx, src, dst, desc); err != nil { + return err + } + + if opts.PostCopy != nil { + return opts.PostCopy(ctx, desc) + } + return nil +} + +// copyCachedNodeWithReference copies a single content with a reference from the +// source cache to the destination ReferencePusher. +func copyCachedNodeWithReference(ctx context.Context, src *cas.Proxy, dst registry.ReferencePusher, desc ocispec.Descriptor, dstRef string) error { + rc, err := src.FetchCached(ctx, desc) + if err != nil { + return err + } + defer rc.Close() + + err = dst.PushReference(ctx, desc, rc, dstRef) + if err != nil && !errors.Is(err, errdef.ErrAlreadyExists) { + return err + } + return nil +} + +// resolveRoot resolves the source reference to the root node. +func resolveRoot(ctx context.Context, src ReadOnlyTarget, srcRef string, proxy *cas.Proxy) (ocispec.Descriptor, error) { + refFetcher, ok := src.(registry.ReferenceFetcher) + if !ok { + return src.Resolve(ctx, srcRef) + } + + // optimize performance for ReferenceFetcher targets + refProxy := ®istryutil.Proxy{ + ReferenceFetcher: refFetcher, + Proxy: proxy, + } + root, rc, err := refProxy.FetchReference(ctx, srcRef) + if err != nil { + return ocispec.Descriptor{}, err + } + defer rc.Close() + // cache root if it is a non-leaf node + fetcher := content.FetcherFunc(func(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) { + if content.Equal(target, root) { + return rc, nil + } + return nil, errors.New("fetching only root node expected") + }) + if _, err = content.Successors(ctx, fetcher, root); err != nil { + return ocispec.Descriptor{}, err + } + + // TODO: optimize special case where root is a leaf node (i.e. a blob) + // and dst is a ReferencePusher. + return root, nil +} + +// prepareCopy prepares the hooks for copy. +func prepareCopy(ctx context.Context, dst Target, dstRef string, proxy *cas.Proxy, root ocispec.Descriptor, opts *CopyOptions) error { + if refPusher, ok := dst.(registry.ReferencePusher); ok { + // optimize performance for ReferencePusher targets + preCopy := opts.PreCopy + opts.PreCopy = func(ctx context.Context, desc ocispec.Descriptor) error { + if preCopy != nil { + if err := preCopy(ctx, desc); err != nil { + return err + } + } + if !content.Equal(desc, root) { + // for non-root node, do nothing + return nil + } + + // for root node, prepare optimized copy + if err := copyCachedNodeWithReference(ctx, proxy, refPusher, desc, dstRef); err != nil { + return err + } + if opts.PostCopy != nil { + if err := opts.PostCopy(ctx, desc); err != nil { + return err + } + } + // skip the regular copy workflow + return SkipNode + } + } else { + postCopy := opts.PostCopy + opts.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error { + if content.Equal(desc, root) { + // for root node, tag it after copying it + if err := dst.Tag(ctx, root, dstRef); err != nil { + return err + } + } + if postCopy != nil { + return postCopy(ctx, desc) + } + return nil + } + } + + onCopySkipped := opts.OnCopySkipped + opts.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error { + if !content.Equal(desc, root) { + if onCopySkipped != nil { + return onCopySkipped(ctx, desc) + } + return nil + } + + // enforce tagging when the skipped node is root + if refPusher, ok := dst.(registry.ReferencePusher); ok { + // NOTE: refPusher tags the node by copying it with the reference, + // so onCopySkipped shouldn't be invoked in this case + return copyCachedNodeWithReference(ctx, proxy, refPusher, desc, dstRef) + } + + // invoke onCopySkipped before tagging + if onCopySkipped != nil { + if err := onCopySkipped(ctx, desc); err != nil { + return err + } + } + return dst.Tag(ctx, root, dstRef) + } + + return nil +} + +// removeForeignLayers in-place removes all foreign layers in the given slice. +func removeForeignLayers(descs []ocispec.Descriptor) []ocispec.Descriptor { + var j int + for i, desc := range descs { + if !descriptor.IsForeignLayer(desc) { + if i != j { + descs[j] = desc + } + j++ + } + } + return descs[:j] +} diff --git a/vendor/oras.land/oras-go/v2/errdef/errors.go b/vendor/oras.land/oras-go/v2/errdef/errors.go new file mode 100644 index 00000000..7adb44b1 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/errdef/errors.go @@ -0,0 +1,31 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package errdef + +import "errors" + +// Common errors used in ORAS +var ( + ErrAlreadyExists = errors.New("already exists") + ErrInvalidDigest = errors.New("invalid digest") + ErrInvalidReference = errors.New("invalid reference") + ErrInvalidMediaType = errors.New("invalid media type") + ErrMissingReference = errors.New("missing reference") + ErrNotFound = errors.New("not found") + ErrSizeExceedsLimit = errors.New("size exceeds limit") + ErrUnsupported = errors.New("unsupported") + ErrUnsupportedVersion = errors.New("unsupported version") +) diff --git a/vendor/oras.land/oras-go/v2/extendedcopy.go b/vendor/oras.land/oras-go/v2/extendedcopy.go new file mode 100644 index 00000000..49b6264e --- /dev/null +++ b/vendor/oras.land/oras-go/v2/extendedcopy.go @@ -0,0 +1,389 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package oras + +import ( + "context" + "encoding/json" + "errors" + "regexp" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "golang.org/x/sync/semaphore" + "oras.land/oras-go/v2/content" + "oras.land/oras-go/v2/internal/cas" + "oras.land/oras-go/v2/internal/container/set" + "oras.land/oras-go/v2/internal/copyutil" + "oras.land/oras-go/v2/internal/descriptor" + "oras.land/oras-go/v2/internal/docker" + "oras.land/oras-go/v2/internal/spec" + "oras.land/oras-go/v2/internal/status" + "oras.land/oras-go/v2/internal/syncutil" + "oras.land/oras-go/v2/registry" +) + +// DefaultExtendedCopyOptions provides the default ExtendedCopyOptions. +var DefaultExtendedCopyOptions ExtendedCopyOptions = ExtendedCopyOptions{ + ExtendedCopyGraphOptions: DefaultExtendedCopyGraphOptions, +} + +// ExtendedCopyOptions contains parameters for [oras.ExtendedCopy]. +type ExtendedCopyOptions struct { + ExtendedCopyGraphOptions +} + +// DefaultExtendedCopyGraphOptions provides the default ExtendedCopyGraphOptions. +var DefaultExtendedCopyGraphOptions ExtendedCopyGraphOptions = ExtendedCopyGraphOptions{ + CopyGraphOptions: DefaultCopyGraphOptions, +} + +// ExtendedCopyGraphOptions contains parameters for [oras.ExtendedCopyGraph]. +type ExtendedCopyGraphOptions struct { + CopyGraphOptions + // Depth limits the maximum depth of the directed acyclic graph (DAG) that + // will be extended-copied. + // If Depth is no specified, or the specified value is less than or + // equal to 0, the depth limit will be considered as infinity. + Depth int + // FindPredecessors finds the predecessors of the current node. + // If FindPredecessors is nil, src.Predecessors will be adapted and used. + FindPredecessors func(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) +} + +// ExtendedCopy copies the directed acyclic graph (DAG) that are reachable from +// the given tagged node from the source GraphTarget to the destination Target. +// The destination reference will be the same as the source reference if the +// destination reference is left blank. +// +// Returns the descriptor of the tagged node on successful copy. +func ExtendedCopy(ctx context.Context, src ReadOnlyGraphTarget, srcRef string, dst Target, dstRef string, opts ExtendedCopyOptions) (ocispec.Descriptor, error) { + if src == nil { + return ocispec.Descriptor{}, errors.New("nil source graph target") + } + if dst == nil { + return ocispec.Descriptor{}, errors.New("nil destination target") + } + if dstRef == "" { + dstRef = srcRef + } + + node, err := src.Resolve(ctx, srcRef) + if err != nil { + return ocispec.Descriptor{}, err + } + + if err := ExtendedCopyGraph(ctx, src, dst, node, opts.ExtendedCopyGraphOptions); err != nil { + return ocispec.Descriptor{}, err + } + + if err := dst.Tag(ctx, node, dstRef); err != nil { + return ocispec.Descriptor{}, err + } + + return node, nil +} + +// ExtendedCopyGraph copies the directed acyclic graph (DAG) that are reachable +// from the given node from the source GraphStorage to the destination Storage. +func ExtendedCopyGraph(ctx context.Context, src content.ReadOnlyGraphStorage, dst content.Storage, node ocispec.Descriptor, opts ExtendedCopyGraphOptions) error { + roots, err := findRoots(ctx, src, node, opts) + if err != nil { + return err + } + + // if Concurrency is not set or invalid, use the default concurrency + if opts.Concurrency <= 0 { + opts.Concurrency = defaultConcurrency + } + limiter := semaphore.NewWeighted(int64(opts.Concurrency)) + // use caching proxy on non-leaf nodes + if opts.MaxMetadataBytes <= 0 { + opts.MaxMetadataBytes = defaultCopyMaxMetadataBytes + } + proxy := cas.NewProxyWithLimit(src, cas.NewMemory(), opts.MaxMetadataBytes) + // track content status + tracker := status.NewTracker() + + // copy the sub-DAGs rooted by the root nodes + return syncutil.Go(ctx, limiter, func(ctx context.Context, region *syncutil.LimitedRegion, root ocispec.Descriptor) error { + // As a root can be a predecessor of other roots, release the limit here + // for dispatching, to avoid dead locks where predecessor roots are + // handled first and are waiting for its successors to complete. + region.End() + if err := copyGraph(ctx, src, dst, root, proxy, limiter, tracker, opts.CopyGraphOptions); err != nil { + return err + } + return region.Start() + }, roots...) +} + +// findRoots finds the root nodes reachable from the given node through a +// depth-first search. +func findRoots(ctx context.Context, storage content.ReadOnlyGraphStorage, node ocispec.Descriptor, opts ExtendedCopyGraphOptions) ([]ocispec.Descriptor, error) { + visited := set.New[descriptor.Descriptor]() + rootMap := make(map[descriptor.Descriptor]ocispec.Descriptor) + addRoot := func(key descriptor.Descriptor, val ocispec.Descriptor) { + if _, exists := rootMap[key]; !exists { + rootMap[key] = val + } + } + + // if FindPredecessors is not provided, use the default one + if opts.FindPredecessors == nil { + opts.FindPredecessors = func(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { + return src.Predecessors(ctx, desc) + } + } + + var stack copyutil.Stack + // push the initial node to the stack, set the depth to 0 + stack.Push(copyutil.NodeInfo{Node: node, Depth: 0}) + for { + current, ok := stack.Pop() + if !ok { + // empty stack + break + } + currentNode := current.Node + currentKey := descriptor.FromOCI(currentNode) + + if visited.Contains(currentKey) { + // skip the current node if it has been visited + continue + } + visited.Add(currentKey) + + // stop finding predecessors if the target depth is reached + if opts.Depth > 0 && current.Depth == opts.Depth { + addRoot(currentKey, currentNode) + continue + } + + predecessors, err := opts.FindPredecessors(ctx, storage, currentNode) + if err != nil { + return nil, err + } + + // The current node has no predecessor node, + // which means it is a root node of a sub-DAG. + if len(predecessors) == 0 { + addRoot(currentKey, currentNode) + continue + } + + // The current node has predecessor nodes, which means it is NOT a root node. + // Push the predecessor nodes to the stack and keep finding from there. + for _, predecessor := range predecessors { + predecessorKey := descriptor.FromOCI(predecessor) + if !visited.Contains(predecessorKey) { + // push the predecessor node with increased depth + stack.Push(copyutil.NodeInfo{Node: predecessor, Depth: current.Depth + 1}) + } + } + } + + roots := make([]ocispec.Descriptor, 0, len(rootMap)) + for _, root := range rootMap { + roots = append(roots, root) + } + return roots, nil +} + +// FilterAnnotation configures opts.FindPredecessors to filter the predecessors +// whose annotation matches a given regex pattern. +// +// A predecessor is kept if key is in its annotations and the annotation value +// matches regex. +// If regex is nil, predecessors whose annotations contain key will be kept, +// no matter of the annotation value. +// +// For performance consideration, when using both FilterArtifactType and +// FilterAnnotation, it's recommended to call FilterArtifactType first. +func (opts *ExtendedCopyGraphOptions) FilterAnnotation(key string, regex *regexp.Regexp) { + keep := func(desc ocispec.Descriptor) bool { + value, ok := desc.Annotations[key] + return ok && (regex == nil || regex.MatchString(value)) + } + + fp := opts.FindPredecessors + opts.FindPredecessors = func(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { + var predecessors []ocispec.Descriptor + var err error + if fp == nil { + if rf, ok := src.(registry.ReferrerLister); ok { + // if src is a ReferrerLister, use Referrers() for possible memory saving + if err := rf.Referrers(ctx, desc, "", func(referrers []ocispec.Descriptor) error { + // for each page of the results, filter the referrers + for _, r := range referrers { + if keep(r) { + predecessors = append(predecessors, r) + } + } + return nil + }); err != nil { + return nil, err + } + return predecessors, nil + } + predecessors, err = src.Predecessors(ctx, desc) + } else { + predecessors, err = fp(ctx, src, desc) + } + if err != nil { + return nil, err + } + + // Predecessor descriptors that are not from Referrers API are not + // guaranteed to include the annotations of the corresponding manifests. + var kept []ocispec.Descriptor + for _, p := range predecessors { + if p.Annotations == nil { + // If the annotations are not present in the descriptors, + // fetch it from the manifest content. + switch p.MediaType { + case docker.MediaTypeManifest, ocispec.MediaTypeImageManifest, + docker.MediaTypeManifestList, ocispec.MediaTypeImageIndex, + spec.MediaTypeArtifactManifest: + annotations, err := fetchAnnotations(ctx, src, p) + if err != nil { + return nil, err + } + p.Annotations = annotations + } + } + if keep(p) { + kept = append(kept, p) + } + } + return kept, nil + } +} + +// fetchAnnotations fetches the annotations of the manifest described by desc. +func fetchAnnotations(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) (map[string]string, error) { + rc, err := src.Fetch(ctx, desc) + if err != nil { + return nil, err + } + defer rc.Close() + + var manifest struct { + Annotations map[string]string `json:"annotations"` + } + if err := json.NewDecoder(rc).Decode(&manifest); err != nil { + return nil, err + } + if manifest.Annotations == nil { + // to differentiate with nil + return make(map[string]string), nil + } + return manifest.Annotations, nil +} + +// FilterArtifactType configures opts.FindPredecessors to filter the +// predecessors whose artifact type matches a given regex pattern. +// +// A predecessor is kept if its artifact type matches regex. +// If regex is nil, all predecessors will be kept. +// +// For performance consideration, when using both FilterArtifactType and +// FilterAnnotation, it's recommended to call FilterArtifactType first. +func (opts *ExtendedCopyGraphOptions) FilterArtifactType(regex *regexp.Regexp) { + if regex == nil { + return + } + keep := func(desc ocispec.Descriptor) bool { + return regex.MatchString(desc.ArtifactType) + } + + fp := opts.FindPredecessors + opts.FindPredecessors = func(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { + var predecessors []ocispec.Descriptor + var err error + if fp == nil { + if rf, ok := src.(registry.ReferrerLister); ok { + // if src is a ReferrerLister, use Referrers() for possible memory saving + if err := rf.Referrers(ctx, desc, "", func(referrers []ocispec.Descriptor) error { + // for each page of the results, filter the referrers + for _, r := range referrers { + if keep(r) { + predecessors = append(predecessors, r) + } + } + return nil + }); err != nil { + return nil, err + } + return predecessors, nil + } + predecessors, err = src.Predecessors(ctx, desc) + } else { + predecessors, err = fp(ctx, src, desc) + } + if err != nil { + return nil, err + } + + // predecessor descriptors that are not from Referrers API are not + // guaranteed to include the artifact type of the corresponding + // manifests. + var kept []ocispec.Descriptor + for _, p := range predecessors { + if p.ArtifactType == "" { + // if the artifact type is not present in the descriptors, + // fetch it from the manifest content. + switch p.MediaType { + case spec.MediaTypeArtifactManifest, ocispec.MediaTypeImageManifest: + artifactType, err := fetchArtifactType(ctx, src, p) + if err != nil { + return nil, err + } + p.ArtifactType = artifactType + } + } + if keep(p) { + kept = append(kept, p) + } + } + return kept, nil + } +} + +// fetchArtifactType fetches the artifact type of the manifest described by desc. +func fetchArtifactType(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) (string, error) { + rc, err := src.Fetch(ctx, desc) + if err != nil { + return "", err + } + defer rc.Close() + + switch desc.MediaType { + case spec.MediaTypeArtifactManifest: + var manifest spec.Artifact + if err := json.NewDecoder(rc).Decode(&manifest); err != nil { + return "", err + } + return manifest.ArtifactType, nil + case ocispec.MediaTypeImageManifest: + var manifest ocispec.Manifest + if err := json.NewDecoder(rc).Decode(&manifest); err != nil { + return "", err + } + return manifest.Config.MediaType, nil + default: + return "", nil + } +} diff --git a/vendor/oras.land/oras-go/v2/internal/cas/memory.go b/vendor/oras.land/oras-go/v2/internal/cas/memory.go new file mode 100644 index 00000000..7e358e13 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/internal/cas/memory.go @@ -0,0 +1,88 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cas + +import ( + "bytes" + "context" + "fmt" + "io" + "sync" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + contentpkg "oras.land/oras-go/v2/content" + "oras.land/oras-go/v2/errdef" + "oras.land/oras-go/v2/internal/descriptor" +) + +// Memory is a memory based CAS. +type Memory struct { + content sync.Map // map[descriptor.Descriptor][]byte +} + +// NewMemory creates a new Memory CAS. +func NewMemory() *Memory { + return &Memory{} +} + +// Fetch fetches the content identified by the descriptor. +func (m *Memory) Fetch(_ context.Context, target ocispec.Descriptor) (io.ReadCloser, error) { + key := descriptor.FromOCI(target) + content, exists := m.content.Load(key) + if !exists { + return nil, fmt.Errorf("%s: %s: %w", key.Digest, key.MediaType, errdef.ErrNotFound) + } + return io.NopCloser(bytes.NewReader(content.([]byte))), nil +} + +// Push pushes the content, matching the expected descriptor. +func (m *Memory) Push(_ context.Context, expected ocispec.Descriptor, content io.Reader) error { + key := descriptor.FromOCI(expected) + + // check if the content exists in advance to avoid reading from the content. + if _, exists := m.content.Load(key); exists { + return fmt.Errorf("%s: %s: %w", key.Digest, key.MediaType, errdef.ErrAlreadyExists) + } + + // read and try to store the content. + value, err := contentpkg.ReadAll(content, expected) + if err != nil { + return err + } + if _, exists := m.content.LoadOrStore(key, value); exists { + return fmt.Errorf("%s: %s: %w", key.Digest, key.MediaType, errdef.ErrAlreadyExists) + } + return nil +} + +// Exists returns true if the described content exists. +func (m *Memory) Exists(_ context.Context, target ocispec.Descriptor) (bool, error) { + key := descriptor.FromOCI(target) + _, exists := m.content.Load(key) + return exists, nil +} + +// Map dumps the memory into a built-in map structure. +// Like other operations, calling Map() is go-routine safe. However, it does not +// necessarily correspond to any consistent snapshot of the storage contents. +func (m *Memory) Map() map[descriptor.Descriptor][]byte { + res := make(map[descriptor.Descriptor][]byte) + m.content.Range(func(key, value interface{}) bool { + res[key.(descriptor.Descriptor)] = value.([]byte) + return true + }) + return res +} diff --git a/vendor/oras.land/oras-go/v2/internal/cas/proxy.go b/vendor/oras.land/oras-go/v2/internal/cas/proxy.go new file mode 100644 index 00000000..ada5f94e --- /dev/null +++ b/vendor/oras.land/oras-go/v2/internal/cas/proxy.go @@ -0,0 +1,125 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cas + +import ( + "context" + "io" + "sync" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/content" + "oras.land/oras-go/v2/internal/ioutil" +) + +// Proxy is a caching proxy for the storage. +// The first fetch call of a described content will read from the remote and +// cache the fetched content. +// The subsequent fetch call will read from the local cache. +type Proxy struct { + content.ReadOnlyStorage + Cache content.Storage + StopCaching bool +} + +// NewProxy creates a proxy for the `base` storage, using the `cache` storage as +// the cache. +func NewProxy(base content.ReadOnlyStorage, cache content.Storage) *Proxy { + return &Proxy{ + ReadOnlyStorage: base, + Cache: cache, + } +} + +// NewProxyWithLimit creates a proxy for the `base` storage, using the `cache` +// storage with a push size limit as the cache. +func NewProxyWithLimit(base content.ReadOnlyStorage, cache content.Storage, pushLimit int64) *Proxy { + limitedCache := content.LimitStorage(cache, pushLimit) + return &Proxy{ + ReadOnlyStorage: base, + Cache: limitedCache, + } +} + +// Fetch fetches the content identified by the descriptor. +func (p *Proxy) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) { + if p.StopCaching { + return p.FetchCached(ctx, target) + } + + rc, err := p.Cache.Fetch(ctx, target) + if err == nil { + return rc, nil + } + + rc, err = p.ReadOnlyStorage.Fetch(ctx, target) + if err != nil { + return nil, err + } + pr, pw := io.Pipe() + var wg sync.WaitGroup + wg.Add(1) + var pushErr error + go func() { + defer wg.Done() + pushErr = p.Cache.Push(ctx, target, pr) + if pushErr != nil { + pr.CloseWithError(pushErr) + } + }() + closer := ioutil.CloserFunc(func() error { + rcErr := rc.Close() + if err := pw.Close(); err != nil { + return err + } + wg.Wait() + if pushErr != nil { + return pushErr + } + return rcErr + }) + + return struct { + io.Reader + io.Closer + }{ + Reader: io.TeeReader(rc, pw), + Closer: closer, + }, nil +} + +// FetchCached fetches the content identified by the descriptor. +// If the content is not cached, it will be fetched from the remote without +// caching. +func (p *Proxy) FetchCached(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) { + exists, err := p.Cache.Exists(ctx, target) + if err != nil { + return nil, err + } + if exists { + return p.Cache.Fetch(ctx, target) + } + return p.ReadOnlyStorage.Fetch(ctx, target) +} + +// Exists returns true if the described content exists. +func (p *Proxy) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) { + exists, err := p.Cache.Exists(ctx, target) + if err == nil && exists { + return true, nil + } + return p.ReadOnlyStorage.Exists(ctx, target) +} diff --git a/vendor/oras.land/oras-go/v2/internal/container/set/set.go b/vendor/oras.land/oras-go/v2/internal/container/set/set.go new file mode 100644 index 00000000..07c96d47 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/internal/container/set/set.go @@ -0,0 +1,40 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package set + +// Set represents a set data structure. +type Set[T comparable] map[T]struct{} + +// New returns an initialized set. +func New[T comparable]() Set[T] { + return make(Set[T]) +} + +// Add adds item into the set s. +func (s Set[T]) Add(item T) { + s[item] = struct{}{} +} + +// Contains returns true if the set s contains item. +func (s Set[T]) Contains(item T) bool { + _, ok := s[item] + return ok +} + +// Delete deletes an item from the set. +func (s Set[T]) Delete(item T) { + delete(s, item) +} diff --git a/vendor/oras.land/oras-go/v2/internal/copyutil/stack.go b/vendor/oras.land/oras-go/v2/internal/copyutil/stack.go new file mode 100644 index 00000000..69412b00 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/internal/copyutil/stack.go @@ -0,0 +1,55 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package copyutil + +import ( + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +// NodeInfo represents information of a node that is being visited in +// ExtendedCopy. +type NodeInfo struct { + // Node represents a node in the graph. + Node ocispec.Descriptor + // Depth represents the depth of the node in the graph. + Depth int +} + +// Stack represents a stack data structure that is used in ExtendedCopy for +// storing node information. +type Stack []NodeInfo + +// IsEmpty returns true if the stack is empty, otherwise returns false. +func (s *Stack) IsEmpty() bool { + return len(*s) == 0 +} + +// Push pushes an item to the stack. +func (s *Stack) Push(i NodeInfo) { + *s = append(*s, i) +} + +// Pop pops the top item out of the stack. +func (s *Stack) Pop() (NodeInfo, bool) { + if s.IsEmpty() { + return NodeInfo{}, false + } + + last := len(*s) - 1 + top := (*s)[last] + *s = (*s)[:last] + return top, true +} diff --git a/vendor/oras.land/oras-go/v2/internal/descriptor/descriptor.go b/vendor/oras.land/oras-go/v2/internal/descriptor/descriptor.go new file mode 100644 index 00000000..b9b339c0 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/internal/descriptor/descriptor.go @@ -0,0 +1,89 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package descriptor + +import ( + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/internal/docker" + "oras.land/oras-go/v2/internal/spec" +) + +// DefaultMediaType is the media type used when no media type is specified. +const DefaultMediaType string = "application/octet-stream" + +// Descriptor contains the minimun information to describe the disposition of +// targeted content. +// Since it only has strings and integers, Descriptor is a comparable struct. +type Descriptor struct { + // MediaType is the media type of the object this schema refers to. + MediaType string `json:"mediaType,omitempty"` + + // Digest is the digest of the targeted content. + Digest digest.Digest `json:"digest"` + + // Size specifies the size in bytes of the blob. + Size int64 `json:"size"` +} + +// Empty is an empty descriptor +var Empty Descriptor + +// FromOCI shrinks the OCI descriptor to the minimum. +func FromOCI(desc ocispec.Descriptor) Descriptor { + return Descriptor{ + MediaType: desc.MediaType, + Digest: desc.Digest, + Size: desc.Size, + } +} + +// IsForeignLayer checks if a descriptor describes a foreign layer. +func IsForeignLayer(desc ocispec.Descriptor) bool { + switch desc.MediaType { + case ocispec.MediaTypeImageLayerNonDistributable, + ocispec.MediaTypeImageLayerNonDistributableGzip, + ocispec.MediaTypeImageLayerNonDistributableZstd, + docker.MediaTypeForeignLayer: + return true + default: + return false + } +} + +// IsManifest checks if a descriptor describes a manifest. +func IsManifest(desc ocispec.Descriptor) bool { + switch desc.MediaType { + case docker.MediaTypeManifest, + docker.MediaTypeManifestList, + ocispec.MediaTypeImageManifest, + ocispec.MediaTypeImageIndex, + spec.MediaTypeArtifactManifest: + return true + default: + return false + } +} + +// Plain returns a plain descriptor that contains only MediaType, Digest and +// Size. +func Plain(desc ocispec.Descriptor) ocispec.Descriptor { + return ocispec.Descriptor{ + MediaType: desc.MediaType, + Digest: desc.Digest, + Size: desc.Size, + } +} diff --git a/vendor/oras.land/oras-go/v2/internal/docker/mediatype.go b/vendor/oras.land/oras-go/v2/internal/docker/mediatype.go new file mode 100644 index 00000000..76a4ba9e --- /dev/null +++ b/vendor/oras.land/oras-go/v2/internal/docker/mediatype.go @@ -0,0 +1,24 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package docker + +// docker media types +const ( + MediaTypeConfig = "application/vnd.docker.container.image.v1+json" + MediaTypeManifestList = "application/vnd.docker.distribution.manifest.list.v2+json" + MediaTypeManifest = "application/vnd.docker.distribution.manifest.v2+json" + MediaTypeForeignLayer = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip" +) diff --git a/vendor/oras.land/oras-go/v2/internal/graph/memory.go b/vendor/oras.land/oras-go/v2/internal/graph/memory.go new file mode 100644 index 00000000..016e5f96 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/internal/graph/memory.go @@ -0,0 +1,201 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package graph + +import ( + "context" + "errors" + "sync" + + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/content" + "oras.land/oras-go/v2/errdef" + "oras.land/oras-go/v2/internal/container/set" + "oras.land/oras-go/v2/internal/descriptor" + "oras.land/oras-go/v2/internal/status" + "oras.land/oras-go/v2/internal/syncutil" +) + +// Memory is a memory based PredecessorFinder. +type Memory struct { + // nodes has the following properties and behaviors: + // 1. a node exists in Memory.nodes if and only if it exists in the memory + // 2. Memory.nodes saves the ocispec.Descriptor map keys, which are used by + // the other fields. + nodes map[descriptor.Descriptor]ocispec.Descriptor + + // predecessors has the following properties and behaviors: + // 1. a node exists in Memory.predecessors if it has at least one predecessor + // in the memory, regardless of whether or not the node itself exists in + // the memory. + // 2. a node does not exist in Memory.predecessors, if it doesn't have any predecessors + // in the memory. + predecessors map[descriptor.Descriptor]set.Set[descriptor.Descriptor] + + // successors has the following properties and behaviors: + // 1. a node exists in Memory.successors if and only if it exists in the memory. + // 2. a node's entry in Memory.successors is always consistent with the actual + // content of the node, regardless of whether or not each successor exists + // in the memory. + successors map[descriptor.Descriptor]set.Set[descriptor.Descriptor] + + lock sync.RWMutex +} + +// NewMemory creates a new memory PredecessorFinder. +func NewMemory() *Memory { + return &Memory{ + nodes: make(map[descriptor.Descriptor]ocispec.Descriptor), + predecessors: make(map[descriptor.Descriptor]set.Set[descriptor.Descriptor]), + successors: make(map[descriptor.Descriptor]set.Set[descriptor.Descriptor]), + } +} + +// Index indexes predecessors for each direct successor of the given node. +func (m *Memory) Index(ctx context.Context, fetcher content.Fetcher, node ocispec.Descriptor) error { + _, err := m.index(ctx, fetcher, node) + return err +} + +// Index indexes predecessors for all the successors of the given node. +func (m *Memory) IndexAll(ctx context.Context, fetcher content.Fetcher, node ocispec.Descriptor) error { + // track content status + tracker := status.NewTracker() + var fn syncutil.GoFunc[ocispec.Descriptor] + fn = func(ctx context.Context, region *syncutil.LimitedRegion, desc ocispec.Descriptor) error { + // skip the node if other go routine is working on it + _, committed := tracker.TryCommit(desc) + if !committed { + return nil + } + successors, err := m.index(ctx, fetcher, desc) + if err != nil { + if errors.Is(err, errdef.ErrNotFound) { + // skip the node if it does not exist + return nil + } + return err + } + if len(successors) > 0 { + // traverse and index successors + return syncutil.Go(ctx, nil, fn, successors...) + } + return nil + } + return syncutil.Go(ctx, nil, fn, node) +} + +// Predecessors returns the nodes directly pointing to the current node. +// Predecessors returns nil without error if the node does not exists in the +// store. Like other operations, calling Predecessors() is go-routine safe. +// However, it does not necessarily correspond to any consistent snapshot of +// the stored contents. +func (m *Memory) Predecessors(_ context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) { + m.lock.RLock() + defer m.lock.RUnlock() + + key := descriptor.FromOCI(node) + set, exists := m.predecessors[key] + if !exists { + return nil, nil + } + var res []ocispec.Descriptor + for k := range set { + res = append(res, m.nodes[k]) + } + return res, nil +} + +// Remove removes the node from its predecessors and successors, and returns the +// dangling root nodes caused by the deletion. +func (m *Memory) Remove(node ocispec.Descriptor) []ocispec.Descriptor { + m.lock.Lock() + defer m.lock.Unlock() + + nodeKey := descriptor.FromOCI(node) + var danglings []ocispec.Descriptor + // remove the node from its successors' predecessor list + for successorKey := range m.successors[nodeKey] { + predecessorEntry := m.predecessors[successorKey] + predecessorEntry.Delete(nodeKey) + + // if none of the predecessors of the node still exists, we remove the + // predecessors entry and return it as a dangling node. Otherwise, we do + // not remove the entry. + if len(predecessorEntry) == 0 { + delete(m.predecessors, successorKey) + if _, exists := m.nodes[successorKey]; exists { + danglings = append(danglings, m.nodes[successorKey]) + } + } + } + delete(m.successors, nodeKey) + delete(m.nodes, nodeKey) + return danglings +} + +// DigestSet returns the set of node digest in memory. +func (m *Memory) DigestSet() set.Set[digest.Digest] { + m.lock.RLock() + defer m.lock.RUnlock() + + s := set.New[digest.Digest]() + for desc := range m.nodes { + s.Add(desc.Digest) + } + return s +} + +// index indexes predecessors for each direct successor of the given node. +func (m *Memory) index(ctx context.Context, fetcher content.Fetcher, node ocispec.Descriptor) ([]ocispec.Descriptor, error) { + successors, err := content.Successors(ctx, fetcher, node) + if err != nil { + return nil, err + } + m.lock.Lock() + defer m.lock.Unlock() + + // index the node + nodeKey := descriptor.FromOCI(node) + m.nodes[nodeKey] = node + + // for each successor, put it into the node's successors list, and + // put node into the succeesor's predecessors list + successorSet := set.New[descriptor.Descriptor]() + m.successors[nodeKey] = successorSet + for _, successor := range successors { + successorKey := descriptor.FromOCI(successor) + successorSet.Add(successorKey) + predecessorSet, exists := m.predecessors[successorKey] + if !exists { + predecessorSet = set.New[descriptor.Descriptor]() + m.predecessors[successorKey] = predecessorSet + } + predecessorSet.Add(nodeKey) + } + return successors, nil +} + +// Exists checks if the node exists in the graph +func (m *Memory) Exists(node ocispec.Descriptor) bool { + m.lock.RLock() + defer m.lock.RUnlock() + + nodeKey := descriptor.FromOCI(node) + _, exists := m.nodes[nodeKey] + return exists +} diff --git a/vendor/oras.land/oras-go/v2/internal/httputil/seek.go b/vendor/oras.land/oras-go/v2/internal/httputil/seek.go new file mode 100644 index 00000000..3fa14e2d --- /dev/null +++ b/vendor/oras.land/oras-go/v2/internal/httputil/seek.go @@ -0,0 +1,116 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package httputil + +import ( + "errors" + "fmt" + "io" + "net/http" +) + +// Client is an interface for a HTTP client. +// This interface is defined inside this package to prevent potential import +// loop. +type Client interface { + // Do sends an HTTP request and returns an HTTP response. + Do(*http.Request) (*http.Response, error) +} + +// readSeekCloser seeks http body by starting new connections. +type readSeekCloser struct { + client Client + req *http.Request + rc io.ReadCloser + size int64 + offset int64 + closed bool +} + +// NewReadSeekCloser returns a seeker to make the HTTP response seekable. +// Callers should ensure that the server supports Range request. +func NewReadSeekCloser(client Client, req *http.Request, respBody io.ReadCloser, size int64) io.ReadSeekCloser { + return &readSeekCloser{ + client: client, + req: req, + rc: respBody, + size: size, + } +} + +// Read reads the content body and counts offset. +func (rsc *readSeekCloser) Read(p []byte) (n int, err error) { + if rsc.closed { + return 0, errors.New("read: already closed") + } + n, err = rsc.rc.Read(p) + rsc.offset += int64(n) + return +} + +// Seek starts a new connection to the remote for reading if position changes. +func (rsc *readSeekCloser) Seek(offset int64, whence int) (int64, error) { + if rsc.closed { + return 0, errors.New("seek: already closed") + } + switch whence { + case io.SeekCurrent: + offset += rsc.offset + case io.SeekStart: + // no-op + case io.SeekEnd: + offset += rsc.size + default: + return 0, errors.New("seek: invalid whence") + } + if offset < 0 { + return 0, errors.New("seek: an attempt was made to move the pointer before the beginning of the content") + } + if offset == rsc.offset { + return offset, nil + } + if offset >= rsc.size { + rsc.rc.Close() + rsc.rc = http.NoBody + rsc.offset = offset + return offset, nil + } + + req := rsc.req.Clone(rsc.req.Context()) + req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", offset, rsc.size-1)) + resp, err := rsc.client.Do(req) + if err != nil { + return 0, fmt.Errorf("seek: %s %q: %w", req.Method, req.URL, err) + } + if resp.StatusCode != http.StatusPartialContent { + resp.Body.Close() + return 0, fmt.Errorf("seek: %s %q: unexpected status code %d", resp.Request.Method, resp.Request.URL, resp.StatusCode) + } + + rsc.rc.Close() + rsc.rc = resp.Body + rsc.offset = offset + return offset, nil +} + +// Close closes the content body. +func (rsc *readSeekCloser) Close() error { + if rsc.closed { + return nil + } + rsc.closed = true + return rsc.rc.Close() +} diff --git a/vendor/oras.land/oras-go/v2/internal/interfaces/registry.go b/vendor/oras.land/oras-go/v2/internal/interfaces/registry.go new file mode 100644 index 00000000..05600148 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/internal/interfaces/registry.go @@ -0,0 +1,24 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package interfaces + +import "oras.land/oras-go/v2/registry" + +// ReferenceParser provides reference parsing. +type ReferenceParser interface { + // ParseReference parses a reference to a fully qualified reference. + ParseReference(reference string) (registry.Reference, error) +} diff --git a/vendor/oras.land/oras-go/v2/internal/ioutil/io.go b/vendor/oras.land/oras-go/v2/internal/ioutil/io.go new file mode 100644 index 00000000..de41bda9 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/internal/ioutil/io.go @@ -0,0 +1,66 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package ioutil + +import ( + "fmt" + "io" + "reflect" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/content" +) + +// CloserFunc is the basic Close method defined in io.Closer. +type CloserFunc func() error + +// Close performs close operation by the CloserFunc. +func (fn CloserFunc) Close() error { + return fn() +} + +// CopyBuffer copies from src to dst through the provided buffer +// until either EOF is reached on src, or an error occurs. +// The copied content is verified against the size and the digest. +func CopyBuffer(dst io.Writer, src io.Reader, buf []byte, desc ocispec.Descriptor) error { + // verify while copying + vr := content.NewVerifyReader(src, desc) + if _, err := io.CopyBuffer(dst, vr, buf); err != nil { + return fmt.Errorf("copy failed: %w", err) + } + return vr.Verify() +} + +// Types returned by `io.NopCloser()`. +var ( + nopCloserType = reflect.TypeOf(io.NopCloser(nil)) + nopCloserWriterToType = reflect.TypeOf(io.NopCloser(struct { + io.Reader + io.WriterTo + }{})) +) + +// UnwrapNopCloser unwraps the reader wrapped by `io.NopCloser()`. +// Similar implementation can be found in the built-in package `net/http`. +// Reference: https://github.com/golang/go/blob/go1.22.1/src/net/http/transfer.go#L1090-L1105 +func UnwrapNopCloser(r io.Reader) io.Reader { + switch reflect.TypeOf(r) { + case nopCloserType, nopCloserWriterToType: + return reflect.ValueOf(r).Field(0).Interface().(io.Reader) + default: + return r + } +} diff --git a/vendor/oras.land/oras-go/v2/internal/manifestutil/parser.go b/vendor/oras.land/oras-go/v2/internal/manifestutil/parser.go new file mode 100644 index 00000000..89d556b8 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/internal/manifestutil/parser.go @@ -0,0 +1,84 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package manifestutil + +import ( + "context" + "encoding/json" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/content" + "oras.land/oras-go/v2/internal/docker" + "oras.land/oras-go/v2/internal/spec" +) + +// Config returns the config of desc, if present. +func Config(ctx context.Context, fetcher content.Fetcher, desc ocispec.Descriptor) (*ocispec.Descriptor, error) { + switch desc.MediaType { + case docker.MediaTypeManifest, ocispec.MediaTypeImageManifest: + content, err := content.FetchAll(ctx, fetcher, desc) + if err != nil { + return nil, err + } + // OCI manifest schema can be used to marshal docker manifest + var manifest ocispec.Manifest + if err := json.Unmarshal(content, &manifest); err != nil { + return nil, err + } + return &manifest.Config, nil + default: + return nil, nil + } +} + +// Manifest returns the manifests of desc, if present. +func Manifests(ctx context.Context, fetcher content.Fetcher, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { + switch desc.MediaType { + case docker.MediaTypeManifestList, ocispec.MediaTypeImageIndex: + content, err := content.FetchAll(ctx, fetcher, desc) + if err != nil { + return nil, err + } + // OCI manifest index schema can be used to marshal docker manifest list + var index ocispec.Index + if err := json.Unmarshal(content, &index); err != nil { + return nil, err + } + return index.Manifests, nil + default: + return nil, nil + } +} + +// Subject returns the subject of desc, if present. +func Subject(ctx context.Context, fetcher content.Fetcher, desc ocispec.Descriptor) (*ocispec.Descriptor, error) { + switch desc.MediaType { + case ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex, spec.MediaTypeArtifactManifest: + content, err := content.FetchAll(ctx, fetcher, desc) + if err != nil { + return nil, err + } + var manifest struct { + Subject *ocispec.Descriptor `json:"subject,omitempty"` + } + if err := json.Unmarshal(content, &manifest); err != nil { + return nil, err + } + return manifest.Subject, nil + default: + return nil, nil + } +} diff --git a/vendor/oras.land/oras-go/v2/internal/platform/platform.go b/vendor/oras.land/oras-go/v2/internal/platform/platform.go new file mode 100644 index 00000000..3aea3a1b --- /dev/null +++ b/vendor/oras.land/oras-go/v2/internal/platform/platform.go @@ -0,0 +1,145 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package platform + +import ( + "context" + "encoding/json" + "fmt" + "io" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/content" + "oras.land/oras-go/v2/errdef" + "oras.land/oras-go/v2/internal/docker" + "oras.land/oras-go/v2/internal/manifestutil" +) + +// Match checks whether the current platform matches the target platform. +// Match will return true if all of the following conditions are met. +// - Architecture and OS exactly match. +// - Variant and OSVersion exactly match if target platform provided. +// - OSFeatures of the target platform are the subsets of the OSFeatures +// array of the current platform. +// +// Note: Variant, OSVersion and OSFeatures are optional fields, will skip +// the comparison if the target platform does not provide specific value. +func Match(got *ocispec.Platform, want *ocispec.Platform) bool { + if got == nil && want == nil { + return true + } + + if got == nil || want == nil { + return false + } + + if got.Architecture != want.Architecture || got.OS != want.OS { + return false + } + + if want.OSVersion != "" && got.OSVersion != want.OSVersion { + return false + } + + if want.Variant != "" && got.Variant != want.Variant { + return false + } + + if len(want.OSFeatures) != 0 && !isSubset(want.OSFeatures, got.OSFeatures) { + return false + } + + return true +} + +// isSubset returns true if all items in slice A are present in slice B. +func isSubset(a, b []string) bool { + set := make(map[string]bool, len(b)) + for _, v := range b { + set[v] = true + } + for _, v := range a { + if _, ok := set[v]; !ok { + return false + } + } + + return true +} + +// SelectManifest implements platform filter and returns the descriptor of the +// first matched manifest if the root is a manifest list. If the root is a +// manifest, then return the root descriptor if platform matches. +func SelectManifest(ctx context.Context, src content.ReadOnlyStorage, root ocispec.Descriptor, p *ocispec.Platform) (ocispec.Descriptor, error) { + switch root.MediaType { + case docker.MediaTypeManifestList, ocispec.MediaTypeImageIndex: + manifests, err := manifestutil.Manifests(ctx, src, root) + if err != nil { + return ocispec.Descriptor{}, err + } + + // platform filter + for _, m := range manifests { + if Match(m.Platform, p) { + return m, nil + } + } + return ocispec.Descriptor{}, fmt.Errorf("%s: %w: no matching manifest was found in the manifest list", root.Digest, errdef.ErrNotFound) + case docker.MediaTypeManifest, ocispec.MediaTypeImageManifest: + // config will be non-nil for docker manifest and OCI image manifest + config, err := manifestutil.Config(ctx, src, root) + if err != nil { + return ocispec.Descriptor{}, err + } + + configMediaType := docker.MediaTypeConfig + if root.MediaType == ocispec.MediaTypeImageManifest { + configMediaType = ocispec.MediaTypeImageConfig + } + cfgPlatform, err := getPlatformFromConfig(ctx, src, *config, configMediaType) + if err != nil { + return ocispec.Descriptor{}, err + } + + if Match(cfgPlatform, p) { + return root, nil + } + return ocispec.Descriptor{}, fmt.Errorf("%s: %w: platform in manifest does not match target platform", root.Digest, errdef.ErrNotFound) + default: + return ocispec.Descriptor{}, fmt.Errorf("%s: %s: %w", root.Digest, root.MediaType, errdef.ErrUnsupported) + } +} + +// getPlatformFromConfig returns a platform object which is made up from the +// fields in config blob. +func getPlatformFromConfig(ctx context.Context, src content.ReadOnlyStorage, desc ocispec.Descriptor, targetConfigMediaType string) (*ocispec.Platform, error) { + if desc.MediaType != targetConfigMediaType { + return nil, fmt.Errorf("fail to recognize platform from unknown config %s: expect %s", desc.MediaType, targetConfigMediaType) + } + + rc, err := src.Fetch(ctx, desc) + if err != nil { + return nil, err + } + defer rc.Close() + + var platform ocispec.Platform + if err = json.NewDecoder(rc).Decode(&platform); err != nil && err != io.EOF { + return nil, err + } + + return &platform, nil +} diff --git a/vendor/oras.land/oras-go/v2/internal/registryutil/proxy.go b/vendor/oras.land/oras-go/v2/internal/registryutil/proxy.go new file mode 100644 index 00000000..c238713e --- /dev/null +++ b/vendor/oras.land/oras-go/v2/internal/registryutil/proxy.go @@ -0,0 +1,102 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package registryutil + +import ( + "context" + "io" + "sync" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/content" + "oras.land/oras-go/v2/internal/cas" + "oras.land/oras-go/v2/internal/ioutil" + "oras.land/oras-go/v2/registry" +) + +// ReferenceStorage represents a CAS that supports registry.ReferenceFetcher. +type ReferenceStorage interface { + content.ReadOnlyStorage + registry.ReferenceFetcher +} + +// Proxy is a caching proxy dedicated for registry.ReferenceFetcher. +// The first fetch call of a described content will read from the remote and +// cache the fetched content. +// The subsequent fetch call will read from the local cache. +type Proxy struct { + registry.ReferenceFetcher + *cas.Proxy +} + +// NewProxy creates a proxy for the `base` ReferenceStorage, using the `cache` +// storage as the cache. +func NewProxy(base ReferenceStorage, cache content.Storage) *Proxy { + return &Proxy{ + ReferenceFetcher: base, + Proxy: cas.NewProxy(base, cache), + } +} + +// FetchReference fetches the content identified by the reference from the +// remote and cache the fetched content. +func (p *Proxy) FetchReference(ctx context.Context, reference string) (ocispec.Descriptor, io.ReadCloser, error) { + target, rc, err := p.ReferenceFetcher.FetchReference(ctx, reference) + if err != nil { + return ocispec.Descriptor{}, nil, err + } + + // skip caching if the content already exists in cache + exists, err := p.Cache.Exists(ctx, target) + if err != nil { + return ocispec.Descriptor{}, nil, err + } + if exists { + return target, rc, nil + } + + // cache content while reading + pr, pw := io.Pipe() + var wg sync.WaitGroup + wg.Add(1) + var pushErr error + go func() { + defer wg.Done() + pushErr = p.Cache.Push(ctx, target, pr) + if pushErr != nil { + pr.CloseWithError(pushErr) + } + }() + closer := ioutil.CloserFunc(func() error { + rcErr := rc.Close() + if err := pw.Close(); err != nil { + return err + } + wg.Wait() + if pushErr != nil { + return pushErr + } + return rcErr + }) + + return target, struct { + io.Reader + io.Closer + }{ + Reader: io.TeeReader(rc, pw), + Closer: closer, + }, nil +} diff --git a/vendor/oras.land/oras-go/v2/internal/resolver/memory.go b/vendor/oras.land/oras-go/v2/internal/resolver/memory.go new file mode 100644 index 00000000..092a29e9 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/internal/resolver/memory.go @@ -0,0 +1,104 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resolver + +import ( + "context" + "maps" + "sync" + + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/errdef" + "oras.land/oras-go/v2/internal/container/set" +) + +// Memory is a memory based resolver. +type Memory struct { + lock sync.RWMutex + index map[string]ocispec.Descriptor + tags map[digest.Digest]set.Set[string] +} + +// NewMemory creates a new Memory resolver. +func NewMemory() *Memory { + return &Memory{ + index: make(map[string]ocispec.Descriptor), + tags: make(map[digest.Digest]set.Set[string]), + } +} + +// Resolve resolves a reference to a descriptor. +func (m *Memory) Resolve(_ context.Context, reference string) (ocispec.Descriptor, error) { + m.lock.RLock() + defer m.lock.RUnlock() + + desc, ok := m.index[reference] + if !ok { + return ocispec.Descriptor{}, errdef.ErrNotFound + } + return desc, nil +} + +// Tag tags a descriptor with a reference string. +func (m *Memory) Tag(_ context.Context, desc ocispec.Descriptor, reference string) error { + m.lock.Lock() + defer m.lock.Unlock() + + m.index[reference] = desc + tagSet, ok := m.tags[desc.Digest] + if !ok { + tagSet = set.New[string]() + m.tags[desc.Digest] = tagSet + } + tagSet.Add(reference) + return nil +} + +// Untag removes a reference from index map. +func (m *Memory) Untag(reference string) { + m.lock.Lock() + defer m.lock.Unlock() + + desc, ok := m.index[reference] + if !ok { + return + } + delete(m.index, reference) + tagSet := m.tags[desc.Digest] + tagSet.Delete(reference) + if len(tagSet) == 0 { + delete(m.tags, desc.Digest) + } +} + +// Map dumps the memory into a built-in map structure. +// Like other operations, calling Map() is go-routine safe. +func (m *Memory) Map() map[string]ocispec.Descriptor { + m.lock.RLock() + defer m.lock.RUnlock() + + return maps.Clone(m.index) +} + +// TagSet returns the set of tags of the descriptor. +func (m *Memory) TagSet(desc ocispec.Descriptor) set.Set[string] { + m.lock.RLock() + defer m.lock.RUnlock() + + tagSet := m.tags[desc.Digest] + return maps.Clone(tagSet) +} diff --git a/vendor/oras.land/oras-go/v2/internal/spec/artifact.go b/vendor/oras.land/oras-go/v2/internal/spec/artifact.go new file mode 100644 index 00000000..7f801fd9 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/internal/spec/artifact.go @@ -0,0 +1,57 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package spec + +import ocispec "github.com/opencontainers/image-spec/specs-go/v1" + +const ( + // AnnotationArtifactCreated is the annotation key for the date and time on which the artifact was built, conforming to RFC 3339. + AnnotationArtifactCreated = "org.opencontainers.artifact.created" + + // AnnotationArtifactDescription is the annotation key for the human readable description for the artifact. + AnnotationArtifactDescription = "org.opencontainers.artifact.description" + + // AnnotationReferrersFiltersApplied is the annotation key for the comma separated list of filters applied by the registry in the referrers listing. + AnnotationReferrersFiltersApplied = "org.opencontainers.referrers.filtersApplied" +) + +// MediaTypeArtifactManifest specifies the media type for a content descriptor. +const MediaTypeArtifactManifest = "application/vnd.oci.artifact.manifest.v1+json" + +// Artifact describes an artifact manifest. +// This structure provides `application/vnd.oci.artifact.manifest.v1+json` mediatype when marshalled to JSON. +// +// This manifest type was introduced in image-spec v1.1.0-rc1 and was removed in +// image-spec v1.1.0-rc3. It is not part of the current image-spec and is kept +// here for Go compatibility. +// +// Reference: https://github.com/opencontainers/image-spec/pull/999 +type Artifact struct { + // MediaType is the media type of the object this schema refers to. + MediaType string `json:"mediaType"` + + // ArtifactType is the IANA media type of the artifact this schema refers to. + ArtifactType string `json:"artifactType"` + + // Blobs is a collection of blobs referenced by this manifest. + Blobs []ocispec.Descriptor `json:"blobs,omitempty"` + + // Subject (reference) is an optional link from the artifact to another manifest forming an association between the artifact and the other manifest. + Subject *ocispec.Descriptor `json:"subject,omitempty"` + + // Annotations contains arbitrary metadata for the artifact manifest. + Annotations map[string]string `json:"annotations,omitempty"` +} diff --git a/vendor/oras.land/oras-go/v2/internal/status/tracker.go b/vendor/oras.land/oras-go/v2/internal/status/tracker.go new file mode 100644 index 00000000..1a48bb5a --- /dev/null +++ b/vendor/oras.land/oras-go/v2/internal/status/tracker.go @@ -0,0 +1,43 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package status + +import ( + "sync" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/internal/descriptor" +) + +// Tracker tracks content status described by a descriptor. +type Tracker struct { + status sync.Map // map[descriptor.Descriptor]chan struct{} +} + +// NewTracker creates a new content status tracker. +func NewTracker() *Tracker { + return &Tracker{} +} + +// TryCommit tries to commit the work for the target descriptor. +// Returns true if committed. A channel is also returned for sending +// notifications. Once the work is done, the channel should be closed. +// Returns false if the work is done or still in progress. +func (t *Tracker) TryCommit(target ocispec.Descriptor) (chan struct{}, bool) { + key := descriptor.FromOCI(target) + status, exists := t.status.LoadOrStore(key, make(chan struct{})) + return status.(chan struct{}), !exists +} diff --git a/vendor/oras.land/oras-go/v2/internal/syncutil/limit.go b/vendor/oras.land/oras-go/v2/internal/syncutil/limit.go new file mode 100644 index 00000000..2a05d4ea --- /dev/null +++ b/vendor/oras.land/oras-go/v2/internal/syncutil/limit.go @@ -0,0 +1,84 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package syncutil + +import ( + "context" + + "golang.org/x/sync/errgroup" + "golang.org/x/sync/semaphore" +) + +// LimitedRegion provides a way to bound concurrent access to a code block. +type LimitedRegion struct { + ctx context.Context + limiter *semaphore.Weighted + ended bool +} + +// LimitRegion creates a new LimitedRegion. +func LimitRegion(ctx context.Context, limiter *semaphore.Weighted) *LimitedRegion { + if limiter == nil { + return nil + } + return &LimitedRegion{ + ctx: ctx, + limiter: limiter, + ended: true, + } +} + +// Start starts the region with concurrency limit. +func (lr *LimitedRegion) Start() error { + if lr == nil || !lr.ended { + return nil + } + if err := lr.limiter.Acquire(lr.ctx, 1); err != nil { + return err + } + lr.ended = false + return nil +} + +// End ends the region with concurrency limit. +func (lr *LimitedRegion) End() { + if lr == nil || lr.ended { + return + } + lr.limiter.Release(1) + lr.ended = true +} + +// GoFunc represents a function that can be invoked by Go. +type GoFunc[T any] func(ctx context.Context, region *LimitedRegion, t T) error + +// Go concurrently invokes fn on items. +func Go[T any](ctx context.Context, limiter *semaphore.Weighted, fn GoFunc[T], items ...T) error { + eg, egCtx := errgroup.WithContext(ctx) + for _, item := range items { + region := LimitRegion(ctx, limiter) + if err := region.Start(); err != nil { + return err + } + eg.Go(func(t T) func() error { + return func() error { + defer region.End() + return fn(egCtx, region, t) + } + }(item)) + } + return eg.Wait() +} diff --git a/vendor/oras.land/oras-go/v2/internal/syncutil/limitgroup.go b/vendor/oras.land/oras-go/v2/internal/syncutil/limitgroup.go new file mode 100644 index 00000000..1071bedc --- /dev/null +++ b/vendor/oras.land/oras-go/v2/internal/syncutil/limitgroup.go @@ -0,0 +1,67 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package syncutil + +import ( + "context" + + "golang.org/x/sync/errgroup" +) + +// A LimitedGroup is a collection of goroutines working on subtasks that are part of +// the same overall task. +type LimitedGroup struct { + grp *errgroup.Group + ctx context.Context +} + +// LimitGroup returns a new LimitedGroup and an associated Context derived from ctx. +// +// The number of active goroutines in this group is limited to the given limit. +// A negative value indicates no limit. +// +// The derived Context is canceled the first time a function passed to Go +// returns a non-nil error or the first time Wait returns, whichever occurs +// first. +func LimitGroup(ctx context.Context, limit int) (*LimitedGroup, context.Context) { + grp, ctx := errgroup.WithContext(ctx) + grp.SetLimit(limit) + return &LimitedGroup{grp: grp, ctx: ctx}, ctx +} + +// Go calls the given function in a new goroutine. +// It blocks until the new goroutine can be added without the number of +// active goroutines in the group exceeding the configured limit. +// +// The first call to return a non-nil error cancels the group's context. +// After which, any subsequent calls to Go will not execute their given function. +// The error will be returned by Wait. +func (g *LimitedGroup) Go(f func() error) { + g.grp.Go(func() error { + select { + case <-g.ctx.Done(): + return g.ctx.Err() + default: + return f() + } + }) +} + +// Wait blocks until all function calls from the Go method have returned, then +// returns the first non-nil error (if any) from them. +func (g *LimitedGroup) Wait() error { + return g.grp.Wait() +} diff --git a/vendor/oras.land/oras-go/v2/internal/syncutil/merge.go b/vendor/oras.land/oras-go/v2/internal/syncutil/merge.go new file mode 100644 index 00000000..44788990 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/internal/syncutil/merge.go @@ -0,0 +1,140 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package syncutil + +import "sync" + +// mergeStatus represents the merge status of an item. +type mergeStatus struct { + // main indicates if items are being merged by the current go-routine. + main bool + // err represents the error of the merge operation. + err error +} + +// Merge represents merge operations on items. +// The state transfer is shown as below: +// +// +----------+ +// | Start +--------+-------------+ +// +----+-----+ | | +// | | | +// v v v +// +----+-----+ +----+----+ +----+----+ +// +-------+ Prepare +<--+ Pending +-->+ Waiting | +// | +----+-----+ +---------+ +----+----+ +// | | | +// | v | +// | + ---+---- + | +// On Error | Resolve | | +// | + ---+---- + | +// | | | +// | v | +// | +----+-----+ | +// +------>+ Complete +<---------------------+ +// +----+-----+ +// | +// v +// +----+-----+ +// | End | +// +----------+ +type Merge[T any] struct { + lock sync.Mutex + committed bool + items []T + status chan mergeStatus + pending []T + pendingStatus chan mergeStatus +} + +// Do merges concurrent operations of items into a single call of prepare and +// resolve. +// If Do is called multiple times concurrently, only one of the calls will be +// selected to invoke prepare and resolve. +func (m *Merge[T]) Do(item T, prepare func() error, resolve func(items []T) error) error { + status := <-m.assign(item) + if status.main { + err := prepare() + items := m.commit() + if err == nil { + err = resolve(items) + } + m.complete(err) + return err + } + return status.err +} + +// assign adds a new item into the item list. +func (m *Merge[T]) assign(item T) <-chan mergeStatus { + m.lock.Lock() + defer m.lock.Unlock() + + if m.committed { + if m.pendingStatus == nil { + m.pendingStatus = make(chan mergeStatus, 1) + } + m.pending = append(m.pending, item) + return m.pendingStatus + } + + if m.status == nil { + m.status = make(chan mergeStatus, 1) + m.status <- mergeStatus{main: true} + } + m.items = append(m.items, item) + return m.status +} + +// commit closes the assignment window, and the assigned items will be ready +// for resolve. +func (m *Merge[T]) commit() []T { + m.lock.Lock() + defer m.lock.Unlock() + + m.committed = true + return m.items +} + +// complete completes the previous merge, and moves the pending items to the +// stage for the next merge. +func (m *Merge[T]) complete(err error) { + // notify results + if err == nil { + close(m.status) + } else { + remaining := len(m.items) - 1 + status := m.status + for remaining > 0 { + status <- mergeStatus{err: err} + remaining-- + } + } + + // move pending items to the stage + m.lock.Lock() + defer m.lock.Unlock() + + m.committed = false + m.items = m.pending + m.status = m.pendingStatus + m.pending = nil + m.pendingStatus = nil + + if m.status != nil { + m.status <- mergeStatus{main: true} + } +} diff --git a/vendor/oras.land/oras-go/v2/internal/syncutil/once.go b/vendor/oras.land/oras-go/v2/internal/syncutil/once.go new file mode 100644 index 00000000..e4497053 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/internal/syncutil/once.go @@ -0,0 +1,102 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package syncutil + +import ( + "context" + "sync" + "sync/atomic" +) + +// Once is an object that will perform exactly one action. +// Unlike sync.Once, this Once allows the action to have return values. +type Once struct { + result interface{} + err error + status chan bool +} + +// NewOnce creates a new Once instance. +func NewOnce() *Once { + status := make(chan bool, 1) + status <- true + return &Once{ + status: status, + } +} + +// Do calls the function f if and only if Do is being called first time or all +// previous function calls are cancelled, deadline exceeded, or panicking. +// When `once.Do(ctx, f)` is called multiple times, the return value of the +// first call of the function f is stored, and is directly returned for other +// calls. +// Besides the return value of the function f, including the error, Do returns +// true if the function f passed is called first and is not cancelled, deadline +// exceeded, or panicking. Otherwise, returns false. +func (o *Once) Do(ctx context.Context, f func() (interface{}, error)) (bool, interface{}, error) { + defer func() { + if r := recover(); r != nil { + o.status <- true + panic(r) + } + }() + for { + select { + case inProgress := <-o.status: + if !inProgress { + return false, o.result, o.err + } + result, err := f() + if err == context.Canceled || err == context.DeadlineExceeded { + o.status <- true + return false, nil, err + } + o.result, o.err = result, err + close(o.status) + return true, result, err + case <-ctx.Done(): + return false, nil, ctx.Err() + } + } +} + +// OnceOrRetry is an object that will perform exactly one success action. +type OnceOrRetry struct { + done atomic.Bool + lock sync.Mutex +} + +// OnceOrRetry calls the function f if and only if Do is being called for the +// first time for this instance of Once or all previous calls to Do are failed. +func (o *OnceOrRetry) Do(f func() error) error { + // fast path + if o.done.Load() { + return nil + } + + // slow path + o.lock.Lock() + defer o.lock.Unlock() + + if o.done.Load() { + return nil + } + if err := f(); err != nil { + return err + } + o.done.Store(true) + return nil +} diff --git a/vendor/oras.land/oras-go/v2/internal/syncutil/pool.go b/vendor/oras.land/oras-go/v2/internal/syncutil/pool.go new file mode 100644 index 00000000..6fb4a69c --- /dev/null +++ b/vendor/oras.land/oras-go/v2/internal/syncutil/pool.go @@ -0,0 +1,64 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package syncutil + +import "sync" + +// poolItem represents an item in Pool. +type poolItem[T any] struct { + value T + refCount int +} + +// Pool is a scalable pool with items identified by keys. +type Pool[T any] struct { + // New optionally specifies a function to generate a value when Get would + // otherwise return nil. + // It may not be changed concurrently with calls to Get. + New func() T + + lock sync.Mutex + items map[any]*poolItem[T] +} + +// Get gets the value identified by key. +// The caller should invoke the returned function after using the returned item. +func (p *Pool[T]) Get(key any) (*T, func()) { + p.lock.Lock() + defer p.lock.Unlock() + + item, ok := p.items[key] + if !ok { + if p.items == nil { + p.items = make(map[any]*poolItem[T]) + } + item = &poolItem[T]{} + if p.New != nil { + item.value = p.New() + } + p.items[key] = item + } + item.refCount++ + + return &item.value, func() { + p.lock.Lock() + defer p.lock.Unlock() + item.refCount-- + if item.refCount <= 0 { + delete(p.items, key) + } + } +} diff --git a/vendor/oras.land/oras-go/v2/pack.go b/vendor/oras.land/oras-go/v2/pack.go new file mode 100644 index 00000000..1b995612 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/pack.go @@ -0,0 +1,439 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package oras + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "maps" + "regexp" + "time" + + specs "github.com/opencontainers/image-spec/specs-go" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/content" + "oras.land/oras-go/v2/errdef" + "oras.land/oras-go/v2/internal/spec" +) + +const ( + // MediaTypeUnknownConfig is the default config mediaType used + // - for [Pack] when PackOptions.PackImageManifest is true and + // PackOptions.ConfigDescriptor is not specified. + // - for [PackManifest] when packManifestVersion is PackManifestVersion1_0 + // and PackManifestOptions.ConfigDescriptor is not specified. + MediaTypeUnknownConfig = "application/vnd.unknown.config.v1+json" + + // MediaTypeUnknownArtifact is the default artifactType used for [Pack] + // when PackOptions.PackImageManifest is false and artifactType is + // not specified. + MediaTypeUnknownArtifact = "application/vnd.unknown.artifact.v1" +) + +var ( + // ErrInvalidDateTimeFormat is returned by [Pack] and [PackManifest] when + // AnnotationArtifactCreated or AnnotationCreated is provided, but its value + // is not in RFC 3339 format. + // Reference: https://www.rfc-editor.org/rfc/rfc3339#section-5.6 + ErrInvalidDateTimeFormat = errors.New("invalid date and time format") + + // ErrMissingArtifactType is returned by [PackManifest] when + // packManifestVersion is PackManifestVersion1_1 and artifactType is + // empty and the config media type is set to + // "application/vnd.oci.empty.v1+json". + ErrMissingArtifactType = errors.New("missing artifact type") +) + +// PackManifestVersion represents the manifest version used for [PackManifest]. +type PackManifestVersion int + +const ( + // PackManifestVersion1_0 represents the OCI Image Manifest defined in + // image-spec v1.0.2. + // Reference: https://github.com/opencontainers/image-spec/blob/v1.0.2/manifest.md + PackManifestVersion1_0 PackManifestVersion = 1 + + // PackManifestVersion1_1_RC4 represents the OCI Image Manifest defined + // in image-spec v1.1.0-rc4. + // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc4/manifest.md + // + // Deprecated: This constant is deprecated and not recommended for future use. + // Use [PackManifestVersion1_1] instead. + PackManifestVersion1_1_RC4 PackManifestVersion = PackManifestVersion1_1 + + // PackManifestVersion1_1 represents the OCI Image Manifest defined in + // image-spec v1.1.0. + // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md + PackManifestVersion1_1 PackManifestVersion = 2 +) + +// PackManifestOptions contains optional parameters for [PackManifest]. +type PackManifestOptions struct { + // Subject is the subject of the manifest. + // This option is only valid when PackManifestVersion is + // NOT PackManifestVersion1_0. + Subject *ocispec.Descriptor + + // Layers is the layers of the manifest. + Layers []ocispec.Descriptor + + // ManifestAnnotations is the annotation map of the manifest. + ManifestAnnotations map[string]string + + // ConfigDescriptor is a pointer to the descriptor of the config blob. + // If not nil, ConfigAnnotations will be ignored. + ConfigDescriptor *ocispec.Descriptor + + // ConfigAnnotations is the annotation map of the config descriptor. + // This option is valid only when ConfigDescriptor is nil. + ConfigAnnotations map[string]string +} + +// mediaTypeRegexp checks the format of media types. +// References: +// - https://github.com/opencontainers/image-spec/blob/v1.1.0/schema/defs-descriptor.json#L7 +// - https://datatracker.ietf.org/doc/html/rfc6838#section-4.2 +var mediaTypeRegexp = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}$`) + +// PackManifest generates an OCI Image Manifest based on the given parameters +// and pushes the packed manifest to a content storage using pusher. The version +// of the manifest to be packed is determined by packManifestVersion +// (Recommended value: PackManifestVersion1_1). +// +// - If packManifestVersion is [PackManifestVersion1_1]: +// artifactType MUST NOT be empty unless opts.ConfigDescriptor is specified. +// - If packManifestVersion is [PackManifestVersion1_0]: +// if opts.ConfigDescriptor is nil, artifactType will be used as the +// config media type; if artifactType is empty, +// "application/vnd.unknown.config.v1+json" will be used. +// if opts.ConfigDescriptor is NOT nil, artifactType will be ignored. +// +// artifactType and opts.ConfigDescriptor.MediaType MUST comply with RFC 6838. +// +// If succeeded, returns a descriptor of the packed manifest. +func PackManifest(ctx context.Context, pusher content.Pusher, packManifestVersion PackManifestVersion, artifactType string, opts PackManifestOptions) (ocispec.Descriptor, error) { + switch packManifestVersion { + case PackManifestVersion1_0: + return packManifestV1_0(ctx, pusher, artifactType, opts) + case PackManifestVersion1_1: + return packManifestV1_1(ctx, pusher, artifactType, opts) + default: + return ocispec.Descriptor{}, fmt.Errorf("PackManifestVersion(%v): %w", packManifestVersion, errdef.ErrUnsupported) + } +} + +// PackOptions contains optional parameters for [Pack]. +// +// Deprecated: This type is deprecated and not recommended for future use. +// Use [PackManifestOptions] instead. +type PackOptions struct { + // Subject is the subject of the manifest. + Subject *ocispec.Descriptor + + // ManifestAnnotations is the annotation map of the manifest. + ManifestAnnotations map[string]string + + // PackImageManifest controls whether to pack an OCI Image Manifest or not. + // - If true, pack an OCI Image Manifest. + // - If false, pack an OCI Artifact Manifest (deprecated). + // + // Default value: false. + PackImageManifest bool + + // ConfigDescriptor is a pointer to the descriptor of the config blob. + // If not nil, artifactType will be implied by the mediaType of the + // specified ConfigDescriptor, and ConfigAnnotations will be ignored. + // This option is valid only when PackImageManifest is true. + ConfigDescriptor *ocispec.Descriptor + + // ConfigAnnotations is the annotation map of the config descriptor. + // This option is valid only when PackImageManifest is true + // and ConfigDescriptor is nil. + ConfigAnnotations map[string]string +} + +// Pack packs the given blobs, generates a manifest for the pack, +// and pushes it to a content storage. +// +// When opts.PackImageManifest is true, artifactType will be used as the +// the config descriptor mediaType of the image manifest. +// +// If succeeded, returns a descriptor of the manifest. +// +// Deprecated: This method is deprecated and not recommended for future use. +// Use [PackManifest] instead. +func Pack(ctx context.Context, pusher content.Pusher, artifactType string, blobs []ocispec.Descriptor, opts PackOptions) (ocispec.Descriptor, error) { + if opts.PackImageManifest { + return packManifestV1_1_RC2(ctx, pusher, artifactType, blobs, opts) + } + return packArtifact(ctx, pusher, artifactType, blobs, opts) +} + +// packArtifact packs an Artifact manifest as defined in image-spec v1.1.0-rc2. +// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/artifact.md +func packArtifact(ctx context.Context, pusher content.Pusher, artifactType string, blobs []ocispec.Descriptor, opts PackOptions) (ocispec.Descriptor, error) { + if artifactType == "" { + artifactType = MediaTypeUnknownArtifact + } + + annotations, err := ensureAnnotationCreated(opts.ManifestAnnotations, spec.AnnotationArtifactCreated) + if err != nil { + return ocispec.Descriptor{}, err + } + manifest := spec.Artifact{ + MediaType: spec.MediaTypeArtifactManifest, + ArtifactType: artifactType, + Blobs: blobs, + Subject: opts.Subject, + Annotations: annotations, + } + return pushManifest(ctx, pusher, manifest, manifest.MediaType, manifest.ArtifactType, manifest.Annotations) +} + +// packManifestV1_0 packs an image manifest defined in image-spec v1.0.2. +// Reference: https://github.com/opencontainers/image-spec/blob/v1.0.2/manifest.md +func packManifestV1_0(ctx context.Context, pusher content.Pusher, artifactType string, opts PackManifestOptions) (ocispec.Descriptor, error) { + if opts.Subject != nil { + return ocispec.Descriptor{}, fmt.Errorf("subject is not supported for manifest version %v: %w", PackManifestVersion1_0, errdef.ErrUnsupported) + } + + // prepare config + var configDesc ocispec.Descriptor + if opts.ConfigDescriptor != nil { + if err := validateMediaType(opts.ConfigDescriptor.MediaType); err != nil { + return ocispec.Descriptor{}, fmt.Errorf("invalid config mediaType format: %w", err) + } + configDesc = *opts.ConfigDescriptor + } else { + if artifactType == "" { + artifactType = MediaTypeUnknownConfig + } else if err := validateMediaType(artifactType); err != nil { + return ocispec.Descriptor{}, fmt.Errorf("invalid artifactType format: %w", err) + } + var err error + configDesc, err = pushCustomEmptyConfig(ctx, pusher, artifactType, opts.ConfigAnnotations) + if err != nil { + return ocispec.Descriptor{}, err + } + } + + annotations, err := ensureAnnotationCreated(opts.ManifestAnnotations, ocispec.AnnotationCreated) + if err != nil { + return ocispec.Descriptor{}, err + } + if opts.Layers == nil { + opts.Layers = []ocispec.Descriptor{} // make it an empty array to prevent potential server-side bugs + } + manifest := ocispec.Manifest{ + Versioned: specs.Versioned{ + SchemaVersion: 2, // historical value. does not pertain to OCI or docker version + }, + Config: configDesc, + MediaType: ocispec.MediaTypeImageManifest, + Layers: opts.Layers, + Annotations: annotations, + } + return pushManifest(ctx, pusher, manifest, manifest.MediaType, manifest.Config.MediaType, manifest.Annotations) +} + +// packManifestV1_1_RC2 packs an image manifest as defined in image-spec +// v1.1.0-rc2. +// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/manifest.md +func packManifestV1_1_RC2(ctx context.Context, pusher content.Pusher, configMediaType string, layers []ocispec.Descriptor, opts PackOptions) (ocispec.Descriptor, error) { + if configMediaType == "" { + configMediaType = MediaTypeUnknownConfig + } + + // prepare config + var configDesc ocispec.Descriptor + if opts.ConfigDescriptor != nil { + configDesc = *opts.ConfigDescriptor + } else { + var err error + configDesc, err = pushCustomEmptyConfig(ctx, pusher, configMediaType, opts.ConfigAnnotations) + if err != nil { + return ocispec.Descriptor{}, err + } + } + + annotations, err := ensureAnnotationCreated(opts.ManifestAnnotations, ocispec.AnnotationCreated) + if err != nil { + return ocispec.Descriptor{}, err + } + if layers == nil { + layers = []ocispec.Descriptor{} // make it an empty array to prevent potential server-side bugs + } + manifest := ocispec.Manifest{ + Versioned: specs.Versioned{ + SchemaVersion: 2, // historical value. does not pertain to OCI or docker version + }, + Config: configDesc, + MediaType: ocispec.MediaTypeImageManifest, + Layers: layers, + Subject: opts.Subject, + Annotations: annotations, + } + return pushManifest(ctx, pusher, manifest, manifest.MediaType, manifest.Config.MediaType, manifest.Annotations) +} + +// packManifestV1_1 packs an image manifest defined in image-spec v1.1.0. +// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md#guidelines-for-artifact-usage +func packManifestV1_1(ctx context.Context, pusher content.Pusher, artifactType string, opts PackManifestOptions) (ocispec.Descriptor, error) { + if artifactType == "" && (opts.ConfigDescriptor == nil || opts.ConfigDescriptor.MediaType == ocispec.MediaTypeEmptyJSON) { + // artifactType MUST be set when config.mediaType is set to the empty value + return ocispec.Descriptor{}, ErrMissingArtifactType + } + if artifactType != "" { + if err := validateMediaType(artifactType); err != nil { + return ocispec.Descriptor{}, fmt.Errorf("invalid artifactType format: %w", err) + } + } + + // prepare config + var emptyBlobExists bool + var configDesc ocispec.Descriptor + if opts.ConfigDescriptor != nil { + if err := validateMediaType(opts.ConfigDescriptor.MediaType); err != nil { + return ocispec.Descriptor{}, fmt.Errorf("invalid config mediaType format: %w", err) + } + configDesc = *opts.ConfigDescriptor + } else { + // use the empty descriptor for config + configDesc = ocispec.DescriptorEmptyJSON + configDesc.Annotations = opts.ConfigAnnotations + configBytes := ocispec.DescriptorEmptyJSON.Data + // push config + if err := pushIfNotExist(ctx, pusher, configDesc, configBytes); err != nil { + return ocispec.Descriptor{}, fmt.Errorf("failed to push config: %w", err) + } + emptyBlobExists = true + } + + annotations, err := ensureAnnotationCreated(opts.ManifestAnnotations, ocispec.AnnotationCreated) + if err != nil { + return ocispec.Descriptor{}, err + } + if len(opts.Layers) == 0 { + // use the empty descriptor as the single layer + layerDesc := ocispec.DescriptorEmptyJSON + layerData := ocispec.DescriptorEmptyJSON.Data + if !emptyBlobExists { + if err := pushIfNotExist(ctx, pusher, layerDesc, layerData); err != nil { + return ocispec.Descriptor{}, fmt.Errorf("failed to push layer: %w", err) + } + } + opts.Layers = []ocispec.Descriptor{layerDesc} + } + + manifest := ocispec.Manifest{ + Versioned: specs.Versioned{ + SchemaVersion: 2, // historical value. does not pertain to OCI or docker version + }, + Config: configDesc, + MediaType: ocispec.MediaTypeImageManifest, + Layers: opts.Layers, + Subject: opts.Subject, + ArtifactType: artifactType, + Annotations: annotations, + } + return pushManifest(ctx, pusher, manifest, manifest.MediaType, manifest.ArtifactType, manifest.Annotations) +} + +// pushIfNotExist pushes data described by desc if it does not exist in the +// target. +func pushIfNotExist(ctx context.Context, pusher content.Pusher, desc ocispec.Descriptor, data []byte) error { + if ros, ok := pusher.(content.ReadOnlyStorage); ok { + exists, err := ros.Exists(ctx, desc) + if err != nil { + return fmt.Errorf("failed to check existence: %s: %s: %w", desc.Digest.String(), desc.MediaType, err) + } + if exists { + return nil + } + } + + if err := pusher.Push(ctx, desc, bytes.NewReader(data)); err != nil && !errors.Is(err, errdef.ErrAlreadyExists) { + return fmt.Errorf("failed to push: %s: %s: %w", desc.Digest.String(), desc.MediaType, err) + } + return nil +} + +// pushManifest marshals manifest into JSON bytes and pushes it. +func pushManifest(ctx context.Context, pusher content.Pusher, manifest any, mediaType string, artifactType string, annotations map[string]string) (ocispec.Descriptor, error) { + manifestJSON, err := json.Marshal(manifest) + if err != nil { + return ocispec.Descriptor{}, fmt.Errorf("failed to marshal manifest: %w", err) + } + manifestDesc := content.NewDescriptorFromBytes(mediaType, manifestJSON) + // populate ArtifactType and Annotations of the manifest into manifestDesc + manifestDesc.ArtifactType = artifactType + manifestDesc.Annotations = annotations + // push manifest + if err := pusher.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON)); err != nil && !errors.Is(err, errdef.ErrAlreadyExists) { + return ocispec.Descriptor{}, fmt.Errorf("failed to push manifest: %w", err) + } + return manifestDesc, nil +} + +// pushCustomEmptyConfig generates and pushes an empty config blob. +func pushCustomEmptyConfig(ctx context.Context, pusher content.Pusher, mediaType string, annotations map[string]string) (ocispec.Descriptor, error) { + // Use an empty JSON object here, because some registries may not accept + // empty config blob. + // As of September 2022, GAR is known to return 400 on empty blob upload. + // See https://github.com/oras-project/oras-go/issues/294 for details. + configBytes := []byte("{}") + configDesc := content.NewDescriptorFromBytes(mediaType, configBytes) + configDesc.Annotations = annotations + // push config + if err := pushIfNotExist(ctx, pusher, configDesc, configBytes); err != nil { + return ocispec.Descriptor{}, fmt.Errorf("failed to push config: %w", err) + } + return configDesc, nil +} + +// ensureAnnotationCreated ensures that annotationCreatedKey is in annotations, +// and that its value conforms to RFC 3339. Otherwise returns a new annotation +// map with annotationCreatedKey created. +func ensureAnnotationCreated(annotations map[string]string, annotationCreatedKey string) (map[string]string, error) { + if createdTime, ok := annotations[annotationCreatedKey]; ok { + // if annotationCreatedKey is provided, validate its format + if _, err := time.Parse(time.RFC3339, createdTime); err != nil { + return nil, fmt.Errorf("%w: %v", ErrInvalidDateTimeFormat, err) + } + return annotations, nil + } + + // copy the original annotation map + copied := make(map[string]string, len(annotations)+1) + maps.Copy(copied, annotations) + + // set creation time in RFC 3339 format + // reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/annotations.md#pre-defined-annotation-keys + now := time.Now().UTC() + copied[annotationCreatedKey] = now.Format(time.RFC3339) + return copied, nil +} + +// validateMediaType validates the format of mediaType. +func validateMediaType(mediaType string) error { + if !mediaTypeRegexp.MatchString(mediaType) { + return fmt.Errorf("%s: %w", mediaType, errdef.ErrInvalidMediaType) + } + return nil +} diff --git a/vendor/oras.land/oras-go/v2/registry/reference.go b/vendor/oras.land/oras-go/v2/registry/reference.go new file mode 100644 index 00000000..fc3e95e5 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/registry/reference.go @@ -0,0 +1,276 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package registry + +import ( + "fmt" + "net/url" + "regexp" + "strings" + + "github.com/opencontainers/go-digest" + "oras.land/oras-go/v2/errdef" +) + +// regular expressions for components. +var ( + // repositoryRegexp is adapted from the distribution implementation. The + // repository name set under OCI distribution spec is a subset of the docker + // spec. For maximum compatability, the docker spec is verified client-side. + // Further checks are left to the server-side. + // + // References: + // - https://github.com/distribution/distribution/blob/v2.7.1/reference/regexp.go#L53 + // - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#pulling-manifests + repositoryRegexp = regexp.MustCompile(`^[a-z0-9]+(?:(?:[._]|__|[-]*)[a-z0-9]+)*(?:/[a-z0-9]+(?:(?:[._]|__|[-]*)[a-z0-9]+)*)*$`) + + // tagRegexp checks the tag name. + // The docker and OCI spec have the same regular expression. + // + // Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#pulling-manifests + tagRegexp = regexp.MustCompile(`^[\w][\w.-]{0,127}$`) +) + +// Reference references either a resource descriptor (where Reference.Reference +// is a tag or a digest), or a resource repository (where Reference.Reference +// is the empty string). +type Reference struct { + // Registry is the name of the registry. It is usually the domain name of + // the registry optionally with a port. + Registry string + + // Repository is the name of the repository. + Repository string + + // Reference is the reference of the object in the repository. This field + // can take any one of the four valid forms (see ParseReference). In the + // case where it's the empty string, it necessarily implies valid form D, + // and where it is non-empty, then it is either a tag, or a digest + // (implying one of valid forms A, B, or C). + Reference string +} + +// ParseReference parses a string (artifact) into an `artifact reference`. +// Corresponding cryptographic hash implementations are required to be imported +// as specified by https://pkg.go.dev/github.com/opencontainers/go-digest#readme-usage +// if the string contains a digest. +// +// Note: An "image" is an "artifact", however, an "artifact" is not necessarily +// an "image". +// +// The token `artifact` is composed of other tokens, and those in turn are +// composed of others. This definition recursivity requires a notation capable +// of recursion, thus the following two forms have been adopted: +// +// 1. Backus–Naur Form (BNF) has been adopted to address the recursive nature +// of the definition. +// 2. Token opacity is revealed via its label letter-casing. That is, "opaque" +// tokens (i.e., tokens that are not final, and must therefore be further +// broken down into their constituents) are denoted in *lowercase*, while +// final tokens (i.e., leaf-node tokens that are final) are denoted in +// *uppercase*. +// +// Finally, note that a number of the opaque tokens are polymorphic in nature; +// that is, they can take on one of numerous forms, not restricted to a single +// defining form. +// +// The top-level token, `artifact`, is composed of two (opaque) tokens; namely +// `socketaddr` and `path`: +// +// ::= "/" +// +// The former is described as follows: +// +// ::= | ":" +// ::= | +// ::= | +// +// The latter, which is of greater interest here, is described as follows: +// +// ::= | +// ::= "@" | ":" "@" | ":" +// ::= ":" +// +// This second token--`path`--can take on exactly four forms, each of which will +// now be illustrated: +// +// <--- path --------------------------------------------> | - Decode `path` +// <=== REPOSITORY ===> <--- reference ------------------> | - Decode `reference` +// <=== REPOSITORY ===> @ <=================== digest ===> | - Valid Form A +// <=== REPOSITORY ===> : @ <=== digest ===> | - Valid Form B (tag is dropped) +// <=== REPOSITORY ===> : <=== TAG ======================> | - Valid Form C +// <=== REPOSITORY ======================================> | - Valid Form D +// +// Note: In the case of Valid Form B, TAG is dropped without any validation or +// further consideration. +func ParseReference(artifact string) (Reference, error) { + parts := strings.SplitN(artifact, "/", 2) + if len(parts) == 1 { + // Invalid Form + return Reference{}, fmt.Errorf("%w: missing registry or repository", errdef.ErrInvalidReference) + } + registry, path := parts[0], parts[1] + + var isTag bool + var repository string + var reference string + if index := strings.Index(path, "@"); index != -1 { + // `digest` found; Valid Form A (if not B) + isTag = false + repository = path[:index] + reference = path[index+1:] + + if index = strings.Index(repository, ":"); index != -1 { + // `tag` found (and now dropped without validation) since `the + // `digest` already present; Valid Form B + repository = repository[:index] + } + } else if index = strings.Index(path, ":"); index != -1 { + // `tag` found; Valid Form C + isTag = true + repository = path[:index] + reference = path[index+1:] + } else { + // empty `reference`; Valid Form D + repository = path + } + ref := Reference{ + Registry: registry, + Repository: repository, + Reference: reference, + } + + if err := ref.ValidateRegistry(); err != nil { + return Reference{}, err + } + + if err := ref.ValidateRepository(); err != nil { + return Reference{}, err + } + + if len(ref.Reference) == 0 { + return ref, nil + } + + validator := ref.ValidateReferenceAsDigest + if isTag { + validator = ref.ValidateReferenceAsTag + } + if err := validator(); err != nil { + return Reference{}, err + } + + return ref, nil +} + +// Validate the entire reference object; the registry, the repository, and the +// reference. +func (r Reference) Validate() error { + if err := r.ValidateRegistry(); err != nil { + return err + } + + if err := r.ValidateRepository(); err != nil { + return err + } + + return r.ValidateReference() +} + +// ValidateRegistry validates the registry. +func (r Reference) ValidateRegistry() error { + if uri, err := url.ParseRequestURI("dummy://" + r.Registry); err != nil || uri.Host == "" || uri.Host != r.Registry { + return fmt.Errorf("%w: invalid registry %q", errdef.ErrInvalidReference, r.Registry) + } + return nil +} + +// ValidateRepository validates the repository. +func (r Reference) ValidateRepository() error { + if !repositoryRegexp.MatchString(r.Repository) { + return fmt.Errorf("%w: invalid repository %q", errdef.ErrInvalidReference, r.Repository) + } + return nil +} + +// ValidateReferenceAsTag validates the reference as a tag. +func (r Reference) ValidateReferenceAsTag() error { + if !tagRegexp.MatchString(r.Reference) { + return fmt.Errorf("%w: invalid tag %q", errdef.ErrInvalidReference, r.Reference) + } + return nil +} + +// ValidateReferenceAsDigest validates the reference as a digest. +func (r Reference) ValidateReferenceAsDigest() error { + if _, err := r.Digest(); err != nil { + return fmt.Errorf("%w: invalid digest %q: %v", errdef.ErrInvalidReference, r.Reference, err) + } + return nil +} + +// ValidateReference where the reference is first tried as an ampty string, then +// as a digest, and if that fails, as a tag. +func (r Reference) ValidateReference() error { + if len(r.Reference) == 0 { + return nil + } + + if index := strings.IndexByte(r.Reference, ':'); index != -1 { + return r.ValidateReferenceAsDigest() + } + + return r.ValidateReferenceAsTag() +} + +// Host returns the host name of the registry. +func (r Reference) Host() string { + if r.Registry == "docker.io" { + return "registry-1.docker.io" + } + return r.Registry +} + +// ReferenceOrDefault returns the reference or the default reference if empty. +func (r Reference) ReferenceOrDefault() string { + if r.Reference == "" { + return "latest" + } + return r.Reference +} + +// Digest returns the reference as a digest. +// Corresponding cryptographic hash implementations are required to be imported +// as specified by https://pkg.go.dev/github.com/opencontainers/go-digest#readme-usage +func (r Reference) Digest() (digest.Digest, error) { + return digest.Parse(r.Reference) +} + +// String implements `fmt.Stringer` and returns the reference string. +// The resulted string is meaningful only if the reference is valid. +func (r Reference) String() string { + if r.Repository == "" { + return r.Registry + } + ref := r.Registry + "/" + r.Repository + if r.Reference == "" { + return ref + } + if d, err := r.Digest(); err == nil { + return ref + "@" + d.String() + } + return ref + ":" + r.Reference +} diff --git a/vendor/oras.land/oras-go/v2/registry/registry.go b/vendor/oras.land/oras-go/v2/registry/registry.go new file mode 100644 index 00000000..e1da0ab9 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/registry/registry.go @@ -0,0 +1,52 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package registry provides high-level operations to manage registries. +package registry + +import "context" + +// Registry represents a collection of repositories. +type Registry interface { + // Repositories lists the name of repositories available in the registry. + // Since the returned repositories may be paginated by the underlying + // implementation, a function should be passed in to process the paginated + // repository list. + // `last` argument is the `last` parameter when invoking the catalog API. + // If `last` is NOT empty, the entries in the response start after the + // repo specified by `last`. Otherwise, the response starts from the top + // of the Repositories list. + // Note: When implemented by a remote registry, the catalog API is called. + // However, not all registries supports pagination or conforms the + // specification. + // Reference: https://docs.docker.com/registry/spec/api/#catalog + // See also `Repositories()` in this package. + Repositories(ctx context.Context, last string, fn func(repos []string) error) error + + // Repository returns a repository reference by the given name. + Repository(ctx context.Context, name string) (Repository, error) +} + +// Repositories lists the name of repositories available in the registry. +func Repositories(ctx context.Context, reg Registry) ([]string, error) { + var res []string + if err := reg.Repositories(ctx, "", func(repos []string) error { + res = append(res, repos...) + return nil + }); err != nil { + return nil, err + } + return res, nil +} diff --git a/vendor/oras.land/oras-go/v2/registry/remote/auth/cache.go b/vendor/oras.land/oras-go/v2/registry/remote/auth/cache.go new file mode 100644 index 00000000..d11c092b --- /dev/null +++ b/vendor/oras.land/oras-go/v2/registry/remote/auth/cache.go @@ -0,0 +1,232 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package auth + +import ( + "context" + "strings" + "sync" + + "oras.land/oras-go/v2/errdef" + "oras.land/oras-go/v2/internal/syncutil" +) + +// DefaultCache is the sharable cache used by DefaultClient. +var DefaultCache Cache = NewCache() + +// Cache caches the auth-scheme and auth-token for the "Authorization" header in +// accessing the remote registry. +// Precisely, the header is `Authorization: auth-scheme auth-token`. +// The `auth-token` is a generic term as `token68` in RFC 7235 section 2.1. +type Cache interface { + // GetScheme returns the auth-scheme part cached for the given registry. + // A single registry is assumed to have a consistent scheme. + // If a registry has different schemes per path, the auth client is still + // workable. However, the cache may not be effective as the cache cannot + // correctly guess the scheme. + GetScheme(ctx context.Context, registry string) (Scheme, error) + + // GetToken returns the auth-token part cached for the given registry of a + // given scheme. + // The underlying implementation MAY cache the token for all schemes for the + // given registry. + GetToken(ctx context.Context, registry string, scheme Scheme, key string) (string, error) + + // Set fetches the token using the given fetch function and caches the token + // for the given scheme with the given key for the given registry. + // The return values of the fetch function is returned by this function. + // The underlying implementation MAY combine the fetch operation if the Set + // function is invoked multiple times at the same time. + Set(ctx context.Context, registry string, scheme Scheme, key string, fetch func(context.Context) (string, error)) (string, error) +} + +// cacheEntry is a cache entry for a single registry. +type cacheEntry struct { + scheme Scheme + tokens sync.Map // map[string]string +} + +// concurrentCache is a cache suitable for concurrent invocation. +type concurrentCache struct { + status sync.Map // map[string]*syncutil.Once + cache sync.Map // map[string]*cacheEntry +} + +// NewCache creates a new go-routine safe cache instance. +func NewCache() Cache { + return &concurrentCache{} +} + +// GetScheme returns the auth-scheme part cached for the given registry. +func (cc *concurrentCache) GetScheme(ctx context.Context, registry string) (Scheme, error) { + entry, ok := cc.cache.Load(registry) + if !ok { + return SchemeUnknown, errdef.ErrNotFound + } + return entry.(*cacheEntry).scheme, nil +} + +// GetToken returns the auth-token part cached for the given registry of a given +// scheme. +func (cc *concurrentCache) GetToken(ctx context.Context, registry string, scheme Scheme, key string) (string, error) { + entryValue, ok := cc.cache.Load(registry) + if !ok { + return "", errdef.ErrNotFound + } + entry := entryValue.(*cacheEntry) + if entry.scheme != scheme { + return "", errdef.ErrNotFound + } + if token, ok := entry.tokens.Load(key); ok { + return token.(string), nil + } + return "", errdef.ErrNotFound +} + +// Set fetches the token using the given fetch function and caches the token +// for the given scheme with the given key for the given registry. +// Set combines the fetch operation if the Set is invoked multiple times at the +// same time. +func (cc *concurrentCache) Set(ctx context.Context, registry string, scheme Scheme, key string, fetch func(context.Context) (string, error)) (string, error) { + // fetch token + statusKey := strings.Join([]string{ + registry, + scheme.String(), + key, + }, " ") + statusValue, _ := cc.status.LoadOrStore(statusKey, syncutil.NewOnce()) + fetchOnce := statusValue.(*syncutil.Once) + fetchedFirst, result, err := fetchOnce.Do(ctx, func() (interface{}, error) { + return fetch(ctx) + }) + if fetchedFirst { + cc.status.Delete(statusKey) + } + if err != nil { + return "", err + } + token := result.(string) + if !fetchedFirst { + return token, nil + } + + // cache token + newEntry := &cacheEntry{ + scheme: scheme, + } + entryValue, exists := cc.cache.LoadOrStore(registry, newEntry) + entry := entryValue.(*cacheEntry) + if exists && entry.scheme != scheme { + // there is a scheme change, which is not expected in most scenarios. + // force invalidating all previous cache. + entry = newEntry + cc.cache.Store(registry, entry) + } + entry.tokens.Store(key, token) + + return token, nil +} + +// noCache is a cache implementation that does not do cache at all. +type noCache struct{} + +// GetScheme always returns not found error as it has no cache. +func (noCache) GetScheme(ctx context.Context, registry string) (Scheme, error) { + return SchemeUnknown, errdef.ErrNotFound +} + +// GetToken always returns not found error as it has no cache. +func (noCache) GetToken(ctx context.Context, registry string, scheme Scheme, key string) (string, error) { + return "", errdef.ErrNotFound +} + +// Set calls fetch directly without caching. +func (noCache) Set(ctx context.Context, registry string, scheme Scheme, key string, fetch func(context.Context) (string, error)) (string, error) { + return fetch(ctx) +} + +// hostCache is an auth cache that ignores scopes. Uses only the registry's hostname to find a token. +type hostCache struct { + Cache +} + +// GetToken implements Cache. +func (c *hostCache) GetToken(ctx context.Context, registry string, scheme Scheme, key string) (string, error) { + return c.Cache.GetToken(ctx, registry, scheme, "") +} + +// Set implements Cache. +func (c *hostCache) Set(ctx context.Context, registry string, scheme Scheme, key string, fetch func(context.Context) (string, error)) (string, error) { + return c.Cache.Set(ctx, registry, scheme, "", fetch) +} + +// fallbackCache tries the primary cache then falls back to the secondary cache. +type fallbackCache struct { + primary Cache + secondary Cache +} + +// GetScheme implements Cache. +func (fc *fallbackCache) GetScheme(ctx context.Context, registry string) (Scheme, error) { + scheme, err := fc.primary.GetScheme(ctx, registry) + if err == nil { + return scheme, nil + } + + // fallback + return fc.secondary.GetScheme(ctx, registry) +} + +// GetToken implements Cache. +func (fc *fallbackCache) GetToken(ctx context.Context, registry string, scheme Scheme, key string) (string, error) { + token, err := fc.primary.GetToken(ctx, registry, scheme, key) + if err == nil { + return token, nil + } + + // fallback + return fc.secondary.GetToken(ctx, registry, scheme, key) +} + +// Set implements Cache. +func (fc *fallbackCache) Set(ctx context.Context, registry string, scheme Scheme, key string, fetch func(context.Context) (string, error)) (string, error) { + token, err := fc.primary.Set(ctx, registry, scheme, key, fetch) + if err != nil { + return "", err + } + + return fc.secondary.Set(ctx, registry, scheme, key, func(ctx context.Context) (string, error) { + return token, nil + }) +} + +// NewSingleContextCache creates a host-based cache for optimizing the auth flow for non-compliant registries. +// It is intended to be used in a single context, such as pulling from a single repository. +// This cache should not be shared. +// +// Note: [NewCache] should be used for compliant registries as it can be shared +// across context and will generally make less re-authentication requests. +func NewSingleContextCache() Cache { + cache := NewCache() + return &fallbackCache{ + primary: cache, + // We can re-use the came concurrentCache here because the key space is different + // (keys are always empty for the hostCache) so there is no collision. + // Even if there is a collision it is not an issue. + // Re-using saves a little memory. + secondary: &hostCache{cache}, + } +} diff --git a/vendor/oras.land/oras-go/v2/registry/remote/auth/challenge.go b/vendor/oras.land/oras-go/v2/registry/remote/auth/challenge.go new file mode 100644 index 00000000..58bdefda --- /dev/null +++ b/vendor/oras.land/oras-go/v2/registry/remote/auth/challenge.go @@ -0,0 +1,167 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package auth + +import ( + "strconv" + "strings" +) + +// Scheme define the authentication method. +type Scheme byte + +const ( + // SchemeUnknown represents unknown or unsupported schemes + SchemeUnknown Scheme = iota + + // SchemeBasic represents the "Basic" HTTP authentication scheme. + // Reference: https://tools.ietf.org/html/rfc7617 + SchemeBasic + + // SchemeBearer represents the Bearer token in OAuth 2.0. + // Reference: https://tools.ietf.org/html/rfc6750 + SchemeBearer +) + +// parseScheme parse the authentication scheme from the given string +// case-insensitively. +func parseScheme(scheme string) Scheme { + switch { + case strings.EqualFold(scheme, "basic"): + return SchemeBasic + case strings.EqualFold(scheme, "bearer"): + return SchemeBearer + } + return SchemeUnknown +} + +// String return the string for the scheme. +func (s Scheme) String() string { + switch s { + case SchemeBasic: + return "Basic" + case SchemeBearer: + return "Bearer" + } + return "Unknown" +} + +// parseChallenge parses the "WWW-Authenticate" header returned by the remote +// registry, and extracts parameters if scheme is Bearer. +// References: +// - https://docs.docker.com/registry/spec/auth/token/#how-to-authenticate +// - https://tools.ietf.org/html/rfc7235#section-2.1 +func parseChallenge(header string) (scheme Scheme, params map[string]string) { + // as defined in RFC 7235 section 2.1, we have + // challenge = auth-scheme [ 1*SP ( token68 / #auth-param ) ] + // auth-scheme = token + // auth-param = token BWS "=" BWS ( token / quoted-string ) + // + // since we focus parameters only on Bearer, we have + // challenge = auth-scheme [ 1*SP #auth-param ] + schemeString, rest := parseToken(header) + scheme = parseScheme(schemeString) + + // fast path for non bearer challenge + if scheme != SchemeBearer { + return + } + + // parse params for bearer auth. + // combining RFC 7235 section 2.1 with RFC 7230 section 7, we have + // #auth-param => auth-param *( OWS "," OWS auth-param ) + var key, value string + for { + key, rest = parseToken(skipSpace(rest)) + if key == "" { + return + } + + rest = skipSpace(rest) + if rest == "" || rest[0] != '=' { + return + } + rest = skipSpace(rest[1:]) + if rest == "" { + return + } + + if rest[0] == '"' { + prefix, err := strconv.QuotedPrefix(rest) + if err != nil { + return + } + value, err = strconv.Unquote(prefix) + if err != nil { + return + } + rest = rest[len(prefix):] + } else { + value, rest = parseToken(rest) + if value == "" { + return + } + } + if params == nil { + params = map[string]string{ + key: value, + } + } else { + params[key] = value + } + + rest = skipSpace(rest) + if rest == "" || rest[0] != ',' { + return + } + rest = rest[1:] + } +} + +// isNotTokenChar reports whether rune is not a `tchar` defined in RFC 7230 +// section 3.2.6. +func isNotTokenChar(r rune) bool { + // tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" + // / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" + // / DIGIT / ALPHA + // ; any VCHAR, except delimiters + return (r < 'A' || r > 'Z') && (r < 'a' || r > 'z') && + (r < '0' || r > '9') && !strings.ContainsRune("!#$%&'*+-.^_`|~", r) +} + +// parseToken finds the next token from the given string. If no token found, +// an empty token is returned and the whole of the input is returned in rest. +// Note: Since token = 1*tchar, empty string is not a valid token. +func parseToken(s string) (token, rest string) { + if i := strings.IndexFunc(s, isNotTokenChar); i != -1 { + return s[:i], s[i:] + } + return s, "" +} + +// skipSpace skips "bad" whitespace (BWS) defined in RFC 7230 section 3.2.3. +func skipSpace(s string) string { + // OWS = *( SP / HTAB ) + // ; optional whitespace + // BWS = OWS + // ; "bad" whitespace + if i := strings.IndexFunc(s, func(r rune) bool { + return r != ' ' && r != '\t' + }); i != -1 { + return s[i:] + } + return s +} diff --git a/vendor/oras.land/oras-go/v2/registry/remote/auth/client.go b/vendor/oras.land/oras-go/v2/registry/remote/auth/client.go new file mode 100644 index 00000000..8d9685a2 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/registry/remote/auth/client.go @@ -0,0 +1,430 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package auth provides authentication for a client to a remote registry. +package auth + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "oras.land/oras-go/v2/registry/remote/internal/errutil" + "oras.land/oras-go/v2/registry/remote/retry" +) + +// ErrBasicCredentialNotFound is returned when the credential is not found for +// basic auth. +var ErrBasicCredentialNotFound = errors.New("basic credential not found") + +// DefaultClient is the default auth-decorated client. +var DefaultClient = &Client{ + Client: retry.DefaultClient, + Header: http.Header{ + "User-Agent": {"oras-go"}, + }, + Cache: DefaultCache, +} + +// maxResponseBytes specifies the default limit on how many response bytes are +// allowed in the server's response from authorization service servers. +// A typical response message from authorization service servers is around 1 to +// 4 KiB. Since the size of a token must be smaller than the HTTP header size +// limit, which is usually 16 KiB. As specified by the distribution, the +// response may contain 2 identical tokens, that is, 16 x 2 = 32 KiB. +// Hence, 128 KiB should be sufficient. +// References: https://docs.docker.com/registry/spec/auth/token/ +var maxResponseBytes int64 = 128 * 1024 // 128 KiB + +// defaultClientID specifies the default client ID used in OAuth2. +// See also ClientID. +var defaultClientID = "oras-go" + +// CredentialFunc represents a function that resolves the credential for the +// given registry (i.e. host:port). +// +// [EmptyCredential] is a valid return value and should not be considered as +// an error. +type CredentialFunc func(ctx context.Context, hostport string) (Credential, error) + +// StaticCredential specifies static credentials for the given host. +func StaticCredential(registry string, cred Credential) CredentialFunc { + if registry == "docker.io" { + // it is expected that traffic targeting "docker.io" will be redirected + // to "registry-1.docker.io" + // reference: https://github.com/moby/moby/blob/v24.0.0-beta.2/registry/config.go#L25-L48 + registry = "registry-1.docker.io" + } + return func(_ context.Context, hostport string) (Credential, error) { + if hostport == registry { + return cred, nil + } + return EmptyCredential, nil + } +} + +// Client is an auth-decorated HTTP client. +// Its zero value is a usable client that uses http.DefaultClient with no cache. +type Client struct { + // Client is the underlying HTTP client used to access the remote + // server. + // If nil, http.DefaultClient is used. + // It is possible to use the default retry client from the package + // `oras.land/oras-go/v2/registry/remote/retry`. That client is already available + // in the DefaultClient. + // It is also possible to use a custom client. For example, github.com/hashicorp/go-retryablehttp + // is a popular HTTP client that supports retries. + Client *http.Client + + // Header contains the custom headers to be added to each request. + Header http.Header + + // Credential specifies the function for resolving the credential for the + // given registry (i.e. host:port). + // EmptyCredential is a valid return value and should not be considered as + // an error. + // If nil, the credential is always resolved to EmptyCredential. + Credential CredentialFunc + + // Cache caches credentials for direct accessing the remote registry. + // If nil, no cache is used. + Cache Cache + + // ClientID used in fetching OAuth2 token as a required field. + // If empty, a default client ID is used. + // Reference: https://docs.docker.com/registry/spec/auth/oauth/#getting-a-token + ClientID string + + // ForceAttemptOAuth2 controls whether to follow OAuth2 with password grant + // instead the distribution spec when authenticating using username and + // password. + // References: + // - https://docs.docker.com/registry/spec/auth/jwt/ + // - https://docs.docker.com/registry/spec/auth/oauth/ + ForceAttemptOAuth2 bool +} + +// client returns an HTTP client used to access the remote registry. +// http.DefaultClient is return if the client is not configured. +func (c *Client) client() *http.Client { + if c.Client == nil { + return http.DefaultClient + } + return c.Client +} + +// send adds headers to the request and sends the request to the remote server. +func (c *Client) send(req *http.Request) (*http.Response, error) { + for key, values := range c.Header { + req.Header[key] = append(req.Header[key], values...) + } + return c.client().Do(req) +} + +// credential resolves the credential for the given registry. +func (c *Client) credential(ctx context.Context, reg string) (Credential, error) { + if c.Credential == nil { + return EmptyCredential, nil + } + return c.Credential(ctx, reg) +} + +// cache resolves the cache. +// noCache is return if the cache is not configured. +func (c *Client) cache() Cache { + if c.Cache == nil { + return noCache{} + } + return c.Cache +} + +// SetUserAgent sets the user agent for all out-going requests. +func (c *Client) SetUserAgent(userAgent string) { + if c.Header == nil { + c.Header = http.Header{} + } + c.Header.Set("User-Agent", userAgent) +} + +// Do sends the request to the remote server, attempting to resolve +// authentication if 'Authorization' header is not set. +// +// On authentication failure due to bad credential, +// - Do returns error if it fails to fetch token for bearer auth. +// - Do returns the registry response without error for basic auth. +func (c *Client) Do(originalReq *http.Request) (*http.Response, error) { + if auth := originalReq.Header.Get("Authorization"); auth != "" { + return c.send(originalReq) + } + + ctx := originalReq.Context() + req := originalReq.Clone(ctx) + + // attempt cached auth token + var attemptedKey string + cache := c.cache() + host := originalReq.Host + scheme, err := cache.GetScheme(ctx, host) + if err == nil { + switch scheme { + case SchemeBasic: + token, err := cache.GetToken(ctx, host, SchemeBasic, "") + if err == nil { + req.Header.Set("Authorization", "Basic "+token) + } + case SchemeBearer: + scopes := GetAllScopesForHost(ctx, host) + attemptedKey = strings.Join(scopes, " ") + token, err := cache.GetToken(ctx, host, SchemeBearer, attemptedKey) + if err == nil { + req.Header.Set("Authorization", "Bearer "+token) + } + } + } + + resp, err := c.send(req) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusUnauthorized { + return resp, nil + } + + // attempt again with credentials for recognized schemes + challenge := resp.Header.Get("Www-Authenticate") + scheme, params := parseChallenge(challenge) + switch scheme { + case SchemeBasic: + resp.Body.Close() + + token, err := cache.Set(ctx, host, SchemeBasic, "", func(ctx context.Context) (string, error) { + return c.fetchBasicAuth(ctx, host) + }) + if err != nil { + return nil, fmt.Errorf("%s %q: %w", resp.Request.Method, resp.Request.URL, err) + } + + req = originalReq.Clone(ctx) + req.Header.Set("Authorization", "Basic "+token) + case SchemeBearer: + resp.Body.Close() + + scopes := GetAllScopesForHost(ctx, host) + if paramScope := params["scope"]; paramScope != "" { + // merge hinted scopes with challenged scopes + scopes = append(scopes, strings.Split(paramScope, " ")...) + scopes = CleanScopes(scopes) + } + key := strings.Join(scopes, " ") + + // attempt the cache again if there is a scope change + if key != attemptedKey { + if token, err := cache.GetToken(ctx, host, SchemeBearer, key); err == nil { + req = originalReq.Clone(ctx) + req.Header.Set("Authorization", "Bearer "+token) + if err := rewindRequestBody(req); err != nil { + return nil, err + } + + resp, err := c.send(req) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusUnauthorized { + return resp, nil + } + resp.Body.Close() + } + } + + // attempt with credentials + realm := params["realm"] + service := params["service"] + token, err := cache.Set(ctx, host, SchemeBearer, key, func(ctx context.Context) (string, error) { + return c.fetchBearerToken(ctx, host, realm, service, scopes) + }) + if err != nil { + return nil, fmt.Errorf("%s %q: %w", resp.Request.Method, resp.Request.URL, err) + } + + req = originalReq.Clone(ctx) + req.Header.Set("Authorization", "Bearer "+token) + default: + return resp, nil + } + if err := rewindRequestBody(req); err != nil { + return nil, err + } + + return c.send(req) +} + +// fetchBasicAuth fetches a basic auth token for the basic challenge. +func (c *Client) fetchBasicAuth(ctx context.Context, registry string) (string, error) { + cred, err := c.credential(ctx, registry) + if err != nil { + return "", fmt.Errorf("failed to resolve credential: %w", err) + } + if cred == EmptyCredential { + return "", ErrBasicCredentialNotFound + } + if cred.Username == "" || cred.Password == "" { + return "", errors.New("missing username or password for basic auth") + } + auth := cred.Username + ":" + cred.Password + return base64.StdEncoding.EncodeToString([]byte(auth)), nil +} + +// fetchBearerToken fetches an access token for the bearer challenge. +func (c *Client) fetchBearerToken(ctx context.Context, registry, realm, service string, scopes []string) (string, error) { + cred, err := c.credential(ctx, registry) + if err != nil { + return "", err + } + if cred.AccessToken != "" { + return cred.AccessToken, nil + } + if cred == EmptyCredential || (cred.RefreshToken == "" && !c.ForceAttemptOAuth2) { + return c.fetchDistributionToken(ctx, realm, service, scopes, cred.Username, cred.Password) + } + return c.fetchOAuth2Token(ctx, realm, service, scopes, cred) +} + +// fetchDistributionToken fetches an access token as defined by the distribution +// specification. +// It fetches anonymous tokens if no credential is provided. +// References: +// - https://docs.docker.com/registry/spec/auth/jwt/ +// - https://docs.docker.com/registry/spec/auth/token/ +func (c *Client) fetchDistributionToken(ctx context.Context, realm, service string, scopes []string, username, password string) (string, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, realm, nil) + if err != nil { + return "", err + } + if username != "" || password != "" { + req.SetBasicAuth(username, password) + } + q := req.URL.Query() + if service != "" { + q.Set("service", service) + } + for _, scope := range scopes { + q.Add("scope", scope) + } + req.URL.RawQuery = q.Encode() + + resp, err := c.send(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", errutil.ParseErrorResponse(resp) + } + + // As specified in https://docs.docker.com/registry/spec/auth/token/ section + // "Token Response Fields", the token is either in `token` or + // `access_token`. If both present, they are identical. + var result struct { + Token string `json:"token"` + AccessToken string `json:"access_token"` + } + lr := io.LimitReader(resp.Body, maxResponseBytes) + if err := json.NewDecoder(lr).Decode(&result); err != nil { + return "", fmt.Errorf("%s %q: failed to decode response: %w", resp.Request.Method, resp.Request.URL, err) + } + if result.AccessToken != "" { + return result.AccessToken, nil + } + if result.Token != "" { + return result.Token, nil + } + return "", fmt.Errorf("%s %q: empty token returned", resp.Request.Method, resp.Request.URL) +} + +// fetchOAuth2Token fetches an OAuth2 access token. +// Reference: https://docs.docker.com/registry/spec/auth/oauth/ +func (c *Client) fetchOAuth2Token(ctx context.Context, realm, service string, scopes []string, cred Credential) (string, error) { + form := url.Values{} + if cred.RefreshToken != "" { + form.Set("grant_type", "refresh_token") + form.Set("refresh_token", cred.RefreshToken) + } else if cred.Username != "" && cred.Password != "" { + form.Set("grant_type", "password") + form.Set("username", cred.Username) + form.Set("password", cred.Password) + } else { + return "", errors.New("missing username or password for bearer auth") + } + form.Set("service", service) + clientID := c.ClientID + if clientID == "" { + clientID = defaultClientID + } + form.Set("client_id", clientID) + if len(scopes) != 0 { + form.Set("scope", strings.Join(scopes, " ")) + } + body := strings.NewReader(form.Encode()) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, realm, body) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := c.send(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", errutil.ParseErrorResponse(resp) + } + + var result struct { + AccessToken string `json:"access_token"` + } + lr := io.LimitReader(resp.Body, maxResponseBytes) + if err := json.NewDecoder(lr).Decode(&result); err != nil { + return "", fmt.Errorf("%s %q: failed to decode response: %w", resp.Request.Method, resp.Request.URL, err) + } + if result.AccessToken != "" { + return result.AccessToken, nil + } + return "", fmt.Errorf("%s %q: empty token returned", resp.Request.Method, resp.Request.URL) +} + +// rewindRequestBody tries to rewind the request body if exists. +func rewindRequestBody(req *http.Request) error { + if req.Body == nil || req.Body == http.NoBody { + return nil + } + if req.GetBody == nil { + return fmt.Errorf("%s %q: request body is not rewindable", req.Method, req.URL) + } + body, err := req.GetBody() + if err != nil { + return fmt.Errorf("%s %q: failed to get request body: %w", req.Method, req.URL, err) + } + req.Body = body + return nil +} diff --git a/vendor/oras.land/oras-go/v2/registry/remote/auth/credential.go b/vendor/oras.land/oras-go/v2/registry/remote/auth/credential.go new file mode 100644 index 00000000..013305f7 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/registry/remote/auth/credential.go @@ -0,0 +1,40 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package auth + +// EmptyCredential represents an empty credential. +var EmptyCredential Credential + +// Credential contains authentication credentials used to access remote +// registries. +type Credential struct { + // Username is the name of the user for the remote registry. + Username string + + // Password is the secret associated with the username. + Password string + + // RefreshToken is a bearer token to be sent to the authorization service + // for fetching access tokens. + // A refresh token is often referred as an identity token. + // Reference: https://docs.docker.com/registry/spec/auth/oauth/ + RefreshToken string + + // AccessToken is a bearer token to be sent to the registry. + // An access token is often referred as a registry token. + // Reference: https://docs.docker.com/registry/spec/auth/token/ + AccessToken string +} diff --git a/vendor/oras.land/oras-go/v2/registry/remote/auth/scope.go b/vendor/oras.land/oras-go/v2/registry/remote/auth/scope.go new file mode 100644 index 00000000..d81cc0d4 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/registry/remote/auth/scope.go @@ -0,0 +1,325 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package auth + +import ( + "context" + "slices" + "strings" + + "oras.land/oras-go/v2/registry" +) + +// Actions used in scopes. +// Reference: https://docs.docker.com/registry/spec/auth/scope/ +const ( + // ActionPull represents generic read access for resources of the repository + // type. + ActionPull = "pull" + + // ActionPush represents generic write access for resources of the + // repository type. + ActionPush = "push" + + // ActionDelete represents the delete permission for resources of the + // repository type. + ActionDelete = "delete" +) + +// ScopeRegistryCatalog is the scope for registry catalog access. +const ScopeRegistryCatalog = "registry:catalog:*" + +// ScopeRepository returns a repository scope with given actions. +// Reference: https://docs.docker.com/registry/spec/auth/scope/ +func ScopeRepository(repository string, actions ...string) string { + actions = cleanActions(actions) + if repository == "" || len(actions) == 0 { + return "" + } + return strings.Join([]string{ + "repository", + repository, + strings.Join(actions, ","), + }, ":") +} + +// AppendRepositoryScope returns a new context containing scope hints for the +// auth client to fetch bearer tokens with the given actions on the repository. +// If called multiple times, the new scopes will be appended to the existing +// scopes. The resulted scopes are de-duplicated. +// +// For example, uploading blob to the repository "hello-world" does HEAD request +// first then POST and PUT. The HEAD request will return a challenge for scope +// `repository:hello-world:pull`, and the auth client will fetch a token for +// that challenge. Later, the POST request will return a challenge for scope +// `repository:hello-world:push`, and the auth client will fetch a token for +// that challenge again. By invoking AppendRepositoryScope with the actions +// [ActionPull] and [ActionPush] for the repository `hello-world`, +// the auth client with cache is hinted to fetch a token via a single token +// fetch request for all the HEAD, POST, PUT requests. +func AppendRepositoryScope(ctx context.Context, ref registry.Reference, actions ...string) context.Context { + if len(actions) == 0 { + return ctx + } + scope := ScopeRepository(ref.Repository, actions...) + return AppendScopesForHost(ctx, ref.Host(), scope) +} + +// scopesContextKey is the context key for scopes. +type scopesContextKey struct{} + +// WithScopes returns a context with scopes added. Scopes are de-duplicated. +// Scopes are used as hints for the auth client to fetch bearer tokens with +// larger scopes. +// +// For example, uploading blob to the repository "hello-world" does HEAD request +// first then POST and PUT. The HEAD request will return a challenge for scope +// `repository:hello-world:pull`, and the auth client will fetch a token for +// that challenge. Later, the POST request will return a challenge for scope +// `repository:hello-world:push`, and the auth client will fetch a token for +// that challenge again. By invoking WithScopes with the scope +// `repository:hello-world:pull,push`, the auth client with cache is hinted to +// fetch a token via a single token fetch request for all the HEAD, POST, PUT +// requests. +// +// Passing an empty list of scopes will virtually remove the scope hints in the +// context. +// +// Reference: https://docs.docker.com/registry/spec/auth/scope/ +func WithScopes(ctx context.Context, scopes ...string) context.Context { + scopes = CleanScopes(scopes) + return context.WithValue(ctx, scopesContextKey{}, scopes) +} + +// AppendScopes appends additional scopes to the existing scopes in the context +// and returns a new context. The resulted scopes are de-duplicated. +// The append operation does modify the existing scope in the context passed in. +func AppendScopes(ctx context.Context, scopes ...string) context.Context { + if len(scopes) == 0 { + return ctx + } + return WithScopes(ctx, append(GetScopes(ctx), scopes...)...) +} + +// GetScopes returns the scopes in the context. +func GetScopes(ctx context.Context) []string { + if scopes, ok := ctx.Value(scopesContextKey{}).([]string); ok { + return slices.Clone(scopes) + } + return nil +} + +// scopesForHostContextKey is the context key for per-host scopes. +type scopesForHostContextKey string + +// WithScopesForHost returns a context with per-host scopes added. +// Scopes are de-duplicated. +// Scopes are used as hints for the auth client to fetch bearer tokens with +// larger scopes. +// +// For example, uploading blob to the repository "hello-world" does HEAD request +// first then POST and PUT. The HEAD request will return a challenge for scope +// `repository:hello-world:pull`, and the auth client will fetch a token for +// that challenge. Later, the POST request will return a challenge for scope +// `repository:hello-world:push`, and the auth client will fetch a token for +// that challenge again. By invoking WithScopesForHost with the scope +// `repository:hello-world:pull,push`, the auth client with cache is hinted to +// fetch a token via a single token fetch request for all the HEAD, POST, PUT +// requests. +// +// Passing an empty list of scopes will virtually remove the scope hints in the +// context for the given host. +// +// Reference: https://docs.docker.com/registry/spec/auth/scope/ +func WithScopesForHost(ctx context.Context, host string, scopes ...string) context.Context { + scopes = CleanScopes(scopes) + return context.WithValue(ctx, scopesForHostContextKey(host), scopes) +} + +// AppendScopesForHost appends additional scopes to the existing scopes +// in the context for the given host and returns a new context. +// The resulted scopes are de-duplicated. +// The append operation does modify the existing scope in the context passed in. +func AppendScopesForHost(ctx context.Context, host string, scopes ...string) context.Context { + if len(scopes) == 0 { + return ctx + } + oldScopes := GetScopesForHost(ctx, host) + return WithScopesForHost(ctx, host, append(oldScopes, scopes...)...) +} + +// GetScopesForHost returns the scopes in the context for the given host, +// excluding global scopes added by [WithScopes] and [AppendScopes]. +func GetScopesForHost(ctx context.Context, host string) []string { + if scopes, ok := ctx.Value(scopesForHostContextKey(host)).([]string); ok { + return slices.Clone(scopes) + } + return nil +} + +// GetAllScopesForHost returns the scopes in the context for the given host, +// including global scopes added by [WithScopes] and [AppendScopes]. +func GetAllScopesForHost(ctx context.Context, host string) []string { + scopes := GetScopesForHost(ctx, host) + globalScopes := GetScopes(ctx) + + if len(scopes) == 0 { + return globalScopes + } + if len(globalScopes) == 0 { + return scopes + } + // re-clean the scopes + allScopes := append(scopes, globalScopes...) + return CleanScopes(allScopes) +} + +// CleanScopes merges and sort the actions in ascending order if the scopes have +// the same resource type and name. The final scopes are sorted in ascending +// order. In other words, the scopes passed in are de-duplicated and sorted. +// Therefore, the output of this function is deterministic. +// +// If there is a wildcard `*` in the action, other actions in the same resource +// type and name are ignored. +func CleanScopes(scopes []string) []string { + // fast paths + switch len(scopes) { + case 0: + return nil + case 1: + scope := scopes[0] + i := strings.LastIndex(scope, ":") + if i == -1 { + return []string{scope} + } + actionList := strings.Split(scope[i+1:], ",") + actionList = cleanActions(actionList) + if len(actionList) == 0 { + return nil + } + actions := strings.Join(actionList, ",") + scope = scope[:i+1] + actions + return []string{scope} + } + + // slow path + var result []string + + // merge recognizable scopes + resourceTypes := make(map[string]map[string]map[string]struct{}) + for _, scope := range scopes { + // extract resource type + i := strings.Index(scope, ":") + if i == -1 { + result = append(result, scope) + continue + } + resourceType := scope[:i] + + // extract resource name and actions + rest := scope[i+1:] + i = strings.LastIndex(rest, ":") + if i == -1 { + result = append(result, scope) + continue + } + resourceName := rest[:i] + actions := rest[i+1:] + if actions == "" { + // drop scope since no action found + continue + } + + // add to the intermediate map for de-duplication + namedActions := resourceTypes[resourceType] + if namedActions == nil { + namedActions = make(map[string]map[string]struct{}) + resourceTypes[resourceType] = namedActions + } + actionSet := namedActions[resourceName] + if actionSet == nil { + actionSet = make(map[string]struct{}) + namedActions[resourceName] = actionSet + } + for _, action := range strings.Split(actions, ",") { + if action != "" { + actionSet[action] = struct{}{} + } + } + } + + // reconstruct scopes + for resourceType, namedActions := range resourceTypes { + for resourceName, actionSet := range namedActions { + if len(actionSet) == 0 { + continue + } + var actions []string + for action := range actionSet { + if action == "*" { + actions = []string{"*"} + break + } + actions = append(actions, action) + } + slices.Sort(actions) + scope := resourceType + ":" + resourceName + ":" + strings.Join(actions, ",") + result = append(result, scope) + } + } + + // sort and return + slices.Sort(result) + return result +} + +// cleanActions removes the duplicated actions and sort in ascending order. +// If there is a wildcard `*` in the action, other actions are ignored. +func cleanActions(actions []string) []string { + // fast paths + switch len(actions) { + case 0: + return nil + case 1: + if actions[0] == "" { + return nil + } + return actions + } + + // slow path + slices.Sort(actions) + n := 0 + for i := 0; i < len(actions); i++ { + if actions[i] == "*" { + return []string{"*"} + } + if actions[i] != actions[n] { + n++ + if n != i { + actions[n] = actions[i] + } + } + } + n++ + if actions[0] == "" { + if n == 1 { + return nil + } + return actions[1:n] + } + return actions[:n] +} diff --git a/vendor/oras.land/oras-go/v2/registry/remote/errcode/errors.go b/vendor/oras.land/oras-go/v2/registry/remote/errcode/errors.go new file mode 100644 index 00000000..9f87d86d --- /dev/null +++ b/vendor/oras.land/oras-go/v2/registry/remote/errcode/errors.go @@ -0,0 +1,128 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package errcode + +import ( + "fmt" + "net/http" + "net/url" + "strings" + "unicode" +) + +// References: +// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#error-codes +// - https://docs.docker.com/registry/spec/api/#errors-2 +const ( + ErrorCodeBlobUnknown = "BLOB_UNKNOWN" + ErrorCodeBlobUploadInvalid = "BLOB_UPLOAD_INVALID" + ErrorCodeBlobUploadUnknown = "BLOB_UPLOAD_UNKNOWN" + ErrorCodeDigestInvalid = "DIGEST_INVALID" + ErrorCodeManifestBlobUnknown = "MANIFEST_BLOB_UNKNOWN" + ErrorCodeManifestInvalid = "MANIFEST_INVALID" + ErrorCodeManifestUnknown = "MANIFEST_UNKNOWN" + ErrorCodeNameInvalid = "NAME_INVALID" + ErrorCodeNameUnknown = "NAME_UNKNOWN" + ErrorCodeSizeInvalid = "SIZE_INVALID" + ErrorCodeUnauthorized = "UNAUTHORIZED" + ErrorCodeDenied = "DENIED" + ErrorCodeUnsupported = "UNSUPPORTED" +) + +// Error represents a response inner error returned by the remote +// registry. +// References: +// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#error-codes +// - https://docs.docker.com/registry/spec/api/#errors-2 +type Error struct { + Code string `json:"code"` + Message string `json:"message"` + Detail any `json:"detail,omitempty"` +} + +// Error returns a error string describing the error. +func (e Error) Error() string { + code := strings.Map(func(r rune) rune { + if r == '_' { + return ' ' + } + return unicode.ToLower(r) + }, e.Code) + if e.Message == "" { + return code + } + if e.Detail == nil { + return fmt.Sprintf("%s: %s", code, e.Message) + } + return fmt.Sprintf("%s: %s: %v", code, e.Message, e.Detail) +} + +// Errors represents a list of response inner errors returned by the remote +// server. +// References: +// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#error-codes +// - https://docs.docker.com/registry/spec/api/#errors-2 +type Errors []Error + +// Error returns a error string describing the error. +func (errs Errors) Error() string { + switch len(errs) { + case 0: + return "" + case 1: + return errs[0].Error() + } + var errmsgs []string + for _, err := range errs { + errmsgs = append(errmsgs, err.Error()) + } + return strings.Join(errmsgs, "; ") +} + +// Unwrap returns the inner error only when there is exactly one error. +func (errs Errors) Unwrap() error { + if len(errs) == 1 { + return errs[0] + } + return nil +} + +// ErrorResponse represents an error response. +type ErrorResponse struct { + Method string + URL *url.URL + StatusCode int + Errors Errors +} + +// Error returns a error string describing the error. +func (err *ErrorResponse) Error() string { + var errmsg string + if len(err.Errors) > 0 { + errmsg = err.Errors.Error() + } else { + errmsg = http.StatusText(err.StatusCode) + } + return fmt.Sprintf("%s %q: response status code %d: %s", err.Method, err.URL, err.StatusCode, errmsg) +} + +// Unwrap returns the internal errors of err if any. +func (err *ErrorResponse) Unwrap() error { + if len(err.Errors) == 0 { + return nil + } + return err.Errors +} diff --git a/vendor/oras.land/oras-go/v2/registry/remote/internal/errutil/errutil.go b/vendor/oras.land/oras-go/v2/registry/remote/internal/errutil/errutil.go new file mode 100644 index 00000000..52dc3612 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/registry/remote/internal/errutil/errutil.go @@ -0,0 +1,54 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package errutil + +import ( + "encoding/json" + "errors" + "io" + "net/http" + + "oras.land/oras-go/v2/registry/remote/errcode" +) + +// maxErrorBytes specifies the default limit on how many response bytes are +// allowed in the server's error response. +// A typical error message is around 200 bytes. Hence, 8 KiB should be +// sufficient. +const maxErrorBytes int64 = 8 * 1024 // 8 KiB + +// ParseErrorResponse parses the error returned by the remote registry. +func ParseErrorResponse(resp *http.Response) error { + resultErr := &errcode.ErrorResponse{ + Method: resp.Request.Method, + URL: resp.Request.URL, + StatusCode: resp.StatusCode, + } + var body struct { + Errors errcode.Errors `json:"errors"` + } + lr := io.LimitReader(resp.Body, maxErrorBytes) + if err := json.NewDecoder(lr).Decode(&body); err == nil { + resultErr.Errors = body.Errors + } + return resultErr +} + +// IsErrorCode returns true if err is an Error and its Code equals to code. +func IsErrorCode(err error, code string) bool { + var ec errcode.Error + return errors.As(err, &ec) && ec.Code == code +} diff --git a/vendor/oras.land/oras-go/v2/registry/remote/manifest.go b/vendor/oras.land/oras-go/v2/registry/remote/manifest.go new file mode 100644 index 00000000..0e10297c --- /dev/null +++ b/vendor/oras.land/oras-go/v2/registry/remote/manifest.go @@ -0,0 +1,59 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package remote + +import ( + "strings" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/internal/docker" + "oras.land/oras-go/v2/internal/spec" +) + +// defaultManifestMediaTypes contains the default set of manifests media types. +var defaultManifestMediaTypes = []string{ + docker.MediaTypeManifest, + docker.MediaTypeManifestList, + ocispec.MediaTypeImageManifest, + ocispec.MediaTypeImageIndex, + spec.MediaTypeArtifactManifest, +} + +// defaultManifestAcceptHeader is the default set in the `Accept` header for +// resolving manifests from tags. +var defaultManifestAcceptHeader = strings.Join(defaultManifestMediaTypes, ", ") + +// isManifest determines if the given descriptor points to a manifest. +func isManifest(manifestMediaTypes []string, desc ocispec.Descriptor) bool { + if len(manifestMediaTypes) == 0 { + manifestMediaTypes = defaultManifestMediaTypes + } + for _, mediaType := range manifestMediaTypes { + if desc.MediaType == mediaType { + return true + } + } + return false +} + +// manifestAcceptHeader generates the set in the `Accept` header for resolving +// manifests from tags. +func manifestAcceptHeader(manifestMediaTypes []string) string { + if len(manifestMediaTypes) == 0 { + return defaultManifestAcceptHeader + } + return strings.Join(manifestMediaTypes, ", ") +} diff --git a/vendor/oras.land/oras-go/v2/registry/remote/referrers.go b/vendor/oras.land/oras-go/v2/registry/remote/referrers.go new file mode 100644 index 00000000..74668089 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/registry/remote/referrers.go @@ -0,0 +1,221 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package remote + +import ( + "errors" + "strings" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/content" + "oras.land/oras-go/v2/internal/descriptor" +) + +// zeroDigest represents a digest that consists of zeros. zeroDigest is used +// for pinging Referrers API. +const zeroDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000" + +// referrersState represents the state of Referrers API. +type referrersState = int32 + +const ( + // referrersStateUnknown represents an unknown state of Referrers API. + referrersStateUnknown referrersState = iota + // referrersStateSupported represents that the repository is known to + // support Referrers API. + referrersStateSupported + // referrersStateUnsupported represents that the repository is known to + // not support Referrers API. + referrersStateUnsupported +) + +// referrerOperation represents an operation on a referrer. +type referrerOperation = int32 + +const ( + // referrerOperationAdd represents an addition operation on a referrer. + referrerOperationAdd referrerOperation = iota + // referrerOperationRemove represents a removal operation on a referrer. + referrerOperationRemove +) + +// referrerChange represents a change on a referrer. +type referrerChange struct { + referrer ocispec.Descriptor + operation referrerOperation +} + +var ( + // ErrReferrersCapabilityAlreadySet is returned by SetReferrersCapability() + // when the Referrers API capability has been already set. + ErrReferrersCapabilityAlreadySet = errors.New("referrers capability cannot be changed once set") + + // errNoReferrerUpdate is returned by applyReferrerChanges() when there + // is no any referrer update. + errNoReferrerUpdate = errors.New("no referrer update") +) + +const ( + // opDeleteReferrersIndex represents the operation for deleting a + // referrers index. + opDeleteReferrersIndex = "DeleteReferrersIndex" +) + +// ReferrersError records an error and the operation and the subject descriptor. +type ReferrersError struct { + // Op represents the failing operation. + Op string + // Subject is the descriptor of referenced artifact. + Subject ocispec.Descriptor + // Err is the entity of referrers error. + Err error +} + +// Error returns error msg of IgnorableError. +func (e *ReferrersError) Error() string { + return e.Err.Error() +} + +// Unwrap returns the inner error of IgnorableError. +func (e *ReferrersError) Unwrap() error { + return errors.Unwrap(e.Err) +} + +// IsIndexDelete tells if e is kind of error related to referrers +// index deletion. +func (e *ReferrersError) IsReferrersIndexDelete() bool { + return e.Op == opDeleteReferrersIndex +} + +// buildReferrersTag builds the referrers tag for the given manifest descriptor. +// Format: - +// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#unavailable-referrers-api +func buildReferrersTag(desc ocispec.Descriptor) string { + alg := desc.Digest.Algorithm().String() + encoded := desc.Digest.Encoded() + return alg + "-" + encoded +} + +// isReferrersFilterApplied checks if requsted is in the applied filter list. +func isReferrersFilterApplied(applied, requested string) bool { + if applied == "" || requested == "" { + return false + } + filters := strings.Split(applied, ",") + for _, f := range filters { + if f == requested { + return true + } + } + return false +} + +// filterReferrers filters a slice of referrers by artifactType in place. +// The returned slice contains matching referrers. +func filterReferrers(refs []ocispec.Descriptor, artifactType string) []ocispec.Descriptor { + if artifactType == "" { + return refs + } + var j int + for i, ref := range refs { + if ref.ArtifactType == artifactType { + if i != j { + refs[j] = ref + } + j++ + } + } + return refs[:j] +} + +// applyReferrerChanges applies referrerChanges on referrers and returns the +// updated referrers. +// Returns errNoReferrerUpdate if there is no any referrers updates. +func applyReferrerChanges(referrers []ocispec.Descriptor, referrerChanges []referrerChange) ([]ocispec.Descriptor, error) { + referrersMap := make(map[descriptor.Descriptor]int, len(referrers)+len(referrerChanges)) + updatedReferrers := make([]ocispec.Descriptor, 0, len(referrers)+len(referrerChanges)) + var updateRequired bool + for _, r := range referrers { + if content.Equal(r, ocispec.Descriptor{}) { + // skip bad entry + updateRequired = true + continue + } + key := descriptor.FromOCI(r) + if _, ok := referrersMap[key]; ok { + // skip duplicates + updateRequired = true + continue + } + updatedReferrers = append(updatedReferrers, r) + referrersMap[key] = len(updatedReferrers) - 1 + } + + // apply changes + for _, change := range referrerChanges { + key := descriptor.FromOCI(change.referrer) + switch change.operation { + case referrerOperationAdd: + if _, ok := referrersMap[key]; !ok { + // add distinct referrers + updatedReferrers = append(updatedReferrers, change.referrer) + referrersMap[key] = len(updatedReferrers) - 1 + } + case referrerOperationRemove: + if pos, ok := referrersMap[key]; ok { + // remove referrers that are already in the map + updatedReferrers[pos] = ocispec.Descriptor{} + delete(referrersMap, key) + } + } + } + + // skip unnecessary update + if !updateRequired && len(referrersMap) == len(referrers) { + // if the result referrer map contains the same content as the + // original referrers, consider that there is no update on the + // referrers. + for _, r := range referrers { + key := descriptor.FromOCI(r) + if _, ok := referrersMap[key]; !ok { + updateRequired = true + } + } + if !updateRequired { + return nil, errNoReferrerUpdate + } + } + + return removeEmptyDescriptors(updatedReferrers, len(referrersMap)), nil +} + +// removeEmptyDescriptors in-place removes empty items from descs, given a hint +// of the number of non-empty descriptors. +func removeEmptyDescriptors(descs []ocispec.Descriptor, hint int) []ocispec.Descriptor { + j := 0 + for i, r := range descs { + if !content.Equal(r, ocispec.Descriptor{}) { + if i > j { + descs[j] = r + } + j++ + } + if j == hint { + break + } + } + return descs[:j] +} diff --git a/vendor/oras.land/oras-go/v2/registry/remote/registry.go b/vendor/oras.land/oras-go/v2/registry/remote/registry.go new file mode 100644 index 00000000..1099b585 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/registry/remote/registry.go @@ -0,0 +1,190 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package remote provides a client to the remote registry. +// Reference: https://github.com/distribution/distribution +package remote + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strconv" + + "oras.land/oras-go/v2/errdef" + "oras.land/oras-go/v2/registry" + "oras.land/oras-go/v2/registry/remote/auth" + "oras.land/oras-go/v2/registry/remote/internal/errutil" +) + +// RepositoryOptions is an alias of Repository to avoid name conflicts. +// It also hides all methods associated with Repository. +type RepositoryOptions Repository + +// Registry is an HTTP client to a remote registry. +type Registry struct { + // RepositoryOptions contains common options for Registry and Repository. + // It is also used as a template for derived repositories. + RepositoryOptions + + // RepositoryListPageSize specifies the page size when invoking the catalog + // API. + // If zero, the page size is determined by the remote registry. + // Reference: https://docs.docker.com/registry/spec/api/#catalog + RepositoryListPageSize int +} + +// NewRegistry creates a client to the remote registry with the specified domain +// name. +// Example: localhost:5000 +func NewRegistry(name string) (*Registry, error) { + ref := registry.Reference{ + Registry: name, + } + if err := ref.ValidateRegistry(); err != nil { + return nil, err + } + return &Registry{ + RepositoryOptions: RepositoryOptions{ + Reference: ref, + }, + }, nil +} + +// client returns an HTTP client used to access the remote registry. +// A default HTTP client is return if the client is not configured. +func (r *Registry) client() Client { + if r.Client == nil { + return auth.DefaultClient + } + return r.Client +} + +// do sends an HTTP request and returns an HTTP response using the HTTP client +// returned by r.client(). +func (r *Registry) do(req *http.Request) (*http.Response, error) { + if r.HandleWarning == nil { + return r.client().Do(req) + } + + resp, err := r.client().Do(req) + if err != nil { + return nil, err + } + handleWarningHeaders(resp.Header.Values(headerWarning), r.HandleWarning) + return resp, nil +} + +// Ping checks whether or not the registry implement Docker Registry API V2 or +// OCI Distribution Specification. +// Ping can be used to check authentication when an auth client is configured. +// +// References: +// - https://docs.docker.com/registry/spec/api/#base +// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#api +func (r *Registry) Ping(ctx context.Context) error { + url := buildRegistryBaseURL(r.PlainHTTP, r.Reference) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return err + } + + resp, err := r.do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + switch resp.StatusCode { + case http.StatusOK: + return nil + case http.StatusNotFound: + return errdef.ErrNotFound + default: + return errutil.ParseErrorResponse(resp) + } +} + +// Repositories lists the name of repositories available in the registry. +// See also `RepositoryListPageSize`. +// +// If `last` is NOT empty, the entries in the response start after the +// repo specified by `last`. Otherwise, the response starts from the top +// of the Repositories list. +// +// Reference: https://docs.docker.com/registry/spec/api/#catalog +func (r *Registry) Repositories(ctx context.Context, last string, fn func(repos []string) error) error { + ctx = auth.AppendScopesForHost(ctx, r.Reference.Host(), auth.ScopeRegistryCatalog) + url := buildRegistryCatalogURL(r.PlainHTTP, r.Reference) + var err error + for err == nil { + url, err = r.repositories(ctx, last, fn, url) + // clear `last` for subsequent pages + last = "" + } + if err != errNoLink { + return err + } + return nil +} + +// repositories returns a single page of repository list with the next link. +func (r *Registry) repositories(ctx context.Context, last string, fn func(repos []string) error, url string) (string, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return "", err + } + if r.RepositoryListPageSize > 0 || last != "" { + q := req.URL.Query() + if r.RepositoryListPageSize > 0 { + q.Set("n", strconv.Itoa(r.RepositoryListPageSize)) + } + if last != "" { + q.Set("last", last) + } + req.URL.RawQuery = q.Encode() + } + resp, err := r.do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", errutil.ParseErrorResponse(resp) + } + var page struct { + Repositories []string `json:"repositories"` + } + lr := limitReader(resp.Body, r.MaxMetadataBytes) + if err := json.NewDecoder(lr).Decode(&page); err != nil { + return "", fmt.Errorf("%s %q: failed to decode response: %w", resp.Request.Method, resp.Request.URL, err) + } + if err := fn(page.Repositories); err != nil { + return "", err + } + + return parseLink(resp) +} + +// Repository returns a repository reference by the given name. +func (r *Registry) Repository(ctx context.Context, name string) (registry.Repository, error) { + ref := registry.Reference{ + Registry: r.Reference.Registry, + Repository: name, + } + return newRepositoryWithOptions(ref, &r.RepositoryOptions) +} diff --git a/vendor/oras.land/oras-go/v2/registry/remote/repository.go b/vendor/oras.land/oras-go/v2/registry/remote/repository.go new file mode 100644 index 00000000..7c36dc1c --- /dev/null +++ b/vendor/oras.land/oras-go/v2/registry/remote/repository.go @@ -0,0 +1,1667 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package remote + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "mime" + "net/http" + "slices" + "strconv" + "strings" + "sync" + "sync/atomic" + + "github.com/opencontainers/go-digest" + specs "github.com/opencontainers/image-spec/specs-go" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/content" + "oras.land/oras-go/v2/errdef" + "oras.land/oras-go/v2/internal/cas" + "oras.land/oras-go/v2/internal/httputil" + "oras.land/oras-go/v2/internal/ioutil" + "oras.land/oras-go/v2/internal/spec" + "oras.land/oras-go/v2/internal/syncutil" + "oras.land/oras-go/v2/registry" + "oras.land/oras-go/v2/registry/remote/auth" + "oras.land/oras-go/v2/registry/remote/errcode" + "oras.land/oras-go/v2/registry/remote/internal/errutil" +) + +const ( + // headerDockerContentDigest is the "Docker-Content-Digest" header. + // If present on the response, it contains the canonical digest of the + // uploaded blob. + // + // References: + // - https://docs.docker.com/registry/spec/api/#digest-header + // - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#pull + headerDockerContentDigest = "Docker-Content-Digest" + + // headerOCIFiltersApplied is the "OCI-Filters-Applied" header. + // If present on the response, it contains a comma-separated list of the + // applied filters. + // + // Reference: + // - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers + headerOCIFiltersApplied = "OCI-Filters-Applied" + + // headerOCISubject is the "OCI-Subject" header. + // If present on the response, it contains the digest of the subject, + // indicating that Referrers API is supported by the registry. + headerOCISubject = "OCI-Subject" +) + +// filterTypeArtifactType is the "artifactType" filter applied on the list of +// referrers. +// +// References: +// - Latest spec: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers +// - Compatible spec: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#listing-referrers +const filterTypeArtifactType = "artifactType" + +// Client is an interface for a HTTP client. +type Client interface { + // Do sends an HTTP request and returns an HTTP response. + // + // Unlike http.RoundTripper, Client can attempt to interpret the response + // and handle higher-level protocol details such as redirects and + // authentication. + // + // Like http.RoundTripper, Client should not modify the request, and must + // always close the request body. + Do(*http.Request) (*http.Response, error) +} + +// Repository is an HTTP client to a remote repository. +type Repository struct { + // Client is the underlying HTTP client used to access the remote registry. + // If nil, auth.DefaultClient is used. + Client Client + + // Reference references the remote repository. + Reference registry.Reference + + // PlainHTTP signals the transport to access the remote repository via HTTP + // instead of HTTPS. + PlainHTTP bool + + // ManifestMediaTypes is used in `Accept` header for resolving manifests + // from references. It is also used in identifying manifests and blobs from + // descriptors. If an empty list is present, default manifest media types + // are used. + ManifestMediaTypes []string + + // TagListPageSize specifies the page size when invoking the tag list API. + // If zero, the page size is determined by the remote registry. + // Reference: https://docs.docker.com/registry/spec/api/#tags + TagListPageSize int + + // ReferrerListPageSize specifies the page size when invoking the Referrers + // API. + // If zero, the page size is determined by the remote registry. + // Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers + ReferrerListPageSize int + + // MaxMetadataBytes specifies a limit on how many response bytes are allowed + // in the server's response to the metadata APIs, such as catalog list, tag + // list, and referrers list. + // If less than or equal to zero, a default (currently 4MiB) is used. + MaxMetadataBytes int64 + + // SkipReferrersGC specifies whether to delete the dangling referrers + // index when referrers tag schema is utilized. + // - If false, the old referrers index will be deleted after the new one + // is successfully uploaded. + // - If true, the old referrers index is kept. + // By default, it is disabled (set to false). See also: + // - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#referrers-tag-schema + // - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#pushing-manifests-with-subject + // - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#deleting-manifests + SkipReferrersGC bool + + // HandleWarning handles the warning returned by the remote server. + // Callers SHOULD deduplicate warnings from multiple associated responses. + // + // References: + // - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#warnings + // - https://www.rfc-editor.org/rfc/rfc7234#section-5.5 + HandleWarning func(warning Warning) + + // NOTE: Must keep fields in sync with clone(). + + // referrersState represents that if the repository supports Referrers API. + // default: referrersStateUnknown + referrersState referrersState + + // referrersPingLock locks the pingReferrers() method and allows only + // one go-routine to send the request. + referrersPingLock sync.Mutex + + // referrersMergePool provides a way to manage concurrent updates to a + // referrers index tagged by referrers tag schema. + referrersMergePool syncutil.Pool[syncutil.Merge[referrerChange]] +} + +// NewRepository creates a client to the remote repository identified by a +// reference. +// Example: localhost:5000/hello-world +func NewRepository(reference string) (*Repository, error) { + ref, err := registry.ParseReference(reference) + if err != nil { + return nil, err + } + return &Repository{ + Reference: ref, + }, nil +} + +// newRepositoryWithOptions returns a Repository with the given Reference and +// RepositoryOptions. +// +// RepositoryOptions are part of the Registry struct and set its defaults. +// RepositoryOptions shares the same struct definition as Repository, which +// contains unexported state that must not be copied to multiple Repositories. +// To handle this we explicitly copy only the fields that we want to reproduce. +func newRepositoryWithOptions(ref registry.Reference, opts *RepositoryOptions) (*Repository, error) { + if err := ref.ValidateRepository(); err != nil { + return nil, err + } + repo := (*Repository)(opts).clone() + repo.Reference = ref + return repo, nil +} + +// clone makes a copy of the Repository being careful not to copy non-copyable fields (sync.Mutex and syncutil.Pool types) +func (r *Repository) clone() *Repository { + return &Repository{ + Client: r.Client, + Reference: r.Reference, + PlainHTTP: r.PlainHTTP, + ManifestMediaTypes: slices.Clone(r.ManifestMediaTypes), + TagListPageSize: r.TagListPageSize, + ReferrerListPageSize: r.ReferrerListPageSize, + MaxMetadataBytes: r.MaxMetadataBytes, + SkipReferrersGC: r.SkipReferrersGC, + HandleWarning: r.HandleWarning, + } +} + +// SetReferrersCapability indicates the Referrers API capability of the remote +// repository. true: capable; false: not capable. +// +// SetReferrersCapability is valid only when it is called for the first time. +// SetReferrersCapability returns ErrReferrersCapabilityAlreadySet if the +// Referrers API capability has been already set. +// - When the capability is set to true, the Referrers() function will always +// request the Referrers API. Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers +// - When the capability is set to false, the Referrers() function will always +// request the Referrers Tag. Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#referrers-tag-schema +// - When the capability is not set, the Referrers() function will automatically +// determine which API to use. +func (r *Repository) SetReferrersCapability(capable bool) error { + var state referrersState + if capable { + state = referrersStateSupported + } else { + state = referrersStateUnsupported + } + if swapped := atomic.CompareAndSwapInt32(&r.referrersState, referrersStateUnknown, state); !swapped { + if fact := r.loadReferrersState(); fact != state { + return fmt.Errorf("%w: current capability = %v, new capability = %v", + ErrReferrersCapabilityAlreadySet, + fact == referrersStateSupported, + capable) + } + } + return nil +} + +// setReferrersState atomically loads r.referrersState. +func (r *Repository) loadReferrersState() referrersState { + return atomic.LoadInt32(&r.referrersState) +} + +// client returns an HTTP client used to access the remote repository. +// A default HTTP client is return if the client is not configured. +func (r *Repository) client() Client { + if r.Client == nil { + return auth.DefaultClient + } + return r.Client +} + +// do sends an HTTP request and returns an HTTP response using the HTTP client +// returned by r.client(). +func (r *Repository) do(req *http.Request) (*http.Response, error) { + if r.HandleWarning == nil { + return r.client().Do(req) + } + + resp, err := r.client().Do(req) + if err != nil { + return nil, err + } + handleWarningHeaders(resp.Header.Values(headerWarning), r.HandleWarning) + return resp, nil +} + +// blobStore detects the blob store for the given descriptor. +func (r *Repository) blobStore(desc ocispec.Descriptor) registry.BlobStore { + if isManifest(r.ManifestMediaTypes, desc) { + return r.Manifests() + } + return r.Blobs() +} + +// Fetch fetches the content identified by the descriptor. +func (r *Repository) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) { + return r.blobStore(target).Fetch(ctx, target) +} + +// Push pushes the content, matching the expected descriptor. +func (r *Repository) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error { + return r.blobStore(expected).Push(ctx, expected, content) +} + +// Mount makes the blob with the given digest in fromRepo +// available in the repository signified by the receiver. +// +// This avoids the need to pull content down from fromRepo only to push it to r. +// +// If the registry does not implement mounting, getContent will be used to get the +// content to push. If getContent is nil, the content will be pulled from the source +// repository. If getContent returns an error, it will be wrapped inside the error +// returned from Mount. +func (r *Repository) Mount(ctx context.Context, desc ocispec.Descriptor, fromRepo string, getContent func() (io.ReadCloser, error)) error { + return r.Blobs().(registry.Mounter).Mount(ctx, desc, fromRepo, getContent) +} + +// Exists returns true if the described content exists. +func (r *Repository) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) { + return r.blobStore(target).Exists(ctx, target) +} + +// Delete removes the content identified by the descriptor. +func (r *Repository) Delete(ctx context.Context, target ocispec.Descriptor) error { + return r.blobStore(target).Delete(ctx, target) +} + +// Blobs provides access to the blob CAS only, which contains config blobs, +// layers, and other generic blobs. +func (r *Repository) Blobs() registry.BlobStore { + return &blobStore{repo: r} +} + +// Manifests provides access to the manifest CAS only. +func (r *Repository) Manifests() registry.ManifestStore { + return &manifestStore{repo: r} +} + +// Resolve resolves a reference to a manifest descriptor. +// See also `ManifestMediaTypes`. +func (r *Repository) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) { + return r.Manifests().Resolve(ctx, reference) +} + +// Tag tags a manifest descriptor with a reference string. +func (r *Repository) Tag(ctx context.Context, desc ocispec.Descriptor, reference string) error { + return r.Manifests().Tag(ctx, desc, reference) +} + +// PushReference pushes the manifest with a reference tag. +func (r *Repository) PushReference(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error { + return r.Manifests().PushReference(ctx, expected, content, reference) +} + +// FetchReference fetches the manifest identified by the reference. +// The reference can be a tag or digest. +func (r *Repository) FetchReference(ctx context.Context, reference string) (ocispec.Descriptor, io.ReadCloser, error) { + return r.Manifests().FetchReference(ctx, reference) +} + +// ParseReference resolves a tag or a digest reference to a fully qualified +// reference from a base reference r.Reference. +// Tag, digest, or fully qualified references are accepted as input. +// +// If reference is a fully qualified reference, then ParseReference parses it +// and returns the parsed reference. If the parsed reference does not share +// the same base reference with the Repository r, ParseReference returns a +// wrapped error ErrInvalidReference. +func (r *Repository) ParseReference(reference string) (registry.Reference, error) { + ref, err := registry.ParseReference(reference) + if err != nil { + ref = registry.Reference{ + Registry: r.Reference.Registry, + Repository: r.Reference.Repository, + Reference: reference, + } + + // reference is not a FQDN + if index := strings.IndexByte(reference, '@'); index != -1 { + // `@` implies *digest*, so drop the *tag* (irrespective of what it is). + ref.Reference = reference[index+1:] + err = ref.ValidateReferenceAsDigest() + } else { + err = ref.ValidateReference() + } + + if err != nil { + return registry.Reference{}, err + } + } else if ref.Registry != r.Reference.Registry || ref.Repository != r.Reference.Repository { + return registry.Reference{}, fmt.Errorf( + "%w: mismatch between received %q and expected %q", + errdef.ErrInvalidReference, ref, r.Reference, + ) + } + + if len(ref.Reference) == 0 { + return registry.Reference{}, errdef.ErrInvalidReference + } + + return ref, nil +} + +// Tags lists the tags available in the repository. +// See also `TagListPageSize`. +// If `last` is NOT empty, the entries in the response start after the +// tag specified by `last`. Otherwise, the response starts from the top +// of the Tags list. +// +// References: +// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#content-discovery +// - https://docs.docker.com/registry/spec/api/#tags +func (r *Repository) Tags(ctx context.Context, last string, fn func(tags []string) error) error { + ctx = auth.AppendRepositoryScope(ctx, r.Reference, auth.ActionPull) + url := buildRepositoryTagListURL(r.PlainHTTP, r.Reference) + var err error + for err == nil { + url, err = r.tags(ctx, last, fn, url) + // clear `last` for subsequent pages + last = "" + } + if err != errNoLink { + return err + } + return nil +} + +// tags returns a single page of tag list with the next link. +func (r *Repository) tags(ctx context.Context, last string, fn func(tags []string) error, url string) (string, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return "", err + } + if r.TagListPageSize > 0 || last != "" { + q := req.URL.Query() + if r.TagListPageSize > 0 { + q.Set("n", strconv.Itoa(r.TagListPageSize)) + } + if last != "" { + q.Set("last", last) + } + req.URL.RawQuery = q.Encode() + } + resp, err := r.do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", errutil.ParseErrorResponse(resp) + } + var page struct { + Tags []string `json:"tags"` + } + lr := limitReader(resp.Body, r.MaxMetadataBytes) + if err := json.NewDecoder(lr).Decode(&page); err != nil { + return "", fmt.Errorf("%s %q: failed to decode response: %w", resp.Request.Method, resp.Request.URL, err) + } + if err := fn(page.Tags); err != nil { + return "", err + } + + return parseLink(resp) +} + +// Predecessors returns the descriptors of image or artifact manifests directly +// referencing the given manifest descriptor. +// Predecessors internally leverages Referrers. +// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers +func (r *Repository) Predecessors(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { + var res []ocispec.Descriptor + if err := r.Referrers(ctx, desc, "", func(referrers []ocispec.Descriptor) error { + res = append(res, referrers...) + return nil + }); err != nil { + return nil, err + } + return res, nil +} + +// Referrers lists the descriptors of image or artifact manifests directly +// referencing the given manifest descriptor. +// +// fn is called for each page of the referrers result. +// If artifactType is not empty, only referrers of the same artifact type are +// fed to fn. +// +// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers +func (r *Repository) Referrers(ctx context.Context, desc ocispec.Descriptor, artifactType string, fn func(referrers []ocispec.Descriptor) error) error { + state := r.loadReferrersState() + if state == referrersStateUnsupported { + // The repository is known to not support Referrers API, fallback to + // referrers tag schema. + return r.referrersByTagSchema(ctx, desc, artifactType, fn) + } + + err := r.referrersByAPI(ctx, desc, artifactType, fn) + if state == referrersStateSupported { + // The repository is known to support Referrers API, no fallback. + return err + } + + // The referrers state is unknown. + if err != nil { + if errors.Is(err, errdef.ErrUnsupported) { + // Referrers API is not supported, fallback to referrers tag schema. + r.SetReferrersCapability(false) + return r.referrersByTagSchema(ctx, desc, artifactType, fn) + } + return err + } + + r.SetReferrersCapability(true) + return nil +} + +// referrersByAPI lists the descriptors of manifests directly referencing +// the given manifest descriptor by requesting Referrers API. +// fn is called for the referrers result. If artifactType is not empty, +// only referrers of the same artifact type are fed to fn. +func (r *Repository) referrersByAPI(ctx context.Context, desc ocispec.Descriptor, artifactType string, fn func(referrers []ocispec.Descriptor) error) error { + ref := r.Reference + ref.Reference = desc.Digest.String() + ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull) + + url := buildReferrersURL(r.PlainHTTP, ref, artifactType) + var err error + for err == nil { + url, err = r.referrersPageByAPI(ctx, artifactType, fn, url) + } + if err == errNoLink { + return nil + } + return err +} + +// referrersPageByAPI lists a single page of the descriptors of manifests +// directly referencing the given manifest descriptor. fn is called for +// a page of referrersPageByAPI result. +// If artifactType is not empty, only referrersPageByAPI of the same +// artifact type are fed to fn. +// referrersPageByAPI returns the link url for the next page. +func (r *Repository) referrersPageByAPI(ctx context.Context, artifactType string, fn func(referrers []ocispec.Descriptor) error, url string) (string, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return "", err + } + if r.ReferrerListPageSize > 0 { + q := req.URL.Query() + q.Set("n", strconv.Itoa(r.ReferrerListPageSize)) + req.URL.RawQuery = q.Encode() + } + + resp, err := r.do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + switch resp.StatusCode { + case http.StatusOK: + case http.StatusNotFound: + if errResp := errutil.ParseErrorResponse(resp); errutil.IsErrorCode(errResp, errcode.ErrorCodeNameUnknown) { + // The repository is not found, Referrers API status is unknown + return "", errResp + } + // Referrers API is not supported. + return "", fmt.Errorf("failed to query referrers API: %w", errdef.ErrUnsupported) + default: + return "", errutil.ParseErrorResponse(resp) + } + + // also check the content type + if ct := resp.Header.Get("Content-Type"); ct != ocispec.MediaTypeImageIndex { + return "", fmt.Errorf("unknown content returned (%s), expecting image index: %w", ct, errdef.ErrUnsupported) + } + + var index ocispec.Index + lr := limitReader(resp.Body, r.MaxMetadataBytes) + if err := json.NewDecoder(lr).Decode(&index); err != nil { + return "", fmt.Errorf("%s %q: failed to decode response: %w", resp.Request.Method, resp.Request.URL, err) + } + + referrers := index.Manifests + if artifactType != "" { + // check both filters header and filters annotations for compatibility + // latest spec for filters header: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers + // older spec for filters annotations: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#listing-referrers + filtersHeader := resp.Header.Get(headerOCIFiltersApplied) + filtersAnnotation := index.Annotations[spec.AnnotationReferrersFiltersApplied] + if !isReferrersFilterApplied(filtersHeader, filterTypeArtifactType) && + !isReferrersFilterApplied(filtersAnnotation, filterTypeArtifactType) { + // perform client side filtering if the filter is not applied on the server side + referrers = filterReferrers(referrers, artifactType) + } + } + if len(referrers) > 0 { + if err := fn(referrers); err != nil { + return "", err + } + } + return parseLink(resp) +} + +// referrersByTagSchema lists the descriptors of manifests directly +// referencing the given manifest descriptor by requesting referrers tag. +// fn is called for the referrers result. If artifactType is not empty, +// only referrers of the same artifact type are fed to fn. +// reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#backwards-compatibility +func (r *Repository) referrersByTagSchema(ctx context.Context, desc ocispec.Descriptor, artifactType string, fn func(referrers []ocispec.Descriptor) error) error { + referrersTag := buildReferrersTag(desc) + _, referrers, err := r.referrersFromIndex(ctx, referrersTag) + if err != nil { + if errors.Is(err, errdef.ErrNotFound) { + // no referrers to the manifest + return nil + } + return err + } + + filtered := filterReferrers(referrers, artifactType) + if len(filtered) == 0 { + return nil + } + return fn(filtered) +} + +// referrersFromIndex queries the referrers index using the the given referrers +// tag. If Succeeded, returns the descriptor of referrers index and the +// referrers list. +func (r *Repository) referrersFromIndex(ctx context.Context, referrersTag string) (ocispec.Descriptor, []ocispec.Descriptor, error) { + desc, rc, err := r.FetchReference(ctx, referrersTag) + if err != nil { + return ocispec.Descriptor{}, nil, err + } + defer rc.Close() + + if err := limitSize(desc, r.MaxMetadataBytes); err != nil { + return ocispec.Descriptor{}, nil, fmt.Errorf("failed to read referrers index from referrers tag %s: %w", referrersTag, err) + } + var index ocispec.Index + if err := decodeJSON(rc, desc, &index); err != nil { + return ocispec.Descriptor{}, nil, fmt.Errorf("failed to decode referrers index from referrers tag %s: %w", referrersTag, err) + } + + return desc, index.Manifests, nil +} + +// pingReferrers returns true if the Referrers API is available for r. +func (r *Repository) pingReferrers(ctx context.Context) (bool, error) { + switch r.loadReferrersState() { + case referrersStateSupported: + return true, nil + case referrersStateUnsupported: + return false, nil + } + + // referrers state is unknown + // limit the rate of pinging referrers API + r.referrersPingLock.Lock() + defer r.referrersPingLock.Unlock() + + switch r.loadReferrersState() { + case referrersStateSupported: + return true, nil + case referrersStateUnsupported: + return false, nil + } + + ref := r.Reference + ref.Reference = zeroDigest + ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull) + + url := buildReferrersURL(r.PlainHTTP, ref, "") + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return false, err + } + resp, err := r.do(req) + if err != nil { + return false, err + } + defer resp.Body.Close() + + switch resp.StatusCode { + case http.StatusOK: + supported := resp.Header.Get("Content-Type") == ocispec.MediaTypeImageIndex + r.SetReferrersCapability(supported) + return supported, nil + case http.StatusNotFound: + if err := errutil.ParseErrorResponse(resp); errutil.IsErrorCode(err, errcode.ErrorCodeNameUnknown) { + // repository not found + return false, err + } + r.SetReferrersCapability(false) + return false, nil + default: + return false, errutil.ParseErrorResponse(resp) + } +} + +// delete removes the content identified by the descriptor in the entity "blobs" +// or "manifests". +func (r *Repository) delete(ctx context.Context, target ocispec.Descriptor, isManifest bool) error { + ref := r.Reference + ref.Reference = target.Digest.String() + ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionDelete) + buildURL := buildRepositoryBlobURL + if isManifest { + buildURL = buildRepositoryManifestURL + } + url := buildURL(r.PlainHTTP, ref) + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil) + if err != nil { + return err + } + + resp, err := r.do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + switch resp.StatusCode { + case http.StatusAccepted: + return verifyContentDigest(resp, target.Digest) + case http.StatusNotFound: + return fmt.Errorf("%s: %w", target.Digest, errdef.ErrNotFound) + default: + return errutil.ParseErrorResponse(resp) + } +} + +// blobStore accesses the blob part of the repository. +type blobStore struct { + repo *Repository +} + +// Fetch fetches the content identified by the descriptor. +func (s *blobStore) Fetch(ctx context.Context, target ocispec.Descriptor) (rc io.ReadCloser, err error) { + ref := s.repo.Reference + ref.Reference = target.Digest.String() + ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull) + url := buildRepositoryBlobURL(s.repo.PlainHTTP, ref) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + resp, err := s.repo.do(req) + if err != nil { + return nil, err + } + defer func() { + if err != nil { + resp.Body.Close() + } + }() + + switch resp.StatusCode { + case http.StatusOK: // server does not support seek as `Range` was ignored. + if size := resp.ContentLength; size != -1 && size != target.Size { + return nil, fmt.Errorf("%s %q: mismatch Content-Length", resp.Request.Method, resp.Request.URL) + } + + // check server range request capability. + // Docker spec allows range header form of "Range: bytes=-". + // However, the remote server may still not RFC 7233 compliant. + // Reference: https://docs.docker.com/registry/spec/api/#blob + if rangeUnit := resp.Header.Get("Accept-Ranges"); rangeUnit == "bytes" { + return httputil.NewReadSeekCloser(s.repo.client(), req, resp.Body, target.Size), nil + } + return resp.Body, nil + case http.StatusNotFound: + return nil, fmt.Errorf("%s: %w", target.Digest, errdef.ErrNotFound) + default: + return nil, errutil.ParseErrorResponse(resp) + } +} + +// Mount mounts the given descriptor from fromRepo into s. +func (s *blobStore) Mount(ctx context.Context, desc ocispec.Descriptor, fromRepo string, getContent func() (io.ReadCloser, error)) error { + // pushing usually requires both pull and push actions. + // Reference: https://github.com/distribution/distribution/blob/v2.7.1/registry/handlers/app.go#L921-L930 + ctx = auth.AppendRepositoryScope(ctx, s.repo.Reference, auth.ActionPull, auth.ActionPush) + + // We also need pull access to the source repo. + fromRef := s.repo.Reference + fromRef.Repository = fromRepo + ctx = auth.AppendRepositoryScope(ctx, fromRef, auth.ActionPull) + + url := buildRepositoryBlobMountURL(s.repo.PlainHTTP, s.repo.Reference, desc.Digest, fromRepo) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil) + if err != nil { + return err + } + resp, err := s.repo.do(req) + if err != nil { + return err + } + if resp.StatusCode == http.StatusCreated { + defer resp.Body.Close() + // Check the server seems to be behaving. + return verifyContentDigest(resp, desc.Digest) + } + if resp.StatusCode != http.StatusAccepted { + defer resp.Body.Close() + return errutil.ParseErrorResponse(resp) + } + resp.Body.Close() + // From the [spec]: + // + // "If a registry does not support cross-repository mounting + // or is unable to mount the requested blob, + // it SHOULD return a 202. + // This indicates that the upload session has begun + // and that the client MAY proceed with the upload." + // + // So we need to get the content from somewhere in order to + // push it. If the caller has provided a getContent function, we + // can use that, otherwise pull the content from the source repository. + // + // [spec]: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#mounting-a-blob-from-another-repository + + var r io.ReadCloser + if getContent != nil { + r, err = getContent() + } else { + r, err = s.sibling(fromRepo).Fetch(ctx, desc) + } + if err != nil { + return fmt.Errorf("cannot read source blob: %w", err) + } + defer r.Close() + return s.completePushAfterInitialPost(ctx, req, resp, desc, r) +} + +// sibling returns a blob store for another repository in the same +// registry. +func (s *blobStore) sibling(otherRepoName string) *blobStore { + otherRepo := s.repo.clone() + otherRepo.Reference.Repository = otherRepoName + return &blobStore{ + repo: otherRepo, + } +} + +// Push pushes the content, matching the expected descriptor. +// Existing content is not checked by Push() to minimize the number of out-going +// requests. +// Push is done by conventional 2-step monolithic upload instead of a single +// `POST` request for better overall performance. It also allows early fail on +// authentication errors. +// +// References: +// - https://docs.docker.com/registry/spec/api/#pushing-an-image +// - https://docs.docker.com/registry/spec/api/#initiate-blob-upload +// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#pushing-a-blob-monolithically +func (s *blobStore) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error { + // start an upload + // pushing usually requires both pull and push actions. + // Reference: https://github.com/distribution/distribution/blob/v2.7.1/registry/handlers/app.go#L921-L930 + ctx = auth.AppendRepositoryScope(ctx, s.repo.Reference, auth.ActionPull, auth.ActionPush) + url := buildRepositoryBlobUploadURL(s.repo.PlainHTTP, s.repo.Reference) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil) + if err != nil { + return err + } + + resp, err := s.repo.do(req) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusAccepted { + defer resp.Body.Close() + return errutil.ParseErrorResponse(resp) + } + resp.Body.Close() + return s.completePushAfterInitialPost(ctx, req, resp, expected, content) +} + +// completePushAfterInitialPost implements step 2 of the push protocol. This can be invoked either by +// Push or by Mount when the receiving repository does not implement the +// mount endpoint. +func (s *blobStore) completePushAfterInitialPost(ctx context.Context, req *http.Request, resp *http.Response, expected ocispec.Descriptor, content io.Reader) error { + reqHostname := req.URL.Hostname() + reqPort := req.URL.Port() + // monolithic upload + location, err := resp.Location() + if err != nil { + return err + } + // work-around solution for https://github.com/oras-project/oras-go/issues/177 + // For some registries, if the port 443 is explicitly set to the hostname + // like registry.wabbit-networks.io:443/myrepo, blob push will fail since + // the hostname of the Location header in the response is set to + // registry.wabbit-networks.io instead of registry.wabbit-networks.io:443. + locationHostname := location.Hostname() + locationPort := location.Port() + // if location port 443 is missing, add it back + if reqPort == "443" && locationHostname == reqHostname && locationPort == "" { + location.Host = locationHostname + ":" + reqPort + } + url := location.String() + req, err = http.NewRequestWithContext(ctx, http.MethodPut, url, content) + if err != nil { + return err + } + if req.GetBody != nil && req.ContentLength != expected.Size { + // short circuit a size mismatch for built-in types. + return fmt.Errorf("mismatch content length %d: expect %d", req.ContentLength, expected.Size) + } + req.ContentLength = expected.Size + // the expected media type is ignored as in the API doc. + req.Header.Set("Content-Type", "application/octet-stream") + q := req.URL.Query() + q.Set("digest", expected.Digest.String()) + req.URL.RawQuery = q.Encode() + + // reuse credential from previous POST request + if auth := resp.Request.Header.Get("Authorization"); auth != "" { + req.Header.Set("Authorization", auth) + } + resp, err = s.repo.do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusCreated { + return errutil.ParseErrorResponse(resp) + } + return nil +} + +// Exists returns true if the described content exists. +func (s *blobStore) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) { + _, err := s.Resolve(ctx, target.Digest.String()) + if err == nil { + return true, nil + } + if errors.Is(err, errdef.ErrNotFound) { + return false, nil + } + return false, err +} + +// Delete removes the content identified by the descriptor. +func (s *blobStore) Delete(ctx context.Context, target ocispec.Descriptor) error { + return s.repo.delete(ctx, target, false) +} + +// Resolve resolves a reference to a descriptor. +func (s *blobStore) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) { + ref, err := s.repo.ParseReference(reference) + if err != nil { + return ocispec.Descriptor{}, err + } + refDigest, err := ref.Digest() + if err != nil { + return ocispec.Descriptor{}, err + } + ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull) + url := buildRepositoryBlobURL(s.repo.PlainHTTP, ref) + req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil) + if err != nil { + return ocispec.Descriptor{}, err + } + + resp, err := s.repo.do(req) + if err != nil { + return ocispec.Descriptor{}, err + } + defer resp.Body.Close() + + switch resp.StatusCode { + case http.StatusOK: + return generateBlobDescriptor(resp, refDigest) + case http.StatusNotFound: + return ocispec.Descriptor{}, fmt.Errorf("%s: %w", ref, errdef.ErrNotFound) + default: + return ocispec.Descriptor{}, errutil.ParseErrorResponse(resp) + } +} + +// FetchReference fetches the blob identified by the reference. +// The reference must be a digest. +func (s *blobStore) FetchReference(ctx context.Context, reference string) (desc ocispec.Descriptor, rc io.ReadCloser, err error) { + ref, err := s.repo.ParseReference(reference) + if err != nil { + return ocispec.Descriptor{}, nil, err + } + refDigest, err := ref.Digest() + if err != nil { + return ocispec.Descriptor{}, nil, err + } + + ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull) + url := buildRepositoryBlobURL(s.repo.PlainHTTP, ref) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return ocispec.Descriptor{}, nil, err + } + + resp, err := s.repo.do(req) + if err != nil { + return ocispec.Descriptor{}, nil, err + } + defer func() { + if err != nil { + resp.Body.Close() + } + }() + + switch resp.StatusCode { + case http.StatusOK: // server does not support seek as `Range` was ignored. + if resp.ContentLength == -1 { + desc, err = s.Resolve(ctx, reference) + } else { + desc, err = generateBlobDescriptor(resp, refDigest) + } + if err != nil { + return ocispec.Descriptor{}, nil, err + } + + // check server range request capability. + // Docker spec allows range header form of "Range: bytes=-". + // However, the remote server may still not RFC 7233 compliant. + // Reference: https://docs.docker.com/registry/spec/api/#blob + if rangeUnit := resp.Header.Get("Accept-Ranges"); rangeUnit == "bytes" { + return desc, httputil.NewReadSeekCloser(s.repo.client(), req, resp.Body, desc.Size), nil + } + return desc, resp.Body, nil + case http.StatusNotFound: + return ocispec.Descriptor{}, nil, fmt.Errorf("%s: %w", ref, errdef.ErrNotFound) + default: + return ocispec.Descriptor{}, nil, errutil.ParseErrorResponse(resp) + } +} + +// generateBlobDescriptor returns a descriptor generated from the response. +func generateBlobDescriptor(resp *http.Response, refDigest digest.Digest) (ocispec.Descriptor, error) { + mediaType, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if mediaType == "" { + mediaType = "application/octet-stream" + } + + size := resp.ContentLength + if size == -1 { + return ocispec.Descriptor{}, fmt.Errorf("%s %q: unknown response Content-Length", resp.Request.Method, resp.Request.URL) + } + + if err := verifyContentDigest(resp, refDigest); err != nil { + return ocispec.Descriptor{}, err + } + + return ocispec.Descriptor{ + MediaType: mediaType, + Digest: refDigest, + Size: size, + }, nil +} + +// manifestStore accesses the manifest part of the repository. +type manifestStore struct { + repo *Repository +} + +// Fetch fetches the content identified by the descriptor. +func (s *manifestStore) Fetch(ctx context.Context, target ocispec.Descriptor) (rc io.ReadCloser, err error) { + ref := s.repo.Reference + ref.Reference = target.Digest.String() + ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull) + url := buildRepositoryManifestURL(s.repo.PlainHTTP, ref) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", target.MediaType) + + resp, err := s.repo.do(req) + if err != nil { + return nil, err + } + defer func() { + if err != nil { + resp.Body.Close() + } + }() + + switch resp.StatusCode { + case http.StatusOK: + // no-op + case http.StatusNotFound: + return nil, fmt.Errorf("%s: %w", target.Digest, errdef.ErrNotFound) + default: + return nil, errutil.ParseErrorResponse(resp) + } + mediaType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return nil, fmt.Errorf("%s %q: invalid response Content-Type: %w", resp.Request.Method, resp.Request.URL, err) + } + if mediaType != target.MediaType { + return nil, fmt.Errorf("%s %q: mismatch response Content-Type %q: expect %q", resp.Request.Method, resp.Request.URL, mediaType, target.MediaType) + } + if size := resp.ContentLength; size != -1 && size != target.Size { + return nil, fmt.Errorf("%s %q: mismatch Content-Length", resp.Request.Method, resp.Request.URL) + } + if err := verifyContentDigest(resp, target.Digest); err != nil { + return nil, err + } + return resp.Body, nil +} + +// Push pushes the content, matching the expected descriptor. +func (s *manifestStore) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error { + return s.pushWithIndexing(ctx, expected, content, expected.Digest.String()) +} + +// Exists returns true if the described content exists. +func (s *manifestStore) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) { + _, err := s.Resolve(ctx, target.Digest.String()) + if err == nil { + return true, nil + } + if errors.Is(err, errdef.ErrNotFound) { + return false, nil + } + return false, err +} + +// Delete removes the manifest content identified by the descriptor. +func (s *manifestStore) Delete(ctx context.Context, target ocispec.Descriptor) error { + return s.deleteWithIndexing(ctx, target) +} + +// deleteWithIndexing removes the manifest content identified by the descriptor, +// and indexes referrers for the manifest when needed. +func (s *manifestStore) deleteWithIndexing(ctx context.Context, target ocispec.Descriptor) error { + switch target.MediaType { + case spec.MediaTypeArtifactManifest, ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex: + if state := s.repo.loadReferrersState(); state == referrersStateSupported { + // referrers API is available, no client-side indexing needed + return s.repo.delete(ctx, target, true) + } + + if err := limitSize(target, s.repo.MaxMetadataBytes); err != nil { + return err + } + ctx = auth.AppendRepositoryScope(ctx, s.repo.Reference, auth.ActionPull, auth.ActionDelete) + manifestJSON, err := content.FetchAll(ctx, s, target) + if err != nil { + return err + } + if err := s.indexReferrersForDelete(ctx, target, manifestJSON); err != nil { + return err + } + } + + return s.repo.delete(ctx, target, true) +} + +// indexReferrersForDelete indexes referrers for manifests with a subject field +// on manifest delete. +// +// References: +// - Latest spec: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#deleting-manifests +// - Compatible spec: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#deleting-manifests +func (s *manifestStore) indexReferrersForDelete(ctx context.Context, desc ocispec.Descriptor, manifestJSON []byte) error { + var manifest struct { + Subject *ocispec.Descriptor `json:"subject"` + } + if err := json.Unmarshal(manifestJSON, &manifest); err != nil { + return fmt.Errorf("failed to decode manifest: %s: %s: %w", desc.Digest, desc.MediaType, err) + } + if manifest.Subject == nil { + // no subject, no indexing needed + return nil + } + + subject := *manifest.Subject + ok, err := s.repo.pingReferrers(ctx) + if err != nil { + return err + } + if ok { + // referrers API is available, no client-side indexing needed + return nil + } + return s.updateReferrersIndex(ctx, subject, referrerChange{desc, referrerOperationRemove}) +} + +// Resolve resolves a reference to a descriptor. +// See also `ManifestMediaTypes`. +func (s *manifestStore) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) { + ref, err := s.repo.ParseReference(reference) + if err != nil { + return ocispec.Descriptor{}, err + } + ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull) + url := buildRepositoryManifestURL(s.repo.PlainHTTP, ref) + req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil) + if err != nil { + return ocispec.Descriptor{}, err + } + req.Header.Set("Accept", manifestAcceptHeader(s.repo.ManifestMediaTypes)) + + resp, err := s.repo.do(req) + if err != nil { + return ocispec.Descriptor{}, err + } + defer resp.Body.Close() + + switch resp.StatusCode { + case http.StatusOK: + return s.generateDescriptor(resp, ref, req.Method) + case http.StatusNotFound: + return ocispec.Descriptor{}, fmt.Errorf("%s: %w", ref, errdef.ErrNotFound) + default: + return ocispec.Descriptor{}, errutil.ParseErrorResponse(resp) + } +} + +// FetchReference fetches the manifest identified by the reference. +// The reference can be a tag or digest. +func (s *manifestStore) FetchReference(ctx context.Context, reference string) (desc ocispec.Descriptor, rc io.ReadCloser, err error) { + ref, err := s.repo.ParseReference(reference) + if err != nil { + return ocispec.Descriptor{}, nil, err + } + + ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull) + url := buildRepositoryManifestURL(s.repo.PlainHTTP, ref) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return ocispec.Descriptor{}, nil, err + } + req.Header.Set("Accept", manifestAcceptHeader(s.repo.ManifestMediaTypes)) + + resp, err := s.repo.do(req) + if err != nil { + return ocispec.Descriptor{}, nil, err + } + defer func() { + if err != nil { + resp.Body.Close() + } + }() + + switch resp.StatusCode { + case http.StatusOK: + if resp.ContentLength == -1 { + desc, err = s.Resolve(ctx, reference) + } else { + desc, err = s.generateDescriptor(resp, ref, req.Method) + } + if err != nil { + return ocispec.Descriptor{}, nil, err + } + return desc, resp.Body, nil + case http.StatusNotFound: + return ocispec.Descriptor{}, nil, fmt.Errorf("%s: %w", ref, errdef.ErrNotFound) + default: + return ocispec.Descriptor{}, nil, errutil.ParseErrorResponse(resp) + } +} + +// Tag tags a manifest descriptor with a reference string. +func (s *manifestStore) Tag(ctx context.Context, desc ocispec.Descriptor, reference string) error { + ref, err := s.repo.ParseReference(reference) + if err != nil { + return err + } + + ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull, auth.ActionPush) + rc, err := s.Fetch(ctx, desc) + if err != nil { + return err + } + defer rc.Close() + + return s.push(ctx, desc, rc, ref.Reference) +} + +// PushReference pushes the manifest with a reference tag. +func (s *manifestStore) PushReference(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error { + ref, err := s.repo.ParseReference(reference) + if err != nil { + return err + } + return s.pushWithIndexing(ctx, expected, content, ref.Reference) +} + +// push pushes the manifest content, matching the expected descriptor. +func (s *manifestStore) push(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error { + ref := s.repo.Reference + ref.Reference = reference + // pushing usually requires both pull and push actions. + // Reference: https://github.com/distribution/distribution/blob/v2.7.1/registry/handlers/app.go#L921-L930 + ctx = auth.AppendRepositoryScope(ctx, ref, auth.ActionPull, auth.ActionPush) + url := buildRepositoryManifestURL(s.repo.PlainHTTP, ref) + // unwrap the content for optimizations of built-in types. + body := ioutil.UnwrapNopCloser(content) + if _, ok := body.(io.ReadCloser); ok { + // undo unwrap if the nopCloser is intended. + body = content + } + req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, body) + if err != nil { + return err + } + if req.GetBody != nil && req.ContentLength != expected.Size { + // short circuit a size mismatch for built-in types. + return fmt.Errorf("mismatch content length %d: expect %d", req.ContentLength, expected.Size) + } + req.ContentLength = expected.Size + req.Header.Set("Content-Type", expected.MediaType) + + // if the underlying client is an auth client, the content might be read + // more than once for obtaining the auth challenge and the actual request. + // To prevent double reading, the manifest is read and stored in the memory, + // and serve from the memory. + client := s.repo.client() + if _, ok := client.(*auth.Client); ok && req.GetBody == nil { + store := cas.NewMemory() + err := store.Push(ctx, expected, content) + if err != nil { + return err + } + req.GetBody = func() (io.ReadCloser, error) { + return store.Fetch(ctx, expected) + } + req.Body, err = req.GetBody() + if err != nil { + return err + } + } + resp, err := s.repo.do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusCreated { + return errutil.ParseErrorResponse(resp) + } + s.checkOCISubjectHeader(resp) + return verifyContentDigest(resp, expected.Digest) +} + +// checkOCISubjectHeader checks the "OCI-Subject" header in the response and +// sets referrers capability accordingly. +// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#pushing-manifests-with-subject +func (s *manifestStore) checkOCISubjectHeader(resp *http.Response) { + // If the "OCI-Subject" header is set, it indicates that the registry + // supports the Referrers API and has processed the subject of the manifest. + if subjectHeader := resp.Header.Get(headerOCISubject); subjectHeader != "" { + s.repo.SetReferrersCapability(true) + } + + // If the "OCI-Subject" header is NOT set, it means that either the manifest + // has no subject OR the referrers API is NOT supported by the registry. + // + // Since we don't know whether the pushed manifest has a subject or not, + // we do not set the referrers capability to false at here. +} + +// pushWithIndexing pushes the manifest content matching the expected descriptor, +// and indexes referrers for the manifest when needed. +func (s *manifestStore) pushWithIndexing(ctx context.Context, expected ocispec.Descriptor, r io.Reader, reference string) error { + switch expected.MediaType { + case spec.MediaTypeArtifactManifest, ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex: + if state := s.repo.loadReferrersState(); state == referrersStateSupported { + // referrers API is available, no client-side indexing needed + return s.push(ctx, expected, r, reference) + } + + if err := limitSize(expected, s.repo.MaxMetadataBytes); err != nil { + return err + } + manifestJSON, err := content.ReadAll(r, expected) + if err != nil { + return err + } + if err := s.push(ctx, expected, bytes.NewReader(manifestJSON), reference); err != nil { + return err + } + // check referrers API availability again after push + if state := s.repo.loadReferrersState(); state == referrersStateSupported { + // the subject has been processed the registry, no client-side + // indexing needed + return nil + } + return s.indexReferrersForPush(ctx, expected, manifestJSON) + default: + return s.push(ctx, expected, r, reference) + } +} + +// indexReferrersForPush indexes referrers for manifests with a subject field +// on manifest push. +// +// References: +// - Latest spec: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#pushing-manifests-with-subject +// - Compatible spec: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#pushing-manifests-with-subject +func (s *manifestStore) indexReferrersForPush(ctx context.Context, desc ocispec.Descriptor, manifestJSON []byte) error { + var subject ocispec.Descriptor + switch desc.MediaType { + case spec.MediaTypeArtifactManifest: + var manifest spec.Artifact + if err := json.Unmarshal(manifestJSON, &manifest); err != nil { + return fmt.Errorf("failed to decode manifest: %s: %s: %w", desc.Digest, desc.MediaType, err) + } + if manifest.Subject == nil { + // no subject, no indexing needed + return nil + } + subject = *manifest.Subject + desc.ArtifactType = manifest.ArtifactType + desc.Annotations = manifest.Annotations + case ocispec.MediaTypeImageManifest: + var manifest ocispec.Manifest + if err := json.Unmarshal(manifestJSON, &manifest); err != nil { + return fmt.Errorf("failed to decode manifest: %s: %s: %w", desc.Digest, desc.MediaType, err) + } + if manifest.Subject == nil { + // no subject, no indexing needed + return nil + } + subject = *manifest.Subject + desc.ArtifactType = manifest.ArtifactType + if desc.ArtifactType == "" { + desc.ArtifactType = manifest.Config.MediaType + } + desc.Annotations = manifest.Annotations + case ocispec.MediaTypeImageIndex: + var manifest ocispec.Index + if err := json.Unmarshal(manifestJSON, &manifest); err != nil { + return fmt.Errorf("failed to decode manifest: %s: %s: %w", desc.Digest, desc.MediaType, err) + } + if manifest.Subject == nil { + // no subject, no indexing needed + return nil + } + subject = *manifest.Subject + desc.ArtifactType = manifest.ArtifactType + desc.Annotations = manifest.Annotations + default: + return nil + } + + // if the manifest has a subject but the remote registry does not process it, + // it means that the Referrers API is not supported by the registry. + s.repo.SetReferrersCapability(false) + return s.updateReferrersIndex(ctx, subject, referrerChange{desc, referrerOperationAdd}) +} + +// updateReferrersIndex updates the referrers index for desc referencing subject +// on manifest push and manifest delete. +// References: +// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#pushing-manifests-with-subject +// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#deleting-manifests +func (s *manifestStore) updateReferrersIndex(ctx context.Context, subject ocispec.Descriptor, change referrerChange) (err error) { + referrersTag := buildReferrersTag(subject) + + var oldIndexDesc *ocispec.Descriptor + var oldReferrers []ocispec.Descriptor + prepare := func() error { + // 1. pull the original referrers list using the referrers tag schema + indexDesc, referrers, err := s.repo.referrersFromIndex(ctx, referrersTag) + if err != nil { + if errors.Is(err, errdef.ErrNotFound) { + // valid case: no old referrers index + return nil + } + return err + } + oldIndexDesc = &indexDesc + oldReferrers = referrers + return nil + } + update := func(referrerChanges []referrerChange) error { + // 2. apply the referrer changes on the referrers list + updatedReferrers, err := applyReferrerChanges(oldReferrers, referrerChanges) + if err != nil { + if err == errNoReferrerUpdate { + return nil + } + return err + } + + // 3. push the updated referrers list using referrers tag schema + if len(updatedReferrers) > 0 || s.repo.SkipReferrersGC { + // push a new index in either case: + // 1. the referrers list has been updated with a non-zero size + // 2. OR the updated referrers list is empty but referrers GC + // is skipped, in this case an empty index should still be pushed + // as the old index won't get deleted + newIndexDesc, newIndex, err := generateIndex(updatedReferrers) + if err != nil { + return fmt.Errorf("failed to generate referrers index for referrers tag %s: %w", referrersTag, err) + } + if err := s.push(ctx, newIndexDesc, bytes.NewReader(newIndex), referrersTag); err != nil { + return fmt.Errorf("failed to push referrers index tagged by %s: %w", referrersTag, err) + } + } + + // 4. delete the dangling original referrers index, if applicable + if s.repo.SkipReferrersGC || oldIndexDesc == nil { + return nil + } + if err := s.repo.delete(ctx, *oldIndexDesc, true); err != nil { + return &ReferrersError{ + Op: opDeleteReferrersIndex, + Err: fmt.Errorf("failed to delete dangling referrers index %s for referrers tag %s: %w", oldIndexDesc.Digest.String(), referrersTag, err), + Subject: subject, + } + } + return nil + } + + merge, done := s.repo.referrersMergePool.Get(referrersTag) + defer done() + return merge.Do(change, prepare, update) +} + +// ParseReference parses a reference to a fully qualified reference. +func (s *manifestStore) ParseReference(reference string) (registry.Reference, error) { + return s.repo.ParseReference(reference) +} + +// generateDescriptor returns a descriptor generated from the response. +// See the truth table at the top of `repository_test.go` +func (s *manifestStore) generateDescriptor(resp *http.Response, ref registry.Reference, httpMethod string) (ocispec.Descriptor, error) { + // 1. Validate Content-Type + mediaType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if err != nil { + return ocispec.Descriptor{}, fmt.Errorf( + "%s %q: invalid response `Content-Type` header; %w", + resp.Request.Method, + resp.Request.URL, + err, + ) + } + + // 2. Validate Size + if resp.ContentLength == -1 { + return ocispec.Descriptor{}, fmt.Errorf( + "%s %q: unknown response Content-Length", + resp.Request.Method, + resp.Request.URL, + ) + } + + // 3. Validate Client Reference + var refDigest digest.Digest + if d, err := ref.Digest(); err == nil { + refDigest = d + } + + // 4. Validate Server Digest (if present) + var serverHeaderDigest digest.Digest + if serverHeaderDigestStr := resp.Header.Get(headerDockerContentDigest); serverHeaderDigestStr != "" { + if serverHeaderDigest, err = digest.Parse(serverHeaderDigestStr); err != nil { + return ocispec.Descriptor{}, fmt.Errorf( + "%s %q: invalid response header value: `%s: %s`; %w", + resp.Request.Method, + resp.Request.URL, + headerDockerContentDigest, + serverHeaderDigestStr, + err, + ) + } + } + + /* 5. Now, look for specific error conditions; see truth table in method docstring */ + var contentDigest digest.Digest + + if len(serverHeaderDigest) == 0 { + if httpMethod == http.MethodHead { + if len(refDigest) == 0 { + // HEAD without server `Docker-Content-Digest` header is an + // immediate fail + return ocispec.Descriptor{}, fmt.Errorf( + "HTTP %s request missing required header %q", + httpMethod, headerDockerContentDigest, + ) + } + // Otherwise, just trust the client-supplied digest + contentDigest = refDigest + } else { + // GET without server `Docker-Content-Digest` header forces the + // expensive calculation + var calculatedDigest digest.Digest + if calculatedDigest, err = calculateDigestFromResponse(resp, s.repo.MaxMetadataBytes); err != nil { + return ocispec.Descriptor{}, fmt.Errorf("failed to calculate digest on response body; %w", err) + } + contentDigest = calculatedDigest + } + } else { + contentDigest = serverHeaderDigest + } + + if len(refDigest) > 0 && refDigest != contentDigest { + return ocispec.Descriptor{}, fmt.Errorf( + "%s %q: invalid response; digest mismatch in %s: received %q when expecting %q", + resp.Request.Method, resp.Request.URL, + headerDockerContentDigest, contentDigest, + refDigest, + ) + } + + // 6. Finally, if we made it this far, then all is good; return. + return ocispec.Descriptor{ + MediaType: mediaType, + Digest: contentDigest, + Size: resp.ContentLength, + }, nil +} + +// calculateDigestFromResponse calculates the actual digest of the response body +// taking care not to destroy it in the process. +func calculateDigestFromResponse(resp *http.Response, maxMetadataBytes int64) (digest.Digest, error) { + defer resp.Body.Close() + + body := limitReader(resp.Body, maxMetadataBytes) + content, err := io.ReadAll(body) + if err != nil { + return "", fmt.Errorf("%s %q: failed to read response body: %w", resp.Request.Method, resp.Request.URL, err) + } + resp.Body = io.NopCloser(bytes.NewReader(content)) + + return digest.FromBytes(content), nil +} + +// verifyContentDigest verifies "Docker-Content-Digest" header if present. +// OCI distribution-spec states the Docker-Content-Digest header is optional. +// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.0.1/spec.md#legacy-docker-support-http-headers +func verifyContentDigest(resp *http.Response, expected digest.Digest) error { + digestStr := resp.Header.Get(headerDockerContentDigest) + + if len(digestStr) == 0 { + return nil + } + + contentDigest, err := digest.Parse(digestStr) + if err != nil { + return fmt.Errorf( + "%s %q: invalid response header: `%s: %s`", + resp.Request.Method, resp.Request.URL, + headerDockerContentDigest, digestStr, + ) + } + + if contentDigest != expected { + return fmt.Errorf( + "%s %q: invalid response; digest mismatch in %s: received %q when expecting %q", + resp.Request.Method, resp.Request.URL, + headerDockerContentDigest, contentDigest, + expected, + ) + } + + return nil +} + +// generateIndex generates an image index containing the given manifests list. +func generateIndex(manifests []ocispec.Descriptor) (ocispec.Descriptor, []byte, error) { + if manifests == nil { + manifests = []ocispec.Descriptor{} // make it an empty array to prevent potential server-side bugs + } + index := ocispec.Index{ + Versioned: specs.Versioned{ + SchemaVersion: 2, // historical value. does not pertain to OCI or docker version + }, + MediaType: ocispec.MediaTypeImageIndex, + Manifests: manifests, + } + indexJSON, err := json.Marshal(index) + if err != nil { + return ocispec.Descriptor{}, nil, err + } + indexDesc := content.NewDescriptorFromBytes(index.MediaType, indexJSON) + return indexDesc, indexJSON, nil +} diff --git a/vendor/oras.land/oras-go/v2/registry/remote/retry/client.go b/vendor/oras.land/oras-go/v2/registry/remote/retry/client.go new file mode 100644 index 00000000..5e986ea0 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/registry/remote/retry/client.go @@ -0,0 +1,114 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package retry + +import ( + "net/http" + "time" +) + +// DefaultClient is a client with the default retry policy. +var DefaultClient = NewClient() + +// NewClient creates an HTTP client with the default retry policy. +func NewClient() *http.Client { + return &http.Client{ + Transport: NewTransport(nil), + } +} + +// Transport is an HTTP transport with retry policy. +type Transport struct { + // Base is the underlying HTTP transport to use. + // If nil, http.DefaultTransport is used for round trips. + Base http.RoundTripper + + // Policy returns a retry Policy to use for the request. + // If nil, DefaultPolicy is used to determine if the request should be retried. + Policy func() Policy +} + +// NewTransport creates an HTTP Transport with the default retry policy. +func NewTransport(base http.RoundTripper) *Transport { + return &Transport{ + Base: base, + } +} + +// RoundTrip executes a single HTTP transaction, returning a Response for the +// provided Request. +// It relies on the configured Policy to determine if the request should be +// retried and to backoff. +func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { + ctx := req.Context() + policy := t.policy() + attempt := 0 + for { + resp, respErr := t.roundTrip(req) + duration, err := policy.Retry(attempt, resp, respErr) + if err != nil { + if respErr == nil { + resp.Body.Close() + } + return nil, err + } + if duration < 0 { + return resp, respErr + } + + // rewind the body if possible + if req.Body != nil { + if req.GetBody == nil { + // body can't be rewound, so we can't retry + return resp, respErr + } + body, err := req.GetBody() + if err != nil { + // failed to rewind the body, so we can't retry + return resp, respErr + } + req.Body = body + } + + // close the response body if needed + if respErr == nil { + resp.Body.Close() + } + + timer := time.NewTimer(duration) + select { + case <-ctx.Done(): + timer.Stop() + return nil, ctx.Err() + case <-timer.C: + } + attempt++ + } +} + +func (t *Transport) roundTrip(req *http.Request) (*http.Response, error) { + if t.Base == nil { + return http.DefaultTransport.RoundTrip(req) + } + return t.Base.RoundTrip(req) +} + +func (t *Transport) policy() Policy { + if t.Policy == nil { + return DefaultPolicy + } + return t.Policy() +} diff --git a/vendor/oras.land/oras-go/v2/registry/remote/retry/policy.go b/vendor/oras.land/oras-go/v2/registry/remote/retry/policy.go new file mode 100644 index 00000000..fe7fadee --- /dev/null +++ b/vendor/oras.land/oras-go/v2/registry/remote/retry/policy.go @@ -0,0 +1,154 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package retry + +import ( + "hash/maphash" + "math" + "math/rand" + "net" + "net/http" + "strconv" + "time" +) + +// headerRetryAfter is the header key for Retry-After. +const headerRetryAfter = "Retry-After" + +// DefaultPolicy is a policy with fine-tuned retry parameters. +// It uses an exponential backoff with jitter. +var DefaultPolicy Policy = &GenericPolicy{ + Retryable: DefaultPredicate, + Backoff: DefaultBackoff, + MinWait: 200 * time.Millisecond, + MaxWait: 3 * time.Second, + MaxRetry: 5, +} + +// DefaultPredicate is a predicate that retries on 5xx errors, 429 Too Many +// Requests, 408 Request Timeout and on network dial timeout. +var DefaultPredicate Predicate = func(resp *http.Response, err error) (bool, error) { + if err != nil { + // retry on Dial timeout + if err, ok := err.(net.Error); ok && err.Timeout() { + return true, nil + } + return false, err + } + + if resp.StatusCode == http.StatusRequestTimeout || resp.StatusCode == http.StatusTooManyRequests { + return true, nil + } + + if resp.StatusCode == 0 || resp.StatusCode >= 500 { + return true, nil + } + + return false, nil +} + +// DefaultBackoff is a backoff that uses an exponential backoff with jitter. +// It uses a base of 250ms, a factor of 2 and a jitter of 10%. +var DefaultBackoff Backoff = ExponentialBackoff(250*time.Millisecond, 2, 0.1) + +// Policy is a retry policy. +type Policy interface { + // Retry returns the duration to wait before retrying the request. + // It returns a negative value if the request should not be retried. + // The attempt is used to: + // - calculate the backoff duration, the default backoff is an exponential backoff. + // - determine if the request should be retried. + // The attempt starts at 0 and should be less than MaxRetry for the request to + // be retried. + Retry(attempt int, resp *http.Response, err error) (time.Duration, error) +} + +// Predicate is a function that returns true if the request should be retried. +type Predicate func(resp *http.Response, err error) (bool, error) + +// Backoff is a function that returns the duration to wait before retrying the +// request. The attempt, is the next attempt number. The response is the +// response from the previous request. +type Backoff func(attempt int, resp *http.Response) time.Duration + +// ExponentialBackoff returns a Backoff that uses an exponential backoff with +// jitter. The backoff is calculated as: +// +// temp = backoff * factor ^ attempt +// interval = temp * (1 - jitter) + rand.Int63n(2 * jitter * temp) +// +// The HTTP response is checked for a Retry-After header. If it is present, the +// value is used as the backoff duration. +func ExponentialBackoff(backoff time.Duration, factor, jitter float64) Backoff { + return func(attempt int, resp *http.Response) time.Duration { + var h maphash.Hash + h.SetSeed(maphash.MakeSeed()) + rand := rand.New(rand.NewSource(int64(h.Sum64()))) + + // check Retry-After + if resp != nil && resp.StatusCode == http.StatusTooManyRequests { + if v := resp.Header.Get(headerRetryAfter); v != "" { + if retryAfter, _ := strconv.ParseInt(v, 10, 64); retryAfter > 0 { + return time.Duration(retryAfter) * time.Second + } + } + } + + // do exponential backoff with jitter + temp := float64(backoff) * math.Pow(factor, float64(attempt)) + return time.Duration(temp*(1-jitter)) + time.Duration(rand.Int63n(int64(2*jitter*temp))) + } +} + +// GenericPolicy is a generic retry policy. +type GenericPolicy struct { + // Retryable is a predicate that returns true if the request should be + // retried. + Retryable Predicate + + // Backoff is a function that returns the duration to wait before retrying. + Backoff Backoff + + // MinWait is the minimum duration to wait before retrying. + MinWait time.Duration + + // MaxWait is the maximum duration to wait before retrying. + MaxWait time.Duration + + // MaxRetry is the maximum number of retries. + MaxRetry int +} + +// Retry returns the duration to wait before retrying the request. +// It returns -1 if the request should not be retried. +func (p *GenericPolicy) Retry(attempt int, resp *http.Response, err error) (time.Duration, error) { + if attempt >= p.MaxRetry { + return -1, nil + } + if ok, err := p.Retryable(resp, err); err != nil { + return -1, err + } else if !ok { + return -1, nil + } + backoff := p.Backoff(attempt, resp) + if backoff < p.MinWait { + backoff = p.MinWait + } + if backoff > p.MaxWait { + backoff = p.MaxWait + } + return backoff, nil +} diff --git a/vendor/oras.land/oras-go/v2/registry/remote/url.go b/vendor/oras.land/oras-go/v2/registry/remote/url.go new file mode 100644 index 00000000..2d4b422b --- /dev/null +++ b/vendor/oras.land/oras-go/v2/registry/remote/url.go @@ -0,0 +1,119 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package remote + +import ( + "fmt" + "net/url" + "strings" + + "github.com/opencontainers/go-digest" + "oras.land/oras-go/v2/registry" +) + +// buildScheme returns HTTP scheme used to access the remote registry. +func buildScheme(plainHTTP bool) string { + if plainHTTP { + return "http" + } + return "https" +} + +// buildRegistryBaseURL builds the URL for accessing the base API. +// Format: :///v2/ +// Reference: https://docs.docker.com/registry/spec/api/#base +func buildRegistryBaseURL(plainHTTP bool, ref registry.Reference) string { + return fmt.Sprintf("%s://%s/v2/", buildScheme(plainHTTP), ref.Host()) +} + +// buildRegistryCatalogURL builds the URL for accessing the catalog API. +// Format: :///v2/_catalog +// Reference: https://docs.docker.com/registry/spec/api/#catalog +func buildRegistryCatalogURL(plainHTTP bool, ref registry.Reference) string { + return fmt.Sprintf("%s://%s/v2/_catalog", buildScheme(plainHTTP), ref.Host()) +} + +// buildRepositoryBaseURL builds the base endpoint of the remote repository. +// Format: :///v2/ +func buildRepositoryBaseURL(plainHTTP bool, ref registry.Reference) string { + return fmt.Sprintf("%s://%s/v2/%s", buildScheme(plainHTTP), ref.Host(), ref.Repository) +} + +// buildRepositoryTagListURL builds the URL for accessing the tag list API. +// Format: :///v2//tags/list +// Reference: https://docs.docker.com/registry/spec/api/#tags +func buildRepositoryTagListURL(plainHTTP bool, ref registry.Reference) string { + return buildRepositoryBaseURL(plainHTTP, ref) + "/tags/list" +} + +// buildRepositoryManifestURL builds the URL for accessing the manifest API. +// Format: :///v2//manifests/ +// Reference: https://docs.docker.com/registry/spec/api/#manifest +func buildRepositoryManifestURL(plainHTTP bool, ref registry.Reference) string { + return strings.Join([]string{ + buildRepositoryBaseURL(plainHTTP, ref), + "manifests", + ref.Reference, + }, "/") +} + +// buildRepositoryBlobURL builds the URL for accessing the blob API. +// Format: :///v2//blobs/ +// Reference: https://docs.docker.com/registry/spec/api/#blob +func buildRepositoryBlobURL(plainHTTP bool, ref registry.Reference) string { + return strings.Join([]string{ + buildRepositoryBaseURL(plainHTTP, ref), + "blobs", + ref.Reference, + }, "/") +} + +// buildRepositoryBlobUploadURL builds the URL for blob uploading. +// Format: :///v2//blobs/uploads/ +// Reference: https://docs.docker.com/registry/spec/api/#initiate-blob-upload +func buildRepositoryBlobUploadURL(plainHTTP bool, ref registry.Reference) string { + return buildRepositoryBaseURL(plainHTTP, ref) + "/blobs/uploads/" +} + +// buildRepositoryBlobMountURLbuilds the URL for cross-repository mounting. +// Format: :///v2//blobs/uploads/?mount=&from= +// Reference: https://docs.docker.com/registry/spec/api/#blob +func buildRepositoryBlobMountURL(plainHTTP bool, ref registry.Reference, d digest.Digest, fromRepo string) string { + return fmt.Sprintf("%s?mount=%s&from=%s", + buildRepositoryBlobUploadURL(plainHTTP, ref), + d, + fromRepo, + ) +} + +// buildReferrersURL builds the URL for querying the Referrers API. +// Format: :///v2//referrers/?artifactType= +// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers +func buildReferrersURL(plainHTTP bool, ref registry.Reference, artifactType string) string { + var query string + if artifactType != "" { + v := url.Values{} + v.Set("artifactType", artifactType) + query = "?" + v.Encode() + } + + return fmt.Sprintf( + "%s/referrers/%s%s", + buildRepositoryBaseURL(plainHTTP, ref), + ref.Reference, + query, + ) +} diff --git a/vendor/oras.land/oras-go/v2/registry/remote/utils.go b/vendor/oras.land/oras-go/v2/registry/remote/utils.go new file mode 100644 index 00000000..797169f4 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/registry/remote/utils.go @@ -0,0 +1,94 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package remote + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/content" + "oras.land/oras-go/v2/errdef" +) + +// defaultMaxMetadataBytes specifies the default limit on how many response +// bytes are allowed in the server's response to the metadata APIs. +// See also: Repository.MaxMetadataBytes +var defaultMaxMetadataBytes int64 = 4 * 1024 * 1024 // 4 MiB + +// errNoLink is returned by parseLink() when no Link header is present. +var errNoLink = errors.New("no Link header in response") + +// parseLink returns the URL of the response's "Link" header, if present. +func parseLink(resp *http.Response) (string, error) { + link := resp.Header.Get("Link") + if link == "" { + return "", errNoLink + } + if link[0] != '<' { + return "", fmt.Errorf("invalid next link %q: missing '<'", link) + } + if i := strings.IndexByte(link, '>'); i == -1 { + return "", fmt.Errorf("invalid next link %q: missing '>'", link) + } else { + link = link[1:i] + } + + linkURL, err := resp.Request.URL.Parse(link) + if err != nil { + return "", err + } + return linkURL.String(), nil +} + +// limitReader returns a Reader that reads from r but stops with EOF after n +// bytes. If n is less than or equal to zero, defaultMaxMetadataBytes is used. +func limitReader(r io.Reader, n int64) io.Reader { + if n <= 0 { + n = defaultMaxMetadataBytes + } + return io.LimitReader(r, n) +} + +// limitSize returns ErrSizeExceedsLimit if the size of desc exceeds the limit n. +// If n is less than or equal to zero, defaultMaxMetadataBytes is used. +func limitSize(desc ocispec.Descriptor, n int64) error { + if n <= 0 { + n = defaultMaxMetadataBytes + } + if desc.Size > n { + return fmt.Errorf( + "content size %v exceeds MaxMetadataBytes %v: %w", + desc.Size, + n, + errdef.ErrSizeExceedsLimit) + } + return nil +} + +// decodeJSON safely reads the JSON content described by desc, and +// decodes it into v. +func decodeJSON(r io.Reader, desc ocispec.Descriptor, v any) error { + jsonBytes, err := content.ReadAll(r, desc) + if err != nil { + return err + } + return json.Unmarshal(jsonBytes, v) +} diff --git a/vendor/oras.land/oras-go/v2/registry/remote/warning.go b/vendor/oras.land/oras-go/v2/registry/remote/warning.go new file mode 100644 index 00000000..20f5071f --- /dev/null +++ b/vendor/oras.land/oras-go/v2/registry/remote/warning.go @@ -0,0 +1,100 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package remote + +import ( + "errors" + "fmt" + "strconv" + "strings" +) + +const ( + // headerWarning is the "Warning" header. + // Reference: https://www.rfc-editor.org/rfc/rfc7234#section-5.5 + headerWarning = "Warning" + + // warnCode299 is the 299 warn-code. + // Reference: https://www.rfc-editor.org/rfc/rfc7234#section-5.5 + warnCode299 = 299 + + // warnAgentUnknown represents an unknown warn-agent. + // Reference: https://www.rfc-editor.org/rfc/rfc7234#section-5.5 + warnAgentUnknown = "-" +) + +// errUnexpectedWarningFormat is returned by parseWarningHeader when +// an unexpected warning format is encountered. +var errUnexpectedWarningFormat = errors.New("unexpected warning format") + +// WarningValue represents the value of the Warning header. +// +// References: +// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#warnings +// - https://www.rfc-editor.org/rfc/rfc7234#section-5.5 +type WarningValue struct { + // Code is the warn-code. + Code int + // Agent is the warn-agent. + Agent string + // Text is the warn-text. + Text string +} + +// Warning contains the value of the warning header and may contain +// other information related to the warning. +// +// References: +// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#warnings +// - https://www.rfc-editor.org/rfc/rfc7234#section-5.5 +type Warning struct { + // WarningValue is the value of the warning header. + WarningValue +} + +// parseWarningHeader parses the warning header into WarningValue. +func parseWarningHeader(header string) (WarningValue, error) { + if len(header) < 9 || !strings.HasPrefix(header, `299 - "`) || !strings.HasSuffix(header, `"`) { + // minimum header value: `299 - "x"` + return WarningValue{}, fmt.Errorf("%s: %w", header, errUnexpectedWarningFormat) + } + + // validate text only as code and agent are fixed + quotedText := header[6:] // behind `299 - `, quoted by " + text, err := strconv.Unquote(quotedText) + if err != nil { + return WarningValue{}, fmt.Errorf("%s: unexpected text: %w: %v", header, errUnexpectedWarningFormat, err) + } + + return WarningValue{ + Code: warnCode299, + Agent: warnAgentUnknown, + Text: text, + }, nil +} + +// handleWarningHeaders parses the warning headers and handles the parsed +// warnings using handleWarning. +func handleWarningHeaders(headers []string, handleWarning func(Warning)) { + for _, h := range headers { + if value, err := parseWarningHeader(h); err == nil { + // ignore warnings in unexpected formats + handleWarning(Warning{ + WarningValue: value, + }) + } + } +} diff --git a/vendor/oras.land/oras-go/v2/registry/repository.go b/vendor/oras.land/oras-go/v2/registry/repository.go new file mode 100644 index 00000000..84a50e2a --- /dev/null +++ b/vendor/oras.land/oras-go/v2/registry/repository.go @@ -0,0 +1,226 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package registry + +import ( + "context" + "encoding/json" + "fmt" + "io" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/content" + "oras.land/oras-go/v2/errdef" + "oras.land/oras-go/v2/internal/descriptor" + "oras.land/oras-go/v2/internal/spec" +) + +// Repository is an ORAS target and an union of the blob and the manifest CASs. +// +// As specified by https://docs.docker.com/registry/spec/api/, it is natural to +// assume that content.Resolver interface only works for manifests. Tagging a +// blob may be resulted in an `ErrUnsupported` error. However, this interface +// does not restrict tagging blobs. +// +// Since a repository is an union of the blob and the manifest CASs, all +// operations defined in the `BlobStore` are executed depending on the media +// type of the given descriptor accordingly. +// +// Furthermore, this interface also provides the ability to enforce the +// separation of the blob and the manifests CASs. +type Repository interface { + content.Storage + content.Deleter + content.TagResolver + ReferenceFetcher + ReferencePusher + ReferrerLister + TagLister + + // Blobs provides access to the blob CAS only, which contains config blobs, + // layers, and other generic blobs. + Blobs() BlobStore + + // Manifests provides access to the manifest CAS only. + Manifests() ManifestStore +} + +// BlobStore is a CAS with the ability to stat and delete its content. +type BlobStore interface { + content.Storage + content.Deleter + content.Resolver + ReferenceFetcher +} + +// ManifestStore is a CAS with the ability to stat and delete its content. +// Besides, ManifestStore provides reference tagging. +type ManifestStore interface { + BlobStore + content.Tagger + ReferencePusher +} + +// ReferencePusher provides advanced push with the tag service. +type ReferencePusher interface { + // PushReference pushes the manifest with a reference tag. + PushReference(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error +} + +// ReferenceFetcher provides advanced fetch with the tag service. +type ReferenceFetcher interface { + // FetchReference fetches the content identified by the reference. + FetchReference(ctx context.Context, reference string) (ocispec.Descriptor, io.ReadCloser, error) +} + +// ReferrerLister provides the Referrers API. +// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers +type ReferrerLister interface { + Referrers(ctx context.Context, desc ocispec.Descriptor, artifactType string, fn func(referrers []ocispec.Descriptor) error) error +} + +// TagLister lists tags by the tag service. +type TagLister interface { + // Tags lists the tags available in the repository. + // Since the returned tag list may be paginated by the underlying + // implementation, a function should be passed in to process the paginated + // tag list. + // + // `last` argument is the `last` parameter when invoking the tags API. + // If `last` is NOT empty, the entries in the response start after the + // tag specified by `last`. Otherwise, the response starts from the top + // of the Tags list. + // + // Note: When implemented by a remote registry, the tags API is called. + // However, not all registries supports pagination or conforms the + // specification. + // + // References: + // - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#content-discovery + // - https://docs.docker.com/registry/spec/api/#tags + // See also `Tags()` in this package. + Tags(ctx context.Context, last string, fn func(tags []string) error) error +} + +// Mounter allows cross-repository blob mounts. +// For backward compatibility reasons, this is not implemented by +// BlobStore: use a type assertion to check availability. +type Mounter interface { + // Mount makes the blob with the given descriptor in fromRepo + // available in the repository signified by the receiver. + Mount(ctx context.Context, + desc ocispec.Descriptor, + fromRepo string, + getContent func() (io.ReadCloser, error), + ) error +} + +// Tags lists the tags available in the repository. +func Tags(ctx context.Context, repo TagLister) ([]string, error) { + var res []string + if err := repo.Tags(ctx, "", func(tags []string) error { + res = append(res, tags...) + return nil + }); err != nil { + return nil, err + } + return res, nil +} + +// Referrers lists the descriptors of image or artifact manifests directly +// referencing the given manifest descriptor. +// +// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#listing-referrers +func Referrers(ctx context.Context, store content.ReadOnlyGraphStorage, desc ocispec.Descriptor, artifactType string) ([]ocispec.Descriptor, error) { + if !descriptor.IsManifest(desc) { + return nil, fmt.Errorf("the descriptor %v is not a manifest: %w", desc, errdef.ErrUnsupported) + } + + var results []ocispec.Descriptor + + // use the Referrer API if it is available + if rf, ok := store.(ReferrerLister); ok { + if err := rf.Referrers(ctx, desc, artifactType, func(referrers []ocispec.Descriptor) error { + results = append(results, referrers...) + return nil + }); err != nil { + return nil, err + } + return results, nil + } + + predecessors, err := store.Predecessors(ctx, desc) + if err != nil { + return nil, err + } + for _, node := range predecessors { + switch node.MediaType { + case ocispec.MediaTypeImageManifest: + fetched, err := content.FetchAll(ctx, store, node) + if err != nil { + return nil, err + } + var manifest ocispec.Manifest + if err := json.Unmarshal(fetched, &manifest); err != nil { + return nil, err + } + if manifest.Subject == nil || !content.Equal(*manifest.Subject, desc) { + continue + } + node.ArtifactType = manifest.ArtifactType + if node.ArtifactType == "" { + node.ArtifactType = manifest.Config.MediaType + } + node.Annotations = manifest.Annotations + case ocispec.MediaTypeImageIndex: + fetched, err := content.FetchAll(ctx, store, node) + if err != nil { + return nil, err + } + var index ocispec.Index + if err := json.Unmarshal(fetched, &index); err != nil { + return nil, err + } + if index.Subject == nil || !content.Equal(*index.Subject, desc) { + continue + } + node.ArtifactType = index.ArtifactType + node.Annotations = index.Annotations + case spec.MediaTypeArtifactManifest: + fetched, err := content.FetchAll(ctx, store, node) + if err != nil { + return nil, err + } + var artifact spec.Artifact + if err := json.Unmarshal(fetched, &artifact); err != nil { + return nil, err + } + if artifact.Subject == nil || !content.Equal(*artifact.Subject, desc) { + continue + } + node.ArtifactType = artifact.ArtifactType + node.Annotations = artifact.Annotations + default: + continue + } + if artifactType == "" || artifactType == node.ArtifactType { + // the field artifactType in referrers descriptor is allowed to be empty + // https://github.com/opencontainers/distribution-spec/issues/458 + results = append(results, node) + } + } + return results, nil +} diff --git a/vendor/oras.land/oras-go/v2/target.go b/vendor/oras.land/oras-go/v2/target.go new file mode 100644 index 00000000..c6dcaef9 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/target.go @@ -0,0 +1,43 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package oras + +import "oras.land/oras-go/v2/content" + +// Target is a CAS with generic tags. +type Target interface { + content.Storage + content.TagResolver +} + +// GraphTarget is a CAS with generic tags that supports direct predecessor node +// finding. +type GraphTarget interface { + content.GraphStorage + content.TagResolver +} + +// ReadOnlyTarget represents a read-only Target. +type ReadOnlyTarget interface { + content.ReadOnlyStorage + content.Resolver +} + +// ReadOnlyGraphTarget represents a read-only GraphTarget. +type ReadOnlyGraphTarget interface { + content.ReadOnlyGraphStorage + content.Resolver +}