diff --git a/api/v1beta2/ocirepository_types.go b/api/v1beta2/ocirepository_types.go index bc6d830ba..eac3f1c40 100644 --- a/api/v1beta2/ocirepository_types.go +++ b/api/v1beta2/ocirepository_types.go @@ -25,12 +25,16 @@ import ( const ( // OCIRepositoryKind is the string representation of a OCIRepository. OCIRepositoryKind = "OCIRepository" + + // OCIRepositoryPrefix is the prefix used for OCIRepository URLs. + OCIRepositoryPrefix = "oci://" ) // OCIRepositorySpec defines the desired state of OCIRepository type OCIRepositorySpec struct { // URL is a reference to an OCI artifact repository hosted // on a remote container registry. + // +kubebuilder:validation:Pattern="^oci://" // +required URL string `json:"url"` diff --git a/config/samples/source_v1beta2_ocirepository.yaml b/config/samples/source_v1beta2_ocirepository.yaml index 2fbdf9969..e06241b97 100644 --- a/config/samples/source_v1beta2_ocirepository.yaml +++ b/config/samples/source_v1beta2_ocirepository.yaml @@ -4,6 +4,6 @@ metadata: name: ocirepository-sample spec: interval: 1m - url: ghcr.io/stefanprodan/manifests/podinfo + url: oci://ghcr.io/stefanprodan/manifests/podinfo ref: tag: 6.1.6 diff --git a/controllers/ocirepository_controller.go b/controllers/ocirepository_controller.go index 0e441f8a5..16e40a90a 100644 --- a/controllers/ocirepository_controller.go +++ b/controllers/ocirepository_controller.go @@ -20,8 +20,10 @@ import ( "context" "errors" "fmt" + "github.com/google/go-containerregistry/pkg/name" "os" "sort" + "strings" "time" "github.com/Masterminds/semver/v3" @@ -363,12 +365,31 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour return sreconcile.ResultSuccess, nil } +// parseRepositoryURL extracts the repository URL. +func (r *OCIRepositoryReconciler) parseRepositoryURL(obj *sourcev1.OCIRepository) (string, error) { + if !strings.HasPrefix(obj.Spec.URL, sourcev1.OCIRepositoryPrefix) { + return "", fmt.Errorf("URL must be in format 'oci:////'") + } + + url := strings.TrimPrefix(obj.Spec.URL, sourcev1.OCIRepositoryPrefix) + ref, err := name.ParseReference(url) + if err != nil { + return "", fmt.Errorf("'%s' invalid URL: %w", obj.Spec.URL, err) + } + + return ref.Context().Name(), nil +} + // getArtifactURL determines which tag or digest should be used and returns the OCI artifact FQN. func (r *OCIRepositoryReconciler) getArtifactURL(ctx context.Context, obj *sourcev1.OCIRepository, keychain authn.Keychain) (string, error) { - url := obj.Spec.URL + url, err := r.parseRepositoryURL(obj) + if err != nil { + return "", err + } + if obj.Spec.Reference != nil { if obj.Spec.Reference.Digest != "" { - return fmt.Sprintf("%s@%s", obj.Spec.URL, obj.Spec.Reference.Digest), nil + return fmt.Sprintf("%s@%s", url, obj.Spec.Reference.Digest), nil } if obj.Spec.Reference.SemVer != "" { @@ -376,11 +397,11 @@ func (r *OCIRepositoryReconciler) getArtifactURL(ctx context.Context, obj *sourc if err != nil { return "", err } - return fmt.Sprintf("%s:%s", obj.Spec.URL, tag), nil + return fmt.Sprintf("%s:%s", url, tag), nil } if obj.Spec.Reference.Tag != "" { - return fmt.Sprintf("%s:%s", obj.Spec.URL, obj.Spec.Reference.Tag), nil + return fmt.Sprintf("%s:%s", url, obj.Spec.Reference.Tag), nil } } diff --git a/controllers/ocirepository_controller_test.go b/controllers/ocirepository_controller_test.go index bcae3ad1d..5963702f0 100644 --- a/controllers/ocirepository_controller_test.go +++ b/controllers/ocirepository_controller_test.go @@ -239,6 +239,7 @@ func TestOCIRepository_SecretRef(t *testing.T) { g.Expect(err).ToNot(HaveOccurred()) repositoryURL := fmt.Sprintf("%s/podinfo", regServer.registryHost) + ociURL := fmt.Sprintf("oci://%s", repositoryURL) // Push Test Image err = crane.Push(image, repositoryURL, crane.WithAuth(&authn.Basic{ @@ -260,14 +261,14 @@ func TestOCIRepository_SecretRef(t *testing.T) { }{ { name: "private-registry-access-via-secretref", - url: repositoryURL, + url: ociURL, digest: podinfoImageDigest, includeSecretRef: true, includeServiceAccount: false, }, { name: "private-registry-access-via-serviceaccount", - url: repositoryURL, + url: ociURL, digest: podinfoImageDigest, includeSecretRef: false, includeServiceAccount: true, @@ -289,7 +290,7 @@ func TestOCIRepository_SecretRef(t *testing.T) { }, Type: corev1.SecretTypeDockerConfigJson, StringData: map[string]string{ - ".dockerconfigjson": fmt.Sprintf(`{"auths": {%q: {"username": %q, "password": %q}}}`, tt.url, testRegistryUsername, testRegistryPassword), + ".dockerconfigjson": fmt.Sprintf(`{"auths": {%q: {"username": %q, "password": %q}}}`, repositoryURL, testRegistryUsername, testRegistryPassword), }, } g.Expect(testEnv.CreateAndWait(ctx, secret)).To(Succeed()) @@ -435,6 +436,7 @@ func TestOCIRepository_FailedAuth(t *testing.T) { g.Expect(err).ToNot(HaveOccurred()) repositoryURL := fmt.Sprintf("%s/podinfo", regServer.registryHost) + ociURL := fmt.Sprintf("oci://%s", repositoryURL) // Push Test Image err = crane.Push(image, repositoryURL, crane.WithAuth(&authn.Basic{ @@ -458,7 +460,7 @@ func TestOCIRepository_FailedAuth(t *testing.T) { }{ { name: "missing-auth", - url: repositoryURL, + url: ociURL, repoUsername: "", repoPassword: "", digest: podinfoImageDigest, @@ -467,7 +469,7 @@ func TestOCIRepository_FailedAuth(t *testing.T) { }, { name: "invalid-auth-via-secret", - url: repositoryURL, + url: ociURL, repoUsername: "InvalidUser", repoPassword: "InvalidPassword", digest: podinfoImageDigest, @@ -476,7 +478,7 @@ func TestOCIRepository_FailedAuth(t *testing.T) { }, { name: "invalid-auth-via-service-account", - url: repositoryURL, + url: ociURL, repoUsername: "InvalidUser", repoPassword: "InvalidPassword", digest: podinfoImageDigest, @@ -500,7 +502,7 @@ func TestOCIRepository_FailedAuth(t *testing.T) { }, Type: corev1.SecretTypeDockerConfigJson, StringData: map[string]string{ - ".dockerconfigjson": fmt.Sprintf(`{"auths": {%q: {"username": %q, "password": %q}}}`, tt.url, tt.repoUsername, tt.repoPassword), + ".dockerconfigjson": fmt.Sprintf(`{"auths": {%q: {"username": %q, "password": %q}}}`, repositoryURL, tt.repoUsername, tt.repoPassword), }, } g.Expect(testEnv.CreateAndWait(ctx, secret)).To(Succeed()) @@ -623,7 +625,7 @@ func createPodinfoImageFromTar(tarFileName, tag string, imageServer *httptest.Se } return &podinfoImage{ - url: repositoryURL, + url: "oci://" + repositoryURL, tag: tag, digest: podinfoImageDigest, }, nil