Skip to content

Commit 71e0fcc

Browse files
committed
feat(registry): Add generic registry for source, not target
This allows auth to a generic source registry, such as dockerhub. This partially solves estahn#50
1 parent 5fa9e57 commit 71e0fcc

File tree

9 files changed

+215
-7
lines changed

9 files changed

+215
-7
lines changed

cmd/root.go

+5
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,11 @@ func initConfig() {
251251
log.Err(err).Msg("failed to unmarshal the config file")
252252
}
253253

254+
if err := config.CheckTargetRegistryConfiguration(cfg.Target); err != nil {
255+
log.Err(err).Msg("invalid target configuration")
256+
os.Exit(1)
257+
}
258+
254259
//validate := validator.New()
255260
//if err := validate.Struct(cfg); err != nil {
256261
// validationErrors := err.(validator.ValidationErrors)

pkg/config/config.go

+32-3
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,21 @@ type Source struct {
6060
}
6161

6262
type Registry struct {
63-
Type string `yaml:"type"`
64-
AWS AWS `yaml:"aws"`
65-
GCP GCP `yaml:"gcp"`
63+
Type string `yaml:"type"`
64+
AWS AWS `yaml:"aws"`
65+
GCP GCP `yaml:"gcp"`
66+
Generic Generic `yaml:"generic"`
67+
}
68+
69+
type Generic struct {
70+
Name string `yaml:"name"`
71+
GenericOptions GenericOptions `yaml:"genericOptions"`
72+
}
73+
74+
type GenericOptions struct {
75+
Domain string `yaml:"domain"`
76+
Username string `yaml:"username"`
77+
Password string `yaml:"password"`
6678
}
6779

