#28 Use new API for groovy and CasC user configuration
This commit is contained in:
parent
6a3a68bec0
commit
7d716b972f
|
|
@ -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 = '` + resources.UserConfigurationSecretVolumePath + `'
|
|
||||||
def configsPath = '` + resources.JenkinsUserConfigurationVolumePath + `'
|
|
||||||
def userConfigurationSecretExpectedHash = params.` + userConfigurationSecretHashParameterName + `
|
|
||||||
def userConfigurationExpectedHash = params.` + userConfigurationHashParameterName + `
|
|
||||||
|
|
||||||
node('master') {
|
|
||||||
def secretsText = sh(script: "ls ${secretsPath} | sort", returnStdout: true).trim()
|
|
||||||
def secrets = []
|
|
||||||
secrets.addAll(secretsText.tokenize('\n'))
|
|
||||||
|
|
||||||
def configsText = sh(script: "ls ${configsPath} | grep .yaml | sort", returnStdout: true).trim()
|
|
||||||
def configs = []
|
|
||||||
configs.addAll(configsText.tokenize('\n'))
|
|
||||||
|
|
||||||
stage('Synchronizing files') {
|
|
||||||
println "Synchronizing Kubernetes ConfigMaps and Secrets to the Jenkins master pod."
|
|
||||||
println "This step may fail and will be retried in the next job build if necessary."
|
|
||||||
|
|
||||||
synchronizeFiles(secretsPath, (String[])secrets, userConfigurationSecretExpectedHash)
|
|
||||||
synchronizeFiles(configsPath, (String[])configs, userConfigurationExpectedHash)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
io.jenkins.plugins.casc.ConfigurationAsCode.get().configureWith(source)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def synchronizeFiles(String path, String[] files, String hash) {
|
|
||||||
def complete = false
|
|
||||||
for(int i = 1; i <= 10; i++) {
|
|
||||||
def actualHash = calculateHash(files, path)
|
|
||||||
println "Expected hash '${hash}', actual hash '${actualHash}', path '${path}', will retry"
|
|
||||||
if(hash == actualHash) {
|
|
||||||
complete = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
sleep 2
|
|
||||||
}
|
|
||||||
if(!complete) {
|
|
||||||
error("Timeout while synchronizing files")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@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>
|
|
||||||
`
|
`
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue