diff --git a/pkg/apis/jenkinsio/v1alpha1/jenkins_types.go b/pkg/apis/jenkinsio/v1alpha1/jenkins_types.go index 30483cef..2002fbca 100644 --- a/pkg/apis/jenkinsio/v1alpha1/jenkins_types.go +++ b/pkg/apis/jenkinsio/v1alpha1/jenkins_types.go @@ -25,6 +25,7 @@ type JenkinsMaster struct { NodeSelector map[string]string `json:"nodeSelector,omitempty"` Annotations map[string]string `json:"masterAnnotations,omitempty"` Resources corev1.ResourceRequirements `json:"resources,omitempty"` + Env []corev1.EnvVar `json:"env,omitempty"` // OperatorPlugins contains plugins required by operator OperatorPlugins map[string][]string `json:"basePlugins,omitempty"` // Plugins contains plugins required by user diff --git a/pkg/apis/jenkinsio/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/jenkinsio/v1alpha1/zz_generated.deepcopy.go index 16681b77..edd8b3a0 100644 --- a/pkg/apis/jenkinsio/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/jenkinsio/v1alpha1/zz_generated.deepcopy.go @@ -128,6 +128,13 @@ func (in *JenkinsMaster) DeepCopyInto(out *JenkinsMaster) { } } in.Resources.DeepCopyInto(&out.Resources) + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = make([]v1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.OperatorPlugins != nil { in, out := &in.OperatorPlugins, &out.OperatorPlugins *out = make(map[string][]string, len(*in)) diff --git a/pkg/controller/jenkins/configuration/base/reconcile.go b/pkg/controller/jenkins/configuration/base/reconcile.go index 28d8e8ba..fdd99cf9 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile.go +++ b/pkg/controller/jenkins/configuration/base/reconcile.go @@ -401,7 +401,15 @@ func (r *ReconcileJenkinsBaseConfiguration) isRecreatePodNeeded(currentJenkinsMa 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", - r.jenkins.Spec.Master.NodeSelector, currentJenkinsMasterPod.Spec.NodeSelector)) + currentJenkinsMasterPod.Spec.NodeSelector, r.jenkins.Spec.Master.NodeSelector)) + recreatePod = true + } + + requiredEnvs := resources.GetJenkinsMasterPodBaseEnvs() + requiredEnvs = append(requiredEnvs, r.jenkins.Spec.Master.Env...) + if !reflect.DeepEqual(requiredEnvs, currentJenkinsMasterPod.Spec.Containers[0].Env) { + r.logger.Info(fmt.Sprintf("Jenkins env have changed, actual '%+v' required '%+v' - recreating pod", + currentJenkinsMasterPod.Spec.Containers[0].Env, requiredEnvs)) recreatePod = true } diff --git a/pkg/controller/jenkins/configuration/base/resources/pod.go b/pkg/controller/jenkins/configuration/base/resources/pod.go index 583c2325..898b75ac 100644 --- a/pkg/controller/jenkins/configuration/base/resources/pod.go +++ b/pkg/controller/jenkins/configuration/base/resources/pod.go @@ -55,6 +55,24 @@ func buildPodTypeMeta() metav1.TypeMeta { } } +// GetJenkinsMasterPodBaseEnvs returns Jenkins master pod envs required by operator +func GetJenkinsMasterPodBaseEnvs() []corev1.EnvVar { + return []corev1.EnvVar{ + { + Name: "JENKINS_HOME", + Value: jenkinsHomePath, + }, + { + Name: "JAVA_OPTS", + Value: "-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=1 -Djenkins.install.runSetupWizard=false -Djava.awt.headless=true", + }, + { + Name: "SECRETS", // https://github.com/jenkinsci/configuration-as-code-plugin/blob/master/demos/kubernetes-secrets/README.md + Value: UserConfigurationSecretVolumePath, + }, + } +} + // NewJenkinsMasterPod builds Jenkins Master Kubernetes Pod resource func NewJenkinsMasterPod(objectMeta metav1.ObjectMeta, jenkins *v1alpha1.Jenkins) *corev1.Pod { initialDelaySeconds := int32(30) @@ -63,6 +81,8 @@ func NewJenkinsMasterPod(objectMeta metav1.ObjectMeta, jenkins *v1alpha1.Jenkins runAsUser := jenkinsUserUID objectMeta.Annotations = jenkins.Spec.Master.Annotations + envs := GetJenkinsMasterPodBaseEnvs() + envs = append(envs, jenkins.Spec.Master.Env...) return &corev1.Pod{ TypeMeta: buildPodTypeMeta(), @@ -115,20 +135,7 @@ func NewJenkinsMasterPod(objectMeta metav1.ObjectMeta, jenkins *v1alpha1.Jenkins ContainerPort: constants.DefaultSlavePortInt32, }, }, - Env: []corev1.EnvVar{ - { - Name: "JENKINS_HOME", - Value: jenkinsHomePath, - }, - { - Name: "JAVA_OPTS", - Value: "-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=1 -Djenkins.install.runSetupWizard=false -Djava.awt.headless=true", - }, - { - Name: "SECRETS", // https://github.com/jenkinsci/configuration-as-code-plugin/blob/master/demos/kubernetes-secrets/README.md - Value: UserConfigurationSecretVolumePath, - }, - }, + Env: envs, Resources: jenkins.Spec.Master.Resources, VolumeMounts: []corev1.VolumeMount{ { diff --git a/pkg/controller/jenkins/configuration/base/validate.go b/pkg/controller/jenkins/configuration/base/validate.go index 4bd7f1b1..bbce249a 100644 --- a/pkg/controller/jenkins/configuration/base/validate.go +++ b/pkg/controller/jenkins/configuration/base/validate.go @@ -5,6 +5,7 @@ import ( "regexp" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkinsio/v1alpha1" + "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/plugins" "github.com/jenkinsci/kubernetes-operator/pkg/log" @@ -32,9 +33,31 @@ func (r *ReconcileJenkinsBaseConfiguration) Validate(jenkins *v1alpha1.Jenkins) return false, nil } + if !r.validateJenkinsMasterPodEnvs() { + return false, nil + } + return true, nil } +func (r *ReconcileJenkinsBaseConfiguration) validateJenkinsMasterPodEnvs() bool { + baseEnvs := resources.GetJenkinsMasterPodBaseEnvs() + baseEnvNames := map[string]string{} + for _, env := range baseEnvs { + baseEnvNames[env.Name] = env.Value + } + + valid := true + for _, userEnv := range r.jenkins.Spec.Master.Env { + if _, overriding := baseEnvNames[userEnv.Name]; overriding { + r.logger.V(log.VWarn).Info(fmt.Sprintf("Jenkins Master pod env '%s' cannot be overridden", userEnv.Name)) + valid = false + } + } + + return valid +} + func (r *ReconcileJenkinsBaseConfiguration) validatePlugins(pluginsWithVersionSlice ...map[string][]string) bool { valid := true allPlugins := map[plugins.Plugin][]plugins.Plugin{} diff --git a/pkg/controller/jenkins/configuration/base/validate_test.go b/pkg/controller/jenkins/configuration/base/validate_test.go index 6df2c8e2..301df06c 100644 --- a/pkg/controller/jenkins/configuration/base/validate_test.go +++ b/pkg/controller/jenkins/configuration/base/validate_test.go @@ -3,7 +3,10 @@ package base import ( "testing" + "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkinsio/v1alpha1" + "github.com/stretchr/testify/assert" + "k8s.io/api/core/v1" logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" ) @@ -65,3 +68,42 @@ func TestValidatePlugins(t *testing.T) { }) } + +func TestValidateJenkinsMasterPodEnvs(t *testing.T) { + t.Run("happy", func(t *testing.T) { + jenkins := v1alpha1.Jenkins{ + Spec: v1alpha1.JenkinsSpec{ + Master: v1alpha1.JenkinsMaster{ + Env: []v1.EnvVar{ + { + Name: "SOME_VALUE", + Value: "", + }, + }, + }, + }, + } + baseReconcileLoop := New(nil, nil, logf.ZapLogger(false), + &jenkins, false, false) + got := baseReconcileLoop.validateJenkinsMasterPodEnvs() + assert.Equal(t, true, got) + }) + t.Run("override JENKINS_HOME env", func(t *testing.T) { + jenkins := v1alpha1.Jenkins{ + Spec: v1alpha1.JenkinsSpec{ + Master: v1alpha1.JenkinsMaster{ + Env: []v1.EnvVar{ + { + Name: "JENKINS_HOME", + Value: "", + }, + }, + }, + }, + } + baseReconcileLoop := New(nil, nil, logf.ZapLogger(false), + &jenkins, false, false) + got := baseReconcileLoop.validateJenkinsMasterPodEnvs() + assert.Equal(t, false, got) + }) +} diff --git a/test/e2e/configuration_test.go b/test/e2e/configuration_test.go index 202e9eec..541e9ea9 100644 --- a/test/e2e/configuration_test.go +++ b/test/e2e/configuration_test.go @@ -143,6 +143,12 @@ func verifyJenkinsMasterPodAttributes(t *testing.T, jenkins *v1alpha1.Jenkins) { t.Fatalf("Invalid jenkins pod node selector expected '%+v', actual '%+v'", jenkins.Spec.Master.NodeSelector, jenkinsPod.Spec.NodeSelector) } + requiredEnvs := resources.GetJenkinsMasterPodBaseEnvs() + requiredEnvs = append(requiredEnvs, jenkins.Spec.Master.Env...) + if !reflect.DeepEqual(jenkinsPod.Spec.Containers[0].Env, requiredEnvs) { + t.Fatalf("Invalid jenkins pod continer resources expected '%+v', actual '%+v'", requiredEnvs, jenkinsPod.Spec.Containers[0].Env) + } + t.Log("Jenkins pod attributes are valid") } diff --git a/test/e2e/jenkins.go b/test/e2e/jenkins.go index a43ccb80..85abda5b 100644 --- a/test/e2e/jenkins.go +++ b/test/e2e/jenkins.go @@ -73,6 +73,12 @@ func createJenkinsCR(t *testing.T, name, namespace string) *v1alpha1.Jenkins { "simple-theme-plugin:0.5.1": {}, }, NodeSelector: map[string]string{"kubernetes.io/hostname": "minikube"}, + Env: []v1.EnvVar{ + { + Name: "TEST_ENV", + Value: "test_env_value", + }, + }, }, //TODO(bantoniak) add seed job with private key SeedJobs: []v1alpha1.SeedJob{