From 71e0fcc49810a120e9b394d361b839e67454c392 Mon Sep 17 00:00:00 2001 From: "Bradon Kanyid (rattboi)" Date: Wed, 29 May 2024 17:27:02 +0000 Subject: [PATCH] feat(registry): Add generic registry for source, not target This allows auth to a generic source registry, such as dockerhub. This partially solves #50 --- cmd/root.go | 5 +++ pkg/config/config.go | 35 ++++++++++++++++-- pkg/config/config_test.go | 42 ++++++++++++++++++++++ pkg/registry/client.go | 2 ++ pkg/registry/generic.go | 66 ++++++++++++++++++++++++++++++++++ pkg/registry/generic_test.go | 26 ++++++++++++++ pkg/secrets/kubernetes.go | 2 +- pkg/secrets/kubernetes_test.go | 39 ++++++++++++++++++-- pkg/types/types.go | 5 ++- 9 files changed, 215 insertions(+), 7 deletions(-) create mode 100644 pkg/registry/generic.go create mode 100644 pkg/registry/generic_test.go diff --git a/cmd/root.go b/cmd/root.go index 7d2121b4..eda843ee 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -251,6 +251,11 @@ func initConfig() { log.Err(err).Msg("failed to unmarshal the config file") } + if err := config.CheckTargetRegistryConfiguration(cfg.Target); err != nil { + log.Err(err).Msg("invalid target configuration") + os.Exit(1) + } + //validate := validator.New() //if err := validate.Struct(cfg); err != nil { // validationErrors := err.(validator.ValidationErrors) diff --git a/pkg/config/config.go b/pkg/config/config.go index 3dabc717..8c3a81a6 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -60,9 +60,21 @@ type Source struct { } type Registry struct { - Type string `yaml:"type"` - AWS AWS `yaml:"aws"` - GCP GCP `yaml:"gcp"` + Type string `yaml:"type"` + AWS AWS `yaml:"aws"` + GCP GCP `yaml:"gcp"` + Generic Generic `yaml:"generic"` +} + +type Generic struct { + Name string `yaml:"name"` + GenericOptions GenericOptions `yaml:"genericOptions"` +} + +type GenericOptions struct { + Domain string `yaml:"domain"` + Username string `yaml:"username"` + Password string `yaml:"password"` } type AWS struct { @@ -109,6 +121,10 @@ func (g *GCP) GarDomain() string { return fmt.Sprintf("%s-docker.pkg.dev/%s/%s", g.Location, g.ProjectID, g.RepositoryID) } +func (g *Generic) GenericDomain() string { + return g.GenericOptions.Domain +} + func (r Registry) Domain() string { registry, _ := types.ParseRegistry(r.Type) switch registry { @@ -116,6 +132,8 @@ func (r Registry) Domain() string { return r.AWS.EcrDomain() case types.RegistryGCP: return r.GCP.GarDomain() + case types.RegistryGeneric: + return r.Generic.GenericDomain() default: return "" } @@ -155,6 +173,17 @@ func CheckRegistryConfiguration(r Registry) error { return nil } +// provides detailed information about wrongly provided configuration (target specific) +func CheckTargetRegistryConfiguration(r Registry) error { + registryType, err := types.ParseRegistry(r.Type) + if err != nil { + return fmt.Errorf("couldn't parse target registry type") + } else if registryType == types.RegistryGeneric { + return fmt.Errorf("generic registry not allowed as target: %s", r.Generic.Name) + } + return nil +} + // SetViperDefaults configures default values for config items that are not set. func SetViperDefaults(v *viper.Viper) { v.SetDefault("Target.Type", "aws") diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 779dffe7..0785f5a2 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -149,6 +149,48 @@ source: }, }, }, + { + name: "should render generic source registry", + cfg: ` +source: + registries: + - type: "generic" + generic: + name: "dockerio" + genericOptions: + domain: "docker.io" + username: "testuser" + password: "testpass" +`, + expCfg: Config{ + Target: Registry{ + Type: "aws", + AWS: AWS{ + ECROptions: ECROptions{ + ImageTagMutability: "MUTABLE", + ImageScanningConfiguration: ImageScanningConfiguration{ + ImageScanOnPush: true, + }, + }, + }, + }, + Source: Source{ + Registries: []Registry{ + { + Type: "generic", + Generic: Generic{ + Name: "dockerio", + GenericOptions: GenericOptions{ + Domain: "docker.io", + Username: "testuser", + Password: "testpass", + }, + }, + }, + }, + }, + }, + }, { name: "should use previous defaults", cfg: ` diff --git a/pkg/registry/client.go b/pkg/registry/client.go index da373e2c..2bdf2b43 100644 --- a/pkg/registry/client.go +++ b/pkg/registry/client.go @@ -53,6 +53,8 @@ func NewClient(r config.Registry) (Client, error) { return NewECRClient(r.AWS) case types.RegistryGCP: return NewGARClient(r.GCP) + case types.RegistryGeneric: + return NewGenericClient(r.Generic) default: return nil, fmt.Errorf(`registry of type "%s" is not supported`, r.Type) } diff --git a/pkg/registry/generic.go b/pkg/registry/generic.go new file mode 100644 index 00000000..9755e66e --- /dev/null +++ b/pkg/registry/generic.go @@ -0,0 +1,66 @@ +package registry + +import ( + "context" + "fmt" + + ctypes "github.com/containers/image/v5/types" + "github.com/estahn/k8s-image-swapper/pkg/config" +) + +type GenericClient struct { + options config.GenericOptions +} + +func NewGenericClient(clientConfig config.Generic) (*GenericClient, error) { + client := GenericClient{} + + client.options = clientConfig.GenericOptions + + return &client, nil +} + +func (g *GenericClient) CreateRepository(ctx context.Context, name string) error { + return nil +} + +func (g *GenericClient) RepositoryExists() bool { + return true +} + +func (g *GenericClient) CopyImage(ctx context.Context, src ctypes.ImageReference, srcCreds string, dest ctypes.ImageReference, destCreds string) error { + panic("implement me") +} + +func (g *GenericClient) PullImage() error { + panic("implement me") +} + +func (g *GenericClient) PutImage() error { + panic("implement me") +} + +func (g *GenericClient) ImageExists(ctx context.Context, ref ctypes.ImageReference) bool { + return true +} + +// Endpoint returns the domain of the registry +func (g *GenericClient) Endpoint() string { + return g.options.Domain +} + +func (g *GenericClient) Credentials() string { + return fmt.Sprintf("%s:%s", g.options.Username, g.options.Password) +} + +// IsOrigin returns true if the imageRef originates from this registry +func (g *GenericClient) IsOrigin(imageRef ctypes.ImageReference) bool { + return true +} + +// For testing purposes +func NewDummyGenericClient(domain string, options config.GenericOptions) *GenericClient { + return &GenericClient{ + options: options, + } +} diff --git a/pkg/registry/generic_test.go b/pkg/registry/generic_test.go new file mode 100644 index 00000000..3eaa9cad --- /dev/null +++ b/pkg/registry/generic_test.go @@ -0,0 +1,26 @@ +package registry + +import ( + "encoding/base64" + "testing" + + "github.com/estahn/k8s-image-swapper/pkg/config" + "github.com/stretchr/testify/assert" +) + +func TestGenericDockerConfig(t *testing.T) { + fakeToken := []byte("username:password") + fakeBase64Token := base64.StdEncoding.EncodeToString(fakeToken) + + expected := []byte("{\"auths\":{\"docker.io\":{\"auth\":\"" + fakeBase64Token + "\"}}}") + + fakeRegistry := NewDummyGenericClient("docker.io", config.GenericOptions{ + Domain: "docker.io", + Username: "username", + Password: "password", + }) + + r, _ := GenerateDockerConfig(fakeRegistry) + + assert.Equal(t, r, expected) +} diff --git a/pkg/secrets/kubernetes.go b/pkg/secrets/kubernetes.go index 70f68478..eabf50c8 100644 --- a/pkg/secrets/kubernetes.go +++ b/pkg/secrets/kubernetes.go @@ -42,7 +42,7 @@ func NewImagePullSecretsResultWithDefaults(defaultImagePullSecrets []registry.Cl if err != nil { log.Err(err) } else { - imagePullSecretsResult.Add(fmt.Sprintf("source-ecr-%d", index), dockerConfig) + imagePullSecretsResult.Add(fmt.Sprintf("source-registry-%d", index), dockerConfig) } } return imagePullSecretsResult diff --git a/pkg/secrets/kubernetes_test.go b/pkg/secrets/kubernetes_test.go index 85c1e5cd..2c252880 100644 --- a/pkg/secrets/kubernetes_test.go +++ b/pkg/secrets/kubernetes_test.go @@ -115,8 +115,8 @@ func TestImagePullSecretsResult_WithDefault(t *testing.T) { expected := &ImagePullSecretsResult{ Secrets: map[string][]byte{ - "source-ecr-0": []byte("{\"auths\":{\"" + fakeEcrDomains[0] + "\":{\"auth\":\"" + fakeBase64Token + "\"}}}"), - "source-ecr-1": []byte("{\"auths\":{\"" + fakeEcrDomains[1] + "\":{\"auth\":\"" + fakeBase64Token + "\"}}}"), + "source-registry-0": []byte("{\"auths\":{\"" + fakeEcrDomains[0] + "\":{\"auth\":\"" + fakeBase64Token + "\"}}}"), + "source-registry-1": []byte("{\"auths\":{\"" + fakeEcrDomains[1] + "\":{\"auth\":\"" + fakeBase64Token + "\"}}}"), }, Aggregate: []byte("{\"auths\":{\"" + fakeEcrDomains[0] + "\":{\"auth\":\"" + fakeBase64Token + "\"},\"" + fakeEcrDomains[1] + "\":{\"auth\":\"" + fakeBase64Token + "\"}}}"), } @@ -130,6 +130,41 @@ func TestImagePullSecretsResult_WithDefault(t *testing.T) { assert.Equal(t, r, expected) } +// TestImagePullSecretsResult_WithDefault tests if authenticated private registries work +func TestImagePullSecretsResult_WithDefault_MixedRegistries(t *testing.T) { + // Fake ECR Source Registry + fakeToken := []byte("token") + fakeBase64Token := base64.StdEncoding.EncodeToString(fakeToken) + fakeAccountId := "12345678912" + fakeRegion := "us-east-1" + fakeEcrDomain := fmt.Sprintf("%s.dkr.ecr.%s.amazonaws.com", fakeAccountId, fakeRegion) + + // Fake Generic Source Registry + fakeGenericToken := []byte("username:password") + fakeGenericBase64Token := base64.StdEncoding.EncodeToString(fakeGenericToken) + fakeGenericDomain := "https://index.docker.io/v1/" + + expected := &ImagePullSecretsResult{ + Secrets: map[string][]byte{ + "source-registry-0": []byte("{\"auths\":{\"" + fakeEcrDomain + "\":{\"auth\":\"" + fakeBase64Token + "\"}}}"), + "source-registry-1": []byte("{\"auths\":{\"" + fakeGenericDomain + "\":{\"auth\":\"" + fakeGenericBase64Token + "\"}}}"), + }, + Aggregate: []byte("{\"auths\":{\"" + fakeEcrDomain + "\":{\"auth\":\"" + fakeBase64Token + "\"},\"" + fakeGenericDomain + "\":{\"auth\":\"" + fakeGenericBase64Token + "\"}}}"), + } + + fakeRegistry1 := registry.NewDummyECRClient(fakeRegion, fakeAccountId, "", config.ECROptions{}, fakeToken) + fakeRegistry2 := registry.NewDummyGenericClient("docker.io", config.GenericOptions{ + Domain: "https://index.docker.io/v1/", + Username: "username", + Password: "password", + }) + fakeRegistries := []registry.Client{fakeRegistry1, fakeRegistry2} + + r := NewImagePullSecretsResultWithDefaults(fakeRegistries) + + assert.Equal(t, r, expected) +} + // TestImagePullSecretsResult_Add tests if aggregation works func TestImagePullSecretsResult_Add(t *testing.T) { expected := &ImagePullSecretsResult{ diff --git a/pkg/types/types.go b/pkg/types/types.go index 647395e5..dd146572 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -8,10 +8,11 @@ const ( RegistryUnknown = iota RegistryAWS RegistryGCP + RegistryGeneric ) func (p Registry) String() string { - return [...]string{"unknown", "aws", "gcp"}[p] + return [...]string{"unknown", "aws", "gcp", "generic"}[p] } func ParseRegistry(p string) (Registry, error) { @@ -20,6 +21,8 @@ func ParseRegistry(p string) (Registry, error) { return RegistryAWS, nil case Registry(RegistryGCP).String(): return RegistryGCP, nil + case Registry(RegistryGeneric).String(): + return RegistryGeneric, nil } return RegistryUnknown, fmt.Errorf("unknown target registry string: '%s', defaulting to unknown", p) }