From 55d6aecb936e4ef8bcf995ef663918b7b662937a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20S=C4=99k?= Date: Mon, 27 May 2019 23:35:09 +0200 Subject: [PATCH] #9 Allow mount secrets and configmaps in Jenkins pod --- docs/developer-guide.md | 6 + pkg/apis/jenkinsio/v1alpha1/jenkins_types.go | 3 +- .../jenkins/configuration/base/reconcile.go | 26 +- .../configuration/base/reconcile_test.go | 68 +++++ .../configuration/base/resources/pod.go | 231 ++++++++------- .../jenkins/configuration/base/validate.go | 121 +++++++- .../configuration/base/validate_test.go | 268 ++++++++++++++++++ test/e2e/configuration_test.go | 36 ++- test/e2e/jenkins.go | 3 +- test/e2e/restart_test.go | 4 +- test/e2e/seedjobs_test.go | 2 +- 11 files changed, 654 insertions(+), 114 deletions(-) diff --git a/docs/developer-guide.md b/docs/developer-guide.md index 75bc5fac..7ad25538 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -53,6 +53,12 @@ eval $(minikube docker-env) make e2e ``` +Run the specific e2e test: + +```bash +make e2e E2E_TEST_SELECTOR='^TestConfiguration$' +``` + ## Tips & Tricks ### Building docker image on minikube (for e2e tests) diff --git a/pkg/apis/jenkinsio/v1alpha1/jenkins_types.go b/pkg/apis/jenkinsio/v1alpha1/jenkins_types.go index dc4b8288..9b99403d 100644 --- a/pkg/apis/jenkinsio/v1alpha1/jenkins_types.go +++ b/pkg/apis/jenkinsio/v1alpha1/jenkins_types.go @@ -40,12 +40,13 @@ type Container struct { // JenkinsMaster defines the Jenkins master pod attributes and plugins, // every single change requires Jenkins master pod restart type JenkinsMaster struct { - Container + Container //TODO move to containers // pod properties Annotations map[string]string `json:"masterAnnotations,omitempty"` NodeSelector map[string]string `json:"nodeSelector,omitempty"` Containers []Container `json:"containers,omitempty"` + Volumes []corev1.Volume `json:"volumes,omitempty"` // OperatorPlugins contains plugins required by operator OperatorPlugins map[string][]string `json:"basePlugins,omitempty"` diff --git a/pkg/controller/jenkins/configuration/base/reconcile.go b/pkg/controller/jenkins/configuration/base/reconcile.go index 40bee07f..b7e5fdf9 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile.go +++ b/pkg/controller/jenkins/configuration/base/reconcile.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "reflect" + "strings" "time" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkinsio/v1alpha1" @@ -378,6 +379,8 @@ func (r *ReconcileJenkinsBaseConfiguration) isRecreatePodNeeded(currentJenkinsMa return true } + //TODO check if image can't be pulled, volume can't be mounted etc. (get info from events) + if currentJenkinsMasterPod.Status.Phase == corev1.PodFailed || currentJenkinsMasterPod.Status.Phase == corev1.PodSucceeded || currentJenkinsMasterPod.Status.Phase == corev1.PodUnknown { @@ -397,6 +400,12 @@ func (r *ReconcileJenkinsBaseConfiguration) isRecreatePodNeeded(currentJenkinsMa return true } + if !r.compareVolumes(currentJenkinsMasterPod) { + r.logger.Info(fmt.Sprintf("Jenkins pod volumes have changed, actual '%+v' required '%+v', recreating pod", + currentJenkinsMasterPod.Spec.Volumes, r.jenkins.Spec.Master.Volumes)) + return true + } + if (len(r.jenkins.Spec.Master.Containers) + 1) != len(currentJenkinsMasterPod.Spec.Containers) { r.logger.Info(fmt.Sprintf("Jenkins amount of containers has changed to '%+v', recreating pod", len(r.jenkins.Spec.Master.Containers)+1)) return true @@ -485,7 +494,7 @@ func (r *ReconcileJenkinsBaseConfiguration) compareContainers(expected corev1.Co return true } if !CompareContainerVolumeMounts(expected, actual) { - r.logger.Info(fmt.Sprintf("Volume mounts has changed to '%+v' in container '%s', recreating pod", expected.VolumeMounts, expected.Name)) + r.logger.Info(fmt.Sprintf("Volume mounts have changed to '%+v' in container '%s', recreating pod", expected.VolumeMounts, expected.Name)) return true } @@ -504,6 +513,21 @@ func CompareContainerVolumeMounts(expected corev1.Container, actual corev1.Conta return reflect.DeepEqual(expected.VolumeMounts, withoutServiceAccount) } +// compareVolumes returns true if Jenkins pod and Jenkins CR volumes are the same +func (r *ReconcileJenkinsBaseConfiguration) compareVolumes(actualPod corev1.Pod) bool { + var withoutServiceAccount []corev1.Volume + for _, volume := range actualPod.Spec.Volumes { + if !strings.HasPrefix(volume.Name, actualPod.Spec.ServiceAccountName) { + withoutServiceAccount = append(withoutServiceAccount, volume) + } + } + + return reflect.DeepEqual( + append(resources.GetJenkinsMasterPodBaseVolumes(r.jenkins), r.jenkins.Spec.Master.Volumes...), + withoutServiceAccount, + ) +} + func (r *ReconcileJenkinsBaseConfiguration) restartJenkinsMasterPod(meta metav1.ObjectMeta) error { currentJenkinsMasterPod, err := r.getJenkinsMasterPod(meta) if err != nil { diff --git a/pkg/controller/jenkins/configuration/base/reconcile_test.go b/pkg/controller/jenkins/configuration/base/reconcile_test.go index 1144f55d..7eeea3bb 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile_test.go +++ b/pkg/controller/jenkins/configuration/base/reconcile_test.go @@ -3,6 +3,9 @@ package base import ( "testing" + "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkinsio/v1alpha1" + "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources" + "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" ) @@ -81,3 +84,68 @@ func TestCompareContainerVolumeMounts(t *testing.T) { assert.False(t, got) }) } + +func TestCompareVolumes(t *testing.T) { + t.Run("defaults", func(t *testing.T) { + jenkins := &v1alpha1.Jenkins{} + pod := corev1.Pod{ + Spec: corev1.PodSpec{ + ServiceAccountName: "service-account-name", + Volumes: resources.GetJenkinsMasterPodBaseVolumes(jenkins), + }, + } + reconciler := New(nil, nil, nil, jenkins, false, false) + + got := reconciler.compareVolumes(pod) + + assert.True(t, got) + }) + t.Run("different", func(t *testing.T) { + jenkins := &v1alpha1.Jenkins{ + Spec: v1alpha1.JenkinsSpec{ + Master: v1alpha1.JenkinsMaster{ + Volumes: []corev1.Volume{ + { + Name: "added", + }, + }, + }, + }, + } + pod := corev1.Pod{ + Spec: corev1.PodSpec{ + ServiceAccountName: "service-account-name", + Volumes: resources.GetJenkinsMasterPodBaseVolumes(jenkins), + }, + } + reconciler := New(nil, nil, nil, jenkins, false, false) + + got := reconciler.compareVolumes(pod) + + assert.False(t, got) + }) + t.Run("added one volume", func(t *testing.T) { + jenkins := &v1alpha1.Jenkins{ + Spec: v1alpha1.JenkinsSpec{ + Master: v1alpha1.JenkinsMaster{ + Volumes: []corev1.Volume{ + { + Name: "added", + }, + }, + }, + }, + } + pod := corev1.Pod{ + Spec: corev1.PodSpec{ + ServiceAccountName: "service-account-name", + Volumes: append(resources.GetJenkinsMasterPodBaseVolumes(jenkins), corev1.Volume{Name: "added"}), + }, + } + reconciler := New(nil, nil, nil, jenkins, false, false) + + got := reconciler.compareVolumes(pod) + + assert.True(t, got) + }) +} diff --git a/pkg/controller/jenkins/configuration/base/resources/pod.go b/pkg/controller/jenkins/configuration/base/resources/pod.go index c41fc13e..9b63969e 100644 --- a/pkg/controller/jenkins/configuration/base/resources/pod.go +++ b/pkg/controller/jenkins/configuration/base/resources/pod.go @@ -13,9 +13,10 @@ import ( const ( // JenkinsMasterContainerName is the Jenkins master container name in pod JenkinsMasterContainerName = "jenkins-master" - jenkinsHomeVolumeName = "home" - jenkinsPath = "/var/jenkins" - jenkinsHomePath = jenkinsPath + "/home" + // JenkinsHomeVolumeName is the Jenkins home volume name + JenkinsHomeVolumeName = "home" + jenkinsPath = "/var/jenkins" + jenkinsHomePath = jenkinsPath + "/home" jenkinsScriptsVolumeName = "scripts" jenkinsScriptsVolumePath = jenkinsPath + "/scripts" @@ -74,6 +75,123 @@ func GetJenkinsMasterPodBaseEnvs() []corev1.EnvVar { } } +// GetJenkinsMasterPodBaseVolumes returns Jenkins master pod volumes required by operator +func GetJenkinsMasterPodBaseVolumes(jenkins *v1alpha1.Jenkins) []corev1.Volume { + configMapVolumeSourceDefaultMode := corev1.ConfigMapVolumeSourceDefaultMode + secretVolumeSourceDefaultMode := corev1.SecretVolumeSourceDefaultMode + return []corev1.Volume{ + { + Name: JenkinsHomeVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + { + Name: jenkinsScriptsVolumeName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + DefaultMode: &configMapVolumeSourceDefaultMode, + LocalObjectReference: corev1.LocalObjectReference{ + Name: getScriptsConfigMapName(jenkins), + }, + }, + }, + }, + { + Name: jenkinsInitConfigurationVolumeName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + DefaultMode: &configMapVolumeSourceDefaultMode, + LocalObjectReference: corev1.LocalObjectReference{ + Name: GetInitConfigurationConfigMapName(jenkins), + }, + }, + }, + }, + { + Name: jenkinsBaseConfigurationVolumeName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + DefaultMode: &configMapVolumeSourceDefaultMode, + LocalObjectReference: corev1.LocalObjectReference{ + Name: GetBaseConfigurationConfigMapName(jenkins), + }, + }, + }, + }, + { + Name: jenkinsUserConfigurationVolumeName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + DefaultMode: &configMapVolumeSourceDefaultMode, + LocalObjectReference: corev1.LocalObjectReference{ + Name: GetUserConfigurationConfigMapNameFromJenkins(jenkins), + }, + }, + }, + }, + { + Name: jenkinsOperatorCredentialsVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + DefaultMode: &secretVolumeSourceDefaultMode, + SecretName: GetOperatorCredentialsSecretName(jenkins), + }, + }, + }, + { + Name: userConfigurationSecretVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + DefaultMode: &secretVolumeSourceDefaultMode, + SecretName: GetUserConfigurationSecretNameFromJenkins(jenkins), + }, + }, + }, + } +} + +// GetJenkinsMasterContainerBaseVolumeMounts returns Jenkins master pod volume mounts required by operator +func GetJenkinsMasterContainerBaseVolumeMounts() []corev1.VolumeMount { + return []corev1.VolumeMount{ + { + Name: JenkinsHomeVolumeName, + MountPath: jenkinsHomePath, + ReadOnly: false, + }, + { + Name: jenkinsScriptsVolumeName, + MountPath: jenkinsScriptsVolumePath, + ReadOnly: true, + }, + { + Name: jenkinsInitConfigurationVolumeName, + MountPath: jenkinsInitConfigurationVolumePath, + ReadOnly: true, + }, + { + Name: jenkinsBaseConfigurationVolumeName, + MountPath: JenkinsBaseConfigurationVolumePath, + ReadOnly: true, + }, + { + Name: jenkinsUserConfigurationVolumeName, + MountPath: JenkinsUserConfigurationVolumePath, + ReadOnly: true, + }, + { + Name: jenkinsOperatorCredentialsVolumeName, + MountPath: jenkinsOperatorCredentialsVolumePath, + ReadOnly: true, + }, + { + Name: userConfigurationSecretVolumeName, + MountPath: UserConfigurationSecretVolumePath, + ReadOnly: true, + }, + } +} + // NewJenkinsMasterContainer returns Jenkins master Kubernetes container func NewJenkinsMasterContainer(jenkins *v1alpha1.Jenkins) corev1.Container { envs := GetJenkinsMasterPodBaseEnvs() @@ -101,45 +219,9 @@ func NewJenkinsMasterContainer(jenkins *v1alpha1.Jenkins) corev1.Container { Protocol: corev1.ProtocolTCP, }, }, - Env: envs, - Resources: jenkins.Spec.Master.Resources, - VolumeMounts: []corev1.VolumeMount{ - { - Name: jenkinsHomeVolumeName, - MountPath: jenkinsHomePath, - ReadOnly: false, - }, - { - Name: jenkinsScriptsVolumeName, - MountPath: jenkinsScriptsVolumePath, - ReadOnly: true, - }, - { - Name: jenkinsInitConfigurationVolumeName, - MountPath: jenkinsInitConfigurationVolumePath, - ReadOnly: true, - }, - { - Name: jenkinsBaseConfigurationVolumeName, - MountPath: JenkinsBaseConfigurationVolumePath, - ReadOnly: true, - }, - { - Name: jenkinsUserConfigurationVolumeName, - MountPath: JenkinsUserConfigurationVolumePath, - ReadOnly: true, - }, - { - Name: jenkinsOperatorCredentialsVolumeName, - MountPath: jenkinsOperatorCredentialsVolumePath, - ReadOnly: true, - }, - { - Name: userConfigurationSecretVolumeName, - MountPath: UserConfigurationSecretVolumePath, - ReadOnly: true, - }, - }, + Env: envs, + Resources: jenkins.Spec.Master.Resources, + VolumeMounts: append(GetJenkinsMasterContainerBaseVolumeMounts(), jenkins.Spec.Master.VolumeMounts...), } } @@ -192,70 +274,7 @@ func NewJenkinsMasterPod(objectMeta metav1.ObjectMeta, jenkins *v1alpha1.Jenkins }, NodeSelector: jenkins.Spec.Master.NodeSelector, Containers: newContainers(jenkins), - Volumes: []corev1.Volume{ - { - Name: jenkinsHomeVolumeName, - VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{}, - }, - }, - { - Name: jenkinsScriptsVolumeName, - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: getScriptsConfigMapName(jenkins), - }, - }, - }, - }, - { - Name: jenkinsInitConfigurationVolumeName, - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: GetInitConfigurationConfigMapName(jenkins), - }, - }, - }, - }, - { - Name: jenkinsBaseConfigurationVolumeName, - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: GetBaseConfigurationConfigMapName(jenkins), - }, - }, - }, - }, - { - Name: jenkinsUserConfigurationVolumeName, - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: GetUserConfigurationConfigMapNameFromJenkins(jenkins), - }, - }, - }, - }, - { - Name: jenkinsOperatorCredentialsVolumeName, - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: GetOperatorCredentialsSecretName(jenkins), - }, - }, - }, - { - Name: userConfigurationSecretVolumeName, - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: GetUserConfigurationSecretNameFromJenkins(jenkins), - }, - }, - }, - }, + Volumes: append(GetJenkinsMasterPodBaseVolumes(jenkins), jenkins.Spec.Master.Volumes...), }, } } diff --git a/pkg/controller/jenkins/configuration/base/validate.go b/pkg/controller/jenkins/configuration/base/validate.go index 2a895d62..0fb038c2 100644 --- a/pkg/controller/jenkins/configuration/base/validate.go +++ b/pkg/controller/jenkins/configuration/base/validate.go @@ -1,6 +1,7 @@ package base import ( + "context" "fmt" "regexp" @@ -10,6 +11,10 @@ import ( "github.com/jenkinsci/kubernetes-operator/pkg/log" docker "github.com/docker/distribution/reference" + stackerr "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" ) var ( @@ -18,6 +23,15 @@ var ( // Validate validates Jenkins CR Spec.master section func (r *ReconcileJenkinsBaseConfiguration) Validate(jenkins *v1alpha1.Jenkins) (bool, error) { + if !r.validateReservedVolumes() { + return false, nil + } + if valid, err := r.validateVolumes(); err != nil { + return false, err + } else if !valid { + return false, nil + } + if !r.validateContainer(jenkins.Spec.Master.Container) { return false, nil } @@ -39,6 +53,80 @@ func (r *ReconcileJenkinsBaseConfiguration) Validate(jenkins *v1alpha1.Jenkins) return true, nil } +func (r *ReconcileJenkinsBaseConfiguration) validateVolumes() (bool, error) { + valid := true + for _, volume := range r.jenkins.Spec.Master.Volumes { + switch { + case volume.ConfigMap != nil: + if ok, err := r.validateConfigMapVolume(volume); err != nil { + return false, err + } else if !ok { + valid = false + } + case volume.Secret != nil: + if ok, err := r.validateSecretVolume(volume); err != nil { + return false, err + } else if !ok { + valid = false + } + default: //TODO add support for rest of volumes + valid = false + r.logger.V(log.VWarn).Info(fmt.Sprintf("Unsupported volume '%+v'", volume)) + } + } + + return valid, nil +} + +func (r *ReconcileJenkinsBaseConfiguration) validateConfigMapVolume(volume corev1.Volume) (bool, error) { + if volume.ConfigMap.Optional != nil && *volume.ConfigMap.Optional { + return true, nil + } + + configMap := &corev1.ConfigMap{} + err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: volume.ConfigMap.Name, Namespace: r.jenkins.ObjectMeta.Namespace}, configMap) + if err != nil && apierrors.IsNotFound(err) { + r.logger.V(log.VWarn).Info(fmt.Sprintf("ConfigMap '%s' not found for volume '%+v'", volume.ConfigMap.Name, volume)) + return false, nil + } else if err != nil && !apierrors.IsNotFound(err) { + return false, stackerr.WithStack(err) + } + + return true, nil +} + +func (r *ReconcileJenkinsBaseConfiguration) validateSecretVolume(volume corev1.Volume) (bool, error) { + if volume.Secret.Optional != nil && *volume.Secret.Optional { + return true, nil + } + + secret := &corev1.Secret{} + err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: volume.Secret.SecretName, Namespace: r.jenkins.ObjectMeta.Namespace}, secret) + if err != nil && apierrors.IsNotFound(err) { + r.logger.V(log.VWarn).Info(fmt.Sprintf("Secret '%s' not found for volume '%+v'", volume.Secret.SecretName, volume)) + return false, nil + } else if err != nil && !apierrors.IsNotFound(err) { + return false, stackerr.WithStack(err) + } + + return true, nil +} + +func (r *ReconcileJenkinsBaseConfiguration) validateReservedVolumes() bool { + valid := true + + for _, baseVolume := range resources.GetJenkinsMasterPodBaseVolumes(r.jenkins) { + for _, volume := range r.jenkins.Spec.Master.Volumes { + if baseVolume.Name == volume.Name { + r.logger.V(log.VWarn).Info(fmt.Sprintf("Jenkins Master pod volume '%s' is reserved please choose different one", volume.Name)) + valid = false + } + } + } + + return valid +} + func (r *ReconcileJenkinsBaseConfiguration) validateContainer(container v1alpha1.Container) bool { logger := r.logger.WithValues("container", container.Name) if container.Image == "" { @@ -47,7 +135,7 @@ func (r *ReconcileJenkinsBaseConfiguration) validateContainer(container v1alpha1 } if !dockerImageRegexp.MatchString(container.Image) && !docker.ReferenceRegexp.MatchString(container.Image) { - r.logger.V(log.VWarn).Info("Invalid image") + logger.V(log.VWarn).Info("Invalid image") return false } @@ -56,9 +144,40 @@ func (r *ReconcileJenkinsBaseConfiguration) validateContainer(container v1alpha1 return false } + if !r.validateContainerVolumeMounts(container) { + return false + } + return true } +func (r *ReconcileJenkinsBaseConfiguration) validateContainerVolumeMounts(container v1alpha1.Container) bool { + logger := r.logger.WithValues("container", container.Name) + allVolumes := append(resources.GetJenkinsMasterPodBaseVolumes(r.jenkins), r.jenkins.Spec.Master.Volumes...) + valid := true + + for _, volumeMount := range container.VolumeMounts { + if len(volumeMount.MountPath) == 0 { + logger.V(log.VWarn).Info(fmt.Sprintf("mountPath not set for '%s' volume mount", volumeMount.Name)) + valid = false + } + + foundVolume := false + for _, volume := range allVolumes { + if volumeMount.Name == volume.Name { + foundVolume = true + } + } + + if !foundVolume { + logger.V(log.VWarn).Info(fmt.Sprintf("Not found volume for '%s' volume mount", volumeMount.Name)) + valid = false + } + } + + return valid +} + func (r *ReconcileJenkinsBaseConfiguration) validateJenkinsMasterPodEnvs() bool { baseEnvs := resources.GetJenkinsMasterPodBaseEnvs() baseEnvNames := map[string]string{} diff --git a/pkg/controller/jenkins/configuration/base/validate_test.go b/pkg/controller/jenkins/configuration/base/validate_test.go index ad127755..c6003ec5 100644 --- a/pkg/controller/jenkins/configuration/base/validate_test.go +++ b/pkg/controller/jenkins/configuration/base/validate_test.go @@ -1,12 +1,17 @@ package base import ( + "context" "testing" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkinsio/v1alpha1" + "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources" "github.com/stretchr/testify/assert" "k8s.io/api/core/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" ) @@ -111,3 +116,266 @@ func TestValidateJenkinsMasterPodEnvs(t *testing.T) { assert.Equal(t, false, got) }) } + +func TestValidateReservedVolumes(t *testing.T) { + t.Run("happy", func(t *testing.T) { + jenkins := v1alpha1.Jenkins{ + Spec: v1alpha1.JenkinsSpec{ + Master: v1alpha1.JenkinsMaster{ + Volumes: []v1.Volume{ + { + Name: "not-used-name", + }, + }, + }, + }, + } + baseReconcileLoop := New(nil, nil, logf.ZapLogger(false), + &jenkins, false, false) + got := baseReconcileLoop.validateReservedVolumes() + assert.Equal(t, true, got) + }) + t.Run("used reserved name", func(t *testing.T) { + jenkins := v1alpha1.Jenkins{ + Spec: v1alpha1.JenkinsSpec{ + Master: v1alpha1.JenkinsMaster{ + Volumes: []v1.Volume{ + { + Name: resources.JenkinsHomeVolumeName, + }, + }, + }, + }, + } + baseReconcileLoop := New(nil, nil, logf.ZapLogger(false), + &jenkins, false, false) + got := baseReconcileLoop.validateReservedVolumes() + assert.Equal(t, false, got) + }) +} + +func TestValidateContainerVolumeMounts(t *testing.T) { + t.Run("default Jenkins master container", func(t *testing.T) { + jenkins := v1alpha1.Jenkins{ + Spec: v1alpha1.JenkinsSpec{ + Master: v1alpha1.JenkinsMaster{}, + }, + } + baseReconcileLoop := New(nil, nil, logf.ZapLogger(false), + &jenkins, false, false) + got := baseReconcileLoop.validateContainerVolumeMounts(jenkins.Spec.Master.Container) + assert.Equal(t, true, got) + }) + t.Run("one extra volume", func(t *testing.T) { + jenkins := v1alpha1.Jenkins{ + Spec: v1alpha1.JenkinsSpec{ + Master: v1alpha1.JenkinsMaster{ + Volumes: []v1.Volume{ + { + Name: "example", + }, + }, + Container: v1alpha1.Container{ + VolumeMounts: []v1.VolumeMount{ + { + Name: "example", + MountPath: "/test", + }, + }, + }, + }, + }, + } + baseReconcileLoop := New(nil, nil, logf.ZapLogger(false), + &jenkins, false, false) + got := baseReconcileLoop.validateContainerVolumeMounts(jenkins.Spec.Master.Container) + assert.Equal(t, true, got) + }) + t.Run("empty mountPath", func(t *testing.T) { + jenkins := v1alpha1.Jenkins{ + Spec: v1alpha1.JenkinsSpec{ + Master: v1alpha1.JenkinsMaster{ + Volumes: []v1.Volume{ + { + Name: "example", + }, + }, + Container: v1alpha1.Container{ + VolumeMounts: []v1.VolumeMount{ + { + Name: "example", + MountPath: "", // empty + }, + }, + }, + }, + }, + } + baseReconcileLoop := New(nil, nil, logf.ZapLogger(false), + &jenkins, false, false) + got := baseReconcileLoop.validateContainerVolumeMounts(jenkins.Spec.Master.Container) + assert.Equal(t, false, got) + }) + t.Run("missing volume", func(t *testing.T) { + jenkins := v1alpha1.Jenkins{ + Spec: v1alpha1.JenkinsSpec{ + Master: v1alpha1.JenkinsMaster{ + Container: v1alpha1.Container{ + VolumeMounts: []v1.VolumeMount{ + { + Name: "missing-volume", + MountPath: "/test", + }, + }, + }, + }, + }, + } + baseReconcileLoop := New(nil, nil, logf.ZapLogger(false), + &jenkins, false, false) + got := baseReconcileLoop.validateContainerVolumeMounts(jenkins.Spec.Master.Container) + assert.Equal(t, false, got) + }) +} + +func TestValidateConfigMapVolume(t *testing.T) { + namespace := "default" + t.Run("optional", func(t *testing.T) { + optional := true + volume := corev1.Volume{ + Name: "name", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + Optional: &optional, + }, + }, + } + fakeClient := fake.NewFakeClient() + baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), + nil, false, false) + + got, err := baseReconcileLoop.validateConfigMapVolume(volume) + + assert.NoError(t, err) + assert.True(t, got) + }) + t.Run("happy, required", func(t *testing.T) { + optional := false + configMap := corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: "configmap-name"}} + jenkins := &v1alpha1.Jenkins{ObjectMeta: metav1.ObjectMeta{Namespace: namespace}} + volume := corev1.Volume{ + Name: "volume-name", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + Optional: &optional, + LocalObjectReference: corev1.LocalObjectReference{ + Name: configMap.Name, + }, + }, + }, + } + fakeClient := fake.NewFakeClient() + err := fakeClient.Create(context.TODO(), &configMap) + assert.NoError(t, err) + baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), + jenkins, false, false) + + got, err := baseReconcileLoop.validateConfigMapVolume(volume) + + assert.NoError(t, err) + assert.True(t, got) + }) + t.Run("missing configmap", func(t *testing.T) { + optional := false + configMap := corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: "configmap-name"}} + jenkins := &v1alpha1.Jenkins{ObjectMeta: metav1.ObjectMeta{Namespace: namespace}} + volume := corev1.Volume{ + Name: "volume-name", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + Optional: &optional, + LocalObjectReference: corev1.LocalObjectReference{ + Name: configMap.Name, + }, + }, + }, + } + fakeClient := fake.NewFakeClient() + baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), + jenkins, false, false) + + got, err := baseReconcileLoop.validateConfigMapVolume(volume) + + assert.NoError(t, err) + assert.False(t, got) + }) +} + +func TestValidateSecretVolume(t *testing.T) { + namespace := "default" + t.Run("optional", func(t *testing.T) { + optional := true + volume := corev1.Volume{ + Name: "name", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + Optional: &optional, + }, + }, + } + fakeClient := fake.NewFakeClient() + baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), + nil, false, false) + + got, err := baseReconcileLoop.validateSecretVolume(volume) + + assert.NoError(t, err) + assert.True(t, got) + }) + t.Run("happy, required", func(t *testing.T) { + optional := false + secret := corev1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: "secret-name"}} + jenkins := &v1alpha1.Jenkins{ObjectMeta: metav1.ObjectMeta{Namespace: namespace}} + volume := corev1.Volume{ + Name: "volume-name", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + Optional: &optional, + SecretName: secret.Name, + }, + }, + } + fakeClient := fake.NewFakeClient() + err := fakeClient.Create(context.TODO(), &secret) + assert.NoError(t, err) + baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), + jenkins, false, false) + + got, err := baseReconcileLoop.validateSecretVolume(volume) + + assert.NoError(t, err) + assert.True(t, got) + }) + t.Run("missing secret", func(t *testing.T) { + optional := false + secret := corev1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: "secret-name"}} + jenkins := &v1alpha1.Jenkins{ObjectMeta: metav1.ObjectMeta{Namespace: namespace}} + volume := corev1.Volume{ + Name: "volume-name", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + Optional: &optional, + SecretName: secret.Name, + }, + }, + } + fakeClient := fake.NewFakeClient() + baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), + jenkins, false, false) + + got, err := baseReconcileLoop.validateSecretVolume(volume) + + assert.NoError(t, err) + assert.False(t, got) + }) +} diff --git a/test/e2e/configuration_test.go b/test/e2e/configuration_test.go index 1988d4cb..6e98d621 100644 --- a/test/e2e/configuration_test.go +++ b/test/e2e/configuration_test.go @@ -40,11 +40,31 @@ func TestConfiguration(t *testing.T) { RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git", }, } + volumes := []corev1.Volume{ + { + Name: "test-configmap", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: resources.GetUserConfigurationConfigMapName(jenkinsCRName), + }, + }, + }, + }, + { + Name: "test-secret", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: resources.GetUserConfigurationSecretName(jenkinsCRName), + }, + }, + }, + } // base createUserConfigurationSecret(t, jenkinsCRName, namespace, systemMessageEnvName, systemMessage) createUserConfigurationConfigMap(t, jenkinsCRName, namespace, numberOfExecutors, fmt.Sprintf("${%s}", systemMessageEnvName)) - jenkins := createJenkinsCR(t, jenkinsCRName, namespace, &[]v1alpha1.SeedJob{mySeedJob.SeedJob}) + jenkins := createJenkinsCR(t, jenkinsCRName, namespace, &[]v1alpha1.SeedJob{mySeedJob.SeedJob}, volumes) createDefaultLimitsForContainersInNamespace(t, namespace) createKubernetesCredentialsProviderSecret(t, namespace, mySeedJob) waitForJenkinsBaseConfigurationToComplete(t, jenkins) @@ -161,6 +181,20 @@ func verifyJenkinsMasterPodAttributes(t *testing.T, jenkins *v1alpha1.Jenkins) { verifyContainer(t, *expectedContainer, actualContainer) } + for _, expectedVolume := range jenkins.Spec.Master.Volumes { + volumeFound := false + for _, actualVolume := range jenkinsPod.Spec.Volumes { + if expectedVolume.Name == actualVolume.Name { + volumeFound = true + assert.Equal(t, expectedVolume, actualVolume) + } + } + + if !volumeFound { + t.Errorf("Missing volume '+%v', actaul volumes '%+v'", expectedVolume, jenkinsPod.Spec.Volumes) + } + } + t.Log("Jenkins pod attributes are valid") } diff --git a/test/e2e/jenkins.go b/test/e2e/jenkins.go index 15962069..bb170b3f 100644 --- a/test/e2e/jenkins.go +++ b/test/e2e/jenkins.go @@ -60,7 +60,7 @@ func createJenkinsAPIClient(jenkins *v1alpha1.Jenkins) (jenkinsclient.Jenkins, e ) } -func createJenkinsCR(t *testing.T, name, namespace string, seedJob *[]v1alpha1.SeedJob) *v1alpha1.Jenkins { +func createJenkinsCR(t *testing.T, name, namespace string, seedJob *[]v1alpha1.SeedJob, volumes []corev1.Volume) *v1alpha1.Jenkins { var seedJobs []v1alpha1.SeedJob if seedJob != nil { seedJobs = append(seedJobs, *seedJob...) @@ -118,6 +118,7 @@ func createJenkinsCR(t *testing.T, name, namespace string, seedJob *[]v1alpha1.S "simple-theme-plugin:0.5.1": {}, }, NodeSelector: map[string]string{"kubernetes.io/hostname": "minikube"}, + Volumes: volumes, }, SeedJobs: seedJobs, }, diff --git a/test/e2e/restart_test.go b/test/e2e/restart_test.go index 04c79a12..08963572 100644 --- a/test/e2e/restart_test.go +++ b/test/e2e/restart_test.go @@ -21,7 +21,7 @@ func TestJenkinsMasterPodRestart(t *testing.T) { // Deletes test namespace defer ctx.Cleanup() - jenkins := createJenkinsCR(t, "e2e", namespace, nil) + jenkins := createJenkinsCR(t, "e2e", namespace, nil, []corev1.Volume{}) waitForJenkinsBaseConfigurationToComplete(t, jenkins) restartJenkinsMasterPod(t, jenkins) waitForRecreateJenkinsMasterPod(t, jenkins) @@ -37,7 +37,7 @@ func TestSafeRestart(t *testing.T) { jenkinsCRName := "e2e" configureAuthorizationToUnSecure(t, jenkinsCRName, namespace) - jenkins := createJenkinsCR(t, jenkinsCRName, namespace, nil) + jenkins := createJenkinsCR(t, jenkinsCRName, namespace, nil, []corev1.Volume{}) waitForJenkinsBaseConfigurationToComplete(t, jenkins) waitForJenkinsUserConfigurationToComplete(t, jenkins) jenkinsClient := verifyJenkinsAPIConnection(t, jenkins) diff --git a/test/e2e/seedjobs_test.go b/test/e2e/seedjobs_test.go index 3fa4667f..9b38cd18 100644 --- a/test/e2e/seedjobs_test.go +++ b/test/e2e/seedjobs_test.go @@ -51,7 +51,7 @@ func TestSeedJobs(t *testing.T) { createKubernetesCredentialsProviderSecret(t, namespace, seedJobConfig) seedJobs = append(seedJobs, seedJobConfig.SeedJob) } - jenkins := createJenkinsCR(t, jenkinsCRName, namespace, &seedJobs) + jenkins := createJenkinsCR(t, jenkinsCRName, namespace, &seedJobs, []corev1.Volume{}) waitForJenkinsBaseConfigurationToComplete(t, jenkins) verifyJenkinsMasterPodAttributes(t, jenkins)