mirror of
				https://github.com/fluxcd/flux2.git
				synced 2025-10-31 08:17:19 +08:00 
			
		
		
		
	Add oci:// prefix
				
					
				
			Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
This commit is contained in:
		
							
								
								
									
										8
									
								
								.github/workflows/e2e.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/e2e.yaml
									
									
									
									
										vendored
									
									
								
							| @ -175,17 +175,17 @@ jobs: | |||||||
|           /tmp/flux delete source git podinfo --silent |           /tmp/flux delete source git podinfo --silent | ||||||
|       - name: flux oci artifacts |       - name: flux oci artifacts | ||||||
|         run: | |         run: | | ||||||
|           /tmp/flux push artifact localhost:5000/fluxcd/flux:${{ github.sha }} \ |           /tmp/flux push artifact oci://localhost:5000/fluxcd/flux:${{ github.sha }} \ | ||||||
|             --path="./manifests" \ |             --path="./manifests" \ | ||||||
|             --source="${{ github.repositoryUrl }}" \ |             --source="${{ github.repositoryUrl }}" \ | ||||||
|             --revision="${{ github.ref }}/${{ github.sha }}" |             --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 |             --tag latest | ||||||
|           /tmp/flux list artifacts localhost:5000/fluxcd/flux |           /tmp/flux list artifacts oci://localhost:5000/fluxcd/flux | ||||||
|       - name: flux oci repositories |       - name: flux oci repositories | ||||||
|         run: | |         run: | | ||||||
|           /tmp/flux create source oci podinfo-oci \ |           /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 \ |             --tag-semver 6.1.x \ | ||||||
|             --interval 10m |             --interval 10m | ||||||
|           /tmp/flux create kustomization podinfo-oci \ |           /tmp/flux create kustomization podinfo-oci \ | ||||||
|  | |||||||
| @ -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.`, | 	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 | 	Example: `  # Create an OCIRepository for a public container image | ||||||
|   flux create source oci podinfo \ |   flux create source oci podinfo \ | ||||||
|     --url=ghcr.io/stefanprodan/manifests/podinfo \ |     --url=oci://ghcr.io/stefanprodan/manifests/podinfo \ | ||||||
|     --tag=6.1.6 \ |     --tag=6.1.6 \ | ||||||
|     --interval=10m |     --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.semver, "tag-semver", "", "the OCI artifact tag semver range") | ||||||
| 	createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.digest, "digest", "", "the OCI artifact digest") | 	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, "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)") | 	createSourceOCIRepositoryCmd.Flags().StringSliceVar(&sourceOCIRepositoryArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore resources (can specify multiple paths with commas: path1,path2)") | ||||||
|  |  | ||||||
| 	createSourceCmd.AddCommand(createSourceOCIRepositoryCmd) | 	createSourceCmd.AddCommand(createSourceOCIRepositoryCmd) | ||||||
|  | |||||||
| @ -38,12 +38,12 @@ func TestCreateSourceOCI(t *testing.T) { | |||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			name:       "export manifest", | 			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"), | 			assertFunc: assertGoldenFile("./testdata/oci/export.golden"), | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			name:       "export manifest with secret", | 			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"), | 			assertFunc: assertGoldenFile("./testdata/oci/export_with_secret.golden"), | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -30,7 +30,7 @@ var listArtifactsCmd = &cobra.Command{ | |||||||
| 	Long: `The list command fetches the tags and their metadata from a remote OCI repository. | 	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'.`, | The list command uses the credentials from '~/.docker/config.json'.`, | ||||||
| 	Example: `# list the artifacts stored in an OCI repository | 	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, | 	RunE: listArtifactsCmdRun, | ||||||
| } | } | ||||||
| @ -41,13 +41,18 @@ func init() { | |||||||
|  |  | ||||||
| func listArtifactsCmdRun(cmd *cobra.Command, args []string) error { | func listArtifactsCmdRun(cmd *cobra.Command, args []string) error { | ||||||
| 	if len(args) < 1 { | 	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) | 	ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) | ||||||
| 	defer cancel() | 	defer cancel() | ||||||
|  |  | ||||||
|  | 	url, err := oci.ParseArtifactURL(ociURL) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	metas, err := oci.List(ctx, url) | 	metas, err := oci.List(ctx, url) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
|  | |||||||
| @ -387,6 +387,9 @@ func resetCmdArgs() { | |||||||
| 	createArgs = createFlags{} | 	createArgs = createFlags{} | ||||||
| 	getArgs = GetFlags{} | 	getArgs = GetFlags{} | ||||||
| 	sourceHelmArgs = sourceHelmFlags{} | 	sourceHelmArgs = sourceHelmFlags{} | ||||||
|  | 	sourceOCIRepositoryArgs = sourceOCIRepositoryFlags{} | ||||||
|  | 	sourceGitArgs = sourceGitFlags{} | ||||||
|  | 	sourceBucketArgs = sourceBucketFlags{} | ||||||
| 	secretGitArgs = NewSecretGitFlags() | 	secretGitArgs = NewSecretGitFlags() | ||||||
| 	*kubeconfigArgs.Namespace = rootArgs.defaults.Namespace | 	*kubeconfigArgs.Namespace = rootArgs.defaults.Namespace | ||||||
| } | } | ||||||
|  | |||||||
| @ -32,7 +32,7 @@ var pullArtifactCmd = &cobra.Command{ | |||||||
| 	Long: `The pull artifact command downloads and extracts the OCI artifact content to the given path. | 	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'.`, | The pull command uses the credentials from '~/.docker/config.json'.`, | ||||||
| 	Example: `# Pull an OCI artifact created by flux from GHCR | 	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, | 	RunE: pullArtifactCmdRun, | ||||||
| } | } | ||||||
| @ -50,9 +50,9 @@ func init() { | |||||||
|  |  | ||||||
| func pullArtifactCmdRun(cmd *cobra.Command, args []string) error { | func pullArtifactCmdRun(cmd *cobra.Command, args []string) error { | ||||||
| 	if len(args) < 1 { | 	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 == "" { | 	if pullArtifactArgs.output == "" { | ||||||
| 		return fmt.Errorf("invalid output path %s", 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) | 		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) | 	ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) | ||||||
| 	defer cancel() | 	defer cancel() | ||||||
|  |  | ||||||
|  | |||||||
| @ -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. | 	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'.`, | The push command uses the credentials from '~/.docker/config.json'.`, | ||||||
| 	Example: `# Push the local manifests to GHCR | 	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" \ | 	--path="./path/to/local/manifests" \ | ||||||
| 	--source="$(git config --get remote.origin.url)" \ | 	--source="$(git config --get remote.origin.url)" \ | ||||||
| 	--revision="$(git branch --show-current)/$(git rev-parse HEAD)" | 	--revision="$(git branch --show-current)/$(git rev-parse HEAD)" | ||||||
| @ -57,9 +57,9 @@ func init() { | |||||||
|  |  | ||||||
| func pushArtifactCmdRun(cmd *cobra.Command, args []string) error { | func pushArtifactCmdRun(cmd *cobra.Command, args []string) error { | ||||||
| 	if len(args) < 1 { | 	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 == "" { | 	if pushArtifactArgs.source == "" { | ||||||
| 		return fmt.Errorf("--source is required") | 		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) | 		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() { | 	if fs, err := os.Stat(pushArtifactArgs.path); err != nil || !fs.IsDir() { | ||||||
| 		return fmt.Errorf("invalid path %q", pushArtifactArgs.path) | 		return fmt.Errorf("invalid path %q", pushArtifactArgs.path) | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -29,7 +29,7 @@ func TestSourceOCI(t *testing.T) { | |||||||
| 		goldenFile string | 		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", | 			"testdata/oci/create_source_oci.golden", | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
|  | |||||||
| @ -29,7 +29,7 @@ var tagArtifactCmd = &cobra.Command{ | |||||||
| 	Long: `The tag artifact command creates tags for the given OCI artifact. | 	Long: `The tag artifact command creates tags for the given OCI artifact. | ||||||
| The tag command uses the credentials from '~/.docker/config.json'.`, | The tag command uses the credentials from '~/.docker/config.json'.`, | ||||||
| 	Example: `# Tag an artifact version as latest | 	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, | 	RunE: tagArtifactCmdRun, | ||||||
| } | } | ||||||
| @ -49,12 +49,17 @@ func tagArtifactCmdRun(cmd *cobra.Command, args []string) error { | |||||||
| 	if len(args) < 1 { | 	if len(args) < 1 { | ||||||
| 		return fmt.Errorf("artifact name is required") | 		return fmt.Errorf("artifact name is required") | ||||||
| 	} | 	} | ||||||
| 	url := args[0] | 	ociURL := args[0] | ||||||
|  |  | ||||||
| 	if len(tagArtifactArgs.tags) < 1 { | 	if len(tagArtifactArgs.tags) < 1 { | ||||||
| 		return fmt.Errorf("--tag is required") | 		return fmt.Errorf("--tag is required") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	url, err := oci.ParseArtifactURL(ociURL) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) | 	ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) | ||||||
| 	defer cancel() | 	defer cancel() | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								cmd/flux/testdata/oci/export.golden
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								cmd/flux/testdata/oci/export.golden
									
									
									
									
										vendored
									
									
								
							| @ -8,5 +8,5 @@ spec: | |||||||
|   interval: 10m0s |   interval: 10m0s | ||||||
|   ref: |   ref: | ||||||
|     tag: 6.1.6 |     tag: 6.1.6 | ||||||
|   url: ghcr.io/stefanprodan/manifests/podinfo |   url: oci://ghcr.io/stefanprodan/manifests/podinfo | ||||||
|  |  | ||||||
|  | |||||||
| @ -10,5 +10,5 @@ spec: | |||||||
|     tag: 6.1.6 |     tag: 6.1.6 | ||||||
|   secretRef: |   secretRef: | ||||||
|     name: creds |     name: creds | ||||||
|   url: ghcr.io/stefanprodan/manifests/podinfo |   url: oci://ghcr.io/stefanprodan/manifests/podinfo | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										54
									
								
								internal/oci/url.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								internal/oci/url.go
									
									
									
									
									
										Normal file
									
								
							| @ -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://<domain>/<org>/<repo>'") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	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://<domain>/<org>/<repo>'") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	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 | ||||||
|  | } | ||||||
| @ -15,4 +15,4 @@ patchesJson6902: | |||||||
| # TODO: remove the hardcoded image when OCIRepository is released | # TODO: remove the hardcoded image when OCIRepository is released | ||||||
| images: | images: | ||||||
|   - name: fluxcd/source-controller |   - name: fluxcd/source-controller | ||||||
|     newTag: oci-8509ac03 |     newTag: oci-ba5f5353 | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Stefan Prodan
					Stefan Prodan