diff --git a/go.mod b/go.mod index 1d70b38..8c8e864 100644 --- a/go.mod +++ b/go.mod @@ -46,6 +46,7 @@ require ( github.com/gogo/protobuf v1.3.2 github.com/golang/protobuf v1.4.3 github.com/moby/buildkit v0.8.3 + github.com/moby/term v0.0.0-20200915141129-7f0af18e79f2 github.com/opencontainers/image-spec v1.0.1 github.com/pkg/errors v0.9.1 github.com/rancher/wrangler v0.7.3-0.20201002224307-4303c423125a diff --git a/pkg/cli/command/builder/builder.go b/pkg/cli/command/builder/builder.go index 6f27328..e8e1145 100644 --- a/pkg/cli/command/builder/builder.go +++ b/pkg/cli/command/builder/builder.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/rancher/kim/pkg/cli/command/builder/install" + "github.com/rancher/kim/pkg/cli/command/builder/login" "github.com/rancher/kim/pkg/cli/command/builder/uninstall" wrangler "github.com/rancher/wrangler-cli" "github.com/spf13/cobra" @@ -26,6 +27,7 @@ func Command() *cobra.Command { cmd.AddCommand( install.Command(), uninstall.Command(), + login.Command(), ) return cmd } diff --git a/pkg/cli/command/builder/login/login.go b/pkg/cli/command/builder/login/login.go new file mode 100644 index 0000000..4bd212c --- /dev/null +++ b/pkg/cli/command/builder/login/login.go @@ -0,0 +1,99 @@ +package login + +import ( + "bufio" + "fmt" + "io/ioutil" + "net/url" + "os" + "strings" + + "github.com/moby/term" + "github.com/pkg/errors" + "github.com/rancher/kim/pkg/client" + "github.com/rancher/kim/pkg/client/builder" + wrangler "github.com/rancher/wrangler-cli" + "github.com/spf13/cobra" + "k8s.io/kubernetes/pkg/credentialprovider" +) + +func Command() *cobra.Command { + return wrangler.Command(&CommandSpec{}, cobra.Command{ + Use: "login [OPTIONS] [SERVER]", + Short: "Establish credentials for a registry.", + DisableFlagsInUseLine: true, + Args: cobra.ExactArgs(1), + }) +} + +type CommandSpec struct { + builder.Login +} + +func (s *CommandSpec) Run(cmd *cobra.Command, args []string) error { + k8s, err := client.DefaultConfig.Interface() + if err != nil { + return err + } + if s.PasswordStdin { + if s.Password != "" { + return errors.New("--password and --password-stdin are mutually exclusive") + } + if s.Username == "" { + return errors.New("must provide --username with --password-stdin") + } + password, err := ioutil.ReadAll(cmd.InOrStdin()) + if err != nil { + return err + } + s.Password = strings.TrimSuffix(string(password), "\n") + s.Password = strings.TrimSuffix(s.Password, "\r") + } + if (s.Username == "" || s.Password == "") && !term.IsTerminal(os.Stdout.Fd()) { + return errors.New("cannot perform interactive login from non tty device") + } + if s.Username == "" { + fmt.Fprintf(os.Stdout, "Username: ") + reader := bufio.NewReader(os.Stdin) + line, _, err := reader.ReadLine() + if err != nil { + return err + } + s.Username = strings.TrimSpace(string(line)) + } + if s.Password == "" { + state, err := term.SaveState(os.Stdin.Fd()) + if err != nil { + return err + } + fmt.Fprintf(os.Stdout, "Password: ") + term.DisableEcho(os.Stdin.Fd(), state) + reader := bufio.NewReader(os.Stdin) + line, _, err := reader.ReadLine() + if err != nil { + return err + } + fmt.Fprintln(os.Stdout) + term.RestoreTerminal(os.Stdin.Fd(), state) + s.Password = strings.TrimSpace(string(line)) + if s.Password == "" { + return errors.New("password is required") + } + } + server, err := credentialprovider.ParseSchemelessURL(args[0]) + if err != nil { + if server, err = url.Parse(args[0]); err != nil { + return err + } + } + // special case for [*.]docker.io -> https://index.docker.io/v1/ + if strings.HasSuffix(server.Host, "docker.io") { + server.Scheme = "https" + server.Host = "index.docker.io" + if server.Path == "" { + server.Path = "/v1/" + } + return s.Login.Do(cmd.Context(), k8s, server.String()) + } + return s.Login.Do(cmd.Context(), k8s, server.Host) +} diff --git a/pkg/client/builder/login.go b/pkg/client/builder/login.go new file mode 100644 index 0000000..e1b6752 --- /dev/null +++ b/pkg/client/builder/login.go @@ -0,0 +1,73 @@ +package builder + +import ( + "context" + "encoding/json" + + "github.com/rancher/kim/pkg/client" + corev1 "k8s.io/api/core/v1" + apierr "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/util/retry" + "k8s.io/kubernetes/pkg/credentialprovider" +) + +type Login struct { + Password string `usage:"Password" short:"p"` + PasswordStdin bool `usage:"Take the password from stdin"` + Username string `usage:"Username" short:"u"` +} + +func (s *Login) Do(_ context.Context, k *client.Interface, server string) error { + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + login, err := k.Core.Secret().Get(k.Namespace, "kim-docker-config", metav1.GetOptions{}) + if apierr.IsNotFound(err) { + dockerConfigJSON := credentialprovider.DockerConfigJSON{ + Auths: map[string]credentialprovider.DockerConfigEntry{ + server: { + Username: s.Username, + Password: s.Password, + }, + }, + } + dockerConfigJSONBytes, err := json.Marshal(&dockerConfigJSON) + if err != nil { + return err + } + login = &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kim-docker-config", + Namespace: k.Namespace, + Labels: labels.Set{ + "app.kubernetes.io/managed-by": "kim", + }, + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + corev1.DockerConfigJsonKey: dockerConfigJSONBytes, + }, + } + _, err = k.Core.Secret().Create(login) + return err + } + var dockerConfigJSON credentialprovider.DockerConfigJSON + if dockerConfigJSONBytes, ok := login.Data[corev1.DockerConfigJsonKey]; ok { + if err := json.Unmarshal(dockerConfigJSONBytes, &dockerConfigJSON); err != nil { + return err + } + } + dockerConfigJSON.Auths[server] = credentialprovider.DockerConfigEntry{ + Username: s.Username, + Password: s.Password, + } + dockerConfigJSONBytes, err := json.Marshal(&dockerConfigJSON) + if err != nil { + return err + } + login.Type = corev1.SecretTypeDockerConfigJson + login.Data[corev1.DockerConfigJsonKey] = dockerConfigJSONBytes + _, err = k.Core.Secret().Update(login) + return err + }) +} diff --git a/pkg/client/client.go b/pkg/client/client.go index 47e285d..a85d795 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -14,7 +14,11 @@ import ( rbacctl "github.com/rancher/wrangler/pkg/generated/controllers/rbac" rbacctlv1 "github.com/rancher/wrangler/pkg/generated/controllers/rbac/v1" "github.com/rancher/wrangler/pkg/kubeconfig" + "github.com/sirupsen/logrus" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/kubernetes/pkg/credentialprovider" + "k8s.io/kubernetes/pkg/credentialprovider/secrets" ) const ( @@ -127,3 +131,17 @@ func GetServiceAddress(_ context.Context, k8s *Interface, port string) (string, } return "", errors.New("unknown service port") } + +func GetDockerKeyring(_ context.Context, k8s *Interface) credentialprovider.DockerKeyring { + secret, err := k8s.Core.Secret().Get(k8s.Namespace, "kim-docker-config", metav1.GetOptions{}) + if err != nil { + logrus.Debug(err) + return credentialprovider.NewDockerKeyring() + } + keyring, err := secrets.MakeDockerKeyring([]corev1.Secret{*secret}, nil) + if err != nil { + logrus.Debug(err) + return credentialprovider.NewDockerKeyring() + } + return keyring +} diff --git a/pkg/client/control.go b/pkg/client/control.go index a5d552c..befbdb0 100644 --- a/pkg/client/control.go +++ b/pkg/client/control.go @@ -9,6 +9,7 @@ import ( buildkit "github.com/moby/buildkit/client" "github.com/pkg/errors" + "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -21,7 +22,7 @@ func Control(ctx context.Context, k8s *Interface, fn ControlFunc) error { return err } - tmp, err := ioutil.TempDir("", "kim-tls-*") + tmp, err := ioutil.TempDir("", "kim-private-*") if err != nil { return errors.Wrap(err, "failed to create temp directory") } @@ -64,6 +65,26 @@ func Control(ctx context.Context, k8s *Interface, fn ControlFunc) error { } } + // docker-config + secret, err = k8s.Core.Secret().Get(k8s.Namespace, "kim-docker-config", metav1.GetOptions{}) + switch { + case err != nil: + logrus.Debugf("skipping kim-docker-config with error: %v", err) + case secret.Type != corev1.SecretTypeDockerConfigJson: + logrus.Warnf("skipping kim-docker-config with unsupported type: %s", secret.Type) + case secret.Type == corev1.SecretTypeDockerConfigJson: + if dockerConfigJSONBytes, ok := secret.Data[corev1.DockerConfigJsonKey]; ok { + if err := ioutil.WriteFile(filepath.Join(tmp, "config.json"), dockerConfigJSONBytes, 0600); err != nil { + return errors.Wrap(err, "failed to write docker config") + } + if err := os.Setenv("DOCKER_CONFIG", tmp); err != nil { + return errors.Wrap(err, "failed to setup docker config") + } + } else { + logrus.Warnf("skipping kim-docker-config with missing value %s", corev1.DockerConfigJsonKey) + } + } + bkc, err := buildkit.New(ctx, fmt.Sprintf("tcp://%s", addr), options...) if err != nil { return err diff --git a/pkg/client/image/pull.go b/pkg/client/image/pull.go index a2d5ce0..be6fee5 100644 --- a/pkg/client/image/pull.go +++ b/pkg/client/image/pull.go @@ -14,7 +14,6 @@ import ( "github.com/sirupsen/logrus" "golang.org/x/sync/errgroup" criv1 "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - "k8s.io/kubernetes/pkg/credentialprovider" ) type Pull struct { @@ -74,7 +73,7 @@ func (s *Pull) Do(ctx context.Context, k8s *client.Interface, image string) erro if s.Cri { req.Image.Annotations["images.cattle.io/pull-backend"] = "cri" } - keyring := credentialprovider.NewDockerKeyring() + keyring := client.GetDockerKeyring(ctx, k8s) if auth, ok := keyring.Lookup(image); ok { req.Auth = &criv1.AuthConfig{ Username: auth[0].Username, diff --git a/pkg/client/image/push.go b/pkg/client/image/push.go index 5a34b5c..1d0a958 100644 --- a/pkg/client/image/push.go +++ b/pkg/client/image/push.go @@ -13,7 +13,6 @@ import ( "github.com/sirupsen/logrus" "golang.org/x/sync/errgroup" criv1 "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - "k8s.io/kubernetes/pkg/credentialprovider" ) type Push struct { @@ -58,7 +57,7 @@ func (s *Push) Do(ctx context.Context, k8s *client.Interface, image string) erro Image: image, }, } - keyring := credentialprovider.NewDockerKeyring() + keyring := client.GetDockerKeyring(ctx, k8s) if auth, ok := keyring.Lookup(image); ok { req.Auth = &criv1.AuthConfig{ Username: auth[0].Username,