#8 Add Configuration as Code plugin integration

This commit is contained in:
Tomasz Sęk 2019-03-01 17:48:35 +01:00
parent 5af834869b
commit ebf1163b28
No known key found for this signature in database
GPG Key ID: DC356D23F6A644D0
8 changed files with 244 additions and 21 deletions

View File

@ -221,7 +221,6 @@ kubectl get configmap jenkins-operator-user-configuration-example -o yaml
apiVersion: v1 apiVersion: v1
data: data:
1-configure-theme.groovy: |2 1-configure-theme.groovy: |2
import jenkins.* import jenkins.*
import jenkins.model.* import jenkins.model.*
import hudson.* import hudson.*
@ -241,6 +240,9 @@ data:
decorator.save(); decorator.save();
jenkins.save() jenkins.save()
1-system-message.yaml: |2
jenkins:
systemMessage: "Configuration as Code integration works!!!"
kind: ConfigMap kind: ConfigMap
metadata: metadata:
labels: labels:
@ -251,7 +253,9 @@ metadata:
namespace: default 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 ## Install Plugins

View File

@ -451,7 +451,7 @@ func (r *ReconcileJenkinsBaseConfiguration) ensureJenkinsClient(meta metav1.Obje
func (r *ReconcileJenkinsBaseConfiguration) ensureBaseConfiguration(jenkinsClient jenkinsclient.Jenkins) (reconcile.Result, error) { 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) 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 { if err != nil {
return reconcile.Result{}, err return reconcile.Result{}, err
} }
@ -463,7 +463,7 @@ func (r *ReconcileJenkinsBaseConfiguration) ensureBaseConfiguration(jenkinsClien
return reconcile.Result{}, stackerr.WithStack(err) 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 { if err != nil {
return reconcile.Result{}, err return reconcile.Result{}, err
} }

View File

@ -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 = &apos;%s&apos;
def expectedHash = params.hash
node(&apos;master&apos;) {
def configsText = sh(script: &quot;ls ${configsPath} | grep .yaml | sort&quot;, returnStdout: true).trim()
def configs = []
configs.addAll(configsText.tokenize(&apos;\n&apos;))
stage(&apos;Synchronizing files&apos;) {
def complete = false
for(int i = 1; i &lt;= 10; i++) {
def actualHash = calculateHash((String[])configs, configsPath)
println &quot;Expected hash &apos;${expectedHash}&apos;, actual hash &apos;${actualHash}&apos;&quot;
if(expectedHash == actualHash) {
complete = true
break
}
sleep 2
}
if(!complete) {
error(&quot;Timeout while synchronizing files&quot;)
}
}
for(config in configs) {
stage(config) {
def path = java.nio.file.Paths.get(&quot;${configsPath}/${config}&quot;)
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(&quot;SHA-256&quot;)
for(config in configs) {
hash.update(config.getBytes())
def fileLocation = java.nio.file.Paths.get(&quot;${configsPath}/${config}&quot;)
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>
`

View File

@ -0,0 +1,2 @@
// Package casc configures Jenkins with help Configuration as a code plugin
package casc

View File

@ -7,6 +7,7 @@ import (
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkinsio/v1alpha1" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkinsio/v1alpha1"
jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client" 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/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/configuration/user/seedjobs"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants" "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/groovy"
@ -84,25 +85,35 @@ func (r *ReconcileUserConfiguration) ensureSeedJobs() (reconcile.Result, error)
} }
func (r *ReconcileUserConfiguration) ensureUserConfiguration(jenkinsClient jenkinsclient.Jenkins) (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{} configuration := &corev1.ConfigMap{}
namespaceName := types.NamespacedName{Namespace: r.jenkins.Namespace, Name: resources.GetUserConfigurationConfigMapNameFromJenkins(r.jenkins)} 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 { if err != nil {
return reconcile.Result{}, errors.WithStack(err) 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 { if err != nil {
return reconcile.Result{}, err 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 { if !done {
return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 10}, nil return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 10}, nil
} }

View File

@ -11,4 +11,6 @@ const (
DefaultJenkinsMasterImage = "jenkins/jenkins:lts" DefaultJenkinsMasterImage = "jenkins/jenkins:lts"
// UserConfigurationJobName is the Jenkins job name used to configure Jenkins by groovy scripts provided by user // UserConfigurationJobName is the Jenkins job name used to configure Jenkins by groovy scripts provided by user
UserConfigurationJobName = OperatorName + "-user-configuration" 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"
) )

View File

@ -5,6 +5,7 @@ import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"sort" "sort"
"strings"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkinsio/v1alpha1" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkinsio/v1alpha1"
jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client" 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 // ConfigureJob configures jenkins job for executing groovy scripts
func (g *Groovy) ConfigureGroovyJob() error { func (g *Groovy) ConfigureJob() error {
_, created, err := g.jenkinsClient.CreateOrUpdateJob(fmt.Sprintf(configurationJobXMLFmt, g.scriptsPath), g.jobName) _, created, err := g.jenkinsClient.CreateOrUpdateJob(fmt.Sprintf(configurationJobXMLFmt, g.scriptsPath), g.jobName)
if err != nil { if err != nil {
return err return err
@ -50,8 +51,8 @@ func (g *Groovy) ConfigureGroovyJob() error {
return nil return nil
} }
// EnsureGroovyJob executes groovy script and verifies jenkins job status according to reconciliation loop lifecycle // Ensure 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) { func (g *Groovy) Ensure(secretOrConfigMapData map[string]string, jenkins *v1alpha1.Jenkins) (bool, error) {
jobsClient := jobs.New(g.jenkinsClient, g.k8sClient, g.logger) jobsClient := jobs.New(g.jenkinsClient, g.k8sClient, g.logger)
hash := g.calculateHash(secretOrConfigMapData) hash := g.calculateHash(secretOrConfigMapData)
@ -71,8 +72,10 @@ func (g *Groovy) calculateHash(secretOrConfigMapData map[string]string) string {
} }
sort.Strings(keys) sort.Strings(keys)
for _, key := range keys { for _, key := range keys {
hash.Write([]byte(key)) if strings.HasSuffix(key, ".groovy") {
hash.Write([]byte(secretOrConfigMapData[key])) hash.Write([]byte(key))
hash.Write([]byte(secretOrConfigMapData[key]))
}
} }
return base64.StdEncoding.EncodeToString(hash.Sum(nil)) return base64.StdEncoding.EncodeToString(hash.Sum(nil))
} }
@ -100,7 +103,7 @@ const configurationJobXMLFmt = `<?xml version='1.1' encoding='UTF-8'?>
def expectedHash = params.hash def expectedHash = params.hash
node(&apos;master&apos;) { node(&apos;master&apos;) {
def scriptsText = sh(script: &quot;ls ${scriptsPath} | sort&quot;, returnStdout: true).trim() def scriptsText = sh(script: &quot;ls ${scriptsPath} | grep .groovy | sort&quot;, returnStdout: true).trim()
def scripts = [] def scripts = []
scripts.addAll(scriptsText.tokenize(&apos;\n&apos;)) scripts.addAll(scriptsText.tokenize(&apos;\n&apos;))

View File

@ -2,12 +2,14 @@ package e2e
import ( import (
"context" "context"
"fmt"
"reflect" "reflect"
"testing" "testing"
"time" "time"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkinsio/v1alpha1" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkinsio/v1alpha1"
jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client" 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/configuration/user/seedjobs"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/plugins" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/plugins"
@ -27,8 +29,13 @@ func TestConfiguration(t *testing.T) {
// Deletes test namespace // Deletes test namespace
defer ctx.Cleanup() defer ctx.Cleanup()
jenkinsCRName := "e2e"
numberOfExecutors := 6
systemMessage := "Configuration as Code integration works!!!"
// base // base
jenkins := createJenkinsCR(t, "e2e", namespace) createUserConfigurationConfigMap(t, jenkinsCRName, namespace, numberOfExecutors, systemMessage)
jenkins := createJenkinsCR(t, jenkinsCRName, namespace)
createDefaultLimitsForContainersInNamespace(t, namespace) createDefaultLimitsForContainersInNamespace(t, namespace)
waitForJenkinsBaseConfigurationToComplete(t, jenkins) waitForJenkinsBaseConfigurationToComplete(t, jenkins)
@ -39,6 +46,31 @@ func TestConfiguration(t *testing.T) {
// user // user
waitForJenkinsUserConfigurationToComplete(t, jenkins) waitForJenkinsUserConfigurationToComplete(t, jenkins)
verifyJenkinsSeedJobs(t, client, 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) { func createDefaultLimitsForContainersInNamespace(t *testing.T, namespace string) {
@ -171,3 +203,19 @@ func verifyJenkinsSeedJobs(t *testing.T, client jenkinsclient.Jenkins, jenkins *
}) })
assert.NoError(t, err) 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)
}