6880
type AWS struct {
@@ -109,13 +121,19 @@ func (g *GCP) GarDomain() string {
109121
return fmt.Sprintf("%s-docker.pkg.dev/%s/%s", g.Location, g.ProjectID, g.RepositoryID)
110122
}
111123

124+
func (g *Generic) GenericDomain() string {
125+
return g.GenericOptions.Domain
126+
}
127+
112128
func (r Registry) Domain() string {
113129
registry, _ := types.ParseRegistry(r.Type)
114130
switch registry {
115131
case types.RegistryAWS:
116132
return r.AWS.EcrDomain()
117133
case types.RegistryGCP:
118134
return r.GCP.GarDomain()
135+
case types.RegistryGeneric:
136+
return r.Generic.GenericDomain()
119137
default:
120138
return ""
121139
}
@@ -155,6 +173,17 @@ func CheckRegistryConfiguration(r Registry) error {
155173
return nil
156174
}
157175

176+
// provides detailed information about wrongly provided configuration (target specific)
177+
func CheckTargetRegistryConfiguration(r Registry) error {
178+
registryType, err := types.ParseRegistry(r.Type)
179+
if err != nil {
180+
return fmt.Errorf("couldn't parse target registry type")
181+
} else if registryType == types.RegistryGeneric {
182+
return fmt.Errorf("generic registry not allowed as target: %s", r.Generic.Name)
183+
}
184+
return nil
185+
}
186+
158187
// SetViperDefaults configures default values for config items that are not set.
159188
func SetViperDefaults(v *viper.Viper) {
160189
v.SetDefault("Target.Type", "aws")

pkg/config/config_test.go

+42
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,48 @@ source:
149149
},
150150
},
151151
},
152+
{
153+
name: "should render generic source registry",
154+
cfg: `
155+
source:
156+
registries:
157+
- type: "generic"
158+
generic:
159+
name: "dockerio"
160+
genericOptions:
161+
domain: "docker.io"
162+
username: "testuser"
163+
password: "testpass"
164+
`,
165+
expCfg: Config{
166+
Target: Registry{
167+
Type: "aws",
168+
AWS: AWS{
169+
ECROptions: ECROptions{
170+
ImageTagMutability: "MUTABLE",
171+
ImageScanningConfiguration: ImageScanningConfiguration{
172+
ImageScanOnPush: true,
173+
},
174+
},
175+
},
176+
},
177+
Source: Source{
178+
Registries: []Registry{
179+
{
180+
Type: "generic",
181+
Generic: Generic{
182+
Name: "dockerio",
183+
GenericOptions: GenericOptions{
184+
Domain: "docker.io",
185+
Username: "testuser",
186+
Password: "testpass",
187+
},
188+
},
189+
},
190+
},
191+
},
192+
},
193+
},
152194
{
153195
name: "should use previous defaults",
154196
cfg: `

pkg/registry/client.go

+2
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ func NewClient(r config.Registry) (Client, error) {
5353
return NewECRClient(r.AWS)
5454
case types.RegistryGCP:
5555
return NewGARClient(r.GCP)
56+
case types.RegistryGeneric:
57+
return NewGenericClient(r.Generic)
5658
default:
5759
return nil, fmt.Errorf(`registry of type "%s" is not supported`, r.Type)
5860
}

pkg/registry/generic.go

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package registry
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
ctypes "github.com/containers/image/v5/types"
8+
"github.com/estahn/k8s-image-swapper/pkg/config"
9+
)
10+
11+
type GenericClient struct {
12+
options config.GenericOptions
13+
}
14+
15+
func NewGenericClient(clientConfig config.Generic) (*GenericClient, error) {
16+
client := GenericClient{}
17+
18+
client.options = clientConfig.GenericOptions
19+
20+
return &client, nil
21+
}
22+
23+
func (g *GenericClient) CreateRepository(ctx context.Context, name string) error {
24+
return nil
25+
}
26+
27+
func (g *GenericClient) RepositoryExists() bool {
28+
return true
29+
}
30+
31+
func (g *GenericClient) CopyImage(ctx context.Context, src ctypes.ImageReference, srcCreds string, dest ctypes.ImageReference, destCreds string) error {
32+
panic("implement me")
33+
}
34+
35+
func (g *GenericClient) PullImage() error {
36+
panic("implement me")
37+
}
38+
39+
func (g *GenericClient) PutImage() error {
40+
panic("implement me")
41+
}
42+
43+
func (g *GenericClient) ImageExists(ctx context.Context, ref ctypes.ImageReference) bool {
44+
return true
45+
}
46+
47+
// Endpoint returns the domain of the registry
48+
func (g *GenericClient) Endpoint() string {
49+
return g.options.Domain
50+
}
51+
52+
func (g *GenericClient) Credentials() string {
53+
return fmt.Sprintf("%s:%s", g.options.Username, g.options.Password)
54+
}
55+
56+
// IsOrigin returns true if the imageRef originates from this registry
57+
func (g *GenericClient) IsOrigin(imageRef ctypes.ImageReference) bool {
58+
return true
59+
}
60+
61+
// For testing purposes
62+
func NewDummyGenericClient(domain string, options config.GenericOptions) *GenericClient {
63+
return &GenericClient{
64+
options: options,
65+
}
66+
}

pkg/registry/generic_test.go

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package registry
2+
3+
import (
4+
"encoding/base64"
5+
"testing"
6+
7+
"github.com/estahn/k8s-image-swapper/pkg/config"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestGenericDockerConfig(t *testing.T) {
12+
fakeToken := []byte("username:password")
13+
fakeBase64Token := base64.StdEncoding.EncodeToString(fakeToken)
14+
15+
expected := []byte("{\"auths\":{\"docker.io\":{\"auth\":\"" + fakeBase64Token + "\"}}}")
16+
17+
fakeRegistry := NewDummyGenericClient("docker.io", config.GenericOptions{
18+
Domain: "docker.io",
19+
Username: "username",
20+
Password: "password",
21+
})
22+
23+
r, _ := GenerateDockerConfig(fakeRegistry)
24+
25+
assert.Equal(t, r, expected)
26+
}

pkg/secrets/kubernetes.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func NewImagePullSecretsResultWithDefaults(defaultImagePullSecrets []registry.Cl
4242
if err != nil {
4343
log.Err(err)
4444
} else {
45-
imagePullSecretsResult.Add(fmt.Sprintf("source-ecr-%d", index), dockerConfig)
45+
imagePullSecretsResult.Add(fmt.Sprintf("source-registry-%d", index), dockerConfig)
4646
}
4747
}
4848
return imagePullSecretsResult

pkg/secrets/kubernetes_test.go

+37-2
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ func TestImagePullSecretsResult_WithDefault(t *testing.T) {
115115

116116
expected := &ImagePullSecretsResult{
117117
Secrets: map[string][]byte{
118-
"source-ecr-0": []byte("{\"auths\":{\"" + fakeEcrDomains[0] + "\":{\"auth\":\"" + fakeBase64Token + "\"}}}"),
119-
"source-ecr-1": []byte("{\"auths\":{\"" + fakeEcrDomains[1] + "\":{\"auth\":\"" + fakeBase64Token + "\"}}}"),
118+
"source-registry-0": []byte("{\"auths\":{\"" + fakeEcrDomains[0] + "\":{\"auth\":\"" + fakeBase64Token + "\"}}}"),
119+
"source-registry-1": []byte("{\"auths\":{\"" + fakeEcrDomains[1] + "\":{\"auth\":\"" + fakeBase64Token + "\"}}}"),
120120
},
121121
Aggregate: []byte("{\"auths\":{\"" + fakeEcrDomains[0] + "\":{\"auth\":\"" + fakeBase64Token + "\"},\"" + fakeEcrDomains[1] + "\":{\"auth\":\"" + fakeBase64Token + "\"}}}"),
122122
}
@@ -130,6 +130,41 @@ func TestImagePullSecretsResult_WithDefault(t *testing.T) {
130130
assert.Equal(t, r, expected)
131131
}
132132

133+
// TestImagePullSecretsResult_WithDefault tests if authenticated private registries work
134+
func TestImagePullSecretsResult_WithDefault_MixedRegistries(t *testing.T) {
135+
// Fake ECR Source Registry
136+
fakeToken := []byte("token")
137+
fakeBase64Token := base64.StdEncoding.EncodeToString(fakeToken)
138+
fakeAccountId := "12345678912"
139+
fakeRegion := "us-east-1"
140+
fakeEcrDomain := fmt.Sprintf("%s.dkr.ecr.%s.amazonaws.com", fakeAccountId, fakeRegion)
141+
142+
// Fake Generic Source Registry
143+
fakeGenericToken := []byte("username:password")
144+
fakeGenericBase64Token := base64.StdEncoding.EncodeToString(fakeGenericToken)
145+
fakeGenericDomain := "https://index.docker.io/v1/"
146+
147+
expected := &ImagePullSecretsResult{
148+
Secrets: map[string][]byte{
149+
"source-registry-0": []byte("{\"auths\":{\"" + fakeEcrDomain + "\":{\"auth\":\"" + fakeBase64Token + "\"}}}"),
150+
"source-registry-1": []byte("{\"auths\":{\"" + fakeGenericDomain + "\":{\"auth\":\"" + fakeGenericBase64Token + "\"}}}"),
151+
},
152+
Aggregate: []byte("{\"auths\":{\"" + fakeEcrDomain + "\":{\"auth\":\"" + fakeBase64Token + "\"},\"" + fakeGenericDomain + "\":{\"auth\":\"" + fakeGenericBase64Token + "\"}}}"),
153+
}
154+
155+
fakeRegistry1 := registry.NewDummyECRClient(fakeRegion, fakeAccountId, "", config.ECROptions{}, fakeToken)
156+
fakeRegistry2 := registry.NewDummyGenericClient("docker.io", config.GenericOptions{
157+
Domain: "https://index.docker.io/v1/",
158+
Username: "username",
159+
Password: "password",
160+
})
161+
fakeRegistries := []registry.Client{fakeRegistry1, fakeRegistry2}
162+
163+
r := NewImagePullSecretsResultWithDefaults(fakeRegistries)
164+
165+
assert.Equal(t, r, expected)
166+
}
167+
133168
// TestImagePullSecretsResult_Add tests if aggregation works
134169
func TestImagePullSecretsResult_Add(t *testing.T) {
135170
expected := &ImagePullSecretsResult{

pkg/types/types.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ const (
88
RegistryUnknown = iota
99
RegistryAWS
1010
RegistryGCP
11+
RegistryGeneric
1112
)
1213

1314
func (p Registry) String() string {
14-
return [...]string{"unknown", "aws", "gcp"}[p]
15+
return [...]string{"unknown", "aws", "gcp", "generic"}[p]
1516
}
1617

1718
func ParseRegistry(p string) (Registry, error) {
@@ -20,6 +21,8 @@ func ParseRegistry(p string) (Registry, error) {
2021
return RegistryAWS, nil
2122
case Registry(RegistryGCP).String():
2223
return RegistryGCP, nil
24+
case Registry(RegistryGeneric).String():
25+
return RegistryGeneric, nil
2326
}
2427
return RegistryUnknown, fmt.Errorf("unknown target registry string: '%s', defaulting to unknown", p)
2528
}

0 commit comments

Comments
 (0)