diff --git a/pkg/apis/jenkins/v1alpha2/jenkins_types.go b/pkg/apis/jenkins/v1alpha2/jenkins_types.go index abc35481..a961b7a9 100644 --- a/pkg/apis/jenkins/v1alpha2/jenkins_types.go +++ b/pkg/apis/jenkins/v1alpha2/jenkins_types.go @@ -210,6 +210,13 @@ type JenkinsMaster struct { // memory: 600Mi Containers []Container `json:"containers,omitempty"` + // ImagePullSecrets is an optional list of references to secrets in the same namespace to use for pulling any of the images used by this PodSpec. + // If specified, these secrets will be passed to individual puller implementations for them to use. For example, + // in the case of docker, only DockerConfig type secrets are honored. + // More info: https://kubernetes.io/docs/concepts/containers/images#specifying-imagepullsecrets-on-a-pod + // +optional + ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"` + // List of volumes that can be mounted by containers belonging to the pod. // More info: https://kubernetes.io/docs/concepts/storage/volumes // +optional diff --git a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go index ab88bd8c..33b5ff35 100644 --- a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go @@ -322,6 +322,11 @@ func (in *JenkinsMaster) DeepCopyInto(out *JenkinsMaster) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.ImagePullSecrets != nil { + in, out := &in.ImagePullSecrets, &out.ImagePullSecrets + *out = make([]v1.LocalObjectReference, len(*in)) + copy(*out, *in) + } if in.Volumes != nil { in, out := &in.Volumes, &out.Volumes *out = make([]v1.Volume, len(*in)) diff --git a/pkg/controller/jenkins/configuration/base/reconcile.go b/pkg/controller/jenkins/configuration/base/reconcile.go index 7cb591a4..ed8ac2f1 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile.go +++ b/pkg/controller/jenkins/configuration/base/reconcile.go @@ -527,6 +527,12 @@ func (r *ReconcileJenkinsBaseConfiguration) isRecreatePodNeeded(currentJenkinsMa return true } + if !reflect.DeepEqual(r.jenkins.Spec.Master.ImagePullSecrets, currentJenkinsMasterPod.Spec.ImagePullSecrets) { + r.logger.Info(fmt.Sprintf("Jenkins Pod ImagePullSecrets has changed, actual '%+v' required '%+v', recreating pod", + currentJenkinsMasterPod.Spec.ImagePullSecrets, r.jenkins.Spec.Master.ImagePullSecrets)) + return true + } + if !reflect.DeepEqual(r.jenkins.Spec.Master.NodeSelector, currentJenkinsMasterPod.Spec.NodeSelector) { r.logger.Info(fmt.Sprintf("Jenkins pod node selector has changed, actual '%+v' required '%+v', recreating pod", currentJenkinsMasterPod.Spec.NodeSelector, r.jenkins.Spec.Master.NodeSelector)) diff --git a/pkg/controller/jenkins/configuration/base/reconcile_test.go b/pkg/controller/jenkins/configuration/base/reconcile_test.go index 82386155..a81aaf06 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile_test.go +++ b/pkg/controller/jenkins/configuration/base/reconcile_test.go @@ -124,6 +124,29 @@ func TestGetJenkinsOpts(t *testing.T) { assert.Contains(t, opts, "httpPort") assert.Equal(t, opts["httpPort"], "8080") }) + + t.Run("JENKINS_OPTS have --httpPort=--8080 argument", func(t *testing.T) { + jenkins := &v1alpha2.Jenkins{ + Spec: v1alpha2.JenkinsSpec{ + Master: v1alpha2.JenkinsMaster{ + Containers: []v1alpha2.Container{ + { + Env: []corev1.EnvVar{ + {Name: "JENKINS_OPTS", Value: "--httpPort=--8080"}, + }, + }, + }, + }, + }, + } + + opts := GetJenkinsOpts(jenkins) + + assert.Equal(t, 1, len(opts)) + assert.NotContains(t, opts, "prefix") + assert.Contains(t, opts, "httpPort") + assert.Equal(t, opts["httpPort"], "--8080") + }) } func TestCompareContainerVolumeMounts(t *testing.T) { diff --git a/pkg/controller/jenkins/configuration/base/resources/pod.go b/pkg/controller/jenkins/configuration/base/resources/pod.go index 1a40a4b9..fe899575 100644 --- a/pkg/controller/jenkins/configuration/base/resources/pod.go +++ b/pkg/controller/jenkins/configuration/base/resources/pod.go @@ -288,6 +288,7 @@ func NewJenkinsMasterPod(objectMeta metav1.ObjectMeta, jenkins *v1alpha2.Jenkins Containers: newContainers(jenkins), Volumes: append(GetJenkinsMasterPodBaseVolumes(jenkins), jenkins.Spec.Master.Volumes...), SecurityContext: jenkins.Spec.Master.SecurityContext, + ImagePullSecrets: jenkins.Spec.Master.ImagePullSecrets, }, } } diff --git a/pkg/controller/jenkins/configuration/base/validate.go b/pkg/controller/jenkins/configuration/base/validate.go index f3de634a..b49f370c 100644 --- a/pkg/controller/jenkins/configuration/base/validate.go +++ b/pkg/controller/jenkins/configuration/base/validate.go @@ -2,6 +2,7 @@ package base import ( "context" + "errors" "fmt" "regexp" @@ -60,6 +61,46 @@ func (r *ReconcileJenkinsBaseConfiguration) Validate(jenkins *v1alpha2.Jenkins) return true, nil } +func (r *ReconcileJenkinsBaseConfiguration) validateImagePullSecrets() (bool, error) { + var err error + valid := true + ips := r.jenkins.Spec.Master.ImagePullSecrets + for _, sr := range ips { + valid, err = r.validateImagePullSecret(sr.Name) + if err != nil || !valid { + return valid, err + } + } + + return valid, err +} + +func (r *ReconcileJenkinsBaseConfiguration) validateImagePullSecret(name string) (bool, error) { + secret := &corev1.Secret{} + err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: r.jenkins.ObjectMeta.Namespace}, secret) + if err != nil && apierrors.IsNotFound(err) { + r.logger.V(log.VWarn).Info(fmt.Sprintf("Secret '%s' not found", name)) + return false, nil + } else if err != nil && !apierrors.IsNotFound(err) { + return false, stackerr.WithStack(err) + } + + if secret.Data["docker-server"] == nil { + return false, errors.New("docker server not set") + } + if secret.Data["docker-username"] == nil { + return false, errors.New("docker username not set") + } + if secret.Data["docker-password"] == nil { + return false, errors.New("docker password not set") + } + if secret.Data["docker-email"] == nil { + return false, errors.New("docker email not set") + } + + return true, nil +} + func (r *ReconcileJenkinsBaseConfiguration) validateVolumes() (bool, error) { valid := true for _, volume := range r.jenkins.Spec.Master.Volumes { diff --git a/pkg/controller/jenkins/configuration/base/validate_test.go b/pkg/controller/jenkins/configuration/base/validate_test.go index 07b7c208..cac463b2 100644 --- a/pkg/controller/jenkins/configuration/base/validate_test.go +++ b/pkg/controller/jenkins/configuration/base/validate_test.go @@ -132,6 +132,199 @@ func TestValidatePlugins(t *testing.T) { }) } +func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T) { + t.Run("happy", func(t *testing.T) { + lor := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ref", + }, + Data: map[string][]byte{ + "docker-server": []byte("test_server"), + "docker-username": []byte("test_user"), + "docker-password": []byte("test_password"), + "docker-email": []byte("test_email"), + }, + } + + jenkins := v1alpha2.Jenkins{ + Spec: v1alpha2.JenkinsSpec{ + Master: v1alpha2.JenkinsMaster{ + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: lor.ObjectMeta.Name}, + }, + }, + }, + } + + fakeClient := fake.NewFakeClient() + err := fakeClient.Create(context.TODO(), lor) + assert.NoError(t, err) + + baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), + &jenkins, false, false, nil, nil) + + got, err := baseReconcileLoop.validateImagePullSecrets() + assert.Equal(t, got, true) + assert.NoError(t, err) + }) + + t.Run("no secret", func(t *testing.T) { + jenkins := v1alpha2.Jenkins{ + Spec: v1alpha2.JenkinsSpec{ + Master: v1alpha2.JenkinsMaster{ + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: "test-ref"}, + }, + }, + }, + } + + fakeClient := fake.NewFakeClient() + + baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), + &jenkins, false, false, nil, nil) + + got, _ := baseReconcileLoop.validateImagePullSecrets() + assert.Equal(t, got, false) + }) + + t.Run("no docker email", func(t *testing.T) { + lor := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ref", + }, + Data: map[string][]byte{ + "docker-server": []byte("test_server"), + "docker-username": []byte("test_user"), + "docker-password": []byte("test_password"), + }, + } + + jenkins := v1alpha2.Jenkins{ + Spec: v1alpha2.JenkinsSpec{ + Master: v1alpha2.JenkinsMaster{ + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: lor.ObjectMeta.Name}, + }, + }, + }, + } + + fakeClient := fake.NewFakeClient() + err := fakeClient.Create(context.TODO(), lor) + assert.NoError(t, err) + + baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), + &jenkins, false, false, nil, nil) + + got, err := baseReconcileLoop.validateImagePullSecrets() + assert.Equal(t, got, false) + assert.Error(t, err) + }) + + t.Run("no docker password", func(t *testing.T) { + lor := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ref", + }, + Data: map[string][]byte{ + "docker-server": []byte("test_server"), + "docker-username": []byte("test_user"), + "docker-email": []byte("test_email"), + }, + } + + jenkins := v1alpha2.Jenkins{ + Spec: v1alpha2.JenkinsSpec{ + Master: v1alpha2.JenkinsMaster{ + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: lor.ObjectMeta.Name}, + }, + }, + }, + } + + fakeClient := fake.NewFakeClient() + err := fakeClient.Create(context.TODO(), lor) + assert.NoError(t, err) + + baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), + &jenkins, false, false, nil, nil) + + got, err := baseReconcileLoop.validateImagePullSecrets() + assert.Equal(t, got, false) + assert.Error(t, err) + }) + + t.Run("no docker username", func(t *testing.T) { + lor := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ref", + }, + Data: map[string][]byte{ + "docker-server": []byte("test_server"), + "docker-password": []byte("test_password"), + "docker-email": []byte("test_email"), + }, + } + + jenkins := v1alpha2.Jenkins{ + Spec: v1alpha2.JenkinsSpec{ + Master: v1alpha2.JenkinsMaster{ + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: lor.ObjectMeta.Name}, + }, + }, + }, + } + + fakeClient := fake.NewFakeClient() + err := fakeClient.Create(context.TODO(), lor) + assert.NoError(t, err) + + baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), + &jenkins, false, false, nil, nil) + + got, err := baseReconcileLoop.validateImagePullSecrets() + assert.Equal(t, got, false) + assert.Error(t, err) + }) + + t.Run("no docker server", func(t *testing.T) { + lor := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ref", + }, + Data: map[string][]byte{ + "docker-username": []byte("test_user"), + "docker-password": []byte("test_password"), + "docker-email": []byte("test_email"), + }, + } + + jenkins := v1alpha2.Jenkins{ + Spec: v1alpha2.JenkinsSpec{ + Master: v1alpha2.JenkinsMaster{ + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: lor.ObjectMeta.Name}, + }, + }, + }, + } + + fakeClient := fake.NewFakeClient() + err := fakeClient.Create(context.TODO(), lor) + assert.NoError(t, err) + + baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), + &jenkins, false, false, nil, nil) + + got, err := baseReconcileLoop.validateImagePullSecrets() + assert.Equal(t, got, false) + assert.Error(t, err) + }) +} + func TestValidateJenkinsMasterPodEnvs(t *testing.T) { t.Run("happy", func(t *testing.T) { jenkins := v1alpha2.Jenkins{