diff --git a/pkg/controller/jenkins/client/jenkins.go b/pkg/controller/jenkins/client/jenkins.go index 0e278cff..3aa64915 100644 --- a/pkg/controller/jenkins/client/jenkins.go +++ b/pkg/controller/jenkins/client/jenkins.go @@ -3,15 +3,18 @@ package client import ( "bytes" "fmt" - "net/http" - "os/exec" - "strings" - "github.com/bndr/gojenkins" "github.com/pkg/errors" + "net/http" + "os/exec" + "regexp" + "strings" ) -var errorNotFound = errors.New("404") +var ( + errorNotFound = errors.New("404") + regex = regexp.MustCompile("()(?P[a-z0-9]*)") +) // Jenkins defines Jenkins API type Jenkins interface { @@ -52,6 +55,7 @@ type Jenkins interface { CreateView(name string, viewType string) (*gojenkins.View, error) Poll() (int, error) ExecuteScript(groovyScript string) (logs string, err error) + GetNodeSecret(name string) (string, error) } type jenkins struct { @@ -136,3 +140,23 @@ func isNotFoundError(err error) bool { } return false } + +func (jenkins *jenkins) GetNodeSecret(name string) (string, error) { + var content string + _, err := jenkins.Requester.GetXML(fmt.Sprintf("/computer/%s/slave-agent.jnlp", name), &content, nil) + + if err != nil { + return "", err + } + + match := regex.FindStringSubmatch(content) + result := make(map[string]string) + + for i, name := range regex.SubexpNames() { + if i != 0 && name != "" { + result[name] = match[i] + } + } + + return result["secret"], nil +} diff --git a/pkg/controller/jenkins/client/mockgen.go b/pkg/controller/jenkins/client/mockgen.go index 062b7bd0..404584df 100644 --- a/pkg/controller/jenkins/client/mockgen.go +++ b/pkg/controller/jenkins/client/mockgen.go @@ -16,6 +16,19 @@ type MockJenkins struct { recorder *MockJenkinsMockRecorder } +func (m *MockJenkins) GetNodeSecret(name string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetNodeSecret", name) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +func (mr *MockJenkinsMockRecorder) GetNodeSecret(name string) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNodeSecret", reflect.TypeOf((*MockJenkins)(nil).GetNodeSecret), name) +} + // MockJenkinsMockRecorder is the mock recorder for MockJenkins type MockJenkinsMockRecorder struct { mock *MockJenkins 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..9de69757 100644 --- a/pkg/controller/jenkins/configuration/base/resources/base_configuration_configmap.go +++ b/pkg/controller/jenkins/configuration/base/resources/base_configuration_configmap.go @@ -168,7 +168,6 @@ import jenkins.model.GlobalConfiguration GlobalConfiguration.all().get(GlobalJobDslSecurityConfiguration.class).useScriptSecurity=false GlobalConfiguration.all().get(GlobalJobDslSecurityConfiguration.class).save() ` - // GetBaseConfigurationConfigMapName returns name of Kubernetes config map used to base configuration func GetBaseConfigurationConfigMapName(jenkins *v1alpha2.Jenkins) string { return fmt.Sprintf("%s-base-configuration-%s", constants.OperatorName, jenkins.ObjectMeta.Name) diff --git a/pkg/controller/jenkins/configuration/user/seedjobs/seedjobs.go b/pkg/controller/jenkins/configuration/user/seedjobs/seedjobs.go index 26eccdc8..0933937a 100644 --- a/pkg/controller/jenkins/configuration/user/seedjobs/seedjobs.go +++ b/pkg/controller/jenkins/configuration/user/seedjobs/seedjobs.go @@ -5,20 +5,24 @@ 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" + "reflect" "github.com/go-logr/logr" stackerr "github.com/pkg/errors" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" k8s "sigs.k8s.io/controller-runtime/pkg/client" + + apps "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" ) const ( @@ -42,6 +46,10 @@ const ( // JenkinsCredentialTypeLabelName is label for kubernetes-credentials-provider-plugin which determine Jenkins // credential type JenkinsCredentialTypeLabelName = "jenkins.io/credentials-type" + + // AgentName is the name of seed job + AgentName = "seed-job-agent" + AgentNamespace = "default" ) // SeedJobs defines API for configuring and ensuring Jenkins Seed Jobs and Deploy Keys @@ -88,6 +96,24 @@ 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 } @@ -233,6 +259,122 @@ func (s *SeedJobs) isRecreatePodNeeded(jenkins v1alpha2.Jenkins) bool { return false } +func CreateAgent(jenkinsClient jenkinsclient.Jenkins, k8sClient client.Client, jenkinsManifest *v1alpha2.Jenkins, namespace string, agentName string) error { + var exists bool + + nodes, err := jenkinsClient.GetAllNodes() + if err != nil { + return err + } + + if len(nodes) != 0 { + for _, node := range nodes { + if node.GetName() == agentName { + exists = true + } + } + } + + // Create node if not exists + if !exists { + _, err = jenkinsClient.CreateNode(agentName, 1, "The jenkins-operator generated agent", "/home/jenkins", agentName) + if err != nil { + return err + } + } + + 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) + 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) + 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), + Namespace: namespace, + }, + Spec: apps.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: fmt.Sprintf("%s-container", agentName), + Image: "jenkins/jnlp-slave:alpine", + Env: []corev1.EnvVar{ + { + Name: "JENKINS_TUNNEL", + Value: fmt.Sprintf("%s.%s:%d", + resources.GetJenkinsSlavesServiceName(jenkinsManifest), + jenkinsManifest.ObjectMeta.Namespace, + jenkinsManifest.Spec.SlaveService.Port), + }, + { + Name: "JENKINS_SECRET", + Value: secret, + }, + { + Name: "JENKINS_AGENT_NAME", + Value: agentName, + }, + { + Name: "JENKINS_URL", + Value: fmt.Sprintf("http://%s.%s:%d", + resources.GetJenkinsHTTPServiceName(jenkinsManifest), + jenkinsManifest.ObjectMeta.Namespace, + jenkinsManifest.Spec.Service.Port, + ), + }, + { + Name: "JENKINS_AGENT_WORKDIR", + Value: "/home/jenkins/agent", + }, + }, + }, + }, + }, + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app": fmt.Sprintf("%s-selector", agentName), + }, + }, + }, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": fmt.Sprintf("%s-selector", agentName), + }, + }, + }, + Status: apps.DeploymentStatus{}, + } +} + // seedJobConfigXML this is the XML representation of seed job var seedJobConfigXML = ` @@ -319,7 +461,6 @@ executeDslScripts.setSandbox(false) executeDslScripts.setRemovedJobAction(RemovedJobAction.DELETE) executeDslScripts.setRemovedViewAction(RemovedViewAction.DELETE) executeDslScripts.setLookupStrategy(LookupStrategy.SEED_JOB) -executeDslScripts.setAdditionalClasspath("src") if (jobRef == null) { jobRef = jenkins.createProject(FreeStyleProject, jobDslSeedName) @@ -329,7 +470,7 @@ jobRef.getBuildersList().add(executeDslScripts) jobRef.setDisplayName("${params.` + displayNameParameterName + `}") jobRef.setScm(scm) // TODO don't use master executors -jobRef.setAssignedLabel(new LabelAtom("master")) +jobRef.setAssignedLabel(new LabelAtom("`+AgentName+`")) jenkins.getQueue().schedule(jobRef) diff --git a/pkg/controller/jenkins/configuration/user/seedjobs/seedjobs_test.go b/pkg/controller/jenkins/configuration/user/seedjobs/seedjobs_test.go index 3dda0eb3..224f89fe 100644 --- a/pkg/controller/jenkins/configuration/user/seedjobs/seedjobs_test.go +++ b/pkg/controller/jenkins/configuration/user/seedjobs/seedjobs_test.go @@ -6,12 +6,13 @@ import ( "testing" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" - "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/bndr/gojenkins" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -28,7 +29,7 @@ func TestEnsureSeedJobs(t *testing.T) { ctx := context.TODO() defer ctrl.Finish() - jenkinsClient := client.NewMockJenkins(ctrl) + jenkinsClient := jenkinsclient.NewMockJenkins(ctrl) fakeClient := fake.NewFakeClient() err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme) assert.NoError(t, err) @@ -219,3 +220,80 @@ func TestSeedJobs_isRecreatePodNeeded(t *testing.T) { assert.True(t, got) }) } + +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) + ctx := context.TODO() + defer ctrl.Finish() + + namespace := "test-namespace" + agentName := "test-agent" + secret := "test-secret" + + jenkinsClient := jenkinsclient.NewMockJenkins(ctrl) + fakeClient := fake.NewFakeClient() + err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme) + assert.NoError(t, err) + + 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) + + // when + err = fakeClient.Create(ctx, &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-deployment", agentName), + Namespace:namespace, + }, + }) + + assert.NoError(t, err) + + // then + err = CreateAgent(jenkinsClient, fakeClient, jenkinsCustomResource(), namespace, agentName) + assert.NoError(t, err) + }) +} \ No newline at end of file diff --git a/pkg/controller/jenkins/constants/constants.go b/pkg/controller/jenkins/constants/constants.go index fbf5434b..ca9b5e18 100644 --- a/pkg/controller/jenkins/constants/constants.go +++ b/pkg/controller/jenkins/constants/constants.go @@ -4,7 +4,7 @@ const ( // OperatorName is a operator name OperatorName = "jenkins-operator" // DefaultAmountOfExecutors is the default amount of Jenkins executors - DefaultAmountOfExecutors = 3 + DefaultAmountOfExecutors = 0 // SeedJobSuffix is a suffix added for all seed jobs SeedJobSuffix = "job-dsl-seed" // DefaultJenkinsMasterImage is the default Jenkins master docker image