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

Commit 6bc1724

Browse files
authored
[feature] implement kim-native credentials (#71)
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 <[email protected]>
1 parent 6fe4730 commit 6bc1724

File tree

8 files changed

+217
-5
lines changed

8 files changed

+217
-5
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ require (
4646
github.com/gogo/protobuf v1.3.2
4747
github.com/golang/protobuf v1.4.3
4848
github.com/moby/buildkit v0.8.3
49+
github.com/moby/term v0.0.0-20200915141129-7f0af18e79f2
4950
github.com/opencontainers/image-spec v1.0.1
5051
github.com/pkg/errors v0.9.1
5152
github.com/rancher/wrangler v0.7.3-0.20201002224307-4303c423125a

pkg/cli/command/builder/builder.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55

66
"github.com/rancher/kim/pkg/cli/command/builder/install"
7+
"github.com/rancher/kim/pkg/cli/command/builder/login"
78
"github.com/rancher/kim/pkg/cli/command/builder/uninstall"
89
wrangler "github.com/rancher/wrangler-cli"
910
"github.com/spf13/cobra"
@@ -26,6 +27,7 @@ func Command() *cobra.Command {
2627
cmd.AddCommand(
2728
install.Command(),
2829
uninstall.Command(),
30+
login.Command(),
2931
)
3032
return cmd
3133
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package login
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"io/ioutil"
7+
"net/url"
8+
"os"
9+
"strings"
10+
11+
"github.com/moby/term"
12+
"github.com/pkg/errors"
13+
"github.com/rancher/kim/pkg/client"
14+
"github.com/rancher/kim/pkg/client/builder"
15+
wrangler "github.com/rancher/wrangler-cli"
16+
"github.com/spf13/cobra"
17+
"k8s.io/kubernetes/pkg/credentialprovider"
18+
)
19+
20+
func Command() *cobra.Command {
21+
return wrangler.Command(&CommandSpec{}, cobra.Command{
22+
Use: "login [OPTIONS] [SERVER]",
23+
Short: "Establish credentials for a registry.",
24+
DisableFlagsInUseLine: true,
25+
Args: cobra.ExactArgs(1),
26+
})
27+
}
28+
29+
type CommandSpec struct {
30+
builder.Login
31+
}
32+
33+
func (s *CommandSpec) Run(cmd *cobra.Command, args []string) error {
34+
k8s, err := client.DefaultConfig.Interface()
35+
if err != nil {
36+
return err
37+
}
38+
if s.PasswordStdin {
39+
if s.Password != "" {
40+
return errors.New("--password and --password-stdin are mutually exclusive")
41+
}
42+
if s.Username == "" {
43+
return errors.New("must provide --username with --password-stdin")
44+
}
45+
password, err := ioutil.ReadAll(cmd.InOrStdin())
46+
if err != nil {
47+
return err
48+
}
49+
s.Password = strings.TrimSuffix(string(password), "\n")
50+
s.Password = strings.TrimSuffix(s.Password, "\r")
51+
}
52+
if (s.Username == "" || s.Password == "") && !term.IsTerminal(os.Stdout.Fd()) {
53+
return errors.New("cannot perform interactive login from non tty device")
54+
}
55+
if s.Username == "" {
56+
fmt.Fprintf(os.Stdout, "Username: ")
57+
reader := bufio.NewReader(os.Stdin)
58+
line, _, err := reader.ReadLine()
59+
if err != nil {
60+
return err
61+
}
62+
s.Username = strings.TrimSpace(string(line))
63+
}
64+
if s.Password == "" {
65+
state, err := term.SaveState(os.Stdin.Fd())
66+
if err != nil {
67+
return err
68+
}
69+
fmt.Fprintf(os.Stdout, "Password: ")
70+
term.DisableEcho(os.Stdin.Fd(), state)
71+
reader := bufio.NewReader(os.Stdin)
72+
line, _, err := reader.ReadLine()
73+
if err != nil {
74+
return err
75+
}
76+
fmt.Fprintln(os.Stdout)
77+
term.RestoreTerminal(os.Stdin.Fd(), state)
78+
s.Password = strings.TrimSpace(string(line))
79+
if s.Password == "" {
80+
return errors.New("password is required")
81+
}
82+
}
83+
server, err := credentialprovider.ParseSchemelessURL(args[0])
84+
if err != nil {
85+
if server, err = url.Parse(args[0]); err != nil {
86+
return err
87+
}
88+
}
89+
// special case for [*.]docker.io -> https://index.docker.io/v1/
90+
if strings.HasSuffix(server.Host, "docker.io") {
91+
server.Scheme = "https"
92+
server.Host = "index.docker.io"
93+
if server.Path == "" {
94+
server.Path = "/v1/"
95+
}
96+
return s.Login.Do(cmd.Context(), k8s, server.String())
97+
}
98+
return s.Login.Do(cmd.Context(), k8s, server.Host)
99+
}

pkg/client/builder/login.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package builder
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
7+
"github.com/rancher/kim/pkg/client"
8+
corev1 "k8s.io/api/core/v1"
9+
apierr "k8s.io/apimachinery/pkg/api/errors"
10+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11+
"k8s.io/apimachinery/pkg/labels"
12+
"k8s.io/client-go/util/retry"
13+
"k8s.io/kubernetes/pkg/credentialprovider"
14+
)
15+
16+
type Login struct {
17+
Password string `usage:"Password" short:"p"`
18+
PasswordStdin bool `usage:"Take the password from stdin"`
19+
Username string `usage:"Username" short:"u"`
20+
}
21+
22+
func (s *Login) Do(_ context.Context, k *client.Interface, server string) error {
23+
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
24+
login, err := k.Core.Secret().Get(k.Namespace, "kim-docker-config", metav1.GetOptions{})
25+
if apierr.IsNotFound(err) {
26+
dockerConfigJSON := credentialprovider.DockerConfigJSON{
27+
Auths: map[string]credentialprovider.DockerConfigEntry{
28+
server: {
29+
Username: s.Username,
30+
Password: s.Password,
31+
},
32+
},
33+
}
34+
dockerConfigJSONBytes, err := json.Marshal(&dockerConfigJSON)
35+
if err != nil {
36+
return err
37+
}
38+
login = &corev1.Secret{
39+
ObjectMeta: metav1.ObjectMeta{
40+
Name: "kim-docker-config",
41+
Namespace: k.Namespace,
42+
Labels: labels.Set{
43+
"app.kubernetes.io/managed-by": "kim",
44+
},
45+
},
46+
Type: corev1.SecretTypeDockerConfigJson,
47+
Data: map[string][]byte{
48+
corev1.DockerConfigJsonKey: dockerConfigJSONBytes,
49+
},
50+
}
51+
_, err = k.Core.Secret().Create(login)
52+
return err
53+
}
54+
var dockerConfigJSON credentialprovider.DockerConfigJSON
55+
if dockerConfigJSONBytes, ok := login.Data[corev1.DockerConfigJsonKey]; ok {
56+
if err := json.Unmarshal(dockerConfigJSONBytes, &dockerConfigJSON); err != nil {
57+
return err
58+
}
59+
}
60+
dockerConfigJSON.Auths[server] = credentialprovider.DockerConfigEntry{
61+
Username: s.Username,
62+
Password: s.Password,
63+
}
64+
dockerConfigJSONBytes, err := json.Marshal(&dockerConfigJSON)
65+
if err != nil {
66+
return err
67+
}
68+
login.Type = corev1.SecretTypeDockerConfigJson
69+
login.Data[corev1.DockerConfigJsonKey] = dockerConfigJSONBytes
70+
_, err = k.Core.Secret().Update(login)
71+
return err
72+
})
73+
}

