#8 Add Configuration as Code plugin integration
This commit is contained in:
		
							parent
							
								
									5af834869b
								
							
						
					
					
						commit
						ebf1163b28
					
				|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
| 	} | ||||
|  |  | |||
|  | @ -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 = `<?xml version='1.1' encoding='UTF-8'?> | ||||
| <flow-definition plugin="workflow-job@2.31"> | ||||
|   <actions/> | ||||
|   <description></description> | ||||
|   <keepDependencies>false</keepDependencies> | ||||
|   <properties> | ||||
|     <org.jenkinsci.plugins.workflow.job.properties.DisableConcurrentBuildsJobProperty/> | ||||
|     <hudson.model.ParametersDefinitionProperty> | ||||
|       <parameterDefinitions> | ||||
|         <hudson.model.StringParameterDefinition> | ||||
|           <name>` + jobHashParameterName + `</name> | ||||
|           <description></description> | ||||
|           <defaultValue></defaultValue> | ||||
|           <trim>false</trim> | ||||
|         </hudson.model.StringParameterDefinition> | ||||
|       </parameterDefinitions> | ||||
|     </hudson.model.ParametersDefinitionProperty> | ||||
|   </properties> | ||||
|   <definition class="org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition" plugin="workflow-cps@2.61.1"> | ||||
|     <script>import io.jenkins.plugins.casc.yaml.YamlSource; | ||||
| 
 | ||||
| def configsPath = '%s' | ||||
| def expectedHash = params.hash | ||||
| 
 | ||||
| node('master') { | ||||
|     def configsText = sh(script: "ls ${configsPath} | grep .yaml | sort", returnStdout: true).trim() | ||||
|     def configs = [] | ||||
|     configs.addAll(configsText.tokenize('\n')) | ||||
|      | ||||
|     stage('Synchronizing files') { | ||||
|         def complete = false | ||||
|         for(int i = 1; i <= 10; i++) { | ||||
|             def actualHash = calculateHash((String[])configs, configsPath) | ||||
|             println "Expected hash '${expectedHash}', actual hash '${actualHash}'" | ||||
|             if(expectedHash == actualHash) { | ||||
|                 complete = true | ||||
|                 break | ||||
|             } | ||||
|             sleep 2 | ||||
|         } | ||||
|         if(!complete) { | ||||
|             error("Timeout while synchronizing files") | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     for(config in configs) { | ||||
|         stage(config) { | ||||
|             def path = java.nio.file.Paths.get("${configsPath}/${config}") | ||||
|             def source = new YamlSource(path, YamlSource.READ_FROM_PATH) | ||||
|             io.jenkins.plugins.casc.ConfigurationAsCode.get().configureWith(source) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @NonCPS | ||||
| def calculateHash(String[] configs, String configsPath) { | ||||
|     def hash = java.security.MessageDigest.getInstance("SHA-256") | ||||
|     for(config in configs) { | ||||
|         hash.update(config.getBytes()) | ||||
|         def fileLocation = java.nio.file.Paths.get("${configsPath}/${config}") | ||||
|         def fileData = java.nio.file.Files.readAllBytes(fileLocation) | ||||
|         hash.update(fileData) | ||||
|     } | ||||
|     return Base64.getEncoder().encodeToString(hash.digest()) | ||||
| }</script> | ||||
|     <sandbox>false</sandbox> | ||||
|   </definition> | ||||
|   <triggers/> | ||||
|   <disabled>false</disabled> | ||||
| </flow-definition> | ||||
| ` | ||||
|  | @ -0,0 +1,2 @@ | |||
| // Package casc configures Jenkins with help Configuration as a code plugin
 | ||||
| package casc | ||||
|  | @ -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 | ||||
| 	} | ||||
|  |  | |||
|  | @ -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" | ||||
| ) | ||||
|  |  | |||
|  | @ -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 = `<?xml version='1.1' encoding='UTF-8'?> | |||
| 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')) | ||||
|      | ||||
|  |  | |||
|  | @ -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) | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue