From db3b134d9e077539133045451976108153c6e601 Mon Sep 17 00:00:00 2001 From: Dimitri Koshkin Date: Thu, 8 Dec 2022 10:07:01 -0800 Subject: [PATCH] feat: CLI to update registry credentials --- .goreleaser.yml | 38 ++++++ cmd/cli/cmd/flags/flags.go | 17 +++ cmd/cli/cmd/root.go | 45 +++++++ cmd/cli/cmd/update/create.go | 21 ++++ cmd/cli/cmd/update/credentials/credentials.go | 59 ++++++++++ cmd/cli/main.go | 10 ++ go.mod | 5 +- go.sum | 8 +- pkg/credentialmanager/plugin/plugin.go | 10 ++ pkg/credentialmanager/secret/secret.go | 110 ++++++++++++++++++ pkg/credentialmanager/secret/secret_test.go | 107 +++++++++++++++++ pkg/credentialprovider/plugin/plugin.go | 4 +- .../static/static_credentials.go | 4 +- pkg/k8s/client/client.go | 59 ++++++++++ 14 files changed, 491 insertions(+), 6 deletions(-) create mode 100644 cmd/cli/cmd/flags/flags.go create mode 100644 cmd/cli/cmd/root.go create mode 100644 cmd/cli/cmd/update/create.go create mode 100644 cmd/cli/cmd/update/credentials/credentials.go create mode 100644 cmd/cli/main.go create mode 100644 pkg/credentialmanager/plugin/plugin.go create mode 100644 pkg/credentialmanager/secret/secret.go create mode 100644 pkg/credentialmanager/secret/secret_test.go create mode 100644 pkg/k8s/client/client.go diff --git a/.goreleaser.yml b/.goreleaser.yml index f3f28ff..171850f 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -72,6 +72,37 @@ builds: - GOARCH={{ .Arch }} post: - cmd: make SKIP_UPX={{ if index .Env "SKIP_UPX" }}{{ .Env.SKIP_UPX }}{{ else }}{{ .IsSnapshot }}{{ end }} GOOS={{ .Os }} GOARCH={{ .Arch }} UPX_TARGET={{ .Path }} upx + - id: credential-manager + dir: ./cmd/cli + binary: credential-manager + env: + - CGO_ENABLED=0 + flags: + - -trimpath + ldflags: + - -s + - -w + - -X 'github.com/mesosphere/dkp-cli-runtime/core/cmd/version.commitDate={{ .CommitDate }}' + - -X 'github.com/mesosphere/dkp-cli-runtime/core/cmd/version.gitCommit={{ .FullCommit }}' + - -X 'github.com/mesosphere/dkp-cli-runtime/core/cmd/version.gitTreeState={{ .Env.GIT_TREE_STATE }}' + - -X 'github.com/mesosphere/dkp-cli-runtime/core/cmd/version.gitVersion=v{{ trimprefix .Version "v" }}' + - -X 'github.com/mesosphere/dkp-cli-runtime/core/cmd/version.major={{ .Major }}' + - -X 'github.com/mesosphere/dkp-cli-runtime/core/cmd/version.minor={{ .Minor }}' + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + mod_timestamp: '{{ .CommitTimestamp }}' + hooks: + pre: + - cmd: make SKIP_UPX={{ if index .Env "SKIP_UPX" }}{{ .Env.SKIP_UPX }}{{ else }}{{ .IsSnapshot }}{{ end }} go-generate + env: + - GOOS={{ .Os }} + - GOARCH={{ .Arch }} + post: + - cmd: make SKIP_UPX={{ if index .Env "SKIP_UPX" }}{{ .Env.SKIP_UPX }}{{ else }}{{ .IsSnapshot }}{{ end }} GOOS={{ .Os }} GOARCH={{ .Arch }} UPX_TARGET={{ .Path }} upx archives: - name_template: '{{ .ProjectName }}_v{{trimprefix .Version "v"}}_{{ .Os }}_{{ .Arch }}' # This is a hack documented in https://github.com/goreleaser/goreleaser/blob/df0216d5855e9283d2106fb5acdb0e7b528a56e8/www/docs/customization/archive.md#packaging-only-the-binaries @@ -86,6 +117,13 @@ archives: - none* builds: - static-credential-provider + - name_template: 'credential-manager_v{{trimprefix .Version "v"}}_{{ .Os }}_{{ .Arch }}' + # This is a hack documented in https://github.com/goreleaser/goreleaser/blob/df0216d5855e9283d2106fb5acdb0e7b528a56e8/www/docs/customization/archive.md#packaging-only-the-binaries + id: credential-manager + files: + - none* + builds: + - credential-manager dockers: - image_templates: # Specify the image tag including `-amd64` suffix if the build is not a snapshot build or is not being built on diff --git a/cmd/cli/cmd/flags/flags.go b/cmd/cli/cmd/flags/flags.go new file mode 100644 index 0000000..fd9a9b5 --- /dev/null +++ b/cmd/cli/cmd/flags/flags.go @@ -0,0 +1,17 @@ +// Copyright 2022 D2iQ, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package flags + +import ( + "io" + + "github.com/mesosphere/dkp-cli-runtime/core/output" +) + +// CLIConfig injects dependencies into CLI that are hard to mock, +// enabling better unittesting. +type CLIConfig struct { + In io.Reader + Output output.Output +} diff --git a/cmd/cli/cmd/root.go b/cmd/cli/cmd/root.go new file mode 100644 index 0000000..f61fddb --- /dev/null +++ b/cmd/cli/cmd/root.go @@ -0,0 +1,45 @@ +// Copyright 2022 D2iQ, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "io" + "os" + + "github.com/spf13/cobra" + + "github.com/mesosphere/dkp-cli-runtime/core/cmd/root" + + "github.com/mesosphere/dynamic-credential-provider/cmd/cli/cmd/flags" + "github.com/mesosphere/dynamic-credential-provider/cmd/cli/cmd/update" +) + +func NewCommand(in io.Reader, out, errOut io.Writer) (*cobra.Command, *flags.CLIConfig) { + rootCmd, rootOptions := root.NewCommand(out, errOut) + rootCmd.Use = "credential-manager" + rootCmd.Short = "Create and dynamically manage registry credentials" + rootCmd.SilenceUsage = true + // disable cobra built-in error printing, we output the error with formatting. + rootCmd.SilenceErrors = true + rootCmd.DisableAutoGenTag = true + + config := &flags.CLIConfig{ + In: in, + Output: rootOptions.Output, + } + + rootCmd.AddCommand(update.NewCommand(config)) + + return rootCmd, config +} + +func Execute() { + rootCmd, config := NewCommand(os.Stdin, os.Stdout, os.Stderr) + + if err := rootCmd.Execute(); err != nil { + config.Output.Error(err, "") + //nolint:revive // Common to do this in Cobra + os.Exit(1) + } +} diff --git a/cmd/cli/cmd/update/create.go b/cmd/cli/cmd/update/create.go new file mode 100644 index 0000000..4becdcd --- /dev/null +++ b/cmd/cli/cmd/update/create.go @@ -0,0 +1,21 @@ +// Copyright 2022 D2iQ, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package update + +import ( + "github.com/spf13/cobra" + + "github.com/mesosphere/dynamic-credential-provider/cmd/cli/cmd/flags" + "github.com/mesosphere/dynamic-credential-provider/cmd/cli/cmd/update/credentials" +) + +func NewCommand(cmdCfg *flags.CLIConfig) *cobra.Command { + cmd := &cobra.Command{ + Use: "update", + Short: "Update one of []", + } + + cmd.AddCommand(credentials.NewCommand(cmdCfg)) + return cmd +} diff --git a/cmd/cli/cmd/update/credentials/credentials.go b/cmd/cli/cmd/update/credentials/credentials.go new file mode 100644 index 0000000..e737c34 --- /dev/null +++ b/cmd/cli/cmd/update/credentials/credentials.go @@ -0,0 +1,59 @@ +// Copyright 2022 D2iQ, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package credentials + +import ( + "context" + + "github.com/spf13/cobra" + + "github.com/mesosphere/dynamic-credential-provider/cmd/cli/cmd/flags" + "github.com/mesosphere/dynamic-credential-provider/pkg/credentialmanager/secret" + "github.com/mesosphere/dynamic-credential-provider/pkg/k8s/client" +) + +func NewCommand(cmdCfg *flags.CLIConfig) *cobra.Command { + var ( + address string + username string + password string + ) + + cmd := &cobra.Command{ + Use: "registry-credentials [address] [username] [password]", + Short: "Update image registry credentials", + Long: `Update image registry credentials in the running cluster: + +Examples: + update registry-credentials --address=docker.io --username=myusername --password=mypassword + update registry-credentials --address=myregistry:5000 --username=myusername --password=mypassword + update registry-credentials --address=myregistry:5000/somepath --username=myusername --password=mypassword +`, + RunE: func(cmd *cobra.Command, args []string) error { + k8sCLient, _, err := client.NewFromKubeconfig("") + if err != nil { + return err + } + + manager := secret.NewSecretsCredentialManager(k8sCLient) + + err = manager.Update(context.Background(), address, username, password) + if err != nil { + return err + } + + cmdCfg.Output.Infof("Updated credentials") + return nil + }, + } + + cmd.Flags().StringVar(&address, "address", "", "Address of the registry to update credentials") + _ = cmd.MarkFlagRequired("address") + cmd.Flags().StringVar(&username, "username", "", "New username for the registry") + _ = cmd.MarkFlagRequired("username") + cmd.Flags().StringVar(&password, "password", "", "New password for the registry") + _ = cmd.MarkFlagRequired("password") + + return cmd +} diff --git a/cmd/cli/main.go b/cmd/cli/main.go new file mode 100644 index 0000000..6bcfadc --- /dev/null +++ b/cmd/cli/main.go @@ -0,0 +1,10 @@ +// Copyright 2022 D2iQ, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import "github.com/mesosphere/dynamic-credential-provider/cmd/cli/cmd" + +func main() { + cmd.Execute() +} diff --git a/go.mod b/go.mod index 8d07f86..9d3e50c 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/foomo/htpasswd v0.0.0-20200116085101-e3a90e78da9c github.com/fsnotify/fsnotify v1.5.4 github.com/kelseyhightower/envconfig v1.4.0 + github.com/mesosphere/dkp-cli-runtime/core v0.7.1 github.com/onsi/ginkgo/v2 v2.5.1 github.com/onsi/gomega v1.24.1 github.com/otiai10/copy v1.9.0 @@ -60,6 +61,7 @@ require ( github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect github.com/containerd/containerd v1.6.6 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/cyphar/filepath-securejoin v0.2.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/cli v20.10.17+incompatible // indirect @@ -103,7 +105,7 @@ require ( github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-colorable v0.1.12 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect @@ -128,6 +130,7 @@ require ( github.com/prometheus/procfs v0.7.3 // indirect github.com/rubenv/sql-migrate v1.1.2 // indirect github.com/russross/blackfriday v1.5.2 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect github.com/spf13/cast v1.4.1 // indirect github.com/spf13/pflag v1.0.5 // indirect diff --git a/go.sum b/go.sum index 34962db..64aa70e 100644 --- a/go.sum +++ b/go.sum @@ -145,6 +145,7 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= @@ -446,8 +447,9 @@ github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZb github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= @@ -457,6 +459,8 @@ github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mesosphere/dkp-cli-runtime/core v0.7.1 h1:t4MUV6X3VMaQcx4H9//UtGBU7cA0r3l9FEq4aqdczrY= +github.com/mesosphere/dkp-cli-runtime/core v0.7.1/go.mod h1:mlSRuXJaHeOFfSKhC3ZxOm+gfQuP9jT5WuFe3e0EGYs= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.1.2/go.mod h1:6iaV0fGdElS6dPBx0EApTxHrcWvmJphyh2n8YBLPPZ4= @@ -566,6 +570,7 @@ github.com/rubenv/sql-migrate v1.1.2/go.mod h1:/7TZymwxN8VWumcIxw1jjHEcR1djpdkMH github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= @@ -858,6 +863,7 @@ golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/pkg/credentialmanager/plugin/plugin.go b/pkg/credentialmanager/plugin/plugin.go new file mode 100644 index 0000000..d90a537 --- /dev/null +++ b/pkg/credentialmanager/plugin/plugin.go @@ -0,0 +1,10 @@ +// Copyright 2022 D2iQ, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package plugin + +import "context" + +type CredentialManager interface { + Update(ctx context.Context, address, username, password string) error +} diff --git a/pkg/credentialmanager/secret/secret.go b/pkg/credentialmanager/secret/secret.go new file mode 100644 index 0000000..0608d9e --- /dev/null +++ b/pkg/credentialmanager/secret/secret.go @@ -0,0 +1,110 @@ +// Copyright 2022 D2iQ, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package secret + +import ( + "context" + "fmt" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + + "github.com/mesosphere/dynamic-credential-provider/pkg/credentialmanager/plugin" + credentialproviderplugin "github.com/mesosphere/dynamic-credential-provider/pkg/credentialprovider/plugin" + "github.com/mesosphere/dynamic-credential-provider/pkg/credentialprovider/static" +) + +//nolint:gosec // No credentials here. +const ( + SecretName = "staticcredentialproviderauth" + SecretNamespace = "kube-system" + + SecretKeyName = "static-image-credentials.json" +) + +type CredentialManager struct { + client kubernetes.Interface + + name string + namespace string + key string +} + +func NewSecretsCredentialManager(client kubernetes.Interface) plugin.CredentialManager { + return &CredentialManager{ + client: client, + name: SecretName, + namespace: SecretNamespace, + key: SecretKeyName, + } +} + +func (m *CredentialManager) Update( + ctx context.Context, + address, username, password string, +) error { + secrets := m.client.CoreV1().Secrets( + m.namespace, + ) + secret, err := secrets.Get( + ctx, + m.name, + metav1.GetOptions{}, + ) + if err != nil { + return fmt.Errorf("unable to get secret: %w", err) + } + + err = m.updateSecret(secret, address, username, password) + if err != nil { + return err + } + + _, err = secrets.Update(ctx, secret, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("unable to update secret: %w", err) + } + + return nil +} + +func (m *CredentialManager) updateSecret( + secret *v1.Secret, + address, username, password string, +) error { + data, found := secret.Data[m.key] + if !found { + return fmt.Errorf("secret %s/%s exists, but missing key %q", m.namespace, m.name, m.key) + } + + credentialProviderResponse, err := static.DecodeResponse(data) + if err != nil { + return fmt.Errorf("failed to decode Secret data: %w", err) + } + + auth, found := credentialProviderResponse.Auth[address] + if !found { + return fmt.Errorf( + "secret %s/%s exists, but missing entry for registry %q", + m.namespace, + m.name, + address, + ) + } + auth.Username = username + auth.Password = password + + credentialProviderResponse.Auth[address] = auth + + data, err = credentialproviderplugin.EncodeResponse(credentialProviderResponse) + if err != nil { + return fmt.Errorf("failed to encode Secret data: %w", err) + } + + // Secret found. Update it. + secret.Data[m.key] = data + + return nil +} diff --git a/pkg/credentialmanager/secret/secret_test.go b/pkg/credentialmanager/secret/secret_test.go new file mode 100644 index 0000000..e887836 --- /dev/null +++ b/pkg/credentialmanager/secret/secret_test.go @@ -0,0 +1,107 @@ +// Copyright 2022 D2iQ, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package secret + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var ( + //nolint:lll // Just a long string. + initialResponse = []byte( + `{"kind":"CredentialProviderResponse","apiVersion":"credentialprovider.kubelet.k8s.io/v1beta1","cacheKeyType":"Image","cacheDuration":"0s","auth":{"docker.io":{"username":"initialusername","password":"initialpassword"}}} +`, + ) + //nolint:lll // Just a long string. + updatedResponse = []byte( + `{"kind":"CredentialProviderResponse","apiVersion":"credentialprovider.kubelet.k8s.io/v1beta1","cacheKeyType":"Image","cacheDuration":"0s","auth":{"docker.io":{"username":"newusername","password":"newpassword"}}} +`, + ) +) + +func Test_updateSecret(t *testing.T) { + t.Parallel() + + testcases := []struct { + name string + address string + username string + password string + initialData map[string][]byte + expectedData map[string][]byte + expectErr error + }{ + { + name: "credentials are updated", + address: "docker.io", + username: "newusername", + password: "newpassword", + initialData: map[string][]byte{SecretKeyName: initialResponse}, + expectedData: map[string][]byte{SecretKeyName: updatedResponse}, + }, + { + name: "credentials not updated, missing Secret key", + address: "docker.io", + username: "newusername", + password: "newpassword", + initialData: map[string][]byte{"wrong-key": initialResponse}, + expectedData: map[string][]byte{"wrong-key": initialResponse}, + expectErr: fmt.Errorf( + "secret kube-system/staticcredentialproviderauth exists, but missing key \"static-image-credentials.json\"", + ), + }, + { + name: "credentials not updated, missing registry entry", + address: "docker.com", + username: "newusername", + password: "newpassword", + initialData: map[string][]byte{SecretKeyName: initialResponse}, + expectedData: map[string][]byte{SecretKeyName: initialResponse}, + expectErr: fmt.Errorf( + "secret kube-system/staticcredentialproviderauth exists, but missing entry for registry \"docker.com\"", + ), + }, + } + + for _, tt := range testcases { + tt := tt // Capture range variable. + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + manager := testManager() + secret := testSecret(tt.initialData) + err := manager.updateSecret(secret, tt.address, tt.username, tt.password) + assert.Equal(t, tt.expectErr, err) + + assert.Equal( + t, + string(tt.expectedData[SecretKeyName]), + string(secret.Data[SecretKeyName]), + ) + }) + } +} + +func testManager() *CredentialManager { + return &CredentialManager{ + name: SecretName, + namespace: SecretNamespace, + key: SecretKeyName, + } +} + +func testSecret(data map[string][]byte) *v1.Secret { + return &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: SecretName, + Namespace: SecretNamespace, + }, + Data: data, + } +} diff --git a/pkg/credentialprovider/plugin/plugin.go b/pkg/credentialprovider/plugin/plugin.go index 4681250..84ca746 100644 --- a/pkg/credentialprovider/plugin/plugin.go +++ b/pkg/credentialprovider/plugin/plugin.go @@ -97,7 +97,7 @@ func (e *ExecPlugin) runPlugin(ctx context.Context, r io.Reader, w io.Writer, ar return fmt.Errorf("%w", ErrNilCredentialProviderResponse) } - encodedResponse, err := encodeResponse(response) + encodedResponse, err := EncodeResponse(response) if err != nil { return err } @@ -154,7 +154,7 @@ func decodeRequest(data []byte) (*v1beta1.CredentialProviderRequest, error) { return request, nil } -func encodeResponse(response *v1beta1.CredentialProviderResponse) ([]byte, error) { +func EncodeResponse(response *v1beta1.CredentialProviderResponse) ([]byte, error) { mediaType := "application/json" info, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), mediaType) if !ok { diff --git a/pkg/credentialprovider/static/static_credentials.go b/pkg/credentialprovider/static/static_credentials.go index 3d96661..32ee681 100644 --- a/pkg/credentialprovider/static/static_credentials.go +++ b/pkg/credentialprovider/static/static_credentials.go @@ -47,10 +47,10 @@ func (s staticProvider) GetCredentials( return nil, fmt.Errorf("error reading credentials file: %w", err) } - return decodeResponse(credentials) + return DecodeResponse(credentials) } -func decodeResponse(data []byte) (*v1beta1.CredentialProviderResponse, error) { +func DecodeResponse(data []byte) (*v1beta1.CredentialProviderResponse, error) { obj, gvk, err := codecs.UniversalDecoder(v1beta1.SchemeGroupVersion).Decode(data, nil, nil) if err != nil { return nil, err diff --git a/pkg/k8s/client/client.go b/pkg/k8s/client/client.go new file mode 100644 index 0000000..1d9d613 --- /dev/null +++ b/pkg/k8s/client/client.go @@ -0,0 +1,59 @@ +// Copyright 2022 D2iQ, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package client + +import ( + "fmt" + + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + + "github.com/mesosphere/dkp-cli-runtime/core/cmd/version" +) + +type k8sClientCreateError struct { + err error +} + +func (e k8sClientCreateError) Error() string { + return fmt.Sprintf("unable to create kubernetes client: %v", e.err) +} + +func NewFromKubeconfig(kubeconfig string) (kubernetes.Interface, clientcmd.ClientConfig, error) { + config, clientConfig, err := clientConfigsFromKubeconfig(kubeconfig) + if err != nil { + return nil, nil, err + } + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, nil, k8sClientCreateError{err: err} + } + return clientset, clientConfig, nil +} + +func clientConfigsFromKubeconfig(kubeconfig string) (*rest.Config, clientcmd.ClientConfig, error) { + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + if kubeconfig != "" { + loadingRules.ExplicitPath = kubeconfig + } + overrides := &clientcmd.ConfigOverrides{} + clientConfig := clientcmd.NewInteractiveDeferredLoadingClientConfig( + loadingRules, + overrides, + nil, + ) + + config, err := clientConfig.ClientConfig() + if err != nil { + return nil, nil, k8sClientCreateError{err: err} + } + + config.UserAgent = UserAgent() + return config, clientConfig, nil +} + +func UserAgent() string { + return fmt.Sprintf("credential-manager/%s", version.GetVersion().GitVersion) +}