diff --git a/cmd/flux/create_source_oci.go b/cmd/flux/create_source_oci.go index e1d26c43..a97c64fb 100644 --- a/cmd/flux/create_source_oci.go +++ b/cmd/flux/create_source_oci.go @@ -19,7 +19,6 @@ package main import ( "context" "fmt" - "os" "strings" "github.com/spf13/cobra" @@ -43,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=ghcr.io/stefanprodan/manifests/podinfo \ --tag=6.1.6 \ --interval=10m `, @@ -51,11 +50,13 @@ var createSourceOCIRepositoryCmd = &cobra.Command{ } type sourceOCIRepositoryFlags struct { - url string - tag string - digest string - secretRef string - ignorePaths []string + url string + tag string + semver string + digest string + secretRef string + serviceAccount string + ignorePaths []string } var sourceOCIRepositoryArgs = sourceOCIRepositoryFlags{} @@ -63,8 +64,10 @@ var sourceOCIRepositoryArgs = sourceOCIRepositoryFlags{} func init() { createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.url, "url", "", "the OCI repository URL") createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.tag, "tag", "", "the OCI artifact tag") + 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 an existing secret containing credentials") + 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().StringSliceVar(&sourceOCIRepositoryArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore resources (can specify multiple paths with commas: path1,path2)") createSourceCmd.AddCommand(createSourceOCIRepositoryCmd) @@ -77,8 +80,8 @@ func createSourceOCIRepositoryCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("url is required") } - if sourceOCIRepositoryArgs.tag == "" && sourceOCIRepositoryArgs.digest == "" { - return fmt.Errorf("--tag or --digest is required") + if sourceOCIRepositoryArgs.semver == "" && sourceOCIRepositoryArgs.tag == "" && sourceOCIRepositoryArgs.digest == "" { + return fmt.Errorf("--tag, --tag-semver or --digest is required") } sourceLabels, err := parseLabels() @@ -86,12 +89,6 @@ func createSourceOCIRepositoryCmdRun(cmd *cobra.Command, args []string) error { return err } - tmpDir, err := os.MkdirTemp("", name) - if err != nil { - return err - } - defer os.RemoveAll(tmpDir) - var ignorePaths *string if len(sourceOCIRepositoryArgs.ignorePaths) > 0 { ignorePathsStr := strings.Join(sourceOCIRepositoryArgs.ignorePaths, "\n") @@ -114,20 +111,27 @@ func createSourceOCIRepositoryCmdRun(cmd *cobra.Command, args []string) error { }, } - if sourceOCIRepositoryArgs.tag != "" { - repository.Spec.Reference.Tag = sourceOCIRepositoryArgs.tag + if digest := sourceOCIRepositoryArgs.digest; digest != "" { + repository.Spec.Reference.Digest = digest } - if sourceOCIRepositoryArgs.digest != "" { - repository.Spec.Reference.Digest = sourceOCIRepositoryArgs.digest + if semver := sourceOCIRepositoryArgs.semver; semver != "" { + repository.Spec.Reference.SemVer = semver + } + if tag := sourceOCIRepositoryArgs.tag; tag != "" { + repository.Spec.Reference.Tag = tag } if createSourceArgs.fetchTimeout > 0 { repository.Spec.Timeout = &metav1.Duration{Duration: createSourceArgs.fetchTimeout} } - if sourceOCIRepositoryArgs.secretRef != "" { + if saName := sourceOCIRepositoryArgs.serviceAccount; saName != "" { + repository.Spec.ServiceAccountName = saName + } + + if secretName := sourceOCIRepositoryArgs.secretRef; secretName != "" { repository.Spec.SecretRef = &meta.LocalObjectReference{ - Name: sourceOCIRepositoryArgs.secretRef, + Name: secretName, } } diff --git a/cmd/flux/list.go b/cmd/flux/list.go new file mode 100644 index 00000000..e89dc149 --- /dev/null +++ b/cmd/flux/list.go @@ -0,0 +1,31 @@ +/* +Copyright 2021 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 main + +import ( + "github.com/spf13/cobra" +) + +var listCmd = &cobra.Command{ + Use: "list", + Short: "List artifacts", + Long: "The list command is used for printing the OCI artifacts metadata.", +} + +func init() { + rootCmd.AddCommand(listCmd) +} diff --git a/cmd/flux/list_artifact.go b/cmd/flux/list_artifact.go new file mode 100644 index 00000000..a53e4f99 --- /dev/null +++ b/cmd/flux/list_artifact.go @@ -0,0 +1,68 @@ +/* +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 main + +import ( + "context" + "fmt" + "github.com/fluxcd/flux2/internal/oci" + "github.com/fluxcd/flux2/pkg/printers" + "github.com/spf13/cobra" +) + +var listArtifactsCmd = &cobra.Command{ + Use: "artifacts", + Short: "list artifacts", + 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 +`, + RunE: listArtifactsCmdRun, +} + +func init() { + listCmd.AddCommand(listArtifactsCmd) +} + +func listArtifactsCmdRun(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("artifact repository is required") + } + url := args[0] + + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) + defer cancel() + + metas, err := oci.List(ctx, url) + if err != nil { + return err + } + + var rows [][]string + for _, meta := range metas { + rows = append(rows, []string{meta.URL, meta.Digest, meta.Source, meta.Revision}) + } + + err = printers.TablePrinter([]string{"artifact", "digest", "source", "revision"}).Print(cmd.OutOrStdout(), rows) + if err != nil { + return err + } + + return nil + +} diff --git a/internal/oci/list.go b/internal/oci/list.go new file mode 100644 index 00000000..e827c472 --- /dev/null +++ b/internal/oci/list.go @@ -0,0 +1,70 @@ +/* +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 ( + "bytes" + "context" + "fmt" + "sort" + "strings" + + "github.com/google/go-containerregistry/pkg/crane" + gcrv1 "github.com/google/go-containerregistry/pkg/v1" +) + +// List fetches the tags and their manifests for a given OCI repository. +func List(ctx context.Context, url string) ([]Metadata, error) { + metas := make([]Metadata, 0) + tags, err := crane.ListTags(url, craneOptions(ctx)...) + if err != nil { + return nil, fmt.Errorf("listing tags failed: %w", err) + } + + sort.Slice(tags, func(i, j int) bool { return tags[i] > tags[j] }) + + for _, tag := range tags { + // exclude cosign signatures + if strings.HasSuffix(tag, ".sig") { + continue + } + + meta := Metadata{ + URL: fmt.Sprintf("%s:%s", url, tag), + } + + manifestJSON, err := crane.Manifest(meta.URL, craneOptions(ctx)...) + if err != nil { + return nil, fmt.Errorf("fetching manifest failed: %w", err) + } + + manifest, err := gcrv1.ParseManifest(bytes.NewReader(manifestJSON)) + if err != nil { + return nil, fmt.Errorf("parsing manifest failed: %w", err) + } + + meta.Digest = manifest.Config.Digest.String() + if m, err := MetadataFromAnnotations(manifest.Annotations); err == nil { + meta.Revision = m.Revision + meta.Source = m.Source + } + + metas = append(metas, meta) + } + + return metas, nil +} diff --git a/internal/oci/meta.go b/internal/oci/meta.go index 3196cb26..9e0c3c0c 100644 --- a/internal/oci/meta.go +++ b/internal/oci/meta.go @@ -29,6 +29,7 @@ type Metadata struct { Source string `json:"source_url"` Revision string `json:"source_revision"` Digest string `json:"digest"` + URL string `json:"url"` } func (m *Metadata) ToAnnotations() map[string]string { @@ -40,7 +41,7 @@ func (m *Metadata) ToAnnotations() map[string]string { return annotations } -func GetMetadata(annotations map[string]string) (*Metadata, error) { +func MetadataFromAnnotations(annotations map[string]string) (*Metadata, error) { source, ok := annotations[SourceAnnotation] if !ok { return nil, fmt.Errorf("'%s' annotation not found", SourceAnnotation) diff --git a/internal/oci/pull.go b/internal/oci/pull.go index b37642a8..f08c0e34 100644 --- a/internal/oci/pull.go +++ b/internal/oci/pull.go @@ -47,7 +47,7 @@ func Pull(ctx context.Context, url, outDir string) (*Metadata, error) { return nil, fmt.Errorf("parsing manifest failed: %w", err) } - meta, err := GetMetadata(manifest.Annotations) + meta, err := MetadataFromAnnotations(manifest.Annotations) if err != nil { return nil, err }