Skip to content

Commit

Permalink
feat(registry): Add generic registry for source, not target
Browse files Browse the repository at this point in the history
This allows auth to a generic source registry, such as dockerhub.

This partially solves estahn#50
  • Loading branch information
rattboi committed May 29, 2024
1 parent 5fa9e57 commit 71e0fcc
Show file tree
Hide file tree
Showing 9 changed files with 215 additions and 7 deletions.
5 changes: 5 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
35 changes: 32 additions & 3 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -109,13 +121,19 @@ 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 {
case types.RegistryAWS:
return r.AWS.EcrDomain()
case types.RegistryGCP:
return r.GCP.GarDomain()
case types.RegistryGeneric:
return r.Generic.GenericDomain()
default:
return ""
}
Expand Down Expand Up @@ -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")
Expand Down
42 changes: 42 additions & 0 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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: `
Expand Down
2 changes: 2 additions & 0 deletions pkg/registry/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
66 changes: 66 additions & 0 deletions pkg/registry/generic.go
Original file line number Diff line number Diff line change
@@ -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,
}
}
26 changes: 26 additions & 0 deletions pkg/registry/generic_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
2 changes: 1 addition & 1 deletion pkg/secrets/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 37 additions & 2 deletions pkg/secrets/kubernetes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 + "\"}}}"),
}
Expand All @@ -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{
Expand Down
5 changes: 4 additions & 1 deletion pkg/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
}
Expand Down

0 comments on commit 71e0fcc

Please sign in to comment.