Skip to content

Commit 2b136a6

Browse files
authored
Merge pull request #1963 from lizardruss/merge-pull-secrets
feat: enable multiple pull secrets to be used by a service account
2 parents 77c7967 + c974f44 commit 2b136a6

3 files changed

Lines changed: 162 additions & 60 deletions

File tree

e2e/tests/pullsecret/pullsecrets.go

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package pullsecret
33
import (
44
"context"
55
"encoding/base64"
6+
"os"
7+
"sort"
8+
69
"github.com/loft-sh/devspace/cmd"
710
"github.com/loft-sh/devspace/cmd/flags"
811
"github.com/loft-sh/devspace/e2e/framework"
@@ -11,8 +14,6 @@ import (
1114
"github.com/onsi/ginkgo"
1215
k8sv1 "k8s.io/api/core/v1"
1316
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14-
"os"
15-
"sort"
1617
)
1718

1819
var _ = DevSpaceDescribe("pullsecret", func() {
@@ -58,42 +59,40 @@ var _ = DevSpaceDescribe("pullsecret", func() {
5859
err = deployCmd.Run(f)
5960
framework.ExpectNoError(err)
6061

61-
// check if secrets are created
62+
// check if named secret is created
6263
pullSecret, err := kubeClient.RawClient().CoreV1().Secrets(ns).Get(context.TODO(), "test-secret", metav1.GetOptions{})
6364
framework.ExpectNoError(err)
6465
framework.ExpectEqual(len(pullSecret.Data), 1)
6566
registryAuthEncoded := base64.StdEncoding.EncodeToString([]byte("my-user:my-password"))
66-
pullSecretDataValue := []byte(`{
67-
"auths": {
68-
"ghcr.io": {
69-
"auth": "` + registryAuthEncoded + `",
70-
"email": "noreply@devspace.sh"
71-
}
72-
}
73-
}`)
67+
pullSecretDataValue := []byte(`{"auths":{"ghcr.io":{"auth":"` + registryAuthEncoded + `","email":"noreply@devspace.sh"}}}`)
68+
framework.ExpectEqual(string(pullSecret.Data[k8sv1.DockerConfigJsonKey]), string(pullSecretDataValue))
69+
70+
// check if default secrets are created and merged
71+
pullSecret, err = kubeClient.RawClient().CoreV1().Secrets(ns).Get(context.TODO(), "devspace-pull-secrets", metav1.GetOptions{})
72+
framework.ExpectNoError(err)
73+
framework.ExpectEqual(len(pullSecret.Data), 1)
74+
registryAuth2Encoded := base64.StdEncoding.EncodeToString([]byte("my-user2:my-password2"))
75+
registryAuth3Encoded := base64.StdEncoding.EncodeToString([]byte("my-user3:my-password3"))
76+
pullSecretDataValue = []byte(`{"auths":{"ghcr2.io":{"auth":"` + registryAuth2Encoded + `","email":"noreply@devspace.sh"},"ghcr3.io":{"auth":"` + registryAuth3Encoded + `","email":"noreply@devspace.sh"}}}`)
7477
framework.ExpectEqual(string(pullSecret.Data[k8sv1.DockerConfigJsonKey]), string(pullSecretDataValue))
7578

76-
pullSecret, err = kubeClient.RawClient().CoreV1().Secrets(ns).Get(context.TODO(), "devspace-auth-ghcr2-io", metav1.GetOptions{})
79+
// check if named secrets are created and merged
80+
pullSecret, err = kubeClient.RawClient().CoreV1().Secrets(ns).Get(context.TODO(), "merged-secret", metav1.GetOptions{})
7781
framework.ExpectNoError(err)
7882
framework.ExpectEqual(len(pullSecret.Data), 1)
79-
registryAuthEncoded = base64.StdEncoding.EncodeToString([]byte("my-user2:my-password2"))
80-
pullSecretDataValue = []byte(`{
81-
"auths": {
82-
"ghcr2.io": {
83-
"auth": "` + registryAuthEncoded + `",
84-
"email": "noreply@devspace.sh"
85-
}
86-
}
87-
}`)
83+
registryAuth4Encoded := base64.StdEncoding.EncodeToString([]byte("my-user4:my-password4"))
84+
registryAuth5Encoded := base64.StdEncoding.EncodeToString([]byte("my-user5:my-password5"))
85+
pullSecretDataValue = []byte(`{"auths":{"ghcr4.io":{"auth":"` + registryAuth4Encoded + `","email":"noreply@devspace.sh"},"ghcr5.io":{"auth":"` + registryAuth5Encoded + `","email":"noreply@devspace.sh"}}}`)
8886
framework.ExpectEqual(string(pullSecret.Data[k8sv1.DockerConfigJsonKey]), string(pullSecretDataValue))
8987

9088
serviceAccount, err := kubeClient.RawClient().CoreV1().ServiceAccounts(ns).Get(context.TODO(), "default", metav1.GetOptions{})
9189
framework.ExpectNoError(err)
92-
framework.ExpectEqual(len(serviceAccount.ImagePullSecrets), 2)
90+
framework.ExpectEqual(len(serviceAccount.ImagePullSecrets), 3)
9391
sort.Slice(serviceAccount.ImagePullSecrets, func(i, j int) bool {
9492
return serviceAccount.ImagePullSecrets[i].Name < serviceAccount.ImagePullSecrets[j].Name
9593
})
96-
framework.ExpectEqual(serviceAccount.ImagePullSecrets[0].Name, "devspace-auth-ghcr2-io")
97-
framework.ExpectEqual(serviceAccount.ImagePullSecrets[1].Name, "test-secret")
94+
framework.ExpectEqual(serviceAccount.ImagePullSecrets[0].Name, "devspace-pull-secrets")
95+
framework.ExpectEqual(serviceAccount.ImagePullSecrets[1].Name, "merged-secret")
96+
framework.ExpectEqual(serviceAccount.ImagePullSecrets[2].Name, "test-secret")
9897
})
9998
})

e2e/tests/pullsecret/testdata/simple/devspace.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,23 @@ pullSecrets:
66
username: my-user2
77
password: my-password2
88
serviceAccounts: ["default"]
9+
test3:
10+
registry: ghcr3.io
11+
username: my-user3
12+
password: my-password3
13+
serviceAccounts: ["default"]
914
test:
1015
registry: ghcr.io
1116
username: my-user
1217
password: my-password
1318
secret: test-secret
19+
test4:
20+
registry: ghcr4.io
21+
username: my-user4
22+
password: my-password4
23+
secret: merged-secret
24+
test5:
25+
registry: ghcr5.io
26+
username: my-user5
27+
password: my-password5
28+
secret: merged-secret

pkg/devspace/pullsecrets/registry.go

Lines changed: 124 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import (
44
"crypto/sha256"
55
"encoding/base64"
66
"encoding/hex"
7-
devspacecontext "github.com/loft-sh/devspace/pkg/devspace/context"
7+
"encoding/json"
88
"regexp"
9-
"strings"
109
"time"
1110

11+
devspacecontext "github.com/loft-sh/devspace/pkg/devspace/context"
12+
1213
kerrors "k8s.io/apimachinery/pkg/api/errors"
1314
"k8s.io/apimachinery/pkg/util/wait"
1415

@@ -32,15 +33,33 @@ type PullSecretOptions struct {
3233
Secret string
3334
}
3435

36+
// DockerConfigJSON represents a local docker auth config file
37+
// for pulling images.
38+
type DockerConfigJSON struct {
39+
Auths DockerConfig `json:"auths"`
40+
}
41+
42+
// DockerConfig represents the config file used by the docker CLI.
43+
// This config that represents the credentials that should be used
44+
// when pulling images from specific image repositories.
45+
type DockerConfig map[string]DockerConfigEntry
46+
47+
// DockerConfigEntry holds the user information that grant the access to docker registry
48+
type DockerConfigEntry struct {
49+
Auth string `json:"auth"`
50+
Email string `json:"email"`
51+
}
52+
3553
// CreatePullSecret creates an image pull secret for a registry
3654
func (r *client) CreatePullSecret(ctx *devspacecontext.Context, options *PullSecretOptions) error {
3755
pullSecretName := options.Secret
3856
if pullSecretName == "" {
3957
pullSecretName = GetRegistryAuthSecretName(options.RegistryURL)
4058
}
4159

42-
if options.RegistryURL == "hub.docker.com" || options.RegistryURL == "" {
43-
options.RegistryURL = "https://index.docker.io/v1/"
60+
registryURL := options.RegistryURL
61+
if registryURL == "hub.docker.com" || registryURL == "" {
62+
registryURL = "https://index.docker.io/v1/"
4463
}
4564

4665
authToken := options.PasswordOrToken
@@ -53,48 +72,64 @@ func (r *client) CreatePullSecret(ctx *devspacecontext.Context, options *PullSec
5372
email = "noreply@devspace.sh"
5473
}
5574

56-
registryAuthEncoded := base64.StdEncoding.EncodeToString([]byte(authToken))
57-
pullSecretDataValue := []byte(`{
58-
"auths": {
59-
"` + options.RegistryURL + `": {
60-
"auth": "` + registryAuthEncoded + `",
61-
"email": "` + email + `"
75+
err := wait.PollImmediate(time.Second, time.Second*30, func() (bool, error) {
76+
secret, err := ctx.KubeClient.KubeClient().CoreV1().Secrets(options.Namespace).Get(ctx.Context, pullSecretName, metav1.GetOptions{})
77+
if err != nil {
78+
if kerrors.IsNotFound(err) {
79+
// Create the pull secret
80+
secret, err := newPullSecret(pullSecretName, registryURL, authToken, email)
81+
if err != nil {
82+
return false, err
6283
}
63-
}
64-
}`)
6584

66-
pullSecretData := map[string][]byte{}
67-
pullSecretDataKey := k8sv1.DockerConfigJsonKey
68-
pullSecretData[pullSecretDataKey] = pullSecretDataValue
85+
_, err = ctx.KubeClient.KubeClient().CoreV1().Secrets(options.Namespace).Create(ctx.Context, secret, metav1.CreateOptions{})
86+
if err != nil {
87+
if kerrors.IsAlreadyExists(err) {
88+
// Retry
89+
return false, nil
90+
}
6991

70-
registryPullSecret := &k8sv1.Secret{
71-
ObjectMeta: metav1.ObjectMeta{
72-
Name: pullSecretName,
73-
},
74-
Data: pullSecretData,
75-
Type: k8sv1.SecretTypeDockerConfigJson,
76-
}
92+
return false, errors.Wrap(err, "create pull secret")
93+
}
7794

78-
err := wait.PollImmediate(time.Second, time.Second*30, func() (bool, error) {
79-
secret, err := ctx.KubeClient.KubeClient().CoreV1().Secrets(options.Namespace).Get(ctx.Context, pullSecretName, metav1.GetOptions{})
95+
ctx.Log.Donef("Created image pull secret %s/%s", options.Namespace, pullSecretName)
96+
return true, nil
97+
} else {
98+
// Retry
99+
return false, nil
100+
}
101+
}
102+
103+
dockerConfigJSON, err := fromPullSecretData(secret.Data)
80104
if err != nil {
81-
_, err = ctx.KubeClient.KubeClient().CoreV1().Secrets(options.Namespace).Create(ctx.Context, registryPullSecret, metav1.CreateOptions{})
105+
return false, err
106+
}
107+
108+
existingEntry := dockerConfigJSON.Auths[registryURL]
109+
updatedEntry := newDockerConfigEntry(authToken, email)
110+
if hasChanges(existingEntry, updatedEntry) {
111+
// Update secret entry
112+
dockerConfigJSON.Auths[registryURL] = updatedEntry
113+
114+
// Update secret data
115+
secret.Data, err = toPullSecretData(dockerConfigJSON)
82116
if err != nil {
83-
return false, errors.Errorf("Unable to create image pull secret: %s", err.Error())
117+
return false, err
84118
}
85119

86-
ctx.Log.Donef("Created image pull secret %s/%s", options.Namespace, pullSecretName)
87-
} else if secret.Data == nil || string(secret.Data[pullSecretDataKey]) != string(pullSecretData[pullSecretDataKey]) {
88-
_, err = ctx.KubeClient.KubeClient().CoreV1().Secrets(options.Namespace).Update(ctx.Context, registryPullSecret, metav1.UpdateOptions{})
120+
// Update secret
121+
_, err = ctx.KubeClient.KubeClient().CoreV1().Secrets(options.Namespace).Update(ctx.Context, secret, metav1.UpdateOptions{})
89122
if err != nil {
90123
if kerrors.IsConflict(err) {
124+
// Retry
91125
return false, nil
92126
}
93127

94-
return false, errors.Errorf("Unable to update image pull secret: %s", err.Error())
128+
return false, errors.Wrap(err, "update pull secret")
95129
}
96-
}
97130

131+
ctx.Log.Donef("Updated image pull secret %s/%s", options.Namespace, pullSecretName)
132+
}
98133
return true, nil
99134
})
100135
if err != nil {
@@ -106,11 +141,7 @@ func (r *client) CreatePullSecret(ctx *devspacecontext.Context, options *PullSec
106141

107142
// GetRegistryAuthSecretName returns the name of the image pull secret for a registry
108143
func GetRegistryAuthSecretName(registryURL string) string {
109-
if registryURL == "" {
110-
return registryAuthSecretNamePrefix + "docker"
111-
}
112-
113-
return SafeName(registryAuthSecretNamePrefix + registryNameReplaceRegex.ReplaceAllString(strings.ToLower(registryURL), "-"))
144+
return "devspace-pull-secrets"
114145
}
115146

116147
func SafeName(name string) string {
@@ -120,3 +151,60 @@ func SafeName(name string) string {
120151
}
121152
return name
122153
}
154+
155+
func newPullSecret(name, registryURL, authToken, email string) (*k8sv1.Secret, error) {
156+
dockerConfig := &DockerConfigJSON{
157+
Auths: DockerConfig{
158+
registryURL: newDockerConfigEntry(authToken, email),
159+
},
160+
}
161+
162+
pullSecretData, err := toPullSecretData(dockerConfig)
163+
if err != nil {
164+
return nil, errors.Wrap(err, "new pull secret")
165+
}
166+
167+
return &k8sv1.Secret{
168+
ObjectMeta: metav1.ObjectMeta{
169+
Name: name,
170+
},
171+
Data: pullSecretData,
172+
Type: k8sv1.SecretTypeDockerConfigJson,
173+
}, nil
174+
}
175+
176+
func newDockerConfigEntry(authToken, email string) DockerConfigEntry {
177+
return DockerConfigEntry{
178+
Auth: base64.StdEncoding.EncodeToString([]byte(authToken)),
179+
Email: email,
180+
}
181+
}
182+
183+
func hasChanges(existing, updated DockerConfigEntry) bool {
184+
return existing.Auth != updated.Auth || existing.Email != updated.Email
185+
}
186+
187+
func toPullSecretData(dockerConfig *DockerConfigJSON) (map[string][]byte, error) {
188+
data, err := json.Marshal(dockerConfig)
189+
if err != nil {
190+
return nil, errors.Wrap(err, "marshal docker config")
191+
}
192+
193+
return map[string][]byte{
194+
k8sv1.DockerConfigJsonKey: data,
195+
}, nil
196+
}
197+
198+
func fromPullSecretData(data map[string][]byte) (*DockerConfigJSON, error) {
199+
dockerConfig := &DockerConfigJSON{}
200+
if data == nil {
201+
return dockerConfig, nil
202+
}
203+
204+
err := json.Unmarshal(data[k8sv1.DockerConfigJsonKey], &dockerConfig)
205+
if err != nil {
206+
return nil, errors.Wrap(err, "unmarshal docker config")
207+
}
208+
209+
return dockerConfig, nil
210+
}

0 commit comments

Comments
 (0)