#9 Allow mount secrets and configmaps in Jenkins pod
This commit is contained in:
parent
af10a97299
commit
55d6aecb93
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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...),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue