diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 709912de..6b8566e9 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -175,17 +175,17 @@ jobs: /tmp/flux delete source git podinfo --silent - name: flux oci artifacts run: | - /tmp/flux push artifact localhost:5000/fluxcd/flux:${{ github.sha }} \ + /tmp/flux push artifact oci://localhost:5000/fluxcd/flux:${{ github.sha }} \ --path="./manifests" \ --source="${{ github.repositoryUrl }}" \ --revision="${{ github.ref }}/${{ github.sha }}" - /tmp/flux tag artifact localhost:5000/fluxcd/flux:${{ github.sha }} \ + /tmp/flux tag artifact oci://localhost:5000/fluxcd/flux:${{ github.sha }} \ --tag latest - /tmp/flux list artifacts localhost:5000/fluxcd/flux + /tmp/flux list artifacts oci://localhost:5000/fluxcd/flux - name: flux oci repositories run: | /tmp/flux create source oci podinfo-oci \ - --url ghcr.io/stefanprodan/manifests/podinfo \ + --url oci://ghcr.io/stefanprodan/manifests/podinfo \ --tag-semver 6.1.x \ --interval 10m /tmp/flux create kustomization podinfo-oci \ diff --git a/cmd/flux/create_source_oci.go b/cmd/flux/create_source_oci.go index a97c64fb..dd2b4fbf 100644 --- a/cmd/flux/create_source_oci.go +++ b/cmd/flux/create_source_oci.go @@ -42,7 +42,7 @@ var createSourceOCIRepositoryCmd = &cobra.Command{ Long: `The create source oci command generates an OCIRepository resource and waits for it to be ready.`, Example: ` # Create an OCIRepository for a public container image flux create source oci podinfo \ - --url=ghcr.io/stefanprodan/manifests/podinfo \ + --url=oci://ghcr.io/stefanprodan/manifests/podinfo \ --tag=6.1.6 \ --interval=10m `, @@ -67,7 +67,7 @@ func init() { createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.semver, "tag-semver", "", "the OCI artifact tag semver range") createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.digest, "digest", "", "the OCI artifact digest") createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.secretRef, "secret-ref", "", "the name of the Kubernetes image pull secret (type 'kubernetes.io/dockerconfigjson')") - createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.secretRef, "service-account", "", "the name of the Kubernetes service account that refers to an image pull secret") + createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.serviceAccount, "service-account", "", "the name of the Kubernetes service account that refers to an image pull secret") createSourceOCIRepositoryCmd.Flags().StringSliceVar(&sourceOCIRepositoryArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore resources (can specify multiple paths with commas: path1,path2)") createSourceCmd.AddCommand(createSourceOCIRepositoryCmd) diff --git a/cmd/flux/create_source_oci_test.go b/cmd/flux/create_source_oci_test.go index 027c1789..04be9cb1 100644 --- a/cmd/flux/create_source_oci_test.go +++ b/cmd/flux/create_source_oci_test.go @@ -38,12 +38,12 @@ func TestCreateSourceOCI(t *testing.T) { }, { name: "export manifest", - args: "create source oci podinfo --url=ghcr.io/stefanprodan/manifests/podinfo --tag=6.1.6 --interval 10m --export", + args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.1.6 --interval 10m --export", assertFunc: assertGoldenFile("./testdata/oci/export.golden"), }, { name: "export manifest with secret", - args: "create source oci podinfo --url=ghcr.io/stefanprodan/manifests/podinfo --tag=6.1.6 --interval 10m --secret-ref=creds --export", + args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.1.6 --interval 10m --secret-ref=creds --export", assertFunc: assertGoldenFile("./testdata/oci/export_with_secret.golden"), }, } diff --git a/cmd/flux/list_artifact.go b/cmd/flux/list_artifact.go index a53e4f99..e499fc73 100644 --- a/cmd/flux/list_artifact.go +++ b/cmd/flux/list_artifact.go @@ -30,7 +30,7 @@ var listArtifactsCmd = &cobra.Command{ Long: `The list command fetches the tags and their metadata from a remote OCI repository. The list command uses the credentials from '~/.docker/config.json'.`, Example: `# list the artifacts stored in an OCI repository -flux list artifact ghcr.io/org/manifests/app +flux list artifact oci://ghcr.io/org/manifests/app `, RunE: listArtifactsCmdRun, } @@ -41,13 +41,18 @@ func init() { func listArtifactsCmdRun(cmd *cobra.Command, args []string) error { if len(args) < 1 { - return fmt.Errorf("artifact repository is required") + return fmt.Errorf("artifact repository URL is required") } - url := args[0] + ociURL := args[0] ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() + url, err := oci.ParseArtifactURL(ociURL) + if err != nil { + return err + } + metas, err := oci.List(ctx, url) if err != nil { return err diff --git a/cmd/flux/main_test.go b/cmd/flux/main_test.go index 59568dee..2adf4715 100644 --- a/cmd/flux/main_test.go +++ b/cmd/flux/main_test.go @@ -387,6 +387,9 @@ func resetCmdArgs() { createArgs = createFlags{} getArgs = GetFlags{} sourceHelmArgs = sourceHelmFlags{} + sourceOCIRepositoryArgs = sourceOCIRepositoryFlags{} + sourceGitArgs = sourceGitFlags{} + sourceBucketArgs = sourceBucketFlags{} secretGitArgs = NewSecretGitFlags() *kubeconfigArgs.Namespace = rootArgs.defaults.Namespace } diff --git a/cmd/flux/pull_artifact.go b/cmd/flux/pull_artifact.go index 37560f68..382cdc69 100644 --- a/cmd/flux/pull_artifact.go +++ b/cmd/flux/pull_artifact.go @@ -32,7 +32,7 @@ var pullArtifactCmd = &cobra.Command{ Long: `The pull artifact command downloads and extracts the OCI artifact content to the given path. The pull command uses the credentials from '~/.docker/config.json'.`, Example: `# Pull an OCI artifact created by flux from GHCR -flux pull artifact ghcr.io/org/manifests/app:v0.0.1 --output ./path/to/local/manifests +flux pull artifact oci://ghcr.io/org/manifests/app:v0.0.1 --output ./path/to/local/manifests `, RunE: pullArtifactCmdRun, } @@ -50,9 +50,9 @@ func init() { func pullArtifactCmdRun(cmd *cobra.Command, args []string) error { if len(args) < 1 { - return fmt.Errorf("artifact name is required") + return fmt.Errorf("artifact URL is required") } - url := args[0] + ociURL := args[0] if pullArtifactArgs.output == "" { return fmt.Errorf("invalid output path %s", pullArtifactArgs.output) @@ -62,6 +62,11 @@ func pullArtifactCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("invalid output path %s", pullArtifactArgs.output) } + url, err := oci.ParseArtifactURL(ociURL) + if err != nil { + return err + } + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() diff --git a/cmd/flux/push_artifact.go b/cmd/flux/push_artifact.go index 813dbb46..49f61202 100644 --- a/cmd/flux/push_artifact.go +++ b/cmd/flux/push_artifact.go @@ -32,7 +32,7 @@ var pushArtifactCmd = &cobra.Command{ Long: `The push artifact command creates a tarball from the given directory and uploads the artifact to a OCI repository. The push command uses the credentials from '~/.docker/config.json'.`, Example: `# Push the local manifests to GHCR -flux push artifact ghcr.io/org/manifests/app:v0.0.1 \ +flux push artifact oci://ghcr.io/org/manifests/app:v0.0.1 \ --path="./path/to/local/manifests" \ --source="$(git config --get remote.origin.url)" \ --revision="$(git branch --show-current)/$(git rev-parse HEAD)" @@ -57,9 +57,9 @@ func init() { func pushArtifactCmdRun(cmd *cobra.Command, args []string) error { if len(args) < 1 { - return fmt.Errorf("artifact name is required") + return fmt.Errorf("artifact URL is required") } - url := args[0] + ociURL := args[0] if pushArtifactArgs.source == "" { return fmt.Errorf("--source is required") @@ -73,6 +73,11 @@ func pushArtifactCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("invalid path %q", pushArtifactArgs.path) } + url, err := oci.ParseArtifactURL(ociURL) + if err != nil { + return err + } + if fs, err := os.Stat(pushArtifactArgs.path); err != nil || !fs.IsDir() { return fmt.Errorf("invalid path %q", pushArtifactArgs.path) } diff --git a/cmd/flux/source_oci_test.go b/cmd/flux/source_oci_test.go index 568ac73e..c0f8125c 100644 --- a/cmd/flux/source_oci_test.go +++ b/cmd/flux/source_oci_test.go @@ -29,7 +29,7 @@ func TestSourceOCI(t *testing.T) { goldenFile string }{ { - "create source oci thrfg --url=ghcr.io/stefanprodan/manifests/podinfo --tag=6.1.6 --interval 10m", + "create source oci thrfg --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.1.6 --interval 10m", "testdata/oci/create_source_oci.golden", }, { diff --git a/cmd/flux/tag_artifact.go b/cmd/flux/tag_artifact.go index be0a6387..2004cebc 100644 --- a/cmd/flux/tag_artifact.go +++ b/cmd/flux/tag_artifact.go @@ -29,7 +29,7 @@ var tagArtifactCmd = &cobra.Command{ Long: `The tag artifact command creates tags for the given OCI artifact. The tag command uses the credentials from '~/.docker/config.json'.`, Example: `# Tag an artifact version as latest -flux tag artifact ghcr.io/org/manifests/app:v0.0.1 --tag latest +flux tag artifact oci://ghcr.io/org/manifests/app:v0.0.1 --tag latest `, RunE: tagArtifactCmdRun, } @@ -49,12 +49,17 @@ func tagArtifactCmdRun(cmd *cobra.Command, args []string) error { if len(args) < 1 { return fmt.Errorf("artifact name is required") } - url := args[0] + ociURL := args[0] if len(tagArtifactArgs.tags) < 1 { return fmt.Errorf("--tag is required") } + url, err := oci.ParseArtifactURL(ociURL) + if err != nil { + return err + } + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() diff --git a/cmd/flux/testdata/oci/export.golden b/cmd/flux/testdata/oci/export.golden index 82d5fa19..b9339d73 100644 --- a/cmd/flux/testdata/oci/export.golden +++ b/cmd/flux/testdata/oci/export.golden @@ -8,5 +8,5 @@ spec: interval: 10m0s ref: tag: 6.1.6 - url: ghcr.io/stefanprodan/manifests/podinfo + url: oci://ghcr.io/stefanprodan/manifests/podinfo diff --git a/cmd/flux/testdata/oci/export_with_secret.golden b/cmd/flux/testdata/oci/export_with_secret.golden index 79fd98a5..385f7707 100644 --- a/cmd/flux/testdata/oci/export_with_secret.golden +++ b/cmd/flux/testdata/oci/export_with_secret.golden @@ -10,5 +10,5 @@ spec: tag: 6.1.6 secretRef: name: creds - url: ghcr.io/stefanprodan/manifests/podinfo + url: oci://ghcr.io/stefanprodan/manifests/podinfo diff --git a/internal/oci/url.go b/internal/oci/url.go new file mode 100644 index 00000000..38dafb1f --- /dev/null +++ b/internal/oci/url.go @@ -0,0 +1,54 @@ +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES 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 ( + "fmt" + "strings" + + sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + "github.com/google/go-containerregistry/pkg/name" +) + +// ParseArtifactURL validates the OCI URL and returns the address of the artifact. +func ParseArtifactURL(ociURL string) (string, error) { + if !strings.HasPrefix(ociURL, sourcev1.OCIRepositoryPrefix) { + return "", fmt.Errorf("URL must be in format 'oci:////'") + } + + url := strings.TrimPrefix(ociURL, sourcev1.OCIRepositoryPrefix) + if _, err := name.ParseReference(url); err != nil { + return "", fmt.Errorf("'%s' invalid URL: %w", ociURL, err) + } + + return url, nil +} + +// ParseRepositoryURL validates the OCI URL and returns the address of the artifact repository. +func ParseRepositoryURL(ociURL string) (string, error) { + if !strings.HasPrefix(ociURL, sourcev1.OCIRepositoryPrefix) { + return "", fmt.Errorf("URL must be in format 'oci:////'") + } + + url := strings.TrimPrefix(ociURL, sourcev1.OCIRepositoryPrefix) + ref, err := name.ParseReference(url) + if err != nil { + return "", fmt.Errorf("'%s' invalid URL: %w", ociURL, err) + } + + return ref.Context().Name(), nil +} diff --git a/manifests/bases/source-controller/kustomization.yaml b/manifests/bases/source-controller/kustomization.yaml index 9d2e44e0..f268590c 100644 --- a/manifests/bases/source-controller/kustomization.yaml +++ b/manifests/bases/source-controller/kustomization.yaml @@ -15,4 +15,4 @@ patchesJson6902: # TODO: remove the hardcoded image when OCIRepository is released images: - name: fluxcd/source-controller - newTag: oci-8509ac03 + newTag: oci-ba5f5353