package seedjobs import ( "context" "crypto/sha256" "encoding/base64" "fmt" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" 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/constants" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/jobs" "github.com/jenkinsci/kubernetes-operator/pkg/log" "github.com/go-logr/logr" stackerr "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" k8s "sigs.k8s.io/controller-runtime/pkg/client" ) const ( // ConfigureSeedJobsName this is the fixed seed job name ConfigureSeedJobsName = constants.OperatorName + "-configure-seed-job" idParameterName = "ID" credentialIDParameterName = "CREDENTIAL_ID" repositoryURLParameterName = "REPOSITORY_URL" repositoryBranchParameterName = "REPOSITORY_BRANCH" targetsParameterName = "TARGETS" displayNameParameterName = "SEED_JOB_DISPLAY_NAME" // UsernameSecretKey is username data key in Kubernetes secret used to create Jenkins username/password credential UsernameSecretKey = "username" // PasswordSecretKey is password data key in Kubernetes secret used to create Jenkins username/password credential PasswordSecretKey = "password" // PrivateKeySecretKey is private key data key in Kubernetes secret used to create Jenkins SSH credential PrivateKeySecretKey = "privateKey" // JenkinsCredentialTypeLabelName is label for kubernetes-credentials-provider-plugin which determine Jenkins // credential type JenkinsCredentialTypeLabelName = "jenkins.io/credentials-type" ) // SeedJobs defines API for configuring and ensuring Jenkins Seed Jobs and Deploy Keys type SeedJobs struct { jenkinsClient jenkinsclient.Jenkins k8sClient k8s.Client logger logr.Logger } // New creates SeedJobs object func New(jenkinsClient jenkinsclient.Jenkins, k8sClient k8s.Client, logger logr.Logger) *SeedJobs { return &SeedJobs{ jenkinsClient: jenkinsClient, k8sClient: k8sClient, logger: logger, } } // EnsureSeedJobs configures seed job and runs it for every entry from Jenkins.Spec.SeedJobs func (s *SeedJobs) EnsureSeedJobs(jenkins *v1alpha2.Jenkins) (done bool, err error) { if err = s.createJob(); err != nil { s.logger.V(log.VWarn).Info("Couldn't create jenkins seed job") return false, err } if err = s.ensureLabelsForSecrets(*jenkins); err != nil { return false, err } done, err = s.buildJobs(jenkins) if err != nil { s.logger.V(log.VWarn).Info("Couldn't build jenkins seed job") return false, err } return done, nil } // createJob is responsible for creating jenkins job which configures jenkins seed jobs and deploy keys func (s *SeedJobs) createJob() error { _, created, err := s.jenkinsClient.CreateOrUpdateJob(seedJobConfigXML, ConfigureSeedJobsName) if err != nil { return err } if created { s.logger.Info(fmt.Sprintf("'%s' job has been created", ConfigureSeedJobsName)) } return nil } // ensureLabelsForSecrets adds labels to Kubernetes secrets where are Jenkins credentials used for seed jobs, // thanks to them kubernetes-credentials-provider-plugin will create Jenkins credentials in Jenkins and // Operator will able to watch any changes made to them func (s *SeedJobs) ensureLabelsForSecrets(jenkins v1alpha2.Jenkins) error { for _, seedJob := range jenkins.Spec.SeedJobs { if seedJob.JenkinsCredentialType == v1alpha2.BasicSSHCredentialType || seedJob.JenkinsCredentialType == v1alpha2.UsernamePasswordCredentialType { requiredLabels := resources.BuildLabelsForWatchedResources(jenkins) requiredLabels[JenkinsCredentialTypeLabelName] = string(seedJob.JenkinsCredentialType) secret := &corev1.Secret{} namespaceName := types.NamespacedName{Namespace: jenkins.ObjectMeta.Namespace, Name: seedJob.CredentialID} err := s.k8sClient.Get(context.TODO(), namespaceName, secret) if err != nil { return stackerr.WithStack(err) } if !resources.VerifyIfLabelsAreSet(secret, requiredLabels) { secret.ObjectMeta.Labels = requiredLabels err = stackerr.WithStack(s.k8sClient.Update(context.TODO(), secret)) if err != nil { return err } } } } return nil } // buildJobs is responsible for running jenkins builds which configures jenkins seed jobs and deploy keys func (s *SeedJobs) buildJobs(jenkins *v1alpha2.Jenkins) (done bool, err error) { allDone := true for _, seedJob := range jenkins.Spec.SeedJobs { credentialValue, err := s.credentialValue(jenkins.Namespace, seedJob) if err != nil { return false, err } parameters := map[string]string{ idParameterName: seedJob.ID, credentialIDParameterName: seedJob.CredentialID, repositoryURLParameterName: seedJob.RepositoryURL, repositoryBranchParameterName: seedJob.RepositoryBranch, targetsParameterName: seedJob.Targets, displayNameParameterName: fmt.Sprintf("Seed Job from %s", seedJob.ID), } hash := sha256.New() hash.Write([]byte(parameters[idParameterName])) hash.Write([]byte(parameters[credentialIDParameterName])) hash.Write([]byte(credentialValue)) hash.Write([]byte(parameters[repositoryURLParameterName])) hash.Write([]byte(parameters[repositoryBranchParameterName])) hash.Write([]byte(parameters[targetsParameterName])) hash.Write([]byte(parameters[displayNameParameterName])) encodedHash := base64.URLEncoding.EncodeToString(hash.Sum(nil)) jobsClient := jobs.New(s.jenkinsClient, s.k8sClient, s.logger) done, err := jobsClient.EnsureBuildJob(ConfigureSeedJobsName, encodedHash, parameters, jenkins, true) if err != nil { return false, err } if !done { allDone = false } } return allDone, nil } func (s *SeedJobs) credentialValue(namespace string, seedJob v1alpha2.SeedJob) (string, error) { if seedJob.JenkinsCredentialType == v1alpha2.BasicSSHCredentialType || seedJob.JenkinsCredentialType == v1alpha2.UsernamePasswordCredentialType { secret := &corev1.Secret{} namespaceName := types.NamespacedName{Namespace: namespace, Name: seedJob.CredentialID} err := s.k8sClient.Get(context.TODO(), namespaceName, secret) if err != nil { return "", err } if seedJob.JenkinsCredentialType == v1alpha2.BasicSSHCredentialType { return string(secret.Data[PrivateKeySecretKey]), nil } return string(secret.Data[UsernameSecretKey]) + string(secret.Data[PasswordSecretKey]), nil } return "", nil } // seedJobConfigXML this is the XML representation of seed job var seedJobConfigXML = ` Configure Seed Jobs false ` + idParameterName + ` false ` + credentialIDParameterName + ` false ` + repositoryURLParameterName + ` false ` + repositoryBranchParameterName + ` master false ` + displayNameParameterName + ` false ` + targetsParameterName + ` cicd/jobs/*.jenkins false false false `