diff --git a/docs/getting-started.md b/docs/getting-started.md index aff5ecf2..7bee7304 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -221,7 +221,6 @@ kubectl get configmap jenkins-operator-user-configuration-example -o yaml apiVersion: v1 data: 1-configure-theme.groovy: |2 - import jenkins.* import jenkins.model.* import hudson.* @@ -241,6 +240,9 @@ data: decorator.save(); jenkins.save() + 1-system-message.yaml: |2 + jenkins: + systemMessage: "Configuration as Code integration works!!!" kind: ConfigMap metadata: labels: @@ -251,7 +253,9 @@ metadata: namespace: default ``` -When **jenkins-operator-user-configuration-example** ConfigMap is updated Jenkins automatically runs the **jenkins-operator-user-configuration** Jenkins Job which executes all scripts. +When **jenkins-operator-user-configuration-example** 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. ## Install Plugins diff --git a/pkg/controller/jenkins/configuration/base/reconcile.go b/pkg/controller/jenkins/configuration/base/reconcile.go index f58b6272..2386179c 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile.go +++ b/pkg/controller/jenkins/configuration/base/reconcile.go @@ -451,7 +451,7 @@ 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.ConfigureGroovyJob() + err := groovyClient.ConfigureJob() if err != nil { return reconcile.Result{}, err } @@ -463,7 +463,7 @@ func (r *ReconcileJenkinsBaseConfiguration) ensureBaseConfiguration(jenkinsClien return reconcile.Result{}, stackerr.WithStack(err) } - done, err := groovyClient.EnsureGroovyJob(configuration.Data, r.jenkins) + done, err := groovyClient.Ensure(configuration.Data, r.jenkins) if err != nil { return reconcile.Result{}, err } diff --git a/pkg/controller/jenkins/configuration/user/casc/caac.go b/pkg/controller/jenkins/configuration/user/casc/caac.go new file mode 100644 index 00000000..6648fe66 --- /dev/null +++ b/pkg/controller/jenkins/configuration/user/casc/caac.go @@ -0,0 +1,153 @@ +package casc + +import ( + "crypto/sha256" + "encoding/base64" + "fmt" + "sort" + "strings" + + "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkinsio/v1alpha1" + jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client" + "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/jobs" + + "github.com/go-logr/logr" + k8s "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + jobHashParameterName = "hash" +) + +// 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 + configsPath string +} + +// New creates new instance of ConfigurationAsCode +func New(jenkinsClient jenkinsclient.Jenkins, k8sClient k8s.Client, logger logr.Logger, jobName, configsPath string) *ConfigurationAsCode { + return &ConfigurationAsCode{ + jenkinsClient: jenkinsClient, + k8sClient: k8sClient, + logger: logger, + jobName: jobName, + configsPath: configsPath, + } +} + +// ConfigureJob configures jenkins job which configures Jenkins with help Configuration as a code plugin +func (g *ConfigurationAsCode) ConfigureJob() error { + _, created, err := g.jenkinsClient.CreateOrUpdateJob(fmt.Sprintf(configurationJobXMLFmt, g.configsPath), 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(secretOrConfigMapData map[string]string, jenkins *v1alpha1.Jenkins) (bool, error) { + jobsClient := jobs.New(g.jenkinsClient, g.k8sClient, g.logger) + + hash := g.calculateHash(secretOrConfigMapData) + done, err := jobsClient.EnsureBuildJob(g.jobName, hash, map[string]string{jobHashParameterName: hash}, jenkins, true) + if err != nil { + return false, err + } + return done, nil +} + +func (g *ConfigurationAsCode) calculateHash(secretOrConfigMapData map[string]string) string { + hash := sha256.New() + + var keys []string + for key := range secretOrConfigMapData { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + if strings.HasSuffix(key, ".yaml") { + hash.Write([]byte(key)) + hash.Write([]byte(secretOrConfigMapData[key])) + } + } + return base64.StdEncoding.EncodeToString(hash.Sum(nil)) +} + +const configurationJobXMLFmt = ` + + + + false + + + + + + ` + jobHashParameterName + ` + + + false + + + + + + + false + + + false + +` diff --git a/pkg/controller/jenkins/configuration/user/casc/doc.go b/pkg/controller/jenkins/configuration/user/casc/doc.go new file mode 100644 index 00000000..0a7c5b9f --- /dev/null +++ b/pkg/controller/jenkins/configuration/user/casc/doc.go @@ -0,0 +1,2 @@ +// Package casc configures Jenkins with help Configuration as a code plugin +package casc diff --git a/pkg/controller/jenkins/configuration/user/reconcile.go b/pkg/controller/jenkins/configuration/user/reconcile.go index c6bcb3fb..3dd55d7b 100644 --- a/pkg/controller/jenkins/configuration/user/reconcile.go +++ b/pkg/controller/jenkins/configuration/user/reconcile.go @@ -7,6 +7,7 @@ import ( "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkinsio/v1alpha1" 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/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" @@ -84,25 +85,35 @@ func (r *ReconcileUserConfiguration) ensureSeedJobs() (reconcile.Result, error) } func (r *ReconcileUserConfiguration) ensureUserConfiguration(jenkinsClient jenkinsclient.Jenkins) (reconcile.Result, error) { - groovyClient := groovy.New(jenkinsClient, r.k8sClient, r.logger, constants.UserConfigurationJobName, resources.JenkinsUserConfigurationVolumePath) - - err := groovyClient.ConfigureGroovyJob() - if err != nil { - return reconcile.Result{}, err - } - configuration := &corev1.ConfigMap{} namespaceName := types.NamespacedName{Namespace: r.jenkins.Namespace, Name: resources.GetUserConfigurationConfigMapNameFromJenkins(r.jenkins)} - err = r.k8sClient.Get(context.TODO(), namespaceName, configuration) + err := r.k8sClient.Get(context.TODO(), namespaceName, configuration) if err != nil { return reconcile.Result{}, errors.WithStack(err) } - done, err := groovyClient.EnsureGroovyJob(configuration.Data, r.jenkins) + groovyClient := groovy.New(jenkinsClient, r.k8sClient, r.logger, constants.UserConfigurationJobName, resources.JenkinsUserConfigurationVolumePath) + err = groovyClient.ConfigureJob() 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, resources.JenkinsUserConfigurationVolumePath) + err = configurationAsCodeClient.ConfigureJob() + if err != nil { + return reconcile.Result{}, err + } + done, err = configurationAsCodeClient.Ensure(configuration.Data, r.jenkins) + if err != nil { + return reconcile.Result{}, err + } if !done { return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 10}, nil } diff --git a/pkg/controller/jenkins/constants/constants.go b/pkg/controller/jenkins/constants/constants.go index 04a7e4eb..175c44ea 100644 --- a/pkg/controller/jenkins/constants/constants.go +++ b/pkg/controller/jenkins/constants/constants.go @@ -11,4 +11,6 @@ const ( DefaultJenkinsMasterImage = "jenkins/jenkins:lts" // UserConfigurationJobName is the Jenkins job name used to configure Jenkins by groovy scripts provided by user UserConfigurationJobName = OperatorName + "-user-configuration" + // UserConfigurationCASCJobName is the Jenkins job name used to configure Jenkins by Configuration as code yaml configs provided by user + UserConfigurationCASCJobName = OperatorName + "-user-configuration-casc" ) diff --git a/pkg/controller/jenkins/groovy/groovy.go b/pkg/controller/jenkins/groovy/groovy.go index dfe86612..cc74cd12 100644 --- a/pkg/controller/jenkins/groovy/groovy.go +++ b/pkg/controller/jenkins/groovy/groovy.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "fmt" "sort" + "strings" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkinsio/v1alpha1" jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client" @@ -38,8 +39,8 @@ func New(jenkinsClient jenkinsclient.Jenkins, k8sClient k8s.Client, logger logr. } } -// ConfigureGroovyJob configures jenkins job for executing groovy scripts -func (g *Groovy) ConfigureGroovyJob() error { +// 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) if err != nil { return err @@ -50,8 +51,8 @@ func (g *Groovy) ConfigureGroovyJob() error { return nil } -// EnsureGroovyJob executes groovy script and verifies jenkins job status according to reconciliation loop lifecycle -func (g *Groovy) EnsureGroovyJob(secretOrConfigMapData map[string]string, jenkins *v1alpha1.Jenkins) (bool, error) { +// Ensure executes groovy script and verifies jenkins job status according to reconciliation loop lifecycle +func (g *Groovy) Ensure(secretOrConfigMapData map[string]string, jenkins *v1alpha1.Jenkins) (bool, error) { jobsClient := jobs.New(g.jenkinsClient, g.k8sClient, g.logger) hash := g.calculateHash(secretOrConfigMapData) @@ -71,8 +72,10 @@ func (g *Groovy) calculateHash(secretOrConfigMapData map[string]string) string { } sort.Strings(keys) for _, key := range keys { - hash.Write([]byte(key)) - hash.Write([]byte(secretOrConfigMapData[key])) + if strings.HasSuffix(key, ".groovy") { + hash.Write([]byte(key)) + hash.Write([]byte(secretOrConfigMapData[key])) + } } return base64.StdEncoding.EncodeToString(hash.Sum(nil)) } @@ -100,7 +103,7 @@ const configurationJobXMLFmt = ` def expectedHash = params.hash node('master') { - def scriptsText = sh(script: "ls ${scriptsPath} | sort", returnStdout: true).trim() + def scriptsText = sh(script: "ls ${scriptsPath} | grep .groovy | sort", returnStdout: true).trim() def scripts = [] scripts.addAll(scriptsText.tokenize('\n')) diff --git a/test/e2e/configuration_test.go b/test/e2e/configuration_test.go index 4ac5e958..ed351f02 100644 --- a/test/e2e/configuration_test.go +++ b/test/e2e/configuration_test.go @@ -2,12 +2,14 @@ package e2e import ( "context" + "fmt" "reflect" "testing" "time" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkinsio/v1alpha1" 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/configuration/user/seedjobs" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/plugins" @@ -27,8 +29,13 @@ func TestConfiguration(t *testing.T) { // Deletes test namespace defer ctx.Cleanup() + jenkinsCRName := "e2e" + numberOfExecutors := 6 + systemMessage := "Configuration as Code integration works!!!" + // base - jenkins := createJenkinsCR(t, "e2e", namespace) + createUserConfigurationConfigMap(t, jenkinsCRName, namespace, numberOfExecutors, systemMessage) + jenkins := createJenkinsCR(t, jenkinsCRName, namespace) createDefaultLimitsForContainersInNamespace(t, namespace) waitForJenkinsBaseConfigurationToComplete(t, jenkins) @@ -39,6 +46,31 @@ func TestConfiguration(t *testing.T) { // user waitForJenkinsUserConfigurationToComplete(t, jenkins) verifyJenkinsSeedJobs(t, client, jenkins) + verifyUserConfiguration(t, client, numberOfExecutors, systemMessage) +} + +func createUserConfigurationConfigMap(t *testing.T, jenkinsCRName string, namespace string, numberOfExecutors int, systemMessage string) { + userConfiguration := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: resources.GetUserConfigurationConfigMapName(jenkinsCRName), + Namespace: namespace, + }, + Data: map[string]string{ + "1-set-executors.groovy": fmt.Sprintf(` +import jenkins.model.Jenkins + +Jenkins.instance.setNumExecutors(%d) +Jenkins.instance.save()`, numberOfExecutors), + "1-casc.yaml": fmt.Sprintf(` +jenkins: + systemMessage: "%s"`, systemMessage), + }, + } + + t.Logf("User configuration %+v", *userConfiguration) + if err := framework.Global.Client.Create(context.TODO(), userConfiguration, nil); err != nil { + t.Fatal(err) + } } func createDefaultLimitsForContainersInNamespace(t *testing.T, namespace string) { @@ -171,3 +203,19 @@ func verifyJenkinsSeedJobs(t *testing.T, client jenkinsclient.Jenkins, jenkins * }) assert.NoError(t, err) } + +func verifyUserConfiguration(t *testing.T, jenkinsClient jenkinsclient.Jenkins, amountOfExecutors int, systemMessage string) { + checkConfigurationViaGroovyScript := fmt.Sprintf(` +if (!new Integer(%d).equals(Jenkins.instance.numExecutors)) { + throw new Exception("Configuration via groovy scripts failed") +}`, amountOfExecutors) + logs, err := jenkinsClient.ExecuteScript(checkConfigurationViaGroovyScript) + assert.NoError(t, err, logs) + + checkConfigurationAsCode := fmt.Sprintf(` +if (!"%s".equals(Jenkins.instance.systemMessage)) { + throw new Exception("Configuration as code failed") +}`, systemMessage) + logs, err = jenkinsClient.ExecuteScript(checkConfigurationAsCode) + assert.NoError(t, err, logs) +}