From 6aee3f8df68e2e816ba9855ba4acd4d9a545e77e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20S=C4=99k?= Date: Sun, 30 Jun 2019 22:51:38 +0200 Subject: [PATCH 01/59] Fix run Makefile goal --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 625ef8e3..ad889fe3 100644 --- a/Makefile +++ b/Makefile @@ -208,8 +208,8 @@ run: export WATCH_NAMESPACE = $(NAMESPACE) run: export OPERATOR_NAME = $(NAME) run: build ## Run the executable, you can use EXTRA_ARGS @echo "+ $@" - kubectl apply -f deploy/crds/jenkins_$(API_VERSION)_jenkins_crd.yaml kubectl config use-context $(KUBECTL_CONTEXT) + kubectl apply -f deploy/crds/jenkins_$(API_VERSION)_jenkins_crd.yaml @echo "Watching '$(WATCH_NAMESPACE)' namespace" build/_output/bin/jenkins-operator --local $(EXTRA_ARGS) From 1930c04b726d3ed25f0c912fc509fa10d8fadfe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20S=C4=99k?= Date: Sun, 30 Jun 2019 22:54:46 +0200 Subject: [PATCH 02/59] #28 Update Jenkins API --- pkg/apis/jenkins/v1alpha2/jenkins_types.go | 55 ++++++++++++++++++++-- pkg/apis/jenkins/v1alpha2/register.go | 1 + 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/pkg/apis/jenkins/v1alpha2/jenkins_types.go b/pkg/apis/jenkins/v1alpha2/jenkins_types.go index 7d924305..abc35481 100644 --- a/pkg/apis/jenkins/v1alpha2/jenkins_types.go +++ b/pkg/apis/jenkins/v1alpha2/jenkins_types.go @@ -40,6 +40,14 @@ type JenkinsSpec struct { // More info: https://github.com/jenkinsci/kubernetes-operator/blob/master/docs/getting-started.md#configure-backup-and-restore // +optional Restore Restore `json:"restore,omitempty"` + + // GroovyScripts defines configuration of Jenkins customization via groovy scripts + // +optional + GroovyScripts GroovyScripts `json:"groovyScripts,omitempty"` + + // ConfigurationAsCode defines configuration of Jenkins customization via Configuration as Code Jenkins plugin + // +optional + ConfigurationAsCode ConfigurationAsCode `json:"configurationAsCode,omitempty"` } // Container defines Kubernetes container attributes @@ -313,7 +321,7 @@ type JenkinsStatus struct { // +optional UserConfigurationCompletedTime *metav1.Time `json:"userConfigurationCompletedTime,omitempty"` - // Builds contains Jenkins builds statues + // Builds contains Jenkins job builds statues // +optional Builds []Build `json:"builds,omitempty"` @@ -340,6 +348,10 @@ type JenkinsStatus struct { // CreatedSeedJobs contains list of seed job id already created in Jenkins // +optional CreatedSeedJobs []string `json:"createdSeedJobs,omitempty"` + + // AppliedGroovyScripts is a list with all applied groovy scripts in Jenkins by the operator + // +optional + AppliedGroovyScripts []AppliedGroovyScript `json:"appliedGroovyScripts,omitempty"` } // BuildStatus defines type of Jenkins build job status @@ -362,9 +374,8 @@ const ( BuildExpiredStatus BuildStatus = "expired" ) -// Build defines Jenkins Build status with corresponding metadata +// Build defines Jenkins job build status with corresponding metadata type Build struct { - // JobName is the Jenkins job name JobName string `json:"jobName,omitempty"` @@ -493,3 +504,41 @@ type Restore struct { // +optional RecoveryOnce uint64 `json:"recoveryOnce,omitempty"` } + +// AppliedGroovyScript is the applied groovy script in Jenkins by the operator +type AppliedGroovyScript struct { + // ConfigurationType is the name of the configuration type(base-groovy, user-groovy, user-casc) + ConfigurationType string `json:"configurationType"` + // Source is the name of source where is located groovy script + Source string `json:"source"` + // Name is the name of the groovy script + Name string `json:"name"` + // Hash is the hash of the groovy script and secrets which it uses + Hash string +} + +// SecretRef is reference to Kubernetes secret +type SecretRef struct { + Name string `json:"name"` +} + +// ConfigMapRef is reference to Kubernetes ConfigMap +type ConfigMapRef struct { + Name string `json:"name"` +} + +// Customization defines configuration of Jenkins customization +type Customization struct { + Secret SecretRef `json:"secret"` + Configurations []ConfigMapRef `json:"configurations"` +} + +// GroovyScripts defines configuration of Jenkins customization via groovy scripts +type GroovyScripts struct { + Customization +} + +// ConfigurationAsCode defines configuration of Jenkins customization via Configuration as Code Jenkins plugin +type ConfigurationAsCode struct { + Customization +} diff --git a/pkg/apis/jenkins/v1alpha2/register.go b/pkg/apis/jenkins/v1alpha2/register.go index 852b4c68..f002bd7a 100644 --- a/pkg/apis/jenkins/v1alpha2/register.go +++ b/pkg/apis/jenkins/v1alpha2/register.go @@ -37,6 +37,7 @@ func (in *Jenkins) GroupVersionKind() schema.GroupVersionKind { } } +// JenkinsTypeMeta returns Jenkins type meta func JenkinsTypeMeta() metav1.TypeMeta { return metav1.TypeMeta{ Kind: Kind, From 4e302c66a1964efbaad0bdf9b47bfa6727dd9eb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20S=C4=99k?= Date: Sun, 30 Jun 2019 22:57:54 +0200 Subject: [PATCH 03/59] #28 Return GroovyScriptExecutionFailed error --- pkg/controller/jenkins/client/script.go | 9 ++++++++- pkg/controller/jenkins/client/script_test.go | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/controller/jenkins/client/script.go b/pkg/controller/jenkins/client/script.go index 6c66b8c9..61077133 100644 --- a/pkg/controller/jenkins/client/script.go +++ b/pkg/controller/jenkins/client/script.go @@ -10,6 +10,13 @@ import ( "github.com/pkg/errors" ) +// GroovyScriptExecutionFailed is custom error type which indicates passed groovy script is invalid +type GroovyScriptExecutionFailed struct{} + +func (e GroovyScriptExecutionFailed) Error() string { + return "script execution failed" +} + func (jenkins *jenkins) ExecuteScript(script string) (string, error) { now := time.Now().Unix() verifier := fmt.Sprintf("verifier-%d", now) @@ -38,7 +45,7 @@ func (jenkins *jenkins) executeScript(script string, verifier string) (string, e } if !strings.Contains(output, verifier) { - return output, errors.Errorf("script execution failed, logs '%s'", output) + return output, &GroovyScriptExecutionFailed{} } return output, nil diff --git a/pkg/controller/jenkins/client/script_test.go b/pkg/controller/jenkins/client/script_test.go index 202c084f..1a8092c5 100644 --- a/pkg/controller/jenkins/client/script_test.go +++ b/pkg/controller/jenkins/client/script_test.go @@ -62,7 +62,7 @@ func Test_ExecuteScript(t *testing.T) { script := "some groovy code" logs, err := jenkinsClient.executeScript(script, verifier) - assert.EqualError(t, err, "script execution failed, logs 'some exception stack trace without verifier'", logs) + assert.EqualError(t, err, "script execution failed", logs) assert.Equal(t, response, logs) }) t.Run("throw 500", func(t *testing.T) { From 66236d5459c79abda918d1ce71e8483f8ec91605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20S=C4=99k?= Date: Sun, 30 Jun 2019 22:59:30 +0200 Subject: [PATCH 04/59] #28 Add validation --- .../jenkins/configuration/base/validate.go | 55 +++++++- .../configuration/base/validate_test.go | 120 ++++++++++++++++++ 2 files changed, 174 insertions(+), 1 deletion(-) diff --git a/pkg/controller/jenkins/configuration/base/validate.go b/pkg/controller/jenkins/configuration/base/validate.go index 906b2f3a..f3de634a 100644 --- a/pkg/controller/jenkins/configuration/base/validate.go +++ b/pkg/controller/jenkins/configuration/base/validate.go @@ -46,6 +46,17 @@ func (r *ReconcileJenkinsBaseConfiguration) Validate(jenkins *v1alpha2.Jenkins) return false, nil } + if valid, err := r.validateCustomization(r.jenkins.Spec.GroovyScripts.Customization, "spec.groovyScripts"); err != nil { + return false, err + } else if !valid { + return false, nil + } + if valid, err := r.validateCustomization(r.jenkins.Spec.ConfigurationAsCode.Customization, "spec.configurationAsCode"); err != nil { + return false, err + } else if !valid { + return false, nil + } + return true, nil } @@ -194,7 +205,7 @@ func (r *ReconcileJenkinsBaseConfiguration) validateContainerVolumeMounts(contai } func (r *ReconcileJenkinsBaseConfiguration) validateJenkinsMasterPodEnvs() bool { - baseEnvs := resources.GetJenkinsMasterContainerBaseEnvs() + baseEnvs := resources.GetJenkinsMasterContainerBaseEnvs(r.jenkins) baseEnvNames := map[string]string{} for _, env := range baseEnvs { baseEnvNames[env.Name] = env.Value @@ -269,3 +280,45 @@ func (r *ReconcileJenkinsBaseConfiguration) verifyBasePlugins(requiredBasePlugin return valid } + +func (r *ReconcileJenkinsBaseConfiguration) validateCustomization(customization v1alpha2.Customization, name string) (bool, error) { + valid := true + if len(customization.Secret.Name) == 0 && len(customization.Configurations) == 0 { + return true, nil + } + if len(customization.Secret.Name) > 0 && len(customization.Configurations) == 0 { + valid = false + r.logger.V(log.VWarn).Info(fmt.Sprintf("%s.secret.name is set but %s.configurations is empty", name, name)) + } + + if len(customization.Secret.Name) > 0 { + secret := &corev1.Secret{} + err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: customization.Secret.Name, Namespace: r.jenkins.ObjectMeta.Namespace}, secret) + if err != nil && apierrors.IsNotFound(err) { + valid = false + r.logger.V(log.VWarn).Info(fmt.Sprintf("Secret '%s' configured in %s.secret.name not found", customization.Secret.Name, name)) + } else if err != nil && !apierrors.IsNotFound(err) { + return false, stackerr.WithStack(err) + } + } + + for index, configMapRef := range customization.Configurations { + if len(configMapRef.Name) == 0 { + r.logger.V(log.VWarn).Info(fmt.Sprintf("%s.configurations[%d] name is empty", name, index)) + valid = false + continue + } + + configMap := &corev1.ConfigMap{} + err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: configMapRef.Name, Namespace: r.jenkins.ObjectMeta.Namespace}, configMap) + if err != nil && apierrors.IsNotFound(err) { + valid = false + r.logger.V(log.VWarn).Info(fmt.Sprintf("ConfigMap '%s' configured in %s.configurations[%d] not found", configMapRef.Name, name, index)) + return false, nil + } else if err != nil && !apierrors.IsNotFound(err) { + return false, stackerr.WithStack(err) + } + } + + return valid, nil +} diff --git a/pkg/controller/jenkins/configuration/base/validate_test.go b/pkg/controller/jenkins/configuration/base/validate_test.go index 9c46380b..07b7c208 100644 --- a/pkg/controller/jenkins/configuration/base/validate_test.go +++ b/pkg/controller/jenkins/configuration/base/validate_test.go @@ -10,6 +10,7 @@ import ( "github.com/jenkinsci/kubernetes-operator/pkg/log" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -446,3 +447,122 @@ func TestValidateSecretVolume(t *testing.T) { assert.False(t, got) }) } + +func TestValidateCustomization(t *testing.T) { + namespace := "default" + secretName := "secretName" + configMapName := "configmap-name" + jenkins := &v1alpha2.Jenkins{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + }, + } + t.Run("empty", func(t *testing.T) { + customization := v1alpha2.Customization{} + fakeClient := fake.NewFakeClient() + baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), + jenkins, false, false, nil, nil) + + got, err := baseReconcileLoop.validateCustomization(customization, "spec.groovyScripts") + + assert.NoError(t, err) + assert.True(t, got) + }) + t.Run("secret set but configurations is empty", func(t *testing.T) { + customization := v1alpha2.Customization{ + Secret: v1alpha2.SecretRef{Name: secretName}, + Configurations: []v1alpha2.ConfigMapRef{}, + } + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + }, + } + fakeClient := fake.NewFakeClient() + baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), + jenkins, false, false, nil, nil) + err := fakeClient.Create(context.TODO(), secret) + require.NoError(t, err) + + got, err := baseReconcileLoop.validateCustomization(customization, "spec.groovyScripts") + + assert.NoError(t, err) + assert.False(t, got) + }) + t.Run("secret and configmap exists", func(t *testing.T) { + customization := v1alpha2.Customization{ + Secret: v1alpha2.SecretRef{Name: secretName}, + Configurations: []v1alpha2.ConfigMapRef{{Name: configMapName}}, + } + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: configMapName, + Namespace: namespace, + }, + } + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + }, + } + fakeClient := fake.NewFakeClient() + baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), + jenkins, false, false, nil, nil) + err := fakeClient.Create(context.TODO(), secret) + require.NoError(t, err) + err = fakeClient.Create(context.TODO(), configMap) + require.NoError(t, err) + + got, err := baseReconcileLoop.validateCustomization(customization, "spec.groovyScripts") + + assert.NoError(t, err) + assert.True(t, got) + }) + t.Run("secret not exists and configmap exists", func(t *testing.T) { + configMapName := "configmap-name" + customization := v1alpha2.Customization{ + Secret: v1alpha2.SecretRef{Name: secretName}, + Configurations: []v1alpha2.ConfigMapRef{{Name: configMapName}}, + } + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: configMapName, + Namespace: namespace, + }, + } + fakeClient := fake.NewFakeClient() + baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), + jenkins, false, false, nil, nil) + err := fakeClient.Create(context.TODO(), configMap) + require.NoError(t, err) + + got, err := baseReconcileLoop.validateCustomization(customization, "spec.groovyScripts") + + assert.NoError(t, err) + assert.False(t, got) + }) + t.Run("secret exists and configmap not exists", func(t *testing.T) { + customization := v1alpha2.Customization{ + Secret: v1alpha2.SecretRef{Name: secretName}, + Configurations: []v1alpha2.ConfigMapRef{{Name: configMapName}}, + } + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + }, + } + fakeClient := fake.NewFakeClient() + baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), + jenkins, false, false, nil, nil) + err := fakeClient.Create(context.TODO(), secret) + require.NoError(t, err) + + got, err := baseReconcileLoop.validateCustomization(customization, "spec.groovyScripts") + + assert.NoError(t, err) + assert.False(t, got) + }) +} From d0b02de4292ecc0130c8c761275c4ff7735a5d6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20S=C4=99k?= Date: Sun, 30 Jun 2019 23:11:45 +0200 Subject: [PATCH 05/59] #28 Mount secrets for groovy and CasC customization --- .../jenkins/configuration/base/reconcile.go | 73 ++++++---- .../configuration/base/resources/pod.go | 135 +++++++++--------- .../configuration/base/resources/service.go | 8 -- .../resources/user_configuration_configmap.go | 58 -------- .../resources/user_configuration_secret.go | 33 ----- 5 files changed, 112 insertions(+), 195 deletions(-) delete mode 100644 pkg/controller/jenkins/configuration/base/resources/user_configuration_configmap.go delete mode 100644 pkg/controller/jenkins/configuration/base/resources/user_configuration_secret.go diff --git a/pkg/controller/jenkins/configuration/base/reconcile.go b/pkg/controller/jenkins/configuration/base/reconcile.go index 5e0ec682..713ebada 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile.go +++ b/pkg/controller/jenkins/configuration/base/reconcile.go @@ -13,7 +13,6 @@ import ( jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/backuprestore" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources" - "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/groovy" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/plugins" "github.com/jenkinsci/kubernetes-operator/pkg/log" @@ -149,15 +148,15 @@ func (r *ReconcileJenkinsBaseConfiguration) ensureResourcesRequiredForJenkinsPod } r.logger.V(log.VDebug).Info("Base configuration config map is present") - if err := r.createUserConfigurationConfigMap(metaObject); err != nil { + if err := r.addLabelForWatchesResources(r.jenkins.Spec.GroovyScripts.Customization); err != nil { return err } - r.logger.V(log.VDebug).Info("User configuration config map is present") + r.logger.V(log.VDebug).Info("GroovyScripts Secret and ConfigMap added watched labels") - if err := r.createUserConfigurationSecret(metaObject); err != nil { + if err := r.addLabelForWatchesResources(r.jenkins.Spec.ConfigurationAsCode.Customization); err != nil { return err } - r.logger.V(log.VDebug).Info("User configuration secret is present") + r.logger.V(log.VDebug).Info("ConfigurationAsCode Secret and ConfigMap added watched labels") if err := r.createRBAC(metaObject); err != nil { return err @@ -289,33 +288,49 @@ func (r *ReconcileJenkinsBaseConfiguration) createBaseConfigurationConfigMap(met return stackerr.WithStack(r.createOrUpdateResource(configMap)) } -func (r *ReconcileJenkinsBaseConfiguration) createUserConfigurationConfigMap(meta metav1.ObjectMeta) error { - currentConfigMap := &corev1.ConfigMap{} - err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: resources.GetUserConfigurationConfigMapNameFromJenkins(r.jenkins), Namespace: r.jenkins.Namespace}, currentConfigMap) - if err != nil && errors.IsNotFound(err) { - return stackerr.WithStack(r.k8sClient.Create(context.TODO(), resources.NewUserConfigurationConfigMap(r.jenkins))) - } else if err != nil { - return stackerr.WithStack(err) - } - if !resources.VerifyIfLabelsAreSet(currentConfigMap, resources.BuildLabelsForWatchedResources(*r.jenkins)) { - currentConfigMap.ObjectMeta.Labels = resources.BuildLabelsForWatchedResources(*r.jenkins) - return stackerr.WithStack(r.k8sClient.Update(context.TODO(), currentConfigMap)) +func (r *ReconcileJenkinsBaseConfiguration) addLabelForWatchesResources(customization v1alpha2.Customization) error { + labelsForWatchedResources := resources.BuildLabelsForWatchedResources(*r.jenkins) + + if len(customization.Secret.Name) > 0 { + secret := &corev1.Secret{} + err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: customization.Secret.Name, Namespace: r.jenkins.Namespace}, secret) + if err != nil { + return stackerr.WithStack(err) + } + + if !resources.VerifyIfLabelsAreSet(secret, labelsForWatchedResources) { + if len(secret.ObjectMeta.Labels) == 0 { + secret.ObjectMeta.Labels = map[string]string{} + } + for key, value := range labelsForWatchedResources { + secret.ObjectMeta.Labels[key] = value + } + + if err = r.k8sClient.Update(context.TODO(), secret); err != nil { + return stackerr.WithStack(r.k8sClient.Update(context.TODO(), secret)) + } + } } - return nil -} + for _, configMapRef := range customization.Configurations { + configMap := &corev1.ConfigMap{} + err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: configMapRef.Name, Namespace: r.jenkins.Namespace}, configMap) + if err != nil { + return stackerr.WithStack(err) + } -func (r *ReconcileJenkinsBaseConfiguration) createUserConfigurationSecret(meta metav1.ObjectMeta) error { - currentSecret := &corev1.Secret{} - err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: resources.GetUserConfigurationSecretNameFromJenkins(r.jenkins), Namespace: r.jenkins.Namespace}, currentSecret) - if err != nil && errors.IsNotFound(err) { - return stackerr.WithStack(r.k8sClient.Create(context.TODO(), resources.NewUserConfigurationSecret(r.jenkins))) - } else if err != nil { - return stackerr.WithStack(err) - } - if !resources.VerifyIfLabelsAreSet(currentSecret, resources.BuildLabelsForWatchedResources(*r.jenkins)) { - currentSecret.ObjectMeta.Labels = resources.BuildLabelsForWatchedResources(*r.jenkins) - return stackerr.WithStack(r.k8sClient.Update(context.TODO(), currentSecret)) + if !resources.VerifyIfLabelsAreSet(configMap, labelsForWatchedResources) { + if len(configMap.ObjectMeta.Labels) == 0 { + configMap.ObjectMeta.Labels = map[string]string{} + } + for key, value := range labelsForWatchedResources { + configMap.ObjectMeta.Labels[key] = value + } + + if err = r.k8sClient.Update(context.TODO(), configMap); err != nil { + return stackerr.WithStack(r.k8sClient.Update(context.TODO(), configMap)) + } + } } return nil diff --git a/pkg/controller/jenkins/configuration/base/resources/pod.go b/pkg/controller/jenkins/configuration/base/resources/pod.go index 6c9b35d3..1a40a4b9 100644 --- a/pkg/controller/jenkins/configuration/base/resources/pod.go +++ b/pkg/controller/jenkins/configuration/base/resources/pod.go @@ -30,19 +30,12 @@ const ( jenkinsInitConfigurationVolumeName = "init-configuration" jenkinsInitConfigurationVolumePath = jenkinsPath + "/init-configuration" - jenkinsBaseConfigurationVolumeName = "base-configuration" - // JenkinsBaseConfigurationVolumePath is a path where are groovy scripts used to configure Jenkins - // this scripts are provided by jenkins-operator - JenkinsBaseConfigurationVolumePath = jenkinsPath + "/base-configuration" - - jenkinsUserConfigurationVolumeName = "user-configuration" - // JenkinsUserConfigurationVolumePath is a path where are groovy scripts and CasC configs used to configure Jenkins - // this script is provided by user - JenkinsUserConfigurationVolumePath = jenkinsPath + "/user-configuration" - - userConfigurationSecretVolumeName = "user-configuration-secrets" - // UserConfigurationSecretVolumePath is a path where are secrets used for groovy scripts and CasC configs - UserConfigurationSecretVolumePath = jenkinsPath + "/user-configuration-secrets" + // GroovyScriptsSecretVolumePath is a path where are groovy scripts used to configure Jenkins + // This script is provided by user + GroovyScriptsSecretVolumePath = jenkinsPath + "/groovy-scripts-secrets" + // ConfigurationAsCodeSecretVolumePath is a path where are CasC configs used to configure Jenkins + // This script is provided by user + ConfigurationAsCodeSecretVolumePath = jenkinsPath + "/configuration-as-code-secrets" httpPortName = "http" slavePortName = "slavelistener" @@ -68,8 +61,8 @@ func GetJenkinsMasterContainerBaseCommand() []string { } // GetJenkinsMasterContainerBaseEnvs returns Jenkins master pod envs required by operator -func GetJenkinsMasterContainerBaseEnvs() []corev1.EnvVar { - return []corev1.EnvVar{ +func GetJenkinsMasterContainerBaseEnvs(jenkins *v1alpha2.Jenkins) []corev1.EnvVar { + envVars := []corev1.EnvVar{ { Name: "JENKINS_HOME", Value: jenkinsHomePath, @@ -78,11 +71,16 @@ func GetJenkinsMasterContainerBaseEnvs() []corev1.EnvVar { Name: "JAVA_OPTS", Value: "-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=1 -Djenkins.install.runSetupWizard=false -Djava.awt.headless=true", }, - { - Name: "SECRETS", // https://github.com/jenkinsci/configuration-as-code-plugin/blob/master/demos/kubernetes-secrets/README.md - Value: UserConfigurationSecretVolumePath, - }, } + + if len(jenkins.Spec.ConfigurationAsCode.Secret.Name) > 0 { + envVars = append(envVars, corev1.EnvVar{ + Name: "SECRETS", // https://github.com/jenkinsci/configuration-as-code-plugin/blob/master/demos/kubernetes-secrets/README.md + Value: ConfigurationAsCodeSecretVolumePath, + }) + } + + return envVars } // GetJenkinsMasterPodBaseVolumes returns Jenkins master pod volumes required by operator @@ -90,7 +88,7 @@ func GetJenkinsMasterPodBaseVolumes(jenkins *v1alpha2.Jenkins) []corev1.Volume { configMapVolumeSourceDefaultMode := corev1.ConfigMapVolumeSourceDefaultMode secretVolumeSourceDefaultMode := corev1.SecretVolumeSourceDefaultMode var scriptsVolumeDefaultMode int32 = 0777 - return []corev1.Volume{ + volumes := []corev1.Volume{ { Name: JenkinsHomeVolumeName, VolumeSource: corev1.VolumeSource{ @@ -119,28 +117,6 @@ func GetJenkinsMasterPodBaseVolumes(jenkins *v1alpha2.Jenkins) []corev1.Volume { }, }, }, - { - 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{ @@ -150,21 +126,45 @@ func GetJenkinsMasterPodBaseVolumes(jenkins *v1alpha2.Jenkins) []corev1.Volume { }, }, }, - { - Name: userConfigurationSecretVolumeName, + } + + if len(jenkins.Spec.GroovyScripts.Secret.Name) > 0 { + volumes = append(volumes, corev1.Volume{ + Name: getGroovyScriptsSecretVolumeName(jenkins), VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ DefaultMode: &secretVolumeSourceDefaultMode, - SecretName: GetUserConfigurationSecretNameFromJenkins(jenkins), + SecretName: jenkins.Spec.GroovyScripts.Secret.Name, }, }, - }, + }) } + if len(jenkins.Spec.ConfigurationAsCode.Secret.Name) > 0 { + volumes = append(volumes, corev1.Volume{ + Name: getConfigurationAsCodeSecretVolumeName(jenkins), + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + DefaultMode: &secretVolumeSourceDefaultMode, + SecretName: jenkins.Spec.ConfigurationAsCode.Secret.Name, + }, + }, + }) + } + + return volumes +} + +func getGroovyScriptsSecretVolumeName(jenkins *v1alpha2.Jenkins) string { + return "gs-" + jenkins.Spec.GroovyScripts.Secret.Name +} + +func getConfigurationAsCodeSecretVolumeName(jenkins *v1alpha2.Jenkins) string { + return "casc-" + jenkins.Spec.GroovyScripts.Secret.Name } // GetJenkinsMasterContainerBaseVolumeMounts returns Jenkins master pod volume mounts required by operator -func GetJenkinsMasterContainerBaseVolumeMounts() []corev1.VolumeMount { - return []corev1.VolumeMount{ +func GetJenkinsMasterContainerBaseVolumeMounts(jenkins *v1alpha2.Jenkins) []corev1.VolumeMount { + volumeMounts := []corev1.VolumeMount{ { Name: JenkinsHomeVolumeName, MountPath: jenkinsHomePath, @@ -180,33 +180,35 @@ func GetJenkinsMasterContainerBaseVolumeMounts() []corev1.VolumeMount { 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, - }, } + + if len(jenkins.Spec.GroovyScripts.Secret.Name) > 0 { + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: getGroovyScriptsSecretVolumeName(jenkins), + MountPath: GroovyScriptsSecretVolumePath, + ReadOnly: true, + }) + } + if len(jenkins.Spec.ConfigurationAsCode.Secret.Name) > 0 { + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: getConfigurationAsCodeSecretVolumeName(jenkins), + MountPath: ConfigurationAsCodeSecretVolumePath, + ReadOnly: true, + }) + } + + return volumeMounts } // NewJenkinsMasterContainer returns Jenkins master Kubernetes container func NewJenkinsMasterContainer(jenkins *v1alpha2.Jenkins) corev1.Container { jenkinsContainer := jenkins.Spec.Master.Containers[0] - envs := GetJenkinsMasterContainerBaseEnvs() + envs := GetJenkinsMasterContainerBaseEnvs(jenkins) envs = append(envs, jenkinsContainer.Env...) return corev1.Container{ @@ -230,7 +232,7 @@ func NewJenkinsMasterContainer(jenkins *v1alpha2.Jenkins) corev1.Container { }, Env: envs, Resources: jenkinsContainer.Resources, - VolumeMounts: append(GetJenkinsMasterContainerBaseVolumeMounts(), jenkinsContainer.VolumeMounts...), + VolumeMounts: append(GetJenkinsMasterContainerBaseVolumeMounts(jenkins), jenkinsContainer.VolumeMounts...), } } @@ -272,7 +274,6 @@ func GetJenkinsMasterPodName(jenkins v1alpha2.Jenkins) string { // NewJenkinsMasterPod builds Jenkins Master Kubernetes Pod resource func NewJenkinsMasterPod(objectMeta metav1.ObjectMeta, jenkins *v1alpha2.Jenkins) *corev1.Pod { - serviceAccountName := objectMeta.Name objectMeta.Annotations = jenkins.Spec.Master.Annotations objectMeta.Name = GetJenkinsMasterPodName(*jenkins) @@ -283,10 +284,10 @@ func NewJenkinsMasterPod(objectMeta metav1.ObjectMeta, jenkins *v1alpha2.Jenkins Spec: corev1.PodSpec{ ServiceAccountName: serviceAccountName, RestartPolicy: corev1.RestartPolicyNever, - SecurityContext: jenkins.Spec.Master.SecurityContext, NodeSelector: jenkins.Spec.Master.NodeSelector, Containers: newContainers(jenkins), Volumes: append(GetJenkinsMasterPodBaseVolumes(jenkins), jenkins.Spec.Master.Volumes...), + SecurityContext: jenkins.Spec.Master.SecurityContext, }, } } diff --git a/pkg/controller/jenkins/configuration/base/resources/service.go b/pkg/controller/jenkins/configuration/base/resources/service.go index db3b1bba..b9e2d96c 100644 --- a/pkg/controller/jenkins/configuration/base/resources/service.go +++ b/pkg/controller/jenkins/configuration/base/resources/service.go @@ -7,16 +7,8 @@ import ( "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants" corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func buildServiceTypeMeta() metav1.TypeMeta { - return metav1.TypeMeta{ - Kind: "Service", - APIVersion: "v1", - } -} - // UpdateService returns new service with override fields from config func UpdateService(actual corev1.Service, config v1alpha2.Service) corev1.Service { actual.ObjectMeta.Annotations = config.Annotations diff --git a/pkg/controller/jenkins/configuration/base/resources/user_configuration_configmap.go b/pkg/controller/jenkins/configuration/base/resources/user_configuration_configmap.go deleted file mode 100644 index 85ad304a..00000000 --- a/pkg/controller/jenkins/configuration/base/resources/user_configuration_configmap.go +++ /dev/null @@ -1,58 +0,0 @@ -package resources - -import ( - "fmt" - - "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" - "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants" - - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -const configureTheme = ` -import jenkins.* -import jenkins.model.* -import hudson.* -import hudson.model.* -import org.jenkinsci.plugins.simpletheme.ThemeElement -import org.jenkinsci.plugins.simpletheme.CssTextThemeElement -import org.jenkinsci.plugins.simpletheme.CssUrlThemeElement - -Jenkins jenkins = Jenkins.getInstance() - -def decorator = Jenkins.instance.getDescriptorByType(org.codefirst.SimpleThemeDecorator.class) - -List configElements = new ArrayList<>(); -configElements.add(new CssTextThemeElement("DEFAULT")); -configElements.add(new CssUrlThemeElement("https://cdn.rawgit.com/afonsof/jenkins-material-theme/gh-pages/dist/material-light-green.css")); -decorator.setElements(configElements); -decorator.save(); - -jenkins.save() -` - -// GetUserConfigurationConfigMapNameFromJenkins returns name of Kubernetes config map used to user configuration -func GetUserConfigurationConfigMapNameFromJenkins(jenkins *v1alpha2.Jenkins) string { - return fmt.Sprintf("%s-user-configuration-%s", constants.OperatorName, jenkins.ObjectMeta.Name) -} - -// GetUserConfigurationConfigMapName returns name of Kubernetes config map used to user configuration -func GetUserConfigurationConfigMapName(jenkinsCRName string) string { - return fmt.Sprintf("%s-user-configuration-%s", constants.OperatorName, jenkinsCRName) -} - -// NewUserConfigurationConfigMap builds Kubernetes config map used to user configuration -func NewUserConfigurationConfigMap(jenkins *v1alpha2.Jenkins) *corev1.ConfigMap { - return &corev1.ConfigMap{ - TypeMeta: buildConfigMapTypeMeta(), - ObjectMeta: metav1.ObjectMeta{ - Name: GetUserConfigurationConfigMapNameFromJenkins(jenkins), - Namespace: jenkins.ObjectMeta.Namespace, - Labels: BuildLabelsForWatchedResources(*jenkins), - }, - Data: map[string]string{ - "1-configure-theme.groovy": configureTheme, - }, - } -} diff --git a/pkg/controller/jenkins/configuration/base/resources/user_configuration_secret.go b/pkg/controller/jenkins/configuration/base/resources/user_configuration_secret.go deleted file mode 100644 index 073c0efd..00000000 --- a/pkg/controller/jenkins/configuration/base/resources/user_configuration_secret.go +++ /dev/null @@ -1,33 +0,0 @@ -package resources - -import ( - "fmt" - - "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" - "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants" - - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// GetUserConfigurationSecretNameFromJenkins returns name of Kubernetes secret used to store jenkins operator credentials -func GetUserConfigurationSecretNameFromJenkins(jenkins *v1alpha2.Jenkins) string { - return fmt.Sprintf("%s-user-configuration-%s", constants.OperatorName, jenkins.Name) -} - -// GetUserConfigurationSecretName returns name of Kubernetes secret used to store jenkins operator credentials -func GetUserConfigurationSecretName(jenkinsCRName string) string { - return fmt.Sprintf("%s-user-configuration-%s", constants.OperatorName, jenkinsCRName) -} - -// NewUserConfigurationSecret builds the Kubernetes secret resource which is used to store user sensitive data for Jenkins configuration -func NewUserConfigurationSecret(jenkins *v1alpha2.Jenkins) *corev1.Secret { - return &corev1.Secret{ - TypeMeta: buildServiceTypeMeta(), - ObjectMeta: metav1.ObjectMeta{ - Name: GetUserConfigurationSecretNameFromJenkins(jenkins), - Namespace: jenkins.ObjectMeta.Namespace, - Labels: BuildLabelsForWatchedResources(*jenkins), - }, - } -} From 54454d9a02f0005eaed4e0777fd807793a491103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20S=C4=99k?= Date: Sun, 30 Jun 2019 23:14:52 +0200 Subject: [PATCH 06/59] #28 Mount secrets for groovy and CasC customization - fix e2e tests --- test/e2e/configuration_test.go | 16 ++++++++-------- test/e2e/jenkins.go | 5 +++++ test/e2e/restart_test.go | 7 +++---- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/test/e2e/configuration_test.go b/test/e2e/configuration_test.go index 089df965..e80384eb 100644 --- a/test/e2e/configuration_test.go +++ b/test/e2e/configuration_test.go @@ -46,7 +46,7 @@ func TestConfiguration(t *testing.T) { VolumeSource: corev1.VolumeSource{ ConfigMap: &corev1.ConfigMapVolumeSource{ LocalObjectReference: corev1.LocalObjectReference{ - Name: resources.GetUserConfigurationConfigMapName(jenkinsCRName), + Name: userConfigurationConfigMapName, }, }, }, @@ -55,15 +55,15 @@ func TestConfiguration(t *testing.T) { Name: "test-secret", VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ - SecretName: resources.GetUserConfigurationSecretName(jenkinsCRName), + SecretName: userConfigurationSecretName, }, }, }, } // base - createUserConfigurationSecret(t, jenkinsCRName, namespace, systemMessageEnvName, systemMessage) - createUserConfigurationConfigMap(t, jenkinsCRName, namespace, numberOfExecutors, fmt.Sprintf("${%s}", systemMessageEnvName)) + createUserConfigurationSecret(t, namespace, systemMessageEnvName, systemMessage) + createUserConfigurationConfigMap(t, namespace, numberOfExecutors, fmt.Sprintf("${%s}", systemMessageEnvName)) jenkins := createJenkinsCR(t, jenkinsCRName, namespace, &[]v1alpha2.SeedJob{mySeedJob.SeedJob}, volumes) createDefaultLimitsForContainersInNamespace(t, namespace) createKubernetesCredentialsProviderSecret(t, namespace, mySeedJob) @@ -79,10 +79,10 @@ func TestConfiguration(t *testing.T) { verifyJenkinsSeedJobs(t, client, []seedJobConfig{mySeedJob}) } -func createUserConfigurationSecret(t *testing.T, jenkinsCRName string, namespace string, systemMessageEnvName, systemMessage string) { +func createUserConfigurationSecret(t *testing.T, namespace string, systemMessageEnvName, systemMessage string) { userConfiguration := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: resources.GetUserConfigurationSecretName(jenkinsCRName), + Name: userConfigurationSecretName, Namespace: namespace, }, StringData: map[string]string{ @@ -96,10 +96,10 @@ func createUserConfigurationSecret(t *testing.T, jenkinsCRName string, namespace } } -func createUserConfigurationConfigMap(t *testing.T, jenkinsCRName string, namespace string, numberOfExecutors int, systemMessage string) { +func createUserConfigurationConfigMap(t *testing.T, namespace string, numberOfExecutors int, systemMessage string) { userConfiguration := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Name: resources.GetUserConfigurationConfigMapName(jenkinsCRName), + Name: userConfigurationConfigMapName, Namespace: namespace, }, Data: map[string]string{ diff --git a/test/e2e/jenkins.go b/test/e2e/jenkins.go index 7a1fd010..580b1cb9 100644 --- a/test/e2e/jenkins.go +++ b/test/e2e/jenkins.go @@ -17,6 +17,11 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" ) +const ( + userConfigurationConfigMapName = "user-config" + userConfigurationSecretName = "user-secret" +) + func getJenkins(t *testing.T, namespace, name string) *v1alpha2.Jenkins { jenkins := &v1alpha2.Jenkins{} namespaceName := types.NamespacedName{Namespace: namespace, Name: name} diff --git a/test/e2e/restart_test.go b/test/e2e/restart_test.go index ca4f07a9..0dce822d 100644 --- a/test/e2e/restart_test.go +++ b/test/e2e/restart_test.go @@ -6,7 +6,6 @@ import ( "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client" - "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources" framework "github.com/operator-framework/operator-sdk/pkg/test" "github.com/stretchr/testify/require" @@ -36,7 +35,7 @@ func TestSafeRestart(t *testing.T) { defer ctx.Cleanup() jenkinsCRName := "e2e" - configureAuthorizationToUnSecure(t, jenkinsCRName, namespace) + configureAuthorizationToUnSecure(t, namespace) jenkins := createJenkinsCR(t, jenkinsCRName, namespace, nil, []corev1.Volume{}) waitForJenkinsBaseConfigurationToComplete(t, jenkins) waitForJenkinsUserConfigurationToComplete(t, jenkins) @@ -50,10 +49,10 @@ func TestSafeRestart(t *testing.T) { checkIfAuthorizationStrategyUnsecuredIsSet(t, jenkinsClient) } -func configureAuthorizationToUnSecure(t *testing.T, jenkinsCRName, namespace string) { +func configureAuthorizationToUnSecure(t *testing.T, namespace string) { limitRange := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Name: resources.GetUserConfigurationConfigMapName(jenkinsCRName), + Name: userConfigurationConfigMapName, Namespace: namespace, }, Data: map[string]string{ From 87fcc5f8a5a17fa22058bf6e7a9d0cdba93c8677 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20S=C4=99k?= Date: Sun, 30 Jun 2019 23:16:03 +0200 Subject: [PATCH 07/59] #28 Change groovy client implementation --- pkg/controller/jenkins/groovy/groovy.go | 282 +++++++---- pkg/controller/jenkins/groovy/groovy_test.go | 507 +++++++++++++++++++ 2 files changed, 690 insertions(+), 99 deletions(-) create mode 100644 pkg/controller/jenkins/groovy/groovy_test.go diff --git a/pkg/controller/jenkins/groovy/groovy.go b/pkg/controller/jenkins/groovy/groovy.go index da919c2a..675392ed 100644 --- a/pkg/controller/jenkins/groovy/groovy.go +++ b/pkg/controller/jenkins/groovy/groovy.go @@ -1,6 +1,7 @@ package groovy import ( + "context" "crypto/sha256" "encoding/base64" "fmt" @@ -9,144 +10,227 @@ import ( "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client" - "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/jobs" + "github.com/jenkinsci/kubernetes-operator/pkg/log" "github.com/go-logr/logr" + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" k8s "sigs.k8s.io/controller-runtime/pkg/client" ) -const ( - jobHashParameterName = "hash" -) - -// Groovy defines API for groovy scripts execution via jenkins job +// Groovy defines API for groovy secrets execution via jenkins job type Groovy struct { - jenkinsClient jenkinsclient.Jenkins - k8sClient k8s.Client - logger logr.Logger - jobName string - scriptsPath string + k8sClient k8s.Client + logger logr.Logger + jenkins *v1alpha2.Jenkins + jenkinsClient jenkinsclient.Jenkins + configurationType string + customization v1alpha2.Customization } // New creates new instance of Groovy -func New(jenkinsClient jenkinsclient.Jenkins, k8sClient k8s.Client, logger logr.Logger, jobName, scriptsPath string) *Groovy { +func New(jenkinsClient jenkinsclient.Jenkins, k8sClient k8s.Client, logger logr.Logger, jenkins *v1alpha2.Jenkins, configurationType string, customization v1alpha2.Customization) *Groovy { return &Groovy{ - jenkinsClient: jenkinsClient, - k8sClient: k8sClient, - logger: logger, - jobName: jobName, - scriptsPath: scriptsPath, + jenkinsClient: jenkinsClient, + k8sClient: k8sClient, + logger: logger, + jenkins: jenkins, + configurationType: configurationType, + customization: customization, } } -// ConfigureJob configures jenkins job for executing groovy scripts -func (g *Groovy) ConfigureJob() error { - _, created, err := g.jenkinsClient.CreateOrUpdateJob(fmt.Sprintf(configurationJobXMLFmt, g.scriptsPath), g.jobName) +// EnsureSingle runs single groovy script +func (g *Groovy) EnsureSingle(source, name, hash, groovyScript string) (requeue bool, err error) { + if g.isGroovyScriptAlreadyApplied(source, name, hash) { + return false, nil + } + + logs, err := g.jenkinsClient.ExecuteScript(groovyScript) if err != nil { - return err + if _, ok := err.(*jenkinsclient.GroovyScriptExecutionFailed); ok { + g.logger.V(log.VWarn).Info(fmt.Sprintf("%s Source '%s' Name '%s' groovy script execution failed, logs :\n%s", g.configurationType, source, name, logs)) + } + return true, err } - if created { - g.logger.Info(fmt.Sprintf("'%s' job has been created", g.jobName)) - } - return nil + + g.jenkins.Status.AppliedGroovyScripts = append(g.jenkins.Status.AppliedGroovyScripts, v1alpha2.AppliedGroovyScript{ + ConfigurationType: g.configurationType, + Source: source, + Name: name, + Hash: hash, + }) + return true, g.k8sClient.Update(context.TODO(), g.jenkins) } -// Ensure executes groovy script and verifies jenkins job status according to reconciliation loop lifecycle -func (g *Groovy) Ensure(secretOrConfigMapData map[string]string, jenkins *v1alpha2.Jenkins) (bool, error) { - jobsClient := jobs.New(g.jenkinsClient, g.k8sClient, g.logger) +// WaitForSecretSynchronization runs groovy script which waits to synchronize secrets in pod by k8s +func (g *Groovy) WaitForSecretSynchronization(secretsPath string) (requeue bool, err error) { + if len(g.customization.Secret.Name) == 0 { + return false, nil + } - hash := g.calculateHash(secretOrConfigMapData) - done, err := jobsClient.EnsureBuildJob(g.jobName, hash, map[string]string{jobHashParameterName: hash}, jenkins, true) + secret := &corev1.Secret{} + err = g.k8sClient.Get(context.TODO(), types.NamespacedName{Name: g.customization.Secret.Name, Namespace: g.jenkins.ObjectMeta.Namespace}, secret) if err != nil { - return false, err + return true, errors.WithStack(err) } - return done, nil + + toCalculate := map[string]string{} + for secretKey, secretValue := range secret.Data { + toCalculate[secretKey] = string(secretValue) + } + hash := g.calculateHash(toCalculate) + + name := "synchronizing-secret.groovy" + if g.isGroovyScriptAlreadyApplied(g.customization.Secret.Name, name, hash) { + return false, nil + } + + g.logger.Info(fmt.Sprintf("%s Secret '%s' running synchronization", g.configurationType, secret.Name)) + return g.EnsureSingle(g.customization.Secret.Name, name, hash, fmt.Sprintf(synchronizeSecretsGroovyScriptFmt, secretsPath, hash)) } -func (g *Groovy) calculateHash(secretOrConfigMapData map[string]string) string { +// Ensure runs all groovy scripts configured in customization structure +func (g *Groovy) Ensure(filter func(name string) bool, updateGroovyScript func(groovyScript string) string) (requeue bool, err error) { + secret := &corev1.Secret{} + if len(g.customization.Secret.Name) > 0 { + err := g.k8sClient.Get(context.TODO(), types.NamespacedName{Name: g.customization.Secret.Name, Namespace: g.jenkins.ObjectMeta.Namespace}, secret) + if err != nil { + return true, err + } + } + + for _, configMapRef := range g.customization.Configurations { + configMap := &corev1.ConfigMap{} + err := g.k8sClient.Get(context.TODO(), types.NamespacedName{Name: configMapRef.Name, Namespace: g.jenkins.ObjectMeta.Namespace}, configMap) + if err != nil { + return true, errors.WithStack(err) + } + + var names []string + for name := range configMap.Data { + names = append(names, name) + } + sort.Strings(names) + + for _, name := range names { + groovyScript := updateGroovyScript(configMap.Data[name]) + if !filter(name) { + g.logger.V(log.VDebug).Info(fmt.Sprintf("Skipping %s ConfigMap '%s' name '%s'", g.configurationType, configMap.Name, name)) + continue + } + + hash := g.calculateCustomizationHash(*secret, name, groovyScript) + if g.isGroovyScriptAlreadyApplied(configMap.Name, name, hash) { + continue + } + + g.logger.Info(fmt.Sprintf("%s ConfigMap '%s' name '%s' running groovy script", g.configurationType, configMap.Name, name)) + requeue, err := g.EnsureSingle(configMap.Name, name, hash, groovyScript) + if err != nil || requeue { + return requeue, err + } + } + } + + return false, nil +} + +func (g *Groovy) calculateCustomizationHash(secret corev1.Secret, key, groovyScript string) string { + toCalculate := map[string]string{} + for secretKey, secretValue := range secret.Data { + toCalculate[secretKey] = string(secretValue) + } + toCalculate[key] = groovyScript + return g.calculateHash(toCalculate) +} + +func (g *Groovy) isGroovyScriptAlreadyApplied(source, name, hash string) bool { + for _, appliedGroovyScript := range g.jenkins.Status.AppliedGroovyScripts { + if appliedGroovyScript.ConfigurationType == g.configurationType && appliedGroovyScript.Hash == hash && + appliedGroovyScript.Name == name && appliedGroovyScript.Source == source { + return true + } + } + + return false +} + +func (g *Groovy) calculateHash(data map[string]string) string { hash := sha256.New() var keys []string - for key := range secretOrConfigMapData { + for key := range data { keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { - if strings.HasSuffix(key, ".groovy") { - hash.Write([]byte(key)) - hash.Write([]byte(secretOrConfigMapData[key])) - } + hash.Write([]byte(key)) + hash.Write([]byte(data[key])) } return base64.StdEncoding.EncodeToString(hash.Sum(nil)) } -const configurationJobXMLFmt = ` - - - - false - - - - - - ` + jobHashParameterName + ` - - - false - - - - - - - false - - - false - +} ` diff --git a/pkg/controller/jenkins/groovy/groovy_test.go b/pkg/controller/jenkins/groovy/groovy_test.go new file mode 100644 index 00000000..14286764 --- /dev/null +++ b/pkg/controller/jenkins/groovy/groovy_test.go @@ -0,0 +1,507 @@ +package groovy + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" + jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client" + + "github.com/golang/mock/gomock" + "github.com/jenkinsci/kubernetes-operator/pkg/log" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestGroovy_EnsureSingle(t *testing.T) { + log.SetupLogger(true) + configurationType := "test-conf-type" + emptyCustomization := v1alpha2.Customization{} + hash := "hash" + groovyScript := "groovy-script" + groovyScriptName := "groovy-script-name" + source := "source" + ctx := context.TODO() + jenkinsName := "jenkins" + namespace := "default" + + t.Run("execute script and save status", func(t *testing.T) { + // given + jenkins := &v1alpha2.Jenkins{ + ObjectMeta: metav1.ObjectMeta{ + Name: jenkinsName, + Namespace: namespace, + }, + } + err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme) + require.NoError(t, err) + fakeClient := fake.NewFakeClient() + err = fakeClient.Create(ctx, jenkins) + require.NoError(t, err) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + jenkinsClient := jenkinsclient.NewMockJenkins(ctrl) + jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil) + + groovyClient := New(jenkinsClient, fakeClient, log.Log, jenkins, configurationType, emptyCustomization) + + // when + requeue, err := groovyClient.EnsureSingle(source, groovyScriptName, hash, groovyScript) + + // then + require.NoError(t, err) + assert.True(t, requeue) + + err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins) + require.NoError(t, err) + assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts)) + assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType) + assert.Equal(t, hash, jenkins.Status.AppliedGroovyScripts[0].Hash) + assert.Equal(t, source, jenkins.Status.AppliedGroovyScripts[0].Source) + assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name) + }) + t.Run("no execute script", func(t *testing.T) { + // given + jenkins := &v1alpha2.Jenkins{ + ObjectMeta: metav1.ObjectMeta{ + Name: jenkinsName, + Namespace: namespace, + }, + Status: v1alpha2.JenkinsStatus{ + AppliedGroovyScripts: []v1alpha2.AppliedGroovyScript{ + { + ConfigurationType: configurationType, + Source: source, + Name: groovyScriptName, + Hash: hash, + }, + }, + }, + } + err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme) + require.NoError(t, err) + fakeClient := fake.NewFakeClient() + err = fakeClient.Create(ctx, jenkins) + require.NoError(t, err) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + jenkinsClient := jenkinsclient.NewMockJenkins(ctrl) + + groovyClient := New(jenkinsClient, fakeClient, log.Log, jenkins, configurationType, emptyCustomization) + + // when + requeue, err := groovyClient.EnsureSingle(source, groovyScriptName, hash, groovyScript) + + // then + require.NoError(t, err) + assert.False(t, requeue) + + err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins) + require.NoError(t, err) + assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts)) + assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType) + assert.Equal(t, hash, jenkins.Status.AppliedGroovyScripts[0].Hash) + assert.Equal(t, source, jenkins.Status.AppliedGroovyScripts[0].Source) + assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name) + }) + t.Run("execute script fails", func(t *testing.T) { + // given + jenkins := &v1alpha2.Jenkins{ + ObjectMeta: metav1.ObjectMeta{ + Name: jenkinsName, + Namespace: namespace, + }, + } + err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme) + require.NoError(t, err) + fakeClient := fake.NewFakeClient() + err = fakeClient.Create(ctx, jenkins) + require.NoError(t, err) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + jenkinsClient := jenkinsclient.NewMockJenkins(ctrl) + jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("fail logs", &jenkinsclient.GroovyScriptExecutionFailed{}) + + groovyClient := New(jenkinsClient, fakeClient, log.Log, jenkins, configurationType, emptyCustomization) + + // when + requeue, err := groovyClient.EnsureSingle(source, groovyScriptName, hash, groovyScript) + + // then + require.Error(t, err) + assert.True(t, requeue) + + err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins) + require.NoError(t, err) + assert.Equal(t, 0, len(jenkins.Status.AppliedGroovyScripts)) + }) +} + +func TestGroovy_Ensure(t *testing.T) { + log.SetupLogger(true) + configurationType := "test-conf-type" + groovyScript := "groovy-script" + groovyScriptName := "groovy-script-name.groovy" + ctx := context.TODO() + jenkinsName := "jenkins" + namespace := "default" + configMapName := "config-map-name" + secretName := "secret-name" + + allGroovyScriptsFunc := func(name string) bool { + return true + } + noUpdateGroovyScript := func(groovyScript string) string { + return groovyScript + } + + t.Run("select groovy files with .groovy extension", func(t *testing.T) { + // given + groovyScriptExtension := ".groovy" + jenkins := &v1alpha2.Jenkins{ + ObjectMeta: metav1.ObjectMeta{ + Name: jenkinsName, + Namespace: namespace, + }, + } + customization := v1alpha2.Customization{ + Configurations: []v1alpha2.ConfigMapRef{ + { + Name: configMapName, + }, + }, + } + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: configMapName, + Namespace: namespace, + }, + Data: map[string]string{ + groovyScriptName: groovyScript, + "to-ommit": "to-ommit", + }, + } + err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme) + require.NoError(t, err) + fakeClient := fake.NewFakeClient() + err = fakeClient.Create(ctx, jenkins) + require.NoError(t, err) + err = fakeClient.Create(ctx, configMap) + require.NoError(t, err) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + jenkinsClient := jenkinsclient.NewMockJenkins(ctrl) + jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil) + + groovyClient := New(jenkinsClient, fakeClient, log.Log, jenkins, configurationType, customization) + onlyGroovyFilesFunc := func(name string) bool { + return strings.HasSuffix(name, groovyScriptExtension) + } + + // when + requeue, err := groovyClient.Ensure(onlyGroovyFilesFunc, noUpdateGroovyScript) + require.NoError(t, err) + assert.True(t, requeue) + requeue, err = groovyClient.Ensure(onlyGroovyFilesFunc, noUpdateGroovyScript) + require.NoError(t, err) + assert.False(t, requeue) + + // then + err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins) + require.NoError(t, err) + assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts)) + assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType) + assert.Equal(t, "qoXeeh4ia+KXhT01lYNxe+oxByDf8dfT2npP9fgzjbk=", jenkins.Status.AppliedGroovyScripts[0].Hash) + assert.Equal(t, configMapName, jenkins.Status.AppliedGroovyScripts[0].Source) + assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name) + }) + t.Run("change groovy script", func(t *testing.T) { + // given + groovyScriptSuffix := "suffix" + jenkins := &v1alpha2.Jenkins{ + ObjectMeta: metav1.ObjectMeta{ + Name: jenkinsName, + Namespace: namespace, + }, + } + customization := v1alpha2.Customization{ + Configurations: []v1alpha2.ConfigMapRef{ + { + Name: configMapName, + }, + }, + } + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: configMapName, + Namespace: namespace, + }, + Data: map[string]string{ + groovyScriptName: groovyScript, + }, + } + err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme) + require.NoError(t, err) + fakeClient := fake.NewFakeClient() + err = fakeClient.Create(ctx, jenkins) + require.NoError(t, err) + err = fakeClient.Create(ctx, configMap) + require.NoError(t, err) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + jenkinsClient := jenkinsclient.NewMockJenkins(ctrl) + jenkinsClient.EXPECT().ExecuteScript(groovyScript+groovyScriptSuffix).Return("logs", nil) + + groovyClient := New(jenkinsClient, fakeClient, log.Log, jenkins, configurationType, customization) + updateGroovyFunc := func(groovyScript string) string { + return groovyScript + groovyScriptSuffix + } + + // when + requeue, err := groovyClient.Ensure(allGroovyScriptsFunc, updateGroovyFunc) + require.NoError(t, err) + assert.True(t, requeue) + requeue, err = groovyClient.Ensure(allGroovyScriptsFunc, updateGroovyFunc) + require.NoError(t, err) + assert.False(t, requeue) + + // then + err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins) + require.NoError(t, err) + assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts)) + assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType) + assert.Equal(t, "TgTpV3nDxMNMM93t6jgni0UHa7C+uL+D+BLcW3a7b6M=", jenkins.Status.AppliedGroovyScripts[0].Hash) + assert.Equal(t, configMapName, jenkins.Status.AppliedGroovyScripts[0].Source) + assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name) + }) + t.Run("execute script without secret and save status", func(t *testing.T) { + // given + jenkins := &v1alpha2.Jenkins{ + ObjectMeta: metav1.ObjectMeta{ + Name: jenkinsName, + Namespace: namespace, + }, + } + customization := v1alpha2.Customization{ + Configurations: []v1alpha2.ConfigMapRef{ + { + Name: configMapName, + }, + }, + } + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: configMapName, + Namespace: namespace, + }, + Data: map[string]string{ + groovyScriptName: groovyScript, + }, + } + err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme) + require.NoError(t, err) + fakeClient := fake.NewFakeClient() + err = fakeClient.Create(ctx, jenkins) + require.NoError(t, err) + err = fakeClient.Create(ctx, configMap) + require.NoError(t, err) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + jenkinsClient := jenkinsclient.NewMockJenkins(ctrl) + jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil) + + groovyClient := New(jenkinsClient, fakeClient, log.Log, jenkins, configurationType, customization) + + // when + requeue, err := groovyClient.Ensure(allGroovyScriptsFunc, noUpdateGroovyScript) + require.NoError(t, err) + assert.True(t, requeue) + requeue, err = groovyClient.Ensure(allGroovyScriptsFunc, noUpdateGroovyScript) + require.NoError(t, err) + assert.False(t, requeue) + + // then + err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins) + require.NoError(t, err) + assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts)) + assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType) + assert.Equal(t, "qoXeeh4ia+KXhT01lYNxe+oxByDf8dfT2npP9fgzjbk=", jenkins.Status.AppliedGroovyScripts[0].Hash) + assert.Equal(t, configMapName, jenkins.Status.AppliedGroovyScripts[0].Source) + assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name) + }) + t.Run("execute script with secret and save status", func(t *testing.T) { + // given + jenkins := &v1alpha2.Jenkins{ + ObjectMeta: metav1.ObjectMeta{ + Name: jenkinsName, + Namespace: namespace, + }, + } + customization := v1alpha2.Customization{ + Secret: v1alpha2.SecretRef{Name: secretName}, + Configurations: []v1alpha2.ConfigMapRef{ + { + Name: configMapName, + }, + }, + } + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: configMapName, + Namespace: namespace, + }, + Data: map[string]string{ + groovyScriptName: groovyScript, + }, + } + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + }, + Data: map[string][]byte{ + "SECRET_KEY": []byte("secret-value"), + }, + } + err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme) + require.NoError(t, err) + fakeClient := fake.NewFakeClient() + err = fakeClient.Create(ctx, jenkins) + require.NoError(t, err) + err = fakeClient.Create(ctx, secret) + require.NoError(t, err) + err = fakeClient.Create(ctx, configMap) + require.NoError(t, err) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + jenkinsClient := jenkinsclient.NewMockJenkins(ctrl) + jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil) + + groovyClient := New(jenkinsClient, fakeClient, log.Log, jenkins, configurationType, customization) + + // when + requeue, err := groovyClient.Ensure(allGroovyScriptsFunc, noUpdateGroovyScript) + require.NoError(t, err) + assert.True(t, requeue) + requeue, err = groovyClient.Ensure(allGroovyScriptsFunc, noUpdateGroovyScript) + require.NoError(t, err) + assert.False(t, requeue) + + // then + err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins) + require.NoError(t, err) + assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts)) + assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType) + assert.Equal(t, "em9pjw9mUheUpPRCJWD2Dww+80YQPoHCZbzzKZZw4lo=", jenkins.Status.AppliedGroovyScripts[0].Hash) + assert.Equal(t, configMapName, jenkins.Status.AppliedGroovyScripts[0].Source) + assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name) + }) +} + +func TestGroovy_isGroovyScriptAlreadyApplied(t *testing.T) { + log.SetupLogger(true) + emptyCustomization := v1alpha2.Customization{} + configurationType := "test-conf-type" + + t.Run("found", func(t *testing.T) { + jenkins := &v1alpha2.Jenkins{ + Status: v1alpha2.JenkinsStatus{ + AppliedGroovyScripts: []v1alpha2.AppliedGroovyScript{ + { + ConfigurationType: configurationType, + Source: "source", + Name: "name", + Hash: "hash", + }, + }, + }, + } + groovyClient := New(nil, nil, log.Log, jenkins, configurationType, emptyCustomization) + + got := groovyClient.isGroovyScriptAlreadyApplied("source", "name", "hash") + + assert.True(t, got) + }) + t.Run("not found", func(t *testing.T) { + jenkins := &v1alpha2.Jenkins{ + Status: v1alpha2.JenkinsStatus{ + AppliedGroovyScripts: []v1alpha2.AppliedGroovyScript{ + { + ConfigurationType: configurationType, + Source: "source", + Name: "name", + Hash: "hash", + }, + }, + }, + } + groovyClient := New(nil, nil, log.Log, jenkins, configurationType, emptyCustomization) + + got := groovyClient.isGroovyScriptAlreadyApplied("source", "not-exist", "hash") + + assert.False(t, got) + }) + t.Run("empty Jenkins status", func(t *testing.T) { + jenkins := &v1alpha2.Jenkins{} + groovyClient := New(nil, nil, log.Log, jenkins, configurationType, emptyCustomization) + + got := groovyClient.isGroovyScriptAlreadyApplied("source", "name", "hash") + + assert.False(t, got) + }) +} + +func TestAddSecretsLoaderToGroovyScript(t *testing.T) { + secretsPath := "/var/jenkins/groovy-scripts-secrets" + secretsLoader := fmt.Sprintf(secretsLoaderGroovyScriptFmt, secretsPath) + + t.Run("without imports", func(t *testing.T) { + groovyScript := "println 'Simple groovy script" + updater := AddSecretsLoaderToGroovyScript(secretsPath) + + got := updater(groovyScript) + + assert.Equal(t, secretsLoader+groovyScript, got) + }) + t.Run("with imports", func(t *testing.T) { + groovyScript := `import com.foo.bar +import com.foo.bar2 +println 'Simple groovy script'` + imports := `import com.foo.bar +import com.foo.bar2` + tail := `println 'Simple groovy script'` + update := AddSecretsLoaderToGroovyScript(secretsPath) + + got := update(groovyScript) + + assert.Equal(t, imports+"\n\n"+secretsLoader+"\n\n"+tail, got) + }) + t.Run("with imports and separate section", func(t *testing.T) { + groovyScript := `import com.foo.bar +import com.foo.bar2 + +println 'Simple groovy script'` + imports := `import com.foo.bar +import com.foo.bar2` + tail := `println 'Simple groovy script'` + update := AddSecretsLoaderToGroovyScript(secretsPath) + + got := update(groovyScript) + + assert.Equal(t, imports+"\n\n"+secretsLoader+"\n\n\n"+tail, got) + }) +} From 6a3a68bec039bd905d9c2a88d8a13a0f9d48ea60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20S=C4=99k?= Date: Sun, 30 Jun 2019 23:16:42 +0200 Subject: [PATCH 08/59] #28 Use new API for groovy base configuration --- .../jenkins/configuration/base/reconcile.go | 31 +++++++------------ pkg/controller/jenkins/jenkins_controller.go | 8 ++--- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/pkg/controller/jenkins/configuration/base/reconcile.go b/pkg/controller/jenkins/configuration/base/reconcile.go index 713ebada..1efd89f8 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile.go +++ b/pkg/controller/jenkins/configuration/base/reconcile.go @@ -807,27 +807,20 @@ func (r *ReconcileJenkinsBaseConfiguration) ensureJenkinsClient(meta metav1.Obje } func (r *ReconcileJenkinsBaseConfiguration) ensureBaseConfiguration(jenkinsClient jenkinsclient.Jenkins) (reconcile.Result, error) { - groovyClient := groovy.New(jenkinsClient, r.k8sClient, r.logger, fmt.Sprintf("%s-base-configuration", constants.OperatorName), resources.JenkinsBaseConfigurationVolumePath) - - err := groovyClient.ConfigureJob() - if err != nil { - return reconcile.Result{}, err + customization := v1alpha2.GroovyScripts{ + Customization: v1alpha2.Customization{ + Secret: v1alpha2.SecretRef{Name: ""}, + Configurations: []v1alpha2.ConfigMapRef{{Name: resources.GetBaseConfigurationConfigMapName(r.jenkins)}}, + }, } - configuration := &corev1.ConfigMap{} - namespaceName := types.NamespacedName{Namespace: r.jenkins.Namespace, Name: resources.GetBaseConfigurationConfigMapName(r.jenkins)} - err = r.k8sClient.Get(context.TODO(), namespaceName, configuration) - if err != nil { - return reconcile.Result{}, stackerr.WithStack(err) - } + groovyClient := groovy.New(jenkinsClient, r.k8sClient, r.logger, r.jenkins, "base-groovy", customization.Customization) - done, err := groovyClient.Ensure(configuration.Data, r.jenkins) - if err != nil { - return reconcile.Result{}, err - } - if !done { - return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 10}, nil - } + requeue, err := groovyClient.Ensure(func(name string) bool { + return strings.HasSuffix(name, ".groovy") + }, func(groovyScript string) string { + return groovyScript + }) - return reconcile.Result{}, nil + return reconcile.Result{Requeue: requeue}, err } diff --git a/pkg/controller/jenkins/jenkins_controller.go b/pkg/controller/jenkins/jenkins_controller.go index d19203cf..ea56a719 100644 --- a/pkg/controller/jenkins/jenkins_controller.go +++ b/pkg/controller/jenkins/jenkins_controller.go @@ -6,6 +6,7 @@ import ( "reflect" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" + jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/user" @@ -171,6 +172,9 @@ func (r *ReconcileJenkins) Reconcile(request reconcile.Request) (reconcile.Resul if err == jobs.ErrorUnrecoverableBuildFailed { return reconcile.Result{Requeue: false}, nil } + if _, ok := err.(*jenkinsclient.GroovyScriptExecutionFailed); ok { + return reconcile.Result{Requeue: false}, nil + } return reconcile.Result{Requeue: true}, nil } return result, nil @@ -345,10 +349,6 @@ func (r *ReconcileJenkins) setDefaults(jenkins *v1alpha2.Jenkins, logger logr.Lo changed = true jenkins.Status.OperatorVersion = version.Version } - if len(jenkins.Spec.Master.Plugins) == 0 { - changed = true - jenkins.Spec.Master.Plugins = []v1alpha2.Plugin{{Name: "simple-theme-plugin", Version: "0.5.1"}} - } if isResourceRequirementsNotSet(jenkinsContainer.Resources) { logger.Info("Setting default Jenkins master container resource requirements") changed = true From 7d716b972fd4fa4faf05288c82e0a63a5f036cae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20S=C4=99k?= Date: Sun, 30 Jun 2019 23:18:20 +0200 Subject: [PATCH 09/59] #28 Use new API for groovy and CasC user configuration --- .../jenkins/configuration/user/casc/caac.go | 206 ++---------------- .../jenkins/configuration/user/reconcile.go | 53 ++--- 2 files changed, 44 insertions(+), 215 deletions(-) diff --git a/pkg/controller/jenkins/configuration/user/casc/caac.go b/pkg/controller/jenkins/configuration/user/casc/caac.go index dd583164..cf855195 100644 --- a/pkg/controller/jenkins/configuration/user/casc/caac.go +++ b/pkg/controller/jenkins/configuration/user/casc/caac.go @@ -1,216 +1,50 @@ package casc import ( - "context" - "crypto/sha256" - "encoding/base64" "fmt" - "sort" "strings" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources" - "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/jobs" + "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/groovy" "github.com/go-logr/logr" - "github.com/pkg/errors" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/types" k8s "sigs.k8s.io/controller-runtime/pkg/client" ) -const ( - userConfigurationHashParameterName = "userConfigurationHash" - userConfigurationSecretHashParameterName = "userConfigurationSecretHash" -) - // ConfigurationAsCode defines API which configures Jenkins with help Configuration as a code plugin type ConfigurationAsCode struct { - jenkinsClient jenkinsclient.Jenkins - k8sClient k8s.Client - logger logr.Logger - jobName string + groovyClient *groovy.Groovy } // New creates new instance of ConfigurationAsCode -func New(jenkinsClient jenkinsclient.Jenkins, k8sClient k8s.Client, logger logr.Logger, jobName string) *ConfigurationAsCode { +func New(jenkinsClient jenkinsclient.Jenkins, k8sClient k8s.Client, logger logr.Logger, jenkins *v1alpha2.Jenkins) *ConfigurationAsCode { return &ConfigurationAsCode{ - jenkinsClient: jenkinsClient, - k8sClient: k8sClient, - logger: logger, - jobName: jobName, + groovyClient: groovy.New(jenkinsClient, k8sClient, logger, jenkins, "user-casc", jenkins.Spec.ConfigurationAsCode.Customization), } } -// ConfigureJob configures jenkins job which configures Jenkins with help Configuration as a code plugin -func (g *ConfigurationAsCode) ConfigureJob() error { - _, created, err := g.jenkinsClient.CreateOrUpdateJob(configurationJobXMLFmt, g.jobName) - if err != nil { - return err - } - if created { - g.logger.Info(fmt.Sprintf("'%s' job has been created", g.jobName)) - } - return nil -} - // Ensure configures Jenkins with help Configuration as a code plugin -func (g *ConfigurationAsCode) Ensure(jenkins *v1alpha2.Jenkins) (bool, error) { - jobsClient := jobs.New(g.jenkinsClient, g.k8sClient, g.logger) - - configuration := &corev1.ConfigMap{} - ConfigMapNamespaceName := types.NamespacedName{Namespace: jenkins.Namespace, Name: resources.GetUserConfigurationConfigMapNameFromJenkins(jenkins)} - err := g.k8sClient.Get(context.TODO(), ConfigMapNamespaceName, configuration) - if err != nil { - return false, errors.WithStack(err) +func (c *ConfigurationAsCode) Ensure(jenkins *v1alpha2.Jenkins) (requeue bool, err error) { + requeue, err = c.groovyClient.WaitForSecretSynchronization(resources.ConfigurationAsCodeSecretVolumePath) + if err != nil || requeue { + return requeue, err } - secret := &corev1.Secret{} - secretNamespaceName := types.NamespacedName{Namespace: jenkins.Namespace, Name: resources.GetUserConfigurationSecretNameFromJenkins(jenkins)} - err = g.k8sClient.Get(context.TODO(), secretNamespaceName, secret) - if err != nil { - return false, errors.WithStack(err) - } - - userConfigurationSecretHash := g.calculateUserConfigurationSecretHash(secret) - userConfigurationHash := g.calculateUserConfigurationHash(configuration) - done, err := jobsClient.EnsureBuildJob( - g.jobName, - userConfigurationSecretHash+userConfigurationHash, - map[string]string{ - userConfigurationHashParameterName: userConfigurationHash, - userConfigurationSecretHashParameterName: userConfigurationSecretHash, - }, - jenkins, - true) - if err != nil { - return false, err - } - return done, nil + return c.groovyClient.Ensure(func(name string) bool { + return strings.HasSuffix(name, ".yaml") || strings.HasSuffix(name, ".yml") + }, func(groovyScript string) string { + return fmt.Sprintf(applyConfigurationAsCodeGroovyScriptFmt, groovyScript) + }) } -func (g *ConfigurationAsCode) calculateUserConfigurationSecretHash(userConfigurationSecret *corev1.Secret) string { - hash := sha256.New() +const applyConfigurationAsCodeGroovyScriptFmt = ` +def config = ''' +%s +''' +def stream = new ByteArrayInputStream(config.getBytes('UTF-8')) - var keys []string - for key := range userConfigurationSecret.Data { - keys = append(keys, key) - } - sort.Strings(keys) - for _, key := range keys { - hash.Write([]byte(key)) - hash.Write([]byte(userConfigurationSecret.Data[key])) - } - return base64.StdEncoding.EncodeToString(hash.Sum(nil)) -} - -func (g *ConfigurationAsCode) calculateUserConfigurationHash(userConfiguration *corev1.ConfigMap) string { - hash := sha256.New() - - var keys []string - for key := range userConfiguration.Data { - keys = append(keys, key) - } - sort.Strings(keys) - for _, key := range keys { - if strings.HasSuffix(key, ".yaml") { - hash.Write([]byte(key)) - hash.Write([]byte(userConfiguration.Data[key])) - } - } - return base64.StdEncoding.EncodeToString(hash.Sum(nil)) -} - -const configurationJobXMLFmt = ` - - - - false - - - - - - ` + userConfigurationSecretHashParameterName + ` - - - false - - - ` + userConfigurationHashParameterName + ` - - - false - - - - - - - false - - - false - +def source = new io.jenkins.plugins.casc.yaml.YamlSource(stream, io.jenkins.plugins.casc.yaml.YamlSource.READ_FROM_INPUTSTREAM) +io.jenkins.plugins.casc.ConfigurationAsCode.get().configureWith(source) ` diff --git a/pkg/controller/jenkins/configuration/user/reconcile.go b/pkg/controller/jenkins/configuration/user/reconcile.go index c172dfc2..13525a62 100644 --- a/pkg/controller/jenkins/configuration/user/reconcile.go +++ b/pkg/controller/jenkins/configuration/user/reconcile.go @@ -1,7 +1,7 @@ package user import ( - "context" + "strings" "time" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" @@ -10,14 +10,11 @@ import ( "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/user/casc" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/user/seedjobs" - "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/groovy" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/jobs" "github.com/go-logr/logr" "github.com/pkg/errors" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" k8s "sigs.k8s.io/controller-runtime/pkg/client" @@ -104,37 +101,35 @@ func (r *ReconcileUserConfiguration) ensureSeedJobs() (reconcile.Result, error) } func (r *ReconcileUserConfiguration) ensureUserConfiguration(jenkinsClient jenkinsclient.Jenkins) (reconcile.Result, error) { - configuration := &corev1.ConfigMap{} - namespaceName := types.NamespacedName{Namespace: r.jenkins.Namespace, Name: resources.GetUserConfigurationConfigMapNameFromJenkins(r.jenkins)} - err := r.k8sClient.Get(context.TODO(), namespaceName, configuration) + groovyClient := groovy.New(jenkinsClient, r.k8sClient, r.logger, r.jenkins, "user-groovy", r.jenkins.Spec.GroovyScripts.Customization) + + requeue, err := groovyClient.WaitForSecretSynchronization(resources.GroovyScriptsSecretVolumePath) if err != nil { - return reconcile.Result{}, errors.WithStack(err) + return reconcile.Result{}, err + } + if requeue { + return reconcile.Result{Requeue: true}, nil + } + requeue, err = groovyClient.Ensure(func(name string) bool { + return strings.HasSuffix(name, ".groovy") + }, func(groovyScript string) string { + // TODO load secrets to variables + return groovyScript + }) + if err != nil { + return reconcile.Result{}, err + } + if requeue { + return reconcile.Result{Requeue: true}, nil } - groovyClient := groovy.New(jenkinsClient, r.k8sClient, r.logger, constants.UserConfigurationJobName, resources.JenkinsUserConfigurationVolumePath) - err = groovyClient.ConfigureJob() + configurationAsCodeClient := casc.New(jenkinsClient, r.k8sClient, r.logger, r.jenkins) + requeue, err = configurationAsCodeClient.Ensure(r.jenkins) if err != nil { return reconcile.Result{}, err } - done, err := groovyClient.Ensure(configuration.Data, r.jenkins) - if err != nil { - return reconcile.Result{}, err - } - if !done { - return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 10}, nil - } - - configurationAsCodeClient := casc.New(jenkinsClient, r.k8sClient, r.logger, constants.UserConfigurationCASCJobName) - err = configurationAsCodeClient.ConfigureJob() - if err != nil { - return reconcile.Result{}, err - } - done, err = configurationAsCodeClient.Ensure(r.jenkins) - if err != nil { - return reconcile.Result{}, err - } - if !done { - return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 10}, nil + if requeue { + return reconcile.Result{Requeue: true}, nil } return reconcile.Result{}, nil From 05a446e4a2983ed7f907324cc276c0765726f0e0 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Mon, 8 Jul 2019 15:03:23 +0200 Subject: [PATCH 10/59] Added Dockerfile and entrypoint --- Dockerfile | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++ entrypoint.sh | 19 ++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 Dockerfile create mode 100644 entrypoint.sh diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..01cc30cc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,53 @@ +FROM docker:18.09 + +ARG GO_VERSION +ARG OPERATOR_SDK_VERSION +ARG MINIKUBE_VERSION + +ARG GOPATH="/go" + +RUN mkdir -p /go + +# Stage 1 - Install dependencies +RUN apk update && \ + apk add --no-cache \ + curl \ + python \ + py-crcmod \ + bash \ + libc6-compat \ + openssh-client \ + git \ + make \ + gcc \ + libc-dev \ + git + +RUN curl -O https://storage.googleapis.com/golang/go$GO_VERSION.linux-amd64.tar.gz && tar -xvf go$GO_VERSION.linux-amd64.tar.gz + +# Stage 2 - Install operator-sdk +RUN echo $GOPATH/bin/operator-sdk +RUN curl -L https://github.com/operator-framework/operator-sdk/releases/download/v$OPERATOR_SDK_VERSION/operator-sdk-v$OPERATOR_SDK_VERSION-x86_64-linux-gnu -o $GOPATH/bin/operator-sdk \ + && chmod +x $GOPATH/bin/operator-sdk + +RUN curl -Lo minikube https://storage.googleapis.com/minikube/releases/v$MINIKUBE_VERSION/minikube-linux-amd64 \ + && chmod +x minikube \ + && cp minikube /usr/local/bin/ \ + && rm minikube + +RUN curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl \ + && chmod +x ./kubectl \ + && mv ./kubectl /usr/local/bin/kubectl + +RUN export GO111MODULE=auto + +RUN mkdir -p $GOPATH/src/github.com/jenkinsci/kubernetes-operator +ADD . $GOPATH/src/github.com/jenkinsci/kubernetes-operator +WORKDIR $GOPATH/src/github.com/jenkinsci/kubernetes-operator + +RUN mkdir -p /home/builder + +ENV DOCKER_TLS_VERIFY 1 +ENV DOCKER_CERT_PATH /minikube/certs + +ENTRYPOINT ["./entrypoint.sh"] diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 00000000..ef742ebb --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +export GOPATH=/go +export PATH=$PATH:/usr/local/go/bin:$GOPATH/bin +export GO111MODULE=on + +kubectl config set-cluster minikube --server=https://$MINIKUBE_IP:8443 \ + --certificate-authority=/minikube/ca.crt && \ + kubectl config set-credentials minikube --certificate-authority=/root/.minikube/ca.crt \ + --client-key=/minikube/client.key \ + --client-certificate=/minikube/client.crt && \ + kubectl config set-context minikube --cluster=minikube --user=minikube && \ + kubectl config use-context minikube + +make go-dependencies +ln -s $GOPATH/src/github.com/jenkinsci/kubernetes-operator/vendor/k8s.io $GOPATH/src/k8s.i +ln -s $GOPATH/src/github.com/jenkinsci/kubernetes-operator/vendor/sigs.k8s.io $GOPATH/src/sigs.k8s.io + +bash \ No newline at end of file From 5e22cdfdcef8898fd7ac4b51be974459e3d156b8 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Mon, 8 Jul 2019 15:11:12 +0200 Subject: [PATCH 11/59] Fixed e2e tests --- test/e2e/configuration_test.go | 30 ++++++++++++++---------------- test/e2e/jenkins.go | 4 +++- test/e2e/restart_test.go | 4 ++-- test/e2e/seedjobs_test.go | 2 +- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/test/e2e/configuration_test.go b/test/e2e/configuration_test.go index e80384eb..26827d06 100644 --- a/test/e2e/configuration_test.go +++ b/test/e2e/configuration_test.go @@ -40,31 +40,29 @@ 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: userConfigurationConfigMapName, - }, - }, + groovyScripts := v1alpha2.GroovyScripts{ + Customization: v1alpha2.Customization{ + Configurations: []v1alpha2.ConfigMapRef{ + {userConfigurationConfigMapName}, }, + Secret:v1alpha2.SecretRef{userConfigurationSecretName }, }, - { - Name: "test-secret", - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: userConfigurationSecretName, - }, + } + + + casc := v1alpha2.ConfigurationAsCode{ + Customization: v1alpha2.Customization{ + Configurations: []v1alpha2.ConfigMapRef{ + {userConfigurationConfigMapName}, }, + Secret:v1alpha2.SecretRef{userConfigurationSecretName }, }, } // base createUserConfigurationSecret(t, namespace, systemMessageEnvName, systemMessage) createUserConfigurationConfigMap(t, namespace, numberOfExecutors, fmt.Sprintf("${%s}", systemMessageEnvName)) - jenkins := createJenkinsCR(t, jenkinsCRName, namespace, &[]v1alpha2.SeedJob{mySeedJob.SeedJob}, volumes) + jenkins := createJenkinsCR(t, jenkinsCRName, namespace, &[]v1alpha2.SeedJob{mySeedJob.SeedJob}, groovyScripts, casc) createDefaultLimitsForContainersInNamespace(t, namespace) createKubernetesCredentialsProviderSecret(t, namespace, mySeedJob) waitForJenkinsBaseConfigurationToComplete(t, jenkins) diff --git a/test/e2e/jenkins.go b/test/e2e/jenkins.go index 580b1cb9..b62e2930 100644 --- a/test/e2e/jenkins.go +++ b/test/e2e/jenkins.go @@ -65,7 +65,7 @@ func createJenkinsAPIClient(jenkins *v1alpha2.Jenkins) (jenkinsclient.Jenkins, e ) } -func createJenkinsCR(t *testing.T, name, namespace string, seedJob *[]v1alpha2.SeedJob, volumes []corev1.Volume) *v1alpha2.Jenkins { +func createJenkinsCR(t *testing.T, name, namespace string, seedJob *[]v1alpha2.SeedJob, groovyScripts v1alpha2.GroovyScripts, casc v1alpha2.ConfigurationAsCode) *v1alpha2.Jenkins { var seedJobs []v1alpha2.SeedJob if seedJob != nil { seedJobs = append(seedJobs, *seedJob...) @@ -78,6 +78,8 @@ func createJenkinsCR(t *testing.T, name, namespace string, seedJob *[]v1alpha2.S Namespace: namespace, }, Spec: v1alpha2.JenkinsSpec{ + GroovyScripts:groovyScripts, + ConfigurationAsCode:casc, Master: v1alpha2.JenkinsMaster{ Annotations: map[string]string{"test": "label"}, Containers: []v1alpha2.Container{ diff --git a/test/e2e/restart_test.go b/test/e2e/restart_test.go index 0dce822d..fab56c46 100644 --- a/test/e2e/restart_test.go +++ b/test/e2e/restart_test.go @@ -20,7 +20,7 @@ func TestJenkinsMasterPodRestart(t *testing.T) { // Deletes test namespace defer ctx.Cleanup() - jenkins := createJenkinsCR(t, "e2e", namespace, nil, []corev1.Volume{}) + jenkins := createJenkinsCR(t, "e2e", namespace, nil, v1alpha2.GroovyScripts{}, v1alpha2.ConfigurationAsCode{}) waitForJenkinsBaseConfigurationToComplete(t, jenkins) restartJenkinsMasterPod(t, jenkins) waitForRecreateJenkinsMasterPod(t, jenkins) @@ -36,7 +36,7 @@ func TestSafeRestart(t *testing.T) { jenkinsCRName := "e2e" configureAuthorizationToUnSecure(t, namespace) - jenkins := createJenkinsCR(t, jenkinsCRName, namespace, nil, []corev1.Volume{}) + jenkins := createJenkinsCR(t, jenkinsCRName, namespace, nil, v1alpha2.GroovyScripts{}, v1alpha2.ConfigurationAsCode{}) 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 b32da366..8e3d5421 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, []corev1.Volume{}) + jenkins := createJenkinsCR(t, jenkinsCRName, namespace, &seedJobs, v1alpha2.GroovyScripts{}, v1alpha2.ConfigurationAsCode{}) waitForJenkinsBaseConfigurationToComplete(t, jenkins) verifyJenkinsMasterPodAttributes(t, jenkins) From 432988178847cb83e300f02704bde1bb35b17335 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Mon, 8 Jul 2019 15:13:30 +0200 Subject: [PATCH 12/59] Improved TestConfiguration --- test/e2e/configuration_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/e2e/configuration_test.go b/test/e2e/configuration_test.go index 26827d06..fbc5fa7f 100644 --- a/test/e2e/configuration_test.go +++ b/test/e2e/configuration_test.go @@ -70,6 +70,7 @@ func TestConfiguration(t *testing.T) { verifyJenkinsMasterPodAttributes(t, jenkins) client := verifyJenkinsAPIConnection(t, jenkins) verifyPlugins(t, client, jenkins) + verifyPodPropagation(t, jenkins) // user waitForJenkinsUserConfigurationToComplete(t, jenkins) @@ -77,6 +78,15 @@ func TestConfiguration(t *testing.T) { verifyJenkinsSeedJobs(t, client, []seedJobConfig{mySeedJob}) } +func verifyPodPropagation(t *testing.T, jenkins *v1alpha2.Jenkins) { + jenkinsPod := getJenkinsMasterPod(t, jenkins) + jenkins = getJenkins(t, jenkins.Namespace, jenkins.Name) + + assert.Equal(t, jenkins.Spec.Master.SecurityContext, jenkinsPod.Spec.SecurityContext) + assert.Equal(t, jenkins.Spec.Master.Containers[0].Command, jenkinsPod.Spec.Containers[0].Command) +} + + func createUserConfigurationSecret(t *testing.T, namespace string, systemMessageEnvName, systemMessage string) { userConfiguration := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ From 12bd6b0d5437ade122ffe8ed4ef1e894578f229a Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Mon, 8 Jul 2019 15:14:44 +0200 Subject: [PATCH 13/59] Added docker goal to Makefile --- Makefile | 24 ++++++++++++++++++++++++ entrypoint.sh | 0 2 files changed, 24 insertions(+) mode change 100644 => 100755 entrypoint.sh diff --git a/Makefile b/Makefile index ad889fe3..24a33f85 100644 --- a/Makefile +++ b/Makefile @@ -390,3 +390,27 @@ endif @echo "Dependencies:" go mod vendor -v @echo + +.PHONY: image +image: ## Create the docker image from the Dockerfile + @echo "+ $@" + docker build --rm --force-rm --no-cache \ + --build-arg GO_VERSION=$(GO_VERSION) \ + --build-arg MINIKUBE_VERSION=$(MINIKUBE_VERSION) \ + --build-arg OPERATOR_SDK_VERSION=$(OPERATOR_SDK_VERSION) \ + -t infrastructure/runner . + +.PHONY: indocker +PWD := $(shell pwd) +DOCKER_HOST_IP := $(shell minikube docker-env | grep DOCKER_HOST | cut -d '"' -f 2) +MINIKUBE_IP := $(shell minikube ip) +indocker: image ## Run make in a docker container + @echo "+ $@" + docker run --rm -it $(DOCKER_FLAGS) \ + -v /var/run/docker.sock:/var/run/docker.sock \ + --mount type=bind,source=$(PWD),target=/go/src/github.com/jenkinsci/kubernetes-operator \ + --mount type=bind,source=$(HOME)/.minikube,target=/minikube \ + --mount type=bind,source=$(HOME)/.kube,target=/home/builder/.kube \ + -e DOCKER_HOST=$(DOCKER_HOST_IP) \ + -e MINIKUBE_IP=$(MINIKUBE_IP) \ + infrastructure/runner \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh old mode 100644 new mode 100755 From 546edb4d9fcf8e3c96c98524d704d3c17a5109f0 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Mon, 8 Jul 2019 15:16:17 +0200 Subject: [PATCH 14/59] Improved config.env --- config.env | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config.env b/config.env index d2cedd76..f0052c0a 100644 --- a/config.env +++ b/config.env @@ -1,5 +1,7 @@ # Setup variables for the Makefile NAME=kubernetes-operator +OPERATOR_SDK_VERSION=0.8.1 +GO_VERSION=1.12.6 PKG=github.com/jenkinsci/kubernetes-operator DOCKER_ORGANIZATION=virtuslab DOCKER_REGISTRY=jenkins-operator @@ -7,6 +9,7 @@ NAMESPACE=default API_VERSION=v1alpha2 MINIKUBE_KUBERNETES_VERSION=v1.12.9 MINIKUBE_DRIVER=virtualbox +MINIKUBE_VERSION=1.2.0 KUBECTL_CONTEXT=minikube ALL_IN_ONE_DEPLOY_FILE_PREFIX=all-in-one GEN_CRD_API=gen-crd-api-reference-docs \ No newline at end of file From b1696e5d9eba84265428e7306a2e59da9365f2d5 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Mon, 8 Jul 2019 15:30:51 +0200 Subject: [PATCH 15/59] Fixed sed command on macOS --- Makefile | 25 +++- .../jenkins/v1alpha2/zz_generated.deepcopy.go | 111 ++++++++++++++++++ 2 files changed, 135 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 24a33f85..79cbf0b6 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,19 @@ SHELL := /bin/sh PATH := $(GOPATH)/bin:$(PATH) +OSFLAG := +ifeq ($(OS),Windows_NT) + OSFLAG = WIN32 +else + UNAME_S := $(shell uname -s) + ifeq ($(UNAME_S),Linux) + OSFLAG = LINUX + endif + ifeq ($(UNAME_S),Darwin) + OSFLAG = OSX + endif +endif + # Import config # You can change the default config with `make config="config_special.env" build` config ?= config.env @@ -157,10 +170,20 @@ e2e: build docker-build ## Runs e2e tests, you can use EXTRA_ARGS cat deploy/role.yaml >> deploy/namespace-init.yaml cat deploy/role_binding.yaml >> deploy/namespace-init.yaml cat deploy/operator.yaml >> deploy/namespace-init.yaml +ifeq ($(OSFLAG), LINUX) sed -i 's|\(image:\).*|\1 $(DOCKER_REGISTRY):$(GITCOMMIT)|g' deploy/namespace-init.yaml ifeq ($(KUBECTL_CONTEXT),minikube) sed -i 's|\(imagePullPolicy\): IfNotPresent|\1: Never|g' deploy/namespace-init.yaml sed -i 's|\(args:\).*|\1\ ["--minikube"\]|' deploy/namespace-init.yaml +endif +endif + +ifeq ($(OSFLAG), OSX) + sed -i '' 's|\(image:\).*|\1 $(DOCKER_REGISTRY):$(GITCOMMIT)|g' deploy/namespace-init.yaml +ifeq ($(KUBECTL_CONTEXT),minikube) + sed -i '' 's|\(imagePullPolicy\): IfNotPresent|\1: Never|g' deploy/namespace-init.yaml + sed -i '' 's|\(args:\).*|\1\ ["--minikube"\]|' deploy/namespace-init.yaml +endif endif @RUNNING_TESTS=1 go test -parallel=1 "./test/e2e/" -tags "$(BUILDTAGS) cgo" -v -timeout 30m -run "$(E2E_TEST_SELECTOR)" \ @@ -413,4 +436,4 @@ indocker: image ## Run make in a docker container --mount type=bind,source=$(HOME)/.kube,target=/home/builder/.kube \ -e DOCKER_HOST=$(DOCKER_HOST_IP) \ -e MINIKUBE_IP=$(MINIKUBE_IP) \ - infrastructure/runner \ No newline at end of file + infrastructure/runner diff --git a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go index cf8da24f..ab88bd8c 100644 --- a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go @@ -9,6 +9,22 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AppliedGroovyScript) DeepCopyInto(out *AppliedGroovyScript) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppliedGroovyScript. +func (in *AppliedGroovyScript) DeepCopy() *AppliedGroovyScript { + if in == nil { + return nil + } + out := new(AppliedGroovyScript) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Backup) DeepCopyInto(out *Backup) { *out = *in @@ -50,6 +66,39 @@ func (in *Build) DeepCopy() *Build { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfigMapRef) DeepCopyInto(out *ConfigMapRef) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigMapRef. +func (in *ConfigMapRef) DeepCopy() *ConfigMapRef { + if in == nil { + return nil + } + out := new(ConfigMapRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfigurationAsCode) DeepCopyInto(out *ConfigurationAsCode) { + *out = *in + in.Customization.DeepCopyInto(&out.Customization) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigurationAsCode. +func (in *ConfigurationAsCode) DeepCopy() *ConfigurationAsCode { + if in == nil { + return nil + } + out := new(ConfigurationAsCode) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Container) DeepCopyInto(out *Container) { *out = *in @@ -123,6 +172,45 @@ func (in *Container) DeepCopy() *Container { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Customization) DeepCopyInto(out *Customization) { + *out = *in + out.Secret = in.Secret + if in.Configurations != nil { + in, out := &in.Configurations, &out.Configurations + *out = make([]ConfigMapRef, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Customization. +func (in *Customization) DeepCopy() *Customization { + if in == nil { + return nil + } + out := new(Customization) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GroovyScripts) DeepCopyInto(out *GroovyScripts) { + *out = *in + in.Customization.DeepCopyInto(&out.Customization) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GroovyScripts. +func (in *GroovyScripts) DeepCopy() *GroovyScripts { + if in == nil { + return nil + } + out := new(GroovyScripts) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Handler) DeepCopyInto(out *Handler) { *out = *in @@ -277,6 +365,8 @@ func (in *JenkinsSpec) DeepCopyInto(out *JenkinsSpec) { in.SlaveService.DeepCopyInto(&out.SlaveService) in.Backup.DeepCopyInto(&out.Backup) in.Restore.DeepCopyInto(&out.Restore) + in.GroovyScripts.DeepCopyInto(&out.GroovyScripts) + in.ConfigurationAsCode.DeepCopyInto(&out.ConfigurationAsCode) return } @@ -317,6 +407,11 @@ func (in *JenkinsStatus) DeepCopyInto(out *JenkinsStatus) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.AppliedGroovyScripts != nil { + in, out := &in.AppliedGroovyScripts, &out.AppliedGroovyScripts + *out = make([]AppliedGroovyScript, len(*in)) + copy(*out, *in) + } return } @@ -363,6 +458,22 @@ func (in *Restore) DeepCopy() *Restore { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretRef) DeepCopyInto(out *SecretRef) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretRef. +func (in *SecretRef) DeepCopy() *SecretRef { + if in == nil { + return nil + } + out := new(SecretRef) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SeedJob) DeepCopyInto(out *SeedJob) { *out = *in From 3b2a65a57ac398d3cd5ca3c6631f2127980de94a Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Mon, 8 Jul 2019 15:37:06 +0200 Subject: [PATCH 16/59] Removed volumes in e2e tests --- test/e2e/jenkins.go | 1 - 1 file changed, 1 deletion(-) diff --git a/test/e2e/jenkins.go b/test/e2e/jenkins.go index b62e2930..a4085408 100644 --- a/test/e2e/jenkins.go +++ b/test/e2e/jenkins.go @@ -126,7 +126,6 @@ func createJenkinsCR(t *testing.T, name, namespace string, seedJob *[]v1alpha2.S {Name: "simple-theme-plugin", Version: "0.5.1"}, }, NodeSelector: map[string]string{"kubernetes.io/hostname": "minikube"}, - Volumes: volumes, }, SeedJobs: seedJobs, }, From 6d0704ca33059d6f31061b61b4949de61eff36f6 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Tue, 9 Jul 2019 09:26:58 +0200 Subject: [PATCH 17/59] Fixed staticcheck and lint for macOS --- Makefile | 9 +++++---- go.mod | 8 ++++++++ go.sum | 20 ++++++++++++++++++++ test/e2e/configuration_test.go | 6 ++---- test/e2e/jenkins.go | 4 ++-- 5 files changed, 37 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index 79cbf0b6..3b305951 100644 --- a/Makefile +++ b/Makefile @@ -196,13 +196,14 @@ vet: ## Verifies `go vet` passes .PHONY: staticcheck HAS_STATICCHECK := $(shell which staticcheck) +PLATFORM = $(shell echo $(UNAME_S) | tr A-Z a-z) staticcheck: ## Verifies `staticcheck` passes @echo "+ $@" ifndef HAS_STATICCHECK - wget https://github.com/dominikh/go-tools/releases/download/2019.1.1/staticcheck_linux_amd64 - chmod +x staticcheck_linux_amd64 - mkdir -p $(HOME)/bin - mv staticcheck_linux_amd64 $(HOME)/bin/staticcheck + wget https://github.com/dominikh/go-tools/releases/download/2019.1.1/staticcheck_$(PLATFORM)_amd64 + chmod +x staticcheck_$(PLATFORM)_amd64 + mkdir -p $(GOPATH)/bin + mv staticcheck_$(PLATFORM)_amd64 $(GOPATH)/bin/staticcheck endif @staticcheck $(PACKAGES) diff --git a/go.mod b/go.mod index 5690bdc1..8e1b2efd 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/go-logr/zapr v0.1.0 github.com/go-openapi/spec v0.18.0 github.com/golang/groupcache v0.0.0-20180924190550-6f2cf27854a4 // indirect + github.com/golang/lint v0.0.0-20190409202823-959b441ac422 // indirect github.com/golang/mock v1.2.0 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect github.com/google/uuid v1.0.0 // indirect @@ -34,7 +35,12 @@ require ( go.uber.org/atomic v1.3.2 // indirect go.uber.org/multierr v1.1.0 // indirect go.uber.org/zap v1.9.1 + golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 // indirect + golang.org/x/net v0.0.0-20190628185345-da137c7871d7 // indirect + golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb // indirect + golang.org/x/text v0.3.2 // indirect golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 // indirect + golang.org/x/tools v0.0.0-20190708203411-c8855242db9c // indirect k8s.io/api v0.0.0-20190222213804-5cb15d344471 k8s.io/apimachinery v0.0.0-20190221213512-86fb29eff628 k8s.io/client-go v2.0.0-alpha.0.0.20181126152608-d082d5923d3c+incompatible @@ -62,3 +68,5 @@ replace ( sigs.k8s.io/controller-runtime => sigs.k8s.io/controller-runtime v0.1.10 sigs.k8s.io/controller-tools => sigs.k8s.io/controller-tools v0.1.11-0.20190411181648-9d55346c2bde ) + +replace github.com/golang/lint v0.0.0-20190409202823-959b441ac422 => golang.org/x/lint v0.0.0-20190409202823-959b441ac422 diff --git a/go.sum b/go.sum index 6a041cfd..5cee9964 100644 --- a/go.sum +++ b/go.sum @@ -194,12 +194,17 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f h1:hX65Cu3JDlGH3uEdK7I99Ii+9kjD6mvnnpfLdEAH0x4= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422 h1:QzoH/1pFpZguR8NrRHLcO6jKqfv2zpuSqZLgdm7ZmjI= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -213,6 +218,10 @@ golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI= @@ -222,6 +231,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -232,18 +243,27 @@ golang.org/x/sys v0.0.0-20181218192612-074acd46bca6/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb h1:fgwFCsaw9buMuxNd6+DQfAuSFqbNiQZpcgJQAgJsK6k= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 h1:+DCIGbF/swA92ohVg0//6X2IVY3KZs6p9mix0ziNYJM= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181219222714-6e267b5cc78e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190213015956-f7e1b50d2251/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138 h1:H3uGjxCR/6Ds0Mjgyp7LMK81+LvmbvWWEnJhzk1Pi9E= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190708203411-c8855242db9c h1:rRFNgkkT7zOyWlroLBmsrKYtBNhox8WtulQlOr3jIDk= +golang.org/x/tools v0.0.0-20190708203411-c8855242db9c/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= google.golang.org/api v0.0.0-20181220000619-583d854617af/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.2.0 h1:B5VXkdjt7K2Gm6fGBC9C9a1OAKJDT95cTqwet+2zib0= google.golang.org/api v0.2.0/go.mod h1:IfRCZScioGtypHNTlz3gFk67J8uePVW7uDTBzXuIkhU= diff --git a/test/e2e/configuration_test.go b/test/e2e/configuration_test.go index fbc5fa7f..7f2aa58d 100644 --- a/test/e2e/configuration_test.go +++ b/test/e2e/configuration_test.go @@ -45,17 +45,16 @@ func TestConfiguration(t *testing.T) { Configurations: []v1alpha2.ConfigMapRef{ {userConfigurationConfigMapName}, }, - Secret:v1alpha2.SecretRef{userConfigurationSecretName }, + Secret: v1alpha2.SecretRef{userConfigurationSecretName}, }, } - casc := v1alpha2.ConfigurationAsCode{ Customization: v1alpha2.Customization{ Configurations: []v1alpha2.ConfigMapRef{ {userConfigurationConfigMapName}, }, - Secret:v1alpha2.SecretRef{userConfigurationSecretName }, + Secret: v1alpha2.SecretRef{userConfigurationSecretName}, }, } @@ -86,7 +85,6 @@ func verifyPodPropagation(t *testing.T, jenkins *v1alpha2.Jenkins) { assert.Equal(t, jenkins.Spec.Master.Containers[0].Command, jenkinsPod.Spec.Containers[0].Command) } - func createUserConfigurationSecret(t *testing.T, namespace string, systemMessageEnvName, systemMessage string) { userConfiguration := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ diff --git a/test/e2e/jenkins.go b/test/e2e/jenkins.go index a4085408..f6e91176 100644 --- a/test/e2e/jenkins.go +++ b/test/e2e/jenkins.go @@ -78,8 +78,8 @@ func createJenkinsCR(t *testing.T, name, namespace string, seedJob *[]v1alpha2.S Namespace: namespace, }, Spec: v1alpha2.JenkinsSpec{ - GroovyScripts:groovyScripts, - ConfigurationAsCode:casc, + GroovyScripts: groovyScripts, + ConfigurationAsCode: casc, Master: v1alpha2.JenkinsMaster{ Annotations: map[string]string{"test": "label"}, Containers: []v1alpha2.Container{ From 38834ceef478d7e0ba8bc9d71f8d2d3d900e1230 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Tue, 9 Jul 2019 09:31:19 +0200 Subject: [PATCH 18/59] Fixed vet errors --- test/e2e/configuration_test.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/test/e2e/configuration_test.go b/test/e2e/configuration_test.go index 7f2aa58d..2974e8b9 100644 --- a/test/e2e/configuration_test.go +++ b/test/e2e/configuration_test.go @@ -43,18 +43,26 @@ func TestConfiguration(t *testing.T) { groovyScripts := v1alpha2.GroovyScripts{ Customization: v1alpha2.Customization{ Configurations: []v1alpha2.ConfigMapRef{ - {userConfigurationConfigMapName}, + { + Name: userConfigurationConfigMapName, + }, + }, + Secret: v1alpha2.SecretRef{ + Name: userConfigurationSecretName, }, - Secret: v1alpha2.SecretRef{userConfigurationSecretName}, }, } casc := v1alpha2.ConfigurationAsCode{ Customization: v1alpha2.Customization{ Configurations: []v1alpha2.ConfigMapRef{ - {userConfigurationConfigMapName}, + { + Name: userConfigurationConfigMapName, + }, + }, + Secret: v1alpha2.SecretRef{ + Name: userConfigurationSecretName, }, - Secret: v1alpha2.SecretRef{userConfigurationSecretName}, }, } From 21e44049b598f344e006c641b4f98bb614d63e3b Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Tue, 9 Jul 2019 10:07:04 +0200 Subject: [PATCH 19/59] Fixed go.mod dependencies --- go.mod | 1 - 1 file changed, 1 deletion(-) diff --git a/go.mod b/go.mod index 8e1b2efd..7c5a3bdd 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,6 @@ require ( github.com/go-logr/zapr v0.1.0 github.com/go-openapi/spec v0.18.0 github.com/golang/groupcache v0.0.0-20180924190550-6f2cf27854a4 // indirect - github.com/golang/lint v0.0.0-20190409202823-959b441ac422 // indirect github.com/golang/mock v1.2.0 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect github.com/google/uuid v1.0.0 // indirect From cc32111a511d2daeb5013d4b956205069d36ab32 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Tue, 9 Jul 2019 10:34:45 +0200 Subject: [PATCH 20/59] Removed build goal from minikube-run --- Makefile | 2 +- deploy/operator.yaml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 3b305951..64470b52 100644 --- a/Makefile +++ b/Makefile @@ -340,7 +340,7 @@ docker-run: ## Run the container in docker, you can use EXTRA_ARGS .PHONY: minikube-run minikube-run: export WATCH_NAMESPACE = $(NAMESPACE) minikube-run: export OPERATOR_NAME = $(NAME) -minikube-run: minikube-start build ## Run the operator locally and use minikube as Kubernetes cluster, you can use EXTRA_ARGS +minikube-run: minikube-start ## Run the operator locally and use minikube as Kubernetes cluster, you can use EXTRA_ARGS @echo "+ $@" kubectl config use-context minikube kubectl apply -f deploy/crds/jenkins_$(API_VERSION)_jenkins_crd.yaml diff --git a/deploy/operator.yaml b/deploy/operator.yaml index 64890cee..18fe3b29 100644 --- a/deploy/operator.yaml +++ b/deploy/operator.yaml @@ -32,3 +32,4 @@ spec: fieldPath: metadata.name - name: OPERATOR_NAME value: "jenkins-operator" + - From 99e230826470773fdb8a37a9d0566ab279dc41e9 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Tue, 9 Jul 2019 11:46:26 +0200 Subject: [PATCH 21/59] Improved Makefile and Dockerfile --- Dockerfile | 1 - Makefile | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 01cc30cc..79c1371e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -42,7 +42,6 @@ RUN curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s RUN export GO111MODULE=auto RUN mkdir -p $GOPATH/src/github.com/jenkinsci/kubernetes-operator -ADD . $GOPATH/src/github.com/jenkinsci/kubernetes-operator WORKDIR $GOPATH/src/github.com/jenkinsci/kubernetes-operator RUN mkdir -p /home/builder diff --git a/Makefile b/Makefile index 64470b52..772f5ad8 100644 --- a/Makefile +++ b/Makefile @@ -416,19 +416,19 @@ endif @echo .PHONY: image -image: ## Create the docker image from the Dockerfile +image: ## Create the docker image from the Dockerfile. This image is used to build linux binary regardless of the system on the host @echo "+ $@" docker build --rm --force-rm --no-cache \ --build-arg GO_VERSION=$(GO_VERSION) \ --build-arg MINIKUBE_VERSION=$(MINIKUBE_VERSION) \ --build-arg OPERATOR_SDK_VERSION=$(OPERATOR_SDK_VERSION) \ - -t infrastructure/runner . + -t jenkins-operator/runner . .PHONY: indocker PWD := $(shell pwd) DOCKER_HOST_IP := $(shell minikube docker-env | grep DOCKER_HOST | cut -d '"' -f 2) MINIKUBE_IP := $(shell minikube ip) -indocker: image ## Run make in a docker container +indocker: minikube-run image ## Run make in a docker container @echo "+ $@" docker run --rm -it $(DOCKER_FLAGS) \ -v /var/run/docker.sock:/var/run/docker.sock \ @@ -437,4 +437,4 @@ indocker: image ## Run make in a docker container --mount type=bind,source=$(HOME)/.kube,target=/home/builder/.kube \ -e DOCKER_HOST=$(DOCKER_HOST_IP) \ -e MINIKUBE_IP=$(MINIKUBE_IP) \ - infrastructure/runner + jenkins-operator/runner From 9654d66fdc7026411cf1b9213642ec3f75669ea3 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Tue, 9 Jul 2019 11:47:44 +0200 Subject: [PATCH 22/59] Moved pod propagation tests to verifyJenkinsMasterPodAttributes --- test/e2e/configuration_test.go | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/test/e2e/configuration_test.go b/test/e2e/configuration_test.go index 2974e8b9..a50eb293 100644 --- a/test/e2e/configuration_test.go +++ b/test/e2e/configuration_test.go @@ -85,14 +85,6 @@ func TestConfiguration(t *testing.T) { verifyJenkinsSeedJobs(t, client, []seedJobConfig{mySeedJob}) } -func verifyPodPropagation(t *testing.T, jenkins *v1alpha2.Jenkins) { - jenkinsPod := getJenkinsMasterPod(t, jenkins) - jenkins = getJenkins(t, jenkins.Namespace, jenkins.Name) - - assert.Equal(t, jenkins.Spec.Master.SecurityContext, jenkinsPod.Spec.SecurityContext) - assert.Equal(t, jenkins.Spec.Master.Containers[0].Command, jenkinsPod.Spec.Containers[0].Command) -} - func createUserConfigurationSecret(t *testing.T, namespace string, systemMessageEnvName, systemMessage string) { userConfiguration := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -176,6 +168,9 @@ func verifyJenkinsMasterPodAttributes(t *testing.T, jenkins *v1alpha2.Jenkins) { assert.Equal(t, resources.JenkinsMasterContainerName, jenkinsPod.Spec.Containers[0].Name) assert.Equal(t, len(jenkins.Spec.Master.Containers), len(jenkinsPod.Spec.Containers)) + + assert.Equal(t, jenkins.Spec.Master.SecurityContext, jenkinsPod.Spec.SecurityContext) + assert.Equal(t, jenkins.Spec.Master.Containers[0].Command, jenkinsPod.Spec.Containers[0].Command) for _, actualContainer := range jenkinsPod.Spec.Containers { if actualContainer.Name == resources.JenkinsMasterContainerName { From 55601886c1ce73a79397c641426289af1fc12c91 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Tue, 9 Jul 2019 15:00:25 +0200 Subject: [PATCH 23/59] Removed build goal from e2e tests --- Makefile | 6 +++--- deploy/operator.yaml | 2 +- test/e2e/configuration_test.go | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 772f5ad8..61b0bf0f 100644 --- a/Makefile +++ b/Makefile @@ -162,7 +162,7 @@ prepare-all-in-one-deploy-file: ## Prepares all in one deploy file .PHONY: e2e CURRENT_DIRECTORY := $(shell pwd) -e2e: build docker-build ## Runs e2e tests, you can use EXTRA_ARGS +e2e: docker-build ## Runs e2e tests, you can use EXTRA_ARGS @echo "+ $@" @echo "Docker image: $(DOCKER_REGISTRY):$(GITCOMMIT)" kubectl config use-context $(KUBECTL_CONTEXT) @@ -291,7 +291,7 @@ docker-login: ## Log in into the Docker repository @echo "+ $@" .PHONY: docker-build -docker-build: check-env build ## Build the container +docker-build: check-env ## Build the container @echo "+ $@" docker build . -t $(DOCKER_REGISTRY):$(GITCOMMIT) --file build/Dockerfile @@ -428,7 +428,7 @@ image: ## Create the docker image from the Dockerfile. This image is used to bui PWD := $(shell pwd) DOCKER_HOST_IP := $(shell minikube docker-env | grep DOCKER_HOST | cut -d '"' -f 2) MINIKUBE_IP := $(shell minikube ip) -indocker: minikube-run image ## Run make in a docker container +indocker: minikube-start image ## Run make in a docker container @echo "+ $@" docker run --rm -it $(DOCKER_FLAGS) \ -v /var/run/docker.sock:/var/run/docker.sock \ diff --git a/deploy/operator.yaml b/deploy/operator.yaml index 18fe3b29..0245cfbe 100644 --- a/deploy/operator.yaml +++ b/deploy/operator.yaml @@ -32,4 +32,4 @@ spec: fieldPath: metadata.name - name: OPERATOR_NAME value: "jenkins-operator" - - + diff --git a/test/e2e/configuration_test.go b/test/e2e/configuration_test.go index a50eb293..44d641b8 100644 --- a/test/e2e/configuration_test.go +++ b/test/e2e/configuration_test.go @@ -77,7 +77,6 @@ func TestConfiguration(t *testing.T) { verifyJenkinsMasterPodAttributes(t, jenkins) client := verifyJenkinsAPIConnection(t, jenkins) verifyPlugins(t, client, jenkins) - verifyPodPropagation(t, jenkins) // user waitForJenkinsUserConfigurationToComplete(t, jenkins) From 01655785b5c615070850f7a732c8013b3072ee61 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Wed, 10 Jul 2019 13:58:18 +0200 Subject: [PATCH 24/59] #43 Add support for JENKINS_OPTS, fix the --prefix bug --- .../jenkins/configuration/base/reconcile.go | 28 ++++++++++++++ .../configuration/base/reconcile_test.go | 38 +++++++++++++++++++ pkg/controller/jenkins/jenkins_controller.go | 6 +-- test/e2e/configuration_test.go | 2 +- 4 files changed, 69 insertions(+), 5 deletions(-) diff --git a/pkg/controller/jenkins/configuration/base/reconcile.go b/pkg/controller/jenkins/configuration/base/reconcile.go index 1efd89f8..024b85f3 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile.go +++ b/pkg/controller/jenkins/configuration/base/reconcile.go @@ -127,6 +127,29 @@ func (r *ReconcileJenkinsBaseConfiguration) Reconcile() (reconcile.Result, jenki return result, jenkinsClient, err } +// GetJenkinsOpts put container JENKINS_OPTS env parameters in map and returns it +func GetJenkinsOpts(jenkins *v1alpha2.Jenkins) map[string]string { + envs := jenkins.Spec.Master.Containers[0].Env + jenkinsOpts := make(map[string]string) + + for k, v := range envs { + if v.Name == "JENKINS_OPTS" { + jenkinsOptsEnv := envs[k] + jenkinsOptsWithDashes := jenkinsOptsEnv.Value + jenkinsOptsWithDashes = strings.ReplaceAll(jenkinsOptsWithDashes, "--", "") // Remove dashes + jenkinsOptsWithEqOperators := strings.Split(jenkinsOptsWithDashes, " ") + + for _, vx := range jenkinsOptsWithEqOperators { + opt := strings.Split(vx, "=") + jenkinsOpts[opt[0]] = opt[1] + } + + return jenkinsOpts + } + } + return nil +} + func (r *ReconcileJenkinsBaseConfiguration) ensureResourcesRequiredForJenkinsPod(metaObject metav1.ObjectMeta) error { if err := r.createOperatorCredentialsSecret(metaObject); err != nil { return err @@ -748,6 +771,11 @@ func (r *ReconcileJenkinsBaseConfiguration) waitForJenkins(meta metav1.ObjectMet func (r *ReconcileJenkinsBaseConfiguration) ensureJenkinsClient(meta metav1.ObjectMeta) (jenkinsclient.Jenkins, error) { jenkinsURL, err := jenkinsclient.BuildJenkinsAPIUrl( r.jenkins.ObjectMeta.Namespace, resources.GetJenkinsHTTPServiceName(r.jenkins), r.jenkins.Spec.Service.Port, r.local, r.minikube) + + if prefix, ok := GetJenkinsOpts(r.jenkins)["prefix"]; ok { + jenkinsURL = jenkinsURL + prefix + } + if err != nil { return nil, err } diff --git a/pkg/controller/jenkins/configuration/base/reconcile_test.go b/pkg/controller/jenkins/configuration/base/reconcile_test.go index 15073db1..478f5b06 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile_test.go +++ b/pkg/controller/jenkins/configuration/base/reconcile_test.go @@ -14,6 +14,44 @@ import ( corev1 "k8s.io/api/core/v1" ) +func TestGetJenkinsOpts(t *testing.T) { + envs := []corev1.EnvVar{ + {Name: "JENKINS_OPTS", Value: "--prefix=/jenkins --httpPort=8080"}, + } + + jenkins := &v1alpha2.Jenkins{ + Spec: v1alpha2.JenkinsSpec{ + Master: v1alpha2.JenkinsMaster{ + Containers: []v1alpha2.Container{ + { + Env: envs, + }, + }, + }, + }, + } + + opts := GetJenkinsOpts(jenkins) + + t.Run("equal env vars", func(t *testing.T) { + assert.Equal(t, opts["prefix"], "/jenkins") + assert.Equal(t, opts["httpPort"], "8080") + }) + + t.Run("not equal env vars", func(t *testing.T) { + assert.NotEqual(t, opts["prefix"], "/jenkins_not_equal") + assert.NotEqual(t, opts["httpPort"], "80808") + }) + + t.Run("should exists", func(t *testing.T) { + assert.Contains(t, opts, "httpPort") + }) + + t.Run("should exists", func(t *testing.T) { + assert.NotContains(t, opts, "should_not_exists") + }) +} + func TestCompareContainerVolumeMounts(t *testing.T) { t.Run("happy with service account", func(t *testing.T) { expectedContainer := corev1.Container{ diff --git a/pkg/controller/jenkins/jenkins_controller.go b/pkg/controller/jenkins/jenkins_controller.go index ea56a719..83e6ec92 100644 --- a/pkg/controller/jenkins/jenkins_controller.go +++ b/pkg/controller/jenkins/jenkins_controller.go @@ -3,8 +3,7 @@ package jenkins import ( "context" "fmt" - "reflect" - + "github.com/go-logr/logr" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base" @@ -16,8 +15,6 @@ import ( "github.com/jenkinsci/kubernetes-operator/pkg/event" "github.com/jenkinsci/kubernetes-operator/pkg/log" "github.com/jenkinsci/kubernetes-operator/version" - - "github.com/go-logr/logr" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -27,6 +24,7 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" + "reflect" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/handler" diff --git a/test/e2e/configuration_test.go b/test/e2e/configuration_test.go index 44d641b8..a3d06361 100644 --- a/test/e2e/configuration_test.go +++ b/test/e2e/configuration_test.go @@ -167,7 +167,7 @@ func verifyJenkinsMasterPodAttributes(t *testing.T, jenkins *v1alpha2.Jenkins) { assert.Equal(t, resources.JenkinsMasterContainerName, jenkinsPod.Spec.Containers[0].Name) assert.Equal(t, len(jenkins.Spec.Master.Containers), len(jenkinsPod.Spec.Containers)) - + assert.Equal(t, jenkins.Spec.Master.SecurityContext, jenkinsPod.Spec.SecurityContext) assert.Equal(t, jenkins.Spec.Master.Containers[0].Command, jenkinsPod.Spec.Containers[0].Command) From 9e73be7a490a93d6c7f07b4f9503d61297fa0afc Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Wed, 10 Jul 2019 15:19:26 +0200 Subject: [PATCH 25/59] #43 Improve tests and fix GetJenkinsOpts --- .../jenkins/configuration/base/reconcile.go | 7 +- .../configuration/base/reconcile_test.go | 91 ++++++++++++++----- pkg/controller/jenkins/jenkins_controller.go | 6 +- 3 files changed, 75 insertions(+), 29 deletions(-) diff --git a/pkg/controller/jenkins/configuration/base/reconcile.go b/pkg/controller/jenkins/configuration/base/reconcile.go index 024b85f3..1709e37d 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile.go +++ b/pkg/controller/jenkins/configuration/base/reconcile.go @@ -136,12 +136,15 @@ func GetJenkinsOpts(jenkins *v1alpha2.Jenkins) map[string]string { if v.Name == "JENKINS_OPTS" { jenkinsOptsEnv := envs[k] jenkinsOptsWithDashes := jenkinsOptsEnv.Value - jenkinsOptsWithDashes = strings.ReplaceAll(jenkinsOptsWithDashes, "--", "") // Remove dashes + if len(jenkinsOptsWithDashes) == 0 { + return nil + } + jenkinsOptsWithEqOperators := strings.Split(jenkinsOptsWithDashes, " ") for _, vx := range jenkinsOptsWithEqOperators { opt := strings.Split(vx, "=") - jenkinsOpts[opt[0]] = opt[1] + jenkinsOpts[strings.ReplaceAll(opt[0], "--", "")] = opt[1] } return jenkinsOpts diff --git a/pkg/controller/jenkins/configuration/base/reconcile_test.go b/pkg/controller/jenkins/configuration/base/reconcile_test.go index 478f5b06..d8599f81 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile_test.go +++ b/pkg/controller/jenkins/configuration/base/reconcile_test.go @@ -15,40 +15,81 @@ import ( ) func TestGetJenkinsOpts(t *testing.T) { - envs := []corev1.EnvVar{ - {Name: "JENKINS_OPTS", Value: "--prefix=/jenkins --httpPort=8080"}, - } - - jenkins := &v1alpha2.Jenkins{ - Spec: v1alpha2.JenkinsSpec{ - Master: v1alpha2.JenkinsMaster{ - Containers: []v1alpha2.Container{ - { - Env: envs, + t.Run("JENKINS_OPTS is empty", func(t *testing.T) { + jenkins := &v1alpha2.Jenkins{ + Spec: v1alpha2.JenkinsSpec{ + Master: v1alpha2.JenkinsMaster{ + Containers: []v1alpha2.Container{ + { + Env: []corev1.EnvVar{ + {Name: "JENKINS_OPTS", Value: ""}, + }, + }, }, }, }, - }, - } + } - opts := GetJenkinsOpts(jenkins) - - t.Run("equal env vars", func(t *testing.T) { - assert.Equal(t, opts["prefix"], "/jenkins") - assert.Equal(t, opts["httpPort"], "8080") + opts := GetJenkinsOpts(jenkins) + assert.Equal(t, 0, len(opts)) }) - t.Run("not equal env vars", func(t *testing.T) { - assert.NotEqual(t, opts["prefix"], "/jenkins_not_equal") - assert.NotEqual(t, opts["httpPort"], "80808") + t.Run("JENKINS_OPTS have --prefix argument ", func(t *testing.T) { + jenkins := &v1alpha2.Jenkins{ + Spec: v1alpha2.JenkinsSpec{ + Master: v1alpha2.JenkinsMaster{ + Containers: []v1alpha2.Container{ + { + Env: []corev1.EnvVar{ + {Name: "JENKINS_OPTS", Value: "--prefix=/jenkins"}, + }, + }, + }, + }, + }, + } + + opts := GetJenkinsOpts(jenkins) + + assert.Equal(t, 1, len(opts)) + + t.Run("ensure that JENKINS_OPTS not contains --httpPort", func(t *testing.T) { + assert.NotContains(t, opts, "httpPort") + }) + + t.Run("ensure that argument is --prefix", func(t *testing.T) { + assert.Contains(t, opts, "prefix") + }) }) - t.Run("should exists", func(t *testing.T) { - assert.Contains(t, opts, "httpPort") - }) + t.Run("JENKINS_OPTS have --prefix and --httpPort argument", func(t *testing.T) { + jenkins := &v1alpha2.Jenkins{ + Spec: v1alpha2.JenkinsSpec{ + Master: v1alpha2.JenkinsMaster{ + Containers: []v1alpha2.Container{ + { + Env: []corev1.EnvVar{ + {Name: "JENKINS_OPTS", Value: "--prefix=/jenkins --httpPort=8080"}, + }, + }, + }, + }, + }, + } - t.Run("should exists", func(t *testing.T) { - assert.NotContains(t, opts, "should_not_exists") + opts := GetJenkinsOpts(jenkins) + + assert.Equal(t, 2, len(opts)) + + t.Run("ensure that argument is --prefix with value /jenkins", func(t *testing.T) { + assert.Contains(t, opts, "prefix") + assert.Equal(t, opts["prefix"], "/jenkins") + }) + + t.Run("ensure that argument is --httpPort with value 8080", func(t *testing.T) { + assert.Contains(t, opts, "httpPort") + assert.Equal(t, opts["httpPort"], "8080") + }) }) } diff --git a/pkg/controller/jenkins/jenkins_controller.go b/pkg/controller/jenkins/jenkins_controller.go index 83e6ec92..ea56a719 100644 --- a/pkg/controller/jenkins/jenkins_controller.go +++ b/pkg/controller/jenkins/jenkins_controller.go @@ -3,7 +3,8 @@ package jenkins import ( "context" "fmt" - "github.com/go-logr/logr" + "reflect" + "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base" @@ -15,6 +16,8 @@ import ( "github.com/jenkinsci/kubernetes-operator/pkg/event" "github.com/jenkinsci/kubernetes-operator/pkg/log" "github.com/jenkinsci/kubernetes-operator/version" + + "github.com/go-logr/logr" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -24,7 +27,6 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" - "reflect" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/handler" From 076b0aa453db76410c94172190162780a34309a4 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Wed, 10 Jul 2019 15:49:53 +0200 Subject: [PATCH 26/59] #43 Add additional test scenarios --- .../jenkins/configuration/base/reconcile.go | 2 +- .../configuration/base/reconcile_test.go | 65 ++++++++++++++----- 2 files changed, 50 insertions(+), 17 deletions(-) diff --git a/pkg/controller/jenkins/configuration/base/reconcile.go b/pkg/controller/jenkins/configuration/base/reconcile.go index 1709e37d..7cb591a4 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile.go +++ b/pkg/controller/jenkins/configuration/base/reconcile.go @@ -139,7 +139,7 @@ func GetJenkinsOpts(jenkins *v1alpha2.Jenkins) map[string]string { if len(jenkinsOptsWithDashes) == 0 { return nil } - + jenkinsOptsWithEqOperators := strings.Split(jenkinsOptsWithDashes, " ") for _, vx := range jenkinsOptsWithEqOperators { diff --git a/pkg/controller/jenkins/configuration/base/reconcile_test.go b/pkg/controller/jenkins/configuration/base/reconcile_test.go index d8599f81..80b313db 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile_test.go +++ b/pkg/controller/jenkins/configuration/base/reconcile_test.go @@ -15,6 +15,25 @@ import ( ) func TestGetJenkinsOpts(t *testing.T) { + t.Run("JENKINS_OPTS is uninitialized", func(t *testing.T) { + jenkins := &v1alpha2.Jenkins{ + Spec: v1alpha2.JenkinsSpec{ + Master: v1alpha2.JenkinsMaster{ + Containers: []v1alpha2.Container{ + { + Env: []corev1.EnvVar{ + {Name: "", Value: ""}, + }, + }, + }, + }, + }, + } + + opts := GetJenkinsOpts(jenkins) + assert.Equal(t, 0, len(opts)) + }) + t.Run("JENKINS_OPTS is empty", func(t *testing.T) { jenkins := &v1alpha2.Jenkins{ Spec: v1alpha2.JenkinsSpec{ @@ -52,14 +71,9 @@ func TestGetJenkinsOpts(t *testing.T) { opts := GetJenkinsOpts(jenkins) assert.Equal(t, 1, len(opts)) - - t.Run("ensure that JENKINS_OPTS not contains --httpPort", func(t *testing.T) { - assert.NotContains(t, opts, "httpPort") - }) - - t.Run("ensure that argument is --prefix", func(t *testing.T) { - assert.Contains(t, opts, "prefix") - }) + assert.NotContains(t, opts, "httpPort") + assert.Contains(t, opts, "prefix") + assert.Equal(t, opts["prefix"], "/jenkins") }) t.Run("JENKINS_OPTS have --prefix and --httpPort argument", func(t *testing.T) { @@ -81,15 +95,34 @@ func TestGetJenkinsOpts(t *testing.T) { assert.Equal(t, 2, len(opts)) - t.Run("ensure that argument is --prefix with value /jenkins", func(t *testing.T) { - assert.Contains(t, opts, "prefix") - assert.Equal(t, opts["prefix"], "/jenkins") - }) + assert.Contains(t, opts, "prefix") + assert.Equal(t, opts["prefix"], "/jenkins") - t.Run("ensure that argument is --httpPort with value 8080", func(t *testing.T) { - assert.Contains(t, opts, "httpPort") - assert.Equal(t, opts["httpPort"], "8080") - }) + assert.Contains(t, opts, "httpPort") + assert.Equal(t, opts["httpPort"], "8080") + }) + + t.Run("JENKINS_OPTS have --httpPort argument", func(t *testing.T) { + jenkins := &v1alpha2.Jenkins{ + Spec: v1alpha2.JenkinsSpec{ + Master: v1alpha2.JenkinsMaster{ + Containers: []v1alpha2.Container{ + { + Env: []corev1.EnvVar{ + {Name: "JENKINS_OPTS", Value: "--httpPort=8080"}, + }, + }, + }, + }, + }, + } + + opts := GetJenkinsOpts(jenkins) + + assert.Equal(t, 2, len(opts)) + assert.NotContains(t, opts, "prefix") + assert.Contains(t, opts, "httpPort") + assert.Equal(t, opts["httpPort"], "8080") }) } From 164fe30aef4e7680caee3b485f8d5cd351b340f1 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Wed, 10 Jul 2019 16:08:09 +0200 Subject: [PATCH 27/59] #43 Fix tests --- pkg/controller/jenkins/configuration/base/reconcile_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/controller/jenkins/configuration/base/reconcile_test.go b/pkg/controller/jenkins/configuration/base/reconcile_test.go index 80b313db..82386155 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile_test.go +++ b/pkg/controller/jenkins/configuration/base/reconcile_test.go @@ -119,7 +119,7 @@ func TestGetJenkinsOpts(t *testing.T) { opts := GetJenkinsOpts(jenkins) - assert.Equal(t, 2, len(opts)) + assert.Equal(t, 1, len(opts)) assert.NotContains(t, opts, "prefix") assert.Contains(t, opts, "httpPort") assert.Equal(t, opts["httpPort"], "8080") From e049218b78a6e64d3d0e5f4458ac51fd85928213 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Fri, 12 Jul 2019 13:11:42 +0200 Subject: [PATCH 28/59] #45 Add support for imagePullSecrets parameter --- pkg/apis/jenkins/v1alpha2/jenkins_types.go | 7 + .../jenkins/v1alpha2/zz_generated.deepcopy.go | 5 + .../jenkins/configuration/base/reconcile.go | 6 + .../configuration/base/reconcile_test.go | 23 +++ .../configuration/base/resources/pod.go | 1 + .../jenkins/configuration/base/validate.go | 41 ++++ .../configuration/base/validate_test.go | 193 ++++++++++++++++++ 7 files changed, 276 insertions(+) diff --git a/pkg/apis/jenkins/v1alpha2/jenkins_types.go b/pkg/apis/jenkins/v1alpha2/jenkins_types.go index abc35481..a961b7a9 100644 --- a/pkg/apis/jenkins/v1alpha2/jenkins_types.go +++ b/pkg/apis/jenkins/v1alpha2/jenkins_types.go @@ -210,6 +210,13 @@ type JenkinsMaster struct { // memory: 600Mi Containers []Container `json:"containers,omitempty"` + // ImagePullSecrets is an optional list of references to secrets in the same namespace to use for pulling any of the images used by this PodSpec. + // If specified, these secrets will be passed to individual puller implementations for them to use. For example, + // in the case of docker, only DockerConfig type secrets are honored. + // More info: https://kubernetes.io/docs/concepts/containers/images#specifying-imagepullsecrets-on-a-pod + // +optional + ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"` + // List of volumes that can be mounted by containers belonging to the pod. // More info: https://kubernetes.io/docs/concepts/storage/volumes // +optional diff --git a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go index ab88bd8c..33b5ff35 100644 --- a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go @@ -322,6 +322,11 @@ func (in *JenkinsMaster) DeepCopyInto(out *JenkinsMaster) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.ImagePullSecrets != nil { + in, out := &in.ImagePullSecrets, &out.ImagePullSecrets + *out = make([]v1.LocalObjectReference, len(*in)) + copy(*out, *in) + } if in.Volumes != nil { in, out := &in.Volumes, &out.Volumes *out = make([]v1.Volume, len(*in)) diff --git a/pkg/controller/jenkins/configuration/base/reconcile.go b/pkg/controller/jenkins/configuration/base/reconcile.go index 7cb591a4..ed8ac2f1 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile.go +++ b/pkg/controller/jenkins/configuration/base/reconcile.go @@ -527,6 +527,12 @@ func (r *ReconcileJenkinsBaseConfiguration) isRecreatePodNeeded(currentJenkinsMa return true } + if !reflect.DeepEqual(r.jenkins.Spec.Master.ImagePullSecrets, currentJenkinsMasterPod.Spec.ImagePullSecrets) { + r.logger.Info(fmt.Sprintf("Jenkins Pod ImagePullSecrets has changed, actual '%+v' required '%+v', recreating pod", + currentJenkinsMasterPod.Spec.ImagePullSecrets, r.jenkins.Spec.Master.ImagePullSecrets)) + return true + } + if !reflect.DeepEqual(r.jenkins.Spec.Master.NodeSelector, currentJenkinsMasterPod.Spec.NodeSelector) { r.logger.Info(fmt.Sprintf("Jenkins pod node selector has changed, actual '%+v' required '%+v', recreating pod", currentJenkinsMasterPod.Spec.NodeSelector, r.jenkins.Spec.Master.NodeSelector)) diff --git a/pkg/controller/jenkins/configuration/base/reconcile_test.go b/pkg/controller/jenkins/configuration/base/reconcile_test.go index 82386155..a81aaf06 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile_test.go +++ b/pkg/controller/jenkins/configuration/base/reconcile_test.go @@ -124,6 +124,29 @@ func TestGetJenkinsOpts(t *testing.T) { assert.Contains(t, opts, "httpPort") assert.Equal(t, opts["httpPort"], "8080") }) + + t.Run("JENKINS_OPTS have --httpPort=--8080 argument", func(t *testing.T) { + jenkins := &v1alpha2.Jenkins{ + Spec: v1alpha2.JenkinsSpec{ + Master: v1alpha2.JenkinsMaster{ + Containers: []v1alpha2.Container{ + { + Env: []corev1.EnvVar{ + {Name: "JENKINS_OPTS", Value: "--httpPort=--8080"}, + }, + }, + }, + }, + }, + } + + opts := GetJenkinsOpts(jenkins) + + assert.Equal(t, 1, len(opts)) + assert.NotContains(t, opts, "prefix") + assert.Contains(t, opts, "httpPort") + assert.Equal(t, opts["httpPort"], "--8080") + }) } func TestCompareContainerVolumeMounts(t *testing.T) { diff --git a/pkg/controller/jenkins/configuration/base/resources/pod.go b/pkg/controller/jenkins/configuration/base/resources/pod.go index 1a40a4b9..fe899575 100644 --- a/pkg/controller/jenkins/configuration/base/resources/pod.go +++ b/pkg/controller/jenkins/configuration/base/resources/pod.go @@ -288,6 +288,7 @@ func NewJenkinsMasterPod(objectMeta metav1.ObjectMeta, jenkins *v1alpha2.Jenkins Containers: newContainers(jenkins), Volumes: append(GetJenkinsMasterPodBaseVolumes(jenkins), jenkins.Spec.Master.Volumes...), SecurityContext: jenkins.Spec.Master.SecurityContext, + ImagePullSecrets: jenkins.Spec.Master.ImagePullSecrets, }, } } diff --git a/pkg/controller/jenkins/configuration/base/validate.go b/pkg/controller/jenkins/configuration/base/validate.go index f3de634a..b49f370c 100644 --- a/pkg/controller/jenkins/configuration/base/validate.go +++ b/pkg/controller/jenkins/configuration/base/validate.go @@ -2,6 +2,7 @@ package base import ( "context" + "errors" "fmt" "regexp" @@ -60,6 +61,46 @@ func (r *ReconcileJenkinsBaseConfiguration) Validate(jenkins *v1alpha2.Jenkins) return true, nil } +func (r *ReconcileJenkinsBaseConfiguration) validateImagePullSecrets() (bool, error) { + var err error + valid := true + ips := r.jenkins.Spec.Master.ImagePullSecrets + for _, sr := range ips { + valid, err = r.validateImagePullSecret(sr.Name) + if err != nil || !valid { + return valid, err + } + } + + return valid, err +} + +func (r *ReconcileJenkinsBaseConfiguration) validateImagePullSecret(name string) (bool, error) { + secret := &corev1.Secret{} + err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: r.jenkins.ObjectMeta.Namespace}, secret) + if err != nil && apierrors.IsNotFound(err) { + r.logger.V(log.VWarn).Info(fmt.Sprintf("Secret '%s' not found", name)) + return false, nil + } else if err != nil && !apierrors.IsNotFound(err) { + return false, stackerr.WithStack(err) + } + + if secret.Data["docker-server"] == nil { + return false, errors.New("docker server not set") + } + if secret.Data["docker-username"] == nil { + return false, errors.New("docker username not set") + } + if secret.Data["docker-password"] == nil { + return false, errors.New("docker password not set") + } + if secret.Data["docker-email"] == nil { + return false, errors.New("docker email not set") + } + + return true, nil +} + func (r *ReconcileJenkinsBaseConfiguration) validateVolumes() (bool, error) { valid := true for _, volume := range r.jenkins.Spec.Master.Volumes { diff --git a/pkg/controller/jenkins/configuration/base/validate_test.go b/pkg/controller/jenkins/configuration/base/validate_test.go index 07b7c208..cac463b2 100644 --- a/pkg/controller/jenkins/configuration/base/validate_test.go +++ b/pkg/controller/jenkins/configuration/base/validate_test.go @@ -132,6 +132,199 @@ func TestValidatePlugins(t *testing.T) { }) } +func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T) { + t.Run("happy", func(t *testing.T) { + lor := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ref", + }, + Data: map[string][]byte{ + "docker-server": []byte("test_server"), + "docker-username": []byte("test_user"), + "docker-password": []byte("test_password"), + "docker-email": []byte("test_email"), + }, + } + + jenkins := v1alpha2.Jenkins{ + Spec: v1alpha2.JenkinsSpec{ + Master: v1alpha2.JenkinsMaster{ + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: lor.ObjectMeta.Name}, + }, + }, + }, + } + + fakeClient := fake.NewFakeClient() + err := fakeClient.Create(context.TODO(), lor) + assert.NoError(t, err) + + baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), + &jenkins, false, false, nil, nil) + + got, err := baseReconcileLoop.validateImagePullSecrets() + assert.Equal(t, got, true) + assert.NoError(t, err) + }) + + t.Run("no secret", func(t *testing.T) { + jenkins := v1alpha2.Jenkins{ + Spec: v1alpha2.JenkinsSpec{ + Master: v1alpha2.JenkinsMaster{ + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: "test-ref"}, + }, + }, + }, + } + + fakeClient := fake.NewFakeClient() + + baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), + &jenkins, false, false, nil, nil) + + got, _ := baseReconcileLoop.validateImagePullSecrets() + assert.Equal(t, got, false) + }) + + t.Run("no docker email", func(t *testing.T) { + lor := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ref", + }, + Data: map[string][]byte{ + "docker-server": []byte("test_server"), + "docker-username": []byte("test_user"), + "docker-password": []byte("test_password"), + }, + } + + jenkins := v1alpha2.Jenkins{ + Spec: v1alpha2.JenkinsSpec{ + Master: v1alpha2.JenkinsMaster{ + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: lor.ObjectMeta.Name}, + }, + }, + }, + } + + fakeClient := fake.NewFakeClient() + err := fakeClient.Create(context.TODO(), lor) + assert.NoError(t, err) + + baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), + &jenkins, false, false, nil, nil) + + got, err := baseReconcileLoop.validateImagePullSecrets() + assert.Equal(t, got, false) + assert.Error(t, err) + }) + + t.Run("no docker password", func(t *testing.T) { + lor := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ref", + }, + Data: map[string][]byte{ + "docker-server": []byte("test_server"), + "docker-username": []byte("test_user"), + "docker-email": []byte("test_email"), + }, + } + + jenkins := v1alpha2.Jenkins{ + Spec: v1alpha2.JenkinsSpec{ + Master: v1alpha2.JenkinsMaster{ + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: lor.ObjectMeta.Name}, + }, + }, + }, + } + + fakeClient := fake.NewFakeClient() + err := fakeClient.Create(context.TODO(), lor) + assert.NoError(t, err) + + baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), + &jenkins, false, false, nil, nil) + + got, err := baseReconcileLoop.validateImagePullSecrets() + assert.Equal(t, got, false) + assert.Error(t, err) + }) + + t.Run("no docker username", func(t *testing.T) { + lor := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ref", + }, + Data: map[string][]byte{ + "docker-server": []byte("test_server"), + "docker-password": []byte("test_password"), + "docker-email": []byte("test_email"), + }, + } + + jenkins := v1alpha2.Jenkins{ + Spec: v1alpha2.JenkinsSpec{ + Master: v1alpha2.JenkinsMaster{ + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: lor.ObjectMeta.Name}, + }, + }, + }, + } + + fakeClient := fake.NewFakeClient() + err := fakeClient.Create(context.TODO(), lor) + assert.NoError(t, err) + + baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), + &jenkins, false, false, nil, nil) + + got, err := baseReconcileLoop.validateImagePullSecrets() + assert.Equal(t, got, false) + assert.Error(t, err) + }) + + t.Run("no docker server", func(t *testing.T) { + lor := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ref", + }, + Data: map[string][]byte{ + "docker-username": []byte("test_user"), + "docker-password": []byte("test_password"), + "docker-email": []byte("test_email"), + }, + } + + jenkins := v1alpha2.Jenkins{ + Spec: v1alpha2.JenkinsSpec{ + Master: v1alpha2.JenkinsMaster{ + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: lor.ObjectMeta.Name}, + }, + }, + }, + } + + fakeClient := fake.NewFakeClient() + err := fakeClient.Create(context.TODO(), lor) + assert.NoError(t, err) + + baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), + &jenkins, false, false, nil, nil) + + got, err := baseReconcileLoop.validateImagePullSecrets() + assert.Equal(t, got, false) + assert.Error(t, err) + }) +} + func TestValidateJenkinsMasterPodEnvs(t *testing.T) { t.Run("happy", func(t *testing.T) { jenkins := v1alpha2.Jenkins{ From 7849ddc6f5beb0dc490739e8fa926737ff79d3c0 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Fri, 12 Jul 2019 14:15:46 +0200 Subject: [PATCH 29/59] Improve docs --- docs/getting-started.md | 86 +++++++++++++++++-- docs/migration-guide-v1alphav1-to-v1alpha2.md | 2 +- 2 files changed, 81 insertions(+), 7 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 4e63cd7b..a1bf705d 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -5,12 +5,13 @@ This document describes a getting started guide for **jenkins-operator** and an 1. [First Steps](#first-steps) 2. [Deploy Jenkins](#deploy-jenkins) 3. [Configure Seed Jobs and Pipelines](#configure-seed-jobs-and-pipelines) -4. [Install Plugins](#install-plugins) -5. [Configure Backup & Restore](#configure-backup-and-restore) -6. [AKS](#aks) -7. [Jenkins login credentials](#jenkins-login-credentials) -8. [Override default Jenkins container command](#override-default-Jenkins-container-command) -9. [Debugging](#debugging) +4. [Pulling custom Jenkins image from Docker Registry](#Pulling custom Jenkins image from Docker Registry) +5. [Install Plugins](#install-plugins) +6. [Configure Backup & Restore](#configure-backup-and-restore) +7. [AKS](#aks) +8. [Jenkins login credentials](#jenkins-login-credentials) +9. [Override default Jenkins container command](#override-default-Jenkins-container-command) +10. [Debugging](#debugging) ## First Steps @@ -293,6 +294,79 @@ data: password: password_or_token ``` +## Pulling custom Jenkins image from Docker Registry +Since **0.2.0** version it's possible to use custom prebuilt Jenkins Docker Image using `imagePullSecrets` annotation support. + +Please follow the instructions on [creating a secret with a docker config](https://kubernetes.io/docs/concepts/containers/images/?origin_team=T42NTAGHM#creating-a-secret-with-a-docker-config). + +### Docker Hub Configuration +To use Docker Hub additional steps are required. + +Edit the previously created secret: +```bash +kubectl edit secret +``` + +The `data..dockerconfigjson` key's value needs to be replaced with a modified version. + +After modifications it needs to be encoded as Base64 value before setting the `.dockerconfigjson` key:q. + +Example config file to modify and use: +``` +{ + "auths":{ + "https://index.docker.io/v1/":{ + "username":"user", + "password":"password", + "email":"yourdockeremail@gmail.com", + "auth":"base64 of string user:password" + }, + "auth.docker.io":{ + "username":"user", + "password":"password", + "email":"yourdockeremail@gmail.com", + "auth":"base64 of string user:password" + }, + "registry.docker.io":{ + "username":"user", + "password":"password", + "email":"yourdockeremail@gmail.com", + "auth":"base64 of string user:password" + }, + "docker.io":{ + "username":"user", + "password":"password", + "email":"yourdockeremail@gmail.com", + "auth":"base64 of string user:password" + }, + "https://registry-1.docker.io/v2/": { + "username":"user", + "password":"password", + "email":"yourdockeremail@gmail.com", + "auth":"base64 of string user:password" + }, + "registry-1.docker.io/v2/": { + "username":"user", + "password":"password", + "email":"yourdockeremail@gmail.com", + "auth":"base64 of string user:password" + }, + "registry-1.docker.io": { + "username":"user", + "password":"password", + "email":"yourdockeremail@gmail.com", + "auth":"base64 of string user:password" + }, + "https://registry-1.docker.io": { + "username":"user", + "password":"password", + "email":"yourdockeremail@gmail.com", + "auth":"base64 of string user:password" + } + } +} +``` + ## Jenkins Customisation Jenkins can be customized using groovy scripts or configuration as code plugin. All custom configuration is stored in diff --git a/docs/migration-guide-v1alphav1-to-v1alpha2.md b/docs/migration-guide-v1alphav1-to-v1alpha2.md index 6b70f3df..7c7ac93a 100644 --- a/docs/migration-guide-v1alphav1-to-v1alpha2.md +++ b/docs/migration-guide-v1alphav1-to-v1alpha2.md @@ -308,7 +308,7 @@ To use default CRD file: kubectl -n apply -f https://github.com/jenkinsci/kubernetes-operator/blob/master/deploy/crds/jenkins_v1alpha2_jenkins_crd.yaml ``` -## Update RBAC to new verison +## Update RBAC to new version New operator version requires updated RBAC permissions: From feae4e6e3d11f131e69ea99f3c0f3ffac58fe022 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Fri, 12 Jul 2019 14:53:22 +0200 Subject: [PATCH 30/59] #45 Improve feature --- docs/getting-started.md | 10 +++---- .../jenkins/configuration/base/validate.go | 27 ++++++++--------- .../configuration/base/validate_test.go | 30 +++++++++---------- 3 files changed, 33 insertions(+), 34 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index a1bf705d..7860a4e7 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -5,7 +5,7 @@ This document describes a getting started guide for **jenkins-operator** and an 1. [First Steps](#first-steps) 2. [Deploy Jenkins](#deploy-jenkins) 3. [Configure Seed Jobs and Pipelines](#configure-seed-jobs-and-pipelines) -4. [Pulling custom Jenkins image from Docker Registry](#Pulling custom Jenkins image from Docker Registry) +4. [Pulling Docker images from private repositories](#Pulling Docker images from private repositories) 5. [Install Plugins](#install-plugins) 6. [Configure Backup & Restore](#configure-backup-and-restore) 7. [AKS](#aks) @@ -294,8 +294,8 @@ data: password: password_or_token ``` -## Pulling custom Jenkins image from Docker Registry -Since **0.2.0** version it's possible to use custom prebuilt Jenkins Docker Image using `imagePullSecrets` annotation support. +## Pulling Docker images from private repositories +To pull Docker Image from private repository you can use `imagePullSecrets`. Please follow the instructions on [creating a secret with a docker config](https://kubernetes.io/docs/concepts/containers/images/?origin_team=T42NTAGHM#creating-a-secret-with-a-docker-config). @@ -304,10 +304,10 @@ To use Docker Hub additional steps are required. Edit the previously created secret: ```bash -kubectl edit secret +kubectl -n edit secret ``` -The `data..dockerconfigjson` key's value needs to be replaced with a modified version. +The `.dockerconfigjson` key's value needs to be replaced with a modified version. After modifications it needs to be encoded as Base64 value before setting the `.dockerconfigjson` key:q. diff --git a/pkg/controller/jenkins/configuration/base/validate.go b/pkg/controller/jenkins/configuration/base/validate.go index b49f370c..c96da282 100644 --- a/pkg/controller/jenkins/configuration/base/validate.go +++ b/pkg/controller/jenkins/configuration/base/validate.go @@ -2,7 +2,6 @@ package base import ( "context" - "errors" "fmt" "regexp" @@ -62,40 +61,40 @@ func (r *ReconcileJenkinsBaseConfiguration) Validate(jenkins *v1alpha2.Jenkins) } func (r *ReconcileJenkinsBaseConfiguration) validateImagePullSecrets() (bool, error) { - var err error - valid := true - ips := r.jenkins.Spec.Master.ImagePullSecrets - for _, sr := range ips { - valid, err = r.validateImagePullSecret(sr.Name) + for _, sr := range r.jenkins.Spec.Master.ImagePullSecrets { + valid, err := r.validateImagePullSecret(sr.Name) if err != nil || !valid { - return valid, err + return true, err } } - - return valid, err + return false, nil } func (r *ReconcileJenkinsBaseConfiguration) validateImagePullSecret(name string) (bool, error) { secret := &corev1.Secret{} err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: r.jenkins.ObjectMeta.Namespace}, secret) if err != nil && apierrors.IsNotFound(err) { - r.logger.V(log.VWarn).Info(fmt.Sprintf("Secret '%s' not found", name)) + r.logger.V(log.VWarn).Info(fmt.Sprintf("Secret %s not found defined in spec.master.imagePullSecrets", name)) return false, nil } else if err != nil && !apierrors.IsNotFound(err) { return false, stackerr.WithStack(err) } if secret.Data["docker-server"] == nil { - return false, errors.New("docker server not set") + r.logger.V(log.VWarn).Info("Docker Server is empty") + return false, nil } if secret.Data["docker-username"] == nil { - return false, errors.New("docker username not set") + r.logger.V(log.VWarn).Info("Docker Username is empty") + return false, nil } if secret.Data["docker-password"] == nil { - return false, errors.New("docker password not set") + r.logger.V(log.VWarn).Info("Docker Password is empty") + return false, nil } if secret.Data["docker-email"] == nil { - return false, errors.New("docker email not set") + r.logger.V(log.VWarn).Info("Docker Email is empty") + return false, nil } return true, nil diff --git a/pkg/controller/jenkins/configuration/base/validate_test.go b/pkg/controller/jenkins/configuration/base/validate_test.go index cac463b2..6deba48b 100644 --- a/pkg/controller/jenkins/configuration/base/validate_test.go +++ b/pkg/controller/jenkins/configuration/base/validate_test.go @@ -134,7 +134,7 @@ func TestValidatePlugins(t *testing.T) { func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T) { t.Run("happy", func(t *testing.T) { - lor := &corev1.Secret{ + secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "test-ref", }, @@ -150,14 +150,14 @@ func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T Spec: v1alpha2.JenkinsSpec{ Master: v1alpha2.JenkinsMaster{ ImagePullSecrets: []corev1.LocalObjectReference{ - {Name: lor.ObjectMeta.Name}, + {Name: secret.ObjectMeta.Name}, }, }, }, } fakeClient := fake.NewFakeClient() - err := fakeClient.Create(context.TODO(), lor) + err := fakeClient.Create(context.TODO(), secret) assert.NoError(t, err) baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), @@ -189,7 +189,7 @@ func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T }) t.Run("no docker email", func(t *testing.T) { - lor := &corev1.Secret{ + secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "test-ref", }, @@ -204,14 +204,14 @@ func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T Spec: v1alpha2.JenkinsSpec{ Master: v1alpha2.JenkinsMaster{ ImagePullSecrets: []corev1.LocalObjectReference{ - {Name: lor.ObjectMeta.Name}, + {Name: secret.ObjectMeta.Name}, }, }, }, } fakeClient := fake.NewFakeClient() - err := fakeClient.Create(context.TODO(), lor) + err := fakeClient.Create(context.TODO(), secret) assert.NoError(t, err) baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), @@ -223,7 +223,7 @@ func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T }) t.Run("no docker password", func(t *testing.T) { - lor := &corev1.Secret{ + secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "test-ref", }, @@ -238,14 +238,14 @@ func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T Spec: v1alpha2.JenkinsSpec{ Master: v1alpha2.JenkinsMaster{ ImagePullSecrets: []corev1.LocalObjectReference{ - {Name: lor.ObjectMeta.Name}, + {Name: secret.ObjectMeta.Name}, }, }, }, } fakeClient := fake.NewFakeClient() - err := fakeClient.Create(context.TODO(), lor) + err := fakeClient.Create(context.TODO(), secret) assert.NoError(t, err) baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), @@ -257,7 +257,7 @@ func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T }) t.Run("no docker username", func(t *testing.T) { - lor := &corev1.Secret{ + secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "test-ref", }, @@ -272,14 +272,14 @@ func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T Spec: v1alpha2.JenkinsSpec{ Master: v1alpha2.JenkinsMaster{ ImagePullSecrets: []corev1.LocalObjectReference{ - {Name: lor.ObjectMeta.Name}, + {Name: secret.ObjectMeta.Name}, }, }, }, } fakeClient := fake.NewFakeClient() - err := fakeClient.Create(context.TODO(), lor) + err := fakeClient.Create(context.TODO(), secret) assert.NoError(t, err) baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), @@ -291,7 +291,7 @@ func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T }) t.Run("no docker server", func(t *testing.T) { - lor := &corev1.Secret{ + secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "test-ref", }, @@ -306,14 +306,14 @@ func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T Spec: v1alpha2.JenkinsSpec{ Master: v1alpha2.JenkinsMaster{ ImagePullSecrets: []corev1.LocalObjectReference{ - {Name: lor.ObjectMeta.Name}, + {Name: secret.ObjectMeta.Name}, }, }, }, } fakeClient := fake.NewFakeClient() - err := fakeClient.Create(context.TODO(), lor) + err := fakeClient.Create(context.TODO(), secret) assert.NoError(t, err) baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), From 74b8ec98ec1c3efa35d310dc90d4982ca3ab3c9c Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Mon, 15 Jul 2019 09:04:39 +0200 Subject: [PATCH 31/59] Fix validation bug --- pkg/controller/jenkins/configuration/base/resources/pod.go | 2 +- pkg/controller/jenkins/configuration/base/validate.go | 7 ++++--- pkg/controller/jenkins/configuration/base/validate_test.go | 4 ---- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/pkg/controller/jenkins/configuration/base/resources/pod.go b/pkg/controller/jenkins/configuration/base/resources/pod.go index fe899575..64d911f7 100644 --- a/pkg/controller/jenkins/configuration/base/resources/pod.go +++ b/pkg/controller/jenkins/configuration/base/resources/pod.go @@ -288,7 +288,7 @@ func NewJenkinsMasterPod(objectMeta metav1.ObjectMeta, jenkins *v1alpha2.Jenkins Containers: newContainers(jenkins), Volumes: append(GetJenkinsMasterPodBaseVolumes(jenkins), jenkins.Spec.Master.Volumes...), SecurityContext: jenkins.Spec.Master.SecurityContext, - ImagePullSecrets: jenkins.Spec.Master.ImagePullSecrets, + ImagePullSecrets: jenkins.Spec.Master.ImagePullSecrets, }, } } diff --git a/pkg/controller/jenkins/configuration/base/validate.go b/pkg/controller/jenkins/configuration/base/validate.go index c96da282..61f801e0 100644 --- a/pkg/controller/jenkins/configuration/base/validate.go +++ b/pkg/controller/jenkins/configuration/base/validate.go @@ -61,13 +61,14 @@ func (r *ReconcileJenkinsBaseConfiguration) Validate(jenkins *v1alpha2.Jenkins) } func (r *ReconcileJenkinsBaseConfiguration) validateImagePullSecrets() (bool, error) { + var err error for _, sr := range r.jenkins.Spec.Master.ImagePullSecrets { valid, err := r.validateImagePullSecret(sr.Name) if err != nil || !valid { - return true, err + return false, nil } } - return false, nil + return true, err } func (r *ReconcileJenkinsBaseConfiguration) validateImagePullSecret(name string) (bool, error) { @@ -92,7 +93,7 @@ func (r *ReconcileJenkinsBaseConfiguration) validateImagePullSecret(name string) r.logger.V(log.VWarn).Info("Docker Password is empty") return false, nil } - if secret.Data["docker-email"] == nil { + if secret.Data["docker-email"] == nil { r.logger.V(log.VWarn).Info("Docker Email is empty") return false, nil } diff --git a/pkg/controller/jenkins/configuration/base/validate_test.go b/pkg/controller/jenkins/configuration/base/validate_test.go index 6deba48b..7a8aedbf 100644 --- a/pkg/controller/jenkins/configuration/base/validate_test.go +++ b/pkg/controller/jenkins/configuration/base/validate_test.go @@ -219,7 +219,6 @@ func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T got, err := baseReconcileLoop.validateImagePullSecrets() assert.Equal(t, got, false) - assert.Error(t, err) }) t.Run("no docker password", func(t *testing.T) { @@ -253,7 +252,6 @@ func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T got, err := baseReconcileLoop.validateImagePullSecrets() assert.Equal(t, got, false) - assert.Error(t, err) }) t.Run("no docker username", func(t *testing.T) { @@ -287,7 +285,6 @@ func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T got, err := baseReconcileLoop.validateImagePullSecrets() assert.Equal(t, got, false) - assert.Error(t, err) }) t.Run("no docker server", func(t *testing.T) { @@ -321,7 +318,6 @@ func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T got, err := baseReconcileLoop.validateImagePullSecrets() assert.Equal(t, got, false) - assert.Error(t, err) }) } From 811e816b2f34c28623210de4bee784175c86b06c Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Mon, 15 Jul 2019 09:14:53 +0200 Subject: [PATCH 32/59] Fixed tests --- pkg/controller/jenkins/configuration/base/validate.go | 2 +- .../jenkins/configuration/base/validate_test.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/controller/jenkins/configuration/base/validate.go b/pkg/controller/jenkins/configuration/base/validate.go index 61f801e0..ee7a8822 100644 --- a/pkg/controller/jenkins/configuration/base/validate.go +++ b/pkg/controller/jenkins/configuration/base/validate.go @@ -93,7 +93,7 @@ func (r *ReconcileJenkinsBaseConfiguration) validateImagePullSecret(name string) r.logger.V(log.VWarn).Info("Docker Password is empty") return false, nil } - if secret.Data["docker-email"] == nil { + if secret.Data["docker-email"] == nil { r.logger.V(log.VWarn).Info("Docker Email is empty") return false, nil } diff --git a/pkg/controller/jenkins/configuration/base/validate_test.go b/pkg/controller/jenkins/configuration/base/validate_test.go index 7a8aedbf..3dcb258c 100644 --- a/pkg/controller/jenkins/configuration/base/validate_test.go +++ b/pkg/controller/jenkins/configuration/base/validate_test.go @@ -217,7 +217,7 @@ func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), &jenkins, false, false, nil, nil) - got, err := baseReconcileLoop.validateImagePullSecrets() + got, _ := baseReconcileLoop.validateImagePullSecrets() assert.Equal(t, got, false) }) @@ -250,7 +250,7 @@ func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), &jenkins, false, false, nil, nil) - got, err := baseReconcileLoop.validateImagePullSecrets() + got, _ := baseReconcileLoop.validateImagePullSecrets() assert.Equal(t, got, false) }) @@ -283,7 +283,7 @@ func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), &jenkins, false, false, nil, nil) - got, err := baseReconcileLoop.validateImagePullSecrets() + got, _ := baseReconcileLoop.validateImagePullSecrets() assert.Equal(t, got, false) }) @@ -316,7 +316,7 @@ func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T baseReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), &jenkins, false, false, nil, nil) - got, err := baseReconcileLoop.validateImagePullSecrets() + got, _ := baseReconcileLoop.validateImagePullSecrets() assert.Equal(t, got, false) }) } From 0fed26b7330d7f271e8489e2b380467f71ea14a1 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Mon, 15 Jul 2019 09:56:47 +0200 Subject: [PATCH 33/59] Improve tests & docs --- docs/getting-started.md | 2 +- pkg/controller/jenkins/configuration/base/validate.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 7860a4e7..6907e1b3 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -5,7 +5,7 @@ This document describes a getting started guide for **jenkins-operator** and an 1. [First Steps](#first-steps) 2. [Deploy Jenkins](#deploy-jenkins) 3. [Configure Seed Jobs and Pipelines](#configure-seed-jobs-and-pipelines) -4. [Pulling Docker images from private repositories](#Pulling Docker images from private repositories) +4. [Pulling Docker images from private repositories](#pulling-docker-images-from-private-repositories) 5. [Install Plugins](#install-plugins) 6. [Configure Backup & Restore](#configure-backup-and-restore) 7. [AKS](#aks) diff --git a/pkg/controller/jenkins/configuration/base/validate.go b/pkg/controller/jenkins/configuration/base/validate.go index ee7a8822..4ff7f27f 100644 --- a/pkg/controller/jenkins/configuration/base/validate.go +++ b/pkg/controller/jenkins/configuration/base/validate.go @@ -82,19 +82,19 @@ func (r *ReconcileJenkinsBaseConfiguration) validateImagePullSecret(name string) } if secret.Data["docker-server"] == nil { - r.logger.V(log.VWarn).Info("Docker Server is empty") + r.logger.V(log.VWarn).Info(fmt.Sprintf("Secret '%s' defined in spec.master.imagePullSecrets doesn't have 'docker-server' key.", name)) return false, nil } if secret.Data["docker-username"] == nil { - r.logger.V(log.VWarn).Info("Docker Username is empty") + r.logger.V(log.VWarn).Info(fmt.Sprintf("Secret '%s' defined in spec.master.imagePullSecrets doesn't have 'docker-username' key.", name)) return false, nil } if secret.Data["docker-password"] == nil { - r.logger.V(log.VWarn).Info("Docker Password is empty") + r.logger.V(log.VWarn).Info(fmt.Sprintf("Secret '%s' defined in spec.master.imagePullSecrets doesn't have 'docker-password' key.", name)) return false, nil } if secret.Data["docker-email"] == nil { - r.logger.V(log.VWarn).Info("Docker Email is empty") + r.logger.V(log.VWarn).Info(fmt.Sprintf("Secret '%s' defined in spec.master.imagePullSecrets doesn't have 'docker-email' key.", name)) return false, nil } From 431484882fda7529b7c0cfdebc014c52e1f666ea Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Mon, 15 Jul 2019 15:34:23 +0200 Subject: [PATCH 34/59] #52 Add notice about rbac --- docs/security.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/docs/security.md b/docs/security.md index f5a28889..7c663003 100644 --- a/docs/security.md +++ b/docs/security.md @@ -34,6 +34,53 @@ Kubernetes API permissions are limited by the following roles: - [jenkins-operator role](../deploy/role.yaml) - [Jenkins Master role](../pkg/controller/jenkins/configuration/base/resources/rbac.go) +Since **jenkins-operator** must be able to grant permission for its' deployed Jenkins masters to spawn pods (the `Jenkins Master role` above), +the operator itself requires permission to create RBAC resources (the `jenkins-operator role` above). +Deployed this way, any subject which may create a Pod (including a Jenkins job) may +assume the `jenkins-operator` role by using its' ServiceAccount, create RBAC rules, and thus escape its granted permissions. +Any namespace to which the `jenkins-operator` is deployed must be considered to implicitly grant all +possible permissions to any subject which can create a Pod in that namespace. + +To mitigate this issue **jenkins-operator** should be deployed in one namespace and the Jenkins CR should be created in separate namespace. +To achieve it change watch namespace in https://github.com/jenkinsci/kubernetes-operator/blob/master/deploy/operator.yaml#L25 + +## Setup Jenkins Operator and Jenkins in separated namespaces + +You need to create two namespaces, for example we'll call them **jenkins** for Jenkins and **jenkins-operator** for Jenkins Operator. +```bash +$ kubectl create ns jenkins-operator +$ kubectl create ns jenkins +``` + +Next, apply the RBAC manifests +```bash +$ kubectl -n jenkins apply -f deploy/role.yaml +$ kubectl -n jenkins -n jenkins-operator apply -f deploy/service_account.yaml +$ kubectl -n jenkins -n jenkins apply -f deploy/role_binding.yaml +``` + +Then, you must create operator pod by: +```bash +$ kubectl -n jenkins -n jenkins-operator apply -f deploy/operator.yaml +``` + +To combine pods, you must modify RoleBindings. You can use this example YAML to bind: +```yaml +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: jenkins-operator + namespace: jenkins +subjects: +- kind: ServiceAccount + name: jenkins-operator + namespace: jenkins-operator +roleRef: + kind: Role + name: jenkins-operator + apiGroup: rbac.authorization.k8s.io + ``` + ## Report a Security Vulnerability If you find a vulnerability or any misconfiguration in Jenkins, please report it in the [issues](https://github.com/jenkinsci/kubernetes-operator/issues). From f710704abaa642e0edb5414c2472a62508a1e16c Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Mon, 15 Jul 2019 15:59:53 +0200 Subject: [PATCH 35/59] Improve docs --- docs/security.md | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/docs/security.md b/docs/security.md index 7c663003..32468422 100644 --- a/docs/security.md +++ b/docs/security.md @@ -52,19 +52,13 @@ $ kubectl create ns jenkins-operator $ kubectl create ns jenkins ``` -Next, apply the RBAC manifests +Next, apply the RBAC manifests for **jenkins-operator** namespace ```bash -$ kubectl -n jenkins apply -f deploy/role.yaml -$ kubectl -n jenkins -n jenkins-operator apply -f deploy/service_account.yaml -$ kubectl -n jenkins -n jenkins apply -f deploy/role_binding.yaml +$ kubectl -n jenkins-operator apply -f deploy/service_account.yaml +$ kubectl -n jenkins-operator apply -f deploy/role_binding.yaml ``` -Then, you must create operator pod by: -```bash -$ kubectl -n jenkins -n jenkins-operator apply -f deploy/operator.yaml -``` - -To combine pods, you must modify RoleBindings. You can use this example YAML to bind: +Create file role_binding_jenkins.yaml in `deploy` folder: ```yaml kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 @@ -79,7 +73,19 @@ roleRef: kind: Role name: jenkins-operator apiGroup: rbac.authorization.k8s.io - ``` +``` + +Then, apply RBAC rules for **jenkins** namespace +```bash +$ kubectl -n jenkins apply -f deploy/role.yaml +$ kubectl -n jenkins apply -f role_binding_jenkins.yaml +``` + +Finally, you must create operator pod by: +```bash +$ kubectl -n jenkins -n jenkins-operator apply -f deploy/operator.yaml +``` + ## Report a Security Vulnerability From c9f39e9de4f3cab8a0bdb45704ff5f33a4d4a30f Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Mon, 15 Jul 2019 16:47:12 +0200 Subject: [PATCH 36/59] Add support for namespaces in jenkins client --- pkg/controller/jenkins/client/jenkins.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/controller/jenkins/client/jenkins.go b/pkg/controller/jenkins/client/jenkins.go index fde87464..0e278cff 100644 --- a/pkg/controller/jenkins/client/jenkins.go +++ b/pkg/controller/jenkins/client/jenkins.go @@ -98,7 +98,7 @@ func BuildJenkinsAPIUrl(namespace, serviceName string, portNumber int32, local, } // Connect through Kubernetes service, operator has to be run inside cluster - return fmt.Sprintf("http://%s:%d", serviceName, portNumber), nil + return fmt.Sprintf("http://%s.%s:%d", serviceName, namespace, portNumber), nil } // New creates Jenkins API client From 28dba98cd972f4080b9faac82d7c5a362f0c5864 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Tue, 16 Jul 2019 09:39:55 +0200 Subject: [PATCH 37/59] #45 Add test to check for imagePullSecrets propagation --- pkg/controller/jenkins/groovy/groovy.go | 3 +-- test/e2e/configuration_test.go | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/controller/jenkins/groovy/groovy.go b/pkg/controller/jenkins/groovy/groovy.go index 675392ed..5284f176 100644 --- a/pkg/controller/jenkins/groovy/groovy.go +++ b/pkg/controller/jenkins/groovy/groovy.go @@ -187,9 +187,8 @@ func AddSecretsLoaderToGroovyScript(secretsPath string) func(groovyScript string break } } - asdf := strings.Join(lines[:importIndex], "\n") + "\n\n" + fmt.Sprintf(secretsLoaderGroovyScriptFmt, secretsPath) + "\n\n" + strings.Join(lines[importIndex:], "\n") - return asdf + return strings.Join(lines[:importIndex], "\n") + "\n\n" + fmt.Sprintf(secretsLoaderGroovyScriptFmt, secretsPath) + "\n\n" + strings.Join(lines[importIndex:], "\n") } } diff --git a/test/e2e/configuration_test.go b/test/e2e/configuration_test.go index a3d06361..a6ae0590 100644 --- a/test/e2e/configuration_test.go +++ b/test/e2e/configuration_test.go @@ -171,6 +171,8 @@ func verifyJenkinsMasterPodAttributes(t *testing.T, jenkins *v1alpha2.Jenkins) { assert.Equal(t, jenkins.Spec.Master.SecurityContext, jenkinsPod.Spec.SecurityContext) assert.Equal(t, jenkins.Spec.Master.Containers[0].Command, jenkinsPod.Spec.Containers[0].Command) + assert.Equal(t, jenkins.Spec.Master.ImagePullSecrets, jenkinsPod.Spec.ImagePullSecrets) + for _, actualContainer := range jenkinsPod.Spec.Containers { if actualContainer.Name == resources.JenkinsMasterContainerName { verifyContainer(t, resources.NewJenkinsMasterContainer(jenkins), actualContainer) From 0e4ae1550608b365e8231ef4a56401bc7d0577da Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Wed, 17 Jul 2019 16:01:34 +0200 Subject: [PATCH 38/59] #28 Enable groovy scripts secret loader --- .../jenkins/configuration/user/reconcile.go | 5 +---- test/e2e/configuration_test.go | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/pkg/controller/jenkins/configuration/user/reconcile.go b/pkg/controller/jenkins/configuration/user/reconcile.go index 13525a62..0bdb4c6b 100644 --- a/pkg/controller/jenkins/configuration/user/reconcile.go +++ b/pkg/controller/jenkins/configuration/user/reconcile.go @@ -112,10 +112,7 @@ func (r *ReconcileUserConfiguration) ensureUserConfiguration(jenkinsClient jenki } requeue, err = groovyClient.Ensure(func(name string) bool { return strings.HasSuffix(name, ".groovy") - }, func(groovyScript string) string { - // TODO load secrets to variables - return groovyScript - }) + }, groovy.AddSecretsLoaderToGroovyScript(resources.GroovyScriptsSecretVolumePath)) if err != nil { return reconcile.Result{}, err } diff --git a/test/e2e/configuration_test.go b/test/e2e/configuration_test.go index a6ae0590..f75cb178 100644 --- a/test/e2e/configuration_test.go +++ b/test/e2e/configuration_test.go @@ -3,15 +3,15 @@ package e2e import ( "context" "fmt" + "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/groovy" "testing" + "github.com/bndr/gojenkins" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/plugins" - - "github.com/bndr/gojenkins" framework "github.com/operator-framework/operator-sdk/pkg/test" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" @@ -73,7 +73,6 @@ func TestConfiguration(t *testing.T) { createDefaultLimitsForContainersInNamespace(t, namespace) createKubernetesCredentialsProviderSecret(t, namespace, mySeedJob) waitForJenkinsBaseConfigurationToComplete(t, jenkins) - verifyJenkinsMasterPodAttributes(t, jenkins) client := verifyJenkinsAPIConnection(t, jenkins) verifyPlugins(t, client, jenkins) @@ -92,6 +91,7 @@ func createUserConfigurationSecret(t *testing.T, namespace string, systemMessage }, StringData: map[string]string{ systemMessageEnvName: systemMessage, + "numberOfExecutors": "3", }, } @@ -275,6 +275,15 @@ if (!new Integer(%d).equals(Jenkins.instance.numExecutors)) { logs, err := jenkinsClient.ExecuteScript(checkConfigurationViaGroovyScript) assert.NoError(t, err, logs) + checkSecretLoaderViaGroovyScript := fmt.Sprintf(` +if (new Integer(%d).equals(secrets['numberOfExecutors'])) { + throw new Exception("Falied") +}`, amountOfExecutors) + + loader := groovy.AddSecretsLoaderToGroovyScript("/var/jenkins/groovy-scripts-secrets") + logs, err = jenkinsClient.ExecuteScript(loader(checkSecretLoaderViaGroovyScript)) + assert.NoError(t, err, logs) + checkConfigurationAsCode := fmt.Sprintf(` if (!"%s".equals(Jenkins.instance.systemMessage)) { throw new Exception("Configuration as code failed") From 6a357e28e33348cfcec9b19da17fe622388469d7 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Thu, 18 Jul 2019 10:18:51 +0200 Subject: [PATCH 39/59] #28 Improve e2e tests --- test/e2e/configuration_test.go | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/test/e2e/configuration_test.go b/test/e2e/configuration_test.go index f75cb178..8389ca87 100644 --- a/test/e2e/configuration_test.go +++ b/test/e2e/configuration_test.go @@ -3,20 +3,21 @@ package e2e import ( "context" "fmt" - "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/groovy" "testing" - "github.com/bndr/gojenkins" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" + "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/groovy" jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/plugins" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/bndr/gojenkins" + "k8s.io/apimachinery/pkg/api/resource" framework "github.com/operator-framework/operator-sdk/pkg/test" "github.com/stretchr/testify/assert" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestConfiguration(t *testing.T) { @@ -27,6 +28,7 @@ func TestConfiguration(t *testing.T) { jenkinsCRName := "e2e" numberOfExecutors := 6 + numberOfExecutorsEnvName := "NUMBER_OF_EXECUTORS" systemMessage := "Configuration as Code integration works!!!" systemMessageEnvName := "SYSTEM_MESSAGE" mySeedJob := seedJobConfig{ @@ -66,8 +68,12 @@ func TestConfiguration(t *testing.T) { }, } + stringData := make(map[string]string) + stringData[systemMessageEnvName] = systemMessage + stringData[numberOfExecutorsEnvName] = fmt.Sprintf("%d", numberOfExecutors) + // base - createUserConfigurationSecret(t, namespace, systemMessageEnvName, systemMessage) + createUserConfigurationSecret(t, namespace, stringData) createUserConfigurationConfigMap(t, namespace, numberOfExecutors, fmt.Sprintf("${%s}", systemMessageEnvName)) jenkins := createJenkinsCR(t, jenkinsCRName, namespace, &[]v1alpha2.SeedJob{mySeedJob.SeedJob}, groovyScripts, casc) createDefaultLimitsForContainersInNamespace(t, namespace) @@ -83,16 +89,13 @@ func TestConfiguration(t *testing.T) { verifyJenkinsSeedJobs(t, client, []seedJobConfig{mySeedJob}) } -func createUserConfigurationSecret(t *testing.T, namespace string, systemMessageEnvName, systemMessage string) { +func createUserConfigurationSecret(t *testing.T, namespace string, stringData map[string]string) { userConfiguration := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: userConfigurationSecretName, Namespace: namespace, }, - StringData: map[string]string{ - systemMessageEnvName: systemMessage, - "numberOfExecutors": "3", - }, + StringData: stringData, } t.Logf("User configuration secret %+v", *userConfiguration) @@ -276,8 +279,8 @@ if (!new Integer(%d).equals(Jenkins.instance.numExecutors)) { assert.NoError(t, err, logs) checkSecretLoaderViaGroovyScript := fmt.Sprintf(` -if (new Integer(%d).equals(secrets['numberOfExecutors'])) { - throw new Exception("Falied") +if (!new Integer(%d).equals(new Integer(secrets['NUMBER_OF_EXECUTORS']))) { + throw new Exception("Falied to check secrets by groovy secret loader") }`, amountOfExecutors) loader := groovy.AddSecretsLoaderToGroovyScript("/var/jenkins/groovy-scripts-secrets") From 52cd17e97f3f62874d88477d56f955061234ee65 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Thu, 18 Jul 2019 11:43:28 +0200 Subject: [PATCH 40/59] #28 Improve TestConfiguration e2e test --- test/e2e/configuration_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/e2e/configuration_test.go b/test/e2e/configuration_test.go index 8389ca87..7e0af964 100644 --- a/test/e2e/configuration_test.go +++ b/test/e2e/configuration_test.go @@ -74,7 +74,7 @@ func TestConfiguration(t *testing.T) { // base createUserConfigurationSecret(t, namespace, stringData) - createUserConfigurationConfigMap(t, namespace, numberOfExecutors, fmt.Sprintf("${%s}", systemMessageEnvName)) + createUserConfigurationConfigMap(t, namespace, numberOfExecutorsEnvName, fmt.Sprintf("${%s}", systemMessageEnvName)) jenkins := createJenkinsCR(t, jenkinsCRName, namespace, &[]v1alpha2.SeedJob{mySeedJob.SeedJob}, groovyScripts, casc) createDefaultLimitsForContainersInNamespace(t, namespace) createKubernetesCredentialsProviderSecret(t, namespace, mySeedJob) @@ -104,7 +104,7 @@ func createUserConfigurationSecret(t *testing.T, namespace string, stringData ma } } -func createUserConfigurationConfigMap(t *testing.T, namespace string, numberOfExecutors int, systemMessage string) { +func createUserConfigurationConfigMap(t *testing.T, namespace string, numberOfExecutorsSecretKeyName string, systemMessage string) { userConfiguration := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: userConfigurationConfigMapName, @@ -114,8 +114,8 @@ func createUserConfigurationConfigMap(t *testing.T, namespace string, numberOfEx "1-set-executors.groovy": fmt.Sprintf(` import jenkins.model.Jenkins -Jenkins.instance.setNumExecutors(%d) -Jenkins.instance.save()`, numberOfExecutors), +Jenkins.instance.setNumExecutors(new Integer(secrets['%s'])) +Jenkins.instance.save()`, numberOfExecutorsSecretKeyName), "1-casc.yaml": fmt.Sprintf(` jenkins: systemMessage: "%s"`, systemMessage), @@ -280,7 +280,7 @@ if (!new Integer(%d).equals(Jenkins.instance.numExecutors)) { checkSecretLoaderViaGroovyScript := fmt.Sprintf(` if (!new Integer(%d).equals(new Integer(secrets['NUMBER_OF_EXECUTORS']))) { - throw new Exception("Falied to check secrets by groovy secret loader") + throw new Exception("Secret not found by given key: NUMBER_OF_EXECUTORS") }`, amountOfExecutors) loader := groovy.AddSecretsLoaderToGroovyScript("/var/jenkins/groovy-scripts-secrets") From 78b4a2d9525e9d2d18ae66c1b3cf4fde989c0ca7 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Mon, 22 Jul 2019 15:03:58 +0200 Subject: [PATCH 41/59] #54 Fix groovy script removal bug --- pkg/controller/jenkins/groovy/groovy.go | 7 +++ pkg/controller/jenkins/groovy/groovy_test.go | 49 ++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/pkg/controller/jenkins/groovy/groovy.go b/pkg/controller/jenkins/groovy/groovy.go index 5284f176..2d2826a4 100644 --- a/pkg/controller/jenkins/groovy/groovy.go +++ b/pkg/controller/jenkins/groovy/groovy.go @@ -47,6 +47,13 @@ func (g *Groovy) EnsureSingle(source, name, hash, groovyScript string) (requeue return false, nil } + for i, ags := range g.jenkins.Status.AppliedGroovyScripts { + if ags.ConfigurationType == g.configurationType && ags.Name == name && ags.Source == source { + g.jenkins.Status.AppliedGroovyScripts = append(g.jenkins.Status.AppliedGroovyScripts[:i], + g.jenkins.Status.AppliedGroovyScripts[i+1:]...) + } + } + logs, err := g.jenkinsClient.ExecuteScript(groovyScript) if err != nil { if _, ok := err.(*jenkinsclient.GroovyScriptExecutionFailed); ok { diff --git a/pkg/controller/jenkins/groovy/groovy_test.go b/pkg/controller/jenkins/groovy/groovy_test.go index 14286764..374cf7f3 100644 --- a/pkg/controller/jenkins/groovy/groovy_test.go +++ b/pkg/controller/jenkins/groovy/groovy_test.go @@ -113,6 +113,55 @@ func TestGroovy_EnsureSingle(t *testing.T) { assert.Equal(t, source, jenkins.Status.AppliedGroovyScripts[0].Source) assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name) }) + t.Run("execute script with new version", func(t *testing.T) { + // given + jenkins := &v1alpha2.Jenkins{ + ObjectMeta: metav1.ObjectMeta{ + Name: jenkinsName, + Namespace: namespace, + }, + } + err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme) + require.NoError(t, err) + fakeClient := fake.NewFakeClient() + err = fakeClient.Create(ctx, jenkins) + require.NoError(t, err) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + jenkinsClient := jenkinsclient.NewMockJenkins(ctrl) + + jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil) + jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil) + + groovyClient := New(jenkinsClient, fakeClient, log.Log, jenkins, configurationType, emptyCustomization) + + // when + _, err = groovyClient.EnsureSingle(source, groovyScriptName, hash, groovyScript) + + // then + require.NoError(t, err) + + err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins) + require.NoError(t, err) + assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts)) + assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType) + assert.Equal(t, hash, jenkins.Status.AppliedGroovyScripts[0].Hash) + assert.Equal(t, source, jenkins.Status.AppliedGroovyScripts[0].Source) + assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name) + + // Update with new hash + _, err = groovyClient.EnsureSingle(source, groovyScriptName, "hash1", groovyScript) + require.NoError(t, err) + + err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins) + require.NoError(t, err) + assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts)) + assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType) + assert.Equal(t, "hash1", jenkins.Status.AppliedGroovyScripts[0].Hash) + assert.Equal(t, source, jenkins.Status.AppliedGroovyScripts[0].Source) + assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name) + }) t.Run("execute script fails", func(t *testing.T) { // given jenkins := &v1alpha2.Jenkins{ From 4d2cab510934b493a6d098f11ea305360efbc36b Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Mon, 22 Jul 2019 16:41:00 +0200 Subject: [PATCH 42/59] #54 Improve business logic --- pkg/controller/jenkins/groovy/groovy.go | 19 +++++++++++-------- pkg/controller/jenkins/groovy/groovy_test.go | 10 ++++++---- test/e2e/configuration_test.go | 8 ++++---- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/pkg/controller/jenkins/groovy/groovy.go b/pkg/controller/jenkins/groovy/groovy.go index 2d2826a4..ec9823ed 100644 --- a/pkg/controller/jenkins/groovy/groovy.go +++ b/pkg/controller/jenkins/groovy/groovy.go @@ -47,13 +47,6 @@ func (g *Groovy) EnsureSingle(source, name, hash, groovyScript string) (requeue return false, nil } - for i, ags := range g.jenkins.Status.AppliedGroovyScripts { - if ags.ConfigurationType == g.configurationType && ags.Name == name && ags.Source == source { - g.jenkins.Status.AppliedGroovyScripts = append(g.jenkins.Status.AppliedGroovyScripts[:i], - g.jenkins.Status.AppliedGroovyScripts[i+1:]...) - } - } - logs, err := g.jenkinsClient.ExecuteScript(groovyScript) if err != nil { if _, ok := err.(*jenkinsclient.GroovyScriptExecutionFailed); ok { @@ -62,12 +55,22 @@ func (g *Groovy) EnsureSingle(source, name, hash, groovyScript string) (requeue return true, err } - g.jenkins.Status.AppliedGroovyScripts = append(g.jenkins.Status.AppliedGroovyScripts, v1alpha2.AppliedGroovyScript{ + var appliedGroovyScripts []v1alpha2.AppliedGroovyScript + + for _, ags := range g.jenkins.Status.AppliedGroovyScripts { + if ags.ConfigurationType != g.configurationType && ags.Source != source && ags.Name != name { + appliedGroovyScripts = append(appliedGroovyScripts, ags) + } + } + appliedGroovyScripts = append(appliedGroovyScripts, v1alpha2.AppliedGroovyScript{ ConfigurationType: g.configurationType, Source: source, Name: name, Hash: hash, }) + + g.jenkins.Status.AppliedGroovyScripts = appliedGroovyScripts + return true, g.k8sClient.Update(context.TODO(), g.jenkins) } diff --git a/pkg/controller/jenkins/groovy/groovy_test.go b/pkg/controller/jenkins/groovy/groovy_test.go index 374cf7f3..5d3d8571 100644 --- a/pkg/controller/jenkins/groovy/groovy_test.go +++ b/pkg/controller/jenkins/groovy/groovy_test.go @@ -114,6 +114,7 @@ func TestGroovy_EnsureSingle(t *testing.T) { assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name) }) t.Run("execute script with new version", func(t *testing.T) { + anotherHash := "hash1" // given jenkins := &v1alpha2.Jenkins{ ObjectMeta: metav1.ObjectMeta{ @@ -137,11 +138,11 @@ func TestGroovy_EnsureSingle(t *testing.T) { groovyClient := New(jenkinsClient, fakeClient, log.Log, jenkins, configurationType, emptyCustomization) // when - _, err = groovyClient.EnsureSingle(source, groovyScriptName, hash, groovyScript) + requeue, err := groovyClient.EnsureSingle(source, groovyScriptName, hash, groovyScript) // then require.NoError(t, err) - + assert.True(t, requeue) err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins) require.NoError(t, err) assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts)) @@ -151,14 +152,15 @@ func TestGroovy_EnsureSingle(t *testing.T) { assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name) // Update with new hash - _, err = groovyClient.EnsureSingle(source, groovyScriptName, "hash1", groovyScript) + requeue, err = groovyClient.EnsureSingle(source, groovyScriptName, anotherHash, groovyScript) require.NoError(t, err) + assert.True(t, requeue) err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins) require.NoError(t, err) assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts)) assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType) - assert.Equal(t, "hash1", jenkins.Status.AppliedGroovyScripts[0].Hash) + assert.Equal(t, anotherHash, jenkins.Status.AppliedGroovyScripts[0].Hash) assert.Equal(t, source, jenkins.Status.AppliedGroovyScripts[0].Source) assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name) }) diff --git a/test/e2e/configuration_test.go b/test/e2e/configuration_test.go index 7e0af964..0beae099 100644 --- a/test/e2e/configuration_test.go +++ b/test/e2e/configuration_test.go @@ -6,18 +6,18 @@ import ( "testing" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" - "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/groovy" jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources" + "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/groovy" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/plugins" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/bndr/gojenkins" - "k8s.io/apimachinery/pkg/api/resource" framework "github.com/operator-framework/operator-sdk/pkg/test" "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestConfiguration(t *testing.T) { From 2dabc773d4bb7d0fd87a5ffb827fcc4de1349ca1 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Tue, 23 Jul 2019 09:16:38 +0200 Subject: [PATCH 43/59] #54 Improve tests --- pkg/controller/jenkins/groovy/groovy_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pkg/controller/jenkins/groovy/groovy_test.go b/pkg/controller/jenkins/groovy/groovy_test.go index 5d3d8571..1650020a 100644 --- a/pkg/controller/jenkins/groovy/groovy_test.go +++ b/pkg/controller/jenkins/groovy/groovy_test.go @@ -132,6 +132,7 @@ func TestGroovy_EnsureSingle(t *testing.T) { defer ctrl.Finish() jenkinsClient := jenkinsclient.NewMockJenkins(ctrl) + jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil) jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil) jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil) @@ -163,6 +164,18 @@ func TestGroovy_EnsureSingle(t *testing.T) { assert.Equal(t, anotherHash, jenkins.Status.AppliedGroovyScripts[0].Hash) assert.Equal(t, source, jenkins.Status.AppliedGroovyScripts[0].Source) assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name) + + requeue, err = groovyClient.EnsureSingle(source, groovyScriptName, hash, groovyScript) + require.NoError(t, err) + assert.True(t, requeue) + + err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins) + require.NoError(t, err) + assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts)) + assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType) + assert.Equal(t, hash, jenkins.Status.AppliedGroovyScripts[0].Hash) + assert.Equal(t, source, jenkins.Status.AppliedGroovyScripts[0].Source) + assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name) }) t.Run("execute script fails", func(t *testing.T) { // given From 77606717f1d61fb429dfffc2de14ff819748b75a Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Tue, 23 Jul 2019 15:23:53 +0200 Subject: [PATCH 44/59] #54 Improve docs --- docs/getting-started.md | 126 +++++++++++++++++++++++++++++++--------- 1 file changed, 98 insertions(+), 28 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 6907e1b3..aa801eec 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -369,33 +369,34 @@ Example config file to modify and use: ## Jenkins Customisation -Jenkins can be customized using groovy scripts or configuration as code plugin. All custom configuration is stored in -the **jenkins-operator-user-configuration-** ConfigMap which is automatically created by **jenkins-operator**. +Jenkins can be customized using groovy scripts or [configuration as code plugin](https://github.com/jenkinsci/configuration-as-code-plugin). +By using [ConfigMap](https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/) you can create own **Jenkins** customized configuration. +Then you must reference the *ConfigMap* in **Jenkins** pod customization file in `spec.groovyScripts` or `spec.configurationAsCode` -**jenkins-operator** creates **jenkins-operator-user-configuration-** secret where user can store sensitive -information used for custom configuration. If you have entry in secret named `PASSWORD` then you can use it in -Configuration as Plugin as `adminAddress: "${PASSWORD}"`. +For example create *ConfigMap* with name `jenkins-operator-user-configuration`. Then, modify the **Jenkins** manifest to look like this: -``` -kubectl get secret jenkins-operator-user-configuration- -o yaml - -kind: Secret -apiVersion: v1 -type: Opaque +```yaml +apiVersion: jenkins.io/v1alpha2 +kind: Jenkins metadata: - name: jenkins-operator-user-configuration- - namespace: default -data: - SECRET_JENKINS_ADMIN_ADDRESS: YXNkZgo= - + name: example +spec: + configurationAsCode: + configurations: + - name: jenkins-operator-user-configuration + groovyScripts: + configurations: + - name: jenkins-operator-user-configuration ``` -``` -kubectl get configmap jenkins-operator-user-configuration- -o yaml - +Here is example of `jenkins-operator-user-configuration`: +```yaml apiVersion: v1 +kind: ConfigMap +metadata: + name: jenkins-operator-user-configuration data: - 1-configure-theme.groovy: |2 + 1-configure-theme.groovy: | import jenkins.* import jenkins.model.* import hudson.* @@ -415,19 +416,88 @@ data: decorator.save(); jenkins.save() - 1-system-message.yaml: |2 + 1-system-message.yaml: | jenkins: systemMessage: "Configuration as Code integration works!!!" - adminAddress: "${SECRET_JENKINS_ADMIN_ADDRESS}" +``` + +* *.groovy is Groovy script configuration +* *.yaml is configuration as code + +If you want to correct your configuration you can edit it while **jenkins-operator** is running. +Jenkins will reconcile and apply new configuration. + +### Using secrets inside Groovy script + +If you configured `spec.groovyScripts.secret.name`, then this secret is available to use inside map Groovy scripts. +The secrets are loaded to `secrets` map. + +Create a [secret](https://kubernetes.io/docs/concepts/configuration/secret/) with for eg. `jenkins-conf-secrets` name. + +```yaml +kind: Secret +apiVersion: v1 +type: Opaque +metadata: + name: jenkins-conf-secrets + namespace: default +data: + SYSTEM_MESSAGE: SGVsbG8gd29ybGQ= +``` + +Then modify the **Jenkins** pod manifest by changing `spec.groovyScripts.secret.name` to `jenkins-conf-secrets`. + +```yaml +apiVersion: jenkins.io/v1alpha2 +kind: Jenkins +metadata: + name: example +spec: + configurationAsCode: + configurations: + - name: jenkins-operator-user-configuration + secret: + name: jenkins-conf-secrets + groovyScripts: + configurations: + - name: jenkins-operator-user-configuration + secret: + name: jenkins-conf-secrets +``` + +Now you can test that the secret is mounted by applying this ConfigMap for Groovy script: + +```yaml +apiVersion: v1 kind: ConfigMap metadata: - name: jenkins-operator-user-configuration- - namespace: default -``` + name: jenkins-operator-user-configuration +data: + 1-system-message.groovy: | + import jenkins.* + import jenkins.model.* + import hudson.* + import hudson.model.* + Jenkins jenkins = Jenkins.getInstance() + + jenkins.setSystemMessage(secrets["SYSTEM_MESSAGE"]) + jenkins.save() +``` -When **jenkins-operator-user-configuration-** ConfigMap is updated Jenkins automatically -runs the **jenkins-operator-user-configuration** Jenkins Job which executes all scripts then -runs the **jenkins-operator-user-configuration-casc** Jenkins Job which applies Configuration as Code configuration. +Or by applying configuration as code: +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: jenkins-operator-user-configuration +data: + 1-system-message.yaml: | + jenkins: + systemMessage: ${SYSTEM_MESSAGE} +``` + + +After this, you should see the `Hello world` system message at **Jenkins** homepage. ## Install Plugins From 168110f84d9d5aef757f2a5e29ab3bb4fd4f43ea Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Thu, 25 Jul 2019 13:05:59 +0200 Subject: [PATCH 45/59] Fix groovy script versioning bug --- pkg/controller/jenkins/groovy/groovy.go | 2 +- pkg/controller/jenkins/groovy/groovy_test.go | 103 +++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/pkg/controller/jenkins/groovy/groovy.go b/pkg/controller/jenkins/groovy/groovy.go index ec9823ed..555d5e26 100644 --- a/pkg/controller/jenkins/groovy/groovy.go +++ b/pkg/controller/jenkins/groovy/groovy.go @@ -58,7 +58,7 @@ func (g *Groovy) EnsureSingle(source, name, hash, groovyScript string) (requeue var appliedGroovyScripts []v1alpha2.AppliedGroovyScript for _, ags := range g.jenkins.Status.AppliedGroovyScripts { - if ags.ConfigurationType != g.configurationType && ags.Source != source && ags.Name != name { + if ags.Source != source || ags.Name != name { appliedGroovyScripts = append(appliedGroovyScripts, ags) } } diff --git a/pkg/controller/jenkins/groovy/groovy_test.go b/pkg/controller/jenkins/groovy/groovy_test.go index 1650020a..fd3fe88f 100644 --- a/pkg/controller/jenkins/groovy/groovy_test.go +++ b/pkg/controller/jenkins/groovy/groovy_test.go @@ -177,6 +177,109 @@ func TestGroovy_EnsureSingle(t *testing.T) { assert.Equal(t, source, jenkins.Status.AppliedGroovyScripts[0].Source) assert.Equal(t, groovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name) }) + t.Run("execute three groovy scripts with another hash", func(t *testing.T) { + // given + firstGroovyScriptName := "testGroovy1" + firstGroovyScriptHash := "testHash1" + + secondGroovyScriptName := "testGroovy2" + secondGroovyScriptHash := "testHash2" + + thirdGroovyScriptName := "testGroovy3" + thirdGroovyScriptHash := "testHash3" + + jenkins := &v1alpha2.Jenkins{ + ObjectMeta: metav1.ObjectMeta{ + Name: jenkinsName, + Namespace: namespace, + }, + } + err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme) + require.NoError(t, err) + fakeClient := fake.NewFakeClient() + err = fakeClient.Create(ctx, jenkins) + require.NoError(t, err) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + jenkinsClient := jenkinsclient.NewMockJenkins(ctrl) + + jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil) + jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil) + jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil) + + groovyClient := New(jenkinsClient, fakeClient, log.Log, jenkins, configurationType, emptyCustomization) + + requeue, err := groovyClient.EnsureSingle(source, firstGroovyScriptName, firstGroovyScriptHash, groovyScript) + + // then + require.NoError(t, err) + assert.True(t, requeue) + err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins) + require.NoError(t, err) + assert.Equal(t, 1, len(jenkins.Status.AppliedGroovyScripts)) + assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[0].ConfigurationType) + assert.Equal(t, firstGroovyScriptHash, jenkins.Status.AppliedGroovyScripts[0].Hash) + assert.Equal(t, source, jenkins.Status.AppliedGroovyScripts[0].Source) + assert.Equal(t, firstGroovyScriptName, jenkins.Status.AppliedGroovyScripts[0].Name) + + requeue, err = groovyClient.EnsureSingle(source, secondGroovyScriptName, secondGroovyScriptHash, groovyScript) + // then + require.NoError(t, err) + assert.True(t, requeue) + err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins) + require.NoError(t, err) + assert.Equal(t, 2, len(jenkins.Status.AppliedGroovyScripts)) + assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[1].ConfigurationType) + assert.Equal(t, secondGroovyScriptHash, jenkins.Status.AppliedGroovyScripts[1].Hash) + assert.Equal(t, source, jenkins.Status.AppliedGroovyScripts[1].Source) + assert.Equal(t, secondGroovyScriptName, jenkins.Status.AppliedGroovyScripts[1].Name) + + requeue, err = groovyClient.EnsureSingle(source, thirdGroovyScriptName, thirdGroovyScriptHash, groovyScript) + // then + require.NoError(t, err) + assert.True(t, requeue) + err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins) + require.NoError(t, err) + assert.Equal(t, 3, len(jenkins.Status.AppliedGroovyScripts)) + assert.Equal(t, configurationType, jenkins.Status.AppliedGroovyScripts[2].ConfigurationType) + assert.Equal(t, thirdGroovyScriptHash, jenkins.Status.AppliedGroovyScripts[2].Hash) + assert.Equal(t, source, jenkins.Status.AppliedGroovyScripts[2].Source) + assert.Equal(t, thirdGroovyScriptName, jenkins.Status.AppliedGroovyScripts[2].Name) + + }) + t.Run("execute two groovy scripts with same names in two config maps", func(t *testing.T) { + jenkins := &v1alpha2.Jenkins{ + ObjectMeta: metav1.ObjectMeta{ + Name: jenkinsName, + Namespace: namespace, + }, + } + err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme) + require.NoError(t, err) + fakeClient := fake.NewFakeClient() + err = fakeClient.Create(ctx, jenkins) + require.NoError(t, err) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + jenkinsClient := jenkinsclient.NewMockJenkins(ctrl) + + jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil) + jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil) + + groovyClient := New(jenkinsClient, fakeClient, log.Log, jenkins, configurationType, emptyCustomization) + + requeue, err := groovyClient.EnsureSingle("test-conf1", "test.groovy", hash, groovyScript) + require.NoError(t, err) + assert.True(t, requeue) + + requeue, err = groovyClient.EnsureSingle("test-conf2", "test.groovy", "anotherHash", groovyScript) + require.NoError(t, err) + assert.True(t, requeue) + + assert.Equal(t, 2, len(jenkins.Status.AppliedGroovyScripts)) + }) t.Run("execute script fails", func(t *testing.T) { // given jenkins := &v1alpha2.Jenkins{ From 7a149c82579fb1c83039f4acb294e15f39fa7e97 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Thu, 25 Jul 2019 13:20:22 +0200 Subject: [PATCH 46/59] Improve tests --- pkg/controller/jenkins/groovy/groovy_test.go | 34 ++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/pkg/controller/jenkins/groovy/groovy_test.go b/pkg/controller/jenkins/groovy/groovy_test.go index fd3fe88f..b9a7a09b 100644 --- a/pkg/controller/jenkins/groovy/groovy_test.go +++ b/pkg/controller/jenkins/groovy/groovy_test.go @@ -280,6 +280,40 @@ func TestGroovy_EnsureSingle(t *testing.T) { assert.Equal(t, 2, len(jenkins.Status.AppliedGroovyScripts)) }) + t.Run("execute two groovy scripts with different configuration types", func(t *testing.T) { + jenkins := &v1alpha2.Jenkins{ + ObjectMeta: metav1.ObjectMeta{ + Name: jenkinsName, + Namespace: namespace, + }, + } + err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme) + require.NoError(t, err) + fakeClient := fake.NewFakeClient() + err = fakeClient.Create(ctx, jenkins) + require.NoError(t, err) + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + jenkinsClient := jenkinsclient.NewMockJenkins(ctrl) + + jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil) + jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil) + + groovyClient := New(jenkinsClient, fakeClient, log.Log, jenkins, configurationType, emptyCustomization) + + requeue, err := groovyClient.EnsureSingle("test-conf1", "test.groovy", hash, groovyScript) + require.NoError(t, err) + assert.True(t, requeue) + + groovyClient = New(jenkinsClient, fakeClient, log.Log, jenkins, "another-test-configuration-type", emptyCustomization) + + requeue, err = groovyClient.EnsureSingle("test-conf2", "test.groovy", "anotherHash", groovyScript) + require.NoError(t, err) + assert.True(t, requeue) + + assert.Equal(t, 2, len(jenkins.Status.AppliedGroovyScripts)) + }) t.Run("execute script fails", func(t *testing.T) { // given jenkins := &v1alpha2.Jenkins{ From 063f0c074b94932469b1c2971c9d0dac4a9a2e89 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Wed, 31 Jul 2019 10:51:36 +0200 Subject: [PATCH 47/59] Improve unit tests --- pkg/controller/jenkins/groovy/groovy.go | 2 +- pkg/controller/jenkins/groovy/groovy_test.go | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/controller/jenkins/groovy/groovy.go b/pkg/controller/jenkins/groovy/groovy.go index 555d5e26..15b594f7 100644 --- a/pkg/controller/jenkins/groovy/groovy.go +++ b/pkg/controller/jenkins/groovy/groovy.go @@ -58,7 +58,7 @@ func (g *Groovy) EnsureSingle(source, name, hash, groovyScript string) (requeue var appliedGroovyScripts []v1alpha2.AppliedGroovyScript for _, ags := range g.jenkins.Status.AppliedGroovyScripts { - if ags.Source != source || ags.Name != name { + if g.configurationType != ags.ConfigurationType || ags.Source != source || ags.Name != name { appliedGroovyScripts = append(appliedGroovyScripts, ags) } } diff --git a/pkg/controller/jenkins/groovy/groovy_test.go b/pkg/controller/jenkins/groovy/groovy_test.go index b9a7a09b..3bdd20ec 100644 --- a/pkg/controller/jenkins/groovy/groovy_test.go +++ b/pkg/controller/jenkins/groovy/groovy_test.go @@ -296,19 +296,18 @@ func TestGroovy_EnsureSingle(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() jenkinsClient := jenkinsclient.NewMockJenkins(ctrl) - jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil) jenkinsClient.EXPECT().ExecuteScript(groovyScript).Return("logs", nil) groovyClient := New(jenkinsClient, fakeClient, log.Log, jenkins, configurationType, emptyCustomization) - requeue, err := groovyClient.EnsureSingle("test-conf1", "test.groovy", hash, groovyScript) + requeue, err := groovyClient.EnsureSingle(source, "test.groovy", hash, groovyScript) require.NoError(t, err) assert.True(t, requeue) groovyClient = New(jenkinsClient, fakeClient, log.Log, jenkins, "another-test-configuration-type", emptyCustomization) - requeue, err = groovyClient.EnsureSingle("test-conf2", "test.groovy", "anotherHash", groovyScript) + requeue, err = groovyClient.EnsureSingle(source, "test.groovy", "anotherHash", groovyScript) require.NoError(t, err) assert.True(t, requeue) From 3a7a4049f3ebede040eab5501bb92455bd7b7da9 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Wed, 31 Jul 2019 11:11:15 +0200 Subject: [PATCH 48/59] Improve groovy script checker --- pkg/controller/jenkins/groovy/groovy.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/controller/jenkins/groovy/groovy.go b/pkg/controller/jenkins/groovy/groovy.go index 15b594f7..399ec216 100644 --- a/pkg/controller/jenkins/groovy/groovy.go +++ b/pkg/controller/jenkins/groovy/groovy.go @@ -58,9 +58,11 @@ func (g *Groovy) EnsureSingle(source, name, hash, groovyScript string) (requeue var appliedGroovyScripts []v1alpha2.AppliedGroovyScript for _, ags := range g.jenkins.Status.AppliedGroovyScripts { - if g.configurationType != ags.ConfigurationType || ags.Source != source || ags.Name != name { - appliedGroovyScripts = append(appliedGroovyScripts, ags) + if g.configurationType == ags.ConfigurationType && ags.Source == source && ags.Name == name { + continue } + + appliedGroovyScripts = append(appliedGroovyScripts, ags) } appliedGroovyScripts = append(appliedGroovyScripts, v1alpha2.AppliedGroovyScript{ ConfigurationType: g.configurationType, From 5cbd21b03c69bdad15a8932fa68e90058fb35539 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Wed, 31 Jul 2019 14:51:00 +0200 Subject: [PATCH 49/59] Add notification library --- go.mod | 4 + go.sum | 31 ++- internal/notifier/mailgun.go | 62 ++++++ internal/notifier/msteams.go | 90 +++++++++ internal/notifier/msteams_test.go | 59 ++++++ internal/notifier/sender.go | 179 ++++++++++++++++++ internal/notifier/slack.go | 95 ++++++++++ internal/notifier/slack_test.go | 60 ++++++ pkg/apis/jenkins/v1alpha2/jenkins_types.go | 55 ++++++ .../jenkins/v1alpha2/zz_generated.deepcopy.go | 91 +++++++++ .../jenkins/configuration/base/reconcile.go | 16 ++ 11 files changed, 739 insertions(+), 3 deletions(-) create mode 100644 internal/notifier/mailgun.go create mode 100644 internal/notifier/msteams.go create mode 100644 internal/notifier/msteams_test.go create mode 100644 internal/notifier/sender.go create mode 100644 internal/notifier/slack.go create mode 100644 internal/notifier/slack_test.go diff --git a/go.mod b/go.mod index 7c5a3bdd..d998e0c2 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,8 @@ require ( github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect github.com/docker/distribution v2.7.1+incompatible github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect + github.com/elazarl/goproxy v0.0.0-20190711103511-473e67f1d7d2 // indirect + github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 // indirect github.com/emicklei/go-restful v2.8.1+incompatible // indirect github.com/go-logr/logr v0.1.0 github.com/go-logr/zapr v0.1.0 @@ -22,12 +24,14 @@ require ( github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect github.com/grpc-ecosystem/grpc-gateway v1.8.5 // indirect github.com/imdario/mergo v0.3.6 // indirect + github.com/mailgun/mailgun-go/v3 v3.6.0 github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/opencontainers/go-digest v1.0.0-rc1 // indirect github.com/operator-framework/operator-sdk v0.8.2-0.20190522220659-031d71ef8154 github.com/pborman/uuid v0.0.0-20180906182336-adf5a7427709 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/errors v0.8.1 + github.com/sergi/go-diff v1.0.0 // indirect github.com/spf13/pflag v1.0.3 github.com/stretchr/testify v1.3.0 go.opencensus.io v0.19.2 // indirect diff --git a/go.sum b/go.sum index 5cee9964..59c1ac8d 100644 --- a/go.sum +++ b/go.sum @@ -40,13 +40,26 @@ github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c/go.mod h1:Qh8CwZ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/elazarl/goproxy v0.0.0-20190711103511-473e67f1d7d2 h1:aZtFdDNWY/yH86JPR2WX/PN63635VsE/f/nXNPAbYxY= +github.com/elazarl/goproxy v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM= +github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= github.com/emicklei/go-restful v2.8.1+incompatible h1:AyDqLHbJ1quqbWr/OWDw+PlIP8ZFoTmYrGYaxzrLbNg= github.com/emicklei/go-restful v2.8.1+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/evanphx/json-patch v4.0.0+incompatible h1:xregGRMLBeuRcwiOTHRCsPPuzCQlqhxUPbqdw+zNkLc= github.com/evanphx/json-patch v4.0.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ= +github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64= +github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= +github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= +github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y= +github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-chi/chi v4.0.0+incompatible h1:SiLLEDyAkqNnw+T/uDTf3aFB9T4FTrwMpuYrgaRcnW4= +github.com/go-chi/chi v4.0.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logr/logr v0.1.0 h1:M1Tv3VzNlEHg6uyACnRdtrploV2P7wZqH8BoQMtz0cg= @@ -83,6 +96,7 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck= github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= @@ -101,6 +115,7 @@ github.com/grpc-ecosystem/grpc-gateway v1.8.5 h1:2+KSC78XiO6Qy0hIjfc1OD9H+hsaJdJ github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= @@ -116,10 +131,14 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mailgun/mailgun-go/v3 v3.6.0 h1:oQWhyDTFjSiuO6vx1PRlfLZ7Fu+oK0Axn0UTREh3k/g= +github.com/mailgun/mailgun-go/v3 v3.6.0/go.mod h1:E81I5Agcfi/u1szdehi6p6ttdRX/UD3Rq2SrUzwyFIU= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/markbates/inflect v1.0.4 h1:5fh1gzTFhfae06u3hzHYO9xe3l3v3nW5Pwt3naLTP5g= @@ -134,7 +153,9 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= @@ -165,9 +186,12 @@ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1 h1:/K3IL0Z1quvmJ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.2.2 h1:J7U/N7eRtzjhs26d6GqMh2HBuXP8/Z64Densiiieafo= github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= @@ -203,8 +227,6 @@ golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f h1:hX65Cu3JDlGH3uEdK7I99Ii+9kjD6mvnnpfLdEAH0x4= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422 h1:QzoH/1pFpZguR8NrRHLcO6jKqfv2zpuSqZLgdm7ZmjI= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -259,7 +281,6 @@ golang.org/x/tools v0.0.0-20181219222714-6e267b5cc78e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190213015956-f7e1b50d2251/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138 h1:H3uGjxCR/6Ds0Mjgyp7LMK81+LvmbvWWEnJhzk1Pi9E= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190708203411-c8855242db9c h1:rRFNgkkT7zOyWlroLBmsrKYtBNhox8WtulQlOr3jIDk= @@ -282,12 +303,15 @@ google.golang.org/grpc v1.19.1 h1:TrBcJ1yqAl1G++wO39nD/qtgpsW9/1+QGrluyMGEYgM= google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -316,6 +340,7 @@ sigs.k8s.io/controller-runtime v0.1.10 h1:amLOmcekVdnsD1uIpmgRqfTbQWJ2qxvQkcdeFh sigs.k8s.io/controller-runtime v0.1.10/go.mod h1:HFAYoOh6XMV+jKF1UjFwrknPbowfyHEHHRdJMf2jMX8= sigs.k8s.io/controller-tools v0.1.11-0.20190411181648-9d55346c2bde h1:ZkaHf5rNYzIB6CB82keKMQNv7xxkqT0ylOBdfJPfi+k= sigs.k8s.io/controller-tools v0.1.11-0.20190411181648-9d55346c2bde/go.mod h1:ATWLRP3WGxuAN9HcT2LaKHReXIH+EZGzRuMHuxjXfhQ= +sigs.k8s.io/testing_frameworks v0.1.0 h1:2hBE1sDhKWALoqvhi2i/mnQOFZVfWtQFtsfH0QBTI0U= sigs.k8s.io/testing_frameworks v0.1.0/go.mod h1:VVBKrHmJ6Ekkfz284YKhQePcdycOzNH9qL6ht1zEr/U= sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/internal/notifier/mailgun.go b/internal/notifier/mailgun.go new file mode 100644 index 00000000..049f0c49 --- /dev/null +++ b/internal/notifier/mailgun.go @@ -0,0 +1,62 @@ +package notifier + +import ( + "context" + "fmt" + "github.com/mailgun/mailgun-go/v3" + "time" +) + +// Mailgun is service for sending emails +type Mailgun struct { + Domain string + Recipient string + From string +} + +// Send is function for sending directly to API +func (m Mailgun) Send(secret string, i *Information) error { + mg := mailgun.NewMailgun(m.Domain, secret) + + content := ` + + + + +

