diff options
| author | jannfis <jann@mistrust.net> | 2020-08-13 21:48:08 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-08-13 21:48:08 +0200 |
| commit | 4602a02174f081e6fd0d34df4b0cf6a62c546c03 (patch) | |
| tree | b46d2bb2adead8e830db40f41ebf5db683cc0dde | |
| parent | 54c060385cf26d51e9a1f0188fb2b298f21e1e9a (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.txt | 2 | ||||
| -rw-r--r-- | pkg/argocd/update.go | 15 | ||||
| -rw-r--r-- | pkg/argocd/update_test.go | 71 | ||||
| -rw-r--r-- | pkg/image/options.go | 15 | ||||
| -rw-r--r-- | pkg/image/options_test.go | 24 | ||||
| -rw-r--r-- | pkg/registry/client.go | 18 | ||||
| -rw-r--r-- | pkg/registry/registry.go | 14 |
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 ®Mock, 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 ®Mock, 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 ®Mock, 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 ®Mock, 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 ®Mock, 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 ®Mock, 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 ®Mock, 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 |
