#9 Allow mount secrets and configmaps in Jenkins pod

This commit is contained in:
Tomasz Sęk 2019-05-27 23:35:09 +02:00
parent af10a97299
commit 55d6aecb93
No known key found for this signature in database
GPG Key ID: DC356D23F6A644D0
11 changed files with 654 additions and 114 deletions

View File

@ -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)

View File

@ -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"`

View File

@ -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 {

View File

@ -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)
})
}

View File

@ -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...),
},
}
}

View File

@ -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{}

View File

@ -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)
})
}

View File

@ -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")
}

View File

@ -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,
},

View File

@ -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)

View File

@ -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)