Jenkins Operator Reconciled

+

Failed to do something

+ + + + + + + + + + + + + +
CR name:%s
Configuration type:%s
Status:%s
+
Powered by Jenkins Operator <3
+ + + ` + + content = fmt.Sprintf(content, getStatusColor(i.Status, m), i.CrName, i.ConfigurationType, getStatusColor(i.Status, m), getStatusName(i.Status)) + + msg := mg.NewMessage(fmt.Sprintf("Jenkins Operator Notifier <%s>", m.From), "Jenkins Operator Status", "", m.Recipient) + msg.SetHtml(content) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + _, _, err := mg.Send(ctx, msg) + + if err != nil { + return err + } + + return nil +} diff --git a/internal/notifier/msteams.go b/internal/notifier/msteams.go new file mode 100644 index 00000000..9128d149 --- /dev/null +++ b/internal/notifier/msteams.go @@ -0,0 +1,90 @@ +package notifier + +import ( + "bytes" + "encoding/json" + "net/http" +) + +// Teams is Microsoft Teams Service +type Teams struct{} + +// TeamsMessage is representation of json message structure +type TeamsMessage struct { + Type string `json:"@type"` + Context string `json:"@context"` + ThemeColor StatusColor `json:"themeColor"` + Title string `json:"title"` + Sections []TeamsSection `json:"sections"` +} + +// TeamsSection is MS Teams message section +type TeamsSection struct { + Facts []TeamsFact `json:"facts"` + Text string `json:"text"` +} + +// TeamsFact is field where we can put content +type TeamsFact struct { + Name string `json:"name"` + Value string `json:"value"` +} + +// Send is function for sending directly to API +func (t Teams) Send(secret string, i *Information) error { + err := i.Error + var errMessage string + + if err != nil { + errMessage = err.Error() + } else { + errMessage = noErrorMessage + } + + msg, err := json.Marshal(TeamsMessage{ + Type: "MessageCard", + Context: "https://schema.org/extensions", + ThemeColor: getStatusColor(i.Status, t), + Title: titleText, + Sections: []TeamsSection{ + { + Facts: []TeamsFact{ + { + Name: crNameFieldName, + Value: i.CrName, + }, + { + Name: configurationTypeFieldName, + Value: i.ConfigurationType, + }, + { + Name: statusFieldName, + Value: getStatusName(i.Status), + }, + }, + Text: errMessage, + }, + }, + }) + + if err != nil { + return err + } + + request, err := http.NewRequest("POST", secret, bytes.NewBuffer(msg)) + if err != nil { + return err + } + + resp, err := client.Do(request) + if err != nil { + return err + } + + err = resp.Body.Close() + if err != nil { + return err + } + + return nil +} diff --git a/internal/notifier/msteams_test.go b/internal/notifier/msteams_test.go new file mode 100644 index 00000000..33d7af6c --- /dev/null +++ b/internal/notifier/msteams_test.go @@ -0,0 +1,59 @@ +package notifier + +import ( + "encoding/json" + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "testing" +) + +func TestTeams_Send(t *testing.T) { + teams := Teams{} + + i := &Information{ + ConfigurationType: testConfigurationType, + CrName: testCrName, + Status: testStatus, + Error: testError, + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var message TeamsMessage + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&message) + + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, message.Title, titleText) + assert.Equal(t, message.ThemeColor, getStatusColor(i.Status, teams)) + + mainSection := message.Sections[0] + + assert.Equal(t, mainSection.Text, noErrorMessage) + + for _, fact := range mainSection.Facts { + switch fact.Name { + case configurationTypeFieldName: + if fact.Value != i.ConfigurationType { + t.Fatalf("%s is not equal! Must be %s", configurationTypeFieldName, i.ConfigurationType) + } + case crNameFieldName: + if fact.Value != i.CrName { + t.Fatalf("%s is not equal! Must be %s", crNameFieldName, i.CrName) + } + case statusFieldName: + if fact.Value != getStatusName(i.Status) { + t.Fatalf("%s is not equal! Must be %s", statusFieldName, getStatusName(i.Status)) + } + } + } + })) + + defer server.Close() + if err := teams.Send(server.URL, i); err != nil { + t.Fatal(err) + } +} diff --git a/internal/notifier/sender.go b/internal/notifier/sender.go new file mode 100644 index 00000000..5c12669a --- /dev/null +++ b/internal/notifier/sender.go @@ -0,0 +1,179 @@ +package notifier + +import ( + "context" + "fmt" + "net/http" + + "github.com/go-logr/logr" + + "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" + "github.com/jenkinsci/kubernetes-operator/pkg/log" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + k8sclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +var ( + testConfigurationType = "test-configuration" + testCrName = "test-cr" + testStatus Status = 1 + testError error + + client = http.Client{} +) + +const ( + // StatusSuccess contains value for success state + StatusSuccess = 0 + + // StatusError contains value for error state + StatusError = 1 + + noErrorMessage = "No errors has found." + + titleText = "Operator reconciled." + statusMessageFieldName = "Status message" + statusFieldName = "Status" + crNameFieldName = "CR Name" + configurationTypeFieldName = "Configuration Type" + footerContent = "Powered by Jenkins Operator <3" +) + +// Status represents the state of operator +type Status int + +// StatusColor is useful for better UX +type StatusColor string + +// Information represents details about operator status +type Information struct { + ConfigurationType string + CrName string + Status Status + Error error +} + +// Notification contains message which will be sent +type Notification struct { + Jenkins *v1alpha2.Jenkins + K8sClient k8sclient.Client + Logger logr.Logger + + // Recipient is mobile number or email address + // It's not used in Slack or Microsoft Teams + Recipient string + + Information *Information +} + +// Service is skeleton for additional services +type Service interface { + Send(secret string, i *Information) error +} + +// Listen is goroutine that listens for incoming messages and sends it +func Listen(notification chan *Notification) { + n := <-notification + if len(n.Jenkins.Spec.Notification) > 0 { + for _, endpoint := range n.Jenkins.Spec.Notification { + var err error + var service Service + var selector v1alpha2.SecretKeySelector + secret := &corev1.Secret{} + + if endpoint.Slack != (v1alpha2.Slack{}) { + n.Logger.V(log.VDebug).Info("Slack detected") + service = Slack{} + selector = endpoint.Slack.URLSecretKeySelector + } else if endpoint.Teams != (v1alpha2.Teams{}) { + n.Logger.V(log.VDebug).Info("Microsoft Teams detected") + service = Teams{} + selector = endpoint.Teams.URLSecretKeySelector + } else if endpoint.Mailgun != (v1alpha2.Mailgun{}) { + n.Logger.V(log.VDebug).Info("Mailgun detected") + service = Mailgun{ + Domain: endpoint.Mailgun.Domain, + Recipient: endpoint.Mailgun.Recipient, + From: endpoint.Mailgun.From, + } + selector = endpoint.Mailgun.APIKeySecretKeySelector + } else { + n.Logger.Info("Notification service not found or not defined") + } + + err = n.K8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: n.Jenkins.Namespace}, secret) + if err != nil { + n.Logger.Info(fmt.Sprintf("Failed to get secret with name `%s`. %+v", selector.Name, err)) + } + + n.Logger.V(log.VDebug).Info(fmt.Sprintf("Endpoint URL: %s", string(secret.Data[selector.Key]))) + err = notify(service, string(secret.Data[selector.Key]), n.Information) + + if err != nil { + n.Logger.Info(fmt.Sprintf("Failed to send notifications. %+v", err)) + } else { + n.Logger.Info("Sent notification") + } + } + } +} + +func getStatusName(status Status) string { + switch status { + case StatusSuccess: + return "Success" + case StatusError: + return "Error" + default: + return "Undefined" + } +} + +func getStatusColor(status Status, service Service) StatusColor { + switch service.(type) { + case Slack: + switch status { + case StatusSuccess: + return "good" + case StatusError: + return "danger" + default: + return "#c8c8c8" + } + case Teams: + switch status { + case StatusSuccess: + return "54A254" + case StatusError: + return "E81123" + default: + return "C8C8C8" + } + case Mailgun: + switch status { + case StatusSuccess: + return "green" + case StatusError: + return "red" + default: + return "gray" + } + default: + return "#c8c8c8" + } +} + +func notify(service Service, secret string, i *Information) error { + var err error + switch svc := service.(type) { + case Slack: + err = svc.Send(secret, i) + case Teams: + err = svc.Send(secret, i) + case Mailgun: + err = svc.Send(secret, i) + } + + return err +} diff --git a/internal/notifier/slack.go b/internal/notifier/slack.go new file mode 100644 index 00000000..ca4308ee --- /dev/null +++ b/internal/notifier/slack.go @@ -0,0 +1,95 @@ +package notifier + +import ( + "bytes" + "encoding/json" + "net/http" +) + +// Slack is messaging service +type Slack struct{} + +// SlackMessage is representation of json message +type SlackMessage struct { + Text string `json:"text"` + Attachments []SlackAttachment `json:"attachments"` +} + +// SlackAttachment is representation of json attachment +type SlackAttachment struct { + Fallback string `json:"fallback"` + Color StatusColor `json:"color"` + Pretext string `json:"pretext"` + Title string `json:"title"` + Text string `json:"text"` + Fields []SlackField `json:"fields"` + Footer string `json:"footer"` +} + +// SlackField is representation of json field. +type SlackField struct { + Title string `json:"title"` + Value string `json:"value"` + Short bool `json:"short"` +} + +// Send is function for sending directly to API +func (s Slack) Send(secret string, i *Information) error { + err := i.Error + var errMessage string + + if err != nil { + errMessage = err.Error() + } else { + errMessage = noErrorMessage + } + + slackMessage, err := json.Marshal(SlackMessage{ + Attachments: []SlackAttachment{ + { + Fallback: "", + Color: getStatusColor(i.Status, s), + Text: titleText, + Fields: []SlackField{ + { + Title: statusMessageFieldName, + Value: errMessage, + Short: false, + }, + { + Title: crNameFieldName, + Value: i.CrName, + Short: true, + }, + { + Title: configurationTypeFieldName, + Value: i.ConfigurationType, + Short: true, + }, + }, + Footer: footerContent, + }, + }, + }) + + if err != nil { + return err + } + + request, err := http.NewRequest("POST", secret, bytes.NewBuffer(slackMessage)) + if err != nil { + return err + } + + resp, err := client.Do(request) + if err != nil { + return err + } + + err = resp.Body.Close() + if err != nil { + return err + } + + return nil +} diff --git a/internal/notifier/slack_test.go b/internal/notifier/slack_test.go new file mode 100644 index 00000000..98c9181b --- /dev/null +++ b/internal/notifier/slack_test.go @@ -0,0 +1,60 @@ +package notifier + +import ( + "encoding/json" + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "testing" +) + +func TestSlack_Send(t *testing.T) { + slack := Slack{} + + i := &Information{ + ConfigurationType: testConfigurationType, + CrName: testCrName, + Status: testStatus, + Error: testError, + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var message SlackMessage + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&message) + + if err != nil { + t.Fatal(err) + } + + mainAttachment := message.Attachments[0] + + assert.Equal(t, mainAttachment.Text, titleText) + + for _, field := range mainAttachment.Fields { + switch field.Title { + case configurationTypeFieldName: + if field.Value != i.ConfigurationType { + t.Fatalf("%s is not equal! Must be %s", configurationTypeFieldName, i.ConfigurationType) + } + case crNameFieldName: + if field.Value != i.CrName { + t.Fatalf("%s is not equal! Must be %s", crNameFieldName, i.CrName) + } + case statusMessageFieldName: + if field.Value != noErrorMessage { + t.Fatalf("Error thrown but not expected") + } + } + } + + assert.Equal(t, mainAttachment.Footer, footerContent) + assert.Equal(t, mainAttachment.Color, getStatusColor(i.Status, slack)) + })) + + defer server.Close() + + if err := slack.Send(server.URL, i); err != nil { + t.Fatal(err) + } +} diff --git a/pkg/apis/jenkins/v1alpha2/jenkins_types.go b/pkg/apis/jenkins/v1alpha2/jenkins_types.go index a961b7a9..25698f18 100644 --- a/pkg/apis/jenkins/v1alpha2/jenkins_types.go +++ b/pkg/apis/jenkins/v1alpha2/jenkins_types.go @@ -17,6 +17,9 @@ type JenkinsSpec struct { // +optional SeedJobs []SeedJob `json:"seedJobs,omitempty"` + // Notification defines services which are used to inform about Jenkins behavior + Notification []Notification `json:"notification,omitempty"` + // Service is Kubernetes service of Jenkins master HTTP pod // Defaults to : // port: 8080 @@ -50,6 +53,44 @@ type JenkinsSpec struct { ConfigurationAsCode ConfigurationAsCode `json:"configurationAsCode,omitempty"` } +// Notification is info sending service about Jenkins Operator +type Notification struct { + LoggingLevel JenkinsNotificationLogLevel `json:"loggingLevel"` + Verbose bool `json:"verbose"` + Name string `json:"name"` + Slack Slack `json:"slack,omitempty"` + Teams Teams `json:"teams,omitempty"` + Mailgun Mailgun `json:"mailgun,omitempty"` +} + +// Slack is handler for Slack +type Slack struct { + // The web hook url to Slack App + URLSecretKeySelector SecretKeySelector `json:"urlSecretKeySelector"` +} + +// Teams is handler for Microsoft Teams +type Teams struct { + // The web hook url to Teams App + URLSecretKeySelector SecretKeySelector `json:"urlSecretKeySelector"` +} + +// Mailgun is handler for Mailgun email service +type Mailgun struct { + Domain string `json:"domain"` + APIKeySecretKeySelector SecretKeySelector `json:"apiKeySecretKeySelector"` + Recipient string `json:"recipient"` + From string `json:"from"` +} + +// SecretKeySelector selects a key of a Secret. +type SecretKeySelector struct { + // The name of the secret in the pod's namespace to select from. + corev1.LocalObjectReference `json:",inline" protobuf:"bytes,1,opt,name=localObjectReference"` + // The key of the secret to select from. Must be a valid secret key. + Key string `json:"key" protobuf:"bytes,2,opt,name=key"` +} + // Container defines Kubernetes container attributes type Container struct { // Name of the container specified as a DNS_LABEL. @@ -443,6 +484,20 @@ const ( UsernamePasswordCredentialType JenkinsCredentialType = "usernamePassword" ) +// JenkinsNotificationLogLevel defines type of Notification feature frequency of sending logger entries +type JenkinsNotificationLogLevel string + +const ( + // LogLevelNone - No logs + LogLevelNone JenkinsNotificationLogLevel = "" + + // LogLevelWarning - Only Warnings + LogLevelWarning JenkinsNotificationLogLevel = "warning" + + // LogLevelInfo - Only info + LogLevelInfo JenkinsNotificationLogLevel = "info" +) + // AllowedJenkinsCredentialMap contains all allowed Jenkins credentials types var AllowedJenkinsCredentialMap = map[string]string{ string(NoJenkinsCredentialCredentialType): "", diff --git a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go index 33b5ff35..96caca6f 100644 --- a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go @@ -366,6 +366,11 @@ func (in *JenkinsSpec) DeepCopyInto(out *JenkinsSpec) { *out = make([]SeedJob, len(*in)) copy(*out, *in) } + if in.Notification != nil { + in, out := &in.Notification, &out.Notification + *out = make([]Notification, len(*in)) + copy(*out, *in) + } in.Service.DeepCopyInto(&out.Service) in.SlaveService.DeepCopyInto(&out.SlaveService) in.Backup.DeepCopyInto(&out.Backup) @@ -430,6 +435,41 @@ func (in *JenkinsStatus) DeepCopy() *JenkinsStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Mailgun) DeepCopyInto(out *Mailgun) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Mailgun. +func (in *Mailgun) DeepCopy() *Mailgun { + if in == nil { + return nil + } + out := new(Mailgun) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Notification) DeepCopyInto(out *Notification) { + *out = *in + out.Slack = in.Slack + out.Teams = in.Teams + out.Mailgun = in.Mailgun + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Notification. +func (in *Notification) DeepCopy() *Notification { + if in == nil { + return nil + } + out := new(Notification) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Plugin) DeepCopyInto(out *Plugin) { *out = *in @@ -463,6 +503,23 @@ func (in *Restore) DeepCopy() *Restore { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretKeySelector) DeepCopyInto(out *SecretKeySelector) { + *out = *in + out.LocalObjectReference = in.LocalObjectReference + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretKeySelector. +func (in *SecretKeySelector) DeepCopy() *SecretKeySelector { + if in == nil { + return nil + } + out := new(SecretKeySelector) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SecretRef) DeepCopyInto(out *SecretRef) { *out = *in @@ -529,3 +586,37 @@ func (in *Service) DeepCopy() *Service { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Slack) DeepCopyInto(out *Slack) { + *out = *in + out.URLSecretKeySelector = in.URLSecretKeySelector + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Slack. +func (in *Slack) DeepCopy() *Slack { + if in == nil { + return nil + } + out := new(Slack) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Teams) DeepCopyInto(out *Teams) { + *out = *in + out.URLSecretKeySelector = in.URLSecretKeySelector + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Teams. +func (in *Teams) DeepCopy() *Teams { + if in == nil { + return nil + } + out := new(Teams) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/controller/jenkins/configuration/base/reconcile.go b/pkg/controller/jenkins/configuration/base/reconcile.go index ed8ac2f1..489332c9 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile.go +++ b/pkg/controller/jenkins/configuration/base/reconcile.go @@ -4,7 +4,9 @@ import ( "context" "crypto/sha256" "encoding/base64" + er "errors" "fmt" + "github.com/jenkinsci/kubernetes-operator/internal/notifier" "reflect" "strings" "time" @@ -124,6 +126,20 @@ func (r *ReconcileJenkinsBaseConfiguration) Reconcile() (reconcile.Result, jenki } result, err = r.ensureBaseConfiguration(jenkinsClient) + notificationChannel := make(chan *notifier.Notification) + go notifier.Listen(notificationChannel) + + notificationChannel <- ¬ifier.Notification{ + K8sClient: r.k8sClient, + Jenkins: r.jenkins, + Logger: r.logger, + Information: ¬ifier.Information{ + CrName: r.jenkins.Name, + ConfigurationType: "base", + Status: notifier.StatusError, + Error: er.New("failed to do something"), + }, + } return result, jenkinsClient, err } From 61d5311ac2c84aff97abfa300a270dbd91c017a2 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Wed, 31 Jul 2019 14:52:32 +0200 Subject: [PATCH 50/59] Disabled notification feature --- .../jenkins/configuration/base/reconcile.go | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/pkg/controller/jenkins/configuration/base/reconcile.go b/pkg/controller/jenkins/configuration/base/reconcile.go index 489332c9..ed8ac2f1 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile.go +++ b/pkg/controller/jenkins/configuration/base/reconcile.go @@ -4,9 +4,7 @@ import ( "context" "crypto/sha256" "encoding/base64" - er "errors" "fmt" - "github.com/jenkinsci/kubernetes-operator/internal/notifier" "reflect" "strings" "time" @@ -126,20 +124,6 @@ func (r *ReconcileJenkinsBaseConfiguration) Reconcile() (reconcile.Result, jenki } result, err = r.ensureBaseConfiguration(jenkinsClient) - notificationChannel := make(chan *notifier.Notification) - go notifier.Listen(notificationChannel) - - notificationChannel <- ¬ifier.Notification{ - K8sClient: r.k8sClient, - Jenkins: r.jenkins, - Logger: r.logger, - Information: ¬ifier.Information{ - CrName: r.jenkins.Name, - ConfigurationType: "base", - Status: notifier.StatusError, - Error: er.New("failed to do something"), - }, - } return result, jenkinsClient, err } From 75f95be65afd2d7edfd5730b72b3a85e5f34d0ad Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Thu, 1 Aug 2019 11:16:17 +0200 Subject: [PATCH 51/59] Improve notification mechanism --- internal/notifier/mailgun.go | 27 ++++- internal/notifier/msteams.go | 52 ++++++--- internal/notifier/msteams_test.go | 41 +++---- internal/notifier/sender.go | 186 +++++++++++++----------------- internal/notifier/slack.go | 55 ++++++--- internal/notifier/slack_test.go | 39 ++++--- 6 files changed, 214 insertions(+), 186 deletions(-) diff --git a/internal/notifier/mailgun.go b/internal/notifier/mailgun.go index 049f0c49..4c92e7fd 100644 --- a/internal/notifier/mailgun.go +++ b/internal/notifier/mailgun.go @@ -3,8 +3,14 @@ package notifier import ( "context" "fmt" - "github.com/mailgun/mailgun-go/v3" "time" + + "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" + "github.com/jenkinsci/kubernetes-operator/pkg/log" + + "github.com/mailgun/mailgun-go/v3" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" ) // Mailgun is service for sending emails @@ -15,8 +21,19 @@ type Mailgun struct { } // Send is function for sending directly to API -func (m Mailgun) Send(secret string, i *Information) error { - mg := mailgun.NewMailgun(m.Domain, secret) +func (m Mailgun) Send(n *Notification) error { + var selector v1alpha2.SecretKeySelector + secret := &corev1.Secret{} + + i := n.Information + + err := n.K8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: n.Jenkins.Namespace}, secret) + if err != nil { + n.Logger.V(log.VWarn).Info(fmt.Sprintf("Failed to get secret with name `%s`. %+v", selector.Name, err)) + return err + } + + mg := mailgun.NewMailgun(m.Domain, secret.StringData[selector.Name]) content := ` ` - content = fmt.Sprintf(content, getStatusColor(i.Status, m), i.CrName, i.ConfigurationType, getStatusColor(i.Status, m), getStatusName(i.Status)) + content = fmt.Sprintf(content, getStatusColor(i.LogLevel, m), i.CrName, i.ConfigurationType, getStatusColor(i.LogLevel, m), string(i.LogLevel)) msg := mg.NewMessage(fmt.Sprintf("Jenkins Operator Notifier <%s>", m.From), "Jenkins Operator Status", "", m.Recipient) msg.SetHtml(content) ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() - _, _, err := mg.Send(ctx, msg) + _, _, err = mg.Send(ctx, msg) if err != nil { return err diff --git a/internal/notifier/msteams.go b/internal/notifier/msteams.go index 9128d149..596664be 100644 --- a/internal/notifier/msteams.go +++ b/internal/notifier/msteams.go @@ -2,12 +2,22 @@ package notifier import ( "bytes" + "context" "encoding/json" + "fmt" "net/http" + + "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" + "github.com/jenkinsci/kubernetes-operator/pkg/log" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" ) // Teams is Microsoft Teams Service -type Teams struct{} +type Teams struct { + apiURL string +} // TeamsMessage is representation of json message structure type TeamsMessage struct { @@ -31,20 +41,16 @@ type TeamsFact struct { } // Send is function for sending directly to API -func (t Teams) Send(secret string, i *Information) error { - err := i.Error - var errMessage string +func (t Teams) Send(n *Notification) error { + var selector v1alpha2.SecretKeySelector + secret := &corev1.Secret{} - if err != nil { - errMessage = err.Error() - } else { - errMessage = noErrorMessage - } + i := n.Information msg, err := json.Marshal(TeamsMessage{ Type: "MessageCard", Context: "https://schema.org/extensions", - ThemeColor: getStatusColor(i.Status, t), + ThemeColor: getStatusColor(i.LogLevel, t), Title: titleText, Sections: []TeamsSection{ { @@ -58,20 +64,33 @@ func (t Teams) Send(secret string, i *Information) error { Value: i.ConfigurationType, }, { - Name: statusFieldName, - Value: getStatusName(i.Status), + Name: loggingLevelFieldName, + Value: string(i.LogLevel), + }, + { + Name: namespaceFieldName, + Value: i.Namespace, }, }, - Text: errMessage, + Text: i.Message, }, }, }) + if t.apiURL == "" { + err := n.K8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: n.Jenkins.Namespace}, secret) + if err != nil { + n.Logger.V(log.VWarn).Info(fmt.Sprintf("Failed to get secret with name `%s`. %+v", selector.Name, err)) + } + + t.apiURL = secret.StringData[selector.Name] + } + if err != nil { return err } - request, err := http.NewRequest("POST", secret, bytes.NewBuffer(msg)) + request, err := http.NewRequest("POST", t.apiURL, bytes.NewBuffer(msg)) if err != nil { return err } @@ -81,10 +100,7 @@ func (t Teams) Send(secret string, i *Information) error { return err } - err = resp.Body.Close() - if err != nil { - return err - } + defer func() { _ = resp.Body.Close() }() return nil } diff --git a/internal/notifier/msteams_test.go b/internal/notifier/msteams_test.go index 33d7af6c..cda82db2 100644 --- a/internal/notifier/msteams_test.go +++ b/internal/notifier/msteams_test.go @@ -2,20 +2,25 @@ package notifier import ( "encoding/json" - "github.com/stretchr/testify/assert" "net/http" "net/http/httptest" "testing" + + "github.com/stretchr/testify/assert" ) func TestTeams_Send(t *testing.T) { - teams := Teams{} - i := &Information{ ConfigurationType: testConfigurationType, CrName: testCrName, - Status: testStatus, - Error: testError, + Message: testMessage, + MessageVerbose: testMessageVerbose, + Namespace: testNamespace, + LogLevel: testLoggingLevel, + } + + notification := &Notification{ + Information: i, } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -28,32 +33,28 @@ func TestTeams_Send(t *testing.T) { } assert.Equal(t, message.Title, titleText) - assert.Equal(t, message.ThemeColor, getStatusColor(i.Status, teams)) + assert.Equal(t, message.ThemeColor, getStatusColor(i.LogLevel, Teams{})) mainSection := message.Sections[0] - assert.Equal(t, mainSection.Text, noErrorMessage) + assert.Equal(t, mainSection.Text, i.Message) for _, fact := range mainSection.Facts { switch fact.Name { case configurationTypeFieldName: - if fact.Value != i.ConfigurationType { - t.Fatalf("%s is not equal! Must be %s", configurationTypeFieldName, i.ConfigurationType) - } + assert.Equal(t, fact.Value, i.ConfigurationType) case crNameFieldName: - if fact.Value != i.CrName { - t.Fatalf("%s is not equal! Must be %s", crNameFieldName, i.CrName) - } - case statusFieldName: - if fact.Value != getStatusName(i.Status) { - t.Fatalf("%s is not equal! Must be %s", statusFieldName, getStatusName(i.Status)) - } + assert.Equal(t, fact.Value, i.CrName) + case messageFieldName: + assert.Equal(t, fact.Value, i.Message) + case loggingLevelFieldName: + assert.Equal(t, fact.Value, string(i.LogLevel)) } } })) + teams := Teams{apiURL: server.URL} + defer server.Close() - if err := teams.Send(server.URL, i); err != nil { - t.Fatal(err) - } + assert.NoError(t, teams.Send(notification)) } diff --git a/internal/notifier/sender.go b/internal/notifier/sender.go index 5c12669a..a8b802aa 100644 --- a/internal/notifier/sender.go +++ b/internal/notifier/sender.go @@ -1,160 +1,132 @@ package notifier import ( - "context" "fmt" - "net/http" - "github.com/go-logr/logr" + "net/http" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" "github.com/jenkinsci/kubernetes-operator/pkg/log" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/types" + k8sclient "sigs.k8s.io/controller-runtime/pkg/client" ) +const ( + // LogWarn is warning log entry + LogWarn LoggingLevel = "warn" + + // LogInfo is info log entry + LogInfo LoggingLevel = "info" + + titleText = "Operator reconciled." + messageFieldName = "Message" + loggingLevelFieldName = "Logging Level" + crNameFieldName = "CR Name" + configurationTypeFieldName = "Configuration Type" + namespaceFieldName = "Namespace" + footerContent = "Powered by Jenkins Operator <3" +) + var ( - testConfigurationType = "test-configuration" - testCrName = "test-cr" - testStatus Status = 1 - testError error + testConfigurationType = "test-configuration" + testCrName = "test-cr" + testNamespace = "test-namespace" + testMessage = "test-message" + testMessageVerbose = "detail-test-message" + testLoggingLevel = LogWarn client = http.Client{} ) -const ( - // StatusSuccess contains value for success state - StatusSuccess = 0 - - // StatusError contains value for error state - StatusError = 1 - - noErrorMessage = "No errors has found." - - titleText = "Operator reconciled." - statusMessageFieldName = "Status message" - statusFieldName = "Status" - crNameFieldName = "CR Name" - configurationTypeFieldName = "Configuration Type" - footerContent = "Powered by Jenkins Operator <3" -) - -// Status represents the state of operator -type Status int - // StatusColor is useful for better UX type StatusColor string +// LoggingLevel is type for selecting different logging levels +type LoggingLevel string + // Information represents details about operator status type Information struct { ConfigurationType string + Namespace string CrName string - Status Status - Error error + LogLevel LoggingLevel + Message string + MessageVerbose string } // Notification contains message which will be sent type Notification struct { - Jenkins *v1alpha2.Jenkins - K8sClient k8sclient.Client - Logger logr.Logger - - // Recipient is mobile number or email address - // It's not used in Slack or Microsoft Teams - Recipient string - + Jenkins *v1alpha2.Jenkins + K8sClient k8sclient.Client + Logger logr.Logger Information *Information } // Service is skeleton for additional services -type Service interface { - Send(secret string, i *Information) error +type service interface { + Send(i *Notification) error } // Listen is goroutine that listens for incoming messages and sends it func Listen(notification chan *Notification) { - n := <-notification - if len(n.Jenkins.Spec.Notification) > 0 { - for _, endpoint := range n.Jenkins.Spec.Notification { - var err error - var service Service - var selector v1alpha2.SecretKeySelector - secret := &corev1.Secret{} + <-notification + for n := range notification { + if len(n.Jenkins.Spec.Notification) > 0 { + for _, endpoint := range n.Jenkins.Spec.Notification { + var err error + var svc service - if endpoint.Slack != (v1alpha2.Slack{}) { - n.Logger.V(log.VDebug).Info("Slack detected") - service = Slack{} - selector = endpoint.Slack.URLSecretKeySelector - } else if endpoint.Teams != (v1alpha2.Teams{}) { - n.Logger.V(log.VDebug).Info("Microsoft Teams detected") - service = Teams{} - selector = endpoint.Teams.URLSecretKeySelector - } else if endpoint.Mailgun != (v1alpha2.Mailgun{}) { - n.Logger.V(log.VDebug).Info("Mailgun detected") - service = Mailgun{ - Domain: endpoint.Mailgun.Domain, - Recipient: endpoint.Mailgun.Recipient, - From: endpoint.Mailgun.From, + if endpoint.Slack != (v1alpha2.Slack{}) { + svc = Slack{} + } else if endpoint.Teams != (v1alpha2.Teams{}) { + svc = Teams{} + } else if endpoint.Mailgun != (v1alpha2.Mailgun{}) { + svc = Mailgun{ + Domain: endpoint.Mailgun.Domain, + Recipient: endpoint.Mailgun.Recipient, + From: endpoint.Mailgun.From, + } + } else { + n.Logger.V(log.VWarn).Info("Notification service not found or not defined") } - selector = endpoint.Mailgun.APIKeySecretKeySelector - } else { - n.Logger.Info("Notification service not found or not defined") - } - err = n.K8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: n.Jenkins.Namespace}, secret) - if err != nil { - n.Logger.Info(fmt.Sprintf("Failed to get secret with name `%s`. %+v", selector.Name, err)) - } + err = notify(svc, n) - n.Logger.V(log.VDebug).Info(fmt.Sprintf("Endpoint URL: %s", string(secret.Data[selector.Key]))) - err = notify(service, string(secret.Data[selector.Key]), n.Information) - - if err != nil { - n.Logger.Info(fmt.Sprintf("Failed to send notifications. %+v", err)) - } else { - n.Logger.Info("Sent notification") + if err != nil { + n.Logger.V(log.VWarn).Info(fmt.Sprintf("Failed to send notifications. %+v", err)) + } else { + n.Logger.V(log.VDebug).Info("Sent notification") + } } } } } -func getStatusName(status Status) string { - switch status { - case StatusSuccess: - return "Success" - case StatusError: - return "Error" - default: - return "Undefined" - } -} - -func getStatusColor(status Status, service Service) StatusColor { - switch service.(type) { +func getStatusColor(logLevel LoggingLevel, svc service) StatusColor { + switch svc.(type) { case Slack: - switch status { - case StatusSuccess: - return "good" - case StatusError: + switch logLevel { + case LogInfo: + return "#439FE0" + case LogWarn: return "danger" default: return "#c8c8c8" } case Teams: - switch status { - case StatusSuccess: - return "54A254" - case StatusError: + switch logLevel { + case LogInfo: + return "439FE0" + case LogWarn: return "E81123" default: return "C8C8C8" } case Mailgun: - switch status { - case StatusSuccess: - return "green" - case StatusError: + switch logLevel { + case LogInfo: + return "blue" + case LogWarn: return "red" default: return "gray" @@ -164,15 +136,15 @@ func getStatusColor(status Status, service Service) StatusColor { } } -func notify(service Service, secret string, i *Information) error { +func notify(svc service, n *Notification) error { var err error - switch svc := service.(type) { + switch s := svc.(type) { case Slack: - err = svc.Send(secret, i) + err = s.Send(n) case Teams: - err = svc.Send(secret, i) + err = s.Send(n) case Mailgun: - err = svc.Send(secret, i) + err = s.Send(n) } return err diff --git a/internal/notifier/slack.go b/internal/notifier/slack.go index ca4308ee..1222d80d 100644 --- a/internal/notifier/slack.go +++ b/internal/notifier/slack.go @@ -2,12 +2,22 @@ package notifier import ( "bytes" + "context" "encoding/json" + "fmt" "net/http" + + "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" + "github.com/jenkinsci/kubernetes-operator/pkg/log" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" ) // Slack is messaging service -type Slack struct{} +type Slack struct { + apiURL string +} // SlackMessage is representation of json message type SlackMessage struct { @@ -34,26 +44,31 @@ type SlackField struct { } // Send is function for sending directly to API -func (s Slack) Send(secret string, i *Information) error { - err := i.Error - var errMessage string +func (s Slack) Send(n *Notification) error { + var selector v1alpha2.SecretKeySelector + secret := &corev1.Secret{} - if err != nil { - errMessage = err.Error() - } else { - errMessage = noErrorMessage + i := n.Information + + if s.apiURL == "" { + err := n.K8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: n.Jenkins.Namespace}, secret) + if err != nil { + n.Logger.V(log.VWarn).Info(fmt.Sprintf("Failed to get secret with name `%s`. %+v", selector.Name, err)) + } + + s.apiURL = secret.StringData[selector.Name] } slackMessage, err := json.Marshal(SlackMessage{ Attachments: []SlackAttachment{ { Fallback: "", - Color: getStatusColor(i.Status, s), + Color: getStatusColor(i.LogLevel, s), Text: titleText, Fields: []SlackField{ { - Title: statusMessageFieldName, - Value: errMessage, + Title: messageFieldName, + Value: i.Message, Short: false, }, { @@ -66,6 +81,16 @@ func (s Slack) Send(secret string, i *Information) error { Value: i.ConfigurationType, Short: true, }, + { + Title: loggingLevelFieldName, + Value: string(i.LogLevel), + Short: true, + }, + { + Title: namespaceFieldName, + Value: i.Namespace, + Short: true, + }, }, Footer: footerContent, }, @@ -76,7 +101,7 @@ func (s Slack) Send(secret string, i *Information) error { return err } - request, err := http.NewRequest("POST", secret, bytes.NewBuffer(slackMessage)) + request, err := http.NewRequest("POST", s.apiURL, bytes.NewBuffer(slackMessage)) if err != nil { return err } @@ -86,10 +111,6 @@ func (s Slack) Send(secret string, i *Information) error { return err } - err = resp.Body.Close() - if err != nil { - return err - } - + defer func() { _ = resp.Body.Close() }() return nil } diff --git a/internal/notifier/slack_test.go b/internal/notifier/slack_test.go index 98c9181b..b1776bd6 100644 --- a/internal/notifier/slack_test.go +++ b/internal/notifier/slack_test.go @@ -2,20 +2,25 @@ package notifier import ( "encoding/json" - "github.com/stretchr/testify/assert" "net/http" "net/http/httptest" "testing" + + "github.com/stretchr/testify/assert" ) func TestSlack_Send(t *testing.T) { - slack := Slack{} - i := &Information{ ConfigurationType: testConfigurationType, CrName: testCrName, - Status: testStatus, - Error: testError, + Message: testMessage, + MessageVerbose: testMessageVerbose, + Namespace: testNamespace, + LogLevel: testLoggingLevel, + } + + notification := &Notification{ + Information: i, } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -34,27 +39,23 @@ func TestSlack_Send(t *testing.T) { for _, field := range mainAttachment.Fields { switch field.Title { case configurationTypeFieldName: - if field.Value != i.ConfigurationType { - t.Fatalf("%s is not equal! Must be %s", configurationTypeFieldName, i.ConfigurationType) - } + assert.Equal(t, field.Value, i.ConfigurationType) case crNameFieldName: - if field.Value != i.CrName { - t.Fatalf("%s is not equal! Must be %s", crNameFieldName, i.CrName) - } - case statusMessageFieldName: - if field.Value != noErrorMessage { - t.Fatalf("Error thrown but not expected") - } + assert.Equal(t, field.Value, i.CrName) + case messageFieldName: + assert.Equal(t, field.Value, i.Message) + case loggingLevelFieldName: + assert.Equal(t, field.Value, string(i.LogLevel)) } } assert.Equal(t, mainAttachment.Footer, footerContent) - assert.Equal(t, mainAttachment.Color, getStatusColor(i.Status, slack)) + assert.Equal(t, mainAttachment.Color, getStatusColor(i.LogLevel, Slack{})) })) defer server.Close() - if err := slack.Send(server.URL, i); err != nil { - t.Fatal(err) - } + slack := Slack{apiURL: server.URL} + + assert.NoError(t, slack.Send(notification)) } From da31b3b7dd96393ea16ada4207eca1741d34fc46 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Thu, 1 Aug 2019 16:13:18 +0200 Subject: [PATCH 52/59] Enhance notification services mechanism --- internal/notifier/mailgun.go | 63 +++++++++++++++---------------- internal/notifier/msteams.go | 26 ++++++------- internal/notifier/msteams_test.go | 44 +++++++++++++++++++-- internal/notifier/sender.go | 57 ++++++++++++---------------- internal/notifier/slack.go | 24 ++++++------ internal/notifier/slack_test.go | 44 +++++++++++++++++++-- 6 files changed, 161 insertions(+), 97 deletions(-) diff --git a/internal/notifier/mailgun.go b/internal/notifier/mailgun.go index 4c92e7fd..74f524c5 100644 --- a/internal/notifier/mailgun.go +++ b/internal/notifier/mailgun.go @@ -13,29 +13,7 @@ import ( "k8s.io/apimachinery/pkg/types" ) -// Mailgun is service for sending emails -type Mailgun struct { - Domain string - Recipient string - From string -} - -// Send is function for sending directly to API -func (m Mailgun) Send(n *Notification) error { - var selector v1alpha2.SecretKeySelector - secret := &corev1.Secret{} - - i := n.Information - - err := n.K8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: n.Jenkins.Namespace}, secret) - if err != nil { - n.Logger.V(log.VWarn).Info(fmt.Sprintf("Failed to get secret with name `%s`. %+v", selector.Name, err)) - return err - } - - mg := mailgun.NewMailgun(m.Domain, secret.StringData[selector.Name]) - - content := ` +const content = ` @@ -59,21 +37,40 @@ func (m Mailgun) Send(n *Notification) error {
Powered by Jenkins Operator <3
- - ` +` - content = fmt.Sprintf(content, getStatusColor(i.LogLevel, m), i.CrName, i.ConfigurationType, getStatusColor(i.LogLevel, m), string(i.LogLevel)) +// Mailgun is service for sending emails +type Mailgun struct{} - msg := mg.NewMessage(fmt.Sprintf("Jenkins Operator Notifier <%s>", m.From), "Jenkins Operator Status", "", m.Recipient) - msg.SetHtml(content) +// Send is function for sending directly to API +func (m Mailgun) Send(n *Notification, config v1alpha2.Notification) error { + var selector v1alpha2.SecretKeySelector + secret := &corev1.Secret{} + i := n.Information + + selector = config.Mailgun.APIKeySecretKeySelector + + err := n.K8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: n.Jenkins.Namespace}, secret) + if err != nil { + n.Logger.V(log.VWarn).Info(fmt.Sprintf("Failed to get secret with name `%s`. %+v", selector.Name, err)) + return err + } + + secretValue := string(secret.Data[selector.Name]) + if secretValue == "" { + return fmt.Errorf("SecretValue %s is empty", selector.Name) + } + + mg := mailgun.NewMailgun(config.Mailgun.Domain, secretValue) + + htmlMessage := fmt.Sprintf(content, getStatusColor(i.LogLevel, m), i.CrName, i.ConfigurationType, getStatusColor(i.LogLevel, m), string(i.LogLevel)) + + msg := mg.NewMessage(fmt.Sprintf("Jenkins Operator Notifier <%s>", config.Mailgun.From), "Jenkins Operator Status", "", config.Mailgun.Recipient) + msg.SetHtml(htmlMessage) ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() _, _, err = mg.Send(ctx, msg) - if err != nil { - return err - } - - return nil + return err } diff --git a/internal/notifier/msteams.go b/internal/notifier/msteams.go index 596664be..7ebea378 100644 --- a/internal/notifier/msteams.go +++ b/internal/notifier/msteams.go @@ -15,9 +15,7 @@ import ( ) // Teams is Microsoft Teams Service -type Teams struct { - apiURL string -} +type Teams struct{} // TeamsMessage is representation of json message structure type TeamsMessage struct { @@ -41,12 +39,18 @@ type TeamsFact struct { } // Send is function for sending directly to API -func (t Teams) Send(n *Notification) error { +func (t Teams) Send(n *Notification, config v1alpha2.Notification) error { var selector v1alpha2.SecretKeySelector secret := &corev1.Secret{} - i := n.Information + selector = config.Teams.URLSecretKeySelector + + err := n.K8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: n.Jenkins.Namespace}, secret) + if err != nil { + n.Logger.V(log.VWarn).Info(fmt.Sprintf("Failed to get secret with name `%s`. %+v", selector.Name, err)) + } + msg, err := json.Marshal(TeamsMessage{ Type: "MessageCard", Context: "https://schema.org/extensions", @@ -77,20 +81,16 @@ func (t Teams) Send(n *Notification) error { }, }) - if t.apiURL == "" { - err := n.K8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: n.Jenkins.Namespace}, secret) - if err != nil { - n.Logger.V(log.VWarn).Info(fmt.Sprintf("Failed to get secret with name `%s`. %+v", selector.Name, err)) - } - - t.apiURL = secret.StringData[selector.Name] + secretValue := string(secret.Data[selector.Key]) + if secretValue == "" { + return fmt.Errorf("SecretValue %s is empty", selector.Name) } if err != nil { return err } - request, err := http.NewRequest("POST", t.apiURL, bytes.NewBuffer(msg)) + request, err := http.NewRequest("POST", secretValue, bytes.NewBuffer(msg)) if err != nil { return err } diff --git a/internal/notifier/msteams_test.go b/internal/notifier/msteams_test.go index cda82db2..2205eff2 100644 --- a/internal/notifier/msteams_test.go +++ b/internal/notifier/msteams_test.go @@ -1,16 +1,26 @@ package notifier import ( + "context" "encoding/json" "net/http" "net/http/httptest" "testing" + "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" ) func TestTeams_Send(t *testing.T) { - i := &Information{ + fakeClient := fake.NewFakeClient() + testURLSelectorKeyName := "test-url-selector" + testSecretName := "test-secret" + + i := Information{ ConfigurationType: testConfigurationType, CrName: testCrName, Message: testMessage, @@ -20,6 +30,7 @@ func TestTeams_Send(t *testing.T) { } notification := &Notification{ + K8sClient: fakeClient, Information: i, } @@ -49,12 +60,39 @@ func TestTeams_Send(t *testing.T) { assert.Equal(t, fact.Value, i.Message) case loggingLevelFieldName: assert.Equal(t, fact.Value, string(i.LogLevel)) + case namespaceFieldName: + assert.Equal(t, fact.Value, i.Namespace) + default: + t.Fail() } } })) - teams := Teams{apiURL: server.URL} + teams := Teams{} defer server.Close() - assert.NoError(t, teams.Send(notification)) + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: testSecretName, + }, + + Data: map[string][]byte{ + testURLSelectorKeyName: []byte(server.URL), + }, + } + + err := notification.K8sClient.Create(context.TODO(), secret) + assert.NoError(t, err) + + assert.NoError(t, teams.Send(notification, v1alpha2.Notification{ + Teams: v1alpha2.Teams{ + URLSecretKeySelector: v1alpha2.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: testSecretName, + }, + Key: testURLSelectorKeyName, + }, + }, + })) } diff --git a/internal/notifier/sender.go b/internal/notifier/sender.go index a8b802aa..81e70ca5 100644 --- a/internal/notifier/sender.go +++ b/internal/notifier/sender.go @@ -56,48 +56,41 @@ type Information struct { // Notification contains message which will be sent type Notification struct { - Jenkins *v1alpha2.Jenkins + Jenkins v1alpha2.Jenkins K8sClient k8sclient.Client Logger logr.Logger - Information *Information + Information Information } // Service is skeleton for additional services type service interface { - Send(i *Notification) error + Send(i *Notification, config v1alpha2.Notification) error } // Listen is goroutine that listens for incoming messages and sends it func Listen(notification chan *Notification) { - <-notification for n := range notification { - if len(n.Jenkins.Spec.Notification) > 0 { - for _, endpoint := range n.Jenkins.Spec.Notification { - var err error - var svc service + notificationConfig := n.Jenkins.Spec.Notification + var err error + var svc service - if endpoint.Slack != (v1alpha2.Slack{}) { - svc = Slack{} - } else if endpoint.Teams != (v1alpha2.Teams{}) { - svc = Teams{} - } else if endpoint.Mailgun != (v1alpha2.Mailgun{}) { - svc = Mailgun{ - Domain: endpoint.Mailgun.Domain, - Recipient: endpoint.Mailgun.Recipient, - From: endpoint.Mailgun.From, - } - } else { - n.Logger.V(log.VWarn).Info("Notification service not found or not defined") - } + if notificationConfig.Slack != (v1alpha2.Slack{}) { + svc = Slack{} + } else if notificationConfig.Teams != (v1alpha2.Teams{}) { + svc = Teams{} + } else if notificationConfig.Mailgun != (v1alpha2.Mailgun{}) { + svc = Mailgun{} + } else { + n.Logger.V(log.VWarn).Info(fmt.Sprintf("Notification service in `%s` not found or not defined", notificationConfig.Name)) + continue + } - err = notify(svc, n) + err = notify(svc, n, notificationConfig) - if err != nil { - n.Logger.V(log.VWarn).Info(fmt.Sprintf("Failed to send notifications. %+v", err)) - } else { - n.Logger.V(log.VDebug).Info("Sent notification") - } - } + if err != nil { + n.Logger.V(log.VWarn).Info(fmt.Sprintf("Failed to send notifications. %+v", err)) + } else { + n.Logger.V(log.VDebug).Info("Sent notification") } } } @@ -136,15 +129,15 @@ func getStatusColor(logLevel LoggingLevel, svc service) StatusColor { } } -func notify(svc service, n *Notification) error { +func notify(svc service, n *Notification, nc v1alpha2.Notification) error { var err error switch s := svc.(type) { case Slack: - err = s.Send(n) + err = s.Send(n, nc) case Teams: - err = s.Send(n) + err = s.Send(n, nc) case Mailgun: - err = s.Send(n) + err = s.Send(n, nc) } return err diff --git a/internal/notifier/slack.go b/internal/notifier/slack.go index 1222d80d..0a009514 100644 --- a/internal/notifier/slack.go +++ b/internal/notifier/slack.go @@ -15,9 +15,7 @@ import ( ) // Slack is messaging service -type Slack struct { - apiURL string -} +type Slack struct{} // SlackMessage is representation of json message type SlackMessage struct { @@ -44,19 +42,16 @@ type SlackField struct { } // Send is function for sending directly to API -func (s Slack) Send(n *Notification) error { +func (s Slack) Send(n *Notification, config v1alpha2.Notification) error { var selector v1alpha2.SecretKeySelector secret := &corev1.Secret{} - i := n.Information - if s.apiURL == "" { - err := n.K8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: n.Jenkins.Namespace}, secret) - if err != nil { - n.Logger.V(log.VWarn).Info(fmt.Sprintf("Failed to get secret with name `%s`. %+v", selector.Name, err)) - } + selector = config.Slack.URLSecretKeySelector - s.apiURL = secret.StringData[selector.Name] + err := n.K8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: n.Jenkins.Namespace}, secret) + if err != nil { + n.Logger.V(log.VWarn).Info(fmt.Sprintf("Failed to get secret with name `%s`. %+v", selector.Name, err)) } slackMessage, err := json.Marshal(SlackMessage{ @@ -97,11 +92,16 @@ func (s Slack) Send(n *Notification) error { }, }) + secretValue := string(secret.Data[selector.Key]) + if secretValue == "" { + return fmt.Errorf("SecretValue %s is empty", selector.Name) + } + if err != nil { return err } - request, err := http.NewRequest("POST", s.apiURL, bytes.NewBuffer(slackMessage)) + request, err := http.NewRequest("POST", secretValue, bytes.NewBuffer(slackMessage)) if err != nil { return err } diff --git a/internal/notifier/slack_test.go b/internal/notifier/slack_test.go index b1776bd6..06da7aae 100644 --- a/internal/notifier/slack_test.go +++ b/internal/notifier/slack_test.go @@ -1,16 +1,26 @@ package notifier import ( + "context" "encoding/json" "net/http" "net/http/httptest" "testing" + "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" ) func TestSlack_Send(t *testing.T) { - i := &Information{ + fakeClient := fake.NewFakeClient() + testURLSelectorKeyName := "test-url-selector" + testSecretName := "test-secret" + + i := Information{ ConfigurationType: testConfigurationType, CrName: testCrName, Message: testMessage, @@ -20,6 +30,7 @@ func TestSlack_Send(t *testing.T) { } notification := &Notification{ + K8sClient: fakeClient, Information: i, } @@ -35,7 +46,6 @@ func TestSlack_Send(t *testing.T) { mainAttachment := message.Attachments[0] assert.Equal(t, mainAttachment.Text, titleText) - for _, field := range mainAttachment.Fields { switch field.Title { case configurationTypeFieldName: @@ -46,6 +56,10 @@ func TestSlack_Send(t *testing.T) { assert.Equal(t, field.Value, i.Message) case loggingLevelFieldName: assert.Equal(t, field.Value, string(i.LogLevel)) + case namespaceFieldName: + assert.Equal(t, field.Value, i.Namespace) + default: + t.Fail() } } @@ -55,7 +69,29 @@ func TestSlack_Send(t *testing.T) { defer server.Close() - slack := Slack{apiURL: server.URL} + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: testSecretName, + }, - assert.NoError(t, slack.Send(notification)) + Data: map[string][]byte{ + testURLSelectorKeyName: []byte(server.URL), + }, + } + + err := notification.K8sClient.Create(context.TODO(), secret) + assert.NoError(t, err) + + slack := Slack{} + + assert.NoError(t, slack.Send(notification, v1alpha2.Notification{ + Slack: v1alpha2.Slack{ + URLSecretKeySelector: v1alpha2.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: testSecretName, + }, + Key: testURLSelectorKeyName, + }, + }, + })) } From 16fd981703b99cc1716bcac575e81235953862b7 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Thu, 1 Aug 2019 16:36:26 +0200 Subject: [PATCH 53/59] Add notification to api --- pkg/apis/jenkins/v1alpha2/jenkins_types.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pkg/apis/jenkins/v1alpha2/jenkins_types.go b/pkg/apis/jenkins/v1alpha2/jenkins_types.go index 25698f18..d9634fd6 100644 --- a/pkg/apis/jenkins/v1alpha2/jenkins_types.go +++ b/pkg/apis/jenkins/v1alpha2/jenkins_types.go @@ -18,7 +18,8 @@ type JenkinsSpec struct { SeedJobs []SeedJob `json:"seedJobs,omitempty"` // Notification defines services which are used to inform about Jenkins behavior - Notification []Notification `json:"notification,omitempty"` + // Can be used to integrate chat services like Slack or Email services like Mailgun + Notification Notification `json:"notifications,omitempty"` // Service is Kubernetes service of Jenkins master HTTP pod // Defaults to : @@ -488,9 +489,6 @@ const ( type JenkinsNotificationLogLevel string const ( - // LogLevelNone - No logs - LogLevelNone JenkinsNotificationLogLevel = "" - // LogLevelWarning - Only Warnings LogLevelWarning JenkinsNotificationLogLevel = "warning" From dc727f45d71586ae857b565e15f98fb32ed33c1d Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Fri, 2 Aug 2019 09:38:17 +0200 Subject: [PATCH 54/59] Deepcopy fix --- .../jenkins/v1alpha2/zz_generated.deepcopy.go | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go index 96caca6f..ce270cab 100644 --- a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go @@ -260,6 +260,23 @@ func (in *Jenkins) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JenkinsAgent) DeepCopyInto(out *JenkinsAgent) { + *out = *in + in.Container.DeepCopyInto(&out.Container) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JenkinsAgent. +func (in *JenkinsAgent) DeepCopy() *JenkinsAgent { + if in == nil { + return nil + } + out := new(JenkinsAgent) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JenkinsList) DeepCopyInto(out *JenkinsList) { *out = *in @@ -366,11 +383,7 @@ func (in *JenkinsSpec) DeepCopyInto(out *JenkinsSpec) { *out = make([]SeedJob, len(*in)) copy(*out, *in) } - if in.Notification != nil { - in, out := &in.Notification, &out.Notification - *out = make([]Notification, len(*in)) - copy(*out, *in) - } + out.Notification = in.Notification in.Service.DeepCopyInto(&out.Service) in.SlaveService.DeepCopyInto(&out.SlaveService) in.Backup.DeepCopyInto(&out.Backup) @@ -438,6 +451,7 @@ func (in *JenkinsStatus) DeepCopy() *JenkinsStatus { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Mailgun) DeepCopyInto(out *Mailgun) { *out = *in + out.APIKeySecretKeySelector = in.APIKeySecretKeySelector return } From 364ce8ad8afe1aa36906412cede6fe968eb89e98 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Fri, 2 Aug 2019 09:51:42 +0200 Subject: [PATCH 55/59] Fix deepcopy --- .../jenkins/v1alpha2/zz_generated.deepcopy.go | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go index ce270cab..0ed66359 100644 --- a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go @@ -260,23 +260,6 @@ func (in *Jenkins) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *JenkinsAgent) DeepCopyInto(out *JenkinsAgent) { - *out = *in - in.Container.DeepCopyInto(&out.Container) - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JenkinsAgent. -func (in *JenkinsAgent) DeepCopy() *JenkinsAgent { - if in == nil { - return nil - } - out := new(JenkinsAgent) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JenkinsList) DeepCopyInto(out *JenkinsList) { *out = *in From 9bde4cb59fc49e68a92d5724ad08d14d559e8f1a Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Fri, 2 Aug 2019 14:07:37 +0200 Subject: [PATCH 56/59] Improve notification mechanism --- internal/notifier/mailgun.go | 9 +-- internal/notifier/msteams.go | 11 ++-- internal/notifier/sender.go | 55 +++++++++---------- internal/notifier/slack.go | 11 ++-- pkg/apis/jenkins/v1alpha2/jenkins_types.go | 8 +-- .../jenkins/v1alpha2/zz_generated.deepcopy.go | 6 +- 6 files changed, 46 insertions(+), 54 deletions(-) diff --git a/internal/notifier/mailgun.go b/internal/notifier/mailgun.go index 74f524c5..3bf190b9 100644 --- a/internal/notifier/mailgun.go +++ b/internal/notifier/mailgun.go @@ -3,11 +3,10 @@ package notifier import ( "context" "fmt" + "github.com/pkg/errors" "time" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" - "github.com/jenkinsci/kubernetes-operator/pkg/log" - "github.com/mailgun/mailgun-go/v3" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" @@ -44,21 +43,19 @@ type Mailgun struct{} // Send is function for sending directly to API func (m Mailgun) Send(n *Notification, config v1alpha2.Notification) error { - var selector v1alpha2.SecretKeySelector secret := &corev1.Secret{} i := n.Information - selector = config.Mailgun.APIKeySecretKeySelector + selector := config.Mailgun.APIKeySecretKeySelector err := n.K8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: n.Jenkins.Namespace}, secret) if err != nil { - n.Logger.V(log.VWarn).Info(fmt.Sprintf("Failed to get secret with name `%s`. %+v", selector.Name, err)) return err } secretValue := string(secret.Data[selector.Name]) if secretValue == "" { - return fmt.Errorf("SecretValue %s is empty", selector.Name) + return errors.Errorf("SecretValue %s is empty", selector.Name) } mg := mailgun.NewMailgun(config.Mailgun.Domain, secretValue) diff --git a/internal/notifier/msteams.go b/internal/notifier/msteams.go index 7ebea378..f1a1b290 100644 --- a/internal/notifier/msteams.go +++ b/internal/notifier/msteams.go @@ -4,12 +4,10 @@ import ( "bytes" "context" "encoding/json" - "fmt" + "github.com/pkg/errors" "net/http" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" - "github.com/jenkinsci/kubernetes-operator/pkg/log" - corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" ) @@ -40,15 +38,14 @@ type TeamsFact struct { // Send is function for sending directly to API func (t Teams) Send(n *Notification, config v1alpha2.Notification) error { - var selector v1alpha2.SecretKeySelector secret := &corev1.Secret{} i := n.Information - selector = config.Teams.URLSecretKeySelector + selector := config.Teams.URLSecretKeySelector err := n.K8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: n.Jenkins.Namespace}, secret) if err != nil { - n.Logger.V(log.VWarn).Info(fmt.Sprintf("Failed to get secret with name `%s`. %+v", selector.Name, err)) + return err } msg, err := json.Marshal(TeamsMessage{ @@ -83,7 +80,7 @@ func (t Teams) Send(n *Notification, config v1alpha2.Notification) error { secretValue := string(secret.Data[selector.Key]) if secretValue == "" { - return fmt.Errorf("SecretValue %s is empty", selector.Name) + return errors.Errorf("SecretValue %s is empty", selector.Name) } if err != nil { diff --git a/internal/notifier/sender.go b/internal/notifier/sender.go index 81e70ca5..9735fcc5 100644 --- a/internal/notifier/sender.go +++ b/internal/notifier/sender.go @@ -2,9 +2,9 @@ package notifier import ( "fmt" - "github.com/go-logr/logr" "net/http" + "github.com/go-logr/logr" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" "github.com/jenkinsci/kubernetes-operator/pkg/log" @@ -70,27 +70,30 @@ type service interface { // Listen is goroutine that listens for incoming messages and sends it func Listen(notification chan *Notification) { for n := range notification { - notificationConfig := n.Jenkins.Spec.Notification - var err error - var svc service + if len(n.Jenkins.Spec.Notifications) > 0 { + for _, notificationConfig := range n.Jenkins.Spec.Notifications { + var err error + var svc service - if notificationConfig.Slack != (v1alpha2.Slack{}) { - svc = Slack{} - } else if notificationConfig.Teams != (v1alpha2.Teams{}) { - svc = Teams{} - } else if notificationConfig.Mailgun != (v1alpha2.Mailgun{}) { - svc = Mailgun{} - } else { - n.Logger.V(log.VWarn).Info(fmt.Sprintf("Notification service in `%s` not found or not defined", notificationConfig.Name)) - continue - } + if notificationConfig.Slack != (v1alpha2.Slack{}) { + svc = Slack{} + } else if notificationConfig.Teams != (v1alpha2.Teams{}) { + svc = Teams{} + } else if notificationConfig.Mailgun != (v1alpha2.Mailgun{}) { + svc = Mailgun{} + } else { + n.Logger.V(log.VWarn).Info(fmt.Sprintf("Notification service in `%s` not found or not defined", notificationConfig.Name)) + continue + } - err = notify(svc, n, notificationConfig) + err = notify(svc, n, notificationConfig) - if err != nil { - n.Logger.V(log.VWarn).Info(fmt.Sprintf("Failed to send notifications. %+v", err)) - } else { - n.Logger.V(log.VDebug).Info("Sent notification") + if err != nil { + n.Logger.V(log.VWarn).Info(fmt.Sprintf("Failed to send notifications. %+v", err)) + } else { + n.Logger.V(log.VDebug).Info("Sent notification") + } + } } } } @@ -129,16 +132,10 @@ func getStatusColor(logLevel LoggingLevel, svc service) StatusColor { } } -func notify(svc service, n *Notification, nc v1alpha2.Notification) error { - var err error - switch s := svc.(type) { - case Slack: - err = s.Send(n, nc) - case Teams: - err = s.Send(n, nc) - case Mailgun: - err = s.Send(n, nc) +func notify(svc service, n *Notification, manifest v1alpha2.Notification) error { + if n.Information.LogLevel == LogInfo && string(manifest.LoggingLevel) == string(LogWarn) { + return nil } - return err + return svc.Send(n, manifest) } diff --git a/internal/notifier/slack.go b/internal/notifier/slack.go index 0a009514..185d3393 100644 --- a/internal/notifier/slack.go +++ b/internal/notifier/slack.go @@ -4,12 +4,10 @@ import ( "bytes" "context" "encoding/json" - "fmt" + "github.com/pkg/errors" "net/http" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" - "github.com/jenkinsci/kubernetes-operator/pkg/log" - corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" ) @@ -43,15 +41,14 @@ type SlackField struct { // Send is function for sending directly to API func (s Slack) Send(n *Notification, config v1alpha2.Notification) error { - var selector v1alpha2.SecretKeySelector secret := &corev1.Secret{} i := n.Information - selector = config.Slack.URLSecretKeySelector + selector := config.Slack.URLSecretKeySelector err := n.K8sClient.Get(context.TODO(), types.NamespacedName{Name: selector.Name, Namespace: n.Jenkins.Namespace}, secret) if err != nil { - n.Logger.V(log.VWarn).Info(fmt.Sprintf("Failed to get secret with name `%s`. %+v", selector.Name, err)) + return err } slackMessage, err := json.Marshal(SlackMessage{ @@ -94,7 +91,7 @@ func (s Slack) Send(n *Notification, config v1alpha2.Notification) error { secretValue := string(secret.Data[selector.Key]) if secretValue == "" { - return fmt.Errorf("SecretValue %s is empty", selector.Name) + return errors.Errorf("SecretValue %s is empty", selector.Name) } if err != nil { diff --git a/pkg/apis/jenkins/v1alpha2/jenkins_types.go b/pkg/apis/jenkins/v1alpha2/jenkins_types.go index d9634fd6..4afb27b2 100644 --- a/pkg/apis/jenkins/v1alpha2/jenkins_types.go +++ b/pkg/apis/jenkins/v1alpha2/jenkins_types.go @@ -17,9 +17,9 @@ type JenkinsSpec struct { // +optional SeedJobs []SeedJob `json:"seedJobs,omitempty"` - // Notification defines services which are used to inform about Jenkins behavior + // Notifications defines services which are used to inform about Jenkins status // Can be used to integrate chat services like Slack or Email services like Mailgun - Notification Notification `json:"notifications,omitempty"` + Notifications []Notification `json:"notifications,omitempty"` // Service is Kubernetes service of Jenkins master HTTP pod // Defaults to : @@ -66,13 +66,13 @@ type Notification struct { // Slack is handler for Slack type Slack struct { - // The web hook url to Slack App + // The web hook URL to Slack App URLSecretKeySelector SecretKeySelector `json:"urlSecretKeySelector"` } // Teams is handler for Microsoft Teams type Teams struct { - // The web hook url to Teams App + // The web hook URL to Teams App URLSecretKeySelector SecretKeySelector `json:"urlSecretKeySelector"` } diff --git a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go index 0ed66359..5d6d2d18 100644 --- a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go @@ -366,7 +366,11 @@ func (in *JenkinsSpec) DeepCopyInto(out *JenkinsSpec) { *out = make([]SeedJob, len(*in)) copy(*out, *in) } - out.Notification = in.Notification + if in.Notifications != nil { + in, out := &in.Notifications, &out.Notifications + *out = make([]Notification, len(*in)) + copy(*out, *in) + } in.Service.DeepCopyInto(&out.Service) in.SlaveService.DeepCopyInto(&out.SlaveService) in.Backup.DeepCopyInto(&out.Backup) From e641a9fa1202d248b3341b9304baab49e915fd7b Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Fri, 2 Aug 2019 14:24:36 +0200 Subject: [PATCH 57/59] Organize code structure --- internal/notifier/mailgun.go | 1 + internal/notifier/msteams.go | 1 + internal/notifier/msteams_test.go | 5 ++-- internal/notifier/sender.go | 41 +++++++++++++++---------------- internal/notifier/slack.go | 3 ++- internal/notifier/slack_test.go | 6 +++-- 6 files changed, 31 insertions(+), 26 deletions(-) diff --git a/internal/notifier/mailgun.go b/internal/notifier/mailgun.go index 3bf190b9..7f7e5e9f 100644 --- a/internal/notifier/mailgun.go +++ b/internal/notifier/mailgun.go @@ -7,6 +7,7 @@ import ( "time" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" + "github.com/mailgun/mailgun-go/v3" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" diff --git a/internal/notifier/msteams.go b/internal/notifier/msteams.go index f1a1b290..39ca3ae0 100644 --- a/internal/notifier/msteams.go +++ b/internal/notifier/msteams.go @@ -8,6 +8,7 @@ import ( "net/http" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" ) diff --git a/internal/notifier/msteams_test.go b/internal/notifier/msteams_test.go index 2205eff2..f19bcf2e 100644 --- a/internal/notifier/msteams_test.go +++ b/internal/notifier/msteams_test.go @@ -85,7 +85,7 @@ func TestTeams_Send(t *testing.T) { err := notification.K8sClient.Create(context.TODO(), secret) assert.NoError(t, err) - assert.NoError(t, teams.Send(notification, v1alpha2.Notification{ + err := teams.Send(notification, v1alpha2.Notification{ Teams: v1alpha2.Teams{ URLSecretKeySelector: v1alpha2.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ @@ -94,5 +94,6 @@ func TestTeams_Send(t *testing.T) { Key: testURLSelectorKeyName, }, }, - })) + }) + assert.NoError(t, err) } diff --git a/internal/notifier/sender.go b/internal/notifier/sender.go index 9735fcc5..b67bc407 100644 --- a/internal/notifier/sender.go +++ b/internal/notifier/sender.go @@ -4,10 +4,10 @@ import ( "fmt" "net/http" - "github.com/go-logr/logr" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" "github.com/jenkinsci/kubernetes-operator/pkg/log" + "github.com/go-logr/logr" k8sclient "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -70,31 +70,30 @@ type service interface { // Listen is goroutine that listens for incoming messages and sends it func Listen(notification chan *Notification) { for n := range notification { - if len(n.Jenkins.Spec.Notifications) > 0 { - for _, notificationConfig := range n.Jenkins.Spec.Notifications { - var err error - var svc service + for _, notificationConfig := range n.Jenkins.Spec.Notifications { + var err error + var svc service - if notificationConfig.Slack != (v1alpha2.Slack{}) { - svc = Slack{} - } else if notificationConfig.Teams != (v1alpha2.Teams{}) { - svc = Teams{} - } else if notificationConfig.Mailgun != (v1alpha2.Mailgun{}) { - svc = Mailgun{} - } else { - n.Logger.V(log.VWarn).Info(fmt.Sprintf("Notification service in `%s` not found or not defined", notificationConfig.Name)) - continue - } + if notificationConfig.Slack != (v1alpha2.Slack{}) { + svc = Slack{} + } else if notificationConfig.Teams != (v1alpha2.Teams{}) { + svc = Teams{} + } else if notificationConfig.Mailgun != (v1alpha2.Mailgun{}) { + svc = Mailgun{} + } else { + n.Logger.V(log.VWarn).Info(fmt.Sprintf("Notification service in `%s` not found or not defined", notificationConfig.Name)) + continue + } - err = notify(svc, n, notificationConfig) + err = notify(svc, n, notificationConfig) - if err != nil { - n.Logger.V(log.VWarn).Info(fmt.Sprintf("Failed to send notifications. %+v", err)) - } else { - n.Logger.V(log.VDebug).Info("Sent notification") - } + if err != nil { + n.Logger.V(log.VWarn).Info(fmt.Sprintf("Failed to send notifications. %+v", err)) + } else { + n.Logger.V(log.VDebug).Info("Sent notification") } } + } } diff --git a/internal/notifier/slack.go b/internal/notifier/slack.go index 185d3393..c8c3ba94 100644 --- a/internal/notifier/slack.go +++ b/internal/notifier/slack.go @@ -4,10 +4,11 @@ import ( "bytes" "context" "encoding/json" - "github.com/pkg/errors" "net/http" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" + + "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" ) diff --git a/internal/notifier/slack_test.go b/internal/notifier/slack_test.go index 06da7aae..67cf751f 100644 --- a/internal/notifier/slack_test.go +++ b/internal/notifier/slack_test.go @@ -84,7 +84,7 @@ func TestSlack_Send(t *testing.T) { slack := Slack{} - assert.NoError(t, slack.Send(notification, v1alpha2.Notification{ + err := slack.Send(notification, v1alpha2.Notification{ Slack: v1alpha2.Slack{ URLSecretKeySelector: v1alpha2.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ @@ -93,5 +93,7 @@ func TestSlack_Send(t *testing.T) { Key: testURLSelectorKeyName, }, }, - })) + }) + + assert.NoError(t, err) } From aecfd500f15ba52a1e0fc4a0abb0af8b178a1118 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Fri, 2 Aug 2019 14:37:52 +0200 Subject: [PATCH 58/59] Fix err --- internal/notifier/msteams_test.go | 2 +- internal/notifier/slack_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/notifier/msteams_test.go b/internal/notifier/msteams_test.go index f19bcf2e..bc52715d 100644 --- a/internal/notifier/msteams_test.go +++ b/internal/notifier/msteams_test.go @@ -85,7 +85,7 @@ func TestTeams_Send(t *testing.T) { err := notification.K8sClient.Create(context.TODO(), secret) assert.NoError(t, err) - err := teams.Send(notification, v1alpha2.Notification{ + err = teams.Send(notification, v1alpha2.Notification{ Teams: v1alpha2.Teams{ URLSecretKeySelector: v1alpha2.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ diff --git a/internal/notifier/slack_test.go b/internal/notifier/slack_test.go index 67cf751f..cb5f3221 100644 --- a/internal/notifier/slack_test.go +++ b/internal/notifier/slack_test.go @@ -84,7 +84,7 @@ func TestSlack_Send(t *testing.T) { slack := Slack{} - err := slack.Send(notification, v1alpha2.Notification{ + err = slack.Send(notification, v1alpha2.Notification{ Slack: v1alpha2.Slack{ URLSecretKeySelector: v1alpha2.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ From 5c55db489f030f51f31deb4816aa4a6a8884d397 Mon Sep 17 00:00:00 2001 From: Jakub Al-Khalili Date: Wed, 14 Aug 2019 09:51:55 +0200 Subject: [PATCH 59/59] #72 Add support for plugin names with underscore --- pkg/controller/jenkins/plugins/plugin.go | 2 +- pkg/controller/jenkins/plugins/plugin_test.go | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/pkg/controller/jenkins/plugins/plugin.go b/pkg/controller/jenkins/plugins/plugin.go index 2fcc8c3c..75ed6138 100644 --- a/pkg/controller/jenkins/plugins/plugin.go +++ b/pkg/controller/jenkins/plugins/plugin.go @@ -22,7 +22,7 @@ func (p Plugin) String() string { } var ( - namePattern = regexp.MustCompile(`^[0-9a-z-]+$`) + namePattern = regexp.MustCompile(`^[0-9a-z-_]+$`) versionPattern = regexp.MustCompile(`^[0-9\\.]+$`) ) diff --git a/pkg/controller/jenkins/plugins/plugin_test.go b/pkg/controller/jenkins/plugins/plugin_test.go index 0809acc5..4074a03e 100644 --- a/pkg/controller/jenkins/plugins/plugin_test.go +++ b/pkg/controller/jenkins/plugins/plugin_test.go @@ -32,6 +32,18 @@ func TestVerifyDependencies(t *testing.T) { got := VerifyDependencies(basePlugins) assert.Equal(t, true, got) }) + t.Run("happy, two plugin names with names with underscores", func(t *testing.T) { + basePlugins := map[Plugin][]Plugin{ + Must(New("first_root_plugin:1.0.0")): { + Must(New("first_plugin:0.0.1")), + }, + Must(New("second_root_plugin:1.0.0")): { + Must(New("first_plugin:0.0.1")), + }, + } + got := VerifyDependencies(basePlugins) + assert.Equal(t, true, got) + }) t.Run("fail, two root plugins have different versions", func(t *testing.T) { basePlugins := map[Plugin][]Plugin{ Must(New("first-root-plugin:1.0.0")): {