From 4c8e61624edee23fe1508ff0d95c94d87299a8d8 Mon Sep 17 00:00:00 2001 From: antoniaklja Date: Wed, 19 Dec 2018 22:40:08 +0100 Subject: [PATCH] Initial version of user reconciliation loop and seed jobs --- Makefile | 2 +- README.md | 74 +++++- .../crds/virtuslab_v1alpha1_jenkins_cr.yaml | 12 + deploy/operator.yaml | 2 +- deploy/seed_jobs_secret.yaml | 8 + pkg/apis/virtuslab/v1alpha1/jenkins_types.go | 19 +- .../v1alpha1/zz_generated.deepcopy.go | 50 ++++ pkg/controller/jenkins/client/jenkins.go | 2 +- .../jenkins/configuration/base/reconcile.go | 27 +-- .../resources/base_configuration_configmap.go | 3 +- .../base/resources/scripts_configmap.go | 16 +- .../jenkins/configuration/user/reconcile.go | 45 ++++ .../configuration/user/seedjobs/doc.go | 2 + .../configuration/user/seedjobs/seedjobs.go | 221 ++++++++++++++++++ .../jenkins/configuration/user/validate.go | 26 +++ pkg/controller/jenkins/jenkins_controller.go | 26 ++- .../template.go => render/render.go} | 5 +- test/e2e/base_configuration_test.go | 12 - test/e2e/jenkins.go | 51 ++++ test/e2e/restart_pod_test.go | 9 +- test/e2e/user_configuration_test.go | 40 ++++ test/e2e/wait.go | 14 +- 22 files changed, 622 insertions(+), 44 deletions(-) create mode 100644 deploy/seed_jobs_secret.yaml create mode 100644 pkg/controller/jenkins/configuration/user/reconcile.go create mode 100644 pkg/controller/jenkins/configuration/user/seedjobs/doc.go create mode 100644 pkg/controller/jenkins/configuration/user/seedjobs/seedjobs.go create mode 100644 pkg/controller/jenkins/configuration/user/validate.go rename pkg/controller/{jenkins/configuration/base/resources/template.go => render/render.go} (50%) create mode 100644 test/e2e/user_configuration_test.go diff --git a/Makefile b/Makefile index 1751707c..c29f0e36 100644 --- a/Makefile +++ b/Makefile @@ -160,7 +160,7 @@ else sed -i 's|REPLACE_ARGS||g' deploy/namespace-init.yaml endif - @RUNNING_TESTS=1 go test -parallel=2 "./test/e2e/" -tags "$(BUILDTAGS) cgo" -v \ + @RUNNING_TESTS=1 go test -parallel=1 "./test/e2e/" -tags "$(BUILDTAGS) cgo" -v \ -root=$(CURRENT_DIRECTORY) -kubeconfig=$(HOME)/.kube/config -globalMan deploy/crds/virtuslab_v1alpha1_jenkins_crd.yaml -namespacedMan deploy/namespace-init.yaml .PHONY: vet diff --git a/README.md b/README.md index c4f3a04a..a6254a19 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,79 @@ Kubernetes native Jenkins operator. Can be found [here][developer_guide]. +## Configuration + +This section describes Jenkins configuration. + +### Seed Jobs + +Jenkins operator uses [job-dsl][job-dsl] and [ssh-credentials][ssh-credentials] plugins for configuring seed jobs +and deploy keys. + + +It can be configured using `Jenkins.spec.seedJobs` section from custom resource manifest: + +``` +apiVersion: virtuslab.com/v1alpha1 +kind: Jenkins +metadata: + name: example +spec: + master: + image: jenkins/jenkins + seedJobs: + - id: jenkins-operator + targets: "cicd/jobs/*.jenkins" + description: "Jenkins Operator e2e tests repository" + repositoryBranch: master + repositoryUrl: git@github.com:VirtusLab/jenkins-operator-e2e.git + privateKey: + secretKeyRef: + name: deploy-keys + key: jenkins-operator-e2e +``` + +And corresponding Kubernetes Secret (in the same namespace) with private key: + +``` +apiVersion: v1 +kind: Secret +metadata: + name: deploy-keys +data: + jenkins-operator-e2e: | + -----BEGIN RSA PRIVATE KEY----- + MIIJKAIBAAKCAgEAxxDpleJjMCN5nusfW/AtBAZhx8UVVlhhhIKXvQ+dFODQIdzO + oDXybs1zVHWOj31zqbbJnsfsVZ9Uf3p9k6xpJ3WFY9b85WasqTDN1xmSd6swD4N8 + ... +``` + +If your GitHub repository is public, you don't have to configure `privateKey` and create Kubernetes Secret: + +``` +apiVersion: virtuslab.com/v1alpha1 +kind: Jenkins +metadata: + name: example +spec: + master: + image: jenkins/jenkins + seedJobs: + - id: jenkins-operator-e2e + targets: "cicd/jobs/*.jenkins" + description: "Jenkins Operator e2e tests repository" + repositoryBranch: master + repositoryUrl: https://github.com/VirtusLab/jenkins-operator-e2e.git +``` + +Jenkins operator will automatically configure and trigger Seed Job Pipeline for all entries from `Jenkins.spec.seedJobs`. + ## TODO Common: - simple library for sending Kubernetes events - implement Jenkins.Status in custom resource +- implement ensure for Jenkins jobs - state in Jenkins.Status Base configuration: - install configuration as a code Jenkins plugin @@ -21,9 +89,11 @@ Base configuration: User configuration: - user reconciliation loop (work in progress) - configure seed jobs and deploy keys (work in progress) -- e2e tests for seed jobs +- e2e tests for seed jobs (work in progress) - backup and restore for Jenkins jobs running as standalone job - trigger backup job before pod deletion using preStop k8s hooks - verify Jenkins configuration events -[developer_guide]:doc/developer-guide.md \ No newline at end of file +[developer_guide]:doc/developer-guide.md +[job-dsl]:https://github.com/jenkinsci/job-dsl-plugin +[ssh-credentials]:https://github.com/jenkinsci/ssh-credentials-plugin \ No newline at end of file diff --git a/deploy/crds/virtuslab_v1alpha1_jenkins_cr.yaml b/deploy/crds/virtuslab_v1alpha1_jenkins_cr.yaml index a32ae90e..817aa878 100644 --- a/deploy/crds/virtuslab_v1alpha1_jenkins_cr.yaml +++ b/deploy/crds/virtuslab_v1alpha1_jenkins_cr.yaml @@ -5,3 +5,15 @@ metadata: spec: master: image: jenkins/jenkins + seedJobs: + - id: jenkins-operator-e2e + targets: "cicd/jobs/*.jenkins" + description: "Jenkins Operator e2e tests repository" + repositoryBranch: master + repositoryUrl: https://github.com/VirtusLab/jenkins-operator-e2e.git +# Use configuration below if your GitHub repository is private +# repositoryUrl: git@github.com:VirtusLab/jenkins-operator-e2e.git +# privateKey: +# secretKeyRef: +# name: deploy-keys +# key: jenkins-operator-e2e diff --git a/deploy/operator.yaml b/deploy/operator.yaml index 99be636f..73dc7c70 100644 --- a/deploy/operator.yaml +++ b/deploy/operator.yaml @@ -17,7 +17,7 @@ spec: containers: - name: jenkins-operator # Replace this with the built image name - image: REPLACE_IMAGE + image: jenkins-operator ports: - containerPort: 60000 name: metrics diff --git a/deploy/seed_jobs_secret.yaml b/deploy/seed_jobs_secret.yaml new file mode 100644 index 00000000..f32ab471 --- /dev/null +++ b/deploy/seed_jobs_secret.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: deploy-keys +data: + jenkins-operator-e2e: | + REDACTED \ No newline at end of file diff --git a/pkg/apis/virtuslab/v1alpha1/jenkins_types.go b/pkg/apis/virtuslab/v1alpha1/jenkins_types.go index f5406851..fad950db 100644 --- a/pkg/apis/virtuslab/v1alpha1/jenkins_types.go +++ b/pkg/apis/virtuslab/v1alpha1/jenkins_types.go @@ -12,7 +12,8 @@ import ( type JenkinsSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file - Master JenkinsMaster `json:"master,omitempty"` + Master JenkinsMaster `json:"master,omitempty"` + SeedJobs []SeedJob `json:"seedJobs,omitempty"` } // JenkinsMaster defines the Jenkins master pod attributes @@ -27,6 +28,7 @@ type JenkinsStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file BaseConfigurationCompletedTime *metav1.Time `json:"baseConfigurationCompletedTime,omitempty"` + UserConfigurationCompletedTime *metav1.Time `json:"userConfigurationCompletedTime,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -50,6 +52,21 @@ type JenkinsList struct { Items []Jenkins `json:"items"` } +// SeedJob defined configuration for seed jobs and deploy keys +type SeedJob struct { + ID string `json:"id"` + Description string `json:"description,omitempty"` + Targets string `json:"targets,omitempty"` + RepositoryBranch string `json:"repositoryBranch,omitempty"` + RepositoryURL string `json:"repositoryUrl"` + PrivateKey PrivateKey `json:"privateKey,omitempty"` +} + +// PrivateKey contains a private key +type PrivateKey struct { + SecretKeyRef *corev1.SecretKeySelector `json:"secretKeyRef"` +} + func init() { SchemeBuilder.Register(&Jenkins{}, &JenkinsList{}) } diff --git a/pkg/apis/virtuslab/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/virtuslab/v1alpha1/zz_generated.deepcopy.go index f7f25b8d..a7d55c58 100644 --- a/pkg/apis/virtuslab/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/virtuslab/v1alpha1/zz_generated.deepcopy.go @@ -21,6 +21,7 @@ limitations under the License. package v1alpha1 import ( + v1 "k8s.io/api/core/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -113,6 +114,13 @@ func (in *JenkinsMaster) DeepCopy() *JenkinsMaster { func (in *JenkinsSpec) DeepCopyInto(out *JenkinsSpec) { *out = *in in.Master.DeepCopyInto(&out.Master) + if in.SeedJobs != nil { + in, out := &in.SeedJobs, &out.SeedJobs + *out = make([]SeedJob, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } @@ -133,6 +141,10 @@ func (in *JenkinsStatus) DeepCopyInto(out *JenkinsStatus) { in, out := &in.BaseConfigurationCompletedTime, &out.BaseConfigurationCompletedTime *out = (*in).DeepCopy() } + if in.UserConfigurationCompletedTime != nil { + in, out := &in.UserConfigurationCompletedTime, &out.UserConfigurationCompletedTime + *out = (*in).DeepCopy() + } return } @@ -145,3 +157,41 @@ func (in *JenkinsStatus) DeepCopy() *JenkinsStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PrivateKey) DeepCopyInto(out *PrivateKey) { + *out = *in + if in.SecretKeyRef != nil { + in, out := &in.SecretKeyRef, &out.SecretKeyRef + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrivateKey. +func (in *PrivateKey) DeepCopy() *PrivateKey { + if in == nil { + return nil + } + out := new(PrivateKey) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeedJob) DeepCopyInto(out *SeedJob) { + *out = *in + in.PrivateKey.DeepCopyInto(&out.PrivateKey) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeedJob. +func (in *SeedJob) DeepCopy() *SeedJob { + if in == nil { + return nil + } + out := new(SeedJob) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/controller/jenkins/client/jenkins.go b/pkg/controller/jenkins/client/jenkins.go index 1dbd010d..d7959073 100644 --- a/pkg/controller/jenkins/client/jenkins.go +++ b/pkg/controller/jenkins/client/jenkins.go @@ -93,7 +93,7 @@ func New(url, user, passwordOrToken string) (Jenkins, error) { return nil, err } if status != http.StatusOK { - return nil, fmt.Errorf("Invalid status code returned: %d", status) + return nil, fmt.Errorf("invalid status code returned: %d", status) } return jenkinsClient, nil diff --git a/pkg/controller/jenkins/configuration/base/reconcile.go b/pkg/controller/jenkins/configuration/base/reconcile.go index 89026e12..59154dbf 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile.go +++ b/pkg/controller/jenkins/configuration/base/reconcile.go @@ -45,59 +45,59 @@ func New(client client.Client, scheme *runtime.Scheme, logger logr.Logger, } // Reconcile takes care of base configuration -func (r *ReconcileJenkinsBaseConfiguration) Reconcile() (*reconcile.Result, error) { +func (r *ReconcileJenkinsBaseConfiguration) Reconcile() (*reconcile.Result, jenkinsclient.Jenkins, error) { if !r.validate(r.jenkins) { r.logger.V(log.VWarn).Info("Please correct Jenkins CR") - return &reconcile.Result{}, nil + return &reconcile.Result{}, nil, nil } metaObject := resources.NewResourceObjectMeta(r.jenkins) if err := r.createOperatorCredentialsSecret(metaObject); err != nil { - return &reconcile.Result{}, err + return &reconcile.Result{}, nil, err } r.logger.V(log.VDebug).Info("Operator credentials secret is present") if err := r.createScriptsConfigMap(metaObject); err != nil { - return &reconcile.Result{}, err + return &reconcile.Result{}, nil, err } r.logger.V(log.VDebug).Info("Scripts config map is present") if err := r.createBaseConfigurationConfigMap(metaObject); err != nil { - return &reconcile.Result{}, err + return &reconcile.Result{}, nil, err } r.logger.V(log.VDebug).Info("Base configuration config map is present") if err := r.createService(metaObject); err != nil { - return &reconcile.Result{}, err + return &reconcile.Result{}, nil, err } r.logger.V(log.VDebug).Info("Service is present") result, err := r.createJenkinsMasterPod(metaObject) if err != nil { - return &reconcile.Result{}, err + return &reconcile.Result{}, nil, err } if result != nil { - return result, nil + return result, nil, nil } r.logger.V(log.VDebug).Info("Jenkins master pod is present") result, err = r.waitForJenkins(metaObject) if err != nil { - return &reconcile.Result{}, err + return &reconcile.Result{}, nil, err } if result != nil { - return result, nil + return result, nil, nil } r.logger.V(log.VDebug).Info("Jenkins master pod is ready") - _, err = r.getJenkinsClient(metaObject) + jenkinsClient, err := r.getJenkinsClient(metaObject) if err != nil { - return &reconcile.Result{}, err + return &reconcile.Result{}, nil, err } r.logger.V(log.VDebug).Info("Jenkins API client set") - return nil, nil + return nil, jenkinsClient, nil } func (r *ReconcileJenkinsBaseConfiguration) createOperatorCredentialsSecret(meta metav1.ObjectMeta) error { @@ -240,6 +240,7 @@ func (r *ReconcileJenkinsBaseConfiguration) waitForJenkins(meta metav1.ObjectMet return nil, nil } +// FIXME(bantoniak) move jenkins client out of base.reconcile because it's needed for user.reconcile as well func (r *ReconcileJenkinsBaseConfiguration) getJenkinsClient(meta metav1.ObjectMeta) (jenkinsclient.Jenkins, error) { jenkinsURL, err := jenkinsclient.BuildJenkinsAPIUrl( r.jenkins.ObjectMeta.Namespace, meta.Name, resources.HTTPPortInt, r.local, r.minikube) 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 db268edf..5ede315e 100644 --- a/pkg/controller/jenkins/configuration/base/resources/base_configuration_configmap.go +++ b/pkg/controller/jenkins/configuration/base/resources/base_configuration_configmap.go @@ -5,6 +5,7 @@ import ( "text/template" virtuslabv1alpha1 "github.com/VirtusLab/jenkins-operator/pkg/apis/virtuslab/v1alpha1" + "github.com/VirtusLab/jenkins-operator/pkg/controller/render" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -40,7 +41,7 @@ func buildCreateJenkinsOperatorUserGroovyScript() (*string, error) { OperatorPasswordFile: OperatorCredentialsSecretPasswordKey, } - output, err := renderTemplate(createOperatorUserGroovyFmtTemplate, data) + output, err := render.Render(createOperatorUserGroovyFmtTemplate, data) if err != nil { return nil, err } diff --git a/pkg/controller/jenkins/configuration/base/resources/scripts_configmap.go b/pkg/controller/jenkins/configuration/base/resources/scripts_configmap.go index 08d6efff..b2f9abe3 100644 --- a/pkg/controller/jenkins/configuration/base/resources/scripts_configmap.go +++ b/pkg/controller/jenkins/configuration/base/resources/scripts_configmap.go @@ -6,6 +6,7 @@ import ( virtuslabv1alpha1 "github.com/VirtusLab/jenkins-operator/pkg/apis/virtuslab/v1alpha1" + "github.com/VirtusLab/jenkins-operator/pkg/controller/render" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -18,6 +19,19 @@ set -x mkdir -p {{ .JenkinsHomePath }}/init.groovy.d cp -n {{ .BaseConfigurationPath }}/*.groovy {{ .JenkinsHomePath }}/init.groovy.d +touch {{ .JenkinsHomePath }}/plugins.txt +cat > {{ .JenkinsHomePath }}/plugins.txt < + + + false + + + + + DEPLOY_KEY_ID + + + false + + + PRIVATE_KEY + + + + + REPOSITORY_URL + + + false + + + REPOSITORY_BRANCH + + master + false + + + SEED_JOB_DISPLAY_NAME + + + false + + + TARGETS + + cicd/jobs/*.jenkins + false + + + + + + + false + + + false + +` diff --git a/pkg/controller/jenkins/configuration/user/validate.go b/pkg/controller/jenkins/configuration/user/validate.go new file mode 100644 index 00000000..74843d89 --- /dev/null +++ b/pkg/controller/jenkins/configuration/user/validate.go @@ -0,0 +1,26 @@ +package user + +import ( + virtuslabv1alpha1 "github.com/VirtusLab/jenkins-operator/pkg/apis/virtuslab/v1alpha1" + "strings" +) + +func (r *ReconcileUserConfiguration) validate(jenkins *virtuslabv1alpha1.Jenkins) bool { + // validate jenkins.Spec.SeedJobs + if jenkins.Spec.SeedJobs != nil { + for _, seedJob := range jenkins.Spec.SeedJobs { + if len(seedJob.ID) == 0 { + r.logger.V(0).Info("seed job id can't be empty") + return false + } + + if strings.Contains(seedJob.RepositoryURL, "git@") { + if seedJob.PrivateKey.SecretKeyRef == nil { + r.logger.V(0).Info("private key can't be empty while using ssh repository url") + return false + } + } + } + } + return true +} diff --git a/pkg/controller/jenkins/jenkins_controller.go b/pkg/controller/jenkins/jenkins_controller.go index 7a99bebb..5702be6b 100644 --- a/pkg/controller/jenkins/jenkins_controller.go +++ b/pkg/controller/jenkins/jenkins_controller.go @@ -5,6 +5,7 @@ import ( virtuslabv1alpha1 "github.com/VirtusLab/jenkins-operator/pkg/apis/virtuslab/v1alpha1" "github.com/VirtusLab/jenkins-operator/pkg/controller/jenkins/configuration/base" + "github.com/VirtusLab/jenkins-operator/pkg/controller/jenkins/configuration/user" "github.com/VirtusLab/jenkins-operator/pkg/log" "github.com/go-logr/logr" @@ -74,8 +75,8 @@ type ReconcileJenkins struct { local, minikube bool } -// Reconcile reads that state of the cluster for a Jenkins object and makes changes based on the state read -// and what is in the Jenkins.Spec +// Reconcile it's a main reconciliation loop which maintain desired state for on Jenkins.Spec +// including base and user supplied configuration func (r *ReconcileJenkins) Reconcile(request reconcile.Request) (reconcile.Result, error) { logger := r.buildLogger(request.Name) logger.Info("Reconciling Jenkins") @@ -94,8 +95,9 @@ func (r *ReconcileJenkins) Reconcile(request reconcile.Request) (reconcile.Resul return reconcile.Result{}, err } + // Reconcile base configuration baseConfiguration := base.New(r.client, r.scheme, logger, jenkins, r.local, r.minikube) - result, err := baseConfiguration.Reconcile() + result, jenkinsClient, err := baseConfiguration.Reconcile() if err != nil { return reconcile.Result{}, err } @@ -111,6 +113,24 @@ func (r *ReconcileJenkins) Reconcile(request reconcile.Request) (reconcile.Resul } } + // Reconcile user configuration + userConfiguration := user.New(r.client, jenkinsClient, logger, jenkins) + result, err = userConfiguration.Reconcile() + if err != nil { + return reconcile.Result{}, err + } + if result != nil { + return *result, nil + } + if err == nil && result == nil && jenkins.Status.UserConfigurationCompletedTime == nil { + now := metav1.Now() + jenkins.Status.UserConfigurationCompletedTime = &now + err = r.client.Update(context.TODO(), jenkins) + if err != nil { + return reconcile.Result{}, err + } + } + return reconcile.Result{}, nil } diff --git a/pkg/controller/jenkins/configuration/base/resources/template.go b/pkg/controller/render/render.go similarity index 50% rename from pkg/controller/jenkins/configuration/base/resources/template.go rename to pkg/controller/render/render.go index a0f7c588..f0234aec 100644 --- a/pkg/controller/jenkins/configuration/base/resources/template.go +++ b/pkg/controller/render/render.go @@ -1,11 +1,12 @@ -package resources +package render import ( "bytes" "text/template" ) -func renderTemplate(template *template.Template, data interface{}) (string, error) { +// Render executes a parsed template (go-template) with configuration from data +func Render(template *template.Template, data interface{}) (string, error) { var buffer bytes.Buffer if err := template.Execute(&buffer, data); err != nil { return "", err diff --git a/test/e2e/base_configuration_test.go b/test/e2e/base_configuration_test.go index fd77a488..85e86737 100644 --- a/test/e2e/base_configuration_test.go +++ b/test/e2e/base_configuration_test.go @@ -5,8 +5,6 @@ import ( "testing" virtuslabv1alpha1 "github.com/VirtusLab/jenkins-operator/pkg/apis/virtuslab/v1alpha1" - - "github.com/bndr/gojenkins" ) func TestBaseConfiguration(t *testing.T) { @@ -22,16 +20,6 @@ func TestBaseConfiguration(t *testing.T) { verifyJenkinsAPIConnection(t, jenkins) } -func verifyJenkinsAPIConnection(t *testing.T, jenkins *virtuslabv1alpha1.Jenkins) *gojenkins.Jenkins { - client, err := createJenkinsAPIClient(jenkins) - if err != nil { - t.Fatal(err) - } - - t.Log("I can establish connection to Jenkins API") - return client -} - func verifyJenkinsMasterPodAttributes(t *testing.T, jenkins *virtuslabv1alpha1.Jenkins) { jenkinsPod := getJenkinsMasterPod(t, jenkins) diff --git a/test/e2e/jenkins.go b/test/e2e/jenkins.go index 68f4f6f4..f2494585 100644 --- a/test/e2e/jenkins.go +++ b/test/e2e/jenkins.go @@ -97,3 +97,54 @@ func createJenkinsCR(t *testing.T, namespace string) *virtuslabv1alpha1.Jenkins return jenkins } + +func createJenkinsCRWithSeedJob(t *testing.T, namespace string) *virtuslabv1alpha1.Jenkins { + jenkins := &virtuslabv1alpha1.Jenkins{ + ObjectMeta: metav1.ObjectMeta{ + Name: "e2e", + Namespace: namespace, + }, + Spec: virtuslabv1alpha1.JenkinsSpec{ + Master: virtuslabv1alpha1.JenkinsMaster{ + Image: "jenkins/jenkins", + Annotations: map[string]string{"test": "label"}, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("1"), + corev1.ResourceMemory: resource.MustParse("1Gi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("2"), + corev1.ResourceMemory: resource.MustParse("2Gi"), + }, + }, + }, + SeedJobs: []virtuslabv1alpha1.SeedJob{ + { + ID: "jenkins-operator-e2e", + Targets: "cicd/jobs/*.jenkins", + Description: "Jenkins Operator e2e tests repository", + RepositoryBranch: "master", + RepositoryURL: "https://github.com/VirtusLab/jenkins-operator-e2e.git", + }, + }, + }, + } + + t.Logf("Jenkins CR %+v", *jenkins) + if err := framework.Global.Client.Create(context.TODO(), jenkins, nil); err != nil { + t.Fatal(err) + } + + return jenkins +} + +func verifyJenkinsAPIConnection(t *testing.T, jenkins *virtuslabv1alpha1.Jenkins) *gojenkins.Jenkins { + client, err := createJenkinsAPIClient(jenkins) + if err != nil { + t.Fatal(err) + } + + t.Log("I can establish connection to Jenkins API") + return client +} diff --git a/test/e2e/restart_pod_test.go b/test/e2e/restart_pod_test.go index ed10a7e2..a5b2b639 100644 --- a/test/e2e/restart_pod_test.go +++ b/test/e2e/restart_pod_test.go @@ -2,13 +2,12 @@ package e2e import ( "context" - "testing" - "time" - virtuslabv1alpha1 "github.com/VirtusLab/jenkins-operator/pkg/apis/virtuslab/v1alpha1" + "testing" framework "github.com/operator-framework/operator-sdk/pkg/test" "k8s.io/apimachinery/pkg/types" + "time" ) func TestJenkinsMasterPodRestart(t *testing.T) { @@ -27,8 +26,8 @@ func TestJenkinsMasterPodRestart(t *testing.T) { func checkBaseConfigurationCompleteTimeIsNotSet(t *testing.T, jenkins *virtuslabv1alpha1.Jenkins) { jenkinsStatus := &virtuslabv1alpha1.Jenkins{} - namespacedName := types.NamespacedName{Namespace: jenkins.Namespace, Name: jenkins.Name} - err := framework.Global.Client.Get(context.TODO(), namespacedName, jenkinsStatus) + namespaceName := types.NamespacedName{Namespace: jenkins.Namespace, Name: jenkins.Name} + err := framework.Global.Client.Get(context.TODO(), namespaceName, jenkinsStatus) if err != nil { t.Fatal(err) } diff --git a/test/e2e/user_configuration_test.go b/test/e2e/user_configuration_test.go new file mode 100644 index 00000000..0c7dcc87 --- /dev/null +++ b/test/e2e/user_configuration_test.go @@ -0,0 +1,40 @@ +package e2e + +import ( + "github.com/VirtusLab/jenkins-operator/pkg/controller/jenkins/configuration/user/seedjobs" + "github.com/bndr/gojenkins" + "k8s.io/apimachinery/pkg/util/wait" + "testing" + "time" +) + +func TestUserConfiguration(t *testing.T) { + t.Parallel() + namespace, ctx := setupTest(t) + // Deletes test namespace + defer ctx.Cleanup() + + jenkins := createJenkinsCRWithSeedJob(t, namespace) + waitForJenkinsUserConfigurationToComplete(t, jenkins) + client := verifyJenkinsAPIConnection(t, jenkins) + verifyJenkinsSeedJobs(t, client) +} + +func verifyJenkinsSeedJobs(t *testing.T, client *gojenkins.Jenkins) { + // check if job has been configured and executed successfully + err := wait.Poll(time.Second*10, time.Minute*2, func() (bool, error) { + t.Logf("Attempting to get seed job status '%v'", seedjobs.ConfigureSeedJobsName) + seedJob, err := client.GetJob(seedjobs.ConfigureSeedJobsName) + if err != nil || seedJob == nil { + return false, nil + } + build, err := seedJob.GetLastSuccessfulBuild() + if err != nil || build == nil { + return false, nil + } + return true, nil + }) + if err != nil { + t.Fatalf("couldn't get seed job '%v'", err) + } +} diff --git a/test/e2e/wait.go b/test/e2e/wait.go index 092e4a2d..e7233a4b 100644 --- a/test/e2e/wait.go +++ b/test/e2e/wait.go @@ -15,7 +15,7 @@ import ( var ( retryInterval = time.Second * 5 - timeout = time.Second * 30 + timeout = time.Second * 60 ) // checkConditionFunc is used to check if a condition for the jenkins CR is true @@ -33,6 +33,18 @@ func waitForJenkinsBaseConfigurationToComplete(t *testing.T, jenkins *virtuslabv t.Log("Jenkins pod is running") } +func waitForJenkinsUserConfigurationToComplete(t *testing.T, jenkins *virtuslabv1alpha1.Jenkins) { + t.Log("Waiting for Jenkins user configuration to complete") + _, err := WaitUntilJenkinsConditionTrue(retryInterval, 30, jenkins, func(jenkins *virtuslabv1alpha1.Jenkins) bool { + t.Logf("Current Jenkins status '%+v'", jenkins.Status) + return jenkins.Status.UserConfigurationCompletedTime != nil + }) + if err != nil { + t.Fatal(err) + } + t.Log("Jenkins pod is running") +} + // WaitUntilJenkinsConditionTrue retries until the specified condition check becomes true for the jenkins CR func WaitUntilJenkinsConditionTrue(retryInterval time.Duration, retries int, jenkins *virtuslabv1alpha1.Jenkins, checkCondition checkConditionFunc) (*virtuslabv1alpha1.Jenkins, error) { jenkinsStatus := &virtuslabv1alpha1.Jenkins{}