summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjannfis <jann@mistrust.net>2020-08-13 21:48:08 +0200
committerGitHub <noreply@github.com>2020-08-13 21:48:08 +0200
commit4602a02174f081e6fd0d34df4b0cf6a62c546c03 (patch)
treeb46d2bb2adead8e830db40f41ebf5db683cc0dde
parent54c060385cf26d51e9a1f0188fb2b298f21e1e9a (diff)
feat: Enable unique pull secrets per image (#52)
* feat: Enable unique pull secrets per image * allow more spelling
-rw-r--r--.github/actions/spelling/expect.txt2
-rw-r--r--pkg/argocd/update.go15
-rw-r--r--pkg/argocd/update_test.go71
-rw-r--r--pkg/image/options.go15
-rw-r--r--pkg/image/options_test.go24
-rw-r--r--pkg/registry/client.go18
-rw-r--r--pkg/registry/registry.go14
7 files changed, 139 insertions, 20 deletions
diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt
index 1784f61..edee088 100644
--- a/.github/actions/spelling/expect.txt
+++ b/.github/actions/spelling/expect.txt
@@ -1 +1,3 @@
wohoo
+myuser
+mypass
diff --git a/pkg/argocd/update.go b/pkg/argocd/update.go
index bca68a0..75ca4f3 100644
--- a/pkg/argocd/update.go
+++ b/pkg/argocd/update.go
@@ -70,13 +70,26 @@ func UpdateApplication(newRegFn registry.NewRegistryClient, argoClient ArgoCD, k
vc.SortMode = updateableImage.GetParameterUpdateStrategy(curApplication.Application.Annotations)
vc.MatchFunc, vc.MatchArgs = updateableImage.GetParameterMatch(curApplication.Application.Annotations)
+ // The endpoint can provide default credentials for pulling images
err = rep.SetEndpointCredentials(kubeClient)
if err != nil {
imgCtx.Errorf("Could not set registry endpoint credentials: %v", err)
+ result.NumErrors += 1
continue
}
- regClient, err := newRegFn(rep)
+ imgCredSrc := updateableImage.GetParameterPullSecret(curApplication.Application.Annotations)
+ var creds *image.Credential = &image.Credential{}
+ if imgCredSrc != nil {
+ creds, err = imgCredSrc.FetchCredentials(rep.RegistryAPI, kubeClient)
+ if err != nil {
+ imgCtx.Warnf("Could not fetch credentials: %v", err)
+ result.NumErrors += 1
+ continue
+ }
+ }
+
+ regClient, err := newRegFn(rep, creds.Username, creds.Password)
if err != nil {
imgCtx.Errorf("Could not create registry client: %v", err)
result.NumErrors += 1
diff --git a/pkg/argocd/update_test.go b/pkg/argocd/update_test.go
index 9f2e74f..6bad9d5 100644
--- a/pkg/argocd/update_test.go
+++ b/pkg/argocd/update_test.go
@@ -2,14 +2,17 @@ package argocd
import (
"errors"
+ "fmt"
"testing"
argomock "github.com/argoproj-labs/argocd-image-updater/pkg/argocd/mocks"
"github.com/argoproj-labs/argocd-image-updater/pkg/client"
+ "github.com/argoproj-labs/argocd-image-updater/pkg/common"
"github.com/argoproj-labs/argocd-image-updater/pkg/image"
"github.com/argoproj-labs/argocd-image-updater/pkg/registry"
regmock "github.com/argoproj-labs/argocd-image-updater/pkg/registry/mocks"
"github.com/argoproj-labs/argocd-image-updater/test/fake"
+ "github.com/argoproj-labs/argocd-image-updater/test/fixture"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/stretchr/testify/assert"
@@ -19,7 +22,7 @@ import (
func Test_UpdateApplication(t *testing.T) {
t.Run("Test successful update", func(t *testing.T) {
- mockClientFn := func(endpoint *registry.RegistryEndpoint) (registry.RegistryClient, error) {
+ mockClientFn := func(endpoint *registry.RegistryEndpoint, username, password string) (registry.RegistryClient, error) {
regMock := regmock.RegistryClient{}
regMock.On("Tags", mock.Anything).Return([]string{"1.0.1"}, nil)
return &regMock, nil
@@ -67,8 +70,62 @@ func Test_UpdateApplication(t *testing.T) {
assert.Equal(t, 1, res.NumImagesUpdated)
})
+ t.Run("Test successful update with credentials", func(t *testing.T) {
+ mockClientFn := func(endpoint *registry.RegistryEndpoint, username, password string) (registry.RegistryClient, error) {
+ regMock := regmock.RegistryClient{}
+ assert.Equal(t, "myuser", username)
+ assert.Equal(t, "mypass", password)
+ regMock.On("Tags", mock.Anything).Return([]string{"1.0.1"}, nil)
+ return &regMock, nil
+ }
+
+ argoClient := argomock.ArgoCD{}
+ argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil)
+
+ kubeClient := client.KubernetesClient{
+ Clientset: fake.NewFakeClientsetWithResources(fixture.NewSecret("foo", "bar", map[string][]byte{"creds": []byte("myuser:mypass")})),
+ }
+ appImages := &ApplicationImages{
+ Application: v1alpha1.Application{
+ ObjectMeta: v1.ObjectMeta{
+ Name: "guestbook",
+ Namespace: "guestbook",
+ Annotations: map[string]string{
+ fmt.Sprintf(common.SecretListAnnotation, "dummy"): "secret:foo/bar#creds",
+ },
+ },
+ Spec: v1alpha1.ApplicationSpec{
+ Source: v1alpha1.ApplicationSource{
+ Kustomize: &v1alpha1.ApplicationSourceKustomize{
+ Images: v1alpha1.KustomizeImages{
+ "jannfis/foobar:1.0.0",
+ },
+ },
+ },
+ },
+ Status: v1alpha1.ApplicationStatus{
+ SourceType: v1alpha1.ApplicationSourceTypeKustomize,
+ Summary: v1alpha1.ApplicationSummary{
+ Images: []string{
+ "jannfis/foobar:1.0.0",
+ },
+ },
+ },
+ },
+ Images: image.ContainerImageList{
+ image.NewFromIdentifier("dummy=jannfis/foobar:1.0.1"),
+ },
+ }
+ res := UpdateApplication(mockClientFn, &argoClient, &kubeClient, appImages, false)
+ assert.Equal(t, 0, res.NumErrors)
+ assert.Equal(t, 0, res.NumSkipped)
+ assert.Equal(t, 1, res.NumApplicationsProcessed)
+ assert.Equal(t, 1, res.NumImagesConsidered)
+ assert.Equal(t, 1, res.NumImagesUpdated)
+ })
+
t.Run("Test skip because of image not in list", func(t *testing.T) {
- mockClientFn := func(endpoint *registry.RegistryEndpoint) (registry.RegistryClient, error) {
+ mockClientFn := func(endpoint *registry.RegistryEndpoint, username, password string) (registry.RegistryClient, error) {
regMock := regmock.RegistryClient{}
regMock.On("Tags", mock.Anything).Return([]string{"1.0.1"}, nil)
return &regMock, nil
@@ -117,7 +174,7 @@ func Test_UpdateApplication(t *testing.T) {
})
t.Run("Test skip because of image up-to-date", func(t *testing.T) {
- mockClientFn := func(endpoint *registry.RegistryEndpoint) (registry.RegistryClient, error) {
+ mockClientFn := func(endpoint *registry.RegistryEndpoint, username, password string) (registry.RegistryClient, error) {
regMock := regmock.RegistryClient{}
regMock.On("Tags", mock.Anything).Return([]string{"1.0.1"}, nil)
return &regMock, nil
@@ -166,7 +223,7 @@ func Test_UpdateApplication(t *testing.T) {
})
t.Run("Error - unknown registry", func(t *testing.T) {
- mockClientFn := func(endpoint *registry.RegistryEndpoint) (registry.RegistryClient, error) {
+ mockClientFn := func(endpoint *registry.RegistryEndpoint, username, password string) (registry.RegistryClient, error) {
regMock := regmock.RegistryClient{}
regMock.On("Tags", mock.Anything).Return([]string{"1.0.1"}, nil)
return &regMock, nil
@@ -215,7 +272,7 @@ func Test_UpdateApplication(t *testing.T) {
})
t.Run("Test error on generic registry client failure", func(t *testing.T) {
- mockClientFn := func(endpoint *registry.RegistryEndpoint) (registry.RegistryClient, error) {
+ mockClientFn := func(endpoint *registry.RegistryEndpoint, username, password string) (registry.RegistryClient, error) {
return nil, errors.New("some error")
}
@@ -262,7 +319,7 @@ func Test_UpdateApplication(t *testing.T) {
})
t.Run("Test error on failure to list tags", func(t *testing.T) {
- mockClientFn := func(endpoint *registry.RegistryEndpoint) (registry.RegistryClient, error) {
+ mockClientFn := func(endpoint *registry.RegistryEndpoint, username, password string) (registry.RegistryClient, error) {
regMock := regmock.RegistryClient{}
regMock.On("Tags", mock.Anything).Return(nil, errors.New("some error"))
return &regMock, nil
@@ -311,7 +368,7 @@ func Test_UpdateApplication(t *testing.T) {
})
t.Run("Test error on improper semver in tag", func(t *testing.T) {
- mockClientFn := func(endpoint *registry.RegistryEndpoint) (registry.RegistryClient, error) {
+ mockClientFn := func(endpoint *registry.RegistryEndpoint, username, password string) (registry.RegistryClient, error) {
regMock := regmock.RegistryClient{}
regMock.On("Tags", mock.Anything).Return([]string{"1.0.0", "1.0.1"}, nil)
return &regMock, nil
diff --git a/pkg/image/options.go b/pkg/image/options.go
index 4acf603..ea0515e 100644
--- a/pkg/image/options.go
+++ b/pkg/image/options.go
@@ -114,6 +114,21 @@ func (img *ContainerImage) GetParameterMatch(annotations map[string]string) (Mat
}
}
+func (img *ContainerImage) GetParameterPullSecret(annotations map[string]string) *CredentialSource {
+ key := fmt.Sprintf(common.SecretListAnnotation, img.normalizedSymbolicName())
+ val, ok := annotations[key]
+ if !ok {
+ log.Tracef("No secret annotation %s found", key)
+ return nil
+ }
+ credSrc, err := ParseCredentialSource(val, false)
+ if err != nil {
+ log.Warnf("Invalid credential reference specified: %s", val)
+ return nil
+ }
+ return credSrc
+}
+
func (img *ContainerImage) normalizedSymbolicName() string {
return strings.ReplaceAll(img.ImageAlias, "/", "_")
}
diff --git a/pkg/image/options_test.go b/pkg/image/options_test.go
index 6510580..493c59c 100644
--- a/pkg/image/options_test.go
+++ b/pkg/image/options_test.go
@@ -154,3 +154,27 @@ func Test_GetMatchOption(t *testing.T) {
})
}
+
+func Test_GetSecretOption(t *testing.T) {
+ t.Run("Get cred source from annotation", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.SecretListAnnotation, "dummy"): "pullsecret:foo/bar",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ credSrc := img.GetParameterPullSecret(annotations)
+ require.NotNil(t, credSrc)
+ assert.Equal(t, CredentialSourcePullSecret, credSrc.Type)
+ assert.Equal(t, "foo", credSrc.SecretNamespace)
+ assert.Equal(t, "bar", credSrc.SecretName)
+ assert.Equal(t, ".dockerconfigjson", credSrc.SecretField)
+ })
+
+ t.Run("Invalid reference in annotation", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.SecretListAnnotation, "dummy"): "foo/bar",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ credSrc := img.GetParameterPullSecret(annotations)
+ require.Nil(t, credSrc)
+ })
+}
diff --git a/pkg/registry/client.go b/pkg/registry/client.go
index e349bda..d2b6113 100644
--- a/pkg/registry/client.go
+++ b/pkg/registry/client.go
@@ -13,7 +13,7 @@ type RegistryClient interface {
ManifestV1(repository string, reference string) (*schema1.SignedManifest, error)
}
-type NewRegistryClient func(*RegistryEndpoint) (RegistryClient, error)
+type NewRegistryClient func(*RegistryEndpoint, string, string) (RegistryClient, error)
// Helper type for registry clients
type registryClient struct {
@@ -21,17 +21,25 @@ type registryClient struct {
}
// NewMockClient returns a new mocked RegistryClient
-func NewMockClient(endpoint *RegistryEndpoint) (RegistryClient, error) {
+func NewMockClient(endpoint *RegistryEndpoint, username, password string) (RegistryClient, error) {
return &mocks.RegistryClient{}, nil
}
// NewClient returns a new RegistryClient for the given endpoint information
-func NewClient(endpoint *RegistryEndpoint) (RegistryClient, error) {
+func NewClient(endpoint *RegistryEndpoint, username, password string) (RegistryClient, error) {
+
+ if username == "" && endpoint.Username != "" {
+ username = endpoint.Username
+ }
+ if password == "" && endpoint.Password != "" {
+ password = endpoint.Password
+ }
+
client, err := registry.NewCustom(endpoint.RegistryAPI, registry.Options{
DoInitialPing: endpoint.Ping,
Logf: registry.Quiet,
- Username: endpoint.Username,
- Password: endpoint.Password,
+ Username: username,
+ Password: password,
})
if err != nil {
return nil, err
diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go
index 5a9e75f..5231e15 100644
--- a/pkg/registry/registry.go
+++ b/pkg/registry/registry.go
@@ -123,9 +123,9 @@ func (endpoint *RegistryEndpoint) GetTags(img *image.ContainerImage, regClient R
}
// Sets endpoint credentials for this registry from a reference to a K8s secret
-func (clientInfo *RegistryEndpoint) SetEndpointCredentials(kubeClient *client.KubernetesClient) error {
- if clientInfo.Username == "" && clientInfo.Password == "" && clientInfo.Credentials != "" {
- credSrc, err := image.ParseCredentialSource(clientInfo.Credentials, false)
+func (ep *RegistryEndpoint) SetEndpointCredentials(kubeClient *client.KubernetesClient) error {
+ if ep.Username == "" && ep.Password == "" && ep.Credentials != "" {
+ credSrc, err := image.ParseCredentialSource(ep.Credentials, false)
if err != nil {
return err
}
@@ -133,18 +133,18 @@ func (clientInfo *RegistryEndpoint) SetEndpointCredentials(kubeClient *client.Ku
// For fetching credentials, we must have working Kubernetes client.
if (credSrc.Type == image.CredentialSourcePullSecret || credSrc.Type == image.CredentialSourceSecret) && kubeClient == nil {
log.WithContext().
- AddField("registry", clientInfo.RegistryAPI).
+ AddField("registry", ep.RegistryAPI).
Warnf("cannot user K8s credentials without Kubernetes client")
return fmt.Errorf("could not fetch image tags")
}
- creds, err := credSrc.FetchCredentials(clientInfo.RegistryAPI, kubeClient)
+ creds, err := credSrc.FetchCredentials(ep.RegistryAPI, kubeClient)
if err != nil {
return err
}
- clientInfo.Username = creds.Username
- clientInfo.Password = creds.Password
+ ep.Username = creds.Username
+ ep.Password = creds.Password
}
return nil