From 4c576bf59920a34663128a9f7aa3f2dc0e9bd8eb Mon Sep 17 00:00:00 2001 From: Somtochi Onyekwere Date: Mon, 18 Jul 2022 15:54:41 +0100 Subject: [PATCH] Add create oci secret command Signed-off-by: Somtochi Onyekwere --- cmd/flux/create_secret_helm_test.go | 16 +++ cmd/flux/create_secret_oci.go | 117 ++++++++++++++++++ cmd/flux/create_secret_oci_test.go | 51 ++++++++ .../create_secret/oci/create-secret.yaml | 10 ++ pkg/manifestgen/sourcesecret/options.go | 1 + pkg/manifestgen/sourcesecret/sourcesecret.go | 48 ++++++- 6 files changed, 241 insertions(+), 2 deletions(-) create mode 100644 cmd/flux/create_secret_oci.go create mode 100644 cmd/flux/create_secret_oci_test.go create mode 100644 cmd/flux/testdata/create_secret/oci/create-secret.yaml diff --git a/cmd/flux/create_secret_helm_test.go b/cmd/flux/create_secret_helm_test.go index fe0bc0c9..821f8311 100644 --- a/cmd/flux/create_secret_helm_test.go +++ b/cmd/flux/create_secret_helm_test.go @@ -1,3 +1,19 @@ +/* +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 ( diff --git a/cmd/flux/create_secret_oci.go b/cmd/flux/create_secret_oci.go new file mode 100644 index 00000000..decd680c --- /dev/null +++ b/cmd/flux/create_secret_oci.go @@ -0,0 +1,117 @@ +/* +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/utils" + "github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret" + "github.com/google/go-containerregistry/pkg/name" + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/yaml" +) + +var createSecretOCICmd = &cobra.Command{ + Use: "oci [name]", + Short: "Create or update a Kubernetes secret for docker authentication", + Long: `The create secret oci command generates a Kubernetes secret with `, + Example: ` # Create a secret for a OCI repository using basic authentication + flux create secret oci podinfo-auth \ + --url=ghcr.io/stefanprodan/charts \ + --username=username \ + --password=password + `, + RunE: createSecretOCICmdRun, +} + +type secretOCIFlags struct { + url string + password string + username string +} + +var secretOCIArgs = secretOCIFlags{} + +func init() { + createSecretOCICmd.Flags().StringVar(&secretOCIArgs.url, "url", "", "oci repository address e.g ghcr.io/stefanprodan/charts") + createSecretOCICmd.Flags().StringVarP(&secretOCIArgs.username, "username", "u", "", "basic authentication username") + createSecretOCICmd.Flags().StringVarP(&secretOCIArgs.password, "password", "p", "", "basic authentication") + + createSecretCmd.AddCommand(createSecretOCICmd) +} + +func createSecretOCICmdRun(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("name is required") + } + + secretName := args[0] + + if secretOCIArgs.url == "" { + return fmt.Errorf("--url is required") + } + + if secretOCIArgs.username == "" { + return fmt.Errorf("--username is required") + } + + if secretOCIArgs.password == "" { + return fmt.Errorf("--password is required") + } + + if _, err := name.ParseReference(secretOCIArgs.url); err != nil { + return fmt.Errorf("error parsing url: '%s'", err) + } + + opts := sourcesecret.Options{ + Name: secretName, + Namespace: *kubeconfigArgs.Namespace, + Registry: secretOCIArgs.url, + Password: secretOCIArgs.password, + Username: secretOCIArgs.username, + } + + secret, err := sourcesecret.Generate(opts) + if err != nil { + return err + } + + if createArgs.export { + rootCmd.Println(secret.Content) + return nil + } + + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) + defer cancel() + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) + if err != nil { + return err + } + var s corev1.Secret + if err := yaml.Unmarshal([]byte(secret.Content), &s); err != nil { + return err + } + if err := upsertSecret(ctx, kubeClient, s); err != nil { + return err + } + + logger.Actionf("oci secret '%s' created in '%s' namespace", secretName, *kubeconfigArgs.Namespace) + return nil +} diff --git a/cmd/flux/create_secret_oci_test.go b/cmd/flux/create_secret_oci_test.go new file mode 100644 index 00000000..a7897796 --- /dev/null +++ b/cmd/flux/create_secret_oci_test.go @@ -0,0 +1,51 @@ +/* +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 ( + "testing" +) + +func TestCreateSecretOCI(t *testing.T) { + tests := []struct { + name string + args string + assert assertFunc + }{ + { + args: "create secret oci", + assert: assertError("name is required"), + }, + { + args: "create secret oci ghcr", + assert: assertError("--url is required"), + }, + { + args: "create secret oci ghcr --namespace=my-namespace --url ghcr.io --username stefanprodan --password=password --export", + assert: assertGoldenFile("testdata/create_secret/oci/create-secret.yaml"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := cmdTestCase{ + args: tt.args, + assert: tt.assert, + } + cmd.runTestCmd(t) + }) + } +} diff --git a/cmd/flux/testdata/create_secret/oci/create-secret.yaml b/cmd/flux/testdata/create_secret/oci/create-secret.yaml new file mode 100644 index 00000000..b023bbe2 --- /dev/null +++ b/cmd/flux/testdata/create_secret/oci/create-secret.yaml @@ -0,0 +1,10 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: ghcr + namespace: my-namespace +stringData: + .dockerconfigjson: '{"auths":{"ghcr.io":{"username":"stefanprodan","password":"password","auth":"c3RlZmFucHJvZGFuOnBhc3N3b3Jk"}}}' +type: kubernetes.io/dockerconfigjson + diff --git a/pkg/manifestgen/sourcesecret/options.go b/pkg/manifestgen/sourcesecret/options.go index c5239bfb..371e6f9b 100644 --- a/pkg/manifestgen/sourcesecret/options.go +++ b/pkg/manifestgen/sourcesecret/options.go @@ -43,6 +43,7 @@ type Options struct { Name string Namespace string Labels map[string]string + Registry string SSHHostname string PrivateKeyAlgorithm PrivateKeyAlgorithm RSAKeyBits int diff --git a/pkg/manifestgen/sourcesecret/sourcesecret.go b/pkg/manifestgen/sourcesecret/sourcesecret.go index 53806328..2ea33227 100644 --- a/pkg/manifestgen/sourcesecret/sourcesecret.go +++ b/pkg/manifestgen/sourcesecret/sourcesecret.go @@ -18,6 +18,8 @@ package sourcesecret import ( "bytes" + "encoding/base64" + "encoding/json" "fmt" "net" "os" @@ -36,6 +38,18 @@ import ( const defaultSSHPort = 22 +type DockerConfigJson struct { + Auths DockerConfig `json:"auths"` +} + +type DockerConfig map[string]DockerConfigEntry + +type DockerConfigEntry struct { + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Auth string `json:"auth,omitempty"` +} + func Generate(options Options) (*manifestgen.Manifest, error) { var err error @@ -77,7 +91,15 @@ func Generate(options Options) (*manifestgen.Manifest, error) { } } - secret := buildSecret(keypair, hostKey, caFile, certFile, keyFile, options) + var dockerCfgJson []byte + if options.Registry != "" { + dockerCfgJson, err = generateDockerConfigJson(options.Registry, options.Username, options.Password) + if err != nil { + return nil, fmt.Errorf("failed to generate json for docker config: %w", err) + } + } + + secret := buildSecret(keypair, hostKey, caFile, certFile, keyFile, dockerCfgJson, options) b, err := yaml.Marshal(secret) if err != nil { return nil, err @@ -89,7 +111,7 @@ func Generate(options Options) (*manifestgen.Manifest, error) { }, nil } -func buildSecret(keypair *ssh.KeyPair, hostKey, caFile, certFile, keyFile []byte, options Options) (secret corev1.Secret) { +func buildSecret(keypair *ssh.KeyPair, hostKey, caFile, certFile, keyFile, dockerCfg []byte, options Options) (secret corev1.Secret) { secret.TypeMeta = metav1.TypeMeta{ APIVersion: "v1", Kind: "Secret", @@ -101,6 +123,12 @@ func buildSecret(keypair *ssh.KeyPair, hostKey, caFile, certFile, keyFile []byte secret.Labels = options.Labels secret.StringData = map[string]string{} + if dockerCfg != nil { + secret.Type = corev1.SecretTypeDockerConfigJson + secret.StringData[corev1.DockerConfigJsonKey] = string(dockerCfg) + return + } + if options.Username != "" && options.Password != "" { secret.StringData[UsernameSecretKey] = options.Username secret.StringData[PasswordSecretKey] = options.Password @@ -189,3 +217,19 @@ func resourceToString(data []byte) string { data = bytes.Replace(data, []byte("status: {}\n"), []byte(""), 1) return string(data) } + +func generateDockerConfigJson(url, username, password string) ([]byte, error) { + cred := fmt.Sprintf("%s:%s", username, password) + auth := base64.StdEncoding.EncodeToString([]byte(cred)) + cfg := DockerConfigJson{ + Auths: map[string]DockerConfigEntry{ + url: { + Username: username, + Password: password, + Auth: auth, + }, + }, + } + + return json.Marshal(cfg) +}