#28 Use new API for groovy and CasC user configuration

This commit is contained in:
Tomasz Sęk 2019-06-30 23:18:20 +02:00
parent 6a3a68bec0
commit 7d716b972f
No known key found for this signature in database
GPG Key ID: DC356D23F6A644D0
2 changed files with 44 additions and 215 deletions

View File

@ -1,216 +1,50 @@
package casc package casc
import ( import (
"context"
"crypto/sha256"
"encoding/base64"
"fmt" "fmt"
"sort"
"strings" "strings"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
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/jobs" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/groovy"
"github.com/go-logr/logr" "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" 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 // ConfigurationAsCode defines API which configures Jenkins with help Configuration as a code plugin
type ConfigurationAsCode struct { type ConfigurationAsCode struct {
jenkinsClient jenkinsclient.Jenkins groovyClient *groovy.Groovy
k8sClient k8s.Client
logger logr.Logger
jobName string
} }
// New creates new instance of ConfigurationAsCode // 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{ return &ConfigurationAsCode{
jenkinsClient: jenkinsClient, groovyClient: groovy.New(jenkinsClient, k8sClient, logger, jenkins, "user-casc", jenkins.Spec.ConfigurationAsCode.Customization),
k8sClient: k8sClient,
logger: logger,
jobName: jobName,
} }
} }
// 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 // Ensure configures Jenkins with help Configuration as a code plugin
func (g *ConfigurationAsCode) Ensure(jenkins *v1alpha2.Jenkins) (bool, error) { func (c *ConfigurationAsCode) Ensure(jenkins *v1alpha2.Jenkins) (requeue bool, err error) {
jobsClient := jobs.New(g.jenkinsClient, g.k8sClient, g.logger) requeue, err = c.groovyClient.WaitForSecretSynchronization(resources.ConfigurationAsCodeSecretVolumePath)
if err != nil || requeue {
configuration := &corev1.ConfigMap{} return requeue, err
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)
} }
secret := &corev1.Secret{} return c.groovyClient.Ensure(func(name string) bool {
secretNamespaceName := types.NamespacedName{Namespace: jenkins.Namespace, Name: resources.GetUserConfigurationSecretNameFromJenkins(jenkins)} return strings.HasSuffix(name, ".yaml") || strings.HasSuffix(name, ".yml")
err = g.k8sClient.Get(context.TODO(), secretNamespaceName, secret) }, func(groovyScript string) string {
if err != nil { return fmt.Sprintf(applyConfigurationAsCodeGroovyScriptFmt, groovyScript)
return false, errors.WithStack(err) })
} }
userConfigurationSecretHash := g.calculateUserConfigurationSecretHash(secret) const applyConfigurationAsCodeGroovyScriptFmt = `
userConfigurationHash := g.calculateUserConfigurationHash(configuration) def config = '''
done, err := jobsClient.EnsureBuildJob( %s
g.jobName, '''
userConfigurationSecretHash+userConfigurationHash, def stream = new ByteArrayInputStream(config.getBytes('UTF-8'))
map[string]string{
userConfigurationHashParameterName: userConfigurationHash,
userConfigurationSecretHashParameterName: userConfigurationSecretHash,
},
jenkins,
true)
if err != nil {
return false, err
}
return done, nil
}
func (g *ConfigurationAsCode) calculateUserConfigurationSecretHash(userConfigurationSecret *corev1.Secret) string { def source = new io.jenkins.plugins.casc.yaml.YamlSource(stream, io.jenkins.plugins.casc.yaml.YamlSource.READ_FROM_INPUTSTREAM)
hash := sha256.New()
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 = `<?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>` + userConfigurationSecretHashParameterName + `</name>
<description/>
<defaultValue/>
<trim>false</trim>
</hudson.model.StringParameterDefinition>
<hudson.model.StringParameterDefinition>
<name>` + userConfigurationHashParameterName + `</name>
<description/>
<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 secretsPath = &apos;` + resources.UserConfigurationSecretVolumePath + `&apos;
def configsPath = &apos;` + resources.JenkinsUserConfigurationVolumePath + `&apos;
def userConfigurationSecretExpectedHash = params.` + userConfigurationSecretHashParameterName + `
def userConfigurationExpectedHash = params.` + userConfigurationHashParameterName + `
node(&apos;master&apos;) {
def secretsText = sh(script: &quot;ls ${secretsPath} | sort&quot;, returnStdout: true).trim()
def secrets = []
secrets.addAll(secretsText.tokenize(&apos;\n&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;) {
println &quot;Synchronizing Kubernetes ConfigMaps and Secrets to the Jenkins master pod.&quot;
println &quot;This step may fail and will be retried in the next job build if necessary.&quot;
synchronizeFiles(secretsPath, (String[])secrets, userConfigurationSecretExpectedHash)
synchronizeFiles(configsPath, (String[])configs, userConfigurationExpectedHash)
}
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) io.jenkins.plugins.casc.ConfigurationAsCode.get().configureWith(source)
}
}
}
def synchronizeFiles(String path, String[] files, String hash) {
def complete = false
for(int i = 1; i &lt;= 10; i++) {
def actualHash = calculateHash(files, path)
println &quot;Expected hash &apos;${hash}&apos;, actual hash &apos;${actualHash}&apos;, path &apos;${path}&apos;, will retry&quot;
if(hash == actualHash) {
complete = true
break
}
sleep 2
}
if(!complete) {
error(&quot;Timeout while synchronizing files&quot;)
}
}
@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

@ -1,7 +1,7 @@
package user package user
import ( import (
"context" "strings"
"time" "time"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" "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/base/resources"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/user/casc" "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/groovy" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/groovy"
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/jobs" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/jobs"
"github.com/go-logr/logr" "github.com/go-logr/logr"
"github.com/pkg/errors" "github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
k8s "sigs.k8s.io/controller-runtime/pkg/client" 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) { func (r *ReconcileUserConfiguration) ensureUserConfiguration(jenkinsClient jenkinsclient.Jenkins) (reconcile.Result, error) {
configuration := &corev1.ConfigMap{} groovyClient := groovy.New(jenkinsClient, r.k8sClient, r.logger, r.jenkins, "user-groovy", r.jenkins.Spec.GroovyScripts.Customization)
namespaceName := types.NamespacedName{Namespace: r.jenkins.Namespace, Name: resources.GetUserConfigurationConfigMapNameFromJenkins(r.jenkins)}
err := r.k8sClient.Get(context.TODO(), namespaceName, configuration) requeue, err := groovyClient.WaitForSecretSynchronization(resources.GroovyScriptsSecretVolumePath)
if err != nil { 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) configurationAsCodeClient := casc.New(jenkinsClient, r.k8sClient, r.logger, r.jenkins)
err = groovyClient.ConfigureJob() requeue, err = configurationAsCodeClient.Ensure(r.jenkins)
if err != nil { if err != nil {
return reconcile.Result{}, err return reconcile.Result{}, err
} }
done, err := groovyClient.Ensure(configuration.Data, r.jenkins) if requeue {
if err != nil { return reconcile.Result{Requeue: true}, 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
} }
return reconcile.Result{}, nil return reconcile.Result{}, nil