Skip to content
This repository has been archived by the owner on Sep 2, 2024. It is now read-only.

Commit

Permalink
[feature] implement kim-native credentials (#71)
Browse files Browse the repository at this point in the history
Introduce `kim builder login` that works very much like `docker login`
but instead stores the resulting Docker `config.json` in a kubernetes
secret in the builder namespace. This secret is rendered to disk in a
temp directory for `build` operations (to satisfy buildkit) but is
leveraged as an in-memory keyring for shipping auth credentials for
`push` / `pull` operations. If the secret setup by the `login` cli
operation does not exist, kim reverts to the existing behavior of
consulting the `${DOCKER_CONFIG}/config.json` for registry credentials.

Addresses #64

Signed-off-by: Jacob Blain Christen <jacob@rancher.com>
  • Loading branch information
dweomer authored Sep 1, 2021
1 parent 6fe4730 commit 6bc1724
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 5 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions pkg/cli/command/builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -26,6 +27,7 @@ func Command() *cobra.Command {
cmd.AddCommand(
install.Command(),
uninstall.Command(),
login.Command(),
)
return cmd
}
Expand Down
99 changes: 99 additions & 0 deletions pkg/cli/command/builder/login/login.go
Original file line number Diff line number Diff line change
@@ -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)
}
73 changes: 73 additions & 0 deletions pkg/client/builder/login.go
Original file line number Diff line number Diff line change
@@ -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
})
}
18 changes: 18 additions & 0 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
}
23 changes: 22 additions & 1 deletion pkg/client/control.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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")
}
Expand Down Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions pkg/client/image/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 1 addition & 2 deletions pkg/client/image/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 6bc1724

Please sign in to comment.