From cbbd8488c873d67437726cb564ea05dba93c2004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Batuhan=20Apayd=C4=B1n?= <batuhan.apaydin@trendyol.com> Date: Mon, 24 Oct 2022 21:47:20 +0300 Subject: [PATCH] feat: oci pull/push support for policie(s) (#5026) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Batuhan Apaydın <batuhan.apaydin@trendyol.com> Signed-off-by: Batuhan Apaydın <batuhan.apaydin@trendyol.com> Co-authored-by: Charles-Edouard Brétéché <charled.breteche@gmail.com> --- .gitignore | 1 - cmd/cli/kubectl-kyverno/main.go | 16 ++++ cmd/cli/kubectl-kyverno/oci/oci.go | 50 +++++++++++ cmd/cli/kubectl-kyverno/oci/oci_pull.go | 114 ++++++++++++++++++++++++ cmd/cli/kubectl-kyverno/oci/oci_push.go | 114 ++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 + 7 files changed, 297 insertions(+), 1 deletion(-) create mode 100644 cmd/cli/kubectl-kyverno/oci/oci.go create mode 100644 cmd/cli/kubectl-kyverno/oci/oci_pull.go create mode 100644 cmd/cli/kubectl-kyverno/oci/oci_push.go diff --git a/.gitignore b/.gitignore index 6f7d7c261b..7a1bf3bb55 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ coverage.txt .idea cmd/initContainer/kyvernopre cmd/kyverno/kyverno -kubectl-kyverno /release .DS_Store .tools diff --git a/cmd/cli/kubectl-kyverno/main.go b/cmd/cli/kubectl-kyverno/main.go index 407123a47f..079108c947 100644 --- a/cmd/cli/kubectl-kyverno/main.go +++ b/cmd/cli/kubectl-kyverno/main.go @@ -3,9 +3,11 @@ package main import ( "flag" "os" + "strconv" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/apply" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/jp" + "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/oci" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/test" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/version" "github.com/spf13/cobra" @@ -14,10 +16,13 @@ import ( log "sigs.k8s.io/controller-runtime/pkg/log" ) +const EnableExperimentalEnv = "KYVERNO_EXPERIMENTAL" + // CLI ... func main() { cli := &cobra.Command{ Use: "kyverno", + Long: `To enable experimental commands, KYVERNO_EXPERIMENTAL should be configured with true or 1.`, Short: "Kubernetes Native Policy Management", } @@ -30,6 +35,10 @@ func main() { jp.Command(), } + if enableExperimental() { + commands = append(commands, oci.Command()) + } + cli.AddCommand(commands...) if err := cli.Execute(); err != nil { @@ -37,6 +46,13 @@ func main() { } } +func enableExperimental() bool { + if b, err := strconv.ParseBool(os.Getenv(EnableExperimentalEnv)); err == nil { + return b + } + return false +} + func configurelog(cli *cobra.Command) { // clear flags initialized in static dependencies if flag.CommandLine.Lookup("log_dir") != nil { diff --git a/cmd/cli/kubectl-kyverno/oci/oci.go b/cmd/cli/kubectl-kyverno/oci/oci.go new file mode 100644 index 0000000000..130e99357d --- /dev/null +++ b/cmd/cli/kubectl-kyverno/oci/oci.go @@ -0,0 +1,50 @@ +package oci + +import ( + "io" + + "github.com/awslabs/amazon-ecr-credential-helper/ecr-login" + "github.com/chrismellard/docker-credential-acr-env/pkg/credhelper" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/authn/github" + "github.com/google/go-containerregistry/pkg/v1/google" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/spf13/cobra" +) + +const ( + policyConfigMediaType = "application/vnd.cncf.kyverno.config.v1+json" + policyLayerMediaType = "application/vnd.cncf.kyverno.policy.layer.v1+yaml" +) + +var ( + amazonKeychain = authn.NewKeychainFromHelper(ecr.NewECRHelper(ecr.WithLogger(io.Discard))) + azureKeychain = authn.NewKeychainFromHelper(credhelper.NewACRCredentialsHelper()) + keychain = authn.NewMultiKeychain( + authn.DefaultKeychain, + google.Keychain, + github.Keychain, + amazonKeychain, + azureKeychain, + ) + + Get = remote.Get + Write = remote.Write + imageRef string +) + +func Command() *cobra.Command { + cmd := &cobra.Command{ + Use: "oci", + Long: `This command is one of the supported experimental commands, and its behaviour might be changed any time`, + Short: "pulls/pushes images that include policie(s) from/to OCI registries", + Example: "", + RunE: func(cmd *cobra.Command, args []string) error { + return cmd.Help() + }, + } + cmd.PersistentFlags().StringVarP(&imageRef, "image", "i", "", "image reference to push to") + cmd.AddCommand(ociPullCommand()) + cmd.AddCommand(ociPushCommand()) + return cmd +} diff --git a/cmd/cli/kubectl-kyverno/oci/oci_pull.go b/cmd/cli/kubectl-kyverno/oci/oci_pull.go new file mode 100644 index 0000000000..cf07ac2c66 --- /dev/null +++ b/cmd/cli/kubectl-kyverno/oci/oci_pull.go @@ -0,0 +1,114 @@ +package oci + +import ( + "errors" + "fmt" + "io" + "os" + "path/filepath" + + securejoin "github.com/cyphar/filepath-securejoin" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/spf13/cobra" + "sigs.k8s.io/yaml" +) + +var dir string + +func ociPullCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "pull", + Long: "This command is one of the supported experimental commands, and its behaviour might be changed any time", + Short: "pulls policie(s) that are included in an OCI image from OCI registry and saves them to a local directory", + Example: `# pull policy from an OCI image and save it to the specific directory +kyverno oci pull -i <imgref> -d policies`, + RunE: func(cmd *cobra.Command, args []string) error { + if imageRef == "" { + return errors.New("image reference is required") + } + + dir = filepath.Clean(dir) + if !filepath.IsAbs(dir) { + cwd, err := os.Getwd() + if err != nil { + return err + } + + dir, err = securejoin.SecureJoin(cwd, dir) + if err != nil { + return err + } + } + + fi, err := os.Lstat(dir) + // Dir does not need to exist, as it can later be created. + if err != nil && errors.Is(err, os.ErrNotExist) { + if err := os.MkdirAll(dir, 0o750); err != nil { + return fmt.Errorf("unable to create directory %s: %w", dir, err) + } + } + + if err == nil && !fi.IsDir() { + return fmt.Errorf("dir '%s' must be a directory", dir) + } + + ref, err := name.ParseReference(imageRef) + if err != nil { + return fmt.Errorf("parsing image reference: %v", err) + } + + do := []remote.Option{ + remote.WithContext(cmd.Context()), + remote.WithAuthFromKeychain(keychain), + } + + rmt, err := Get(ref, do...) + if err != nil { + return fmt.Errorf("getting image: %v", err) + } + + img, err := rmt.Image() + if err != nil { + return fmt.Errorf("getting image: %v", err) + } + + l, err := img.Layers() + if err != nil { + return fmt.Errorf("getting image layers: %v", err) + } + + for _, layer := range l { + lmt, err := layer.MediaType() + if err != nil { + return fmt.Errorf("getting layer media type: %v", err) + } + + if lmt == policyLayerMediaType { + blob, err := layer.Compressed() + if err != nil { + return fmt.Errorf("getting layer blob: %v", err) + } + defer blob.Close() + + var policy map[string]interface{} + b, err := io.ReadAll(blob) + if err != nil { + return fmt.Errorf("reading layer blob: %v", err) + } + if err := yaml.Unmarshal(b, &policy); err != nil { + return fmt.Errorf("unmarshaling layer blob: %v", err) + } + + fn := policy["metadata"].(map[string]interface{})["name"].(string) + ".yaml" + if err := os.WriteFile(filepath.Join(dir, fn), b, 0o600); err != nil { + return fmt.Errorf("creating file: %v", err) + } + } + } + return nil + }, + } + cmd.Flags().StringVarP(&dir, "directory", "d", ".", "path to a directory") + return cmd +} diff --git a/cmd/cli/kubectl-kyverno/oci/oci_push.go b/cmd/cli/kubectl-kyverno/oci/oci_push.go new file mode 100644 index 0000000000..d0286b2dcb --- /dev/null +++ b/cmd/cli/kubectl-kyverno/oci/oci_push.go @@ -0,0 +1,114 @@ +package oci + +import ( + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/static" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" +) + +var policyRef string + +func ociPushCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "push", + Long: "This command is one of the supported experimental commands in Kyverno CLI, and its behaviour might be changed any time.", + Short: "push policie(s) that are included in an OCI image to OCI registry", + Example: `# push policy to an OCI image from a given policy file +kyverno oci push -p policy.yaml -i <imgref> + +# push multiple policies to an OCI image from a given directory that includes policies +kyverno oci push -p policies. -i <imgref>`, + RunE: func(cmd *cobra.Command, args []string) error { + if imageRef == "" { + return errors.New("image reference is required") + } + + var p []string + f, err := os.Stat(policyRef) + if os.IsNotExist(err) { + return fmt.Errorf("policy file or directory %s does not exist", policyRef) + } + + if f.IsDir() { + err = filepath.Walk(policyRef, func(path string, info os.FileInfo, err error) error { + if !info.IsDir() { + p = append(p, path) + } + + if m := info.Mode(); !(m.IsRegular() || m.IsDir()) { + return nil + } + + return nil + }) + } else { + p = append(p, policyRef) + } + + if err != nil { + return fmt.Errorf("unable to read policy file or directory %s: %w", policyRef, err) + } + + fmt.Println("Policies will be pushing: ", p) + + img := mutate.MediaType(empty.Image, types.OCIManifestSchema1) + img = mutate.ConfigMediaType(img, policyConfigMediaType) + for _, policy := range p { + policyBytes, err := os.ReadFile(filepath.Clean(policy)) + if err != nil { + return fmt.Errorf("failed to read policy file %s: %v", policy, err) + } + + var policyMap map[string]interface{} + if err = yaml.Unmarshal(policyBytes, &policyMap); err != nil { + return fmt.Errorf("failed to unmarshal policy file %s: %v", policy, err) + } + + annotations := map[string]string{} + for k, v := range policyMap["metadata"].(map[string]interface{})["annotations"].(map[string]interface{}) { + annotations[k] = v.(string) + } + + ref, err := name.ParseReference(imageRef) + if err != nil { + return fmt.Errorf("parsing image reference: %v", err) + } + + do := []remote.Option{ + remote.WithContext(cmd.Context()), + remote.WithAuthFromKeychain(keychain), + } + + policyLayer := static.NewLayer(policyBytes, policyLayerMediaType) + img, err = mutate.Append(img, mutate.Addendum{ + Layer: policyLayer, + Annotations: annotations, + }) + + if err != nil { + return fmt.Errorf("mutating image: %v", err) + } + + fmt.Fprintf(os.Stderr, "Uploading Kyverno policy file [%s] to [%s] with mediaType [%s].\n", policy, ref.Name(), policyLayerMediaType) + if err = Write(ref, img, do...); err != nil { + return fmt.Errorf("writing image: %v", err) + } + + fmt.Fprintf(os.Stderr, "Kyverno policy file [%s] successfully uploaded to [%s]\n", policy, ref.Name()) + } + return nil + }, + } + cmd.Flags().StringVarP(&policyRef, "policy", "p", "", "path to policie(s)") + return cmd +} diff --git a/go.mod b/go.mod index 3f2f8304ac..409012aab4 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/blang/semver/v4 v4.0.0 github.com/cenkalti/backoff v2.2.1+incompatible github.com/chrismellard/docker-credential-acr-env v0.0.0-20221002210726-e883f69e0206 + github.com/cyphar/filepath-securejoin v0.2.3 github.com/distribution/distribution v2.8.1+incompatible github.com/evanphx/json-patch v5.6.0+incompatible github.com/evanphx/json-patch/v5 v5.6.0 diff --git a/go.sum b/go.sum index 8e6c261bc7..1e0e54c6e7 100644 --- a/go.sum +++ b/go.sum @@ -541,6 +541,8 @@ github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ github.com/cyberphone/json-canonicalization v0.0.0-20210303052042-6bc126869bf4/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= github.com/cyberphone/json-canonicalization v0.0.0-20210823021906-dc406ceaf94b h1:lMzA7yYThpwx7iYNpTeiQnRH6h5JSfSYMJdz+pxZOW8= github.com/cyberphone/json-canonicalization v0.0.0-20210823021906-dc406ceaf94b/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= +github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI= +github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/daixiang0/gci v0.2.8/go.mod h1:+4dZ7TISfSmqfAGv59ePaHfNzgGtIkHAhhdKggP1JAc= github.com/danieljoos/wincred v1.0.2/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3EJbmjhLdK9U= github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=