diff --git a/cmd/main.go b/cmd/main.go index 81eed761..8f5c0c6b 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -35,7 +35,7 @@ type ImageUpdaterConfig struct { CheckInterval time.Duration ArgoClient argocd.ArgoCD LogLevel string - KubeClient *kube.KubernetesClient + KubeClient *kube.ImageUpdaterKubernetesClient MaxConcurrency int HealthPort int MetricsPort int diff --git a/cmd/run.go b/cmd/run.go index a537a14e..cd7264b0 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -14,10 +14,10 @@ import ( "github.com/argoproj-labs/argocd-image-updater/pkg/common" "github.com/argoproj-labs/argocd-image-updater/pkg/health" "github.com/argoproj-labs/argocd-image-updater/pkg/metrics" - "github.com/argoproj-labs/argocd-image-updater/pkg/registry" "github.com/argoproj-labs/argocd-image-updater/pkg/version" "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/env" "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/log" + "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/registry" "github.com/argoproj/argo-cd/v2/reposerver/askpass" @@ -115,7 +115,7 @@ func newRunCommand() *cobra.Command { log.Fatalf("could not create K8s client: %v", err) } if cfg.ClientOpts.ServerAddr == "" { - cfg.ClientOpts.ServerAddr = fmt.Sprintf("argocd-server.%s", cfg.KubeClient.Namespace) + cfg.ClientOpts.ServerAddr = fmt.Sprintf("argocd-server.%s", cfg.KubeClient.KubeClient.Namespace) } } if cfg.ClientOpts.ServerAddr == "" { diff --git a/cmd/template.go b/cmd/template.go index 04b7ee6d..8d8d8d3d 100644 --- a/cmd/template.go +++ b/cmd/template.go @@ -8,7 +8,7 @@ import ( "github.com/argoproj-labs/argocd-image-updater/pkg/argocd" "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/registry-scanner/pkg/image" "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/tag" "github.com/spf13/cobra" diff --git a/cmd/test.go b/cmd/test.go index fbef681e..005b8bd8 100644 --- a/cmd/test.go +++ b/cmd/test.go @@ -5,11 +5,11 @@ import ( "fmt" "runtime" - "github.com/argoproj-labs/argocd-image-updater/pkg/image" "github.com/argoproj-labs/argocd-image-updater/pkg/kube" - "github.com/argoproj-labs/argocd-image-updater/pkg/registry" + "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/image" "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/log" "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/options" + "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/registry" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -64,7 +64,7 @@ argocd-image-updater test nginx --allow-tags '^1.19.\d+(\-.*)*$' --update-strate log.Fatalf("could not set log level to %s: %v", logLevel, err) } - var kubeClient *kube.KubernetesClient + var kubeClient *kube.ImageUpdaterKubernetesClient var err error if !disableKubernetes { ctx := context.Background() @@ -118,7 +118,7 @@ argocd-image-updater test nginx --allow-tags '^1.19.\d+(\-.*)*$' --update-strate logCtx.Fatalf("could not get registry endpoint: %v", err) } - if err := ep.SetEndpointCredentials(kubeClient); err != nil { + if err := ep.SetEndpointCredentials(kubeClient.KubeClient); err != nil { logCtx.Fatalf("could not set registry credentials: %v", err) } @@ -138,7 +138,7 @@ argocd-image-updater test nginx --allow-tags '^1.19.\d+(\-.*)*$' --update-strate if err != nil { logCtx.Fatalf("could not parse credential definition '%s': %v", credentials, err) } - creds, err = credSrc.FetchCredentials(ep.RegistryAPI, kubeClient) + creds, err = credSrc.FetchCredentials(ep.RegistryAPI, kubeClient.KubeClient) if err != nil { logCtx.Fatalf("could not fetch credentials: %v", err) } diff --git a/cmd/util.go b/cmd/util.go index 4284c810..628a960e 100644 --- a/cmd/util.go +++ b/cmd/util.go @@ -7,6 +7,7 @@ import ( "time" "github.com/argoproj-labs/argocd-image-updater/pkg/kube" + registryKube "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/kube" "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/log" ) @@ -26,9 +27,9 @@ func getPrintableHealthPort(port int) string { } } -func getKubeConfig(ctx context.Context, namespace string, kubeConfig string) (*kube.KubernetesClient, error) { +func getKubeConfig(ctx context.Context, namespace string, kubeConfig string) (*kube.ImageUpdaterKubernetesClient, error) { var fullKubeConfigPath string - var kubeClient *kube.KubernetesClient + var kubeClient *kube.ImageUpdaterKubernetesClient var err error if kubeConfig != "" { @@ -44,10 +45,13 @@ func getKubeConfig(ctx context.Context, namespace string, kubeConfig string) (*k log.Debugf("Creating in-cluster Kubernetes client") } - kubeClient, err = kube.NewKubernetesClientFromConfig(ctx, namespace, fullKubeConfigPath) + kubernetesClient, err := registryKube.NewKubernetesClientFromConfig(ctx, namespace, fullKubeConfigPath) if err != nil { return nil, err } + kubeClient = &kube.ImageUpdaterKubernetesClient{ + KubeClient: kubernetesClient, + } return kubeClient, nil } diff --git a/cmd/util_test.go b/cmd/util_test.go index 68753076..5b3a428b 100644 --- a/cmd/util_test.go +++ b/cmd/util_test.go @@ -47,7 +47,7 @@ func TestGetKubeConfig(t *testing.T) { } else { require.NoError(t, err) assert.NotNil(t, client) - assert.Equal(t, tt.expectedNS, client.Namespace) + assert.Equal(t, tt.expectedNS, client.KubeClient.Namespace) } }) } diff --git a/go.mod b/go.mod index 25a7c794..f0e84af4 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,7 @@ go 1.22.0 toolchain go1.23.0 require ( - github.com/Masterminds/semver/v3 v3.3.1 - github.com/argoproj-labs/argocd-image-updater/registry-scanner v0.0.0-20241218013735-14e60b8a83a4 + github.com/argoproj-labs/argocd-image-updater/registry-scanner v0.0.0-20250106213822-5ae9a451b57d github.com/argoproj/argo-cd/v2 v2.13.1 github.com/argoproj/gitops-engine v0.7.1-0.20240905010810-bd7681ae3f8b github.com/argoproj/pkg v0.13.7-0.20230627120311-a4dd357b057e @@ -15,8 +14,6 @@ require ( github.com/distribution/distribution/v3 v3.0.0-20230722181636-7b502560cad4 github.com/go-git/go-git/v5 v5.13.0 github.com/google/uuid v1.6.0 - github.com/opencontainers/go-digest v1.0.0 - github.com/opencontainers/image-spec v1.1.0 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/prometheus/client_golang v1.20.5 github.com/sirupsen/logrus v1.9.3 @@ -42,6 +39,7 @@ require ( dario.cat/mergo v1.0.1 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect + github.com/Masterminds/semver/v3 v3.3.1 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/ProtonMail/go-crypto v1.1.3 // indirect github.com/benbjohnson/clock v1.3.0 // indirect @@ -115,6 +113,8 @@ require ( github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pkg/errors v0.9.1 // indirect diff --git a/go.sum b/go.sum index e88b11b1..a9fa761e 100644 --- a/go.sum +++ b/go.sum @@ -27,8 +27,8 @@ github.com/alicebob/miniredis/v2 v2.33.0/go.mod h1:MhP4a3EU7aENRi9aO+tHfTBZicLqQ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/argoproj-labs/argocd-image-updater/registry-scanner v0.0.0-20241218013735-14e60b8a83a4 h1:B6z7t09pdODcT4dJ2Rndata6+OFZOWjYlOXynyexFNc= -github.com/argoproj-labs/argocd-image-updater/registry-scanner v0.0.0-20241218013735-14e60b8a83a4/go.mod h1:GJD+hNHt9KbYFo1+OddD3vvqys7ct5ZIOx5yHFCBAXg= +github.com/argoproj-labs/argocd-image-updater/registry-scanner v0.0.0-20250106213822-5ae9a451b57d h1:Ut4/EiaiTDeuchus+bgq3pYu5nIgqMwpBI3+2Znw/VA= +github.com/argoproj-labs/argocd-image-updater/registry-scanner v0.0.0-20250106213822-5ae9a451b57d/go.mod h1:GJD+hNHt9KbYFo1+OddD3vvqys7ct5ZIOx5yHFCBAXg= github.com/argoproj/argo-cd/v2 v2.13.1 h1:qoa8LD5suPCAYtoDNHp+BuqODf46hl5gCuXNj2oiAy0= github.com/argoproj/argo-cd/v2 v2.13.1/go.mod h1:RC23V2744nhZstZVpLCWTQLT2gR0+IXGC3GTBCI6M+I= github.com/argoproj/gitops-engine v0.7.1-0.20240905010810-bd7681ae3f8b h1:wOPWJ5MBScQO767WpU55oUJDXObfvPL0EfAYWxogbSw= diff --git a/pkg/argocd/argocd.go b/pkg/argocd/argocd.go index 6ba85d50..fdcc69c3 100644 --- a/pkg/argocd/argocd.go +++ b/pkg/argocd/argocd.go @@ -9,10 +9,10 @@ import ( "time" "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/kube" "github.com/argoproj-labs/argocd-image-updater/pkg/metrics" "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/env" + "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/image" "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/log" argocdclient "github.com/argoproj/argo-cd/v2/pkg/apiclient" @@ -24,7 +24,7 @@ import ( // Kubernetes based client type k8sClient struct { - kubeClient *kube.KubernetesClient + kubeClient *kube.ImageUpdaterKubernetesClient } // GetApplication retrieves an application by name across all namespaces. @@ -99,7 +99,7 @@ func (client *k8sClient) UpdateSpec(ctx context.Context, spec *application.Appli } // NewK8SClient creates a new kubernetes client to interact with kubernetes api-server. -func NewK8SClient(kubeClient *kube.KubernetesClient) (ArgoCD, error) { +func NewK8SClient(kubeClient *kube.ImageUpdaterKubernetesClient) (ArgoCD, error) { return &k8sClient{kubeClient: kubeClient}, nil } diff --git a/pkg/argocd/argocd_test.go b/pkg/argocd/argocd_test.go index 28124f0c..00660fb2 100644 --- a/pkg/argocd/argocd_test.go +++ b/pkg/argocd/argocd_test.go @@ -7,8 +7,9 @@ import ( "testing" "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/kube" + "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/image" + registryKube "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/kube" "github.com/argoproj/argo-cd/v2/pkg/apiclient/application" "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" @@ -1015,8 +1016,10 @@ func TestKubernetesClient(t *testing.T) { ObjectMeta: v1.ObjectMeta{Name: "test-app2", Namespace: "testns2"}, } - client, err := NewK8SClient(&kube.KubernetesClient{ - Namespace: "testns1", + client, err := NewK8SClient(&kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Namespace: "testns1", + }, ApplicationsClientset: fake.NewSimpleClientset(app1, app2), }) @@ -1057,7 +1060,7 @@ func TestKubernetesClient(t *testing.T) { }) // Create the Kubernetes client - client, err := NewK8SClient(&kube.KubernetesClient{ + client, err := NewK8SClient(&kube.ImageUpdaterKubernetesClient{ ApplicationsClientset: clientset, }) require.NoError(t, err) @@ -1087,7 +1090,7 @@ func TestKubernetesClient(t *testing.T) { ) // Create the Kubernetes client - client, err := NewK8SClient(&kube.KubernetesClient{ + client, err := NewK8SClient(&kube.ImageUpdaterKubernetesClient{ ApplicationsClientset: clientset, }) require.NoError(t, err) @@ -1117,7 +1120,7 @@ func TestKubernetesClientUpdateSpec(t *testing.T) { } }) - client, err := NewK8SClient(&kube.KubernetesClient{ + client, err := NewK8SClient(&kube.ImageUpdaterKubernetesClient{ ApplicationsClientset: clientset, }) require.NoError(t, err) @@ -1138,7 +1141,7 @@ func TestKubernetesClientUpdateSpec(t *testing.T) { // Create a fake empty clientset clientset := fake.NewSimpleClientset() - client, err := NewK8SClient(&kube.KubernetesClient{ + client, err := NewK8SClient(&kube.ImageUpdaterKubernetesClient{ ApplicationsClientset: clientset, }) require.NoError(t, err) @@ -1162,7 +1165,7 @@ func TestKubernetesClientUpdateSpec(t *testing.T) { Spec: v1alpha1.ApplicationSpec{}, }) - client, err := NewK8SClient(&kube.KubernetesClient{ + client, err := NewK8SClient(&kube.ImageUpdaterKubernetesClient{ ApplicationsClientset: clientset, }) require.NoError(t, err) @@ -1191,7 +1194,7 @@ func TestKubernetesClientUpdateSpec(t *testing.T) { Spec: v1alpha1.ApplicationSpec{}, }) - client, err := NewK8SClient(&kube.KubernetesClient{ + client, err := NewK8SClient(&kube.ImageUpdaterKubernetesClient{ ApplicationsClientset: clientset, }) require.NoError(t, err) diff --git a/pkg/argocd/git.go b/pkg/argocd/git.go index 25b25993..8ec22744 100644 --- a/pkg/argocd/git.go +++ b/pkg/argocd/git.go @@ -15,7 +15,7 @@ import ( "sigs.k8s.io/kustomize/kyaml/order" kyaml "sigs.k8s.io/kustomize/kyaml/yaml" - "github.com/argoproj-labs/argocd-image-updater/pkg/image" + "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/image" "github.com/argoproj-labs/argocd-image-updater/ext/git" "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/log" diff --git a/pkg/argocd/git_test.go b/pkg/argocd/git_test.go index 4600c0fa..cfd79148 100644 --- a/pkg/argocd/git_test.go +++ b/pkg/argocd/git_test.go @@ -7,7 +7,7 @@ import ( "time" "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/registry-scanner/pkg/image" "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/tag" "sigs.k8s.io/kustomize/api/types" diff --git a/pkg/argocd/gitcreds.go b/pkg/argocd/gitcreds.go index cfd57060..1aa38970 100644 --- a/pkg/argocd/gitcreds.go +++ b/pkg/argocd/gitcreds.go @@ -18,7 +18,7 @@ import ( ) // getGitCredsSource returns git credentials source that loads credentials from the secret or from Argo CD settings -func getGitCredsSource(creds string, kubeClient *kube.KubernetesClient, wbc *WriteBackConfig) (GitCredsSource, error) { +func getGitCredsSource(creds string, kubeClient *kube.ImageUpdaterKubernetesClient, wbc *WriteBackConfig) (GitCredsSource, error) { switch { case creds == "repocreds": return func(app *v1alpha1.Application) (git.Creds, error) { @@ -33,12 +33,12 @@ func getGitCredsSource(creds string, kubeClient *kube.KubernetesClient, wbc *Wri } // getCredsFromArgoCD loads repository credentials from Argo CD settings -func getCredsFromArgoCD(wbc *WriteBackConfig, kubeClient *kube.KubernetesClient, project string) (git.Creds, error) { +func getCredsFromArgoCD(wbc *WriteBackConfig, kubeClient *kube.ImageUpdaterKubernetesClient, project string) (git.Creds, error) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - settingsMgr := settings.NewSettingsManager(ctx, kubeClient.Clientset, kubeClient.Namespace) - argocdDB := db.NewDB(kubeClient.Namespace, settingsMgr, kubeClient.Clientset) + settingsMgr := settings.NewSettingsManager(ctx, kubeClient.KubeClient.Clientset, kubeClient.KubeClient.Namespace) + argocdDB := db.NewDB(kubeClient.KubeClient.Namespace, settingsMgr, kubeClient.KubeClient.Clientset) repo, err := argocdDB.GetRepository(ctx, wbc.GitRepo, project) if err != nil { return nil, err @@ -112,12 +112,12 @@ func getCAPath(repoURL string) string { } // getCredsFromSecret loads repository credentials from secret -func getCredsFromSecret(wbc *WriteBackConfig, credentialsSecret string, kubeClient *kube.KubernetesClient) (git.Creds, error) { +func getCredsFromSecret(wbc *WriteBackConfig, credentialsSecret string, kubeClient *kube.ImageUpdaterKubernetesClient) (git.Creds, error) { var credentials map[string][]byte var err error s := strings.SplitN(credentialsSecret, "/", 2) if len(s) == 2 { - credentials, err = kubeClient.GetSecretData(s[0], s[1]) + credentials, err = kubeClient.KubeClient.GetSecretData(s[0], s[1]) if err != nil { return nil, err } diff --git a/pkg/argocd/gitcreds_test.go b/pkg/argocd/gitcreds_test.go index 0343cbb2..d9091412 100644 --- a/pkg/argocd/gitcreds_test.go +++ b/pkg/argocd/gitcreds_test.go @@ -5,6 +5,7 @@ import ( "github.com/argoproj-labs/argocd-image-updater/ext/git" "github.com/argoproj-labs/argocd-image-updater/pkg/kube" + registryKube "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/kube" "github.com/argoproj-labs/argocd-image-updater/test/fake" "github.com/argoproj-labs/argocd-image-updater/test/fixture" @@ -21,8 +22,10 @@ func TestGetCredsFromSecret(t *testing.T) { secret1 := fixture.NewSecret("foo", "bar", map[string][]byte{"username": []byte("myuser"), "password": []byte("mypass")}) secret2 := fixture.NewSecret("foo1", "bar1", map[string][]byte{"username": []byte("myuser")}) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeClientsetWithResources(secret1, secret2), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeClientsetWithResources(secret1, secret2), + }, } // Test case 1: Valid secret reference @@ -53,7 +56,7 @@ func TestGetCredsFromSecret(t *testing.T) { } func TestGetGitCredsSource(t *testing.T) { - kubeClient := &kube.KubernetesClient{} + kubeClient := &kube.ImageUpdaterKubernetesClient{} wbc := &WriteBackConfig{ GitRepo: "https://github.com/example/repo.git", GitCreds: git.NoopCredsStore{}, diff --git a/pkg/argocd/update.go b/pkg/argocd/update.go index 0971fdd2..872886bc 100644 --- a/pkg/argocd/update.go +++ b/pkg/argocd/update.go @@ -13,10 +13,10 @@ import ( "github.com/argoproj-labs/argocd-image-updater/ext/git" "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/kube" - "github.com/argoproj-labs/argocd-image-updater/pkg/registry" + "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/image" "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/log" + "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/registry" "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/tag" "github.com/argoproj/argo-cd/v2/pkg/apiclient/application" @@ -37,7 +37,7 @@ type ImageUpdaterResult struct { type UpdateConfiguration struct { NewRegFN registry.NewRegistryClient ArgoClient ArgoCD - KubeClient *kube.KubernetesClient + KubeClient *kube.ImageUpdaterKubernetesClient UpdateApp *ApplicationImages DryRun bool GitCommitUser string @@ -221,7 +221,7 @@ func UpdateApplication(updateConf *UpdateConfiguration, state *SyncIterationStat } // The endpoint can provide default credentials for pulling images - err = rep.SetEndpointCredentials(updateConf.KubeClient) + err = rep.SetEndpointCredentials(updateConf.KubeClient.KubeClient) if err != nil { imgCtx.Errorf("Could not set registry endpoint credentials: %v", err) result.NumErrors += 1 @@ -231,7 +231,7 @@ func UpdateApplication(updateConf *UpdateConfiguration, state *SyncIterationStat imgCredSrc := applicationImage.GetParameterPullSecret(updateConf.UpdateApp.Application.Annotations) var creds *image.Credential = &image.Credential{} if imgCredSrc != nil { - creds, err = imgCredSrc.FetchCredentials(rep.RegistryAPI, updateConf.KubeClient) + creds, err = imgCredSrc.FetchCredentials(rep.RegistryAPI, updateConf.KubeClient.KubeClient) if err != nil { imgCtx.Warnf("Could not fetch credentials: %v", err) result.NumErrors += 1 @@ -633,7 +633,7 @@ func setHelmValue(currentValues *yaml.MapSlice, key string, value interface{}) e return err } -func getWriteBackConfig(app *v1alpha1.Application, kubeClient *kube.KubernetesClient, argoClient ArgoCD) (*WriteBackConfig, error) { +func getWriteBackConfig(app *v1alpha1.Application, kubeClient *kube.ImageUpdaterKubernetesClient, argoClient ArgoCD) (*WriteBackConfig, error) { wbc := &WriteBackConfig{} // Default write-back is to use Argo CD API wbc.Method = WriteBackApplication @@ -675,10 +675,10 @@ func getWriteBackConfig(app *v1alpha1.Application, kubeClient *kube.KubernetesCl return wbc, nil } -func parseDefaultTarget(appNamespace string, appName string, path string, kubeClient *kube.KubernetesClient) string { +func parseDefaultTarget(appNamespace string, appName string, path string, kubeClient *kube.ImageUpdaterKubernetesClient) string { // when running from command line and argocd-namespace is not set, e.g., via --argocd-namespace option, // kubeClient.Namespace may be resolved to "default". In this case, also use the file name without namespace - if appNamespace == kubeClient.Namespace || kubeClient.Namespace == "default" || appNamespace == "" { + if appNamespace == kubeClient.KubeClient.Namespace || kubeClient.KubeClient.Namespace == "default" || appNamespace == "" { defaultTargetFile := fmt.Sprintf(common.DefaultTargetFilePatternWithoutNamespace, appName) return filepath.Join(path, defaultTargetFile) } else { @@ -708,7 +708,7 @@ func parseTarget(writeBackTarget string, sourcePath string) string { } } -func parseGitConfig(app *v1alpha1.Application, kubeClient *kube.KubernetesClient, wbc *WriteBackConfig, creds string) error { +func parseGitConfig(app *v1alpha1.Application, kubeClient *kube.ImageUpdaterKubernetesClient, wbc *WriteBackConfig, creds string) error { branch, ok := app.Annotations[common.GitBranchAnnotation] if ok { branches := strings.Split(strings.TrimSpace(branch), ":") diff --git a/pkg/argocd/update_test.go b/pkg/argocd/update_test.go index a6e87a1c..480f9946 100644 --- a/pkg/argocd/update_test.go +++ b/pkg/argocd/update_test.go @@ -15,10 +15,11 @@ import ( gitmock "github.com/argoproj-labs/argocd-image-updater/ext/git/mocks" argomock "github.com/argoproj-labs/argocd-image-updater/pkg/argocd/mocks" "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/kube" - "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/registry-scanner/pkg/image" + registryKube "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/kube" + "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/registry" + regmock "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/registry/mocks" "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/tag" "github.com/argoproj-labs/argocd-image-updater/test/fake" "github.com/argoproj-labs/argocd-image-updater/test/fixture" @@ -44,8 +45,10 @@ func Test_UpdateApplication(t *testing.T) { argoClient := argomock.ArgoCD{} argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeKubeClient(), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeKubeClient(), + }, } annotations := map[string]string{ common.ImageUpdaterAnnotation: "foobar=gcr.io/jannfis/foobar:>=1.0.1,foobar=gcr.io/jannfis/barbar:>=1.0.1", @@ -111,8 +114,10 @@ func Test_UpdateApplication(t *testing.T) { "githubAppInstallationID": []byte("87654321"), "githubAppPrivateKey": []byte("foo"), }) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeClientsetWithResources(secret), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeClientsetWithResources(secret), + }, } annotations := map[string]string{ @@ -174,8 +179,10 @@ func Test_UpdateApplication(t *testing.T) { return ®Mock, nil } - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeKubeClient(), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeKubeClient(), + }, } appImages := &ApplicationImages{ Application: v1alpha1.Application{ @@ -240,8 +247,10 @@ func Test_UpdateApplication(t *testing.T) { argoClient := argomock.ArgoCD{} argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeKubeClient(), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeKubeClient(), + }, } appImages := &ApplicationImages{ Application: v1alpha1.Application{ @@ -302,8 +311,10 @@ func Test_UpdateApplication(t *testing.T) { argoClient := argomock.ArgoCD{} argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeKubeClient(), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeKubeClient(), + }, } appImages := &ApplicationImages{ Application: v1alpha1.Application{ @@ -366,8 +377,10 @@ func Test_UpdateApplication(t *testing.T) { argoClient := argomock.ArgoCD{} argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeKubeClient(), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeKubeClient(), + }, } appImages := &ApplicationImages{ Application: v1alpha1.Application{ @@ -427,8 +440,10 @@ func Test_UpdateApplication(t *testing.T) { argoClient := argomock.ArgoCD{} argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeKubeClient(), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeKubeClient(), + }, } appImages := &ApplicationImages{ Application: v1alpha1.Application{ @@ -485,8 +500,10 @@ func Test_UpdateApplication(t *testing.T) { argoClient := argomock.ArgoCD{} argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeClientsetWithResources(fixture.NewSecret("foo", "bar", map[string][]byte{"creds": []byte("myuser:mypass")})), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeClientsetWithResources(fixture.NewSecret("foo", "bar", map[string][]byte{"creds": []byte("myuser:mypass")})), + }, } appImages := &ApplicationImages{ Application: v1alpha1.Application{ @@ -543,8 +560,10 @@ func Test_UpdateApplication(t *testing.T) { argoClient := argomock.ArgoCD{} argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeKubeClient(), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeKubeClient(), + }, } appImages := &ApplicationImages{ Application: v1alpha1.Application{ @@ -599,8 +618,10 @@ func Test_UpdateApplication(t *testing.T) { argoClient := argomock.ArgoCD{} argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeKubeClient(), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeKubeClient(), + }, } appImages := &ApplicationImages{ Application: v1alpha1.Application{ @@ -655,8 +676,10 @@ func Test_UpdateApplication(t *testing.T) { argoClient := argomock.ArgoCD{} argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeKubeClient(), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeKubeClient(), + }, } annotations := map[string]string{ common.ImageUpdaterAnnotation: "foobar=gcr.io/jannfis/foobar:>=1.0.1", @@ -714,8 +737,10 @@ func Test_UpdateApplication(t *testing.T) { argoClient := argomock.ArgoCD{} argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeKubeClient(), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeKubeClient(), + }, } annotations := map[string]string{ common.ImageUpdaterAnnotation: "foobar=gcr.io/jannfis/foobar:>=1.0.1", @@ -789,8 +814,10 @@ func Test_UpdateApplication(t *testing.T) { argoClient := argomock.ArgoCD{} argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeKubeClient(), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeKubeClient(), + }, } appImages := &ApplicationImages{ Application: v1alpha1.Application{ @@ -865,8 +892,10 @@ func Test_UpdateApplication(t *testing.T) { argoClient := argomock.ArgoCD{} argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeKubeClient(), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeKubeClient(), + }, } appImages := &ApplicationImages{ Application: v1alpha1.Application{ @@ -925,8 +954,10 @@ func Test_UpdateApplication(t *testing.T) { argoClient := argomock.ArgoCD{} argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeKubeClient(), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeKubeClient(), + }, } appImages := &ApplicationImages{ Application: v1alpha1.Application{ @@ -978,8 +1009,10 @@ func Test_UpdateApplication(t *testing.T) { argoClient := argomock.ArgoCD{} argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeKubeClient(), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeKubeClient(), + }, } appImages := &ApplicationImages{ Application: v1alpha1.Application{ @@ -1034,8 +1067,10 @@ func Test_UpdateApplication(t *testing.T) { argoClient := argomock.ArgoCD{} argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeKubeClient(), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeKubeClient(), + }, } appImages := &ApplicationImages{ Application: v1alpha1.Application{ @@ -1090,8 +1125,10 @@ func Test_UpdateApplication(t *testing.T) { argoClient := argomock.ArgoCD{} argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeKubeClient(), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeKubeClient(), + }, } appImages := &ApplicationImages{ Application: v1alpha1.Application{ @@ -2262,8 +2299,10 @@ func Test_GetWriteBackConfig(t *testing.T) { argoClient := argomock.ArgoCD{} argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeKubeClient(), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeKubeClient(), + }, } wbc, err := getWriteBackConfig(&app, &kubeClient, &argoClient) @@ -2294,8 +2333,10 @@ func Test_GetWriteBackConfig(t *testing.T) { argoClient := argomock.ArgoCD{} argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeKubeClient(), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeKubeClient(), + }, } wbc, err := getWriteBackConfig(&app, &kubeClient, &argoClient) @@ -2325,8 +2366,10 @@ func Test_GetWriteBackConfig(t *testing.T) { argoClient := argomock.ArgoCD{} argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeKubeClient(), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeKubeClient(), + }, } wbc, err := getWriteBackConfig(&app, &kubeClient, &argoClient) @@ -2359,8 +2402,10 @@ func Test_GetWriteBackConfig(t *testing.T) { argoClient := argomock.ArgoCD{} argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeKubeClient(), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeKubeClient(), + }, } wbc, err := getWriteBackConfig(&app, &kubeClient, &argoClient) @@ -2394,8 +2439,10 @@ func Test_GetWriteBackConfig(t *testing.T) { argoClient := argomock.ArgoCD{} argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeKubeClient(), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeKubeClient(), + }, } wbc, err := getWriteBackConfig(&app, &kubeClient, &argoClient) @@ -2430,8 +2477,10 @@ func Test_GetWriteBackConfig(t *testing.T) { argoClient := argomock.ArgoCD{} argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeKubeClient(), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeKubeClient(), + }, } wbc, err := getWriteBackConfig(&app, &kubeClient, &argoClient) @@ -2466,8 +2515,10 @@ func Test_GetWriteBackConfig(t *testing.T) { argoClient := argomock.ArgoCD{} argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeKubeClient(), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeKubeClient(), + }, } wbc, err := getWriteBackConfig(&app, &kubeClient, &argoClient) @@ -2502,8 +2553,10 @@ func Test_GetWriteBackConfig(t *testing.T) { argoClient := argomock.ArgoCD{} argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeKubeClient(), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeKubeClient(), + }, } wbc, err := getWriteBackConfig(&app, &kubeClient, &argoClient) @@ -2538,8 +2591,10 @@ func Test_GetWriteBackConfig(t *testing.T) { argoClient := argomock.ArgoCD{} argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeKubeClient(), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeKubeClient(), + }, } wbc, err := getWriteBackConfig(&app, &kubeClient, &argoClient) @@ -2573,8 +2628,10 @@ func Test_GetWriteBackConfig(t *testing.T) { argoClient := argomock.ArgoCD{} argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeKubeClient(), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeKubeClient(), + }, } _, err := getWriteBackConfig(&app, &kubeClient, &argoClient) @@ -2603,8 +2660,10 @@ func Test_GetWriteBackConfig(t *testing.T) { argoClient := argomock.ArgoCD{} argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeKubeClient(), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeKubeClient(), + }, } wbc, err := getWriteBackConfig(&app, &kubeClient, &argoClient) @@ -2636,8 +2695,10 @@ func Test_GetWriteBackConfig(t *testing.T) { argoClient := argomock.ArgoCD{} argoClient.On("UpdateSpec", mock.Anything, mock.Anything).Return(nil, nil) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeKubeClient(), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeKubeClient(), + }, } wbc, err := getWriteBackConfig(&app, &kubeClient, &argoClient) @@ -2655,8 +2716,10 @@ func Test_GetGitCreds(t *testing.T) { "username": []byte("foo"), "password": []byte("bar"), }) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeClientsetWithResources(secret), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeClientsetWithResources(secret), + }, } app := v1alpha1.Application{ ObjectMeta: v1.ObjectMeta{ @@ -2696,8 +2759,10 @@ func Test_GetGitCreds(t *testing.T) { "githubAppInstallationID": []byte("87654321"), "githubAppPrivateKey": []byte("foo"), }) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeClientsetWithResources(secret), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeClientsetWithResources(secret), + }, } app := v1alpha1.Application{ ObjectMeta: v1.ObjectMeta{ @@ -2751,8 +2816,10 @@ func Test_GetGitCreds(t *testing.T) { } for _, secretEntry := range invalidSecretEntries { secret = fixture.NewSecret("argocd-image-updater", "git-creds", secretEntry) - kubeClient = kube.KubernetesClient{ - Clientset: fake.NewFakeClientsetWithResources(secret), + kubeClient = kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeClientsetWithResources(secret), + }, } wbc, err = getWriteBackConfig(&app, &kubeClient, &argoClient) require.NoError(t, err) @@ -2767,8 +2834,10 @@ func Test_GetGitCreds(t *testing.T) { secret := fixture.NewSecret("argocd-image-updater", "git-creds", map[string][]byte{ "sshPrivateKey": []byte("foo"), }) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeClientsetWithResources(secret), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeClientsetWithResources(secret), + }, } app := v1alpha1.Application{ ObjectMeta: v1.ObjectMeta{ @@ -2818,9 +2887,11 @@ func Test_GetGitCreds(t *testing.T) { }) fixture.AddPartOfArgoCDLabel(secret, configMap) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeClientsetWithResources(secret, configMap), - Namespace: "argocd", + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeClientsetWithResources(secret, configMap), + Namespace: "argocd", + }, } app := v1alpha1.Application{ ObjectMeta: v1.ObjectMeta{ @@ -2857,8 +2928,10 @@ func Test_GetGitCreds(t *testing.T) { secret := fixture.NewSecret("argocd-image-updater", "git-creds", map[string][]byte{ "sshPrivateKex": []byte("foo"), }) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeClientsetWithResources(secret), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeClientsetWithResources(secret), + }, } app := v1alpha1.Application{ ObjectMeta: v1.ObjectMeta{ @@ -2892,8 +2965,10 @@ func Test_GetGitCreds(t *testing.T) { secret := fixture.NewSecret("argocd-image-updater", "git-creds", map[string][]byte{ "sshPrivateKey": []byte("foo"), }) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeClientsetWithResources(secret), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeClientsetWithResources(secret), + }, } app := v1alpha1.Application{ ObjectMeta: v1.ObjectMeta{ @@ -2928,8 +3003,10 @@ func Test_GetGitCreds(t *testing.T) { secret := fixture.NewSecret("argocd-image-updater", "git-creds", map[string][]byte{ "sshPrivateKey": []byte("foo"), }) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeClientsetWithResources(secret), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeClientsetWithResources(secret), + }, } app := v1alpha1.Application{ ObjectMeta: v1.ObjectMeta{ @@ -2964,8 +3041,10 @@ func Test_GetGitCreds(t *testing.T) { secret := fixture.NewSecret("argocd-image-updater", "git-creds", map[string][]byte{ "sshPrivateKey": []byte("foo"), }) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeClientsetWithResources(secret), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeClientsetWithResources(secret), + }, } app := v1alpha1.Application{ @@ -3007,8 +3086,10 @@ func Test_CommitUpdates(t *testing.T) { secret := fixture.NewSecret("argocd-image-updater", "git-creds", map[string][]byte{ "sshPrivateKey": []byte("foo"), }) - kubeClient := kube.KubernetesClient{ - Clientset: fake.NewFakeClientsetWithResources(secret), + kubeClient := kube.ImageUpdaterKubernetesClient{ + KubeClient: ®istryKube.KubernetesClient{ + Clientset: fake.NewFakeClientsetWithResources(secret), + }, } app := v1alpha1.Application{ ObjectMeta: v1.ObjectMeta{ @@ -3167,7 +3248,7 @@ helm: }) t.Run("Good commit to helm override with argocd namespace", func(t *testing.T) { - kubeClient.Namespace = "argocd" + kubeClient.KubeClient.Namespace = "argocd" app := app.DeepCopy() app.Status.SourceType = "Helm" app.ObjectMeta.Namespace = "argocd" @@ -3219,7 +3300,7 @@ helm: }) t.Run("Good commit to helm override with another namespace", func(t *testing.T) { - kubeClient.Namespace = "argocd" + kubeClient.KubeClient.Namespace = "argocd" app := app.DeepCopy() app.Status.SourceType = "Helm" app.ObjectMeta.Namespace = "testNS" diff --git a/pkg/image/credentials.go b/pkg/image/credentials.go deleted file mode 100644 index 4a5559cc..00000000 --- a/pkg/image/credentials.go +++ /dev/null @@ -1,261 +0,0 @@ -package image - -import ( - "encoding/base64" - "encoding/json" - "fmt" - "os" - "os/exec" - "strings" - "time" - - argoexec "github.com/argoproj/pkg/exec" - - "github.com/argoproj-labs/argocd-image-updater/pkg/kube" - "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/log" -) - -type CredentialSourceType int - -const ( - CredentialSourceUnknown CredentialSourceType = 0 - CredentialSourcePullSecret CredentialSourceType = 1 - CredentialSourceSecret CredentialSourceType = 2 - CredentialSourceEnv CredentialSourceType = 3 - CredentialSourceExt CredentialSourceType = 4 -) - -type CredentialSource struct { - Type CredentialSourceType - Registry string - SecretNamespace string - SecretName string - SecretField string - EnvName string - ScriptPath string -} - -type Credential struct { - Username string - Password string -} - -const pullSecretField = ".dockerconfigjson" - -// gcr.io=secret:foo/bar#baz -// gcr.io=pullsecret:foo/bar -// gcr.io=env:FOOBAR - -func ParseCredentialSource(credentialSource string, requirePrefix bool) (*CredentialSource, error) { - src := CredentialSource{} - var secretDef string - tokens := strings.SplitN(credentialSource, "=", 2) - if len(tokens) != 2 || tokens[0] == "" || tokens[1] == "" { - if requirePrefix { - return nil, fmt.Errorf("invalid credential spec: %s", credentialSource) - } - secretDef = credentialSource - } else { - src.Registry = tokens[0] - secretDef = tokens[1] - } - - tokens = strings.Split(secretDef, ":") - if len(tokens) != 2 || tokens[0] == "" || tokens[1] == "" { - return nil, fmt.Errorf("invalid credential spec: %s", credentialSource) - } - - var err error - switch strings.ToLower(tokens[0]) { - case "secret": - err = src.parseSecretDefinition(tokens[1]) - src.Type = CredentialSourceSecret - case "pullsecret": - err = src.parsePullSecretDefinition(tokens[1]) - src.Type = CredentialSourcePullSecret - case "env": - err = src.parseEnvDefinition(tokens[1]) - src.Type = CredentialSourceEnv - case "ext": - err = src.parseExtDefinition(tokens[1]) - src.Type = CredentialSourceExt - default: - err = fmt.Errorf("unknown credential source: %s", tokens[0]) - } - - if err != nil { - return nil, err - } - - return &src, nil -} - -// FetchCredentials fetches the credentials for a given registry according to -// the credential source. -func (src *CredentialSource) FetchCredentials(registryURL string, kubeclient *kube.KubernetesClient) (*Credential, error) { - var creds Credential - log.Tracef("Fetching credentials for registry %s", registryURL) - switch src.Type { - case CredentialSourceEnv: - credEnv := os.Getenv(src.EnvName) - if credEnv == "" { - return nil, fmt.Errorf("could not fetch credentials: env '%s' is not set", src.EnvName) - } - tokens := strings.SplitN(credEnv, ":", 2) - if len(tokens) != 2 || tokens[0] == "" || tokens[1] == "" { - return nil, fmt.Errorf("could not fetch credentials: value of %s is malformed", src.EnvName) - } - creds.Username = tokens[0] - creds.Password = tokens[1] - return &creds, nil - case CredentialSourceSecret: - if kubeclient == nil { - return nil, fmt.Errorf("could not fetch credentials: no Kubernetes client given") - } - data, err := kubeclient.GetSecretField(src.SecretNamespace, src.SecretName, src.SecretField) - if err != nil { - return nil, fmt.Errorf("could not fetch secret '%s' from namespace '%s' (field: '%s'): %v", src.SecretName, src.SecretNamespace, src.SecretField, err) - } - tokens := strings.SplitN(data, ":", 2) - if len(tokens) != 2 { - return nil, fmt.Errorf("invalid credentials in secret '%s' from namespace '%s' (field '%s')", src.SecretName, src.SecretNamespace, src.SecretField) - } - creds.Username = tokens[0] - creds.Password = tokens[1] - return &creds, nil - case CredentialSourcePullSecret: - if kubeclient == nil { - return nil, fmt.Errorf("could not fetch credentials: no Kubernetes client given") - } - src.SecretField = pullSecretField - data, err := kubeclient.GetSecretField(src.SecretNamespace, src.SecretName, src.SecretField) - if err != nil { - return nil, fmt.Errorf("could not fetch secret '%s' from namespace '%s' (field: '%s'): %v", src.SecretName, src.SecretNamespace, src.SecretField, err) - } - creds.Username, creds.Password, err = parseDockerConfigJson(registryURL, data) - if err != nil { - return nil, err - } - return &creds, nil - case CredentialSourceExt: - if !strings.HasPrefix(src.ScriptPath, "/") { - return nil, fmt.Errorf("path to script must be absolute, but is '%s'", src.ScriptPath) - } - _, err := os.Stat(src.ScriptPath) - if err != nil { - return nil, fmt.Errorf("could not stat %s: %v", src.ScriptPath, err) - } - cmd := exec.Command(src.ScriptPath) - out, err := argoexec.RunCommandExt(cmd, argoexec.CmdOpts{Timeout: 10 * time.Second}) - if err != nil { - return nil, fmt.Errorf("error executing %s: %v", src.ScriptPath, err) - } - tokens := strings.SplitN(out, ":", 2) - if len(tokens) != 2 { - return nil, fmt.Errorf("invalid script output, must be single line with syntax :") - } - creds.Username = tokens[0] - creds.Password = tokens[1] - return &creds, nil - - default: - return nil, fmt.Errorf("unknown credential type") - } -} - -// Parse a secret definition in form of 'namespace/name#field' -func (src *CredentialSource) parseSecretDefinition(definition string) error { - tokens := strings.Split(definition, "#") - if len(tokens) != 2 || tokens[0] == "" || tokens[1] == "" { - return fmt.Errorf("invalid secret definition: %s", definition) - } - src.SecretField = tokens[1] - tokens = strings.Split(tokens[0], "/") - if len(tokens) != 2 || tokens[0] == "" || tokens[1] == "" { - return fmt.Errorf("invalid secret definition: %s", definition) - } - src.SecretNamespace = tokens[0] - src.SecretName = tokens[1] - - return nil -} - -// Parse an image pull secret definition in form of 'namespace/name' -func (src *CredentialSource) parsePullSecretDefinition(definition string) error { - tokens := strings.Split(definition, "/") - if len(tokens) != 2 || tokens[0] == "" || tokens[1] == "" { - return fmt.Errorf("invalid secret definition: %s", definition) - } - - src.SecretNamespace = tokens[0] - src.SecretName = tokens[1] - src.SecretField = pullSecretField - - return nil -} - -// Parse an environment definition -// nolint:unparam -func (src *CredentialSource) parseEnvDefinition(definition string) error { - src.EnvName = definition - return nil -} - -// Parse an external script definition -// nolint:unparam -func (src *CredentialSource) parseExtDefinition(definition string) error { - src.ScriptPath = definition - return nil -} - -// This unmarshals & parses Docker's config.json file, returning username and -// password for given registry URL -func parseDockerConfigJson(registryURL string, jsonSource string) (string, string, error) { - var dockerConf map[string]interface{} - err := json.Unmarshal([]byte(jsonSource), &dockerConf) - if err != nil { - return "", "", err - } - auths, ok := dockerConf["auths"].(map[string]interface{}) - if !ok { - return "", "", fmt.Errorf("no credentials in image pull secret") - } - - var regPrefix string - if strings.HasPrefix(registryURL, "http://") { - regPrefix = strings.TrimPrefix(registryURL, "http://") - } else if strings.HasPrefix(registryURL, "https://") { - regPrefix = strings.TrimPrefix(registryURL, "https://") - } else { - regPrefix = registryURL - } - - regPrefix = strings.TrimSuffix(regPrefix, "/") - - for registry, authConf := range auths { - if !strings.HasPrefix(registry, registryURL) && !strings.HasPrefix(registry, regPrefix) { - log.Tracef("found registry %s in image pull secret, but we want %s (%s) - skipping", registry, registryURL, regPrefix) - continue - } - authEntry, ok := authConf.(map[string]interface{}) - if !ok { - return "", "", fmt.Errorf("invalid auth entry for registry entry %s ('auths' entry should be map)", registry) - } - authString, ok := authEntry["auth"].(string) - if !ok { - return "", "", fmt.Errorf("invalid auth token for registry entry %s ('auth' should be string')", registry) - } - authToken, err := base64.StdEncoding.DecodeString(authString) - if err != nil { - return "", "", fmt.Errorf("could not base64-decode auth data for registry entry %s: %v", registry, err) - } - tokens := strings.SplitN(string(authToken), ":", 2) - if len(tokens) != 2 { - return "", "", fmt.Errorf("invalid data after base64 decoding auth entry for registry entry %s", registry) - } - - return tokens[0], tokens[1], nil - } - - return "", "", fmt.Errorf("no valid auth entry for registry %s found in image pull secret", registryURL) -} diff --git a/pkg/image/credentials_test.go b/pkg/image/credentials_test.go deleted file mode 100644 index 4c5b7673..00000000 --- a/pkg/image/credentials_test.go +++ /dev/null @@ -1,410 +0,0 @@ -package image - -import ( - "fmt" - "os" - "path" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/argoproj-labs/argocd-image-updater/pkg/kube" - "github.com/argoproj-labs/argocd-image-updater/test/fake" - "github.com/argoproj-labs/argocd-image-updater/test/fixture" -) - -func Test_ParseCredentialAnnotation(t *testing.T) { - t.Run("Parse valid credentials definition of type secret", func(t *testing.T) { - src, err := ParseCredentialSource("gcr.io=secret:mynamespace/mysecret#anyfield", true) - assert.NoError(t, err) - assert.Equal(t, "gcr.io", src.Registry) - assert.Equal(t, "mynamespace", src.SecretNamespace) - assert.Equal(t, "mysecret", src.SecretName) - assert.Equal(t, "anyfield", src.SecretField) - }) - - t.Run("Parse valid credentials definition of type pullsecret", func(t *testing.T) { - src, err := ParseCredentialSource("gcr.io=pullsecret:mynamespace/mysecret", true) - assert.NoError(t, err) - assert.Equal(t, "gcr.io", src.Registry) - assert.Equal(t, "mynamespace", src.SecretNamespace) - assert.Equal(t, "mysecret", src.SecretName) - assert.Equal(t, ".dockerconfigjson", src.SecretField) - }) - - t.Run("Parse invalid secret definition - missing registry", func(t *testing.T) { - src, err := ParseCredentialSource("secret:mynamespace/mysecret#anyfield", true) - assert.Error(t, err) - assert.Nil(t, src) - }) - - t.Run("Parse invalid secret definition - empty registry", func(t *testing.T) { - src, err := ParseCredentialSource("=secret:mynamespace/mysecret#anyfield", true) - assert.Error(t, err) - assert.Nil(t, src) - }) - - t.Run("Parse invalid secret definition - unknown credential type", func(t *testing.T) { - src, err := ParseCredentialSource("gcr.io=secrets:mynamespace/mysecret#anyfield", true) - assert.Error(t, err) - assert.Nil(t, src) - }) - - t.Run("Parse invalid secret definition - missing field", func(t *testing.T) { - src, err := ParseCredentialSource("gcr.io=secret:mynamespace/mysecret#", true) - assert.Error(t, err) - assert.Nil(t, src) - }) - - t.Run("Parse invalid secret definition - missing namespace", func(t *testing.T) { - src, err := ParseCredentialSource("gcr.io=secret:/mysecret#anyfield", true) - assert.Error(t, err) - assert.Nil(t, src) - }) - - t.Run("Parse invalid credential definition - missing name", func(t *testing.T) { - src, err := ParseCredentialSource("gcr.io=secret:mynamespace/#anyfield", true) - assert.Error(t, err) - assert.Nil(t, src) - }) - - t.Run("Parse invalid credential definition - missing most", func(t *testing.T) { - src, err := ParseCredentialSource("gcr.io=secret:", true) - assert.Error(t, err) - assert.Nil(t, src) - }) - - t.Run("Parse invalid pullsecret definition - missing namespace", func(t *testing.T) { - src, err := ParseCredentialSource("gcr.io=pullsecret:/mysecret", true) - assert.Error(t, err) - assert.Nil(t, src) - }) - - t.Run("Parse invalid credential definition - missing name", func(t *testing.T) { - src, err := ParseCredentialSource("gcr.io=pullsecret:mynamespace", true) - assert.Error(t, err) - assert.Nil(t, src) - }) - - t.Run("Parse valid credentials definition from environment", func(t *testing.T) { - src, err := ParseCredentialSource("env:DUMMY_SECRET", false) - require.NoError(t, err) - require.NotNil(t, src) - assert.Equal(t, "DUMMY_SECRET", src.EnvName) - }) - - t.Run("Parse valid credentials definition from environment", func(t *testing.T) { - src, err := ParseCredentialSource("env:DUMMY_SECRET", false) - require.NoError(t, err) - require.NotNil(t, src) - assert.Equal(t, "DUMMY_SECRET", src.EnvName) - }) - - t.Run("Parse external script credentials", func(t *testing.T) { - src, err := ParseCredentialSource("ext:/tmp/a.sh", false) - require.NoError(t, err) - assert.Equal(t, CredentialSourceExt, src.Type) - assert.Equal(t, "/tmp/a.sh", src.ScriptPath) - }) -} - -func Test_ParseCredentialReference(t *testing.T) { - t.Run("Parse valid credentials definition of type secret", func(t *testing.T) { - src, err := ParseCredentialSource("secret:mynamespace/mysecret#anyfield", false) - assert.NoError(t, err) - assert.Equal(t, "", src.Registry) - assert.Equal(t, "mynamespace", src.SecretNamespace) - assert.Equal(t, "mysecret", src.SecretName) - assert.Equal(t, "anyfield", src.SecretField) - }) - - t.Run("Parse valid credentials definition of type pullsecret", func(t *testing.T) { - src, err := ParseCredentialSource("gcr.io=pullsecret:mynamespace/mysecret", false) - assert.NoError(t, err) - assert.Equal(t, "gcr.io", src.Registry) - assert.Equal(t, "mynamespace", src.SecretNamespace) - assert.Equal(t, "mysecret", src.SecretName) - assert.Equal(t, ".dockerconfigjson", src.SecretField) - }) - - t.Run("Parse invalid secret definition - empty registry", func(t *testing.T) { - src, err := ParseCredentialSource("=secret:mynamespace/mysecret#anyfield", false) - assert.Error(t, err) - assert.Nil(t, src) - }) - -} - -func Test_FetchCredentialsFromSecret(t *testing.T) { - t.Run("Fetch credentials from secret", func(t *testing.T) { - secretData := make(map[string][]byte) - secretData["username_password"] = []byte(fmt.Sprintf("%s:%s", "foo", "bar")) - secret := fixture.NewSecret("test", "test", secretData) - clientset := fake.NewFakeClientsetWithResources(secret) - credSrc := &CredentialSource{ - Type: CredentialSourceSecret, - SecretNamespace: "test", - SecretName: "test", - SecretField: "username_password", - } - creds, err := credSrc.FetchCredentials("NA", &kube.KubernetesClient{Clientset: clientset}) - require.NoError(t, err) - require.NotNil(t, creds) - assert.Equal(t, "foo", creds.Username) - assert.Equal(t, "bar", creds.Password) - - credSrc.SecretNamespace = "test1" // test with a wrong SecretNamespace - creds, err = credSrc.FetchCredentials("NA", &kube.KubernetesClient{Clientset: clientset}) - require.Error(t, err) - require.Nil(t, creds) - }) - - t.Run("Fetch credentials from secret with invalid config", func(t *testing.T) { - secretData := make(map[string][]byte) - secretData["username_password"] = []byte(fmt.Sprintf("%s:%s", "foo", "bar")) - secret := fixture.NewSecret("test", "test", secretData) - clientset := fake.NewFakeClientsetWithResources(secret) - credSrc := &CredentialSource{ - Type: CredentialSourceSecret, - SecretNamespace: "test", - SecretName: "test", - SecretField: "username_password", - } - creds, err := credSrc.FetchCredentials("NA", nil) - require.Error(t, err) // should fail with "could not fetch credentials: no Kubernetes client given" - require.Nil(t, creds) - - credSrc.SecretField = "BAD" // test with a wrong SecretField - creds, err = credSrc.FetchCredentials("NA", &kube.KubernetesClient{Clientset: clientset}) - require.Error(t, err) - require.Nil(t, creds) - - }) -} - -func Test_FetchCredentialsFromPullSecret(t *testing.T) { - t.Run("Fetch credentials from pull secret", func(t *testing.T) { - dockerJson := fixture.MustReadFile("../../test/testdata/docker/valid-config.json") - secretData := make(map[string][]byte) - secretData[pullSecretField] = []byte(dockerJson) - pullSecret := fixture.NewSecret("test", "test", secretData) - clientset := fake.NewFakeClientsetWithResources(pullSecret) - credSrc := &CredentialSource{ - Type: CredentialSourcePullSecret, - Registry: "https://registry-1.docker.io/v2", - SecretNamespace: "test", - SecretName: "test", - } - creds, err := credSrc.FetchCredentials("https://registry-1.docker.io", &kube.KubernetesClient{Clientset: clientset}) - require.NoError(t, err) - require.NotNil(t, creds) - assert.Equal(t, "foo", creds.Username) - assert.Equal(t, "bar", creds.Password) - - credSrc.SecretNamespace = "test1" // test with a wrong SecretNamespace - creds, err = credSrc.FetchCredentials("https://registry-1.docker.io", &kube.KubernetesClient{Clientset: clientset}) - require.Error(t, err) - require.Nil(t, creds) - }) - - t.Run("Fetch credentials from pull secret with invalid config", func(t *testing.T) { - dockerJson := fixture.MustReadFile("../../test/testdata/docker/valid-config.json") - dockerJson = strings.ReplaceAll(dockerJson, "auths", "BAD-KEY") - secretData := make(map[string][]byte) - secretData[pullSecretField] = []byte(dockerJson) - pullSecret := fixture.NewSecret("test", "test", secretData) - clientset := fake.NewFakeClientsetWithResources(pullSecret) - credSrc := &CredentialSource{ - Type: CredentialSourcePullSecret, - Registry: "https://registry-1.docker.io/v2", - SecretNamespace: "test", - SecretName: "test", - } - creds, err := credSrc.FetchCredentials("https://registry-1.docker.io", &kube.KubernetesClient{Clientset: clientset}) - require.Error(t, err) // should fail with "no credentials in image pull secret" - require.Nil(t, creds) - - creds, err = credSrc.FetchCredentials("https://registry-1.docker.io", nil) - require.Error(t, err) // should fail with "could not fetch credentials: no Kubernetes client given" - require.Nil(t, creds) - }) - - t.Run("Fetch credentials from pull secret with protocol stripped", func(t *testing.T) { - dockerJson := fixture.MustReadFile("../../test/testdata/docker/valid-config-noproto.json") - secretData := make(map[string][]byte) - secretData[pullSecretField] = []byte(dockerJson) - pullSecret := fixture.NewSecret("test", "test", secretData) - clientset := fake.NewFakeClientsetWithResources(pullSecret) - credSrc := &CredentialSource{ - Type: CredentialSourcePullSecret, - Registry: "https://registry-1.docker.io/v2", - SecretNamespace: "test", - SecretName: "test", - } - creds, err := credSrc.FetchCredentials("https://registry-1.docker.io", &kube.KubernetesClient{Clientset: clientset}) - require.NoError(t, err) - require.NotNil(t, creds) - assert.Equal(t, "foo", creds.Username) - assert.Equal(t, "bar", creds.Password) - }) -} - -func Test_FetchCredentialsFromEnv(t *testing.T) { - t.Run("Fetch credentials from environment", func(t *testing.T) { - err := os.Setenv("MY_SECRET_ENV", "foo:bar") - require.NoError(t, err) - credSrc := &CredentialSource{ - Type: CredentialSourceEnv, - Registry: "https://registry-1.docker.io/v2", - EnvName: "MY_SECRET_ENV", - } - creds, err := credSrc.FetchCredentials("https://registry-1.docker.io", nil) - require.NoError(t, err) - require.NotNil(t, creds) - assert.Equal(t, "foo", creds.Username) - assert.Equal(t, "bar", creds.Password) - }) - - t.Run("Fetch credentials from environment with missing env var", func(t *testing.T) { - err := os.Setenv("MY_SECRET_ENV", "") - require.NoError(t, err) - credSrc := &CredentialSource{ - Type: CredentialSourceEnv, - Registry: "https://registry-1.docker.io/v2", - EnvName: "MY_SECRET_ENV", - } - creds, err := credSrc.FetchCredentials("https://registry-1.docker.io", nil) - require.Error(t, err) - require.Nil(t, creds) - }) - - t.Run("Fetch credentials from environment with invalid value in env var", func(t *testing.T) { - for _, value := range []string{"babayaga", "foo:", "bar:", ":"} { - err := os.Setenv("MY_SECRET_ENV", value) - require.NoError(t, err) - credSrc := &CredentialSource{ - Type: CredentialSourceEnv, - Registry: "https://registry-1.docker.io/v2", - EnvName: "MY_SECRET_ENV", - } - creds, err := credSrc.FetchCredentials("https://registry-1.docker.io", nil) - require.Error(t, err) - require.Nil(t, creds) - } - }) -} - -func Test_FetchCredentialsFromExt(t *testing.T) { - t.Run("Fetch credentials from external script - valid output", func(t *testing.T) { - pwd, err := os.Getwd() - require.NoError(t, err) - credSrc := &CredentialSource{ - Type: CredentialSourceExt, - Registry: "https://registry-1.docker.io/v2", - ScriptPath: path.Join(pwd, "..", "..", "test", "testdata", "scripts", "get-credentials-valid.sh"), - } - creds, err := credSrc.FetchCredentials("https://registry-1.docker.io", nil) - require.NoError(t, err) - require.NotNil(t, creds) - assert.Equal(t, "username", creds.Username) - assert.Equal(t, "password", creds.Password) - }) - t.Run("Fetch credentials from external script - invalid script output", func(t *testing.T) { - pwd, err := os.Getwd() - require.NoError(t, err) - credSrc := &CredentialSource{ - Type: CredentialSourceExt, - Registry: "https://registry-1.docker.io/v2", - ScriptPath: path.Join(pwd, "..", "..", "test", "testdata", "scripts", "get-credentials-invalid.sh"), - } - creds, err := credSrc.FetchCredentials("https://registry-1.docker.io", nil) - require.Errorf(t, err, "invalid script output") - require.Nil(t, creds) - }) - t.Run("Fetch credentials from external script - script does not exist", func(t *testing.T) { - pwd, err := os.Getwd() - require.NoError(t, err) - credSrc := &CredentialSource{ - Type: CredentialSourceExt, - Registry: "https://registry-1.docker.io/v2", - ScriptPath: path.Join(pwd, "..", "..", "test", "testdata", "scripts", "get-credentials-notexist.sh"), - } - creds, err := credSrc.FetchCredentials("https://registry-1.docker.io", nil) - require.Errorf(t, err, "no such file or directory") - require.Nil(t, creds) - }) - t.Run("Fetch credentials from external script - relative path", func(t *testing.T) { - credSrc := &CredentialSource{ - Type: CredentialSourceExt, - Registry: "https://registry-1.docker.io/v2", - ScriptPath: "get-credentials-notexist.sh", - } - creds, err := credSrc.FetchCredentials("https://registry-1.docker.io", nil) - require.Errorf(t, err, "path to script must be absolute") - require.Nil(t, creds) - }) -} - -func Test_FetchCredentialsFromUnknown(t *testing.T) { - t.Run("Fetch credentials from unknown type", func(t *testing.T) { - credSrc := &CredentialSource{ - Type: CredentialSourceType(-1), - Registry: "https://registry-1.docker.io/v2", - } - creds, err := credSrc.FetchCredentials("https://registry-1.docker.io", nil) - require.Error(t, err) // should fail with "unknown credential type" - require.Nil(t, creds) - }) -} - -func Test_ParseDockerConfig(t *testing.T) { - t.Run("Parse valid Docker configuration with matching registry", func(t *testing.T) { - config := fixture.MustReadFile("../../test/testdata/docker/valid-config.json") - username, password, err := parseDockerConfigJson("https://registry-1.docker.io", config) - require.NoError(t, err) - assert.Equal(t, "foo", username) - assert.Equal(t, "bar", password) - }) - - t.Run("Parse valid Docker configuration with matching registry as prefix", func(t *testing.T) { - config := fixture.MustReadFile("../../test/testdata/docker/valid-config-noproto.json") - username, password, err := parseDockerConfigJson("https://registry-1.docker.io", config) - require.NoError(t, err) - assert.Equal(t, "foo", username) - assert.Equal(t, "bar", password) - }) - - t.Run("Parse valid Docker configuration with matching http registry as prefix", func(t *testing.T) { - config := fixture.MustReadFile("../../test/testdata/docker/valid-config-noproto.json") - username, password, err := parseDockerConfigJson("http://registry-1.docker.io", config) - require.NoError(t, err) - assert.Equal(t, "foo", username) - assert.Equal(t, "bar", password) - }) - - t.Run("Parse valid Docker configuration with matching no-protocol registry as prefix", func(t *testing.T) { - config := fixture.MustReadFile("../../test/testdata/docker/valid-config-noproto.json") - username, password, err := parseDockerConfigJson("registry-1.docker.io", config) - require.NoError(t, err) - assert.Equal(t, "foo", username) - assert.Equal(t, "bar", password) - }) - - t.Run("Parse valid Docker configuration with matching registry as prefix with / in the end", func(t *testing.T) { - config := fixture.MustReadFile("../../test/testdata/docker/valid-config-noproto.json") - username, password, err := parseDockerConfigJson("https://registry-1.docker.io/", config) - require.NoError(t, err) - assert.Equal(t, "foo", username) - assert.Equal(t, "bar", password) - }) - - t.Run("Parse valid Docker configuration without matching registry", func(t *testing.T) { - config := fixture.MustReadFile("../../test/testdata/docker/valid-config.json") - username, password, err := parseDockerConfigJson("https://gcr.io", config) - assert.Error(t, err) - assert.Empty(t, username) - assert.Empty(t, password) - }) -} diff --git a/pkg/image/image.go b/pkg/image/image.go deleted file mode 100644 index 01261be4..00000000 --- a/pkg/image/image.go +++ /dev/null @@ -1,275 +0,0 @@ -package image - -import ( - "strings" - "time" - - "github.com/distribution/distribution/v3/reference" - - "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/log" - "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/tag" -) - -type ContainerImage struct { - RegistryURL string - ImageName string - ImageTag *tag.ImageTag - ImageAlias string - HelmParamImageName string - HelmParamImageVersion string - KustomizeImage *ContainerImage - original string -} - -type ContainerImageList []*ContainerImage - -// NewFromIdentifier parses an image identifier and returns a populated ContainerImage -func NewFromIdentifier(identifier string) *ContainerImage { - imgRef := identifier - alias := "" - if strings.Contains(identifier, "=") { - n := strings.SplitN(identifier, "=", 2) - imgRef = n[1] - alias = n[0] - } - if parsed, err := reference.ParseNormalizedNamed(imgRef); err == nil { - img := ContainerImage{} - img.RegistryURL = reference.Domain(parsed) - // remove default registry for backwards-compatibility - if img.RegistryURL == "docker.io" && !strings.HasPrefix(imgRef, "docker.io") { - img.RegistryURL = "" - } - img.ImageAlias = alias - img.ImageName = reference.Path(parsed) - // if library/ was added to the image name, remove it - if !strings.HasPrefix(imgRef, "library/") { - img.ImageName = strings.TrimPrefix(img.ImageName, "library/") - } - if digested, ok := parsed.(reference.Digested); ok { - img.ImageTag = &tag.ImageTag{ - TagDigest: string(digested.Digest()), - } - } else if tagged, ok := parsed.(reference.Tagged); ok { - img.ImageTag = &tag.ImageTag{ - TagName: tagged.Tag(), - } - } - img.original = identifier - return &img - } - - // if distribution couldn't parse it, fall back to the legacy parsing logic - img := ContainerImage{} - img.RegistryURL = getRegistryFromIdentifier(identifier) - img.ImageAlias, img.ImageName, img.ImageTag = getImageTagFromIdentifier(identifier) - img.original = identifier - return &img -} - -// String returns the string representation of given ContainerImage -func (img *ContainerImage) String() string { - str := "" - if img.ImageAlias != "" { - str += img.ImageAlias - str += "=" - } - str += img.GetFullNameWithTag() - return str -} - -func (img *ContainerImage) GetFullNameWithoutTag() string { - str := "" - if img.RegistryURL != "" { - str += img.RegistryURL + "/" - } - str += img.ImageName - return str -} - -// GetFullNameWithTag returns the complete image slug, including the registry -// and any tag digest or tag name set for the image. -func (img *ContainerImage) GetFullNameWithTag() string { - str := "" - if img.RegistryURL != "" { - str += img.RegistryURL + "/" - } - str += img.ImageName - if img.ImageTag != nil { - if img.ImageTag.TagName != "" { - str += ":" - str += img.ImageTag.TagName - } - if img.ImageTag.TagDigest != "" { - str += "@" - str += img.ImageTag.TagDigest - } - } - return str -} - -// GetTagWithDigest returns tag name along with any tag digest set for the image -func (img *ContainerImage) GetTagWithDigest() string { - str := "" - if img.ImageTag != nil { - if img.ImageTag.TagName != "" { - str += img.ImageTag.TagName - } - if img.ImageTag.TagDigest != "" { - if str == "" { - str += "latest" - } - str += "@" - str += img.ImageTag.TagDigest - } - } - return str -} - -func (img *ContainerImage) Original() string { - return img.original -} - -// IsUpdatable checks whether the given image can be updated with newTag while -// taking tagSpec into account. tagSpec must be given as a semver compatible -// version spec, i.e. ^1.0 or ~2.1 -func (img *ContainerImage) IsUpdatable(newTag, tagSpec string) bool { - return false -} - -// WithTag returns a copy of img with new tag information set -func (img *ContainerImage) WithTag(newTag *tag.ImageTag) *ContainerImage { - nimg := &ContainerImage{} - nimg.RegistryURL = img.RegistryURL - nimg.ImageName = img.ImageName - nimg.ImageTag = newTag - nimg.ImageAlias = img.ImageAlias - nimg.HelmParamImageName = img.HelmParamImageName - nimg.HelmParamImageVersion = img.HelmParamImageVersion - return nimg -} - -func (img *ContainerImage) DiffersFrom(other *ContainerImage, checkVersion bool) bool { - return img.RegistryURL != other.RegistryURL || img.ImageName != other.ImageName || (checkVersion && img.ImageTag.TagName != other.ImageTag.TagName) -} - -// ContainsImage checks whether img is contained in a list of images -func (list *ContainerImageList) ContainsImage(img *ContainerImage, checkVersion bool) *ContainerImage { - // if there is a KustomizeImage override, check it for a match first - if img.KustomizeImage != nil { - if kustomizeMatch := list.ContainsImage(img.KustomizeImage, checkVersion); kustomizeMatch != nil { - return kustomizeMatch - } - } - for _, image := range *list { - if img.ImageName == image.ImageName && image.RegistryURL == img.RegistryURL { - if !checkVersion || image.ImageTag.TagName == img.ImageTag.TagName { - return image - } - } - } - return nil -} - -func (list *ContainerImageList) Originals() []string { - results := make([]string, len(*list)) - for i, img := range *list { - results[i] = img.Original() - } - return results -} - -// String Returns the name of all images as a string, separated using comma -func (list *ContainerImageList) String() string { - imgNameList := make([]string, 0) - for _, image := range *list { - imgNameList = append(imgNameList, image.String()) - } - return strings.Join(imgNameList, ",") -} - -// Gets the registry URL from an image identifier -func getRegistryFromIdentifier(identifier string) string { - var imageString string - comp := strings.Split(identifier, "=") - if len(comp) > 1 { - imageString = comp[1] - } else { - imageString = identifier - } - comp = strings.Split(imageString, "/") - if len(comp) > 1 && strings.Contains(comp[0], ".") { - return comp[0] - } else { - return "" - } -} - -// Gets the image name and tag from an image identifier -func getImageTagFromIdentifier(identifier string) (string, string, *tag.ImageTag) { - var imageString string - var sourceName string - - // The original name is prepended to the image name, separated by = - comp := strings.SplitN(identifier, "=", 2) - if len(comp) == 2 { - sourceName = comp[0] - imageString = comp[1] - } else { - imageString = identifier - } - - // Strip any repository identifier from the string - comp = strings.Split(imageString, "/") - if len(comp) > 1 && strings.Contains(comp[0], ".") { - imageString = strings.Join(comp[1:], "/") - } - - // We can either have a tag name or a digest reference, or both - // jannfis/test-image:0.1 - // gcr.io/jannfis/test-image:0.1 - // gcr.io/jannfis/test-image@sha256:abcde - // gcr.io/jannfis/test-image:test-tag@sha256:abcde - if strings.Contains(imageString, "@") { - comp = strings.SplitN(imageString, "@", 2) - colonPos := strings.LastIndex(comp[0], ":") - slashPos := strings.LastIndex(comp[0], "/") - if colonPos > slashPos { - // first half (before @) contains image and tag name - return sourceName, comp[0][:colonPos], tag.NewImageTag(comp[0][colonPos+1:], time.Unix(0, 0), comp[1]) - } else { - // first half contains image name without tag name - return sourceName, comp[0], tag.NewImageTag("", time.Unix(0, 0), comp[1]) - } - } else { - comp = strings.SplitN(imageString, ":", 2) - if len(comp) != 2 { - return sourceName, imageString, nil - } else { - tagName, tagDigest := getImageDigestFromTag(comp[1]) - return sourceName, comp[0], tag.NewImageTag(tagName, time.Unix(0, 0), tagDigest) - } - } -} - -func getImageDigestFromTag(tagStr string) (string, string) { - a := strings.Split(tagStr, "@") - if len(a) != 2 { - return tagStr, "" - } else { - return a[0], a[1] - } -} - -// LogContext returns a log context for the given image, with required fields -// set to the image's information. -func (img *ContainerImage) LogContext() *log.LogContext { - logCtx := log.WithContext() - logCtx.AddField("image_name", img.GetFullNameWithoutTag()) - logCtx.AddField("image_alias", img.ImageAlias) - logCtx.AddField("registry_url", img.RegistryURL) - if img.ImageTag != nil { - logCtx.AddField("image_tag", img.ImageTag.TagName) - logCtx.AddField("image_digest", img.ImageTag.TagDigest) - } - return logCtx -} diff --git a/pkg/image/image_test.go b/pkg/image/image_test.go deleted file mode 100644 index 17094619..00000000 --- a/pkg/image/image_test.go +++ /dev/null @@ -1,226 +0,0 @@ -package image - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "golang.org/x/exp/slices" - - "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/tag" -) - -func Test_ParseImageTags(t *testing.T) { - t.Run("Parse valid image name without registry info", func(t *testing.T) { - image := NewFromIdentifier("jannfis/test-image:0.1") - assert.Empty(t, image.RegistryURL) - assert.Empty(t, image.ImageAlias) - assert.Equal(t, "jannfis/test-image", image.ImageName) - require.NotNil(t, image.ImageTag) - assert.Equal(t, "0.1", image.ImageTag.TagName) - assert.Equal(t, "jannfis/test-image:0.1", image.GetFullNameWithTag()) - assert.Equal(t, "jannfis/test-image", image.GetFullNameWithoutTag()) - }) - - t.Run("Single element image name is unmodified", func(t *testing.T) { - image := NewFromIdentifier("test-image") - assert.Empty(t, image.RegistryURL) - assert.Empty(t, image.ImageAlias) - assert.Equal(t, "test-image", image.ImageName) - require.Nil(t, image.ImageTag) - assert.Equal(t, "test-image", image.GetFullNameWithTag()) - assert.Equal(t, "test-image", image.GetFullNameWithoutTag()) - }) - - t.Run("library image name is unmodified", func(t *testing.T) { - image := NewFromIdentifier("library/test-image") - assert.Empty(t, image.RegistryURL) - assert.Empty(t, image.ImageAlias) - assert.Equal(t, "library/test-image", image.ImageName) - require.Nil(t, image.ImageTag) - assert.Equal(t, "library/test-image", image.GetFullNameWithTag()) - assert.Equal(t, "library/test-image", image.GetFullNameWithoutTag()) - }) - - t.Run("Parse valid image name with registry info", func(t *testing.T) { - image := NewFromIdentifier("gcr.io/jannfis/test-image:0.1") - assert.Equal(t, "gcr.io", image.RegistryURL) - assert.Empty(t, image.ImageAlias) - assert.Equal(t, "jannfis/test-image", image.ImageName) - require.NotNil(t, image.ImageTag) - assert.Equal(t, "0.1", image.ImageTag.TagName) - assert.Equal(t, "gcr.io/jannfis/test-image:0.1", image.GetFullNameWithTag()) - assert.Equal(t, "gcr.io/jannfis/test-image", image.GetFullNameWithoutTag()) - }) - - t.Run("Parse valid image name with default registry info", func(t *testing.T) { - image := NewFromIdentifier("docker.io/jannfis/test-image:0.1") - assert.Equal(t, "docker.io", image.RegistryURL) - assert.Empty(t, image.ImageAlias) - assert.Equal(t, "jannfis/test-image", image.ImageName) - require.NotNil(t, image.ImageTag) - assert.Equal(t, "0.1", image.ImageTag.TagName) - assert.Equal(t, "docker.io/jannfis/test-image:0.1", image.GetFullNameWithTag()) - assert.Equal(t, "docker.io/jannfis/test-image", image.GetFullNameWithoutTag()) - }) - - t.Run("Parse valid image name with digest tag", func(t *testing.T) { - image := NewFromIdentifier("gcr.io/jannfis/test-image@sha256:abcde") - assert.Equal(t, "gcr.io", image.RegistryURL) - assert.Empty(t, image.ImageAlias) - assert.Equal(t, "jannfis/test-image", image.ImageName) - require.NotNil(t, image.ImageTag) - assert.Empty(t, image.ImageTag.TagName) - assert.Equal(t, "sha256:abcde", image.ImageTag.TagDigest) - assert.Equal(t, "latest@sha256:abcde", image.GetTagWithDigest()) - assert.Equal(t, "gcr.io/jannfis/test-image@sha256:abcde", image.GetFullNameWithTag()) - assert.Equal(t, "gcr.io/jannfis/test-image", image.GetFullNameWithoutTag()) - }) - - t.Run("Parse valid image name with tag and digest", func(t *testing.T) { - image := NewFromIdentifier("gcr.io/jannfis/test-image:test-tag@sha256:abcde") - require.NotNil(t, image.ImageTag) - assert.Equal(t, "test-tag", image.ImageTag.TagName) - assert.Equal(t, "sha256:abcde", image.ImageTag.TagDigest) - assert.Equal(t, "test-tag@sha256:abcde", image.GetTagWithDigest()) - assert.Equal(t, "gcr.io/jannfis/test-image", image.GetFullNameWithoutTag()) - assert.Equal(t, "gcr.io/jannfis/test-image:test-tag@sha256:abcde", image.GetFullNameWithTag()) - }) - - t.Run("Parse valid image name with source name and registry info", func(t *testing.T) { - image := NewFromIdentifier("jannfis/orig-image=gcr.io/jannfis/test-image:0.1") - assert.Equal(t, "gcr.io", image.RegistryURL) - assert.Equal(t, "jannfis/orig-image", image.ImageAlias) - assert.Equal(t, "jannfis/test-image", image.ImageName) - require.NotNil(t, image.ImageTag) - assert.Equal(t, "0.1", image.ImageTag.TagName) - }) - - t.Run("Parse valid image name with source name and registry info with port", func(t *testing.T) { - image := NewFromIdentifier("ghcr.io:4567/jannfis/orig-image=gcr.io:1234/jannfis/test-image:0.1") - assert.Equal(t, "gcr.io:1234", image.RegistryURL) - assert.Equal(t, "ghcr.io:4567/jannfis/orig-image", image.ImageAlias) - assert.Equal(t, "jannfis/test-image", image.ImageName) - require.NotNil(t, image.ImageTag) - assert.Equal(t, "0.1", image.ImageTag.TagName) - }) - - t.Run("Parse image without version source name and registry info", func(t *testing.T) { - image := NewFromIdentifier("jannfis/orig-image=gcr.io/jannfis/test-image") - assert.Equal(t, "gcr.io", image.RegistryURL) - assert.Equal(t, "jannfis/orig-image", image.ImageAlias) - assert.Equal(t, "jannfis/test-image", image.ImageName) - assert.Nil(t, image.ImageTag) - }) - t.Run("#273 classic-web=registry:5000/classic-web", func(t *testing.T) { - image := NewFromIdentifier("classic-web=registry:5000/classic-web") - assert.Equal(t, "registry:5000", image.RegistryURL) - assert.Equal(t, "classic-web", image.ImageAlias) - assert.Equal(t, "classic-web", image.ImageName) - assert.Nil(t, image.ImageTag) - }) -} - -func Test_ImageToString(t *testing.T) { - t.Run("Get string representation of full-qualified image name", func(t *testing.T) { - imageName := "jannfis/argocd=jannfis/orig-image:0.1" - img := NewFromIdentifier(imageName) - assert.Equal(t, imageName, img.String()) - }) - t.Run("Get string representation of full-qualified image name with registry", func(t *testing.T) { - imageName := "jannfis/argocd=gcr.io/jannfis/orig-image:0.1" - img := NewFromIdentifier(imageName) - assert.Equal(t, imageName, img.String()) - }) - t.Run("Get string representation of full-qualified image name with registry", func(t *testing.T) { - imageName := "jannfis/argocd=gcr.io/jannfis/orig-image" - img := NewFromIdentifier(imageName) - assert.Equal(t, imageName, img.String()) - }) - t.Run("Get original value", func(t *testing.T) { - imageName := "invalid==foo" - img := NewFromIdentifier(imageName) - assert.Equal(t, imageName, img.Original()) - }) -} - -func Test_WithTag(t *testing.T) { - t.Run("Get string representation of full-qualified image name", func(t *testing.T) { - imageName := "jannfis/argocd=jannfis/orig-image:0.1" - nimageName := "jannfis/argocd=jannfis/orig-image:0.2" - oImg := NewFromIdentifier(imageName) - nImg := oImg.WithTag(tag.NewImageTag("0.2", time.Unix(0, 0), "")) - assert.Equal(t, nimageName, nImg.String()) - }) -} - -func Test_ContainerList(t *testing.T) { - t.Run("Test whether image is contained in list", func(t *testing.T) { - images := make(ContainerImageList, 0) - image_names := []string{"a/a:0.1", "a/b:1.2", "x/y=foo.bar/a/c:0.23"} - for _, n := range image_names { - images = append(images, NewFromIdentifier(n)) - } - withKustomizeOverride := NewFromIdentifier("k1/k2:k3") - withKustomizeOverride.KustomizeImage = images[0] - images = append(images, withKustomizeOverride) - - assert.NotNil(t, images.ContainsImage(NewFromIdentifier(image_names[0]), false)) - assert.NotNil(t, images.ContainsImage(NewFromIdentifier(image_names[1]), false)) - assert.NotNil(t, images.ContainsImage(NewFromIdentifier(image_names[2]), false)) - assert.Nil(t, images.ContainsImage(NewFromIdentifier("foo/bar"), false)) - - imageMatch := images.ContainsImage(withKustomizeOverride, false) - assert.Equal(t, images[0], imageMatch) - }) -} - -func Test_getImageDigestFromTag(t *testing.T) { - tagAndDigest := "test-tag@sha256:abcde" - tagName, tagDigest := getImageDigestFromTag(tagAndDigest) - assert.Equal(t, "test-tag", tagName) - assert.Equal(t, "sha256:abcde", tagDigest) - - tagAndDigest = "test-tag" - tagName, tagDigest = getImageDigestFromTag(tagAndDigest) - assert.Equal(t, "test-tag", tagName) - assert.Empty(t, tagDigest) -} - -func Test_ContainerImageList_String_Originals(t *testing.T) { - images := make(ContainerImageList, 0) - originals := []string{} - - assert.Equal(t, "", images.String()) - assert.True(t, slices.Equal(originals, images.Originals())) - - images = append(images, NewFromIdentifier("foo/bar:0.1")) - originals = append(originals, "foo/bar:0.1") - assert.Equal(t, "foo/bar:0.1", images.String()) - assert.True(t, slices.Equal(originals, images.Originals())) - - images = append(images, NewFromIdentifier("alias=foo/bar:0.2")) - originals = append(originals, "alias=foo/bar:0.2") - assert.Equal(t, "foo/bar:0.1,alias=foo/bar:0.2", images.String()) - assert.True(t, slices.Equal(originals, images.Originals())) -} - -func TestContainerImage_DiffersFrom(t *testing.T) { - foo1 := NewFromIdentifier("x/foo:1") - foo2 := NewFromIdentifier("x/foo:2") - bar1 := NewFromIdentifier("x/bar:1") - bar1WithRegistry := NewFromIdentifier("docker.io/x/bar:1") - - assert.False(t, foo1.DiffersFrom(foo1, true)) - assert.False(t, foo1.DiffersFrom(foo2, false)) - assert.True(t, foo1.DiffersFrom(foo2, true)) - - assert.True(t, foo1.DiffersFrom(bar1, false)) - assert.True(t, bar1.DiffersFrom(foo1, false)) - assert.True(t, foo1.DiffersFrom(bar1, true)) - assert.True(t, bar1.DiffersFrom(foo1, true)) - assert.True(t, bar1.DiffersFrom(bar1WithRegistry, false)) - - assert.False(t, foo1.IsUpdatable("0.1", "^1.0")) -} diff --git a/pkg/image/kustomize.go b/pkg/image/kustomize.go deleted file mode 100644 index ef7c88b4..00000000 --- a/pkg/image/kustomize.go +++ /dev/null @@ -1,39 +0,0 @@ -package image - -import ( - "strings" -) - -// Shamelessly ripped from ArgoCD CLI code - -type KustomizeImage string - -func (i KustomizeImage) delim() string { - for _, d := range []string{"=", ":", "@"} { - if strings.Contains(string(i), d) { - return d - } - } - return ":" -} - -// if the image name matches (i.e. up to the first delimiter) -func (i KustomizeImage) Match(j KustomizeImage) bool { - delim := j.delim() - if !strings.Contains(string(j), delim) { - return false - } - return strings.HasPrefix(string(i), strings.Split(string(j), delim)[0]) -} - -type KustomizeImages []KustomizeImage - -// find the image or -1 -func (images KustomizeImages) Find(image KustomizeImage) int { - for i, a := range images { - if a.Match(image) { - return i - } - } - return -1 -} diff --git a/pkg/image/kustomize_test.go b/pkg/image/kustomize_test.go deleted file mode 100644 index 98dede9c..00000000 --- a/pkg/image/kustomize_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package image - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func Test_KustomizeImages_Find(t *testing.T) { - images := KustomizeImages{ - "a/b:1.0", - "a/b@sha256:aabb", - "a/b:latest@sha256:aabb", - "x/y=busybox", - "x/y=foo.bar/a/c:0.23", - } - for _, image := range images { - assert.True(t, images.Find(image) >= 0) - } - for _, image := range []string{"a/b:2", "x/y=foo.bar"} { - assert.True(t, images.Find(KustomizeImage(image)) >= 0) - } - for _, image := range []string{"a/b", "x", "x/y"} { - assert.Equal(t, -1, images.Find(KustomizeImage(image))) - } -} diff --git a/pkg/image/matchfunc.go b/pkg/image/matchfunc.go deleted file mode 100644 index 036e6fb0..00000000 --- a/pkg/image/matchfunc.go +++ /dev/null @@ -1,27 +0,0 @@ -package image - -import ( - "regexp" - - "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/log" -) - -// MatchFuncAny matches any pattern, i.e. always returns true -func MatchFuncAny(tagName string, args interface{}) bool { - return true -} - -// MatchFuncNone matches no pattern, i.e. always returns false -func MatchFuncNone(tagName string, args interface{}) bool { - return false -} - -// MatchFuncRegexp matches the tagName against regexp pattern and returns the result -func MatchFuncRegexp(tagName string, args interface{}) bool { - pattern, ok := args.(*regexp.Regexp) - if !ok { - log.Errorf("args is not a RegExp") - return false - } - return pattern.Match([]byte(tagName)) -} diff --git a/pkg/image/matchfunc_test.go b/pkg/image/matchfunc_test.go deleted file mode 100644 index 11929b1d..00000000 --- a/pkg/image/matchfunc_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package image - -import ( - "regexp" - "testing" - - "github.com/stretchr/testify/assert" -) - -func Test_MatchFuncAny(t *testing.T) { - assert.True(t, MatchFuncAny("whatever", nil)) -} - -func Test_MatchFuncNone(t *testing.T) { - assert.False(t, MatchFuncNone("whatever", nil)) -} - -func Test_MatchFuncRegexp(t *testing.T) { - t.Run("Test with valid expression", func(t *testing.T) { - re := regexp.MustCompile("[a-z]+") - assert.True(t, MatchFuncRegexp("lemon", re)) - assert.False(t, MatchFuncRegexp("31337", re)) - }) - t.Run("Test with invalid type", func(t *testing.T) { - assert.False(t, MatchFuncRegexp("lemon", "[a-z]+")) - }) -} diff --git a/pkg/image/options.go b/pkg/image/options.go deleted file mode 100644 index ce2f5ca0..00000000 --- a/pkg/image/options.go +++ /dev/null @@ -1,296 +0,0 @@ -package image - -import ( - "fmt" - "regexp" - "runtime" - "strings" - - "github.com/argoproj-labs/argocd-image-updater/pkg/common" - "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/options" -) - -// GetParameterHelmImageName gets the value for image-name option for the image -// from a set of annotations -func (img *ContainerImage) GetParameterHelmImageName(annotations map[string]string) string { - key := fmt.Sprintf(common.HelmParamImageNameAnnotation, img.normalizedSymbolicName()) - val, ok := annotations[key] - if !ok { - return "" - } - return val -} - -// GetParameterHelmImageTag gets the value for image-tag option for the image -// from a set of annotations -func (img *ContainerImage) GetParameterHelmImageTag(annotations map[string]string) string { - key := fmt.Sprintf(common.HelmParamImageTagAnnotation, img.normalizedSymbolicName()) - val, ok := annotations[key] - if !ok { - return "" - } - return val -} - -// GetParameterHelmImageSpec gets the value for image-spec option for the image -// from a set of annotations -func (img *ContainerImage) GetParameterHelmImageSpec(annotations map[string]string) string { - key := fmt.Sprintf(common.HelmParamImageSpecAnnotation, img.normalizedSymbolicName()) - val, ok := annotations[key] - if !ok { - return "" - } - return val -} - -// GetParameterKustomizeImageName gets the value for image-spec option for the -// image from a set of annotations -func (img *ContainerImage) GetParameterKustomizeImageName(annotations map[string]string) string { - key := fmt.Sprintf(common.KustomizeApplicationNameAnnotation, img.normalizedSymbolicName()) - val, ok := annotations[key] - if !ok { - return "" - } - return val -} - -// HasForceUpdateOptionAnnotation gets the value for force-update option for the -// image from a set of annotations -func (img *ContainerImage) HasForceUpdateOptionAnnotation(annotations map[string]string) bool { - forceUpdateAnnotations := []string{ - fmt.Sprintf(common.ForceUpdateOptionAnnotation, img.normalizedSymbolicName()), - common.ApplicationWideForceUpdateOptionAnnotation, - } - var forceUpdateVal = "" - for _, key := range forceUpdateAnnotations { - if val, ok := annotations[key]; ok { - forceUpdateVal = val - break - } - } - return forceUpdateVal == "true" -} - -// GetParameterSort gets and validates the value for the sort option for the -// image from a set of annotations -func (img *ContainerImage) GetParameterUpdateStrategy(annotations map[string]string) UpdateStrategy { - updateStrategyAnnotations := []string{ - fmt.Sprintf(common.UpdateStrategyAnnotation, img.normalizedSymbolicName()), - common.ApplicationWideUpdateStrategyAnnotation, - } - var updateStrategyVal = "" - for _, key := range updateStrategyAnnotations { - if val, ok := annotations[key]; ok { - updateStrategyVal = val - break - } - } - logCtx := img.LogContext() - if updateStrategyVal == "" { - logCtx.Tracef("No sort option found") - // Default is sort by version - return StrategySemVer - } - logCtx.Tracef("Found update strategy %s", updateStrategyVal) - return img.ParseUpdateStrategy(updateStrategyVal) -} - -func (img *ContainerImage) ParseUpdateStrategy(val string) UpdateStrategy { - logCtx := img.LogContext() - switch strings.ToLower(val) { - case "semver": - return StrategySemVer - case "latest": - logCtx.Warnf("\"latest\" strategy has been renamed to \"newest-build\". Please switch to the new convention as support for the old naming convention will be removed in future versions.") - fallthrough - case "newest-build": - return StrategyNewestBuild - case "name": - logCtx.Warnf("\"name\" strategy has been renamed to \"alphabetical\". Please switch to the new convention as support for the old naming convention will be removed in future versions.") - fallthrough - case "alphabetical": - return StrategyAlphabetical - case "digest": - return StrategyDigest - default: - logCtx.Warnf("Unknown sort option %s -- using semver", val) - return StrategySemVer - } -} - -// GetParameterMatch returns the match function and pattern to use for matching -// tag names. If an invalid option is found, it returns MatchFuncNone as the -// default, to prevent accidental matches. -func (img *ContainerImage) GetParameterMatch(annotations map[string]string) (MatchFuncFn, interface{}) { - allowTagsAnnotations := []string{ - fmt.Sprintf(common.AllowTagsOptionAnnotation, img.normalizedSymbolicName()), - common.ApplicationWideAllowTagsOptionAnnotation, - } - var allowTagsVal = "" - for _, key := range allowTagsAnnotations { - if val, ok := annotations[key]; ok { - allowTagsVal = val - break - } - } - logCtx := img.LogContext() - if allowTagsVal == "" { - // The old match-tag annotation is deprecated and will be subject to removal - // in a future version. - key := fmt.Sprintf(common.OldMatchOptionAnnotation, img.normalizedSymbolicName()) - val, ok := annotations[key] - if ok { - logCtx.Warnf("The 'tag-match' annotation is deprecated and subject to removal. Please use 'allow-tags' annotation instead.") - allowTagsVal = val - } - } - if allowTagsVal == "" { - logCtx.Tracef("No match annotation found") - return MatchFuncAny, "" - } - return img.ParseMatchfunc(allowTagsVal) -} - -// ParseMatchfunc returns a matcher function and its argument from given value -func (img *ContainerImage) ParseMatchfunc(val string) (MatchFuncFn, interface{}) { - logCtx := img.LogContext() - - // The special value "any" doesn't take any parameter - if strings.ToLower(val) == "any" { - return MatchFuncAny, nil - } - - opt := strings.SplitN(val, ":", 2) - if len(opt) != 2 { - logCtx.Warnf("Invalid match option syntax '%s', ignoring", val) - return MatchFuncNone, nil - } - switch strings.ToLower(opt[0]) { - case "regexp": - re, err := regexp.Compile(opt[1]) - if err != nil { - logCtx.Warnf("Could not compile regexp '%s'", opt[1]) - return MatchFuncNone, nil - } - return MatchFuncRegexp, re - default: - logCtx.Warnf("Unknown match function: %s", opt[0]) - return MatchFuncNone, nil - } -} - -// GetParameterPullSecret retrieves an image's pull secret credentials -func (img *ContainerImage) GetParameterPullSecret(annotations map[string]string) *CredentialSource { - pullSecretAnnotations := []string{ - fmt.Sprintf(common.PullSecretAnnotation, img.normalizedSymbolicName()), - common.ApplicationWidePullSecretAnnotation, - } - var pullSecretVal = "" - for _, key := range pullSecretAnnotations { - if val, ok := annotations[key]; ok { - pullSecretVal = val - break - } - } - logCtx := img.LogContext() - if pullSecretVal == "" { - logCtx.Tracef("No pull-secret annotation found") - return nil - } - credSrc, err := ParseCredentialSource(pullSecretVal, false) - if err != nil { - logCtx.Warnf("Invalid credential reference specified: %s", pullSecretVal) - return nil - } - return credSrc -} - -// GetParameterIgnoreTags retrieves a list of tags to ignore from a comma-separated string -func (img *ContainerImage) GetParameterIgnoreTags(annotations map[string]string) []string { - ignoreTagsAnnotations := []string{ - fmt.Sprintf(common.IgnoreTagsOptionAnnotation, img.normalizedSymbolicName()), - common.ApplicationWideIgnoreTagsOptionAnnotation, - } - var ignoreTagsVal = "" - for _, key := range ignoreTagsAnnotations { - if val, ok := annotations[key]; ok { - ignoreTagsVal = val - break - } - } - logCtx := img.LogContext() - if ignoreTagsVal == "" { - logCtx.Tracef("No ignore-tags annotation found") - return nil - } - ignoreList := make([]string, 0) - tags := strings.Split(strings.TrimSpace(ignoreTagsVal), ",") - for _, tag := range tags { - // We ignore empty tags - trimmed := strings.TrimSpace(tag) - if trimmed != "" { - ignoreList = append(ignoreList, trimmed) - } - } - return ignoreList -} - -// GetPlatformOptions sets up platform constraints for an image. If no platform -// is specified in the annotations, we restrict the platform for images to the -// platform we're executed on unless unrestricted is set to true, in which case -// we do not setup a platform restriction if no platform annotation is found. -func (img *ContainerImage) GetPlatformOptions(annotations map[string]string, unrestricted bool) *options.ManifestOptions { - logCtx := img.LogContext() - var opts *options.ManifestOptions = options.NewManifestOptions() - key := fmt.Sprintf(common.PlatformsAnnotation, img.normalizedSymbolicName()) - val, ok := annotations[key] - if !ok { - if !unrestricted { - os := runtime.GOOS - arch := runtime.GOARCH - variant := "" - if strings.Contains(runtime.GOARCH, "/") { - a := strings.SplitN(runtime.GOARCH, "/", 2) - arch = a[0] - variant = a[1] - } - logCtx.Tracef("Using runtime platform constraint %s", options.PlatformKey(os, arch, variant)) - opts = opts.WithPlatform(os, arch, variant) - } - } else { - platforms := strings.Split(val, ",") - for _, ps := range platforms { - pt := strings.TrimSpace(ps) - os, arch, variant, err := ParsePlatform(pt) - if err != nil { - // If the platform identifier could not be parsed, we set the - // constraint intentionally to the invalid value so we don't - // end up updating to the wrong architecture possibly. - os = ps - logCtx.Warnf("could not parse platform identifier '%v': invalid format", pt) - } - logCtx.Tracef("Adding platform constraint %s", options.PlatformKey(os, arch, variant)) - opts = opts.WithPlatform(os, arch, variant) - } - } - - return opts -} - -func ParsePlatform(platformID string) (string, string, string, error) { - p := strings.SplitN(platformID, "/", 3) - if len(p) < 2 { - return "", "", "", fmt.Errorf("could not parse platform constraint '%s'", platformID) - } - os := p[0] - arch := p[1] - variant := "" - if len(p) == 3 { - variant = p[2] - } - return os, arch, variant, nil -} - -func (img *ContainerImage) normalizedSymbolicName() string { - return strings.ReplaceAll(img.ImageAlias, "/", "_") -} diff --git a/pkg/image/options_test.go b/pkg/image/options_test.go deleted file mode 100644 index 7f322443..00000000 --- a/pkg/image/options_test.go +++ /dev/null @@ -1,493 +0,0 @@ -package image - -import ( - "fmt" - "regexp" - "runtime" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/argoproj-labs/argocd-image-updater/pkg/common" - "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/options" -) - -func Test_GetHelmOptions(t *testing.T) { - t.Run("Get Helm parameter for configured application", func(t *testing.T) { - annotations := map[string]string{ - fmt.Sprintf(common.HelmParamImageNameAnnotation, "dummy"): "release.name", - fmt.Sprintf(common.HelmParamImageTagAnnotation, "dummy"): "release.tag", - fmt.Sprintf(common.HelmParamImageSpecAnnotation, "dummy"): "release.image", - } - - img := NewFromIdentifier("dummy=foo/bar:1.12") - paramName := img.GetParameterHelmImageName(annotations) - paramTag := img.GetParameterHelmImageTag(annotations) - paramSpec := img.GetParameterHelmImageSpec(annotations) - assert.Equal(t, "release.name", paramName) - assert.Equal(t, "release.tag", paramTag) - assert.Equal(t, "release.image", paramSpec) - }) - - t.Run("Get Helm parameter for non-configured application", func(t *testing.T) { - annotations := map[string]string{ - fmt.Sprintf(common.HelmParamImageNameAnnotation, "dummy"): "release.name", - fmt.Sprintf(common.HelmParamImageTagAnnotation, "dummy"): "release.tag", - fmt.Sprintf(common.HelmParamImageSpecAnnotation, "dummy"): "release.image", - } - - img := NewFromIdentifier("foo=foo/bar:1.12") - paramName := img.GetParameterHelmImageName(annotations) - paramTag := img.GetParameterHelmImageTag(annotations) - paramSpec := img.GetParameterHelmImageSpec(annotations) - assert.Equal(t, "", paramName) - assert.Equal(t, "", paramTag) - assert.Equal(t, "", paramSpec) - }) - - t.Run("Get Helm parameter for configured application with normalized name", func(t *testing.T) { - annotations := map[string]string{ - fmt.Sprintf(common.HelmParamImageNameAnnotation, "foo_dummy"): "release.name", - fmt.Sprintf(common.HelmParamImageTagAnnotation, "foo_dummy"): "release.tag", - fmt.Sprintf(common.HelmParamImageSpecAnnotation, "foo_dummy"): "release.image", - } - - img := NewFromIdentifier("foo/dummy=foo/bar:1.12") - paramName := img.GetParameterHelmImageName(annotations) - paramTag := img.GetParameterHelmImageTag(annotations) - paramSpec := img.GetParameterHelmImageSpec(annotations) - assert.Equal(t, "release.name", paramName) - assert.Equal(t, "release.tag", paramTag) - assert.Equal(t, "release.image", paramSpec) - }) -} - -func Test_GetKustomizeOptions(t *testing.T) { - t.Run("Get Kustomize parameter for configured application", func(t *testing.T) { - annotations := map[string]string{ - fmt.Sprintf(common.KustomizeApplicationNameAnnotation, "dummy"): "argoproj/argo-cd", - } - - img := NewFromIdentifier("dummy=foo/bar:1.12") - paramName := img.GetParameterKustomizeImageName(annotations) - assert.Equal(t, "argoproj/argo-cd", paramName) - - img = NewFromIdentifier("dummy2=foo2/bar2:1.12") - paramName = img.GetParameterKustomizeImageName(annotations) - assert.Equal(t, "", paramName) - }) -} - -func Test_GetSortOption(t *testing.T) { - t.Run("Get update strategy semver for configured application", func(t *testing.T) { - annotations := map[string]string{ - fmt.Sprintf(common.UpdateStrategyAnnotation, "dummy"): "semver", - } - img := NewFromIdentifier("dummy=foo/bar:1.12") - sortMode := img.GetParameterUpdateStrategy(annotations) - assert.Equal(t, StrategySemVer, sortMode) - }) - - t.Run("Use update strategy newest-build for configured application", func(t *testing.T) { - annotations := map[string]string{ - fmt.Sprintf(common.UpdateStrategyAnnotation, "dummy"): "newest-build", - } - img := NewFromIdentifier("dummy=foo/bar:1.12") - sortMode := img.GetParameterUpdateStrategy(annotations) - assert.Equal(t, StrategyNewestBuild, sortMode) - }) - - t.Run("Get update strategy date for configured application", func(t *testing.T) { - annotations := map[string]string{ - fmt.Sprintf(common.UpdateStrategyAnnotation, "dummy"): "latest", - } - img := NewFromIdentifier("dummy=foo/bar:1.12") - sortMode := img.GetParameterUpdateStrategy(annotations) - assert.Equal(t, StrategyNewestBuild, sortMode) - }) - - t.Run("Get update strategy name for configured application", func(t *testing.T) { - annotations := map[string]string{ - fmt.Sprintf(common.UpdateStrategyAnnotation, "dummy"): "name", - } - img := NewFromIdentifier("dummy=foo/bar:1.12") - sortMode := img.GetParameterUpdateStrategy(annotations) - assert.Equal(t, StrategyAlphabetical, sortMode) - }) - - t.Run("Use update strategy alphabetical for configured application", func(t *testing.T) { - annotations := map[string]string{ - fmt.Sprintf(common.UpdateStrategyAnnotation, "dummy"): "alphabetical", - } - img := NewFromIdentifier("dummy=foo/bar:1.12") - sortMode := img.GetParameterUpdateStrategy(annotations) - assert.Equal(t, StrategyAlphabetical, sortMode) - }) - - t.Run("Get update strategy option configured application because of invalid option", func(t *testing.T) { - annotations := map[string]string{ - fmt.Sprintf(common.UpdateStrategyAnnotation, "dummy"): "invalid", - } - img := NewFromIdentifier("dummy=foo/bar:1.12") - sortMode := img.GetParameterUpdateStrategy(annotations) - assert.Equal(t, StrategySemVer, sortMode) - }) - - t.Run("Get update strategy option configured application because of option not set", func(t *testing.T) { - annotations := map[string]string{} - img := NewFromIdentifier("dummy=foo/bar:1.12") - sortMode := img.GetParameterUpdateStrategy(annotations) - assert.Equal(t, StrategySemVer, sortMode) - }) - - t.Run("Prefer update strategy option from image-specific annotation", func(t *testing.T) { - annotations := map[string]string{ - fmt.Sprintf(common.UpdateStrategyAnnotation, "dummy"): "alphabetical", - common.ApplicationWideUpdateStrategyAnnotation: "newest-build", - } - img := NewFromIdentifier("dummy=foo/bar:1.12") - sortMode := img.GetParameterUpdateStrategy(annotations) - assert.Equal(t, StrategyAlphabetical, sortMode) - }) - - t.Run("Get update strategy option from application-wide annotation", func(t *testing.T) { - annotations := map[string]string{ - common.ApplicationWideUpdateStrategyAnnotation: "newest-build", - } - img := NewFromIdentifier("dummy=foo/bar:1.12") - sortMode := img.GetParameterUpdateStrategy(annotations) - assert.Equal(t, StrategyNewestBuild, sortMode) - }) - - t.Run("Get update strategy option digest from application-wide annotation", func(t *testing.T) { - annotations := map[string]string{ - common.ApplicationWideUpdateStrategyAnnotation: "digest", - } - img := NewFromIdentifier("dummy=foo/bar:1.12") - sortMode := img.GetParameterUpdateStrategy(annotations) - assert.Equal(t, StrategyDigest, sortMode) - }) -} - -func Test_GetMatchOption(t *testing.T) { - t.Run("Get regexp match option for configured application", func(t *testing.T) { - annotations := map[string]string{ - fmt.Sprintf(common.AllowTagsOptionAnnotation, "dummy"): "regexp:a-z", - } - img := NewFromIdentifier("dummy=foo/bar:1.12") - matchFunc, matchArgs := img.GetParameterMatch(annotations) - require.NotNil(t, matchFunc) - require.NotNil(t, matchArgs) - assert.IsType(t, ®exp.Regexp{}, matchArgs) - }) - - t.Run("Get regexp match option for configured application with invalid expression", func(t *testing.T) { - annotations := map[string]string{ - fmt.Sprintf(common.AllowTagsOptionAnnotation, "dummy"): `regexp:/foo\`, - } - img := NewFromIdentifier("dummy=foo/bar:1.12") - matchFunc, matchArgs := img.GetParameterMatch(annotations) - require.NotNil(t, matchFunc) - require.Nil(t, matchArgs) - }) - - t.Run("Get invalid match option for configured application", func(t *testing.T) { - annotations := map[string]string{ - fmt.Sprintf(common.AllowTagsOptionAnnotation, "dummy"): "invalid", - } - img := NewFromIdentifier("dummy=foo/bar:1.12") - matchFunc, matchArgs := img.GetParameterMatch(annotations) - require.NotNil(t, matchFunc) - require.Equal(t, false, matchFunc("", nil)) - assert.Nil(t, matchArgs) - }) - - t.Run("No match option for configured application", func(t *testing.T) { - annotations := map[string]string{} - img := NewFromIdentifier("dummy=foo/bar:1.12") - matchFunc, matchArgs := img.GetParameterMatch(annotations) - require.NotNil(t, matchFunc) - require.Equal(t, true, matchFunc("", nil)) - assert.Equal(t, "", matchArgs) - }) - - t.Run("Prefer match option from image-specific annotation", func(t *testing.T) { - annotations := map[string]string{ - fmt.Sprintf(common.AllowTagsOptionAnnotation, "dummy"): "regexp:^[0-9]", - common.ApplicationWideAllowTagsOptionAnnotation: "regexp:^v", - } - img := NewFromIdentifier("dummy=foo/bar:1.12") - matchFunc, matchArgs := img.GetParameterMatch(annotations) - require.NotNil(t, matchFunc) - require.NotNil(t, matchArgs) - assert.IsType(t, ®exp.Regexp{}, matchArgs) - assert.True(t, matchFunc("0.0.1", matchArgs)) - assert.False(t, matchFunc("v0.0.1", matchArgs)) - }) - - t.Run("Get match option from application-wide annotation", func(t *testing.T) { - annotations := map[string]string{ - common.ApplicationWideAllowTagsOptionAnnotation: "regexp:^v", - } - img := NewFromIdentifier("dummy=foo/bar:1.12") - matchFunc, matchArgs := img.GetParameterMatch(annotations) - require.NotNil(t, matchFunc) - require.NotNil(t, matchArgs) - assert.IsType(t, ®exp.Regexp{}, matchArgs) - assert.False(t, matchFunc("0.0.1", matchArgs)) - assert.True(t, matchFunc("v0.0.1", matchArgs)) - }) -} - -func Test_GetSecretOption(t *testing.T) { - t.Run("Get cred source from annotation", func(t *testing.T) { - annotations := map[string]string{ - fmt.Sprintf(common.PullSecretAnnotation, "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.PullSecretAnnotation, "dummy"): "foo/bar", - } - img := NewFromIdentifier("dummy=foo/bar:1.12") - credSrc := img.GetParameterPullSecret(annotations) - require.Nil(t, credSrc) - }) - - t.Run("Missing pull secret in annotation", func(t *testing.T) { - annotations := map[string]string{} - img := NewFromIdentifier("dummy=foo/bar:1.12") - credSrc := img.GetParameterPullSecret(annotations) - require.Nil(t, credSrc) - }) - - t.Run("Prefer cred source from image-specific annotation", func(t *testing.T) { - annotations := map[string]string{ - fmt.Sprintf(common.PullSecretAnnotation, "dummy"): "pullsecret:image/specific", - common.ApplicationWidePullSecretAnnotation: "pullsecret:app/wide", - } - img := NewFromIdentifier("dummy=foo/bar:1.12") - credSrc := img.GetParameterPullSecret(annotations) - require.NotNil(t, credSrc) - assert.Equal(t, CredentialSourcePullSecret, credSrc.Type) - assert.Equal(t, "image", credSrc.SecretNamespace) - assert.Equal(t, "specific", credSrc.SecretName) - assert.Equal(t, ".dockerconfigjson", credSrc.SecretField) - }) - - t.Run("Get cred source from application-wide annotation", func(t *testing.T) { - annotations := map[string]string{ - common.ApplicationWidePullSecretAnnotation: "pullsecret:app/wide", - } - img := NewFromIdentifier("dummy=foo/bar:1.12") - credSrc := img.GetParameterPullSecret(annotations) - require.NotNil(t, credSrc) - assert.Equal(t, CredentialSourcePullSecret, credSrc.Type) - assert.Equal(t, "app", credSrc.SecretNamespace) - assert.Equal(t, "wide", credSrc.SecretName) - assert.Equal(t, ".dockerconfigjson", credSrc.SecretField) - }) -} - -func Test_GetIgnoreTags(t *testing.T) { - t.Run("Get list of tags to ignore from image-specific annotation", func(t *testing.T) { - annotations := map[string]string{ - fmt.Sprintf(common.IgnoreTagsOptionAnnotation, "dummy"): "tag1, ,tag2, tag3 , tag4", - } - img := NewFromIdentifier("dummy=foo/bar:1.12") - tags := img.GetParameterIgnoreTags(annotations) - require.Len(t, tags, 4) - assert.Equal(t, "tag1", tags[0]) - assert.Equal(t, "tag2", tags[1]) - assert.Equal(t, "tag3", tags[2]) - assert.Equal(t, "tag4", tags[3]) - }) - - t.Run("No tags to ignore from image-specific annotation", func(t *testing.T) { - annotations := map[string]string{} - img := NewFromIdentifier("dummy=foo/bar:1.12") - tags := img.GetParameterIgnoreTags(annotations) - require.Nil(t, tags) - }) - - t.Run("Prefer list of tags to ignore from image-specific annotation", func(t *testing.T) { - annotations := map[string]string{ - fmt.Sprintf(common.IgnoreTagsOptionAnnotation, "dummy"): "tag1, tag2", - common.ApplicationWideIgnoreTagsOptionAnnotation: "tag3, tag4", - } - img := NewFromIdentifier("dummy=foo/bar:1.12") - tags := img.GetParameterIgnoreTags(annotations) - require.Len(t, tags, 2) - assert.Equal(t, "tag1", tags[0]) - assert.Equal(t, "tag2", tags[1]) - }) - - t.Run("Get list of tags to ignore from application-wide annotation", func(t *testing.T) { - annotations := map[string]string{ - common.ApplicationWideIgnoreTagsOptionAnnotation: "tag3, tag4", - } - img := NewFromIdentifier("dummy=foo/bar:1.12") - tags := img.GetParameterIgnoreTags(annotations) - require.Len(t, tags, 2) - assert.Equal(t, "tag3", tags[0]) - assert.Equal(t, "tag4", tags[1]) - }) -} - -func Test_HasForceUpdateOptionAnnotation(t *testing.T) { - t.Run("Get force-update option from image-specific annotation", func(t *testing.T) { - annotations := map[string]string{ - fmt.Sprintf(common.ForceUpdateOptionAnnotation, "dummy"): "true", - } - img := NewFromIdentifier("dummy=foo/bar:1.12") - forceUpdate := img.HasForceUpdateOptionAnnotation(annotations) - assert.True(t, forceUpdate) - }) - - t.Run("Prefer force-update option from image-specific annotation", func(t *testing.T) { - annotations := map[string]string{ - fmt.Sprintf(common.ForceUpdateOptionAnnotation, "dummy"): "true", - common.ApplicationWideForceUpdateOptionAnnotation: "false", - } - img := NewFromIdentifier("dummy=foo/bar:1.12") - forceUpdate := img.HasForceUpdateOptionAnnotation(annotations) - assert.True(t, forceUpdate) - }) - - t.Run("Get force-update option from application-wide annotation", func(t *testing.T) { - annotations := map[string]string{ - common.ApplicationWideForceUpdateOptionAnnotation: "false", - } - img := NewFromIdentifier("dummy=foo/bar:1.12") - forceUpdate := img.HasForceUpdateOptionAnnotation(annotations) - assert.False(t, forceUpdate) - }) -} - -func Test_GetPlatformOptions(t *testing.T) { - t.Run("Empty platform options with restriction", func(t *testing.T) { - annotations := map[string]string{} - img := NewFromIdentifier("dummy=foo/bar:1.12") - opts := img.GetPlatformOptions(annotations, false) - os := runtime.GOOS - arch := runtime.GOARCH - platform := opts.Platforms()[0] - slashCount := strings.Count(platform, "/") - if slashCount == 1 { - assert.True(t, opts.WantsPlatform(os, arch, "")) - assert.True(t, opts.WantsPlatform(os, arch, "invalid")) - } else if slashCount == 2 { - assert.False(t, opts.WantsPlatform(os, arch, "")) - assert.False(t, opts.WantsPlatform(os, arch, "invalid")) - } else { - t.Fatal("invalid platform options ", platform) - } - }) - t.Run("Empty platform options without restriction", func(t *testing.T) { - annotations := map[string]string{} - img := NewFromIdentifier("dummy=foo/bar:1.12") - opts := img.GetPlatformOptions(annotations, true) - os := runtime.GOOS - arch := runtime.GOARCH - assert.True(t, opts.WantsPlatform(os, arch, "")) - assert.True(t, opts.WantsPlatform(os, arch, "invalid")) - assert.True(t, opts.WantsPlatform("windows", "amd64", "")) - }) - t.Run("Single platform without variant requested", func(t *testing.T) { - os := "linux" - arch := "arm64" - variant := "v8" - annotations := map[string]string{ - fmt.Sprintf(common.PlatformsAnnotation, "dummy"): options.PlatformKey(os, arch, variant), - } - img := NewFromIdentifier("dummy=foo/bar:1.12") - opts := img.GetPlatformOptions(annotations, false) - assert.True(t, opts.WantsPlatform(os, arch, variant)) - assert.False(t, opts.WantsPlatform(os, arch, "invalid")) - }) - t.Run("Single platform with variant requested", func(t *testing.T) { - os := "linux" - arch := "arm" - variant := "v6" - annotations := map[string]string{ - fmt.Sprintf(common.PlatformsAnnotation, "dummy"): options.PlatformKey(os, arch, variant), - } - img := NewFromIdentifier("dummy=foo/bar:1.12") - opts := img.GetPlatformOptions(annotations, false) - assert.True(t, opts.WantsPlatform(os, arch, variant)) - assert.False(t, opts.WantsPlatform(os, arch, "")) - assert.False(t, opts.WantsPlatform(runtime.GOOS, runtime.GOARCH, "")) - assert.False(t, opts.WantsPlatform(runtime.GOOS, runtime.GOARCH, variant)) - }) - t.Run("Multiple platforms requested", func(t *testing.T) { - os := "linux" - arch := "arm" - variant := "v6" - annotations := map[string]string{ - fmt.Sprintf(common.PlatformsAnnotation, "dummy"): options.PlatformKey(os, arch, variant) + ", " + options.PlatformKey(runtime.GOOS, runtime.GOARCH, ""), - } - img := NewFromIdentifier("dummy=foo/bar:1.12") - opts := img.GetPlatformOptions(annotations, false) - assert.True(t, opts.WantsPlatform(os, arch, variant)) - assert.True(t, opts.WantsPlatform(runtime.GOOS, runtime.GOARCH, "")) - assert.False(t, opts.WantsPlatform(os, arch, "")) - assert.True(t, opts.WantsPlatform(runtime.GOOS, runtime.GOARCH, variant)) - }) - t.Run("Invalid platform requested", func(t *testing.T) { - os := "linux" - arch := "arm" - variant := "v6" - annotations := map[string]string{ - fmt.Sprintf(common.PlatformsAnnotation, "dummy"): "invalid", - } - img := NewFromIdentifier("dummy=foo/bar:1.12") - opts := img.GetPlatformOptions(annotations, false) - assert.False(t, opts.WantsPlatform(os, arch, variant)) - assert.False(t, opts.WantsPlatform(runtime.GOOS, runtime.GOARCH, "")) - assert.False(t, opts.WantsPlatform(os, arch, "")) - assert.False(t, opts.WantsPlatform(runtime.GOOS, runtime.GOARCH, variant)) - }) -} - -func Test_ContainerImage_ParseMatchfunc(t *testing.T) { - img := NewFromIdentifier("dummy=foo/bar:1.12") - matchFunc, pattern := img.ParseMatchfunc("any") - assert.True(t, matchFunc("MatchFuncAny any tag name", pattern)) - assert.Nil(t, pattern) - - matchFunc, pattern = img.ParseMatchfunc("ANY") - assert.True(t, matchFunc("MatchFuncAny any tag name", pattern)) - assert.Nil(t, pattern) - - matchFunc, pattern = img.ParseMatchfunc("other") - assert.False(t, matchFunc("MatchFuncNone any tag name", pattern)) - assert.Nil(t, pattern) - - matchFunc, pattern = img.ParseMatchfunc("not-regexp:a-z") - assert.False(t, matchFunc("MatchFuncNone any tag name", pattern)) - assert.Nil(t, pattern) - - matchFunc, pattern = img.ParseMatchfunc("regexp:[aA-zZ]") - assert.True(t, matchFunc("MatchFuncRegexp-tag-name", pattern)) - compiledRegexp, _ := regexp.Compile("[aA-zZ]") - assert.Equal(t, compiledRegexp, pattern) - - matchFunc, pattern = img.ParseMatchfunc("RegExp:[aA-zZ]") - assert.True(t, matchFunc("MatchFuncRegexp-tag-name", pattern)) - compiledRegexp, _ = regexp.Compile("[aA-zZ]") - assert.Equal(t, compiledRegexp, pattern) - - matchFunc, pattern = img.ParseMatchfunc("regexp:[aA-zZ") //invalid regexp: missing end ] - assert.False(t, matchFunc("MatchFuncNone-tag-name", pattern)) - assert.Nil(t, pattern) -} diff --git a/pkg/image/version.go b/pkg/image/version.go deleted file mode 100644 index 97437bd5..00000000 --- a/pkg/image/version.go +++ /dev/null @@ -1,220 +0,0 @@ -package image - -import ( - "path/filepath" - - "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/log" - "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/options" - "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/tag" - - "github.com/Masterminds/semver/v3" -) - -// VersionSortMode defines the method to sort a list of tags -type UpdateStrategy int - -const ( - // VersionSortSemVer sorts tags using semver sorting (the default) - StrategySemVer UpdateStrategy = 0 - // VersionSortLatest sorts tags after their creation date - StrategyNewestBuild UpdateStrategy = 1 - // VersionSortName sorts tags alphabetically by name - StrategyAlphabetical UpdateStrategy = 2 - // VersionSortDigest uses latest digest of an image - StrategyDigest UpdateStrategy = 3 -) - -func (us UpdateStrategy) String() string { - switch us { - case StrategySemVer: - return "semver" - case StrategyNewestBuild: - return "newest-build" - case StrategyAlphabetical: - return "alphabetical" - case StrategyDigest: - return "digest" - } - - return "unknown" -} - -// ConstraintMatchMode defines how the constraint should be matched -type ConstraintMatchMode int - -const ( - // ConstraintMatchSemVer uses semver to match a constraint - ConstraintMatchSemver ConstraintMatchMode = 0 - // ConstraintMatchRegExp uses regexp to match a constraint - ConstraintMatchRegExp ConstraintMatchMode = 1 - // ConstraintMatchNone does not enforce a constraint - ConstraintMatchNone ConstraintMatchMode = 2 -) - -// VersionConstraint defines a constraint for comparing versions -type VersionConstraint struct { - Constraint string - MatchFunc MatchFuncFn - MatchArgs interface{} - IgnoreList []string - Strategy UpdateStrategy - Options *options.ManifestOptions -} - -type MatchFuncFn func(tagName string, pattern interface{}) bool - -// String returns the string representation of VersionConstraint -func (vc *VersionConstraint) String() string { - return vc.Constraint -} - -func NewVersionConstraint() *VersionConstraint { - return &VersionConstraint{ - MatchFunc: MatchFuncNone, - Strategy: StrategySemVer, - Options: options.NewManifestOptions(), - } -} - -// GetNewestVersionFromTags returns the latest available version from a list of -// tags while optionally taking a semver constraint into account. Returns the -// original version if no new version could be found from the list of tags. -func (img *ContainerImage) GetNewestVersionFromTags(vc *VersionConstraint, tagList *tag.ImageTagList) (*tag.ImageTag, error) { - logCtx := log.NewContext() - logCtx.AddField("image", img.String()) - - var availableTags tag.SortableImageTagList - switch vc.Strategy { - case StrategySemVer: - availableTags = tagList.SortBySemVer() - case StrategyAlphabetical: - availableTags = tagList.SortAlphabetically() - case StrategyNewestBuild: - availableTags = tagList.SortByDate() - case StrategyDigest: - availableTags = tagList.SortAlphabetically() - } - - considerTags := tag.SortableImageTagList{} - - // It makes no sense to proceed if we have no available tags - if len(availableTags) == 0 { - return img.ImageTag, nil - } - - // The given constraint MUST match a semver constraint - var semverConstraint *semver.Constraints - var err error - if vc.Strategy == StrategySemVer { - // TODO: Shall we really ensure a valid semver on the current tag? - // This prevents updating from a non-semver tag currently. - if img.ImageTag != nil && img.ImageTag.TagName != "" { - _, err := semver.NewVersion(img.ImageTag.TagName) - if err != nil { - return nil, err - } - } - - if vc.Constraint != "" { - if vc.Strategy == StrategySemVer { - semverConstraint, err = semver.NewConstraint(vc.Constraint) - if err != nil { - logCtx.Errorf("invalid constraint '%s' given: '%v'", vc, err) - return nil, err - } - } - } - } - - // Loop through all tags to check whether it's an update candidate. - for _, tag := range availableTags { - logCtx.Tracef("Finding out whether to consider %s for being updateable", tag.TagName) - - if vc.Strategy == StrategySemVer { - // Non-parseable tag does not mean error - just skip it - ver, err := semver.NewVersion(tag.TagName) - if err != nil { - logCtx.Tracef("Not a valid version: %s", tag.TagName) - continue - } - - // If we have a version constraint, check image tag against it. If the - // constraint is not satisfied, skip tag. - if semverConstraint != nil { - if !semverConstraint.Check(ver) { - logCtx.Tracef("%s did not match constraint %s", ver.Original(), vc.Constraint) - continue - } - } - } else if vc.Strategy == StrategyDigest { - if tag.TagName != vc.Constraint { - logCtx.Tracef("%s did not match contraint %s", tag.TagName, vc.Constraint) - continue - } - } - - // Append tag as update candidate - considerTags = append(considerTags, tag) - } - - logCtx.Debugf("found %d from %d tags eligible for consideration", len(considerTags), len(availableTags)) - - // If we found tags to consider, return the most recent tag found according - // to the update strategy. - if len(considerTags) > 0 { - return considerTags[len(considerTags)-1], nil - } - - return nil, nil -} - -// IsTagIgnored matches tag against the patterns in IgnoreList and returns true if one of them matches -func (vc *VersionConstraint) IsTagIgnored(tag string) bool { - for _, t := range vc.IgnoreList { - if match, err := filepath.Match(t, tag); err == nil && match { - log.Tracef("tag %s is ignored by pattern %s", tag, t) - return true - } - } - return false -} - -// IsCacheable returns true if we can safely cache tags for strategy s -func (s UpdateStrategy) IsCacheable() bool { - switch s { - case StrategyDigest: - return false - default: - return true - } -} - -// NeedsMetadata returns true if strategy s requires image metadata to work correctly -func (s UpdateStrategy) NeedsMetadata() bool { - switch s { - case StrategyNewestBuild: - return true - default: - return false - } -} - -// NeedsVersionConstraint returns true if strategy s requires a version constraint to be defined -func (s UpdateStrategy) NeedsVersionConstraint() bool { - switch s { - case StrategyDigest: - return true - default: - return false - } -} - -// WantsOnlyConstraintTag returns true if strategy s only wants to inspect the tag specified by the constraint -func (s UpdateStrategy) WantsOnlyConstraintTag() bool { - switch s { - case StrategyDigest: - return true - default: - return false - } -} diff --git a/pkg/image/version_test.go b/pkg/image/version_test.go deleted file mode 100644 index 4c1fe2cd..00000000 --- a/pkg/image/version_test.go +++ /dev/null @@ -1,196 +0,0 @@ -package image - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/options" - "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/tag" -) - -func newImageTagList(tagNames []string) *tag.ImageTagList { - tagList := tag.NewImageTagList() - for _, tagName := range tagNames { - tagList.Add(tag.NewImageTag(tagName, time.Unix(0, 0), "")) - } - return tagList -} - -func newImageTagListWithDate(tagNames []string) *tag.ImageTagList { - tagList := tag.NewImageTagList() - for i, t := range tagNames { - tagList.Add(tag.NewImageTag(t, time.Unix(int64(i*5), 0), "")) - } - return tagList -} - -func Test_LatestVersion(t *testing.T) { - t.Run("Find the latest version without any constraint", func(t *testing.T) { - tagList := newImageTagList([]string{"0.1", "0.5.1", "0.9", "1.0", "1.0.1", "1.1.2", "2.0.3"}) - img := NewFromIdentifier("jannfis/test:1.0") - vc := VersionConstraint{} - newTag, err := img.GetNewestVersionFromTags(&vc, tagList) - require.NoError(t, err) - require.NotNil(t, newTag) - assert.Equal(t, "2.0.3", newTag.TagName) - }) - - t.Run("Find the latest version with a semver constraint on major", func(t *testing.T) { - tagList := newImageTagList([]string{"0.1", "0.5.1", "0.9", "1.0", "1.0.1", "1.1.2", "2.0.3"}) - img := NewFromIdentifier("jannfis/test:1.0") - vc := VersionConstraint{Constraint: "^1.0"} - newTag, err := img.GetNewestVersionFromTags(&vc, tagList) - require.NoError(t, err) - require.NotNil(t, newTag) - assert.Equal(t, "1.1.2", newTag.TagName) - }) - - t.Run("Find the latest version with a semver constraint on patch", func(t *testing.T) { - tagList := newImageTagList([]string{"0.1", "0.5.1", "0.9", "1.0", "1.0.1", "1.1.2", "2.0.3"}) - img := NewFromIdentifier("jannfis/test:1.0") - vc := VersionConstraint{Constraint: "~1.0"} - newTag, err := img.GetNewestVersionFromTags(&vc, tagList) - require.NoError(t, err) - require.NotNil(t, newTag) - assert.Equal(t, "1.0.1", newTag.TagName) - }) - - t.Run("Find the latest version with a semver constraint that has no match", func(t *testing.T) { - tagList := newImageTagList([]string{"0.1", "0.5.1", "0.9", "2.0.3"}) - img := NewFromIdentifier("jannfis/test:1.0") - vc := VersionConstraint{Constraint: "~1.0"} - newTag, err := img.GetNewestVersionFromTags(&vc, tagList) - require.NoError(t, err) - require.Nil(t, newTag) - }) - - t.Run("Find the latest version with a semver constraint that is invalid", func(t *testing.T) { - tagList := newImageTagList([]string{"0.1", "0.5.1", "0.9", "2.0.3"}) - img := NewFromIdentifier("jannfis/test:1.0") - vc := VersionConstraint{Constraint: "latest"} - newTag, err := img.GetNewestVersionFromTags(&vc, tagList) - assert.Error(t, err) - assert.Nil(t, newTag) - }) - - t.Run("Find the latest version with no tags", func(t *testing.T) { - tagList := newImageTagList([]string{}) - img := NewFromIdentifier("jannfis/test:1.0") - vc := VersionConstraint{Constraint: "~1.0"} - newTag, err := img.GetNewestVersionFromTags(&vc, tagList) - require.NoError(t, err) - require.NotNil(t, newTag) - assert.Equal(t, "1.0", newTag.TagName) - }) - - t.Run("Find the latest version using latest sortmode", func(t *testing.T) { - tagList := newImageTagListWithDate([]string{"zz", "bb", "yy", "cc", "yy", "aa", "ll"}) - img := NewFromIdentifier("jannfis/test:bb") - vc := VersionConstraint{Strategy: StrategyNewestBuild} - newTag, err := img.GetNewestVersionFromTags(&vc, tagList) - require.NoError(t, err) - require.NotNil(t, newTag) - assert.Equal(t, "ll", newTag.TagName) - }) - - t.Run("Find the latest version using latest sortmode, invalid tags", func(t *testing.T) { - tagList := newImageTagListWithDate([]string{"zz", "bb", "yy", "cc", "yy", "aa", "ll"}) - img := NewFromIdentifier("jannfis/test:bb") - vc := VersionConstraint{Strategy: StrategySemVer} - newTag, err := img.GetNewestVersionFromTags(&vc, tagList) - require.NoError(t, err) - require.NotNil(t, newTag) - assert.Equal(t, "bb", newTag.TagName) - }) - - t.Run("Find the latest version using VersionConstraint StrategyAlphabetical", func(t *testing.T) { - tagList := newImageTagListWithDate([]string{"zz", "bb", "yy", "cc", "yy", "aa", "ll"}) - img := NewFromIdentifier("jannfis/test:bb") - vc := VersionConstraint{Strategy: StrategyAlphabetical} - newTag, err := img.GetNewestVersionFromTags(&vc, tagList) - require.NoError(t, err) - require.NotNil(t, newTag) - assert.Equal(t, "zz", newTag.TagName) - }) - - t.Run("Find the latest version using VersionConstraint StrategyDigest", func(t *testing.T) { - tagList := tag.NewImageTagList() - newDigest := "latest@sha:abcdefg" - tagList.Add(tag.NewImageTag("latest", time.Unix(int64(6), 0), newDigest)) - img := NewFromIdentifier("jannfis/test:latest@sha:1234567") - vc := VersionConstraint{Strategy: StrategyDigest, Constraint: "latest"} - newTag, err := img.GetNewestVersionFromTags(&vc, tagList) - require.NoError(t, err) - assert.Equal(t, "latest", newTag.TagName) - assert.Equal(t, newDigest, newTag.TagDigest) - }) - -} - -func Test_UpdateStrategy_String(t *testing.T) { - tests := []struct { - name string - us UpdateStrategy - want string - }{ - {"StrategySemVer", StrategySemVer, "semver"}, - {"StrategyNewestBuild", StrategyNewestBuild, "newest-build"}, - {"StrategyAlphabetical", StrategyAlphabetical, "alphabetical"}, - {"StrategyDigest", StrategyDigest, "digest"}, - {"unknown", UpdateStrategy(-1), "unknown"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, tt.us.String()) - }) - } -} - -func Test_NewVersionConstraint(t *testing.T) { - constraint := NewVersionConstraint() - assert.Equal(t, StrategySemVer, constraint.Strategy) - assert.Equal(t, options.NewManifestOptions(), constraint.Options) - assert.False(t, constraint.MatchFunc("", "")) -} - -func Test_VersionConstraint_IsTagIgnored(t *testing.T) { - versionConstraint := VersionConstraint{IgnoreList: []string{"tag1", "tag2"}} - assert.True(t, versionConstraint.IsTagIgnored("tag1")) - assert.True(t, versionConstraint.IsTagIgnored("tag2")) - assert.False(t, versionConstraint.IsTagIgnored("tag3")) - versionConstraint.IgnoreList = []string{"tag?", "foo"} - assert.True(t, versionConstraint.IsTagIgnored("tag1")) - assert.True(t, versionConstraint.IsTagIgnored("foo")) - assert.False(t, versionConstraint.IsTagIgnored("tag10")) -} - -func Test_UpdateStrategy_IsCacheable(t *testing.T) { - assert.True(t, StrategySemVer.IsCacheable()) - assert.True(t, StrategyNewestBuild.IsCacheable()) - assert.True(t, StrategyAlphabetical.IsCacheable()) - assert.False(t, StrategyDigest.IsCacheable()) -} - -func Test_UpdateStrategy_NeedsMetadata(t *testing.T) { - assert.False(t, StrategySemVer.NeedsMetadata()) - assert.True(t, StrategyNewestBuild.NeedsMetadata()) - assert.False(t, StrategyAlphabetical.NeedsMetadata()) - assert.False(t, StrategyDigest.NeedsMetadata()) -} - -func Test_UpdateStrategy_NeedsVersionConstraint(t *testing.T) { - assert.False(t, StrategySemVer.NeedsVersionConstraint()) - assert.False(t, StrategyNewestBuild.NeedsVersionConstraint()) - assert.False(t, StrategyAlphabetical.NeedsVersionConstraint()) - assert.True(t, StrategyDigest.NeedsVersionConstraint()) -} - -func Test_UpdateStrategy_WantsOnlyConstraintTag(t *testing.T) { - assert.False(t, StrategySemVer.WantsOnlyConstraintTag()) - assert.False(t, StrategyNewestBuild.WantsOnlyConstraintTag()) - assert.False(t, StrategyAlphabetical.WantsOnlyConstraintTag()) - assert.True(t, StrategyDigest.WantsOnlyConstraintTag()) -} diff --git a/pkg/kube/kubernetes.go b/pkg/kube/kubernetes.go index 11583314..cb8c7f84 100644 --- a/pkg/kube/kubernetes.go +++ b/pkg/kube/kubernetes.go @@ -5,10 +5,9 @@ package kube import ( "context" "fmt" - "os" "time" - "github.com/argoproj-labs/argocd-image-updater/pkg/metrics" + kube "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/kube" appv1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned" @@ -16,88 +15,22 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" _ "k8s.io/client-go/plugin/pkg/client/auth" - "k8s.io/client-go/tools/clientcmd" ) -type KubernetesClient struct { - Clientset kubernetes.Interface +type ImageUpdaterKubernetesClient struct { ApplicationsClientset versioned.Interface - Context context.Context - Namespace string + KubeClient *kube.KubernetesClient } -func NewKubernetesClient(ctx context.Context, client kubernetes.Interface, applicationsClientset versioned.Interface, namespace string) *KubernetesClient { - kc := &KubernetesClient{} - kc.Context = ctx - kc.Clientset = client +func NewKubernetesClient(ctx context.Context, client kubernetes.Interface, applicationsClientset versioned.Interface, namespace string) *ImageUpdaterKubernetesClient { + kc := &ImageUpdaterKubernetesClient{} + kc.KubeClient = kube.NewKubernetesClient(ctx, client, namespace) kc.ApplicationsClientset = applicationsClientset - kc.Namespace = namespace return kc } -// NewKubernetesClient creates a new Kubernetes client object from given -// configuration file. If configuration file is the empty string, in-cluster -// client will be created. -func NewKubernetesClientFromConfig(ctx context.Context, namespace string, kubeconfig string) (*KubernetesClient, error) { - loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() - loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig - loadingRules.ExplicitPath = kubeconfig - overrides := clientcmd.ConfigOverrides{} - clientConfig := clientcmd.NewInteractiveDeferredLoadingClientConfig(loadingRules, &overrides, os.Stdin) - - config, err := clientConfig.ClientConfig() - if err != nil { - return nil, err - } - - if namespace == "" { - namespace, _, err = clientConfig.Namespace() - if err != nil { - return nil, err - } - } - - clientset, err := kubernetes.NewForConfig(config) - if err != nil { - return nil, err - } - - applicationsClientset, err := versioned.NewForConfig(config) - if err != nil { - return nil, err - } - - return NewKubernetesClient(ctx, clientset, applicationsClientset, namespace), nil -} - -// GetSecretData returns the raw data from named K8s secret in given namespace -func (client *KubernetesClient) GetSecretData(namespace string, secretName string) (map[string][]byte, error) { - secret, err := client.Clientset.CoreV1().Secrets(namespace).Get(client.Context, secretName, metav1.GetOptions{}) - metrics.Clients().IncreaseK8sClientRequest(1) - if err != nil { - metrics.Clients().IncreaseK8sClientRequest(1) - return nil, err - } - return secret.Data, nil -} - -// GetSecretField returns the value of a field from named K8s secret in given namespace -func (client *KubernetesClient) GetSecretField(namespace string, secretName string, field string) (string, error) { - secret, err := client.GetSecretData(namespace, secretName) - metrics.Clients().IncreaseK8sClientRequest(1) - if err != nil { - metrics.Clients().IncreaseK8sClientRequest(1) - return "", err - } - if data, ok := secret[field]; !ok { - return "", fmt.Errorf("secret '%s/%s' does not have a field '%s'", namespace, secretName, field) - } else { - return string(data), nil - } -} - // CreateApplicationEvent creates a kubernetes event with a custom reason and message for an application. -func (client *KubernetesClient) CreateApplicationEvent(app *appv1alpha1.Application, reason string, message string, annotations map[string]string) (*v1.Event, error) { +func (client *ImageUpdaterKubernetesClient) CreateApplicationEvent(app *appv1alpha1.Application, reason string, message string, annotations map[string]string) (*v1.Event, error) { t := metav1.Time{Time: time.Now()} event := v1.Event{ @@ -125,7 +58,7 @@ func (client *KubernetesClient) CreateApplicationEvent(app *appv1alpha1.Applicat Reason: reason, } - result, err := client.Clientset.CoreV1().Events(app.ObjectMeta.Namespace).Create(client.Context, &event, metav1.CreateOptions{}) + result, err := client.KubeClient.Clientset.CoreV1().Events(app.ObjectMeta.Namespace).Create(client.KubeClient.Context, &event, metav1.CreateOptions{}) if err != nil { return nil, err } diff --git a/pkg/kube/kubernetes_test.go b/pkg/kube/kubernetes_test.go index 35d69223..d1b2fae9 100644 --- a/pkg/kube/kubernetes_test.go +++ b/pkg/kube/kubernetes_test.go @@ -7,6 +7,7 @@ import ( appv1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + registryKube "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/kube" "github.com/argoproj-labs/argocd-image-updater/test/fake" "github.com/argoproj-labs/argocd-image-updater/test/fixture" @@ -16,14 +17,14 @@ import ( func Test_NewKubernetesClient(t *testing.T) { t.Run("Get new K8s client for remote cluster instance", func(t *testing.T) { - client, err := NewKubernetesClientFromConfig(context.TODO(), "", "../../test/testdata/kubernetes/config") + client, err := registryKube.NewKubernetesClientFromConfig(context.TODO(), "", "../../test/testdata/kubernetes/config") require.NoError(t, err) assert.NotNil(t, client) assert.Equal(t, "default", client.Namespace) }) t.Run("Get new K8s client for remote cluster instance specified namespace", func(t *testing.T) { - client, err := NewKubernetesClientFromConfig(context.TODO(), "argocd", "../../test/testdata/kubernetes/config") + client, err := registryKube.NewKubernetesClientFromConfig(context.TODO(), "argocd", "../../test/testdata/kubernetes/config") require.NoError(t, err) assert.NotNil(t, client) assert.Equal(t, "argocd", client.Namespace) @@ -34,8 +35,8 @@ func Test_GetDataFromSecrets(t *testing.T) { t.Run("Get all data from dummy secret", func(t *testing.T) { secret := fixture.MustCreateSecretFromFile("../../test/testdata/resources/dummy-secret.json") clientset := fake.NewFakeClientsetWithResources(secret) - client := &KubernetesClient{Clientset: clientset} - data, err := client.GetSecretData("test-namespace", "test-secret") + client := &ImageUpdaterKubernetesClient{KubeClient: ®istryKube.KubernetesClient{Clientset: clientset}} + data, err := client.KubeClient.GetSecretData("test-namespace", "test-secret") require.NoError(t, err) require.NotNil(t, data) assert.Len(t, data, 1) @@ -45,8 +46,8 @@ func Test_GetDataFromSecrets(t *testing.T) { t.Run("Get string data from dummy secret existing field", func(t *testing.T) { secret := fixture.MustCreateSecretFromFile("../../test/testdata/resources/dummy-secret.json") clientset := fake.NewFakeClientsetWithResources(secret) - client := &KubernetesClient{Clientset: clientset} - data, err := client.GetSecretField("test-namespace", "test-secret", "namespace") + client := &ImageUpdaterKubernetesClient{KubeClient: ®istryKube.KubernetesClient{Clientset: clientset}} + data, err := client.KubeClient.GetSecretField("test-namespace", "test-secret", "namespace") require.NoError(t, err) assert.Equal(t, "argocd", data) }) @@ -54,8 +55,8 @@ func Test_GetDataFromSecrets(t *testing.T) { t.Run("Get string data from dummy secret non-existing field", func(t *testing.T) { secret := fixture.MustCreateSecretFromFile("../../test/testdata/resources/dummy-secret.json") clientset := fake.NewFakeClientsetWithResources(secret) - client := &KubernetesClient{Clientset: clientset} - data, err := client.GetSecretField("test-namespace", "test-secret", "nonexisting") + client := &ImageUpdaterKubernetesClient{KubeClient: ®istryKube.KubernetesClient{Clientset: clientset}} + data, err := client.KubeClient.GetSecretField("test-namespace", "test-secret", "nonexisting") require.Error(t, err) require.Empty(t, data) }) @@ -63,8 +64,8 @@ func Test_GetDataFromSecrets(t *testing.T) { t.Run("Get string data from non-existing secret non-existing field", func(t *testing.T) { secret := fixture.MustCreateSecretFromFile("../../test/testdata/resources/dummy-secret.json") clientset := fake.NewFakeClientsetWithResources(secret) - client := &KubernetesClient{Clientset: clientset} - data, err := client.GetSecretField("test-namespace", "test", "namespace") + client := &ImageUpdaterKubernetesClient{KubeClient: ®istryKube.KubernetesClient{Clientset: clientset}} + data, err := client.KubeClient.GetSecretField("test-namespace", "test", "namespace") require.Error(t, err) require.Empty(t, data) }) @@ -88,7 +89,7 @@ func Test_CreateApplicationEvent(t *testing.T) { "origin": "nginx:1.12.2", } clientset := fake.NewFakeClientsetWithResources() - client := &KubernetesClient{Clientset: clientset, Namespace: "default"} + client := &ImageUpdaterKubernetesClient{KubeClient: ®istryKube.KubernetesClient{Clientset: clientset, Namespace: "default"}} event, err := client.CreateApplicationEvent(application, "TestEvent", "test-message", annotations) require.NoError(t, err) require.NotNil(t, event) diff --git a/pkg/registry/client.go b/pkg/registry/client.go deleted file mode 100644 index 3bad7ec5..00000000 --- a/pkg/registry/client.go +++ /dev/null @@ -1,449 +0,0 @@ -package registry - -import ( - "context" - "crypto/sha256" - "fmt" - - "github.com/argoproj/pkg/json" - - "github.com/argoproj-labs/argocd-image-updater/pkg/metrics" - "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/log" - "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/options" - "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/tag" - - "github.com/distribution/distribution/v3" - "github.com/distribution/distribution/v3/manifest/manifestlist" - "github.com/distribution/distribution/v3/manifest/ocischema" - "github.com/distribution/distribution/v3/manifest/schema1" //nolint:staticcheck - "github.com/distribution/distribution/v3/manifest/schema2" - "github.com/distribution/distribution/v3/reference" - "github.com/distribution/distribution/v3/registry/client" - "github.com/distribution/distribution/v3/registry/client/auth" - "github.com/distribution/distribution/v3/registry/client/auth/challenge" - "github.com/distribution/distribution/v3/registry/client/transport" - - "github.com/opencontainers/go-digest" - ociv1 "github.com/opencontainers/image-spec/specs-go/v1" - - "go.uber.org/ratelimit" - - "net/http" - "net/url" - "strings" - "time" -) - -// TODO: Check image's architecture and OS - -// knownMediaTypes is the list of media types we can process -var knownMediaTypes = []string{ - ocischema.SchemaVersion.MediaType, - schema1.MediaTypeSignedManifest, //nolint:staticcheck - schema2.SchemaVersion.MediaType, - manifestlist.SchemaVersion.MediaType, - ociv1.MediaTypeImageIndex, -} - -// RegistryClient defines the methods we need for querying container registries -type RegistryClient interface { - NewRepository(nameInRepository string) error - Tags() ([]string, error) - ManifestForTag(tagStr string) (distribution.Manifest, error) - ManifestForDigest(dgst digest.Digest) (distribution.Manifest, error) - TagMetadata(manifest distribution.Manifest, opts *options.ManifestOptions) (*tag.TagInfo, error) -} - -type NewRegistryClient func(*RegistryEndpoint, string, string) (RegistryClient, error) - -// Helper type for registry clients -type registryClient struct { - regClient distribution.Repository - endpoint *RegistryEndpoint - creds credentials -} - -// credentials is an implementation of distribution/V3/session struct -// to manage registry credentials and token -type credentials struct { - username string - password string - refreshTokens map[string]string -} - -func (c credentials) Basic(url *url.URL) (string, string) { - return c.username, c.password -} - -func (c credentials) RefreshToken(url *url.URL, service string) string { - return c.refreshTokens[service] -} - -func (c credentials) SetRefreshToken(realm *url.URL, service, token string) { - if c.refreshTokens != nil { - c.refreshTokens[service] = token - } -} - -// rateLimitTransport encapsulates our custom HTTP round tripper with rate -// limiter from the endpoint. -type rateLimitTransport struct { - limiter ratelimit.Limiter - transport http.RoundTripper - endpoint *RegistryEndpoint -} - -// RoundTrip is a custom RoundTrip method with rate-limiter -func (rlt *rateLimitTransport) RoundTrip(r *http.Request) (*http.Response, error) { - rlt.limiter.Take() - log.Tracef("Performing HTTP %s %s", r.Method, r.URL) - resp, err := rlt.transport.RoundTrip(r) - metrics.Endpoint().IncreaseRequest(rlt.endpoint.RegistryAPI, err != nil) - return resp, err -} - -// NewRepository is a wrapper for creating a registry client that is possibly -// rate-limited by using a custom HTTP round tripper method. -func (clt *registryClient) NewRepository(nameInRepository string) error { - urlToCall := strings.TrimSuffix(clt.endpoint.RegistryAPI, "/") - challengeManager1 := challenge.NewSimpleManager() - _, err := ping(challengeManager1, clt.endpoint, "") - if err != nil { - return err - } - - authTransport := transport.NewTransport( - clt.endpoint.GetTransport(), auth.NewAuthorizer( - challengeManager1, - auth.NewTokenHandler(clt.endpoint.GetTransport(), clt.creds, nameInRepository, "pull"), - auth.NewBasicHandler(clt.creds))) - - rlt := &rateLimitTransport{ - limiter: clt.endpoint.Limiter, - transport: authTransport, - endpoint: clt.endpoint, - } - - named, err := reference.WithName(nameInRepository) - if err != nil { - return err - } - clt.regClient, err = client.NewRepository(named, urlToCall, rlt) - if err != nil { - return err - } - return nil -} - -// NewClient returns a new RegistryClient for the given endpoint information -func NewClient(endpoint *RegistryEndpoint, username, password string) (RegistryClient, error) { - if username == "" && endpoint.Username != "" { - username = endpoint.Username - } - if password == "" && endpoint.Password != "" { - password = endpoint.Password - } - creds := credentials{ - username: username, - password: password, - } - return ®istryClient{ - creds: creds, - endpoint: endpoint, - }, nil -} - -// Tags returns a list of tags for given name in repository -func (clt *registryClient) Tags() ([]string, error) { - tagService := clt.regClient.Tags(context.Background()) - tTags, err := tagService.All(context.Background()) - if err != nil { - return nil, err - } - return tTags, nil -} - -// Manifest returns a Manifest for a given tag in repository -func (clt *registryClient) ManifestForTag(tagStr string) (distribution.Manifest, error) { - manService, err := clt.regClient.Manifests(context.Background()) - if err != nil { - return nil, err - } - manifest, err := manService.Get( - context.Background(), - digest.FromString(tagStr), - distribution.WithTag(tagStr), distribution.WithManifestMediaTypes(knownMediaTypes)) - if err != nil { - return nil, err - } - return manifest, nil -} - -// ManifestForDigest returns a Manifest for a given digest in repository -func (clt *registryClient) ManifestForDigest(dgst digest.Digest) (distribution.Manifest, error) { - manService, err := clt.regClient.Manifests(context.Background()) - if err != nil { - return nil, err - } - manifest, err := manService.Get( - context.Background(), - dgst, - distribution.WithManifestMediaTypes(knownMediaTypes)) - if err != nil { - return nil, err - } - return manifest, nil -} - -// TagMetadata retrieves metadata for a given manifest of given repository -func (client *registryClient) TagMetadata(manifest distribution.Manifest, opts *options.ManifestOptions) (*tag.TagInfo, error) { - ti := &tag.TagInfo{} - logCtx := opts.Logger() - var info struct { - Arch string `json:"architecture"` - Created string `json:"created"` - OS string `json:"os"` - Variant string `json:"variant"` - } - - // We support the following types of manifests as returned by the registry: - // - // V1 (legacy, might go away), V2 and OCI - // - // Also ManifestLists (e.g. on multi-arch images) are supported. - // - switch deserialized := manifest.(type) { - - case *schema1.SignedManifest: //nolint:staticcheck - var man schema1.Manifest = deserialized.Manifest //nolint:staticcheck - if len(man.History) == 0 { - return nil, fmt.Errorf("no history information found in schema V1") - } - - _, mBytes, err := manifest.Payload() - if err != nil { - return nil, err - } - ti.Digest = sha256.Sum256(mBytes) - - logCtx.Tracef("v1 SHA digest is %s", ti.EncodedDigest()) - if err := json.Unmarshal([]byte(man.History[0].V1Compatibility), &info); err != nil { - return nil, err - } - if !opts.WantsPlatform(info.OS, info.Arch, "") { - logCtx.Debugf("ignoring v1 manifest %v. Manifest platform: %s, requested: %s", - ti.EncodedDigest(), options.PlatformKey(info.OS, info.Arch, info.Variant), strings.Join(opts.Platforms(), ",")) - return nil, nil - } - if createdAt, err := time.Parse(time.RFC3339Nano, info.Created); err != nil { - return nil, err - } else { - ti.CreatedAt = createdAt - } - return ti, nil - - case *manifestlist.DeserializedManifestList: - var list manifestlist.DeserializedManifestList = *deserialized - - // List must contain at least one image manifest - if len(list.Manifests) == 0 { - return nil, fmt.Errorf("empty manifestlist not supported") - } - - // We use the SHA from the manifest list to let the container engine - // decide which image to pull, in case of multi-arch clusters. - _, mBytes, err := list.Payload() - if err != nil { - return nil, fmt.Errorf("could not retrieve manifestlist payload: %v", err) - } - ti.Digest = sha256.Sum256(mBytes) - - logCtx.Tracef("SHA256 of manifest parent is %v", ti.EncodedDigest()) - - return TagInfoFromReferences(client, opts, logCtx, ti, list.References()) - - case *ocischema.DeserializedImageIndex: - var index ocischema.DeserializedImageIndex = *deserialized - - // Index must contain at least one image manifest - if len(index.Manifests) == 0 { - return nil, fmt.Errorf("empty index not supported") - } - - // We use the SHA from the manifest index to let the container engine - // decide which image to pull, in case of multi-arch clusters. - _, mBytes, err := index.Payload() - if err != nil { - return nil, fmt.Errorf("could not retrieve index payload: %v", err) - } - ti.Digest = sha256.Sum256(mBytes) - - logCtx.Tracef("SHA256 of manifest parent is %v", ti.EncodedDigest()) - - return TagInfoFromReferences(client, opts, logCtx, ti, index.References()) - - case *schema2.DeserializedManifest: - var man schema2.Manifest = deserialized.Manifest - - logCtx.Tracef("Manifest digest is %v", man.Config.Digest.Encoded()) - - _, mBytes, err := manifest.Payload() - if err != nil { - return nil, err - } - ti.Digest = sha256.Sum256(mBytes) - logCtx.Tracef("v2 SHA digest is %s", ti.EncodedDigest()) - - // The data we require from a V2 manifest is in a blob that we need to - // fetch from the registry. - blobReader, err := client.regClient.Blobs(context.Background()).Get(context.Background(), man.Config.Digest) - if err != nil { - return nil, err - } - - if err := json.Unmarshal(blobReader, &info); err != nil { - return nil, err - } - - if !opts.WantsPlatform(info.OS, info.Arch, info.Variant) { - logCtx.Debugf("ignoring v2 manifest %v. Manifest platform: %s, requested: %s", - ti.EncodedDigest(), options.PlatformKey(info.OS, info.Arch, info.Variant), strings.Join(opts.Platforms(), ",")) - return nil, nil - } - - if ti.CreatedAt, err = time.Parse(time.RFC3339Nano, info.Created); err != nil { - return nil, err - } - - return ti, nil - case *ocischema.DeserializedManifest: - var man ocischema.Manifest = deserialized.Manifest - - _, mBytes, err := manifest.Payload() - if err != nil { - return nil, err - } - ti.Digest = sha256.Sum256(mBytes) - logCtx.Tracef("OCI SHA digest is %s", ti.EncodedDigest()) - - // The data we require from a V2 manifest is in a blob that we need to - // fetch from the registry. - blobReader, err := client.regClient.Blobs(context.Background()).Get(context.Background(), man.Config.Digest) - if err != nil { - return nil, err - } - - if err := json.Unmarshal(blobReader, &info); err != nil { - return nil, err - } - - if !opts.WantsPlatform(info.OS, info.Arch, info.Variant) { - logCtx.Debugf("ignoring OCI manifest %v. Manifest platform: %s, requested: %s", - ti.EncodedDigest(), options.PlatformKey(info.OS, info.Arch, info.Variant), strings.Join(opts.Platforms(), ",")) - return nil, nil - } - - if ti.CreatedAt, err = time.Parse(time.RFC3339Nano, info.Created); err != nil { - return nil, err - } - - return ti, nil - default: - return nil, fmt.Errorf("invalid manifest type %T", manifest) - } -} - -// TagInfoFromReferences is a helper method to retrieve metadata for a given -// list of references. It will return the most recent pushed manifest from the -// list of references. -func TagInfoFromReferences(client *registryClient, opts *options.ManifestOptions, logCtx *log.LogContext, ti *tag.TagInfo, references []distribution.Descriptor) (*tag.TagInfo, error) { - var ml []distribution.Descriptor - platforms := []string{} - - for _, ref := range references { - var refOS, refArch, refVariant string - if ref.Platform != nil { - refOS = ref.Platform.OS - refArch = ref.Platform.Architecture - refVariant = ref.Platform.Variant - } - platform1 := options.PlatformKey(refOS, refArch, refVariant) - platforms = append(platforms, platform1) - logCtx.Tracef("Found %s", platform1) - if !opts.WantsPlatform(refOS, refArch, refVariant) { - logCtx.Tracef("Ignoring referenced manifest %v because platform %s does not match any of: %s", - ref.Digest, - platform1, - strings.Join(opts.Platforms(), ",")) - continue - } - ml = append(ml, ref) - } - - // We need at least one reference that matches requested platforms - if len(ml) == 0 { - logCtx.Debugf("Manifest list did not contain any usable reference. Platforms requested: (%s), platforms included: (%s)", - strings.Join(opts.Platforms(), ","), strings.Join(platforms, ",")) - return nil, nil - } - - // For some strategies, we do not need to fetch metadata for further - // processing. - if !opts.WantsMetadata() { - return ti, nil - } - - // Loop through all referenced manifests to get their metadata. We only - // consider manifests for platforms we are interested in. - for _, ref := range ml { - logCtx.Tracef("Inspecting metadata of reference: %v", ref.Digest) - - man, err := client.ManifestForDigest(ref.Digest) - if err != nil { - return nil, fmt.Errorf("could not fetch manifest %v: %v", ref.Digest, err) - } - - cti, err := client.TagMetadata(man, opts) - if err != nil { - return nil, fmt.Errorf("could not fetch metadata for manifest %v: %v", ref.Digest, err) - } - - // We save the timestamp of the most recent pushed manifest for any - // given reference, if the metadata for the tag was correctly - // retrieved. This is important for the latest update strategy to - // be able to handle multi-arch images. The latest strategy will - // consider the most recent reference from an image index. - if cti != nil { - if cti.CreatedAt.After(ti.CreatedAt) { - ti.CreatedAt = cti.CreatedAt - } - } else { - logCtx.Warnf("returned metadata for manifest %v is nil, this should not happen.", ref.Digest) - continue - } - } - - return ti, nil -} - -// Implementation of ping method to initialize the challenge list -// Without this, tokenHandler and AuthorizationHandler won't work -func ping(manager challenge.Manager, endpoint *RegistryEndpoint, versionHeader string) ([]auth.APIVersion, error) { - httpc := &http.Client{Transport: endpoint.GetTransport()} - url := endpoint.RegistryAPI + "/v2/" - resp, err := httpc.Get(url) - if err != nil { - return nil, err - } - defer resp.Body.Close() - // Let's consider only HTTP 200 and 401 valid responses for the initial request - if resp.StatusCode != 200 && resp.StatusCode != 401 { - return nil, fmt.Errorf("endpoint %s does not seem to be a valid v2 Docker Registry API (received HTTP code %d for GET %s)", endpoint.RegistryAPI, resp.StatusCode, url) - } - - if err := manager.AddResponse(resp); err != nil { - return nil, err - } - - return auth.APIVersions(resp, versionHeader), err -} diff --git a/pkg/registry/client_test.go b/pkg/registry/client_test.go deleted file mode 100644 index 6d76512f..00000000 --- a/pkg/registry/client_test.go +++ /dev/null @@ -1,609 +0,0 @@ -package registry - -import ( - "errors" - "fmt" - "net/http" - "net/http/httptest" - "net/url" - "testing" - "time" - - "github.com/distribution/distribution/v3/manifest" - "github.com/distribution/distribution/v3/manifest/manifestlist" - "github.com/distribution/distribution/v3/manifest/ocischema" - "github.com/distribution/distribution/v3/manifest/schema2" - - "github.com/distribution/distribution/v3" - "github.com/distribution/distribution/v3/manifest/schema1" //nolint:staticcheck - v1 "github.com/opencontainers/image-spec/specs-go/v1" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/argoproj-labs/argocd-image-updater/pkg/registry/mocks" - "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/log" - "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/options" - "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/tag" -) - -func TestBasic(t *testing.T) { - creds := credentials{ - username: "testuser", - password: "testpass", - } - - testURL, _ := url.Parse("https://example.com") - username, password := creds.Basic(testURL) - - if username != "testuser" { - t.Errorf("Expected username to be 'testuser', got '%s'", username) - } - if password != "testpass" { - t.Errorf("Expected password to be 'testpass', got '%s'", password) - } -} - -func TestNewRepository(t *testing.T) { - t.Run("Invalid Reference Format", func(t *testing.T) { - ep, err := GetRegistryEndpoint("") - require.NoError(t, err) - client, err := NewClient(ep, "", "") - require.NoError(t, err) - err = client.NewRepository("test@test") - require.Error(t, err) - assert.Contains(t, "invalid reference format", err.Error()) - - }) - t.Run("Success Ping", func(t *testing.T) { - ep, err := GetRegistryEndpoint("") - require.NoError(t, err) - client, err := NewClient(ep, "", "") - require.NoError(t, err) - err = client.NewRepository("test/test") - require.NoError(t, err) - }) - - t.Run("Fail Ping", func(t *testing.T) { - testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - })) - ep := &RegistryEndpoint{RegistryAPI: testServer.URL} - client, err := NewClient(ep, "", "") - require.NoError(t, err) - err = client.NewRepository("") - require.Error(t, err) - }) - -} - -func TestRoundTrip_Success(t *testing.T) { - // Create mocks - mockLimiter := new(mocks.Limiter) - mockTransport := new(mocks.RoundTripper) - endpoint := &RegistryEndpoint{RegistryAPI: "http://example.com"} - // Create an instance of rateLimitTransport with mocks - rlt := &rateLimitTransport{ - limiter: mockLimiter, - transport: mockTransport, - endpoint: endpoint, - } - // Create a sample HTTP request - req, err := http.NewRequest("GET", "http://example.com", nil) - assert.NoError(t, err) - resp := &http.Response{StatusCode: http.StatusOK} - // Set up expectations - mockLimiter.On("Take").Return(time.Now()) - mockTransport.On("RoundTrip", req).Return(resp, nil) - // Call the method under test - actualResp, err := rlt.RoundTrip(req) - // Assert the expectations - mockLimiter.AssertExpectations(t) - mockTransport.AssertExpectations(t) - assert.NoError(t, err) - assert.Equal(t, resp, actualResp) -} -func TestRoundTrip_Failure(t *testing.T) { - // Create mocks - mockLimiter := new(mocks.Limiter) - mockTransport := new(mocks.RoundTripper) - endpoint := &RegistryEndpoint{RegistryAPI: "http://example.com"} - // Create an instance of rateLimitTransport with mocks - rlt := &rateLimitTransport{ - limiter: mockLimiter, - transport: mockTransport, - endpoint: endpoint, - } - // Create a sample HTTP request - req := httptest.NewRequest("GET", "http://example.com", nil) - // Set up expectations - mockLimiter.On("Take").Return(time.Now()) - mockTransport.On("RoundTrip", req).Return(nil, errors.New("Error on caling func RoundTrip")) - // Call the method under test - actualResp, err := rlt.RoundTrip(req) - // Assert the expectations - mockLimiter.AssertExpectations(t) - mockTransport.AssertExpectations(t) - assert.Error(t, err) - assert.Nil(t, actualResp) -} - -func TestRefreshToken(t *testing.T) { - creds := credentials{ - refreshTokens: map[string]string{ - "service1": "token1", - }, - } - testURL, _ := url.Parse("https://example.com") - token := creds.RefreshToken(testURL, "service1") - if token != "token1" { - t.Errorf("Expected token to be 'token1', got '%s'", token) - } -} - -func TestSetRefreshToken(t *testing.T) { - creds := credentials{ - refreshTokens: make(map[string]string), - } - testURL, _ := url.Parse("https://example.com") - creds.SetRefreshToken(testURL, "service1", "token1") - - if token, exists := creds.refreshTokens["service1"]; !exists { - t.Error("Expected token for 'service1' to exist") - } else if token != "token1" { - t.Errorf("Expected token to be 'token1', got '%s'", token) - } -} -func TestNewClient(t *testing.T) { - t.Run("Create client with provided username and password", func(t *testing.T) { - ep, err := GetRegistryEndpoint("") - require.NoError(t, err) - _, err = NewClient(ep, "testuser", "pass") - require.NoError(t, err) - }) - t.Run("Create client with empty username and password", func(t *testing.T) { - ep := &RegistryEndpoint{Username: "testuser", Password: "pass"} - _, err := NewClient(ep, "", "") - require.NoError(t, err) - }) -} - -func TestTags(t *testing.T) { - t.Run("success", func(t *testing.T) { - mockRegClient := new(mocks.Repository) - client := registryClient{ - regClient: mockRegClient, - } - mockTagService := new(mocks.TagService) - mockTagService.On("All", mock.Anything).Return([]string{"testTag-1", "testTag-2"}, nil) - mockRegClient.On("Tags", mock.Anything).Return(mockTagService) - tags, err := client.Tags() - require.NoError(t, err) - assert.Contains(t, tags, "testTag-1") - assert.Contains(t, tags, "testTag-2") - }) - t.Run("Fail", func(t *testing.T) { - mockRegClient := new(mocks.Repository) - client := registryClient{ - regClient: mockRegClient, - } - mockTagService := new(mocks.TagService) - mockTagService.On("All", mock.Anything).Return([]string{}, errors.New("Error on caling func All")) - mockRegClient.On("Tags", mock.Anything).Return(mockTagService) - _, err := client.Tags() - require.Error(t, err) - }) -} - -func TestManifestForTag(t *testing.T) { - t.Run("Successful retrieval of Manifest", func(t *testing.T) { - mockRegClient := new(mocks.Repository) - client := registryClient{ - regClient: mockRegClient, - } - manService := new(mocks.ManifestService) - manService.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) - mockRegClient.On("Manifests", mock.Anything).Return(manService, nil) - _, err := client.ManifestForTag("tagStr") - require.NoError(t, err) - }) - t.Run("Error returned from Manifests call", func(t *testing.T) { - mockRegClient := new(mocks.Repository) - client := registryClient{ - regClient: mockRegClient, - } - manService := new(mocks.ManifestService) - manService.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) - mockRegClient.On("Manifests", mock.Anything).Return(manService, errors.New("Error on caling func Manifests")) - _, err := client.ManifestForTag("tagStr") - require.Error(t, err) - }) - - t.Run("Error returned from Get call", func(t *testing.T) { - mockRegClient := new(mocks.Repository) - client := registryClient{ - regClient: mockRegClient, - } - manService := new(mocks.ManifestService) - manService.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("Error on caling func Get")) - mockRegClient.On("Manifests", mock.Anything).Return(manService, nil) - _, err := client.ManifestForTag("tagStr") - require.Error(t, err) - }) - -} - -func TestManifestForDigest(t *testing.T) { - t.Run("Successful retrieval of manifest", func(t *testing.T) { - mockRegClient := new(mocks.Repository) - client := registryClient{ - regClient: mockRegClient, - } - manService := new(mocks.ManifestService) - manService.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) - mockRegClient.On("Manifests", mock.Anything).Return(manService, nil) - _, err := client.ManifestForDigest("dgst") - require.NoError(t, err) - }) - t.Run("Error returned from Manifests call", func(t *testing.T) { - mockRegClient := new(mocks.Repository) - client := registryClient{ - regClient: mockRegClient, - } - manService := new(mocks.ManifestService) - manService.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) - mockRegClient.On("Manifests", mock.Anything).Return(manService, errors.New("Error on caling func Manifests")) - _, err := client.ManifestForDigest("dgst") - require.Error(t, err) - }) - t.Run("Error returned from Get call", func(t *testing.T) { - mockRegClient := new(mocks.Repository) - client := registryClient{ - regClient: mockRegClient, - } - manService := new(mocks.ManifestService) - manService.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("Error on caling func Get")) - mockRegClient.On("Manifests", mock.Anything).Return(manService, nil) - _, err := client.ManifestForDigest("dgst") - require.Error(t, err) - }) -} - -func TestTagInfoFromReferences(t *testing.T) { - t.Run("No usable reference in manifest list", func(t *testing.T) { - mockRegClient := new(mocks.Repository) - client := registryClient{ - regClient: mockRegClient, - } - tagInfo := &tag.TagInfo{} - tagInfo.CreatedAt = time.Now() - tagInfo.Digest = [32]byte{} - opts := &options.ManifestOptions{} - opts.WithPlatform("testOS", "testArch", "testVarient") - opts.WithLogger(log.NewContext()) - opts.WithMetadata(true) - descriptor := []distribution.Descriptor{ - { - MediaType: "", - Digest: "", - Size: 0, - Platform: &v1.Platform{ - Architecture: "mTestArch", - OS: "mTestOS", - OSVersion: "mTestOSVersion", - OSFeatures: []string{}, - Variant: "mTestVarient", - }, - }, - } - tag, err := TagInfoFromReferences(&client, opts, log.NewContext(), tagInfo, descriptor) - require.Nil(t, tag) - require.NoError(t, err) - }) - t.Run("Return tagInfo when metadata option is false", func(t *testing.T) { - mockRegClient := new(mocks.Repository) - client := registryClient{ - regClient: mockRegClient, - } - tagInfo := &tag.TagInfo{} - tagInfo.CreatedAt = time.Now() - tagInfo.Digest = [32]byte{} - opts := &options.ManifestOptions{} - opts.WithMetadata(false) - opts.WithPlatform("testOS", "testArch", "testVarient") - opts.WithLogger(log.NewContext()) - descriptor := []distribution.Descriptor{ - { - MediaType: "", - Digest: "", - Size: 0, - Platform: &v1.Platform{ - Architecture: "testArch", - OS: "testOS", - OSVersion: "testOSVersion", - OSFeatures: []string{}, - Variant: "testVarient", - }, - }, - } - tag, err := TagInfoFromReferences(&client, opts, log.NewContext(), tagInfo, descriptor) - require.NoError(t, err) - assert.Equal(t, tag, tagInfo) - require.NoError(t, err) - }) - t.Run("Return error from ManifestForDigest", func(t *testing.T) { - mockRegClient := new(mocks.Repository) - client := registryClient{ - regClient: mockRegClient, - } - tagInfo := &tag.TagInfo{} - tagInfo.CreatedAt = time.Now() - tagInfo.Digest = [32]byte{} - opts := &options.ManifestOptions{} - opts.WithMetadata(true) - opts.WithPlatform("testOS", "testArch", "testVarient") - opts.WithLogger(log.NewContext()) - descriptor := []distribution.Descriptor{ - { - MediaType: "", - Digest: "", - Size: 0, - Platform: &v1.Platform{ - Architecture: "testArch", - OS: "testOS", - OSVersion: "testOSVersion", - OSFeatures: []string{}, - Variant: "testVarient", - }, - }, - } - mockRegClient.On("Manifests", mock.Anything).Return(nil, errors.New("Error from Manifests")) - _, err := TagInfoFromReferences(&client, opts, log.NewContext(), tagInfo, descriptor) - require.Error(t, err) - }) - t.Run("Return error from TagMetadata", func(t *testing.T) { - mockRegClient := new(mocks.Repository) - client := registryClient{ - regClient: mockRegClient, - } - tagInfo := &tag.TagInfo{} - tagInfo.CreatedAt = time.Now() - tagInfo.Digest = [32]byte{} - opts := &options.ManifestOptions{} - opts.WithMetadata(true) - opts.WithPlatform("testOS", "testArch", "testVarient") - opts.WithLogger(log.NewContext()) - descriptor := []distribution.Descriptor{ - { - MediaType: "", - Digest: "", - Size: 0, - Platform: &v1.Platform{ - Architecture: "testArch", - OS: "testOS", - OSVersion: "testOSVersion", - OSFeatures: []string{}, - Variant: "testVarient", - }, - }, - } - manService := new(mocks.ManifestService) - manService.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(new(mocks.Manifest), nil) - mockRegClient.On("Manifests", mock.Anything).Return(manService, nil) - _, err := TagInfoFromReferences(&client, opts, log.NewContext(), tagInfo, descriptor) - require.Error(t, err) - }) -} - -func Test_TagMetadata(t *testing.T) { - t.Run("Check for correct error handling when manifest contains no history", func(t *testing.T) { - meta1 := &schema1.SignedManifest{ //nolint:staticcheck - Manifest: schema1.Manifest{ //nolint:staticcheck - History: []schema1.History{}, //nolint:staticcheck - }, - } - ep, err := GetRegistryEndpoint("") - require.NoError(t, err) - client, err := NewClient(ep, "", "") - require.NoError(t, err) - _, err = client.TagMetadata(meta1, &options.ManifestOptions{}) - require.Error(t, err) - }) - - t.Run("Check for correct error handling when manifest contains invalid history", func(t *testing.T) { - meta1 := &schema1.SignedManifest{ //nolint:staticcheck - Manifest: schema1.Manifest{ //nolint:staticcheck - History: []schema1.History{ //nolint:staticcheck - { - V1Compatibility: `{"created": {"something": "notastring"}}`, - }, - }, - }, - } - - ep, err := GetRegistryEndpoint("") - require.NoError(t, err) - client, err := NewClient(ep, "", "") - require.NoError(t, err) - _, err = client.TagMetadata(meta1, &options.ManifestOptions{}) - require.Error(t, err) - }) - - t.Run("Check for correct error handling when manifest contains invalid history", func(t *testing.T) { - meta1 := &schema1.SignedManifest{ //nolint:staticcheck - Manifest: schema1.Manifest{ //nolint:staticcheck - History: []schema1.History{ //nolint:staticcheck - { - V1Compatibility: `{"something": "something"}`, - }, - }, - }, - } - - ep, err := GetRegistryEndpoint("") - require.NoError(t, err) - client, err := NewClient(ep, "", "") - require.NoError(t, err) - _, err = client.TagMetadata(meta1, &options.ManifestOptions{}) - require.Error(t, err) - - }) - - t.Run("Check for invalid/valid timestamp and non-match platforms", func(t *testing.T) { - ts := "invalid" - meta1 := &schema1.SignedManifest{ //nolint:staticcheck - Manifest: schema1.Manifest{ //nolint:staticcheck - History: []schema1.History{ //nolint:staticcheck - { - V1Compatibility: `{"created":"` + ts + `"}`, - }, - }, - }, - } - ep, err := GetRegistryEndpoint("") - require.NoError(t, err) - client, err := NewClient(ep, "", "") - require.NoError(t, err) - _, err = client.TagMetadata(meta1, &options.ManifestOptions{}) - require.Error(t, err) - - ts = time.Now().Format(time.RFC3339Nano) - opts := &options.ManifestOptions{} - meta1.Manifest.History[0].V1Compatibility = `{"created":"` + ts + `"}` - tagInfo, _ := client.TagMetadata(meta1, opts) - assert.Equal(t, ts, tagInfo.CreatedAt.Format(time.RFC3339Nano)) - - opts.WithPlatform("testOS", "testArch", "testVariant") - tagInfo, err = client.TagMetadata(meta1, opts) - assert.Nil(t, tagInfo) - assert.Nil(t, err) - }) -} - -func Test_TagMetadata_2(t *testing.T) { - t.Run("ocischema DeserializedManifest invalid digest format", func(t *testing.T) { - meta1 := &ocischema.DeserializedManifest{ - Manifest: ocischema.Manifest{ - Versioned: manifest.Versioned{ - SchemaVersion: 1, - MediaType: "", - }, - }, - } - ep, err := GetRegistryEndpoint("") - require.NoError(t, err) - client, err := NewClient(ep, "", "") - - require.NoError(t, err) - err = client.NewRepository("test/test") - require.NoError(t, err) - _, err = client.TagMetadata(meta1, &options.ManifestOptions{}) - require.Error(t, err) // invalid digest format - }) - t.Run("schema2 DeserializedManifest invalid digest format", func(t *testing.T) { - meta1 := &schema2.DeserializedManifest{ - Manifest: schema2.Manifest{ - Versioned: manifest.Versioned{ - SchemaVersion: 1, - MediaType: "", - }, - Config: distribution.Descriptor{ - MediaType: "", - Digest: "sha256:abc", - }, - }, - } - ep, err := GetRegistryEndpoint("") - require.NoError(t, err) - client, err := NewClient(ep, "", "") - - require.NoError(t, err) - err = client.NewRepository("test/test") - require.NoError(t, err) - _, err = client.TagMetadata(meta1, &options.ManifestOptions{}) - require.Error(t, err) // invalid digest format - }) - t.Run("ocischema DeserializedImageIndex empty index not supported", func(t *testing.T) { - meta1 := &ocischema.DeserializedImageIndex{ - ImageIndex: ocischema.ImageIndex{ - Versioned: manifest.Versioned{ - SchemaVersion: 1, - MediaType: "", - }, - Manifests: nil, - Annotations: nil, - }, - } - ep, err := GetRegistryEndpoint("") - require.NoError(t, err) - client, err := NewClient(ep, "", "") - - require.NoError(t, err) - err = client.NewRepository("test/test") - require.NoError(t, err) - _, err = client.TagMetadata(meta1, &options.ManifestOptions{}) - require.Error(t, err) // empty index not supported - }) - t.Run("ocischema DeserializedImageIndex empty manifestlist not supported", func(t *testing.T) { - meta1 := &manifestlist.DeserializedManifestList{ - ManifestList: manifestlist.ManifestList{ - Versioned: manifest.Versioned{ - SchemaVersion: 1, - MediaType: "", - }, - Manifests: nil, - }, - } - ep, err := GetRegistryEndpoint("") - require.NoError(t, err) - client, err := NewClient(ep, "", "") - - require.NoError(t, err) - err = client.NewRepository("test/test") - require.NoError(t, err) - _, err = client.TagMetadata(meta1, &options.ManifestOptions{}) - require.Error(t, err) // empty manifestlist not supported - }) -} - -func TestPing(t *testing.T) { - t.Run("fail ping", func(t *testing.T) { - mockManager := new(mocks.Manager) - ep, err := GetRegistryEndpoint("") - require.NoError(t, err) - mockManager.On("AddResponse", mock.Anything).Return(fmt.Errorf("fail ping")) - _, err = ping(mockManager, ep, "") - require.Error(t, err) - }) - - t.Run("success ping", func(t *testing.T) { - mockManager := new(mocks.Manager) - ep, err := GetRegistryEndpoint("") - require.NoError(t, err) - mockManager.On("AddResponse", mock.Anything).Return(nil) - _, err = ping(mockManager, ep, "") - require.NoError(t, err) - }) - - t.Run("Invalid Docker Registry", func(t *testing.T) { - testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - })) - mockManager := new(mocks.Manager) - ep := &RegistryEndpoint{RegistryAPI: testServer.URL} - mockManager.On("AddResponse", mock.Anything).Return(nil) - _, err := ping(mockManager, ep, "") - require.Error(t, err) - assert.ErrorContains(t, err, "does not seem to be a valid v2 Docker Registry API") - }) - - t.Run("Empty Registry API", func(t *testing.T) { - mockManager := new(mocks.Manager) - ep := &RegistryEndpoint{RegistryAPI: ""} - mockManager.On("AddResponse", mock.Anything).Return(nil) - _, err := ping(mockManager, ep, "") - require.Error(t, err) - assert.ErrorContains(t, err, "unsupported protocol scheme") - }) - -} diff --git a/pkg/registry/config.go b/pkg/registry/config.go deleted file mode 100644 index 641c5987..00000000 --- a/pkg/registry/config.go +++ /dev/null @@ -1,139 +0,0 @@ -package registry - -import ( - "fmt" - "os" - "time" - - "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/log" - - "gopkg.in/yaml.v2" -) - -// RegistryConfiguration represents a single repository configuration for being -// unmarshaled from YAML. -type RegistryConfiguration struct { - Name string `yaml:"name"` - ApiURL string `yaml:"api_url"` - Ping bool `yaml:"ping,omitempty"` - Credentials string `yaml:"credentials,omitempty"` - CredsExpire time.Duration `yaml:"credsexpire,omitempty"` - TagSortMode string `yaml:"tagsortmode,omitempty"` - Prefix string `yaml:"prefix,omitempty"` - Insecure bool `yaml:"insecure,omitempty"` - DefaultNS string `yaml:"defaultns,omitempty"` - Limit int `yaml:"limit,omitempty"` - IsDefault bool `yaml:"default,omitempty"` -} - -// RegistryList contains multiple RegistryConfiguration items -type RegistryList struct { - Items []RegistryConfiguration `yaml:"registries"` -} - -func clearRegistries() { - registryLock.Lock() - registries = make(map[string]*RegistryEndpoint) - registryLock.Unlock() -} - -// LoadRegistryConfiguration loads a YAML-formatted registry configuration from -// a given file at path. -func LoadRegistryConfiguration(path string, clear bool) error { - registryBytes, err := os.ReadFile(path) - if err != nil { - return err - } - registryList, err := ParseRegistryConfiguration(string(registryBytes)) - if err != nil { - return err - } - - if clear { - clearRegistries() - } - - haveDefault := false - - for _, reg := range registryList.Items { - tagSortMode := TagListSortFromString(reg.TagSortMode) - if tagSortMode != TagListSortUnsorted { - log.Warnf("Registry %s has tag sort mode set to %s, meta data retrieval will be disabled for this registry.", reg.ApiURL, tagSortMode) - } - ep := NewRegistryEndpoint(reg.Prefix, reg.Name, reg.ApiURL, reg.Credentials, reg.DefaultNS, reg.Insecure, tagSortMode, reg.Limit, reg.CredsExpire) - if reg.IsDefault { - if haveDefault { - dep := GetDefaultRegistry() - if dep == nil { - panic("unexpected: default registry should be set, but is not") - } - return fmt.Errorf("cannot set registry %s as default - only one default registry allowed, currently set to %s", ep.RegistryPrefix, dep.RegistryPrefix) - } - } - - if err := AddRegistryEndpoint(ep); err != nil { - return err - } - - if reg.IsDefault { - SetDefaultRegistry(ep) - haveDefault = true - } - } - - log.Infof("Loaded %d registry configurations from %s", len(registryList.Items), path) - return nil -} - -// Parses a registry configuration from a YAML input string and returns a list -// of registries. -func ParseRegistryConfiguration(yamlSource string) (RegistryList, error) { - var regList RegistryList - var defaultPrefixFound = "" - err := yaml.UnmarshalStrict([]byte(yamlSource), ®List) - if err != nil { - return RegistryList{}, err - } - - // validate the parsed list - for _, registry := range regList.Items { - if registry.Name == "" { - err = fmt.Errorf("registry name is missing for entry %v", registry) - } else if registry.ApiURL == "" { - err = fmt.Errorf("API URL must be specified for registry %s", registry.Name) - } else if registry.Prefix == "" { - if defaultPrefixFound != "" { - err = fmt.Errorf("there must be only one default registry (already is %s), %s needs a prefix", defaultPrefixFound, registry.Name) - } else { - defaultPrefixFound = registry.Name - } - } - - if err == nil { - if tls := TagListSortFromString(registry.TagSortMode); tls == TagListSortUnknown { - err = fmt.Errorf("unknown tag sort mode for registry %s: %s", registry.Name, registry.TagSortMode) - } - } - } - - if err != nil { - return RegistryList{}, err - } - - return regList, nil -} - -// RestRestoreDefaultRegistryConfiguration restores the registry configuration -// to the default values. -func RestoreDefaultRegistryConfiguration() { - registryLock.Lock() - defer registryLock.Unlock() - defaultRegistry = nil - registries = make(map[string]*RegistryEndpoint) - for k, v := range registryTweaks { - registries[k] = v.DeepCopy() - if v.IsDefault { - SetDefaultRegistry(registries[k]) - } - } -} diff --git a/pkg/registry/config_test.go b/pkg/registry/config_test.go deleted file mode 100644 index 080dd004..00000000 --- a/pkg/registry/config_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package registry - -import ( - "testing" - "time" - - "github.com/argoproj-labs/argocd-image-updater/test/fixture" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func Test_ParseRegistryConfFromYaml(t *testing.T) { - t.Run("Parse from valid YAML", func(t *testing.T) { - data := fixture.MustReadFile("../../config/example-config.yaml") - regList, err := ParseRegistryConfiguration(data) - require.NoError(t, err) - assert.Len(t, regList.Items, 4) - }) - - t.Run("Parse from invalid YAML: no name found", func(t *testing.T) { - registries := ` -registries: -- api_url: https://foo.io - ping: false -` - regList, err := ParseRegistryConfiguration(registries) - require.Error(t, err) - assert.Contains(t, err.Error(), "name is missing") - assert.Len(t, regList.Items, 0) - }) - - t.Run("Parse from invalid YAML: no API URL found", func(t *testing.T) { - registries := ` -registries: -- name: Foobar Registry - ping: false -` - regList, err := ParseRegistryConfiguration(registries) - require.Error(t, err) - assert.Contains(t, err.Error(), "API URL must be") - assert.Len(t, regList.Items, 0) - }) - - t.Run("Parse from invalid YAML: multiple registries without prefix", func(t *testing.T) { - registries := ` -registries: -- name: Foobar Registry - api_url: https://foobar.io - ping: false -- name: Barbar Registry - api_url: https://barbar.io - ping: false -` - regList, err := ParseRegistryConfiguration(registries) - require.Error(t, err) - assert.Contains(t, err.Error(), "already is Foobar Registry") - assert.Len(t, regList.Items, 0) - }) - - t.Run("Parse from invalid YAML: invalid tag sort mode", func(t *testing.T) { - registries := ` -registries: -- name: Foobar Registry - api_url: https://foobar.io - ping: false - tagsortmode: invalid -` - regList, err := ParseRegistryConfiguration(registries) - require.Error(t, err) - assert.Contains(t, err.Error(), "unknown tag sort mode") - assert.Len(t, regList.Items, 0) - }) - -} - -func Test_LoadRegistryConfiguration(t *testing.T) { - RestoreDefaultRegistryConfiguration() - - t.Run("Load from valid location", func(t *testing.T) { - err := LoadRegistryConfiguration("../../config/example-config.yaml", true) - require.NoError(t, err) - assert.Len(t, registries, 4) - reg, err := GetRegistryEndpoint("gcr.io") - require.NoError(t, err) - assert.Equal(t, "pullsecret:foo/bar", reg.Credentials) - reg, err = GetRegistryEndpoint("ghcr.io") - require.NoError(t, err) - assert.Equal(t, "ext:/some/script", reg.Credentials) - assert.Equal(t, 5*time.Hour, reg.CredsExpire) - RestoreDefaultRegistryConfiguration() - reg, err = GetRegistryEndpoint("gcr.io") - require.NoError(t, err) - assert.Equal(t, "", reg.Credentials) - }) - - t.Run("Load from invalid location", func(t *testing.T) { - err := LoadRegistryConfiguration("../../test/testdata/registry/config/does-not-exist.yaml", true) - require.Error(t, err) - require.Contains(t, err.Error(), "no such file or directory") - }) - - t.Run("Two defaults defined in same config", func(t *testing.T) { - err := LoadRegistryConfiguration("../../test/testdata/registry/config/two-defaults.yaml", true) - require.Error(t, err) - require.Contains(t, err.Error(), "cannot set registry") - }) - - RestoreDefaultRegistryConfiguration() -} diff --git a/pkg/registry/endpoints.go b/pkg/registry/endpoints.go deleted file mode 100644 index 3b64fc53..00000000 --- a/pkg/registry/endpoints.go +++ /dev/null @@ -1,305 +0,0 @@ -package registry - -import ( - "crypto/tls" - "fmt" - "math" - "net/http" - "strings" - "sync" - "time" - - "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/cache" - "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/log" - - "go.uber.org/ratelimit" -) - -// TagListSort defines how the registry returns the list of tags -type TagListSort int - -const ( - TagListSortUnknown TagListSort = -1 - TagListSortUnsorted TagListSort = 0 - TagListSortLatestFirst TagListSort = 1 - TagListSortLatestLast TagListSort = 2 - TagListSortUnsortedString string = "unsorted" - TagListSortLatestFirstString string = "latest-first" - TagListSortLatestLastString string = "latest-last" - TagListSortUnknownString string = "unknown" -) - -const ( - RateLimitNone = math.MaxInt32 - RateLimitDefault = 10 -) - -// IsTimeSorted returns whether a tag list is time sorted -func (tls TagListSort) IsTimeSorted() bool { - return tls == TagListSortLatestFirst || tls == TagListSortLatestLast -} - -// TagListSortFromString gets the TagListSort value from a given string -func TagListSortFromString(tls string) TagListSort { - switch strings.ToLower(tls) { - case "latest-first": - return TagListSortLatestFirst - case "latest-last": - return TagListSortLatestLast - case "none", "": - return TagListSortUnsorted - default: - log.Warnf("unknown tag list sort mode: %s", tls) - return TagListSortUnknown - } -} - -// String returns the string representation of a TagListSort value -func (tls TagListSort) String() string { - switch tls { - case TagListSortLatestFirst: - return TagListSortLatestFirstString - case TagListSortLatestLast: - return TagListSortLatestLastString - case TagListSortUnsorted: - return TagListSortUnsortedString - } - - return TagListSortUnknownString -} - -// RegistryEndpoint holds information on how to access any specific registry API -// endpoint. -type RegistryEndpoint struct { - RegistryName string - RegistryPrefix string - RegistryAPI string - Username string - Password string - Ping bool - Credentials string - Insecure bool - DefaultNS string - CredsExpire time.Duration - CredsUpdated time.Time - TagListSort TagListSort - Cache cache.ImageTagCache - Limiter ratelimit.Limiter - IsDefault bool - lock sync.RWMutex - limit int -} - -// registryTweaks should contain a list of registries whose settings cannot be -// inferred by just looking at the image prefix. Prominent example here is the -// Docker Hub registry, which is referred to as docker.io from the image, but -// its API endpoint is https://registry-1.docker.io (and not https://docker.io) -var registryTweaks map[string]*RegistryEndpoint = map[string]*RegistryEndpoint{ - "docker.io": { - RegistryName: "Docker Hub", - RegistryPrefix: "docker.io", - RegistryAPI: "https://registry-1.docker.io", - Ping: true, - Insecure: false, - DefaultNS: "library", - Cache: cache.NewMemCache(), - Limiter: ratelimit.New(RateLimitDefault), - IsDefault: true, - }, -} - -var registries map[string]*RegistryEndpoint = make(map[string]*RegistryEndpoint) - -// Default registry points to the registry that is to be used as the default, -// e.g. when no registry prefix is given for a certain image. -var defaultRegistry *RegistryEndpoint - -// Simple RW mutex for concurrent access to registries map -var registryLock sync.RWMutex - -func AddRegistryEndpointFromConfig(epc RegistryConfiguration) error { - ep := NewRegistryEndpoint(epc.Prefix, epc.Name, epc.ApiURL, epc.Credentials, epc.DefaultNS, epc.Insecure, TagListSortFromString(epc.TagSortMode), epc.Limit, epc.CredsExpire) - return AddRegistryEndpoint(ep) -} - -// NewRegistryEndpoint returns an endpoint object with the given configuration -// pre-populated and a fresh cache. -func NewRegistryEndpoint(prefix, name, apiUrl, credentials, defaultNS string, insecure bool, tagListSort TagListSort, limit int, credsExpire time.Duration) *RegistryEndpoint { - if limit <= 0 { - limit = RateLimitNone - } - ep := &RegistryEndpoint{ - RegistryName: name, - RegistryPrefix: prefix, - RegistryAPI: strings.TrimSuffix(apiUrl, "/"), - Credentials: credentials, - CredsExpire: credsExpire, - Cache: cache.NewMemCache(), - Insecure: insecure, - DefaultNS: defaultNS, - TagListSort: tagListSort, - Limiter: ratelimit.New(limit), - limit: limit, - } - return ep -} - -// AddRegistryEndpoint adds registry endpoint information with the given details -func AddRegistryEndpoint(ep *RegistryEndpoint) error { - prefix := ep.RegistryPrefix - - registryLock.Lock() - // If the endpoint is supposed to be the default endpoint, make sure that - // any previously set default endpoint is unset. - if ep.IsDefault { - if dep := GetDefaultRegistry(); dep != nil { - dep.IsDefault = false - } - SetDefaultRegistry(ep) - } - registries[prefix] = ep - registryLock.Unlock() - - logCtx := log.WithContext() - logCtx.AddField("registry", ep.RegistryAPI) - logCtx.AddField("prefix", ep.RegistryPrefix) - if ep.limit != RateLimitNone { - logCtx.Debugf("setting rate limit to %d requests per second", ep.limit) - } else { - logCtx.Debugf("rate limiting is disabled") - } - return nil -} - -// inferRegistryEndpointFromPrefix returns a registry endpoint with the API -// URL inferred from the prefix and adds it to the list of the configured -// registries. -func inferRegistryEndpointFromPrefix(prefix string) *RegistryEndpoint { - apiURL := "https://" + prefix - return NewRegistryEndpoint(prefix, prefix, apiURL, "", "", false, TagListSortUnsorted, 20, 0) -} - -// GetRegistryEndpoint retrieves the endpoint information for the given prefix -func GetRegistryEndpoint(prefix string) (*RegistryEndpoint, error) { - if prefix == "" { - if defaultRegistry == nil { - return nil, fmt.Errorf("no default endpoint configured") - } else { - return defaultRegistry, nil - } - } - - registryLock.RLock() - registry, ok := registries[prefix] - registryLock.RUnlock() - - if ok { - return registry, nil - } else { - var err error - ep := inferRegistryEndpointFromPrefix(prefix) - if ep != nil { - err = AddRegistryEndpoint(ep) - } else { - err = fmt.Errorf("could not infer registry configuration from prefix %s", prefix) - } - if err == nil { - log.Debugf("Inferred registry from prefix %s to use API %s", prefix, ep.RegistryAPI) - } - return ep, err - } -} - -// SetDefaultRegistry sets a given registry endpoint as the default -func SetDefaultRegistry(ep *RegistryEndpoint) { - log.Debugf("Setting default registry endpoint to %s", ep.RegistryPrefix) - ep.IsDefault = true - if defaultRegistry != nil { - log.Debugf("Previous default registry was %s", defaultRegistry.RegistryPrefix) - defaultRegistry.IsDefault = false - } - defaultRegistry = ep -} - -// GetDefaultRegistry returns the registry endpoint that is set as default, -// or nil if no default registry endpoint is set -func GetDefaultRegistry() *RegistryEndpoint { - if defaultRegistry != nil { - log.Debugf("Getting default registry endpoint: %s", defaultRegistry.RegistryPrefix) - } else { - log.Debugf("No default registry defined.") - } - return defaultRegistry -} - -// SetRegistryEndpointCredentials allows to change the credentials used for -// endpoint access for existing RegistryEndpoint configuration -func SetRegistryEndpointCredentials(prefix, credentials string) error { - registry, err := GetRegistryEndpoint(prefix) - if err != nil { - return err - } - registry.lock.Lock() - registry.Credentials = credentials - registry.lock.Unlock() - return nil -} - -// ConfiguredEndpoints returns a list of prefixes that are configured -func ConfiguredEndpoints() []string { - registryLock.RLock() - defer registryLock.RUnlock() - r := make([]string, 0, len(registries)) - for _, v := range registries { - r = append(r, v.RegistryPrefix) - } - return r -} - -// DeepCopy copies the endpoint to a new object, but creating a new Cache -func (ep *RegistryEndpoint) DeepCopy() *RegistryEndpoint { - ep.lock.RLock() - newEp := &RegistryEndpoint{} - newEp.RegistryAPI = ep.RegistryAPI - newEp.RegistryName = ep.RegistryName - newEp.RegistryPrefix = ep.RegistryPrefix - newEp.Credentials = ep.Credentials - newEp.Ping = ep.Ping - newEp.TagListSort = ep.TagListSort - newEp.Cache = cache.NewMemCache() - newEp.Insecure = ep.Insecure - newEp.DefaultNS = ep.DefaultNS - newEp.Limiter = ep.Limiter - newEp.CredsExpire = ep.CredsExpire - newEp.CredsUpdated = ep.CredsUpdated - newEp.IsDefault = ep.IsDefault - newEp.limit = ep.limit - ep.lock.RUnlock() - return newEp -} - -// GetTransport returns a transport object for this endpoint -func (ep *RegistryEndpoint) GetTransport() *http.Transport { - tlsC := &tls.Config{} - if ep.Insecure { - tlsC.InsecureSkipVerify = true - } - return &http.Transport{ - Proxy: http.ProxyFromEnvironment, - TLSClientConfig: tlsC, - } -} - -// init initializes the registry configuration -func init() { - for k, v := range registryTweaks { - registries[k] = v.DeepCopy() - if v.IsDefault { - if defaultRegistry == nil { - defaultRegistry = v - } else { - panic("only one default registry can be configured") - } - } - } -} diff --git a/pkg/registry/endpoints_test.go b/pkg/registry/endpoints_test.go deleted file mode 100644 index d2dae1f7..00000000 --- a/pkg/registry/endpoints_test.go +++ /dev/null @@ -1,354 +0,0 @@ -package registry - -import ( - "fmt" - "strings" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestInferRegistryEndpointFromPrefix(t *testing.T) { - prefix := "example.com" - expectedAPIURL := "https://" + prefix - endpoint := inferRegistryEndpointFromPrefix(prefix) - assert.NotNil(t, endpoint) - assert.Equal(t, prefix, endpoint.RegistryName) - assert.Equal(t, prefix, endpoint.RegistryPrefix) - assert.Equal(t, expectedAPIURL, endpoint.RegistryAPI) - assert.Equal(t, TagListSortUnsorted, endpoint.TagListSort) - assert.Equal(t, 20, endpoint.limit) - assert.False(t, endpoint.Insecure) -} - -func TestNewRegistryEndpoint(t *testing.T) { - prefix := "example.com" - name := "exampleRegistry" - apiUrl := "https://api.example.com" - credentials := "user:pass" - defaultNS := "default" - insecure := true - tagListSort := TagListSortLatestFirst - limit := 10 - credsExpire := time.Minute * 30 - endpoint := NewRegistryEndpoint(prefix, name, apiUrl, credentials, defaultNS, insecure, tagListSort, limit, credsExpire) - assert.NotNil(t, endpoint) - assert.Equal(t, name, endpoint.RegistryName) - assert.Equal(t, prefix, endpoint.RegistryPrefix) - assert.Equal(t, strings.TrimSuffix(apiUrl, "/"), endpoint.RegistryAPI) - assert.Equal(t, credentials, endpoint.Credentials) - assert.Equal(t, credsExpire, endpoint.CredsExpire) - assert.Equal(t, insecure, endpoint.Insecure) - assert.Equal(t, defaultNS, endpoint.DefaultNS) - assert.Equal(t, tagListSort, endpoint.TagListSort) - assert.Equal(t, limit, endpoint.limit) -} - -func TestTagListSortFromString(t *testing.T) { - t.Run("returns TagListSortLatestFirst for 'latest-first'", func(t *testing.T) { - result := TagListSortFromString("latest-first") - assert.Equal(t, TagListSortLatestFirst, result) - }) - - t.Run("returns TagListSortLatestLast for 'latest-last'", func(t *testing.T) { - result := TagListSortFromString("latest-last") - assert.Equal(t, TagListSortLatestLast, result) - }) - - t.Run("returns TagListSortUnsorted for 'none'", func(t *testing.T) { - result := TagListSortFromString("none") - assert.Equal(t, TagListSortUnsorted, result) - }) - - t.Run("returns TagListSortUnsorted for an empty string", func(t *testing.T) { - result := TagListSortFromString("") - assert.Equal(t, TagListSortUnsorted, result) - }) - - t.Run("returns TagListSortUnknown for an unknown value", func(t *testing.T) { - result := TagListSortFromString("unknown-value") - assert.Equal(t, TagListSortUnknown, result) - }) -} - -func TestIsTimeSorted(t *testing.T) { - t.Run("returns true for TagListSortLatestFirst", func(t *testing.T) { - assert.True(t, TagListSortLatestFirst.IsTimeSorted()) - }) - t.Run("returns true for TagListSortLatestLast", func(t *testing.T) { - assert.True(t, TagListSortLatestLast.IsTimeSorted()) - }) - t.Run("returns false for TagListSortUnsorted", func(t *testing.T) { - assert.False(t, TagListSortUnsorted.IsTimeSorted()) - }) - t.Run("returns false for TagListSortUnknown", func(t *testing.T) { - assert.False(t, TagListSortUnknown.IsTimeSorted()) - }) -} - -func TestTagListSort_String(t *testing.T) { - t.Run("returns 'latest-first' for TagListSortLatestFirst", func(t *testing.T) { - assert.Equal(t, TagListSortLatestFirstString, TagListSortLatestFirst.String()) - }) - - t.Run("returns 'latest-last' for TagListSortLatestLast", func(t *testing.T) { - assert.Equal(t, TagListSortLatestLastString, TagListSortLatestLast.String()) - }) - - t.Run("returns 'unsorted' for TagListSortUnsorted", func(t *testing.T) { - assert.Equal(t, TagListSortUnsortedString, TagListSortUnsorted.String()) - }) - - t.Run("returns 'unknown' for TagListSortUnknown", func(t *testing.T) { - assert.Equal(t, TagListSortUnknownString, TagListSortUnknown.String()) - }) - - t.Run("returns 'unknown' for an undefined TagListSort value", func(t *testing.T) { - var undefined TagListSort = 99 - assert.Equal(t, TagListSortUnknownString, undefined.String()) - }) -} - -func Test_GetEndpoints(t *testing.T) { - RestoreDefaultRegistryConfiguration() - - t.Run("Get default endpoint", func(t *testing.T) { - ep, err := GetRegistryEndpoint("") - require.NoError(t, err) - require.NotNil(t, ep) - assert.Equal(t, "docker.io", ep.RegistryPrefix) - }) - - t.Run("Get GCR endpoint", func(t *testing.T) { - ep, err := GetRegistryEndpoint("gcr.io") - require.NoError(t, err) - require.NotNil(t, ep) - assert.Equal(t, ep.RegistryPrefix, "gcr.io") - }) - - t.Run("Infer endpoint", func(t *testing.T) { - ep, err := GetRegistryEndpoint("foobar.com") - require.NoError(t, err) - require.NotNil(t, ep) - assert.Equal(t, "foobar.com", ep.RegistryPrefix) - assert.Equal(t, "https://foobar.com", ep.RegistryAPI) - }) -} - -func Test_AddEndpoint(t *testing.T) { - RestoreDefaultRegistryConfiguration() - - t.Run("Add new endpoint", func(t *testing.T) { - err := AddRegistryEndpoint(NewRegistryEndpoint("example.com", "Example", "https://example.com", "", "", false, TagListSortUnsorted, 5, 0)) - require.NoError(t, err) - }) - t.Run("Get example.com endpoint", func(t *testing.T) { - ep, err := GetRegistryEndpoint("example.com") - require.NoError(t, err) - require.NotNil(t, ep) - assert.Equal(t, ep.RegistryPrefix, "example.com") - assert.Equal(t, ep.RegistryName, "Example") - assert.Equal(t, ep.RegistryAPI, "https://example.com") - assert.Equal(t, ep.Insecure, false) - assert.Equal(t, ep.DefaultNS, "") - assert.Equal(t, ep.TagListSort, TagListSortUnsorted) - }) - t.Run("Change existing endpoint", func(t *testing.T) { - err := AddRegistryEndpoint(NewRegistryEndpoint("example.com", "Example", "https://example.com", "", "library", true, TagListSortLatestFirst, 5, 0)) - require.NoError(t, err) - ep, err := GetRegistryEndpoint("example.com") - require.NoError(t, err) - require.NotNil(t, ep) - assert.Equal(t, ep.Insecure, true) - assert.Equal(t, ep.DefaultNS, "library") - assert.Equal(t, ep.TagListSort, TagListSortLatestFirst) - }) -} - -func Test_SetEndpointCredentials(t *testing.T) { - RestoreDefaultRegistryConfiguration() - - t.Run("Set credentials on default registry", func(t *testing.T) { - err := SetRegistryEndpointCredentials("", "env:FOOBAR") - require.NoError(t, err) - ep, err := GetRegistryEndpoint("") - require.NoError(t, err) - require.NotNil(t, ep) - assert.Equal(t, ep.Credentials, "env:FOOBAR") - }) - - t.Run("Unset credentials on default registry", func(t *testing.T) { - err := SetRegistryEndpointCredentials("", "") - require.NoError(t, err) - ep, err := GetRegistryEndpoint("") - require.NoError(t, err) - require.NotNil(t, ep) - assert.Equal(t, ep.Credentials, "") - }) -} - -func Test_EndpointConcurrentAccess(t *testing.T) { - RestoreDefaultRegistryConfiguration() - const numRuns = 50 - // Make sure we're not deadlocking on read - t.Run("Concurrent read access", func(t *testing.T) { - var wg sync.WaitGroup - wg.Add(numRuns) - for i := 0; i < numRuns; i++ { - go func() { - ep, err := GetRegistryEndpoint("gcr.io") - require.NoError(t, err) - require.NotNil(t, ep) - wg.Done() - }() - } - wg.Wait() - }) - - // Make sure we're not deadlocking on write - t.Run("Concurrent write access", func(t *testing.T) { - var wg sync.WaitGroup - wg.Add(numRuns) - for i := 0; i < numRuns; i++ { - go func(i int) { - creds := fmt.Sprintf("secret:foo/secret-%d", i) - err := SetRegistryEndpointCredentials("", creds) - require.NoError(t, err) - ep, err := GetRegistryEndpoint("") - require.NoError(t, err) - require.NotNil(t, ep) - wg.Done() - }(i) - } - wg.Wait() - }) -} - -func Test_SetDefault(t *testing.T) { - RestoreDefaultRegistryConfiguration() - - dep := GetDefaultRegistry() - require.NotNil(t, dep) - assert.Equal(t, "docker.io", dep.RegistryPrefix) - assert.True(t, dep.IsDefault) - - ep, err := GetRegistryEndpoint("ghcr.io") - require.NoError(t, err) - require.NotNil(t, ep) - require.False(t, ep.IsDefault) - - SetDefaultRegistry(ep) - assert.True(t, ep.IsDefault) - assert.False(t, dep.IsDefault) - require.NotNil(t, GetDefaultRegistry()) - assert.Equal(t, ep.RegistryPrefix, GetDefaultRegistry().RegistryPrefix) -} - -func Test_DeepCopy(t *testing.T) { - t.Run("DeepCopy endpoint object", func(t *testing.T) { - ep, err := GetRegistryEndpoint("docker.pkg.github.com") - require.NoError(t, err) - require.NotNil(t, ep) - newEp := ep.DeepCopy() - assert.Equal(t, ep.RegistryAPI, newEp.RegistryAPI) - assert.Equal(t, ep.RegistryName, newEp.RegistryName) - assert.Equal(t, ep.RegistryPrefix, newEp.RegistryPrefix) - assert.Equal(t, ep.Credentials, newEp.Credentials) - assert.Equal(t, ep.TagListSort, newEp.TagListSort) - assert.Equal(t, ep.Username, newEp.Username) - assert.Equal(t, ep.Ping, newEp.Ping) - }) -} - -func Test_GetTagListSortFromString(t *testing.T) { - t.Run("Get latest-first sorting", func(t *testing.T) { - tls := TagListSortFromString("latest-first") - assert.Equal(t, TagListSortLatestFirst, tls) - }) - t.Run("Get latest-last sorting", func(t *testing.T) { - tls := TagListSortFromString("latest-last") - assert.Equal(t, TagListSortLatestLast, tls) - }) - t.Run("Get none sorting explicit", func(t *testing.T) { - tls := TagListSortFromString("none") - assert.Equal(t, TagListSortUnsorted, tls) - }) - t.Run("Get none sorting implicit", func(t *testing.T) { - tls := TagListSortFromString("") - assert.Equal(t, TagListSortUnsorted, tls) - }) - t.Run("Get unknown sorting from unknown string", func(t *testing.T) { - tls := TagListSortFromString("unknown") - assert.Equal(t, TagListSortUnknown, tls) - }) -} - -func TestGetTransport(t *testing.T) { - t.Run("returns transport with default TLS config when Insecure is false", func(t *testing.T) { - endpoint := &RegistryEndpoint{ - Insecure: false, - } - transport := endpoint.GetTransport() - - assert.NotNil(t, transport) - assert.NotNil(t, transport.TLSClientConfig) - assert.False(t, transport.TLSClientConfig.InsecureSkipVerify) - }) - - t.Run("returns transport with insecure TLS config when Insecure is true", func(t *testing.T) { - endpoint := &RegistryEndpoint{ - Insecure: true, - } - transport := endpoint.GetTransport() - - assert.NotNil(t, transport) - assert.NotNil(t, transport.TLSClientConfig) - assert.True(t, transport.TLSClientConfig.InsecureSkipVerify) - }) -} - -func Test_RestoreDefaultRegistryConfiguration(t *testing.T) { - // Call the function to restore default configuration - RestoreDefaultRegistryConfiguration() - - // Retrieve the default registry endpoint - defaultEp := GetDefaultRegistry() - - // Validate that the default registry endpoint is not nil - require.NotNil(t, defaultEp) - - // Validate that the default registry endpoint has expected properties - assert.Equal(t, "docker.io", defaultEp.RegistryPrefix) - assert.True(t, defaultEp.IsDefault) -} - -func TestConfiguredEndpoints(t *testing.T) { - // Test the function - endpoints := ConfiguredEndpoints() - // Validate the output - expected := []string{"docker.io"} - require.Len(t, endpoints, len(expected), "The number of endpoints should match the expected number") - assert.ElementsMatch(t, expected, endpoints, "The endpoints should match the expected values") - -} - -func TestAddRegistryEndpointFromConfig(t *testing.T) { - t.Run("successfully adds registry endpoint from config", func(t *testing.T) { - config := RegistryConfiguration{ - Prefix: "example.com", - Name: "exampleRegistry", - ApiURL: "https://api.example.com", - Credentials: "user:pass", - DefaultNS: "default", - Insecure: true, - TagSortMode: "latest-first", - Limit: 10, - CredsExpire: time.Minute * 30, - } - err := AddRegistryEndpointFromConfig(config) - require.NoError(t, err) - }) -} diff --git a/pkg/registry/mocks/Limiter.go b/pkg/registry/mocks/Limiter.go deleted file mode 100644 index 81dbb19b..00000000 --- a/pkg/registry/mocks/Limiter.go +++ /dev/null @@ -1,46 +0,0 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. - -package mocks - -import ( - mock "github.com/stretchr/testify/mock" - - time "time" -) - -// Limiter is an autogenerated mock type for the Limiter type -type Limiter struct { - mock.Mock -} - -// Take provides a mock function with given fields: -func (_m *Limiter) Take() time.Time { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for Take") - } - - var r0 time.Time - if rf, ok := ret.Get(0).(func() time.Time); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(time.Time) - } - - return r0 -} - -// NewLimiter creates a new instance of Limiter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewLimiter(t interface { - mock.TestingT - Cleanup(func()) -}) *Limiter { - mock := &Limiter{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/mocks/Manager.go b/pkg/registry/mocks/Manager.go deleted file mode 100644 index 02c37764..00000000 --- a/pkg/registry/mocks/Manager.go +++ /dev/null @@ -1,80 +0,0 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. - -package mocks - -import ( - http "net/http" - - challenge "github.com/distribution/distribution/v3/registry/client/auth/challenge" - - mock "github.com/stretchr/testify/mock" - - url "net/url" -) - -// Manager is an autogenerated mock type for the Manager type -type Manager struct { - mock.Mock -} - -// AddResponse provides a mock function with given fields: resp -func (_m *Manager) AddResponse(resp *http.Response) error { - ret := _m.Called(resp) - - if len(ret) == 0 { - panic("no return value specified for AddResponse") - } - - var r0 error - if rf, ok := ret.Get(0).(func(*http.Response) error); ok { - r0 = rf(resp) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// GetChallenges provides a mock function with given fields: endpoint -func (_m *Manager) GetChallenges(endpoint url.URL) ([]challenge.Challenge, error) { - ret := _m.Called(endpoint) - - if len(ret) == 0 { - panic("no return value specified for GetChallenges") - } - - var r0 []challenge.Challenge - var r1 error - if rf, ok := ret.Get(0).(func(url.URL) ([]challenge.Challenge, error)); ok { - return rf(endpoint) - } - if rf, ok := ret.Get(0).(func(url.URL) []challenge.Challenge); ok { - r0 = rf(endpoint) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]challenge.Challenge) - } - } - - if rf, ok := ret.Get(1).(func(url.URL) error); ok { - r1 = rf(endpoint) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// NewManager creates a new instance of Manager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewManager(t interface { - mock.TestingT - Cleanup(func()) -}) *Manager { - mock := &Manager{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/mocks/Manifest.go b/pkg/registry/mocks/Manifest.go deleted file mode 100644 index 8b92f369..00000000 --- a/pkg/registry/mocks/Manifest.go +++ /dev/null @@ -1,84 +0,0 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. - -package mocks - -import ( - distribution "github.com/distribution/distribution/v3" - mock "github.com/stretchr/testify/mock" -) - -// Manifest is an autogenerated mock type for the Manifest type -type Manifest struct { - mock.Mock -} - -// Payload provides a mock function with given fields: -func (_m *Manifest) Payload() (string, []byte, error) { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for Payload") - } - - var r0 string - var r1 []byte - var r2 error - if rf, ok := ret.Get(0).(func() (string, []byte, error)); ok { - return rf() - } - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - if rf, ok := ret.Get(1).(func() []byte); ok { - r1 = rf() - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).([]byte) - } - } - - if rf, ok := ret.Get(2).(func() error); ok { - r2 = rf() - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// References provides a mock function with given fields: -func (_m *Manifest) References() []distribution.Descriptor { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for References") - } - - var r0 []distribution.Descriptor - if rf, ok := ret.Get(0).(func() []distribution.Descriptor); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]distribution.Descriptor) - } - } - - return r0 -} - -// NewManifest creates a new instance of Manifest. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewManifest(t interface { - mock.TestingT - Cleanup(func()) -}) *Manifest { - mock := &Manifest{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/mocks/ManifestService.go b/pkg/registry/mocks/ManifestService.go deleted file mode 100644 index cb42e32d..00000000 --- a/pkg/registry/mocks/ManifestService.go +++ /dev/null @@ -1,149 +0,0 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. - -package mocks - -import ( - context "context" - - distribution "github.com/distribution/distribution/v3" - digest "github.com/opencontainers/go-digest" - - mock "github.com/stretchr/testify/mock" -) - -// ManifestService is an autogenerated mock type for the ManifestService type -type ManifestService struct { - mock.Mock -} - -// Delete provides a mock function with given fields: ctx, dgst -func (_m *ManifestService) Delete(ctx context.Context, dgst digest.Digest) error { - ret := _m.Called(ctx, dgst) - - if len(ret) == 0 { - panic("no return value specified for Delete") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, digest.Digest) error); ok { - r0 = rf(ctx, dgst) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// Exists provides a mock function with given fields: ctx, dgst -func (_m *ManifestService) Exists(ctx context.Context, dgst digest.Digest) (bool, error) { - ret := _m.Called(ctx, dgst) - - if len(ret) == 0 { - panic("no return value specified for Exists") - } - - var r0 bool - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, digest.Digest) (bool, error)); ok { - return rf(ctx, dgst) - } - if rf, ok := ret.Get(0).(func(context.Context, digest.Digest) bool); ok { - r0 = rf(ctx, dgst) - } else { - r0 = ret.Get(0).(bool) - } - - if rf, ok := ret.Get(1).(func(context.Context, digest.Digest) error); ok { - r1 = rf(ctx, dgst) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Get provides a mock function with given fields: ctx, dgst, options -func (_m *ManifestService) Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error) { - _va := make([]interface{}, len(options)) - for _i := range options { - _va[_i] = options[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx, dgst) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for Get") - } - - var r0 distribution.Manifest - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, digest.Digest, ...distribution.ManifestServiceOption) (distribution.Manifest, error)); ok { - return rf(ctx, dgst, options...) - } - if rf, ok := ret.Get(0).(func(context.Context, digest.Digest, ...distribution.ManifestServiceOption) distribution.Manifest); ok { - r0 = rf(ctx, dgst, options...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(distribution.Manifest) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, digest.Digest, ...distribution.ManifestServiceOption) error); ok { - r1 = rf(ctx, dgst, options...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Put provides a mock function with given fields: ctx, manifest, options -func (_m *ManifestService) Put(ctx context.Context, manifest distribution.Manifest, options ...distribution.ManifestServiceOption) (digest.Digest, error) { - _va := make([]interface{}, len(options)) - for _i := range options { - _va[_i] = options[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx, manifest) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for Put") - } - - var r0 digest.Digest - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, distribution.Manifest, ...distribution.ManifestServiceOption) (digest.Digest, error)); ok { - return rf(ctx, manifest, options...) - } - if rf, ok := ret.Get(0).(func(context.Context, distribution.Manifest, ...distribution.ManifestServiceOption) digest.Digest); ok { - r0 = rf(ctx, manifest, options...) - } else { - r0 = ret.Get(0).(digest.Digest) - } - - if rf, ok := ret.Get(1).(func(context.Context, distribution.Manifest, ...distribution.ManifestServiceOption) error); ok { - r1 = rf(ctx, manifest, options...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// NewManifestService creates a new instance of ManifestService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewManifestService(t interface { - mock.TestingT - Cleanup(func()) -}) *ManifestService { - mock := &ManifestService{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/mocks/RegistryClient.go b/pkg/registry/mocks/RegistryClient.go deleted file mode 100644 index 2943b045..00000000 --- a/pkg/registry/mocks/RegistryClient.go +++ /dev/null @@ -1,125 +0,0 @@ -// Code generated by mockery v1.1.2. DO NOT EDIT. - -package mocks - -import ( - distribution "github.com/distribution/distribution/v3" - digest "github.com/opencontainers/go-digest" - - mock "github.com/stretchr/testify/mock" - - options "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/options" - - tag "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/tag" -) - -// RegistryClient is an autogenerated mock type for the RegistryClient type -type RegistryClient struct { - mock.Mock -} - -// ManifestForDigest provides a mock function with given fields: dgst -func (_m *RegistryClient) ManifestForDigest(dgst digest.Digest) (distribution.Manifest, error) { - ret := _m.Called(dgst) - - var r0 distribution.Manifest - if rf, ok := ret.Get(0).(func(digest.Digest) distribution.Manifest); ok { - r0 = rf(dgst) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(distribution.Manifest) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(digest.Digest) error); ok { - r1 = rf(dgst) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// ManifestForTag provides a mock function with given fields: tagStr -func (_m *RegistryClient) ManifestForTag(tagStr string) (distribution.Manifest, error) { - ret := _m.Called(tagStr) - - var r0 distribution.Manifest - if rf, ok := ret.Get(0).(func(string) distribution.Manifest); ok { - r0 = rf(tagStr) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(distribution.Manifest) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(tagStr) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// NewRepository provides a mock function with given fields: nameInRepository -func (_m *RegistryClient) NewRepository(nameInRepository string) error { - ret := _m.Called(nameInRepository) - - var r0 error - if rf, ok := ret.Get(0).(func(string) error); ok { - r0 = rf(nameInRepository) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// TagMetadata provides a mock function with given fields: manifest, opts -func (_m *RegistryClient) TagMetadata(manifest distribution.Manifest, opts *options.ManifestOptions) (*tag.TagInfo, error) { - ret := _m.Called(manifest, opts) - - var r0 *tag.TagInfo - if rf, ok := ret.Get(0).(func(distribution.Manifest, *options.ManifestOptions) *tag.TagInfo); ok { - r0 = rf(manifest, opts) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*tag.TagInfo) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(distribution.Manifest, *options.ManifestOptions) error); ok { - r1 = rf(manifest, opts) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Tags provides a mock function with given fields: -func (_m *RegistryClient) Tags() ([]string, error) { - ret := _m.Called() - - var r0 []string - if rf, ok := ret.Get(0).(func() []string); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]string) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func() error); ok { - r1 = rf() - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} diff --git a/pkg/registry/mocks/Repository.go b/pkg/registry/mocks/Repository.go deleted file mode 100644 index 04d9b7c3..00000000 --- a/pkg/registry/mocks/Repository.go +++ /dev/null @@ -1,128 +0,0 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. - -package mocks - -import ( - context "context" - - distribution "github.com/distribution/distribution/v3" - mock "github.com/stretchr/testify/mock" - - reference "github.com/distribution/distribution/v3/reference" -) - -// Repository is an autogenerated mock type for the Repository type -type Repository struct { - mock.Mock -} - -// Blobs provides a mock function with given fields: ctx -func (_m *Repository) Blobs(ctx context.Context) distribution.BlobStore { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for Blobs") - } - - var r0 distribution.BlobStore - if rf, ok := ret.Get(0).(func(context.Context) distribution.BlobStore); ok { - r0 = rf(ctx) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(distribution.BlobStore) - } - } - - return r0 -} - -// Manifests provides a mock function with given fields: ctx, options -func (_m *Repository) Manifests(ctx context.Context, options ...distribution.ManifestServiceOption) (distribution.ManifestService, error) { - _va := make([]interface{}, len(options)) - for _i := range options { - _va[_i] = options[_i] - } - var _ca []interface{} - _ca = append(_ca, ctx) - _ca = append(_ca, _va...) - ret := _m.Called(_ca...) - - if len(ret) == 0 { - panic("no return value specified for Manifests") - } - - var r0 distribution.ManifestService - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, ...distribution.ManifestServiceOption) (distribution.ManifestService, error)); ok { - return rf(ctx, options...) - } - if rf, ok := ret.Get(0).(func(context.Context, ...distribution.ManifestServiceOption) distribution.ManifestService); ok { - r0 = rf(ctx, options...) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(distribution.ManifestService) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, ...distribution.ManifestServiceOption) error); ok { - r1 = rf(ctx, options...) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Named provides a mock function with given fields: -func (_m *Repository) Named() reference.Named { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for Named") - } - - var r0 reference.Named - if rf, ok := ret.Get(0).(func() reference.Named); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(reference.Named) - } - } - - return r0 -} - -// Tags provides a mock function with given fields: ctx -func (_m *Repository) Tags(ctx context.Context) distribution.TagService { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for Tags") - } - - var r0 distribution.TagService - if rf, ok := ret.Get(0).(func(context.Context) distribution.TagService); ok { - r0 = rf(ctx) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(distribution.TagService) - } - } - - return r0 -} - -// NewRepository creates a new instance of Repository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewRepository(t interface { - mock.TestingT - Cleanup(func()) -}) *Repository { - mock := &Repository{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/mocks/RoundTripper.go b/pkg/registry/mocks/RoundTripper.go deleted file mode 100644 index 27e22c25..00000000 --- a/pkg/registry/mocks/RoundTripper.go +++ /dev/null @@ -1,58 +0,0 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. - -package mocks - -import ( - http "net/http" - - mock "github.com/stretchr/testify/mock" -) - -// RoundTripper is an autogenerated mock type for the RoundTripper type -type RoundTripper struct { - mock.Mock -} - -// RoundTrip provides a mock function with given fields: _a0 -func (_m *RoundTripper) RoundTrip(_a0 *http.Request) (*http.Response, error) { - ret := _m.Called(_a0) - - if len(ret) == 0 { - panic("no return value specified for RoundTrip") - } - - var r0 *http.Response - var r1 error - if rf, ok := ret.Get(0).(func(*http.Request) (*http.Response, error)); ok { - return rf(_a0) - } - if rf, ok := ret.Get(0).(func(*http.Request) *http.Response); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*http.Response) - } - } - - if rf, ok := ret.Get(1).(func(*http.Request) error); ok { - r1 = rf(_a0) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// NewRoundTripper creates a new instance of RoundTripper. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewRoundTripper(t interface { - mock.TestingT - Cleanup(func()) -}) *RoundTripper { - mock := &RoundTripper{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/mocks/TagService.go b/pkg/registry/mocks/TagService.go deleted file mode 100644 index 50378081..00000000 --- a/pkg/registry/mocks/TagService.go +++ /dev/null @@ -1,153 +0,0 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. - -package mocks - -import ( - context "context" - - distribution "github.com/distribution/distribution/v3" - mock "github.com/stretchr/testify/mock" -) - -// TagService is an autogenerated mock type for the TagService type -type TagService struct { - mock.Mock -} - -// All provides a mock function with given fields: ctx -func (_m *TagService) All(ctx context.Context) ([]string, error) { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for All") - } - - var r0 []string - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) ([]string, error)); ok { - return rf(ctx) - } - if rf, ok := ret.Get(0).(func(context.Context) []string); ok { - r0 = rf(ctx) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]string) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Get provides a mock function with given fields: ctx, tag -func (_m *TagService) Get(ctx context.Context, tag string) (distribution.Descriptor, error) { - ret := _m.Called(ctx, tag) - - if len(ret) == 0 { - panic("no return value specified for Get") - } - - var r0 distribution.Descriptor - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) (distribution.Descriptor, error)); ok { - return rf(ctx, tag) - } - if rf, ok := ret.Get(0).(func(context.Context, string) distribution.Descriptor); ok { - r0 = rf(ctx, tag) - } else { - r0 = ret.Get(0).(distribution.Descriptor) - } - - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, tag) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Lookup provides a mock function with given fields: ctx, digest -func (_m *TagService) Lookup(ctx context.Context, digest distribution.Descriptor) ([]string, error) { - ret := _m.Called(ctx, digest) - - if len(ret) == 0 { - panic("no return value specified for Lookup") - } - - var r0 []string - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, distribution.Descriptor) ([]string, error)); ok { - return rf(ctx, digest) - } - if rf, ok := ret.Get(0).(func(context.Context, distribution.Descriptor) []string); ok { - r0 = rf(ctx, digest) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]string) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, distribution.Descriptor) error); ok { - r1 = rf(ctx, digest) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Tag provides a mock function with given fields: ctx, tag, desc -func (_m *TagService) Tag(ctx context.Context, tag string, desc distribution.Descriptor) error { - ret := _m.Called(ctx, tag, desc) - - if len(ret) == 0 { - panic("no return value specified for Tag") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, distribution.Descriptor) error); ok { - r0 = rf(ctx, tag, desc) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// Untag provides a mock function with given fields: ctx, tag -func (_m *TagService) Untag(ctx context.Context, tag string) error { - ret := _m.Called(ctx, tag) - - if len(ret) == 0 { - panic("no return value specified for Untag") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = rf(ctx, tag) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// NewTagService creates a new instance of TagService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewTagService(t interface { - mock.TestingT - Cleanup(func()) -}) *TagService { - mock := &TagService{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go deleted file mode 100644 index 7300bd32..00000000 --- a/pkg/registry/registry.go +++ /dev/null @@ -1,222 +0,0 @@ -package registry - -// Package registry implements functions for retrieving data from container -// registries. -// -// TODO: Refactor this package and provide mocks for better testing. - -import ( - "context" - "fmt" - "strings" - "sync" - "time" - - "github.com/distribution/distribution/v3" - - "golang.org/x/sync/semaphore" - - "github.com/argoproj-labs/argocd-image-updater/pkg/image" - "github.com/argoproj-labs/argocd-image-updater/pkg/kube" - "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/log" - "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/tag" -) - -const ( - MaxMetadataConcurrency = 20 -) - -// GetTags returns a list of available tags for the given image -func (endpoint *RegistryEndpoint) GetTags(img *image.ContainerImage, regClient RegistryClient, vc *image.VersionConstraint) (*tag.ImageTagList, error) { - var tagList *tag.ImageTagList = tag.NewImageTagList() - var err error - - logCtx := vc.Options.Logger() - - // Some registries have a default namespace that is used when the image name - // doesn't specify one. For example at Docker Hub, this is 'library'. - var nameInRegistry string - if len := len(strings.Split(img.ImageName, "/")); len == 1 && endpoint.DefaultNS != "" { - nameInRegistry = endpoint.DefaultNS + "/" + img.ImageName - logCtx.Debugf("Using canonical image name '%s' for image '%s'", nameInRegistry, img.ImageName) - } else { - nameInRegistry = img.ImageName - } - err = regClient.NewRepository(nameInRegistry) - if err != nil { - return nil, err - } - tTags, err := regClient.Tags() - if err != nil { - return nil, err - } - - tags := []string{} - - // For digest strategy, we do require a version constraint - if vc.Strategy.NeedsVersionConstraint() && vc.Constraint == "" { - return nil, fmt.Errorf("cannot use update strategy 'digest' for image '%s' without a version constraint", img.Original()) - } - - // Loop through tags, removing those we do not want. If update strategy is - // digest, all but the constraint tag are ignored. - if vc.MatchFunc != nil || len(vc.IgnoreList) > 0 || vc.Strategy.WantsOnlyConstraintTag() { - for _, t := range tTags { - if (vc.MatchFunc != nil && !vc.MatchFunc(t, vc.MatchArgs)) || vc.IsTagIgnored(t) || (vc.Strategy.WantsOnlyConstraintTag() && t != vc.Constraint) { - logCtx.Tracef("Removing tag %s because it either didn't match defined pattern or is ignored", t) - } else { - tags = append(tags, t) - } - } - } else { - tags = tTags - } - - // In some cases, we don't need to fetch the metadata to get the creation time - // stamp of from the image's meta data: - // - // - We use an update strategy other than latest or digest - // - The registry doesn't provide meta data and has tags sorted already - // - // We just create a dummy time stamp according to the registry's sort mode, if - // set. - if (vc.Strategy != image.StrategyNewestBuild && vc.Strategy != image.StrategyDigest) || endpoint.TagListSort.IsTimeSorted() { - for i, tagStr := range tags { - var ts int - if endpoint.TagListSort == TagListSortLatestFirst { - ts = len(tags) - i - } else if endpoint.TagListSort == TagListSortLatestLast { - ts = i - } - imgTag := tag.NewImageTag(tagStr, time.Unix(int64(ts), 0), "") - tagList.Add(imgTag) - } - return tagList, nil - } - - sem := semaphore.NewWeighted(int64(MaxMetadataConcurrency)) - tagListLock := &sync.RWMutex{} - - var wg sync.WaitGroup - wg.Add(len(tags)) - - // Fetch the manifest for the tag -- we need v1, because it contains history - // information that we require. - i := 0 - for _, tagStr := range tags { - i += 1 - // Look into the cache first and re-use any found item. If GetTag() returns - // an error, we treat it as a cache miss and just go ahead to invalidate - // the entry. - if vc.Strategy.IsCacheable() { - imgTag, err := endpoint.Cache.GetTag(nameInRegistry, tagStr) - if err != nil { - log.Warnf("invalid entry for %s:%s in cache, invalidating.", nameInRegistry, imgTag.TagName) - } else if imgTag != nil { - logCtx.Debugf("Cache hit for %s:%s", nameInRegistry, imgTag.TagName) - tagListLock.Lock() - tagList.Add(imgTag) - tagListLock.Unlock() - wg.Done() - continue - } - } - - logCtx.Tracef("Getting manifest for image %s:%s (operation %d/%d)", nameInRegistry, tagStr, i, len(tags)) - - lockErr := sem.Acquire(context.TODO(), 1) - if lockErr != nil { - log.Warnf("could not acquire semaphore: %v", lockErr) - wg.Done() - continue - } - logCtx.Tracef("acquired metadata semaphore") - - go func(tagStr string) { - defer func() { - sem.Release(1) - wg.Done() - log.Tracef("released semaphore and terminated waitgroup") - }() - - var ml distribution.Manifest - var err error - - // We first try to fetch a V2 manifest, and if that's not available we fall - // back to fetching V1 manifest. If that fails also, we just skip this tag. - if ml, err = regClient.ManifestForTag(tagStr); err != nil { - logCtx.Errorf("Error fetching metadata for %s:%s - neither V1 or V2 or OCI manifest returned by registry: %v", nameInRegistry, tagStr, err) - return - } - - // Parse required meta data from the manifest. The metadata contains all - // information needed to decide whether to consider this tag or not. - ti, err := regClient.TagMetadata(ml, vc.Options) - if err != nil { - logCtx.Errorf("error fetching metadata for %s:%s: %v", nameInRegistry, tagStr, err) - return - } - if ti == nil { - logCtx.Debugf("No metadata found for %s:%s", nameInRegistry, tagStr) - return - } - - logCtx.Tracef("Found date %s", ti.CreatedAt.String()) - var imgTag *tag.ImageTag - if vc.Strategy == image.StrategyDigest { - imgTag = tag.NewImageTag(tagStr, ti.CreatedAt, fmt.Sprintf("sha256:%x", ti.Digest)) - } else { - imgTag = tag.NewImageTag(tagStr, ti.CreatedAt, "") - } - tagListLock.Lock() - tagList.Add(imgTag) - tagListLock.Unlock() - endpoint.Cache.SetTag(nameInRegistry, imgTag) - }(tagStr) - } - - wg.Wait() - return tagList, err -} - -func (ep *RegistryEndpoint) expireCredentials() bool { - if ep.Credentials != "" && !ep.CredsUpdated.IsZero() && ep.CredsExpire > 0 && time.Since(ep.CredsUpdated) >= ep.CredsExpire { - ep.Username = "" - ep.Password = "" - return true - } - return false -} - -// Sets endpoint credentials for this registry from a reference to a K8s secret -func (ep *RegistryEndpoint) SetEndpointCredentials(kubeClient *kube.KubernetesClient) error { - if ep.expireCredentials() { - log.Debugf("expired credentials for registry %s (updated:%s, expiry:%0fs)", ep.RegistryAPI, ep.CredsUpdated, ep.CredsExpire.Seconds()) - } - if ep.Username == "" && ep.Password == "" && ep.Credentials != "" { - credSrc, err := image.ParseCredentialSource(ep.Credentials, false) - if err != nil { - return err - } - - // For fetching credentials, we must have working Kubernetes client. - if (credSrc.Type == image.CredentialSourcePullSecret || credSrc.Type == image.CredentialSourceSecret) && kubeClient == nil { - log.WithContext(). - AddField("registry", ep.RegistryAPI). - Warnf("cannot use K8s credentials without Kubernetes client") - return fmt.Errorf("could not fetch image tags") - } - - creds, err := credSrc.FetchCredentials(ep.RegistryAPI, kubeClient) - if err != nil { - return err - } - - ep.CredsUpdated = time.Now() - - ep.Username = creds.Username - ep.Password = creds.Password - } - - return nil -} diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go deleted file mode 100644 index 241799fc..00000000 --- a/pkg/registry/registry_test.go +++ /dev/null @@ -1,162 +0,0 @@ -package registry - -import ( - "os" - "testing" - "time" - - "github.com/argoproj-labs/argocd-image-updater/pkg/image" - "github.com/argoproj-labs/argocd-image-updater/pkg/registry/mocks" - "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/options" - "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/tag" - - "github.com/distribution/distribution/v3/manifest/schema1" //nolint:staticcheck - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -func Test_GetTags(t *testing.T) { - - t.Run("Check for correctly returned tags with semver sort", func(t *testing.T) { - regClient := mocks.RegistryClient{} - regClient.On("NewRepository", mock.Anything).Return(nil) - regClient.On("Tags", mock.Anything).Return([]string{"1.2.0", "1.2.1", "1.2.2"}, nil) - - ep, err := GetRegistryEndpoint("") - require.NoError(t, err) - - img := image.NewFromIdentifier("foo/bar:1.2.0") - - tl, err := ep.GetTags(img, ®Client, &image.VersionConstraint{Strategy: image.StrategySemVer, Options: options.NewManifestOptions()}) - require.NoError(t, err) - assert.NotEmpty(t, tl) - - tag, err := ep.Cache.GetTag("foo/bar", "1.2.1") - require.NoError(t, err) - assert.Nil(t, tag) - }) - - t.Run("Check for correctly returned tags with filter function applied", func(t *testing.T) { - regClient := mocks.RegistryClient{} - regClient.On("NewRepository", mock.Anything).Return(nil) - regClient.On("Tags", mock.Anything).Return([]string{"1.2.0", "1.2.1", "1.2.2"}, nil) - - ep, err := GetRegistryEndpoint("") - require.NoError(t, err) - - img := image.NewFromIdentifier("foo/bar:1.2.0") - - tl, err := ep.GetTags(img, ®Client, &image.VersionConstraint{ - Strategy: image.StrategySemVer, - MatchFunc: image.MatchFuncNone, - Options: options.NewManifestOptions()}) - require.NoError(t, err) - assert.Empty(t, tl.Tags()) - - tag, err := ep.Cache.GetTag("foo/bar", "1.2.1") - require.NoError(t, err) - assert.Nil(t, tag) - }) - - t.Run("Check for correctly returned tags with name sort", func(t *testing.T) { - - regClient := mocks.RegistryClient{} - regClient.On("NewRepository", mock.Anything).Return(nil) - regClient.On("Tags", mock.Anything).Return([]string{"1.2.0", "1.2.1", "1.2.2"}, nil) - - ep, err := GetRegistryEndpoint("") - require.NoError(t, err) - - img := image.NewFromIdentifier("foo/bar:1.2.0") - - tl, err := ep.GetTags(img, ®Client, &image.VersionConstraint{Strategy: image.StrategyAlphabetical, Options: options.NewManifestOptions()}) - require.NoError(t, err) - assert.NotEmpty(t, tl) - - tag, err := ep.Cache.GetTag("foo/bar", "1.2.1") - require.NoError(t, err) - assert.Nil(t, tag) - }) - - t.Run("Check for correctly returned tags with latest sort", func(t *testing.T) { - ts := "2006-01-02T15:04:05.999999999Z" - meta1 := &schema1.SignedManifest{ //nolint:staticcheck - Manifest: schema1.Manifest{ //nolint:staticcheck - History: []schema1.History{ //nolint:staticcheck - { - V1Compatibility: `{"created":"` + ts + `"}`, - }, - }, - }, - } - - regClient := mocks.RegistryClient{} - regClient.On("NewRepository", mock.Anything).Return(nil) - regClient.On("Tags", mock.Anything).Return([]string{"1.2.0", "1.2.1", "1.2.2"}, nil) - regClient.On("ManifestForTag", mock.Anything, mock.Anything).Return(meta1, nil) - regClient.On("TagMetadata", mock.Anything, mock.Anything).Return(&tag.TagInfo{}, nil) - - ep, err := GetRegistryEndpoint("") - require.NoError(t, err) - ep.Cache.ClearCache() - - img := image.NewFromIdentifier("foo/bar:1.2.0") - tl, err := ep.GetTags(img, ®Client, &image.VersionConstraint{Strategy: image.StrategyNewestBuild, Options: options.NewManifestOptions()}) - require.NoError(t, err) - assert.NotEmpty(t, tl) - - tag, err := ep.Cache.GetTag("foo/bar", "1.2.1") - require.NoError(t, err) - require.NotNil(t, tag) - require.Equal(t, "1.2.1", tag.TagName) - }) - -} - -func Test_ExpireCredentials(t *testing.T) { - epYAML := ` -registries: -- name: GitHub Container Registry - api_url: https://ghcr.io - ping: no - prefix: ghcr.io - credentials: env:TEST_CREDS - credsexpire: 3s -` - t.Run("Expire credentials", func(t *testing.T) { - epl, err := ParseRegistryConfiguration(epYAML) - require.NoError(t, err) - require.Len(t, epl.Items, 1) - - // New registry configuration - err = AddRegistryEndpointFromConfig(epl.Items[0]) - require.NoError(t, err) - ep, err := GetRegistryEndpoint("ghcr.io") - require.NoError(t, err) - require.NotEqual(t, 0, ep.CredsExpire) - - // Initial creds - os.Setenv("TEST_CREDS", "foo:bar") - err = ep.SetEndpointCredentials(nil) - assert.NoError(t, err) - assert.Equal(t, "foo", ep.Username) - assert.Equal(t, "bar", ep.Password) - assert.False(t, ep.CredsUpdated.IsZero()) - - // Creds should still be cached - os.Setenv("TEST_CREDS", "bar:foo") - err = ep.SetEndpointCredentials(nil) - assert.NoError(t, err) - assert.Equal(t, "foo", ep.Username) - assert.Equal(t, "bar", ep.Password) - - // Pretend 5 minutes have passed - creds have expired and are re-read from env - ep.CredsUpdated = ep.CredsUpdated.Add(time.Minute * -5) - err = ep.SetEndpointCredentials(nil) - assert.NoError(t, err) - assert.Equal(t, "bar", ep.Username) - assert.Equal(t, "foo", ep.Password) - }) - -}