Add seedjob agent
This commit is contained in:
		
							parent
							
								
									76e72702f2
								
							
						
					
					
						commit
						ebd44b83fc
					
				|  | @ -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("(<application-desc main-class=\"hudson.remoting.jnlp.Main\"><argument>)(?P<secret>[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 | ||||
| } | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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 = ` | ||||
| <flow-definition plugin="workflow-job@2.30"> | ||||
|  | @ -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) | ||||
| </script> | ||||
|  |  | |||
|  | @ -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) | ||||
| 	}) | ||||
| } | ||||
|  | @ -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
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue