From 57f0ac3142e949f74b87eac1851d205916c412bb Mon Sep 17 00:00:00 2001 From: Dan Meier Date: Wed, 13 May 2026 16:11:18 +0200 Subject: [PATCH] Use single OCI layer selector flag Signed-off-by: Dan Meier Assisted-by: Codex/gpt-5 --- cmd/flux/create_source_oci.go | 36 +++++++++++++++++++ cmd/flux/create_source_oci_test.go | 15 ++++++++ .../oci/export_with_layer_selector.golden | 14 ++++++++ 3 files changed, 65 insertions(+) create mode 100644 cmd/flux/testdata/oci/export_with_layer_selector.golden diff --git a/cmd/flux/create_source_oci.go b/cmd/flux/create_source_oci.go index 4d563a38..ac6448eb 100644 --- a/cmd/flux/create_source_oci.go +++ b/cmd/flux/create_source_oci.go @@ -53,6 +53,12 @@ var createSourceOCIRepositoryCmd = &cobra.Command{ --verify-provider=cosign \ --verify-subject="^https://github.com/stefanprodan/podinfo/.github/workflows/release.yml@refs/tags/6.6.2$" \ --verify-issuer="^https://token.actions.githubusercontent.com$" + + # Create an OCIRepository for a Helm chart layer + flux create source oci valkey-cluster \ + --url=oci://example.com/charts/valkey \ + --tag=0.11.6 \ + --layer-selector=application/vnd.cncf.helm.chart.content.v1.tar+gzip:copy `, RunE: createSourceOCIRepositoryCmdRun, } @@ -73,6 +79,7 @@ type sourceOCIRepositoryFlags struct { ignorePaths []string provider flags.SourceOCIProvider insecure bool + layerSelector string } var sourceOCIRepositoryArgs = newSourceOCIFlags() @@ -99,6 +106,7 @@ func init() { createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.verifyOIDCIssuer, "verify-issuer", "", "regular expression to use for the OIDC issuer during signature verification") createSourceOCIRepositoryCmd.Flags().StringSliceVar(&sourceOCIRepositoryArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore resources (can specify multiple paths with commas: path1,path2)") createSourceOCIRepositoryCmd.Flags().BoolVar(&sourceOCIRepositoryArgs.insecure, "insecure", false, "for when connecting to a non-TLS registries over plain HTTP") + createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.layerSelector, "layer-selector", "", "the OCI artifact layer selector in the format ':'") createSourceCmd.AddCommand(createSourceOCIRepositoryCmd) } @@ -114,6 +122,11 @@ func createSourceOCIRepositoryCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("--tag, --tag-semver or --digest is required") } + layerSelector, err := parseLayerSelector(sourceOCIRepositoryArgs.layerSelector) + if err != nil { + return err + } + sourceLabels, err := parseLabels() if err != nil { return err @@ -152,6 +165,7 @@ func createSourceOCIRepositoryCmdRun(cmd *cobra.Command, args []string) error { if tag := sourceOCIRepositoryArgs.tag; tag != "" { repository.Spec.Reference.Tag = tag } + repository.Spec.LayerSelector = layerSelector if createSourceArgs.fetchTimeout > 0 { repository.Spec.Timeout = &metav1.Duration{Duration: createSourceArgs.fetchTimeout} @@ -234,6 +248,28 @@ func createSourceOCIRepositoryCmdRun(cmd *cobra.Command, args []string) error { return nil } +func parseLayerSelector(selector string) (*sourcev1.OCILayerSelector, error) { + if selector == "" { + return nil, nil + } + + mediaType, operation, found := strings.Cut(selector, ":") + if !found || mediaType == "" || operation == "" { + return nil, fmt.Errorf("invalid --layer-selector %q: must be in the format ':'", selector) + } + + switch operation { + case sourcev1.OCILayerExtract, sourcev1.OCILayerCopy: + default: + return nil, fmt.Errorf("invalid --layer-selector %q: operation must be %q or %q", selector, sourcev1.OCILayerExtract, sourcev1.OCILayerCopy) + } + + return &sourcev1.OCILayerSelector{ + MediaType: mediaType, + Operation: operation, + }, nil +} + func upsertOCIRepository(ctx context.Context, kubeClient client.Client, ociRepository *sourcev1.OCIRepository) (types.NamespacedName, error) { namespacedName := types.NamespacedName{ diff --git a/cmd/flux/create_source_oci_test.go b/cmd/flux/create_source_oci_test.go index da08d9f6..98ab182a 100644 --- a/cmd/flux/create_source_oci_test.go +++ b/cmd/flux/create_source_oci_test.go @@ -81,6 +81,21 @@ func TestCreateSourceOCI(t *testing.T) { args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --interval 10m --verify-provider=cosign --verify-secret-ref=cosign-pub --export", assertFunc: assertGoldenFile("./testdata/oci/export_with_verify_secret.golden"), }, + { + name: "export manifest with layer selector", + args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --interval 10m --layer-selector=application/vnd.cncf.helm.chart.content.v1.tar+gzip:copy --export", + assertFunc: assertGoldenFile("./testdata/oci/export_with_layer_selector.golden"), + }, + { + name: "invalid layer selector operation", + args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --layer-selector=application/vnd.cncf.helm.chart.content.v1.tar+gzip:move --export", + assertFunc: assertError("invalid --layer-selector \"application/vnd.cncf.helm.chart.content.v1.tar+gzip:move\": operation must be \"extract\" or \"copy\""), + }, + { + name: "invalid layer selector format", + args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --layer-selector=application/vnd.cncf.helm.chart.content.v1.tar+gzip --export", + assertFunc: assertError("invalid --layer-selector \"application/vnd.cncf.helm.chart.content.v1.tar+gzip\": must be in the format ':'"), + }, } for _, tt := range tests { diff --git a/cmd/flux/testdata/oci/export_with_layer_selector.golden b/cmd/flux/testdata/oci/export_with_layer_selector.golden new file mode 100644 index 00000000..91c6effd --- /dev/null +++ b/cmd/flux/testdata/oci/export_with_layer_selector.golden @@ -0,0 +1,14 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: OCIRepository +metadata: + name: podinfo + namespace: flux-system +spec: + interval: 10m0s + layerSelector: + mediaType: application/vnd.cncf.helm.chart.content.v1.tar+gzip + operation: copy + ref: + tag: 6.3.5 + url: oci://ghcr.io/stefanprodan/manifests/podinfo