Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 23 additions & 24 deletions e2e/tests/pullsecret/pullsecrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package pullsecret
import (
"context"
"encoding/base64"
"os"
"sort"

"github.com/loft-sh/devspace/cmd"
"github.com/loft-sh/devspace/cmd/flags"
"github.com/loft-sh/devspace/e2e/framework"
Expand All @@ -11,8 +14,6 @@ import (
"github.com/onsi/ginkgo"
k8sv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"os"
"sort"
)

var _ = DevSpaceDescribe("pullsecret", func() {
Expand Down Expand Up @@ -58,42 +59,40 @@ var _ = DevSpaceDescribe("pullsecret", func() {
err = deployCmd.Run(f)
framework.ExpectNoError(err)

// check if secrets are created
// check if named secret is created
pullSecret, err := kubeClient.RawClient().CoreV1().Secrets(ns).Get(context.TODO(), "test-secret", metav1.GetOptions{})
framework.ExpectNoError(err)
framework.ExpectEqual(len(pullSecret.Data), 1)
registryAuthEncoded := base64.StdEncoding.EncodeToString([]byte("my-user:my-password"))
pullSecretDataValue := []byte(`{
"auths": {
"ghcr.io": {
"auth": "` + registryAuthEncoded + `",
"email": "noreply@devspace.sh"
}
}
}`)
pullSecretDataValue := []byte(`{"auths":{"ghcr.io":{"auth":"` + registryAuthEncoded + `","email":"noreply@devspace.sh"}}}`)
framework.ExpectEqual(string(pullSecret.Data[k8sv1.DockerConfigJsonKey]), string(pullSecretDataValue))

// check if default secrets are created and merged
pullSecret, err = kubeClient.RawClient().CoreV1().Secrets(ns).Get(context.TODO(), "devspace-pull-secrets", metav1.GetOptions{})
framework.ExpectNoError(err)
framework.ExpectEqual(len(pullSecret.Data), 1)
registryAuth2Encoded := base64.StdEncoding.EncodeToString([]byte("my-user2:my-password2"))
registryAuth3Encoded := base64.StdEncoding.EncodeToString([]byte("my-user3:my-password3"))
pullSecretDataValue = []byte(`{"auths":{"ghcr2.io":{"auth":"` + registryAuth2Encoded + `","email":"noreply@devspace.sh"},"ghcr3.io":{"auth":"` + registryAuth3Encoded + `","email":"noreply@devspace.sh"}}}`)
framework.ExpectEqual(string(pullSecret.Data[k8sv1.DockerConfigJsonKey]), string(pullSecretDataValue))

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

serviceAccount, err := kubeClient.RawClient().CoreV1().ServiceAccounts(ns).Get(context.TODO(), "default", metav1.GetOptions{})
framework.ExpectNoError(err)
framework.ExpectEqual(len(serviceAccount.ImagePullSecrets), 2)
framework.ExpectEqual(len(serviceAccount.ImagePullSecrets), 3)
sort.Slice(serviceAccount.ImagePullSecrets, func(i, j int) bool {
return serviceAccount.ImagePullSecrets[i].Name < serviceAccount.ImagePullSecrets[j].Name
})
framework.ExpectEqual(serviceAccount.ImagePullSecrets[0].Name, "devspace-auth-ghcr2-io")
framework.ExpectEqual(serviceAccount.ImagePullSecrets[1].Name, "test-secret")
framework.ExpectEqual(serviceAccount.ImagePullSecrets[0].Name, "devspace-pull-secrets")
framework.ExpectEqual(serviceAccount.ImagePullSecrets[1].Name, "merged-secret")
framework.ExpectEqual(serviceAccount.ImagePullSecrets[2].Name, "test-secret")
})
})
15 changes: 15 additions & 0 deletions e2e/tests/pullsecret/testdata/simple/devspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,23 @@ pullSecrets:
username: my-user2
password: my-password2
serviceAccounts: ["default"]
test3:
registry: ghcr3.io
username: my-user3
password: my-password3
serviceAccounts: ["default"]
test:
registry: ghcr.io
username: my-user
password: my-password
secret: test-secret
test4:
registry: ghcr4.io
username: my-user4
password: my-password4
secret: merged-secret
test5:
registry: ghcr5.io
username: my-user5
password: my-password5
secret: merged-secret
160 changes: 124 additions & 36 deletions pkg/devspace/pullsecrets/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import (
"crypto/sha256"
"encoding/base64"
"encoding/hex"
devspacecontext "github.com/loft-sh/devspace/pkg/devspace/context"
"encoding/json"
"regexp"
"strings"
"time"

devspacecontext "github.com/loft-sh/devspace/pkg/devspace/context"

kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/util/wait"

Expand All @@ -32,15 +33,33 @@ type PullSecretOptions struct {
Secret string
}

// DockerConfigJSON represents a local docker auth config file
// for pulling images.
type DockerConfigJSON struct {
Auths DockerConfig `json:"auths"`
}

// DockerConfig represents the config file used by the docker CLI.
// This config that represents the credentials that should be used
// when pulling images from specific image repositories.
type DockerConfig map[string]DockerConfigEntry

// DockerConfigEntry holds the user information that grant the access to docker registry
type DockerConfigEntry struct {
Auth string `json:"auth"`
Email string `json:"email"`
}

// CreatePullSecret creates an image pull secret for a registry
func (r *client) CreatePullSecret(ctx *devspacecontext.Context, options *PullSecretOptions) error {
pullSecretName := options.Secret
if pullSecretName == "" {
pullSecretName = GetRegistryAuthSecretName(options.RegistryURL)
}

if options.RegistryURL == "hub.docker.com" || options.RegistryURL == "" {
options.RegistryURL = "https://index.docker.io/v1/"
registryURL := options.RegistryURL
if registryURL == "hub.docker.com" || registryURL == "" {
registryURL = "https://index.docker.io/v1/"
}

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

registryAuthEncoded := base64.StdEncoding.EncodeToString([]byte(authToken))
pullSecretDataValue := []byte(`{
"auths": {
"` + options.RegistryURL + `": {
"auth": "` + registryAuthEncoded + `",
"email": "` + email + `"
err := wait.PollImmediate(time.Second, time.Second*30, func() (bool, error) {
secret, err := ctx.KubeClient.KubeClient().CoreV1().Secrets(options.Namespace).Get(ctx.Context, pullSecretName, metav1.GetOptions{})
if err != nil {
if kerrors.IsNotFound(err) {
// Create the pull secret
secret, err := newPullSecret(pullSecretName, registryURL, authToken, email)
if err != nil {
return false, err
}
}
}`)

pullSecretData := map[string][]byte{}
pullSecretDataKey := k8sv1.DockerConfigJsonKey
pullSecretData[pullSecretDataKey] = pullSecretDataValue
_, err = ctx.KubeClient.KubeClient().CoreV1().Secrets(options.Namespace).Create(ctx.Context, secret, metav1.CreateOptions{})
if err != nil {
if kerrors.IsAlreadyExists(err) {
// Retry
return false, nil
}

registryPullSecret := &k8sv1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: pullSecretName,
},
Data: pullSecretData,
Type: k8sv1.SecretTypeDockerConfigJson,
}
return false, errors.Wrap(err, "create pull secret")
}

err := wait.PollImmediate(time.Second, time.Second*30, func() (bool, error) {
secret, err := ctx.KubeClient.KubeClient().CoreV1().Secrets(options.Namespace).Get(ctx.Context, pullSecretName, metav1.GetOptions{})
ctx.Log.Donef("Created image pull secret %s/%s", options.Namespace, pullSecretName)
return true, nil
} else {
// Retry
return false, nil
}
}

dockerConfigJSON, err := fromPullSecretData(secret.Data)
if err != nil {
_, err = ctx.KubeClient.KubeClient().CoreV1().Secrets(options.Namespace).Create(ctx.Context, registryPullSecret, metav1.CreateOptions{})
return false, err
}

existingEntry := dockerConfigJSON.Auths[registryURL]
updatedEntry := newDockerConfigEntry(authToken, email)
if hasChanges(existingEntry, updatedEntry) {
// Update secret entry
dockerConfigJSON.Auths[registryURL] = updatedEntry

// Update secret data
secret.Data, err = toPullSecretData(dockerConfigJSON)
if err != nil {
return false, errors.Errorf("Unable to create image pull secret: %s", err.Error())
return false, err
}

ctx.Log.Donef("Created image pull secret %s/%s", options.Namespace, pullSecretName)
} else if secret.Data == nil || string(secret.Data[pullSecretDataKey]) != string(pullSecretData[pullSecretDataKey]) {
_, err = ctx.KubeClient.KubeClient().CoreV1().Secrets(options.Namespace).Update(ctx.Context, registryPullSecret, metav1.UpdateOptions{})
// Update secret
_, err = ctx.KubeClient.KubeClient().CoreV1().Secrets(options.Namespace).Update(ctx.Context, secret, metav1.UpdateOptions{})
if err != nil {
if kerrors.IsConflict(err) {
// Retry
return false, nil
}

return false, errors.Errorf("Unable to update image pull secret: %s", err.Error())
return false, errors.Wrap(err, "update pull secret")
}
}

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

// GetRegistryAuthSecretName returns the name of the image pull secret for a registry
func GetRegistryAuthSecretName(registryURL string) string {
if registryURL == "" {
return registryAuthSecretNamePrefix + "docker"
}

return SafeName(registryAuthSecretNamePrefix + registryNameReplaceRegex.ReplaceAllString(strings.ToLower(registryURL), "-"))
return "devspace-pull-secrets"
}

func SafeName(name string) string {
Expand All @@ -120,3 +151,60 @@ func SafeName(name string) string {
}
return name
}

func newPullSecret(name, registryURL, authToken, email string) (*k8sv1.Secret, error) {
dockerConfig := &DockerConfigJSON{
Auths: DockerConfig{
registryURL: newDockerConfigEntry(authToken, email),
},
}

pullSecretData, err := toPullSecretData(dockerConfig)
if err != nil {
return nil, errors.Wrap(err, "new pull secret")
}

return &k8sv1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Data: pullSecretData,
Type: k8sv1.SecretTypeDockerConfigJson,
}, nil
}

func newDockerConfigEntry(authToken, email string) DockerConfigEntry {
return DockerConfigEntry{
Auth: base64.StdEncoding.EncodeToString([]byte(authToken)),
Email: email,
}
}

func hasChanges(existing, updated DockerConfigEntry) bool {
return existing.Auth != updated.Auth || existing.Email != updated.Email
}

func toPullSecretData(dockerConfig *DockerConfigJSON) (map[string][]byte, error) {
data, err := json.Marshal(dockerConfig)
if err != nil {
return nil, errors.Wrap(err, "marshal docker config")
}

return map[string][]byte{
k8sv1.DockerConfigJsonKey: data,
}, nil
}

func fromPullSecretData(data map[string][]byte) (*DockerConfigJSON, error) {
dockerConfig := &DockerConfigJSON{}
if data == nil {
return dockerConfig, nil
}

err := json.Unmarshal(data[k8sv1.DockerConfigJsonKey], &dockerConfig)
if err != nil {
return nil, errors.Wrap(err, "unmarshal docker config")
}

return dockerConfig, nil
}