diff --git a/pkg/controller/jenkins/client/jenkins.go b/pkg/controller/jenkins/client/jenkins.go index 246e6da8..5d6f6f35 100644 --- a/pkg/controller/jenkins/client/jenkins.go +++ b/pkg/controller/jenkins/client/jenkins.go @@ -151,6 +151,10 @@ func (jenkins *jenkins) GetNodeSecret(name string) (string, error) { } match := regex.FindStringSubmatch(content) + if match == nil { + return "", errors.New("Node secret cannot be parsed") + } + result := make(map[string]string) for i, name := range regex.SubexpNames() { diff --git a/pkg/controller/jenkins/configuration/base/resources/base_configuration_configmap.go b/pkg/controller/jenkins/configuration/base/resources/base_configuration_configmap.go index 18ec0b2e..258e5756 100644 --- a/pkg/controller/jenkins/configuration/base/resources/base_configuration_configmap.go +++ b/pkg/controller/jenkins/configuration/base/resources/base_configuration_configmap.go @@ -2,7 +2,6 @@ package resources import ( "fmt" - "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants" diff --git a/pkg/controller/jenkins/configuration/user/seedjobs/seedjobs.go b/pkg/controller/jenkins/configuration/user/seedjobs/seedjobs.go index 4791b9e3..7685dcf3 100644 --- a/pkg/controller/jenkins/configuration/user/seedjobs/seedjobs.go +++ b/pkg/controller/jenkins/configuration/user/seedjobs/seedjobs.go @@ -5,14 +5,12 @@ import ( "crypto/sha256" "encoding/base64" "fmt" - "reflect" - "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/jenkinsci/kubernetes-operator/pkg/controller/jenkins/groovy" + "reflect" "github.com/go-logr/logr" stackerr "github.com/pkg/errors" @@ -26,16 +24,6 @@ import ( ) 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 @@ -49,9 +37,6 @@ const ( // AgentName is the name of seed job agent AgentName = "seed-job-agent" - - // AgentNamespace is the namespace of seed job agent - AgentNamespace = "default" ) // SeedJobs defines API for configuring and ensuring Jenkins Seed Jobs and Deploy Keys @@ -77,20 +62,35 @@ func (s *SeedJobs) EnsureSeedJobs(jenkins *v1alpha2.Jenkins) (done bool, err err return false, s.restartJenkinsMasterPod(*jenkins) } - if err = s.createJob(); err != nil { - s.logger.V(log.VWarn).Info("Couldn't create jenkins seed job") - return false, err + if len(jenkins.Spec.SeedJobs) > 0 { + err := s.createAgent(s.jenkinsClient, s.k8sClient, jenkins, jenkins.Namespace, AgentName) + if err != nil { + return false, err + } + } else if len(jenkins.Spec.SeedJobs) == 0 { + err := s.k8sClient.Delete(context.TODO(), &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: jenkins.Namespace, + Name: fmt.Sprintf("%s-deployment", AgentName), + }, + }) + + if err != nil { + return false, err + } } if err = s.ensureLabelsForSecrets(*jenkins); err != nil { return false, err } - done, err = s.buildJobs(jenkins) + requeue, err := s.createJobs(jenkins) if err != nil { - s.logger.V(log.VWarn).Info("Couldn't build jenkins seed job") return false, err } + if requeue { + return false, nil + } seedJobIDs := s.getAllSeedJobIDs(*jenkins) if done && !reflect.DeepEqual(seedJobIDs, jenkins.Status.CreatedSeedJobs) { @@ -98,37 +98,35 @@ func (s *SeedJobs) EnsureSeedJobs(jenkins *v1alpha2.Jenkins) (done bool, err err return false, stackerr.WithStack(s.k8sClient.Update(context.TODO(), jenkins)) } - if len(seedJobIDs) > 0 { - err := CreateAgent(s.jenkinsClient, s.k8sClient, jenkins, jenkins.Namespace, AgentName) - if err != nil { - panic(err) - } - } else if len(seedJobIDs) == 0 { - err := s.k8sClient.Delete(context.TODO(), &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: AgentNamespace, - Name: fmt.Sprintf("%s-deployment", AgentName), - }, - }) - - if err != nil { - return done, err - } - } - - return done, nil + return true, 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 +func (s *SeedJobs) createJobs(jenkins *v1alpha2.Jenkins) (bool, error) { + groovyClient := groovy.New(s.jenkinsClient, s.k8sClient, s.logger, jenkins, "user-groovy", jenkins.Spec.GroovyScripts.Customization) + for _, seedJob := range jenkins.Spec.SeedJobs { + credentialValue, err := s.credentialValue(jenkins.Namespace, seedJob) + if err != nil { + return true, err + } + + groovyScript := seedJobCreatingGroovyScript(seedJob) + + hash := sha256.New() + hash.Write([]byte(groovyScript)) + hash.Write([]byte(credentialValue)) + requeue, err := groovyClient.EnsureSingle(seedJob.ID, fmt.Sprintf("%s.groovy", seedJob.ID), base64.URLEncoding.EncodeToString(hash.Sum(nil)), groovyScript) + + if err != nil { + return true, err + } + + if requeue { + return true, nil + } } - if created { - s.logger.Info(fmt.Sprintf("'%s' job has been created", ConfigureSeedJobsName)) - } - return nil + + return false, nil } // ensureLabelsForSecrets adds labels to Kubernetes secrets where are Jenkins credentials used for seed jobs, @@ -160,45 +158,6 @@ func (s *SeedJobs) ensureLabelsForSecrets(jenkins v1alpha2.Jenkins) error { 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{} @@ -262,7 +221,7 @@ func (s *SeedJobs) isRecreatePodNeeded(jenkins v1alpha2.Jenkins) bool { } // CreateAgent deploys Jenkins agent to Kubernetes cluster -func CreateAgent(jenkinsClient jenkinsclient.Jenkins, k8sClient client.Client, jenkinsManifest *v1alpha2.Jenkins, namespace string, agentName string) error { +func (s SeedJobs) createAgent(jenkinsClient jenkinsclient.Jenkins, k8sClient client.Client, jenkinsManifest *v1alpha2.Jenkins, namespace string, agentName string) error { var exists bool nodes, err := jenkinsClient.GetAllNodes() @@ -286,41 +245,28 @@ func CreateAgent(jenkinsClient jenkinsclient.Jenkins, k8sClient client.Client, j } } - deployments := &apps.DeploymentList{} - exists = false secret, err := jenkinsClient.GetNodeSecret(agentName) if err != nil { return err } deployment := agentDeployment(jenkinsManifest, namespace, agentName, secret) - err = k8sClient.List(context.TODO(), &client.ListOptions{}, deployments) + + err = k8sClient.Create(context.TODO(), deployment) if err != nil { - return err - } - - if len(deployments.Items) > 0 { - for _, deployment := range deployments.Items { - if deployment.ObjectMeta.Name == fmt.Sprintf("%s-deployment", agentName) { - exists = true - } - } - } - - // Create deployment if not exists - if !exists { - err = k8sClient.Create(context.TODO(), deployment) + err := k8sClient.Update(context.TODO(), deployment) if err != nil { return err } } + return nil } func agentDeployment(jenkinsManifest *v1alpha2.Jenkins, namespace string, agentName string, secret string) *apps.Deployment { return &apps.Deployment{ ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("%s-deployment", agentName), + Name: "jnlp", Namespace: namespace, }, Spec: apps.DeploymentSpec{ @@ -374,60 +320,26 @@ func agentDeployment(jenkinsManifest *v1alpha2.Jenkins, namespace string, agentN }, }, }, - Status: apps.DeploymentStatus{}, } } -// 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 - + ` +} diff --git a/pkg/controller/jenkins/configuration/user/seedjobs/seedjobs_test.go b/pkg/controller/jenkins/configuration/user/seedjobs/seedjobs_test.go index d45807ea..c4e96395 100644 --- a/pkg/controller/jenkins/configuration/user/seedjobs/seedjobs_test.go +++ b/pkg/controller/jenkins/configuration/user/seedjobs/seedjobs_test.go @@ -16,7 +16,6 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes/scheme" "sigs.k8s.io/controller-runtime/pkg/client/fake" logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" @@ -37,7 +36,6 @@ func TestEnsureSeedJobs(t *testing.T) { jenkins := jenkinsCustomResource() err = fakeClient.Create(ctx, jenkins) assert.NoError(t, err) - buildNumber := int64(1) agentName := "seed-job-agent" secret := "test-secret" @@ -52,78 +50,12 @@ func TestEnsureSeedJobs(t *testing.T) { jenkinsClient.EXPECT().CreateNode(agentName, 1, "The jenkins-operator generated agent", "/home/jenkins", agentName).Return(testNode, nil) jenkinsClient.EXPECT().GetNode(agentName).Return(testNode, nil).AnyTimes() - for reconcileAttempt := 1; reconcileAttempt <= 2; reconcileAttempt++ { - logger.Info(fmt.Sprintf("Reconcile attempt #%d", reconcileAttempt)) + jenkinsClient.EXPECT().ExecuteScript(seedJobCreatingGroovyScript(jenkins.Spec.SeedJobs[0])).AnyTimes() - seedJobs := New(jenkinsClient, fakeClient, logger) + seedJobClient := New(jenkinsClient, fakeClient, logger) - // first run - should create job and schedule build - if reconcileAttempt == 1 { - jenkinsClient. - EXPECT(). - CreateOrUpdateJob(seedJobConfigXML, ConfigureSeedJobsName). - Return(nil, true, nil) - - jenkinsClient. - EXPECT(). - GetJob(ConfigureSeedJobsName). - Return(&gojenkins.Job{ - Raw: &gojenkins.JobResponse{ - NextBuildNumber: buildNumber, - }, - }, nil) - - jenkinsClient. - EXPECT(). - BuildJob(ConfigureSeedJobsName, gomock.Any()). - Return(int64(0), nil) - } - - // second run - should update and finish job - if reconcileAttempt == 2 { - jenkinsClient. - EXPECT(). - CreateOrUpdateJob(seedJobConfigXML, ConfigureSeedJobsName). - Return(nil, false, nil) - - jenkinsClient. - EXPECT(). - GetBuild(ConfigureSeedJobsName, gomock.Any()). - Return(&gojenkins.Build{ - Raw: &gojenkins.BuildResponse{ - Result: string(v1alpha2.BuildSuccessStatus), - }, - }, nil) - } - - done, err := seedJobs.EnsureSeedJobs(jenkins) - assert.NoError(t, err) - - err = fakeClient.Get(ctx, types.NamespacedName{Name: jenkins.Name, Namespace: jenkins.Namespace}, jenkins) - assert.NoError(t, err) - - assert.Equal(t, 1, len(jenkins.Status.Builds), "There is one running job") - build := jenkins.Status.Builds[0] - assert.Equal(t, buildNumber, build.Number) - assert.Equal(t, ConfigureSeedJobsName, build.JobName) - assert.NotNil(t, build.CreateTime) - assert.NotEmpty(t, build.Hash) - assert.NotNil(t, build.LastUpdateTime) - assert.Equal(t, 0, build.Retires) - - // first run - should create job and schedule build - if reconcileAttempt == 1 { - assert.False(t, done) - assert.Equal(t, string(v1alpha2.BuildRunningStatus), string(build.Status)) - } - - // second run - should update and finish job - if reconcileAttempt == 2 { - assert.False(t, done) - assert.Equal(t, string(v1alpha2.BuildSuccessStatus), string(build.Status)) - } - - } + _, err = seedJobClient.EnsureSeedJobs(jenkins) + assert.NoError(t, err) } func jenkinsCustomResource() *v1alpha2.Jenkins { @@ -235,47 +167,6 @@ func TestSeedJobs_isRecreatePodNeeded(t *testing.T) { } func TestCreateAgent(t *testing.T) { - t.Run("happy", func(t *testing.T) { - // given - //logger := logf.ZapLogger(false) - ctrl := gomock.NewController(t) - ctx := context.TODO() - defer ctrl.Finish() - - namespace := "test-namespace" - agentName := "test-agent" - secret := "test-secret" - jenkinsCustomRes := jenkinsCustomResource() - testNode := &gojenkins.Node{ - Raw: &gojenkins.NodeResponse{ - DisplayName: agentName, - }, - } - - jenkinsClient := jenkinsclient.NewMockJenkins(ctrl) - fakeClient := fake.NewFakeClient() - err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme) - assert.NoError(t, err) - - jenkinsClient.EXPECT().GetNode(agentName).Return(testNode, nil) - jenkinsClient.EXPECT().GetNodeSecret(agentName).Return(secret, nil) - jenkinsClient.EXPECT().GetAllNodes().Return([]*gojenkins.Node{}, nil) - jenkinsClient.EXPECT().CreateNode(agentName, 1, "The jenkins-operator generated agent", "/home/jenkins", agentName).Return(testNode, nil) - - // when - err = CreateAgent(jenkinsClient, fakeClient, jenkinsCustomRes, namespace, agentName) - assert.NoError(t, err) - - //then - err = fakeClient.Get(ctx, types.NamespacedName{Name: fmt.Sprintf("%s-deployment", agentName), Namespace: namespace}, &appsv1.Deployment{}) - assert.NoError(t, err) - - node, err := jenkinsClient.GetNode(agentName) - assert.NoError(t, err) - - assert.Equal(t, node.Raw.DisplayName, testNode.Raw.DisplayName) - }) - t.Run("not fail when deployment is available", func(t *testing.T) { // given ctrl := gomock.NewController(t) @@ -295,6 +186,8 @@ func TestCreateAgent(t *testing.T) { jenkinsClient.EXPECT().GetAllNodes().Return([]*gojenkins.Node{}, nil) jenkinsClient.EXPECT().CreateNode(agentName, 1, "The jenkins-operator generated agent", "/home/jenkins", agentName) + seedJobsClient := New(jenkinsClient, fakeClient, nil) + // when err = fakeClient.Create(ctx, &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ @@ -306,7 +199,7 @@ func TestCreateAgent(t *testing.T) { assert.NoError(t, err) // then - err = CreateAgent(jenkinsClient, fakeClient, jenkinsCustomResource(), namespace, agentName) + err = seedJobsClient.createAgent(jenkinsClient, fakeClient, jenkinsCustomResource(), namespace, agentName) assert.NoError(t, err) }) }