pkg/client/client.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ import (
1414
rbacctl "github.com/rancher/wrangler/pkg/generated/controllers/rbac"
1515
rbacctlv1 "github.com/rancher/wrangler/pkg/generated/controllers/rbac/v1"
1616
"github.com/rancher/wrangler/pkg/kubeconfig"
17+
"github.com/sirupsen/logrus"
18+
corev1 "k8s.io/api/core/v1"
1719
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
20+
"k8s.io/kubernetes/pkg/credentialprovider"
21+
"k8s.io/kubernetes/pkg/credentialprovider/secrets"
1822
)
1923

2024
const (
@@ -127,3 +131,17 @@ func GetServiceAddress(_ context.Context, k8s *Interface, port string) (string,
127131
}
128132
return "", errors.New("unknown service port")
129133
}
134+
135+
func GetDockerKeyring(_ context.Context, k8s *Interface) credentialprovider.DockerKeyring {
136+
secret, err := k8s.Core.Secret().Get(k8s.Namespace, "kim-docker-config", metav1.GetOptions{})
137+
if err != nil {
138+
logrus.Debug(err)
139+
return credentialprovider.NewDockerKeyring()
140+
}
141+
keyring, err := secrets.MakeDockerKeyring([]corev1.Secret{*secret}, nil)
142+
if err != nil {
143+
logrus.Debug(err)
144+
return credentialprovider.NewDockerKeyring()
145+
}
146+
return keyring
147+
}

