diff --git a/pkg/apis/jenkinsio/v1alpha1/jenkins_types.go b/pkg/apis/jenkinsio/v1alpha1/jenkins_types.go index 00ba8b95..30483cef 100644 --- a/pkg/apis/jenkinsio/v1alpha1/jenkins_types.go +++ b/pkg/apis/jenkinsio/v1alpha1/jenkins_types.go @@ -12,8 +12,10 @@ 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"` - SeedJobs []SeedJob `json:"seedJobs,omitempty"` + Master JenkinsMaster `json:"master,omitempty"` + SeedJobs []SeedJob `json:"seedJobs,omitempty"` + Service Service `json:"service,omitempty"` + SlaveService Service `json:"slaveService,omitempty"` } // JenkinsMaster defines the Jenkins master pod attributes and plugins, @@ -29,6 +31,17 @@ type JenkinsMaster struct { Plugins map[string][]string `json:"plugins,omitempty"` } +// Service defines Kubernetes service attributes which Operator will manage +type Service struct { + Annotations map[string]string `json:"annotations,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + Type corev1.ServiceType `json:"type,omitempty"` + Port int32 `json:"port,omitempty"` + NodePort int32 `json:"nodePort,omitempty"` + LoadBalancerSourceRanges []string `json:"loadBalancerSourceRanges,omitempty"` + LoadBalancerIP string `json:"loadBalancerIP,omitempty"` +} + // JenkinsStatus defines the observed state of Jenkins type JenkinsStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster diff --git a/pkg/apis/jenkinsio/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/jenkinsio/v1alpha1/zz_generated.deepcopy.go index 669e6d3d..16681b77 100644 --- a/pkg/apis/jenkinsio/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/jenkinsio/v1alpha1/zz_generated.deepcopy.go @@ -182,6 +182,8 @@ func (in *JenkinsSpec) DeepCopyInto(out *JenkinsSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + in.Service.DeepCopyInto(&out.Service) + in.SlaveService.DeepCopyInto(&out.SlaveService) return } @@ -267,3 +269,38 @@ func (in *SeedJob) DeepCopy() *SeedJob { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Service) DeepCopyInto(out *Service) { + *out = *in + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.LoadBalancerSourceRanges != nil { + in, out := &in.LoadBalancerSourceRanges, &out.LoadBalancerSourceRanges + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Service. +func (in *Service) DeepCopy() *Service { + if in == nil { + return nil + } + out := new(Service) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/controller/jenkins/client/jenkins.go b/pkg/controller/jenkins/client/jenkins.go index 667c1505..fde87464 100644 --- a/pkg/controller/jenkins/client/jenkins.go +++ b/pkg/controller/jenkins/client/jenkins.go @@ -75,7 +75,7 @@ func (jenkins *jenkins) CreateOrUpdateJob(config, jobName string) (job *gojenkin } // BuildJenkinsAPIUrl returns Jenkins API URL -func BuildJenkinsAPIUrl(namespace, serviceName string, portNumber int, local, minikube bool) (string, error) { +func BuildJenkinsAPIUrl(namespace, serviceName string, portNumber int32, local, minikube bool) (string, error) { // Get Jenkins URL from minikube command if local && minikube { cmd := exec.Command("minikube", "service", "--url", "-n", namespace, serviceName) diff --git a/pkg/controller/jenkins/configuration/base/reconcile.go b/pkg/controller/jenkins/configuration/base/reconcile.go index a5809eb7..28d8e8ba 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile.go +++ b/pkg/controller/jenkins/configuration/base/reconcile.go @@ -136,10 +136,14 @@ func (r *ReconcileJenkinsBaseConfiguration) ensureResourcesRequiredForJenkinsPod } r.logger.V(log.VDebug).Info("Service account, role and role binding are present") - if err := r.createService(metaObject); err != nil { + if err := r.createService(metaObject, resources.GetJenkinsHTTPServiceName(r.jenkins), r.jenkins.Spec.Service); err != nil { return err } - r.logger.V(log.VDebug).Info("Service is present") + r.logger.V(log.VDebug).Info("Jenkins HTTP Service is present") + if err := r.createService(metaObject, resources.GetJenkinsSlavesServiceName(r.jenkins), r.jenkins.Spec.SlaveService); err != nil { + return err + } + r.logger.V(log.VDebug).Info("Jenkins slave Service is present") return nil } @@ -296,13 +300,29 @@ func (r *ReconcileJenkinsBaseConfiguration) createRBAC(meta metav1.ObjectMeta) e return nil } -func (r *ReconcileJenkinsBaseConfiguration) createService(meta metav1.ObjectMeta) error { - err := r.createResource(resources.NewService(meta, r.minikube)) - if err != nil && !apierrors.IsAlreadyExists(err) { +func (r *ReconcileJenkinsBaseConfiguration) createService(meta metav1.ObjectMeta, name string, config v1alpha1.Service) error { + service := corev1.Service{} + err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: meta.Namespace}, &service) + if err != nil && errors.IsNotFound(err) { + service = resources.UpdateService(corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: meta.Namespace, + Labels: meta.Labels, + }, + Spec: corev1.ServiceSpec{ + Selector: meta.Labels, + }, + }, config) + if err = r.createResource(&service); err != nil { + return stackerr.WithStack(err) + } + } else if err != nil { return stackerr.WithStack(err) } - return nil + service = resources.UpdateService(service, config) + return stackerr.WithStack(r.updateResource(&service)) } func (r *ReconcileJenkinsBaseConfiguration) getJenkinsMasterPod(meta metav1.ObjectMeta) (*corev1.Pod, error) { @@ -425,7 +445,7 @@ func (r *ReconcileJenkinsBaseConfiguration) waitForJenkins(meta metav1.ObjectMet func (r *ReconcileJenkinsBaseConfiguration) ensureJenkinsClient(meta metav1.ObjectMeta) (jenkinsclient.Jenkins, error) { jenkinsURL, err := jenkinsclient.BuildJenkinsAPIUrl( - r.jenkins.ObjectMeta.Namespace, meta.Name, resources.HTTPPortInt, r.local, r.minikube) + r.jenkins.ObjectMeta.Namespace, resources.GetJenkinsHTTPServiceName(r.jenkins), r.jenkins.Spec.Service.Port, r.local, r.minikube) if err != nil { return nil, err } 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 a0b128e1..6064ddae 100644 --- a/pkg/controller/jenkins/configuration/base/resources/base_configuration_configmap.go +++ b/pkg/controller/jenkins/configuration/base/resources/base_configuration_configmap.go @@ -115,13 +115,21 @@ ServiceAccountCredential serviceAccountCredential = new ServiceAccountCredential ) SystemCredentialsProvider.getInstance().getStore().addCredentials(Domain.global(), serviceAccountCredential) -KubernetesCloud kubernetes = new KubernetesCloud("kubernetes") +def kubernetes = Jenkins.instance.clouds.getByName("kubernetes") +def add = false +if (kubernetes == null) { + add = true + kubernetes = new KubernetesCloud("kubernetes") +} kubernetes.setServerUrl("https://kubernetes.default") kubernetes.setNamespace("%s") kubernetes.setCredentialsId(kubernetesCredentialsId) -kubernetes.setJenkinsUrl("http://%s:%d") +kubernetes.setJenkinsUrl("%s") +kubernetes.setJenkinsTunnel("%s") kubernetes.setRetentionTimeout(15) -jenkins.clouds.add(kubernetes) +if (add) { + jenkins.clouds.add(kubernetes) +} jenkins.save() ` @@ -176,7 +184,10 @@ func NewBaseConfigurationConfigMap(meta metav1.ObjectMeta, jenkins *v1alpha1.Jen "4-enable-master-access-control.groovy": enableMasterAccessControl, "5-disable-insecure-features.groovy": disableInsecureFeatures, "6-configure-kubernetes-plugin.groovy": fmt.Sprintf(configureKubernetesPluginFmt, - jenkins.ObjectMeta.Namespace, GetResourceName(jenkins), HTTPPortInt), + jenkins.ObjectMeta.Namespace, + fmt.Sprintf("http://%s.%s:%d", GetJenkinsHTTPServiceName(jenkins), jenkins.ObjectMeta.Namespace, jenkins.Spec.Service.Port), + fmt.Sprintf("%s.%s:%d", GetJenkinsSlavesServiceName(jenkins), jenkins.ObjectMeta.Namespace, jenkins.Spec.SlaveService.Port), + ), "7-configure-views.groovy": configureViews, }, } diff --git a/pkg/controller/jenkins/configuration/base/resources/pod.go b/pkg/controller/jenkins/configuration/base/resources/pod.go index 26bc9494..583c2325 100644 --- a/pkg/controller/jenkins/configuration/base/resources/pod.go +++ b/pkg/controller/jenkins/configuration/base/resources/pod.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkinsio/v1alpha1" + "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -42,10 +43,7 @@ const ( httpPortName = "http" slavePortName = "slavelistener" // HTTPPortInt defines Jenkins master HTTP port - HTTPPortInt = 8080 - slavePortInt = 50000 - httpPortInt32 = int32(8080) - slavePortInt32 = int32(50000) + HTTPPortInt = 8080 jenkinsUserUID = int64(1000) // build in Docker image jenkins user UID ) @@ -109,12 +107,12 @@ func NewJenkinsMasterPod(objectMeta metav1.ObjectMeta, jenkins *v1alpha1.Jenkins }, Ports: []corev1.ContainerPort{ { - Name: slavePortName, - ContainerPort: slavePortInt32, + Name: httpPortName, + ContainerPort: constants.DefaultHTTPPortInt32, }, { - Name: httpPortName, - ContainerPort: httpPortInt32, + Name: slavePortName, + ContainerPort: constants.DefaultSlavePortInt32, }, }, Env: []corev1.EnvVar{ diff --git a/pkg/controller/jenkins/configuration/base/resources/service.go b/pkg/controller/jenkins/configuration/base/resources/service.go index e37981e3..e2c566b2 100644 --- a/pkg/controller/jenkins/configuration/base/resources/service.go +++ b/pkg/controller/jenkins/configuration/base/resources/service.go @@ -1,9 +1,13 @@ package resources import ( + "fmt" + + "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkinsio/v1alpha1" + "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" ) func buildServiceTypeMeta() metav1.TypeMeta { @@ -13,37 +17,32 @@ func buildServiceTypeMeta() metav1.TypeMeta { } } -// NewService builds the Kubernetes service resource -func NewService(meta metav1.ObjectMeta, minikube bool) *corev1.Service { - service := &corev1.Service{ - TypeMeta: buildServiceTypeMeta(), - ObjectMeta: meta, - Spec: corev1.ServiceSpec{ - Selector: meta.Labels, - // The first port have to be Jenkins http port because when run with minikube - // command 'minikube service' returns endpoints in the same sequence - Ports: []corev1.ServicePort{ - { - Name: httpPortName, - Port: httpPortInt32, - TargetPort: intstr.FromInt(HTTPPortInt), - }, - { - Name: slavePortName, - Port: slavePortInt32, - TargetPort: intstr.FromInt(slavePortInt), - }, - }, - }, +// UpdateService returns new service with override fields from config +func UpdateService(actual corev1.Service, config v1alpha1.Service) corev1.Service { + actual.ObjectMeta.Annotations = config.Annotations + for key, value := range config.Labels { + actual.ObjectMeta.Labels[key] = value + } + actual.Spec.Type = config.Type + actual.Spec.LoadBalancerIP = config.LoadBalancerIP + actual.Spec.LoadBalancerSourceRanges = config.LoadBalancerSourceRanges + if len(actual.Spec.Ports) == 0 { + actual.Spec.Ports = []corev1.ServicePort{{}} + } + actual.Spec.Ports[0].Port = config.Port + if config.NodePort != 0 { + actual.Spec.Ports[0].NodePort = config.NodePort } - if minikube { - // When running locally with minikube cluster Jenkins Service have to be exposed via node port - // to allow communication operator -> Jenkins API - service.Spec.Type = corev1.ServiceTypeNodePort - } else { - service.Spec.Type = corev1.ServiceTypeClusterIP - } - - return service + return actual +} + +// GetJenkinsHTTPServiceName returns Kubernetes service name used for expose Jenkins HTTP endpoint +func GetJenkinsHTTPServiceName(jenkins *v1alpha1.Jenkins) string { + return fmt.Sprintf("%s-http-%s", constants.OperatorName, jenkins.ObjectMeta.Name) +} + +// GetJenkinsSlavesServiceName returns Kubernetes service name used for expose Jenkins slave endpoint +func GetJenkinsSlavesServiceName(jenkins *v1alpha1.Jenkins) string { + return fmt.Sprintf("%s-slave-%s", constants.OperatorName, jenkins.ObjectMeta.Name) } diff --git a/pkg/controller/jenkins/constants/constants.go b/pkg/controller/jenkins/constants/constants.go index 175c44ea..fbf5434b 100644 --- a/pkg/controller/jenkins/constants/constants.go +++ b/pkg/controller/jenkins/constants/constants.go @@ -13,4 +13,8 @@ const ( UserConfigurationJobName = OperatorName + "-user-configuration" // UserConfigurationCASCJobName is the Jenkins job name used to configure Jenkins by Configuration as code yaml configs provided by user UserConfigurationCASCJobName = OperatorName + "-user-configuration-casc" + // DefaultHTTPPortInt32 is the default Jenkins HTTP port + DefaultHTTPPortInt32 = int32(8080) + // DefaultSlavePortInt32 is the default Jenkins port for slaves + DefaultSlavePortInt32 = int32(50000) ) diff --git a/pkg/controller/jenkins/jenkins_controller.go b/pkg/controller/jenkins/jenkins_controller.go index 6c146440..cb22439f 100644 --- a/pkg/controller/jenkins/jenkins_controller.go +++ b/pkg/controller/jenkins/jenkins_controller.go @@ -3,6 +3,7 @@ package jenkins import ( "context" "fmt" + "reflect" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkinsio/v1alpha1" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base" @@ -246,6 +247,30 @@ func (r *ReconcileJenkins) setDefaults(jenkins *v1alpha1.Jenkins, logger logr.Lo }, } } + if reflect.DeepEqual(jenkins.Spec.Service, v1alpha1.Service{}) { + logger.Info("Setting default Jenkins master service") + changed = true + var serviceType corev1.ServiceType + if r.minikube { + // When running locally with minikube cluster Jenkins Service have to be exposed via node port + // to allow communication operator -> Jenkins API + serviceType = corev1.ServiceTypeNodePort + } else { + serviceType = corev1.ServiceTypeClusterIP + } + jenkins.Spec.Service = v1alpha1.Service{ + Type: serviceType, + Port: constants.DefaultHTTPPortInt32, + } + } + if reflect.DeepEqual(jenkins.Spec.SlaveService, v1alpha1.Service{}) { + logger.Info("Setting default Jenkins slave service") + changed = true + jenkins.Spec.SlaveService = v1alpha1.Service{ + Type: corev1.ServiceTypeClusterIP, + Port: constants.DefaultSlavePortInt32, + } + } if changed { return errors.WithStack(r.client.Update(context.TODO(), jenkins)) diff --git a/test/e2e/jenkins.go b/test/e2e/jenkins.go index 05046045..a43ccb80 100644 --- a/test/e2e/jenkins.go +++ b/test/e2e/jenkins.go @@ -46,7 +46,7 @@ func createJenkinsAPIClient(jenkins *v1alpha1.Jenkins) (jenkinsclient.Jenkins, e return nil, err } - jenkinsAPIURL, err := jenkinsclient.BuildJenkinsAPIUrl(jenkins.ObjectMeta.Namespace, resources.GetResourceName(jenkins), resources.HTTPPortInt, true, true) + jenkinsAPIURL, err := jenkinsclient.BuildJenkinsAPIUrl(jenkins.ObjectMeta.Namespace, resources.GetJenkinsHTTPServiceName(jenkins), resources.HTTPPortInt, true, true) if err != nil { return nil, err } diff --git a/test/e2e/wait.go b/test/e2e/wait.go index b79564e5..40299ecb 100644 --- a/test/e2e/wait.go +++ b/test/e2e/wait.go @@ -14,6 +14,7 @@ import ( framework "github.com/operator-framework/operator-sdk/pkg/test" "github.com/pkg/errors" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -35,10 +36,13 @@ func waitForJenkinsBaseConfigurationToComplete(t *testing.T, jenkins *v1alpha1.J t.Logf("Current Jenkins status '%+v'", jenkins.Status) return jenkins.Status.BaseConfigurationCompletedTime != nil }) - if err != nil { - t.Fatal(err) - } + assert.NoError(t, err) t.Log("Jenkins pod is running") + + // update jenkins CR because Operator sets default values + namespacedName := types.NamespacedName{Namespace: jenkins.Namespace, Name: jenkins.Name} + err = framework.Global.Client.Get(goctx.TODO(), namespacedName, jenkins) + assert.NoError(t, err) } func waitForRecreateJenkinsMasterPod(t *testing.T, jenkins *v1alpha1.Jenkins) {