From 9503ecafb1c50a4bdb2ddaf984820fe6ea9c4391 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Tue, 21 Jun 2022 12:38:10 +0300 Subject: [PATCH] Add artifact commands Implement build, push, pull and tag artifact commands. For authentication purposes, all `flux artifact` commands are using the '~/.docker/config.json' config file and the Docker credential helpers. Signed-off-by: Stefan Prodan --- cmd/flux/build_artifact.go | 69 ++++++++++++++++++++++++++ cmd/flux/pull.go | 31 ++++++++++++ cmd/flux/pull_artifact.go | 81 +++++++++++++++++++++++++++++++ cmd/flux/push.go | 31 ++++++++++++ cmd/flux/push_artifact.go | 99 ++++++++++++++++++++++++++++++++++++++ cmd/flux/tag.go | 31 ++++++++++++ cmd/flux/tag_artifact.go | 74 ++++++++++++++++++++++++++++ 7 files changed, 416 insertions(+) create mode 100644 cmd/flux/build_artifact.go create mode 100644 cmd/flux/pull.go create mode 100644 cmd/flux/pull_artifact.go create mode 100644 cmd/flux/push.go create mode 100644 cmd/flux/push_artifact.go create mode 100644 cmd/flux/tag.go create mode 100644 cmd/flux/tag_artifact.go diff --git a/cmd/flux/build_artifact.go b/cmd/flux/build_artifact.go new file mode 100644 index 00000000..83aa30ea --- /dev/null +++ b/cmd/flux/build_artifact.go @@ -0,0 +1,69 @@ +/* +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 ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/fluxcd/flux2/internal/oci" +) + +var buildArtifactCmd = &cobra.Command{ + Use: "artifact", + Short: "Build artifact", + Long: `The build artifact command creates an tgz file with the manifests from the given directory.`, + Example: `# Build the given manifests directory into an artifact +flux build artifact --path ./path/to/local/manifests --output ./path/to/artifact.tgz +`, + RunE: buildArtifactCmdRun, +} + +type buildArtifactFlags struct { + output string + path string +} + +var buildArtifactArgs buildArtifactFlags + +func init() { + buildArtifactCmd.Flags().StringVar(&buildArtifactArgs.path, "path", "", "Path to the directory where the Kubernetes manifests are located.") + buildArtifactCmd.Flags().StringVarP(&buildArtifactArgs.output, "output", "0", "artifact.tgz", "Path to where the artifact tgz file should be written.") + buildCmd.AddCommand(buildArtifactCmd) +} + +func buildArtifactCmdRun(cmd *cobra.Command, args []string) error { + if buildArtifactArgs.path == "" { + return fmt.Errorf("invalid path %q", buildArtifactArgs.path) + } + + if fs, err := os.Stat(buildArtifactArgs.path); err != nil || !fs.IsDir() { + return fmt.Errorf("invalid path %q", buildArtifactArgs.path) + } + + logger.Actionf("building artifact from %s", buildArtifactArgs.path) + + if err := oci.Build(buildArtifactArgs.output, buildArtifactArgs.path); err != nil { + return fmt.Errorf("bulding artifact failed, error: %w", err) + } + + logger.Successf("artifact created at %s", buildArtifactArgs.output) + + return nil +} diff --git a/cmd/flux/pull.go b/cmd/flux/pull.go new file mode 100644 index 00000000..7ea48a1f --- /dev/null +++ b/cmd/flux/pull.go @@ -0,0 +1,31 @@ +/* +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 ( + "github.com/spf13/cobra" +) + +var pullCmd = &cobra.Command{ + Use: "pull", + Short: "Pull artifacts", + Long: "The pull command is used to download OCI artifacts.", +} + +func init() { + rootCmd.AddCommand(pullCmd) +} diff --git a/cmd/flux/pull_artifact.go b/cmd/flux/pull_artifact.go new file mode 100644 index 00000000..37560f68 --- /dev/null +++ b/cmd/flux/pull_artifact.go @@ -0,0 +1,81 @@ +/* +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" + "os" + + "github.com/spf13/cobra" + + "github.com/fluxcd/flux2/internal/oci" +) + +var pullArtifactCmd = &cobra.Command{ + Use: "artifact", + Short: "Push artifact", + 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 +`, + RunE: pullArtifactCmdRun, +} + +type pullArtifactFlags struct { + output string +} + +var pullArtifactArgs pullArtifactFlags + +func init() { + pullArtifactCmd.Flags().StringVarP(&pullArtifactArgs.output, "output", "o", "", "Path where the artifact content should be extracted.") + pullCmd.AddCommand(pullArtifactCmd) +} + +func pullArtifactCmdRun(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("artifact name is required") + } + url := args[0] + + if pullArtifactArgs.output == "" { + return fmt.Errorf("invalid output path %s", pullArtifactArgs.output) + } + + if fs, err := os.Stat(pullArtifactArgs.output); err != nil || !fs.IsDir() { + return fmt.Errorf("invalid output path %s", pullArtifactArgs.output) + } + + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) + defer cancel() + + logger.Actionf("pulling artifact from %s", url) + + meta, err := oci.Pull(ctx, url, pullArtifactArgs.output) + if err != nil { + return err + } + + logger.Successf("source %s", meta.Source) + logger.Successf("revision %s", meta.Revision) + logger.Successf("digest %s", meta.Digest) + logger.Successf("artifact content extracted to %s", pullArtifactArgs.output) + + return nil +} diff --git a/cmd/flux/push.go b/cmd/flux/push.go new file mode 100644 index 00000000..481c6ae9 --- /dev/null +++ b/cmd/flux/push.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 pushCmd = &cobra.Command{ + Use: "push", + Short: "Push artifacts", + Long: "The push command is used to publish OCI artifacts.", +} + +func init() { + rootCmd.AddCommand(pushCmd) +} diff --git a/cmd/flux/push_artifact.go b/cmd/flux/push_artifact.go new file mode 100644 index 00000000..813dbb46 --- /dev/null +++ b/cmd/flux/push_artifact.go @@ -0,0 +1,99 @@ +/* +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" + "os" + + "github.com/spf13/cobra" + + "github.com/fluxcd/flux2/internal/oci" +) + +var pushArtifactCmd = &cobra.Command{ + Use: "artifact", + Short: "Push artifact", + 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 \ + --path="./path/to/local/manifests" \ + --source="$(git config --get remote.origin.url)" \ + --revision="$(git branch --show-current)/$(git rev-parse HEAD)" +`, + RunE: pushArtifactCmdRun, +} + +type pushArtifactFlags struct { + path string + source string + revision string +} + +var pushArtifactArgs pushArtifactFlags + +func init() { + pushArtifactCmd.Flags().StringVar(&pushArtifactArgs.path, "path", "", "Path to the directory where the Kubernetes manifests are located.") + pushArtifactCmd.Flags().StringVar(&pushArtifactArgs.source, "source", "", "The source address, e.g. Git URL.") + pushArtifactCmd.Flags().StringVar(&pushArtifactArgs.revision, "revision", "", "The source revision in the format '/'") + pushCmd.AddCommand(pushArtifactCmd) +} + +func pushArtifactCmdRun(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("artifact name is required") + } + url := args[0] + + if pushArtifactArgs.source == "" { + return fmt.Errorf("--source is required") + } + + if pushArtifactArgs.revision == "" { + return fmt.Errorf("--revision is required") + } + + if pushArtifactArgs.path == "" { + return fmt.Errorf("invalid path %q", pushArtifactArgs.path) + } + + if fs, err := os.Stat(pushArtifactArgs.path); err != nil || !fs.IsDir() { + return fmt.Errorf("invalid path %q", pushArtifactArgs.path) + } + + meta := oci.Metadata{ + Source: pushArtifactArgs.source, + Revision: pushArtifactArgs.revision, + } + + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) + defer cancel() + + logger.Actionf("pushing artifact to %s", url) + + digest, err := oci.Push(ctx, url, pushArtifactArgs.path, meta) + if err != nil { + return fmt.Errorf("pushing artifact failed: %w", err) + } + + logger.Successf("artifact successfully pushed to %s", digest) + + return nil + +} diff --git a/cmd/flux/tag.go b/cmd/flux/tag.go new file mode 100644 index 00000000..56c9485f --- /dev/null +++ b/cmd/flux/tag.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 tagCmd = &cobra.Command{ + Use: "tag", + Short: "Tag artifacts", + Long: "The tag command is used to tag OCI artifacts.", +} + +func init() { + rootCmd.AddCommand(tagCmd) +} diff --git a/cmd/flux/tag_artifact.go b/cmd/flux/tag_artifact.go new file mode 100644 index 00000000..be0a6387 --- /dev/null +++ b/cmd/flux/tag_artifact.go @@ -0,0 +1,74 @@ +/* +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/spf13/cobra" +) + +var tagArtifactCmd = &cobra.Command{ + Use: "artifact", + Short: "Tag artifact", + 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 +`, + RunE: tagArtifactCmdRun, +} + +type tagArtifactFlags struct { + tags []string +} + +var tagArtifactArgs tagArtifactFlags + +func init() { + tagArtifactCmd.Flags().StringSliceVar(&tagArtifactArgs.tags, "tag", nil, "Tag name.") + tagCmd.AddCommand(tagArtifactCmd) +} + +func tagArtifactCmdRun(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("artifact name is required") + } + url := args[0] + + if len(tagArtifactArgs.tags) < 1 { + return fmt.Errorf("--tag is required") + } + + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) + defer cancel() + + logger.Actionf("tagging artifact") + + for _, tag := range tagArtifactArgs.tags { + img, err := oci.Tag(ctx, url, tag) + if err != nil { + return fmt.Errorf("tagging artifact failed: %w", err) + } + + logger.Successf("artifact tagged as %s", img) + } + + return nil + +}