pkg/client/control.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
buildkit "github.com/moby/buildkit/client"
1111
"github.com/pkg/errors"
12+
"github.com/sirupsen/logrus"
1213
corev1 "k8s.io/api/core/v1"
1314
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1415
)
@@ -21,7 +22,7 @@ func Control(ctx context.Context, k8s *Interface, fn ControlFunc) error {
2122
return err
2223
}
2324

24-
tmp, err := ioutil.TempDir("", "kim-tls-*")
25+
tmp, err := ioutil.TempDir("", "kim-private-*")
2526
if err != nil {
2627
return errors.Wrap(err, "failed to create temp directory")
2728
}
@@ -64,6 +65,26 @@ func Control(ctx context.Context, k8s *Interface, fn ControlFunc) error {
6465
}
6566
}
6667

68+
// docker-config
69+
secret, err = k8s.Core.Secret().Get(k8s.Namespace, "kim-docker-config", metav1.GetOptions{})
70+
switch {
71+
case err != nil:
72+
logrus.Debugf("skipping kim-docker-config with error: %v", err)
73+
case secret.Type != corev1.SecretTypeDockerConfigJson:
74+
logrus.Warnf("skipping kim-docker-config with unsupported type: %s", secret.Type)
75+
case secret.Type == corev1.SecretTypeDockerConfigJson:
76+
if dockerConfigJSONBytes, ok := secret.Data[corev1.DockerConfigJsonKey]; ok {
77+
if err := ioutil.WriteFile(filepath.Join(tmp, "config.json"), dockerConfigJSONBytes, 0600); err != nil {
78+
return errors.Wrap(err, "failed to write docker config")
79+
}
80+
if err := os.Setenv("DOCKER_CONFIG", tmp); err != nil {
81+
return errors.Wrap(err, "failed to setup docker config")
82+
}
83+
} else {
84+
logrus.Warnf("skipping kim-docker-config with missing value %s", corev1.DockerConfigJsonKey)
85+
}
86+
}
87+
6788
bkc, err := buildkit.New(ctx, fmt.Sprintf("tcp://%s", addr), options...)
6889
if err != nil {
6990
return err

pkg/client/image/pull.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
"github.com/sirupsen/logrus"
1515
"golang.org/x/sync/errgroup"
1616
criv1 "k8s.io/cri-api/pkg/apis/runtime/v1alpha2"
17-
"k8s.io/kubernetes/pkg/credentialprovider"
1817
)
1918

2019
type Pull struct {
@@ -74,7 +73,7 @@ func (s *Pull) Do(ctx context.Context, k8s *client.Interface, image string) erro
7473
if s.Cri {
7574
req.Image.Annotations["images.cattle.io/pull-backend"] = "cri"
7675
}
77-
keyring := credentialprovider.NewDockerKeyring()
76+
keyring := client.GetDockerKeyring(ctx, k8s)
7877
if auth, ok := keyring.Lookup(image); ok {
7978
req.Auth = &criv1.AuthConfig{
8079
Username: auth[0].Username,

pkg/client/image/push.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"github.com/sirupsen/logrus"
1414
"golang.org/x/sync/errgroup"
1515
criv1 "k8s.io/cri-api/pkg/apis/runtime/v1alpha2"
16-
"k8s.io/kubernetes/pkg/credentialprovider"
1716
)
1817

1918
type Push struct {
@@ -58,7 +57,7 @@ func (s *Push) Do(ctx context.Context, k8s *client.Interface, image string) erro
5857
Image: image,
5958
},
6059
}
61-
keyring := credentialprovider.NewDockerKeyring()
60+
keyring := client.GetDockerKeyring(ctx, k8s)
6261
if auth, ok := keyring.Lookup(image); ok {
6362
req.Auth = &criv1.AuthConfig{
6463
Username: auth[0].Username,

0 commit comments

Comments
 (0)