Skip to content

Commit 32edc11

Browse files
authored
Support for managed pulling from private ECRs (#394)
This PR adds the possibility to set additional references to ECRs as sources in the configuration file. This change allows to take advantage of the logic already in place to rotate ECR tokens and authenticate to private registries, but for pulling images from private ECRs in a context of multi ECR replication. The same AWS credentials as for the target ECR are used. #### Main changes: - Updated the configuration to add the field `privateRegistries` in `source` - Added a new attribute to the `imagePullSecretProviders` containing the registries' clients - Added a function to produce a `dockerconfig` in a JSON format from a registry client to merge with the authfile passed to Skopeo - Updated `GetImagePullSecrets` to include dockerconfigs from private registries to the image pull secrets from pods #### Notes: - `imagePullSecrets` from hooked Pods still have priority over the authentication via these default private registries - Changes do not impact the Helm chart and are compatible with previous version - Source private registries cannot authenticate with different credentials from the ones used by the target registry passed as environment variables
1 parent 934d2d9 commit 32edc11

File tree

16 files changed

+403
-136
lines changed

16 files changed

+403
-136
lines changed

cmd/root.go

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,21 @@ A mutating webhook for Kubernetes, pointing the images to a new location.`,
6363
//metricsRec := metrics.NewPrometheus(promReg)
6464
log.Trace().Interface("config", cfg).Msg("config")
6565

66-
rClient, err := setupTargetRegistryClient()
66+
// Create registry clients for source registries
67+
sourceRegistryClients := []registry.Client{}
68+
for _, reg := range cfg.Source.Registries {
69+
sourceRegistryClient, err := registry.NewClient(reg)
70+
if err != nil {
71+
log.Err(err).Msgf("error connecting to source registry at %s", reg.Domain())
72+
os.Exit(1)
73+
}
74+
sourceRegistryClients = append(sourceRegistryClients, sourceRegistryClient)
75+
}
76+
77+
// Create a registry client for private target registry
78+
targetRegistryClient, err := registry.NewClient(cfg.Target)
6779
if err != nil {
68-
log.Err(err).Msg("error connecting to registry client")
80+
log.Err(err).Msgf("error connecting to target registry at %s", cfg.Target.Domain())
6981
os.Exit(1)
7082
}
7183

@@ -86,8 +98,11 @@ A mutating webhook for Kubernetes, pointing the images to a new location.`,
8698

8799
imagePullSecretProvider := setupImagePullSecretsProvider()
88100

101+
// Inform secret provider about managed private source registries
102+
imagePullSecretProvider.SetAuthenticatedRegistries(sourceRegistryClients)
103+
89104
wh, err := webhook.NewImageSwapperWebhookWithOpts(
90-
rClient,
105+
targetRegistryClient,
91106
webhook.Filters(cfg.Source.Filters),
92107
webhook.ImagePullSecretsProvider(imagePullSecretProvider),
93108
webhook.ImageSwapPolicy(imageSwapPolicy),
@@ -279,20 +294,3 @@ func setupImagePullSecretsProvider() secrets.ImagePullSecretsProvider {
279294

280295
return secrets.NewKubernetesImagePullSecretsProvider(clientset)
281296
}
282-
283-
// setupRegistry configures a target registry client connection
284-
func setupTargetRegistryClient() (registry.Client, error) {
285-
targetRegistry, err := types.ParseTargetRegistry(cfg.Target.Type)
286-
if err != nil {
287-
log.Err(err)
288-
}
289-
290-
switch targetRegistry {
291-
case types.TargetRegistryAws:
292-
return registry.NewECRClient(cfg.Target.AWS)
293-
case types.TargetRegistryGcp:
294-
return registry.NewGARClient(cfg.Target.GCP)
295-
}
296-
297-
return nil, fmt.Errorf("no registry for target registry type: '%s'", targetRegistry)
298-
}

docs/configuration.md

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,30 @@ This option only applies for `immediate` and `force` image copy strategies.
5252

5353
This section configures details about the image source.
5454

55+
### Registries
56+
57+
The option `source.registries` describes a list of registries to pull images from, using a specific configuration.
58+
59+
#### AWS
60+
61+
By providing configuration on AWS registries you can ask `k8s-image-swapper` to handle the authentication using the same credentials as for the target AWS registry.
62+
This authentication method is the default way to get authorized by a private registry if the targeted Pod does not provide an `imagePullSecret`.
63+
64+
Registries are described with an AWS account ID and region, mostly to construct the ECR domain `[ACCOUNT_ID].dkr.ecr.[REGION].amazonaws.com`.
65+
66+
!!! example
67+
```yaml
68+
source:
69+
registries:
70+
- type: aws
71+
aws:
72+
accountId: 123456789
73+
region: ap-southeast-2
74+
- type: aws
75+
aws:
76+
accountId: 234567890
77+
region: us-east-1
78+
```
5579
### Filters
5680

5781
Filters provide control over what pods will be processed.
@@ -130,10 +154,12 @@ has a live editor that can be used as a playground to experiment with more compl
130154
## Target
131155

132156
This section configures details about the image target.
157+
The option `target` allows to specify which type of registry you set as your target (AWS, GCP...).
158+
At the moment, `aws` and `gcp` are the only supported values.
133159

134160
### AWS
135161

136-
The option `target.registry.aws` holds details about the target registry storing the images.
162+
The option `target.aws` holds details about the target registry storing the images.
137163
The AWS Account ID and Region is primarily used to construct the ECR domain `[ACCOUNTID].dkr.ecr.[REGION].amazonaws.com`.
138164

139165
!!! example
@@ -165,7 +191,7 @@ It's a slice of `Key` and `Value`.
165191

166192
### GCP
167193

168-
The option `target.registry.gcp` holds details about the target registry storing the images.
194+
The option `target.gcp` holds details about the target registry storing the images.
169195
The GCP location, projectId, and repositoryId are used to constrct the GCP Artifact Registry domain `[LOCATION]-docker.pkg.dev/[PROJECT_ID]/[REPOSITORY_ID]`.
170196

171197
!!! example

docs/faq.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
### Is pulling from private registries supported?
44

5-
Yes, `imagePullSecrets` on `Pod` and `ServiceAccount` level are supported.
5+
Yes, `imagePullSecrets` on `Pod` and `ServiceAccount` level in the hooked pod definition are supported.
6+
7+
It is also possible to provide a list of ECRs to which authentication is handled by `k8s-image-swapper` using the same credentials as for the target registry. Please see [Configuration > Source - AWS](configuration.md#Private-registries).
68

79
### Are config changes reloaded gracefully?
810

pkg/config/config.go

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ package config
2424
import (
2525
"fmt"
2626
"time"
27+
28+
"github.com/estahn/k8s-image-swapper/pkg/types"
2729
)
2830

2931
const DefaultImageCopyDeadline = 8 * time.Second
@@ -39,22 +41,22 @@ type Config struct {
3941
ImageCopyPolicy string `yaml:"imageCopyPolicy" validate:"oneof=delayed immediate force"`
4042
ImageCopyDeadline time.Duration `yaml:"imageCopyDeadline"`
4143

42-
Source Source `yaml:"source"`
43-
Target Target `yaml:"target"`
44+
Source Source `yaml:"source"`
45+
Target Registry `yaml:"target"`
4446

4547
TLSCertFile string
4648
TLSKeyFile string
4749
}
4850

49-
type Source struct {
50-
Filters []JMESPathFilter `yaml:"filters"`
51-
}
52-
5351
type JMESPathFilter struct {
5452
JMESPath string `yaml:"jmespath"`
5553
}
54+
type Source struct {
55+
Registries []Registry `yaml:"registries"`
56+
Filters []JMESPathFilter `yaml:"filters"`
57+
}
5658

57-
type Target struct {
59+
type Registry struct {
5860
Type string `yaml:"type"`
5961
AWS AWS `yaml:"aws"`
6062
GCP GCP `yaml:"gcp"`
@@ -67,6 +69,12 @@ type AWS struct {
6769
ECROptions ECROptions `yaml:"ecrOptions"`
6870
}
6971

72+
type GCP struct {
73+
Location string `yaml:"location"`
74+
ProjectID string `yaml:"projectId"`
75+
RepositoryID string `yaml:"repositoryId"`
76+
}
77+
7078
type ECROptions struct {
7179
AccessPolicy string `yaml:"accessPolicy"`
7280
LifecyclePolicy string `yaml:"lifecyclePolicy"`
@@ -94,8 +102,52 @@ func (a *AWS) EcrDomain() string {
94102
return fmt.Sprintf("%s.dkr.ecr.%s.amazonaws.com", a.AccountID, a.Region)
95103
}
96104

97-
type GCP struct {
98-
Location string `yaml:"location"`
99-
ProjectID string `yaml:"projectId"`
100-
RepositoryID string `yaml:"repositoryId"`
105+
func (g *GCP) GarDomain() string {
106+
return fmt.Sprintf("%s-docker.pkg.dev/%s/%s", g.Location, g.ProjectID, g.RepositoryID)
107+
}
108+
109+
func (r Registry) Domain() string {
110+
registry, _ := types.ParseRegistry(r.Type)
111+
switch registry {
112+
case types.RegistryAWS:
113+
return r.AWS.EcrDomain()
114+
case types.RegistryGCP:
115+
return r.GCP.GarDomain()
116+
default:
117+
return ""
118+
}
119+
}
120+
121+
// provides detailed information about wrongly provided configuration
122+
func CheckRegistryConfiguration(r Registry) error {
123+
if r.Type == "" {
124+
return fmt.Errorf("a registry requires a type")
125+
}
126+
127+
errorWithType := func(info string) error {
128+
return fmt.Errorf(`registry of type "%s" %s`, r.Type, info)
129+
}
130+
131+
registry, _ := types.ParseRegistry(r.Type)
132+
switch registry {
133+
case types.RegistryAWS:
134+
if r.AWS.Region == "" {
135+
return errorWithType(`requires a field "region"`)
136+
}
137+
if r.AWS.AccountID == "" {
138+
return errorWithType(`requires a field "accountdId"`)
139+
}
140+
case types.RegistryGCP:
141+
if r.GCP.Location == "" {
142+
return errorWithType(`requires a field "location"`)
143+
}
144+
if r.GCP.ProjectID == "" {
145+
return errorWithType(`requires a field "projectId"`)
146+
}
147+
if r.GCP.RepositoryID == "" {
148+
return errorWithType(`requires a field "repositoryId"`)
149+
}
150+
}
151+
152+
return nil
101153
}

pkg/config/config_test.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ target:
5454
value: B
5555
`,
5656
expCfg: Config{
57-
Target: Target{
57+
Target: Registry{
5858
Type: "aws",
5959
AWS: AWS{
6060
AccountID: "123456789",
@@ -76,6 +76,39 @@ target:
7676
},
7777
},
7878
},
79+
{
80+
name: "should render multiple source registries",
81+
cfg: `
82+
source:
83+
registries:
84+
- type: "aws"
85+
aws:
86+
accountId: "12345678912"
87+
region: "us-west-1"
88+
- type: "aws"
89+
aws:
90+
accountId: "12345678912"
91+
region: "us-east-1"
92+
`,
93+
expCfg: Config{
94+
Source: Source{
95+
Registries: []Registry{
96+
{
97+
Type: "aws",
98+
AWS: AWS{
99+
AccountID: "12345678912",
100+
Region: "us-west-1",
101+
}},
102+
{
103+
Type: "aws",
104+
AWS: AWS{
105+
AccountID: "12345678912",
106+
Region: "us-east-1",
107+
}},
108+
},
109+
},
110+
},
111+
},
79112
}
80113

81114
for _, test := range tests {

pkg/registry/client.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ package registry
22

33
import (
44
"context"
5+
"encoding/base64"
6+
"encoding/json"
7+
"fmt"
8+
9+
"github.com/estahn/k8s-image-swapper/pkg/config"
10+
"github.com/estahn/k8s-image-swapper/pkg/types"
511

612
ctypes "github.com/containers/image/v5/types"
713
)
@@ -19,3 +25,49 @@ type Client interface {
1925
Endpoint() string
2026
Credentials() string
2127
}
28+
29+
type DockerConfig struct {
30+
AuthConfigs map[string]AuthConfig `json:"auths"`
31+
}
32+
33+
type AuthConfig struct {
34+
Auth string `json:"auth,omitempty"`
35+
}
36+
37+
// returns a registry client ready for use without the need to specify an implementation
38+
func NewClient(r config.Registry) (Client, error) {
39+
if err := config.CheckRegistryConfiguration(r); err != nil {
40+
return nil, err
41+
}
42+
43+
registry, err := types.ParseRegistry(r.Type)
44+
if err != nil {
45+
return nil, err
46+
}
47+
48+
switch registry {
49+
case types.RegistryAWS:
50+
return NewECRClient(r.AWS)
51+
case types.RegistryGCP:
52+
return NewGARClient(r.GCP)
53+
default:
54+
return nil, fmt.Errorf(`registry of type "%s" is not supported`, r.Type)
55+
}
56+
}
57+
58+
func GenerateDockerConfig(c Client) ([]byte, error) {
59+
dockerConfig := DockerConfig{
60+
AuthConfigs: map[string]AuthConfig{
61+
c.Endpoint(): {
62+
Auth: base64.StdEncoding.EncodeToString([]byte(c.Credentials())),
63+
},
64+
},
65+
}
66+
67+
dockerConfigJson, err := json.Marshal(dockerConfig)
68+
if err != nil {
69+
return []byte{}, err
70+
}
71+
72+
return dockerConfigJson, nil
73+
}

0 commit comments

Comments
 (0)