Repository: grafana/k6-operator (go)

Issue:
Add ImagePullPolicy option on the runner and starter specs
### Feature Description

Would it be possible to add the field "imagePullPolicy" on the K6 CRD spec?
In our environment, we use custom images for both the runner images and the starter, these images containers extensions, internal tools, and certificates authorities to be able to communicate with the service we test.
But in order to ensure we always pull the latest image for a given tag, being able to set the imagePullPolicy to "always" would be a great plus.

### Suggested Solution (optional)

_No response_

### Already existing or connected issues / PRs (optional)

_No response_

Pull request:
Added support for imagePullPolicy
This PR adds the ability to configure ImagePullPolicy for runner and starter specs in K6-CRD. 
By default k8s uses IfNotPresent policy for image pulling (except for :latest tags which in this case is Always). 
I've also adjusted tests for runner, starter and initializer to check imagePullPolicy.

Closes #152 

Patch:
diff --git a/api/v1alpha1/k6_types.go b/api/v1alpha1/k6_types.go
index 5b56321..30ad5b8 100644
--- a/api/v1alpha1/k6_types.go
+++ b/api/v1alpha1/k6_types.go
@@ -30,6 +30,7 @@ type Pod struct {
 	Env                          []corev1.EnvVar               `json:"env,omitempty"`
 	Image                        string                        `json:"image,omitempty"`
 	ImagePullSecrets             []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"`
+	ImagePullPolicy              corev1.PullPolicy             `json:"imagePullPolicy,omitempty"`
 	Metadata                     PodMetadata                   `json:"metadata,omitempty"`
 	NodeSelector                 map[string]string             `json:"nodeselector,omitempty"`
 	Tolerations                  []corev1.Toleration           `json:"tolerations,omitempty"`
diff --git a/pkg/resources/containers/curl.go b/pkg/resources/containers/curl.go
index d3fcf34..b95e107 100644
--- a/pkg/resources/containers/curl.go
+++ b/pkg/resources/containers/curl.go
@@ -11,7 +11,7 @@ import (
 )
 
 // NewCurlContainer is used to get a template for a new k6 starting curl container.
-func NewCurlContainer(hostnames []string, image string, command []string, env []corev1.EnvVar) corev1.Container {
+func NewCurlContainer(hostnames []string, image string, imagePullPolicy corev1.PullPolicy, command []string, env []corev1.EnvVar) corev1.Container {
 	req, _ := json.Marshal(
 		statusAPIRequest{
 			Data: statusAPIRequestData{
@@ -29,9 +29,10 @@ func NewCurlContainer(hostnames []string, image string, command []string, env []
 	}
 
 	return corev1.Container{
-		Name:  "k6-curl",
-		Image: image,
-		Env:   env,
+		Name:            "k6-curl",
+		Image:           image,
+		ImagePullPolicy: imagePullPolicy,
+		Env:             env,
 		Resources: corev1.ResourceRequirements{
 			Requests: corev1.ResourceList{
 				corev1.ResourceCPU:    *resource.NewMilliQuantity(50, resource.DecimalSI),
diff --git a/pkg/resources/jobs/initializer.go b/pkg/resources/jobs/initializer.go
index 06d0f9e..865c54a 100644
--- a/pkg/resources/jobs/initializer.go
+++ b/pkg/resources/jobs/initializer.go
@@ -87,13 +87,14 @@ func NewInitializerJob(k6 *v1alpha1.K6, argLine string) (*batchv1.Job, error) {
 					ImagePullSecrets:             k6.Spec.Runner.ImagePullSecrets,
 					Containers: []corev1.Container{
 						{
-							Image:        image,
-							Name:         "k6",
-							Command:      command,
-							Env:          env,
-							Resources:    k6.Spec.Runner.Resources,
-							VolumeMounts: script.VolumeMount(),
-							Ports:        ports,
+							Image:           image,
+							ImagePullPolicy: k6.Spec.Runner.ImagePullPolicy,
+							Name:            "k6",
+							Command:         command,
+							Env:             env,
+							Resources:       k6.Spec.Runner.Resources,
+							VolumeMounts:    script.VolumeMount(),
+							Ports:           ports,
 						},
 					},
 					Volumes: script.Volume(),
diff --git a/pkg/resources/jobs/runner.go b/pkg/resources/jobs/runner.go
index 5740a88..d9e17d8 100644
--- a/pkg/resources/jobs/runner.go
+++ b/pkg/resources/jobs/runner.go
@@ -151,14 +151,15 @@ func NewRunnerJob(k6 *v1alpha1.K6, index int, testRunId, token string) (*batchv1
 					SecurityContext:              &k6.Spec.Runner.SecurityContext,
 					ImagePullSecrets:             k6.Spec.Runner.ImagePullSecrets,
 					Containers: []corev1.Container{{
-						Image:        image,
-						Name:         "k6",
-						Command:      command,
-						Env:          env,
-						Resources:    k6.Spec.Runner.Resources,
-						VolumeMounts: script.VolumeMount(),
-						Ports:        ports,
-						EnvFrom:      k6.Spec.Runner.EnvFrom,
+						Image:           image,
+						ImagePullPolicy: k6.Spec.Runner.ImagePullPolicy,
+						Name:            "k6",
+						Command:         command,
+						Env:             env,
+						Resources:       k6.Spec.Runner.Resources,
+						VolumeMounts:    script.VolumeMount(),
+						Ports:           ports,
+						EnvFrom:         k6.Spec.Runner.EnvFrom,
 					}},
 					TerminationGracePeriodSeconds: &zero,
 					Volumes:                       script.Volume(),
diff --git a/pkg/resources/jobs/starter.go b/pkg/resources/jobs/starter.go
index 4852e73..7541f1d 100644
--- a/pkg/resources/jobs/starter.go
+++ b/pkg/resources/jobs/starter.go
@@ -66,7 +66,7 @@ func NewStarterJob(k6 *v1alpha1.K6, hostname []string) *batchv1.Job {
 					SecurityContext:              &k6.Spec.Starter.SecurityContext,
 					ImagePullSecrets:             k6.Spec.Starter.ImagePullSecrets,
 					Containers: []corev1.Container{
-						containers.NewCurlContainer(hostname, starterImage, command, env),
+						containers.NewCurlContainer(hostname, starterImage, k6.Spec.Starter.ImagePullPolicy, command, env),
 					},
 				},
 			},


Test patch:
diff --git a/pkg/resources/jobs/initializer_test.go b/pkg/resources/jobs/initializer_test.go
index fc72e63..36bfa35 100644
--- a/pkg/resources/jobs/initializer_test.go
+++ b/pkg/resources/jobs/initializer_test.go
@@ -53,8 +53,9 @@ func TestNewInitializerJob(t *testing.T) {
 					RestartPolicy:                corev1.RestartPolicyNever,
 					Containers: []corev1.Container{
 						{
-							Image: "ghcr.io/grafana/operator:latest-runner",
-							Name:  "k6",
+							Image:           "ghcr.io/grafana/operator:latest-runner",
+							ImagePullPolicy: "",
+							Name:            "k6",
 							Command: []string{
 								"sh", "-c",
 								"k6 archive --log-output=none /test/test.js -O ./test.js.archived.tar --out cloud && k6 inspect --execution-requirements --log-output=none ./test.js.archived.tar",
diff --git a/pkg/resources/jobs/runner_test.go b/pkg/resources/jobs/runner_test.go
index 73032a8..bd45fa5 100644
--- a/pkg/resources/jobs/runner_test.go
+++ b/pkg/resources/jobs/runner_test.go
@@ -288,13 +288,14 @@ func TestNewRunnerJob(t *testing.T) {
 					ServiceAccountName:           "default",
 					AutomountServiceAccountToken: &automountServiceAccountToken,
 					Containers: []corev1.Container{{
-						Image:        "ghcr.io/grafana/operator:latest-runner",
-						Name:         "k6",
-						Command:      []string{"k6", "run", "--quiet", "/test/test.js", "--address=0.0.0.0:6565", "--paused", "--tag", "instance_id=1", "--tag", "job_name=test-1"},
-						Env:          []corev1.EnvVar{},
-						Resources:    corev1.ResourceRequirements{},
-						VolumeMounts: script.VolumeMount(),
-						Ports:        []corev1.ContainerPort{{ContainerPort: 6565}},
+						Image:           "ghcr.io/grafana/operator:latest-runner",
+						ImagePullPolicy: corev1.PullNever,
+						Name:            "k6",
+						Command:         []string{"k6", "run", "--quiet", "/test/test.js", "--address=0.0.0.0:6565", "--paused", "--tag", "instance_id=1", "--tag", "job_name=test-1"},
+						Env:             []corev1.EnvVar{},
+						Resources:       corev1.ResourceRequirements{},
+						VolumeMounts:    script.VolumeMount(),
+						Ports:           []corev1.ContainerPort{{ContainerPort: 6565}},
 						EnvFrom: []corev1.EnvFromSource{
 							{
 								ConfigMapRef: &corev1.ConfigMapEnvSource{
@@ -341,6 +342,7 @@ func TestNewRunnerJob(t *testing.T) {
 						},
 					},
 				},
+				ImagePullPolicy: corev1.PullNever,
 			},
 		},
 	}
@@ -399,13 +401,14 @@ func TestNewRunnerJobNoisy(t *testing.T) {
 					AutomountServiceAccountToken: &automountServiceAccountToken,
 					SecurityContext:              &corev1.PodSecurityContext{},
 					Containers: []corev1.Container{{
-						Image:        "ghcr.io/grafana/operator:latest-runner",
-						Name:         "k6",
-						Command:      []string{"k6", "run", "/test/test.js", "--address=0.0.0.0:6565", "--paused", "--tag", "instance_id=1", "--tag", "job_name=test-1"},
-						Env:          []corev1.EnvVar{},
-						Resources:    corev1.ResourceRequirements{},
-						VolumeMounts: script.VolumeMount(),
-						Ports:        []corev1.ContainerPort{{ContainerPort: 6565}},
+						Image:           "ghcr.io/grafana/operator:latest-runner",
+						ImagePullPolicy: "",
+						Name:            "k6",
+						Command:         []string{"k6", "run", "/test/test.js", "--address=0.0.0.0:6565", "--paused", "--tag", "instance_id=1", "--tag", "job_name=test-1"},
+						Env:             []corev1.EnvVar{},
+						Resources:       corev1.ResourceRequirements{},
+						VolumeMounts:    script.VolumeMount(),
+						Ports:           []corev1.ContainerPort{{ContainerPort: 6565}},
 					}},
 					TerminationGracePeriodSeconds: &zero,
 					Volumes:                       script.Volume(),
@@ -493,13 +496,14 @@ func TestNewRunnerJobUnpaused(t *testing.T) {
 					AutomountServiceAccountToken: &automountServiceAccountToken,
 					SecurityContext:              &corev1.PodSecurityContext{},
 					Containers: []corev1.Container{{
-						Image:        "ghcr.io/grafana/operator:latest-runner",
-						Name:         "k6",
-						Command:      []string{"k6", "run", "--quiet", "/test/test.js", "--address=0.0.0.0:6565", "--tag", "instance_id=1", "--tag", "job_name=test-1"},
-						Env:          []corev1.EnvVar{},
-						Resources:    corev1.ResourceRequirements{},
-						VolumeMounts: script.VolumeMount(),
-						Ports:        []corev1.ContainerPort{{ContainerPort: 6565}},
+						Image:           "ghcr.io/grafana/operator:latest-runner",
+						ImagePullPolicy: "",
+						Name:            "k6",
+						Command:         []string{"k6", "run", "--quiet", "/test/test.js", "--address=0.0.0.0:6565", "--tag", "instance_id=1", "--tag", "job_name=test-1"},
+						Env:             []corev1.EnvVar{},
+						Resources:       corev1.ResourceRequirements{},
+						VolumeMounts:    script.VolumeMount(),
+						Ports:           []corev1.ContainerPort{{ContainerPort: 6565}},
 					}},
 					TerminationGracePeriodSeconds: &zero,
 					Volumes:                       script.Volume(),
@@ -587,13 +591,14 @@ func TestNewRunnerJobArguments(t *testing.T) {
 					AutomountServiceAccountToken: &automountServiceAccountToken,
 					SecurityContext:              &corev1.PodSecurityContext{},
 					Containers: []corev1.Container{{
-						Image:        "ghcr.io/grafana/operator:latest-runner",
-						Name:         "k6",
-						Command:      []string{"k6", "run", "--quiet", "--cool-thing", "/test/test.js", "--address=0.0.0.0:6565", "--paused", "--tag", "instance_id=1", "--tag", "job_name=test-1"},
-						Env:          []corev1.EnvVar{},
-						Resources:    corev1.ResourceRequirements{},
-						VolumeMounts: script.VolumeMount(),
-						Ports:        []corev1.ContainerPort{{ContainerPort: 6565}},
+						Image:           "ghcr.io/grafana/operator:latest-runner",
+						ImagePullPolicy: "",
+						Name:            "k6",
+						Command:         []string{"k6", "run", "--quiet", "--cool-thing", "/test/test.js", "--address=0.0.0.0:6565", "--paused", "--tag", "instance_id=1", "--tag", "job_name=test-1"},
+						Env:             []corev1.EnvVar{},
+						Resources:       corev1.ResourceRequirements{},
+						VolumeMounts:    script.VolumeMount(),
+						Ports:           []corev1.ContainerPort{{ContainerPort: 6565}},
 					}},
 					TerminationGracePeriodSeconds: &zero,
 					Volumes:                       script.Volume(),
@@ -682,13 +687,14 @@ func TestNewRunnerJobServiceAccount(t *testing.T) {
 					AutomountServiceAccountToken: &automountServiceAccountToken,
 					SecurityContext:              &corev1.PodSecurityContext{},
 					Containers: []corev1.Container{{
-						Image:        "ghcr.io/grafana/operator:latest-runner",
-						Name:         "k6",
-						Command:      []string{"k6", "run", "--quiet", "/test/test.js", "--address=0.0.0.0:6565", "--paused", "--tag", "instance_id=1", "--tag", "job_name=test-1"},
-						Env:          []corev1.EnvVar{},
-						Resources:    corev1.ResourceRequirements{},
-						VolumeMounts: script.VolumeMount(),
-						Ports:        []corev1.ContainerPort{{ContainerPort: 6565}},
+						Image:           "ghcr.io/grafana/operator:latest-runner",
+						ImagePullPolicy: "",
+						Name:            "k6",
+						Command:         []string{"k6", "run", "--quiet", "/test/test.js", "--address=0.0.0.0:6565", "--paused", "--tag", "instance_id=1", "--tag", "job_name=test-1"},
+						Env:             []corev1.EnvVar{},
+						Resources:       corev1.ResourceRequirements{},
+						VolumeMounts:    script.VolumeMount(),
+						Ports:           []corev1.ContainerPort{{ContainerPort: 6565}},
 					}},
 					TerminationGracePeriodSeconds: &zero,
 					Volumes:                       script.Volume(),
@@ -778,9 +784,10 @@ func TestNewRunnerJobIstio(t *testing.T) {
 					AutomountServiceAccountToken: &automountServiceAccountToken,
 					SecurityContext:              &corev1.PodSecurityContext{},
 					Containers: []corev1.Container{{
-						Image:   "ghcr.io/grafana/operator:latest-runner",
-						Name:    "k6",
-						Command: []string{"scuttle", "k6", "run", "--quiet", "/test/test.js", "--address=0.0.0.0:6565", "--paused", "--tag", "instance_id=1", "--tag", "job_name=test-1"},
+						Image:           "ghcr.io/grafana/operator:latest-runner",
+						ImagePullPolicy: "",
+						Name:            "k6",
+						Command:         []string{"scuttle", "k6", "run", "--quiet", "/test/test.js", "--address=0.0.0.0:6565", "--paused", "--tag", "instance_id=1", "--tag", "job_name=test-1"},
 						Env: []corev1.EnvVar{
 							{
 								Name:  "ENVOY_ADMIN_API",
@@ -886,9 +893,10 @@ func TestNewRunnerJobCloud(t *testing.T) {
 					SecurityContext:              &corev1.PodSecurityContext{},
 					AutomountServiceAccountToken: &automountServiceAccountToken,
 					Containers: []corev1.Container{{
-						Image:   "ghcr.io/grafana/operator:latest-runner",
-						Name:    "k6",
-						Command: []string{"k6", "run", "--quiet", "--out", "cloud", "/test/test.js", "--address=0.0.0.0:6565", "--paused", "--tag", "instance_id=1", "--tag", "job_name=test-1"},
+						Image:           "ghcr.io/grafana/operator:latest-runner",
+						ImagePullPolicy: "",
+						Name:            "k6",
+						Command:         []string{"k6", "run", "--quiet", "--out", "cloud", "/test/test.js", "--address=0.0.0.0:6565", "--paused", "--tag", "instance_id=1", "--tag", "job_name=test-1"},
 						Env: []corev1.EnvVar{
 							{
 								Name:  "K6_CLOUD_PUSH_REF_ID",
@@ -985,13 +993,14 @@ func TestNewRunnerJobLocalFile(t *testing.T) {
 					AutomountServiceAccountToken: &automountServiceAccountToken,
 					SecurityContext:              &corev1.PodSecurityContext{},
 					Containers: []corev1.Container{{
-						Image:        "ghcr.io/grafana/operator:latest-runner",
-						Name:         "k6",
-						Command:      []string{"sh", "-c", "if [ ! -f /test/test.js ]; then echo \"LocalFile not found exiting...\"; exit 1; fi;\nk6 run --quiet /test/test.js --address=0.0.0.0:6565 --paused --tag instance_id=1 --tag job_name=test-1"},
-						Env:          []corev1.EnvVar{},
-						Resources:    corev1.ResourceRequirements{},
-						VolumeMounts: script.VolumeMount(),
-						Ports:        []corev1.ContainerPort{{ContainerPort: 6565}},
+						Image:           "ghcr.io/grafana/operator:latest-runner",
+						ImagePullPolicy: "",
+						Name:            "k6",
+						Command:         []string{"sh", "-c", "if [ ! -f /test/test.js ]; then echo \"LocalFile not found exiting...\"; exit 1; fi;\nk6 run --quiet /test/test.js --address=0.0.0.0:6565 --paused --tag instance_id=1 --tag job_name=test-1"},
+						Env:             []corev1.EnvVar{},
+						Resources:       corev1.ResourceRequirements{},
+						VolumeMounts:    script.VolumeMount(),
+						Ports:           []corev1.ContainerPort{{ContainerPort: 6565}},
 					}},
 					TerminationGracePeriodSeconds: &zero,
 					Volumes:                       script.Volume(),
diff --git a/pkg/resources/jobs/starter_test.go b/pkg/resources/jobs/starter_test.go
index 873d1ce..3ee5204 100644
--- a/pkg/resources/jobs/starter_test.go
+++ b/pkg/resources/jobs/starter_test.go
@@ -49,7 +49,7 @@ func TestNewStarterJob(t *testing.T) {
 					RestartPolicy:                corev1.RestartPolicyNever,
 					SecurityContext:              &corev1.PodSecurityContext{},
 					Containers: []corev1.Container{
-						containers.NewCurlContainer([]string{"testing"}, "image", []string{"sh", "-c"},
+						containers.NewCurlContainer([]string{"testing"}, "image", corev1.PullNever, []string{"sh", "-c"},
 							[]corev1.EnvVar{}),
 					},
 				},
@@ -78,7 +78,8 @@ func TestNewStarterJob(t *testing.T) {
 						"awesomeAnnotation": "dope",
 					},
 				},
-				Image: "image",
+				Image:           "image",
+				ImagePullPolicy: corev1.PullNever,
 			},
 		},
 	}
@@ -128,7 +129,7 @@ func TestNewStarterJobIstio(t *testing.T) {
 					RestartPolicy:                corev1.RestartPolicyNever,
 					SecurityContext:              &corev1.PodSecurityContext{},
 					Containers: []corev1.Container{
-						containers.NewCurlContainer([]string{"testing"}, "image", []string{"scuttle", "sh", "-c"}, []corev1.EnvVar{
+						containers.NewCurlContainer([]string{"testing"}, "image", "", []string{"scuttle", "sh", "-c"}, []corev1.EnvVar{
 							{
 								Name:  "ENVOY_ADMIN_API",
 								Value: "http://127.0.0.1:15000",