Skip to content

Commit a133c78

Browse files
authored
Fix data race issue on copyFn when there are multiple containers or initContainers (#422)
Fixes #421 Creates a new `ImageCopier` object with its own `imagePullPolicy` to avoid a data race on the `container` object. This new object is dedicated to the image copy process as it was done before but with its own subcontext and abstracted from the core mutation webhook processing. A new configurable `imageCopyDeadline` parameter has been added alongside an implementation of timeout throughout the different steps of the image copy. Every function called as part of the copy has been updated to include context evaluation and support abortion in case of context timeout.
1 parent f0f6439 commit a133c78

14 files changed

+416
-132
lines changed

cmd/root.go

+6
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ A mutating webhook for Kubernetes, pointing the images to a new location.`,
8181
log.Err(err).Str("policy", cfg.ImageCopyPolicy).Msg("parsing image copy policy failed")
8282
}
8383

84+
imageCopyDeadline := config.DefaultImageCopyDeadline
85+
if cfg.ImageCopyDeadline != 0 {
86+
imageCopyDeadline = cfg.ImageCopyDeadline
87+
}
88+
8489
imagePullSecretProvider := setupImagePullSecretsProvider()
8590

8691
wh, err := webhook.NewImageSwapperWebhookWithOpts(
@@ -89,6 +94,7 @@ A mutating webhook for Kubernetes, pointing the images to a new location.`,
8994
webhook.ImagePullSecretsProvider(imagePullSecretProvider),
9095
webhook.ImageSwapPolicy(imageSwapPolicy),
9196
webhook.ImageCopyPolicy(imageCopyPolicy),
97+
webhook.ImageCopyDeadline(imageCopyDeadline),
9298
)
9399
if err != nil {
94100
log.Err(err).Msg("error creating webhook")

docs/configuration.md

+7-2
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,14 @@ The option `imageSwapPolicy` (default: `exists`) defines the mutation strategy u
3838
The option `imageCopyPolicy` (default: `delayed`) defines the image copy strategy used.
3939

4040
* `delayed`: Submits the copy job to a process queue and moves on.
41-
* `immediate`: Submits the copy job to a process queue and waits for it to finish (deadline 8s).
42-
* `force`: Attempts to immediately copy the image (deadline 8s).
41+
* `immediate`: Submits the copy job to a process queue and waits for it to finish (deadline defined by `imageCopyDeadline`).
42+
* `force`: Attempts to immediately copy the image (deadline defined by `imageCopyDeadline`).
4343

44+
## ImageCopyDeadline
45+
46+
The option `imageCopyDeadline` (default: `8s`) defines the duration after which the image copy if aborted.
47+
48+
This option only applies for `immediate` and `force` image copy strategies.
4449

4550

4651
## Source

pkg/config/config.go

+10-5
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,24 @@ package config
2323

2424
import (
2525
"fmt"
26+
"time"
2627
)
2728

29+
const DefaultImageCopyDeadline = 8 * time.Second
30+
2831
type Config struct {
2932
LogLevel string `yaml:"logLevel" validate:"oneof=trace debug info warn error fatal"`
3033
LogFormat string `yaml:"logFormat" validate:"oneof=json console"`
3134

3235
ListenAddress string
3336

34-
DryRun bool `yaml:"dryRun"`
35-
ImageSwapPolicy string `yaml:"imageSwapPolicy" validate:"oneof=always exists"`
36-
ImageCopyPolicy string `yaml:"imageCopyPolicy" validate:"oneof=delayed immediate force"`
37-
Source Source `yaml:"source"`
38-
Target Target `yaml:"target"`
37+
DryRun bool `yaml:"dryRun"`
38+
ImageSwapPolicy string `yaml:"imageSwapPolicy" validate:"oneof=always exists"`
39+
ImageCopyPolicy string `yaml:"imageCopyPolicy" validate:"oneof=delayed immediate force"`
40+
ImageCopyDeadline time.Duration `yaml:"imageCopyDeadline"`
41+
42+
Source Source `yaml:"source"`
43+
Target Target `yaml:"target"`
3944

4045
TLSCertFile string
4146
TLSKeyFile string

pkg/registry/client.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package registry
22

3+
import "context"
4+
35
// Client provides methods required to be implemented by the various target registry clients, e.g. ECR, Docker, Quay.
46
type Client interface {
5-
CreateRepository(string) error
7+
CreateRepository(ctx context.Context, name string) error
68
RepositoryExists() bool
79
CopyImage() error
810
PullImage() error
911
PutImage() error
10-
ImageExists(ref string) bool
12+
ImageExists(ctx context.Context, ref string) bool
1113

1214
// Endpoint returns the domain of the registry
1315
Endpoint() string

pkg/registry/ecr.go

+8-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package registry
22

33
import (
4+
"context"
45
"encoding/base64"
56
"net/http"
67
"os/exec"
@@ -18,8 +19,6 @@ import (
1819
"github.com/rs/zerolog/log"
1920
)
2021

21-
var execCommand = exec.Command
22-
2322
type ECRClient struct {
2423
client ecriface.ECRAPI
2524
ecrDomain string
@@ -36,12 +35,12 @@ func (e *ECRClient) Credentials() string {
3635
return string(e.authToken)
3736
}
3837

39-
func (e *ECRClient) CreateRepository(name string) error {
38+
func (e *ECRClient) CreateRepository(ctx context.Context, name string) error {
4039
if _, found := e.cache.Get(name); found {
4140
return nil
4241
}
4342

44-
_, err := e.client.CreateRepository(&ecr.CreateRepositoryInput{
43+
_, err := e.client.CreateRepositoryWithContext(ctx, &ecr.CreateRepositoryInput{
4544
RepositoryName: aws.String(name),
4645
ImageScanningConfiguration: &ecr.ImageScanningConfiguration{
4746
ScanOnPush: aws.Bool(true),
@@ -68,7 +67,7 @@ func (e *ECRClient) CreateRepository(name string) error {
6867

6968
if len(e.accessPolicy) > 0 {
7069
log.Debug().Str("repo", name).Str("accessPolicy", e.accessPolicy).Msg("setting access policy on repo")
71-
_, err := e.client.SetRepositoryPolicy(&ecr.SetRepositoryPolicyInput{
70+
_, err := e.client.SetRepositoryPolicyWithContext(ctx, &ecr.SetRepositoryPolicyInput{
7271
PolicyText: &e.accessPolicy,
7372
RegistryId: &e.targetAccount,
7473
RepositoryName: aws.String(name),
@@ -82,7 +81,7 @@ func (e *ECRClient) CreateRepository(name string) error {
8281

8382
if len(e.lifecyclePolicy) > 0 {
8483
log.Debug().Str("repo", name).Str("lifecyclePolicy", e.lifecyclePolicy).Msg("setting lifecycle policy on repo")
85-
_, err := e.client.PutLifecyclePolicy(&ecr.PutLifecyclePolicyInput{
84+
_, err := e.client.PutLifecyclePolicyWithContext(ctx, &ecr.PutLifecyclePolicyInput{
8685
LifecyclePolicyText: &e.lifecyclePolicy,
8786
RegistryId: &e.targetAccount,
8887
RepositoryName: aws.String(name),
@@ -130,7 +129,7 @@ func (e *ECRClient) PutImage() error {
130129
panic("implement me")
131130
}
132131

133-
func (e *ECRClient) ImageExists(ref string) bool {
132+
func (e *ECRClient) ImageExists(ctx context.Context, ref string) bool {
134133
if _, found := e.cache.Get(ref); found {
135134
return true
136135
}
@@ -143,10 +142,8 @@ func (e *ECRClient) ImageExists(ref string) bool {
143142
"--creds", e.Credentials(),
144143
}
145144

146-
log.Trace().Str("app", app).Strs("args", args).Msg("executing command to inspect image")
147-
cmd := execCommand(app, args...)
148-
149-
if _, err := cmd.Output(); err != nil {
145+
log.Ctx(ctx).Trace().Str("app", app).Strs("args", args).Msg("executing command to inspect image")
146+
if err := exec.CommandContext(ctx, app, args...).Run(); err != nil {
150147
return false
151148
}
152149

pkg/secrets/dummy.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package secrets
22

3-
import v1 "k8s.io/api/core/v1"
3+
import (
4+
"context"
5+
6+
v1 "k8s.io/api/core/v1"
7+
)
48

59
// DummyImagePullSecretsProvider does nothing
610
type DummyImagePullSecretsProvider struct {
@@ -12,6 +16,6 @@ func NewDummyImagePullSecretsProvider() ImagePullSecretsProvider {
1216
}
1317

1418
// GetImagePullSecrets returns an empty ImagePullSecretsResult
15-
func (p *DummyImagePullSecretsProvider) GetImagePullSecrets(pod *v1.Pod) (*ImagePullSecretsResult, error) {
19+
func (p *DummyImagePullSecretsProvider) GetImagePullSecrets(ctx context.Context, pod *v1.Pod) (*ImagePullSecretsResult, error) {
1620
return NewImagePullSecretsResult(), nil
1721
}

pkg/secrets/dummy_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package secrets
22

33
import (
4+
"context"
45
"reflect"
56
"testing"
67

@@ -41,7 +42,7 @@ func TestDummyImagePullSecretsProvider_GetImagePullSecrets(t *testing.T) {
4142
for _, tt := range tests {
4243
t.Run(tt.name, func(t *testing.T) {
4344
p := &DummyImagePullSecretsProvider{}
44-
got, err := p.GetImagePullSecrets(tt.args.pod)
45+
got, err := p.GetImagePullSecrets(context.Background(), tt.args.pod)
4546
if (err != nil) != tt.wantErr {
4647
t.Errorf("GetImagePullSecrets() error = %v, wantErr %v", err, tt.wantErr)
4748
return

pkg/secrets/kubernetes.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -62,17 +62,17 @@ func NewKubernetesImagePullSecretsProvider(clientset kubernetes.Interface) Image
6262
}
6363

6464
// GetImagePullSecrets returns all secrets with their respective content
65-
func (p *KubernetesImagePullSecretsProvider) GetImagePullSecrets(pod *v1.Pod) (*ImagePullSecretsResult, error) {
65+
func (p *KubernetesImagePullSecretsProvider) GetImagePullSecrets(ctx context.Context, pod *v1.Pod) (*ImagePullSecretsResult, error) {
6666
var secrets = make(map[string][]byte)
6767

6868
imagePullSecrets := pod.Spec.ImagePullSecrets
6969

7070
// retrieve secret names from pod ServiceAccount (spec.imagePullSecrets)
7171
serviceAccount, err := p.kubernetesClient.CoreV1().
7272
ServiceAccounts(pod.Namespace).
73-
Get(context.TODO(), pod.Spec.ServiceAccountName, metav1.GetOptions{})
73+
Get(ctx, pod.Spec.ServiceAccountName, metav1.GetOptions{})
7474
if err != nil {
75-
log.Err(err).Msg("error fetching referenced service account, continue without service account imagePullSecrets")
75+
log.Ctx(ctx).Warn().Msg("error fetching referenced service account, continue without service account imagePullSecrets")
7676
}
7777

7878
if serviceAccount != nil {
@@ -86,9 +86,9 @@ func (p *KubernetesImagePullSecretsProvider) GetImagePullSecrets(pod *v1.Pod) (*
8686
continue
8787
}
8888

89-
secret, err := p.kubernetesClient.CoreV1().Secrets(pod.Namespace).Get(context.TODO(), imagePullSecret.Name, metav1.GetOptions{})
89+
secret, err := p.kubernetesClient.CoreV1().Secrets(pod.Namespace).Get(ctx, imagePullSecret.Name, metav1.GetOptions{})
9090
if err != nil {
91-
log.Err(err).Msg("error fetching secret, continue without imagePullSecrets")
91+
log.Ctx(ctx).Err(err).Msg("error fetching secret, continue without imagePullSecrets")
9292
}
9393

9494
if secret == nil || secret.Type != v1.SecretTypeDockerConfigJson {

pkg/secrets/kubernetes_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ func TestKubernetesCredentialProvider_GetImagePullSecrets(t *testing.T) {
8989
_, _ = clientSet.CoreV1().Secrets("test-ns").Create(context.TODO(), podSecret, metav1.CreateOptions{})
9090

9191
provider := NewKubernetesImagePullSecretsProvider(clientSet)
92-
result, err := provider.GetImagePullSecrets(pod)
92+
result, err := provider.GetImagePullSecrets(context.Background(), pod)
9393

9494
assert.NoError(t, err)
9595
assert.NotNil(t, result)

pkg/secrets/provider.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package secrets
22

3-
import v1 "k8s.io/api/core/v1"
3+
import (
4+
"context"
5+
6+
v1 "k8s.io/api/core/v1"
7+
)
48

59
type ImagePullSecretsProvider interface {
6-
GetImagePullSecrets(pod *v1.Pod) (*ImagePullSecretsResult, error)
10+
GetImagePullSecrets(ctx context.Context, pod *v1.Pod) (*ImagePullSecretsResult, error)
711
}

0 commit comments

Comments
 (0)