package groovy import ( "context" "crypto/sha256" "encoding/base64" "fmt" "sort" "strings" "github.com/jenkinsci/kubernetes-operator/api/v1alpha2" jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/client" "github.com/jenkinsci/kubernetes-operator/pkg/log" "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" ) // Groovy defines API for groovy secrets execution via jenkins job type Groovy struct { k8sClient k8s.Client logger logr.Logger jenkins *v1alpha2.Jenkins jenkinsClient jenkinsclient.Jenkins configurationType string customization v1alpha2.Customization } // New creates new instance of Groovy func New(jenkinsClient jenkinsclient.Jenkins, k8sClient k8s.Client, jenkins *v1alpha2.Jenkins, configurationType string, customization v1alpha2.Customization) *Groovy { return &Groovy{ jenkinsClient: jenkinsClient, k8sClient: k8sClient, jenkins: jenkins, configurationType: configurationType, customization: customization, logger: log.Log.WithValues("cr", jenkins.Name), } } // EnsureSingle runs single groovy script func (g *Groovy) EnsureSingle(source, name, hash, groovyScript string) (requeue bool, err error) { if g.isGroovyScriptAlreadyApplied(source, name, hash) { return false, nil } logs, err := g.jenkinsClient.ExecuteScript(groovyScript) if err != nil { if groovyErr, ok := err.(*jenkinsclient.GroovyScriptExecutionFailed); ok { groovyErr.ConfigurationType = g.configurationType groovyErr.Name = name groovyErr.Source = source groovyErr.Logs = logs g.logger.V(log.VWarn).Info(fmt.Sprintf("%s Source '%s' Name '%s' groovy script execution failed, logs :\n%s", g.configurationType, source, name, logs)) } return true, err } appliedGroovyScripts := []v1alpha2.AppliedGroovyScript{} for _, ags := range g.jenkins.Status.AppliedGroovyScripts { if g.configurationType == ags.ConfigurationType && ags.Source == source && ags.Name == name { continue } appliedGroovyScripts = append(appliedGroovyScripts, ags) } appliedGroovyScripts = append(appliedGroovyScripts, v1alpha2.AppliedGroovyScript{ ConfigurationType: g.configurationType, Source: source, Name: name, Hash: hash, }) g.jenkins.Status.AppliedGroovyScripts = appliedGroovyScripts return true, g.k8sClient.Status().Update(context.TODO(), g.jenkins) } // WaitForSecretSynchronization runs groovy script which waits to synchronize secrets in pod by k8s func (g *Groovy) WaitForSecretSynchronization(secretsPath string) (requeue bool, err error) { if len(g.customization.Secret.Name) == 0 { return false, nil } secret := &corev1.Secret{} err = g.k8sClient.Get(context.TODO(), types.NamespacedName{Name: g.customization.Secret.Name, Namespace: g.jenkins.ObjectMeta.Namespace}, secret) if err != nil { return true, errors.WithStack(err) } toCalculate := map[string]string{} for secretKey, secretValue := range secret.Data { toCalculate[secretKey] = string(secretValue) } hash, err := g.calculateHash(toCalculate) if err != nil { return true, errors.WithStack(err) } name := "synchronizing-secret.groovy" if g.isGroovyScriptAlreadyApplied(g.customization.Secret.Name, name, hash) { return false, nil } g.logger.Info(fmt.Sprintf("%s Secret '%s' running synchronization", g.configurationType, secret.Name)) return g.EnsureSingle(g.customization.Secret.Name, name, hash, fmt.Sprintf(synchronizeSecretsGroovyScriptFmt, secretsPath, hash)) } // Ensure runs all groovy scripts configured in customization structure func (g *Groovy) Ensure(filter func(name string) bool, updateGroovyScript func(groovyScript string) string) (requeue bool, err error) { secret := &corev1.Secret{} if len(g.customization.Secret.Name) > 0 { err := g.k8sClient.Get(context.TODO(), types.NamespacedName{ Name: g.customization.Secret.Name, Namespace: g.jenkins.ObjectMeta.Namespace, }, secret) if err != nil { return true, err } } for _, configMapRef := range g.customization.Configurations { configMap := &corev1.ConfigMap{} err := g.k8sClient.Get(context.TODO(), types.NamespacedName{Name: configMapRef.Name, Namespace: g.jenkins.ObjectMeta.Namespace}, configMap) if err != nil { return true, errors.WithStack(err) } var names []string for name := range configMap.Data { names = append(names, name) } sort.Strings(names) for _, name := range names { groovyScript := updateGroovyScript(configMap.Data[name]) if !filter(name) { g.logger.V(log.VDebug).Info(fmt.Sprintf("Skipping %s ConfigMap '%s' name '%s'", g.configurationType, configMap.Name, name)) continue } hash, err := g.calculateCustomizationHash(*secret, name, groovyScript) if err != nil { return true, errors.WithStack(err) } if g.isGroovyScriptAlreadyApplied(configMap.Name, name, hash) { continue } g.logger.Info(fmt.Sprintf("%s ConfigMap '%s' name '%s' running groovy script", g.configurationType, configMap.Name, name)) requeue, err := g.EnsureSingle(configMap.Name, name, hash, groovyScript) if err != nil || requeue { return requeue, err } } } return false, nil } func (g *Groovy) calculateCustomizationHash(secret corev1.Secret, key, groovyScript string) (string, error) { toCalculate := map[string]string{} for secretKey, secretValue := range secret.Data { toCalculate[secretKey] = string(secretValue) } toCalculate[key] = groovyScript return g.calculateHash(toCalculate) } func (g *Groovy) isGroovyScriptAlreadyApplied(source, name, hash string) bool { for _, appliedGroovyScript := range g.jenkins.Status.AppliedGroovyScripts { if appliedGroovyScript.ConfigurationType == g.configurationType && appliedGroovyScript.Hash == hash && appliedGroovyScript.Name == name && appliedGroovyScript.Source == source { return true } } return false } func (g *Groovy) calculateHash(data map[string]string) (string, error) { hash := sha256.New() keys := []string{} for key := range data { keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { _, err := hash.Write([]byte(key)) if err != nil { return "", err } _, err = hash.Write([]byte(data[key])) if err != nil { return "", err } } return base64.StdEncoding.EncodeToString(hash.Sum(nil)), nil } // AddSecretsLoaderToGroovyScript modify groovy scripts to load Kubernetes secrets into groovy map func AddSecretsLoaderToGroovyScript(secretsPath string) func(groovyScript string) string { return func(groovyScript string) string { if !strings.HasPrefix(groovyScript, importPrefix) { return fmt.Sprintf(secretsLoaderGroovyScriptFmt, secretsPath) + groovyScript } lines := strings.Split(groovyScript, "\n") importIndex := -1 for i, line := range lines { if !strings.HasPrefix(line, importPrefix) { importIndex = i break } } return strings.Join(lines[:importIndex], "\n") + "\n\n" + fmt.Sprintf(secretsLoaderGroovyScriptFmt, secretsPath) + "\n\n" + strings.Join(lines[importIndex:], "\n") } } const importPrefix = "import " const secretsLoaderGroovyScriptFmt = `def secretsPath = '%s' def secrets = [:] "ls ${secretsPath}".execute().text.eachLine {secrets[it] = new File("${secretsPath}/${it}").text}` const synchronizeSecretsGroovyScriptFmt = ` def secretsPath = '%s' def expectedHash = '%s' println "Synchronizing Kubernetes Secret to the Jenkins master pod, timeout 60 seconds." def complete = false for(int i = 1; i <= 30; i++) { def fileList = "ls ${secretsPath}".execute() def secrets = [] fileList .text.eachLine {secrets.add(it)} println "Mounted secrets: ${secrets}" def actualHash = calculateHash((String[])secrets, secretsPath) println "Expected hash '${expectedHash}', actual hash '${actualHash}', will retry" if(expectedHash == actualHash) { complete = true break } sleep 2000 } if(!complete) { throw new Exception("Timeout while synchronizing files") } def calculateHash(String[] secrets, String secretsPath) { def hash = java.security.MessageDigest.getInstance("SHA-256") for(secret in secrets) { hash.update(secret.getBytes()) def fileLocation = java.nio.file.Paths.get("${secretsPath}/${secret}") def fileData = java.nio.file.Files.readAllBytes(fileLocation) hash.update(fileData) } return Base64.getEncoder().encodeToString(hash.digest()) } `