diff --git a/Makefile b/Makefile index 62ec3441..96a5c462 100644 --- a/Makefile +++ b/Makefile @@ -197,7 +197,7 @@ ifeq ($(KUBERNETES_PROVIDER),minikube) endif endif - @RUNNING_TESTS=1 go test -parallel=1 "./test/e2e/" -tags "$(BUILDTAGS) cgo" -v -timeout 60m -run "$(E2E_TEST_SELECTOR)" \ + RUNNING_TESTS=1 go test -parallel=1 "./test/e2e/" -tags "$(BUILDTAGS) cgo" -v -timeout 60m -run "$(E2E_TEST_SELECTOR)" \ -root=$(CURRENT_DIRECTORY) -kubeconfig=$(HOME)/.kube/config -globalMan deploy/crds/jenkins_$(API_VERSION)_jenkins_crd.yaml \ -namespacedMan deploy/namespace-init.yaml $(TEST_ARGS) @@ -251,6 +251,7 @@ ifeq ($(KUBERNETES_PROVIDER),crc) oc project $(CRC_OC_PROJECT) endif kubectl apply -f deploy/crds/jenkins_$(API_VERSION)_jenkins_crd.yaml + kubectl apply -f deploy/crds/jenkins_$(API_VERSION)_jenkinsimage_crd.yaml @echo "Watching '$(WATCH_NAMESPACE)' namespace" build/_output/bin/jenkins-operator $(OPERATOR_ARGS) diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 320fdb7f..b93e3281 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -7,6 +7,8 @@ import ( "os" "runtime" + "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkinsimage" + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) _ "k8s.io/client-go/plugin/pkg/client/auth" @@ -135,6 +137,10 @@ func main() { if err := jenkins.Add(mgr, jenkinsAPIConnectionSettings, *clientSet, *cfg, &c); err != nil { fatal(errors.Wrap(err, "failed to setup controllers"), *debug) } + // setup JenkinsImage controller + if err = jenkinsimage.Add(mgr); err != nil { + fatal(errors.Wrap(err, "failed to setup controllers"), *debug) + } if err = serveCRMetrics(cfg); err != nil { log.Log.V(log.VWarn).Info("Could not generate and serve custom resource metrics", "error", err.Error()) diff --git a/config.base.env b/config.base.env index f86c6d55..868b2b7a 100644 --- a/config.base.env +++ b/config.base.env @@ -7,6 +7,7 @@ DOCKER_ORGANIZATION=virtuslab DOCKER_REGISTRY=jenkins-operator NAMESPACE=default API_VERSION=v1alpha2 +API_VERSION_NEXT=v1alpha3 ALL_IN_ONE_DEPLOY_FILE_PREFIX=all-in-one GEN_CRD_API=gen-crd-api-reference-docs IMAGE_PULL_MODE=local diff --git a/deploy/crds/jenkins.io_jenkinsimages_crd.yaml b/deploy/crds/jenkins.io_jenkinsimages_crd.yaml new file mode 100644 index 00000000..acf4f081 --- /dev/null +++ b/deploy/crds/jenkins.io_jenkinsimages_crd.yaml @@ -0,0 +1,85 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: jenkinsimages.jenkins.io +spec: + group: jenkins.io + names: + kind: JenkinsImage + listKind: JenkinsImageList + plural: jenkinsimages + singular: jenkinsimage + scope: Namespaced + subresources: + status: {} + validation: + openAPIV3Schema: + description: JenkinsImage is the Schema for the jenkinsimages API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: JenkinsImageSpec defines the desired state of JenkinsImage + properties: + image: + description: Defines Jenkins Plugin structure + properties: + name: + type: string + version: + type: string + required: + - name + type: object + plugins: + items: + description: Defines Jenkins Plugin structure + properties: + name: + type: string + version: + type: string + required: + - name + type: object + type: array + required: + - image + - plugins + type: object + status: + description: JenkinsImageStatus defines the observed state of JenkinsImage + properties: + image: + type: string + installedPlugins: + items: + description: Defines Jenkins Plugin structure + properties: + name: + type: string + version: + type: string + required: + - name + type: object + type: array + md5sum: + type: string + type: object + type: object + version: v1alpha2 + versions: + - name: v1alpha2 + served: true + storage: true diff --git a/deploy/crds/jenkins_v1alpha2_jenkins_crd.yaml b/deploy/crds/jenkins_v1alpha2_jenkins_crd.yaml index b254ecae..8206d650 100644 --- a/deploy/crds/jenkins_v1alpha2_jenkins_crd.yaml +++ b/deploy/crds/jenkins_v1alpha2_jenkins_crd.yaml @@ -17,3 +17,90 @@ spec: - name : v1alpha1 served: true storage: false +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: jenkinsimages.jenkins.io +spec: + group: jenkins.io + names: + kind: JenkinsImage + listKind: JenkinsImageList + plural: jenkinsimages + singular: jenkinsimage + scope: Namespaced + subresources: + status: {} + validation: + openAPIV3Schema: + description: JenkinsImage is the Schema for the jenkinsimages API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: JenkinsImageSpec defines the desired state of JenkinsImage + properties: + image: + description: Defines Jenkins Plugin structure + properties: + name: + type: string + version: + type: string + required: + - name + type: object + plugins: + items: + description: Defines Jenkins Plugin structure + properties: + name: + type: string + version: + type: string + required: + - name + type: object + type: array + required: + - image + - plugins + type: object + status: + description: JenkinsImageStatus defines the observed state of JenkinsImage + properties: + image: + type: string + installedPlugins: + items: + description: Defines Jenkins Plugin structure + properties: + name: + type: string + version: + type: string + required: + - name + type: object + type: array + md5sum: + type: string + type: object + type: object + version: v1alpha2 + versions: + - name: v1alpha2 + served: true + storage: true + diff --git a/deploy/crds/jenkins_v1alpha2_jenkinsimage_cr.yaml b/deploy/crds/jenkins_v1alpha2_jenkinsimage_cr.yaml new file mode 100644 index 00000000..f767a254 --- /dev/null +++ b/deploy/crds/jenkins_v1alpha2_jenkinsimage_cr.yaml @@ -0,0 +1,24 @@ +apiVersion: jenkins.io/v1alpha2 +kind: JenkinsImage +metadata: + name: simple-jenkinsimage +spec: + image: + name: jenkins/jenkins + tag: lts + plugins: + - name: kubernetes + version: "1.15.7" + - name: workflow-job + version: "2.32" + - name: workflow-aggregator + version: "2.6" + - name: git + version: "3.10.0" + - name: job-dsl + version: "1.74" + - name: configuration-as-code + version: "1.19" + - name: kubernetes-credentials-provider + version: "0.12.1" + diff --git a/deploy/crds/jenkins_v1alpha2_jenkinsimage_crd.yaml b/deploy/crds/jenkins_v1alpha2_jenkinsimage_crd.yaml new file mode 100644 index 00000000..acf4f081 --- /dev/null +++ b/deploy/crds/jenkins_v1alpha2_jenkinsimage_crd.yaml @@ -0,0 +1,85 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: jenkinsimages.jenkins.io +spec: + group: jenkins.io + names: + kind: JenkinsImage + listKind: JenkinsImageList + plural: jenkinsimages + singular: jenkinsimage + scope: Namespaced + subresources: + status: {} + validation: + openAPIV3Schema: + description: JenkinsImage is the Schema for the jenkinsimages API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: JenkinsImageSpec defines the desired state of JenkinsImage + properties: + image: + description: Defines Jenkins Plugin structure + properties: + name: + type: string + version: + type: string + required: + - name + type: object + plugins: + items: + description: Defines Jenkins Plugin structure + properties: + name: + type: string + version: + type: string + required: + - name + type: object + type: array + required: + - image + - plugins + type: object + status: + description: JenkinsImageStatus defines the observed state of JenkinsImage + properties: + image: + type: string + installedPlugins: + items: + description: Defines Jenkins Plugin structure + properties: + name: + type: string + version: + type: string + required: + - name + type: object + type: array + md5sum: + type: string + type: object + type: object + version: v1alpha2 + versions: + - name: v1alpha2 + served: true + storage: true diff --git a/pkg/apis/jenkins/v1alpha2/jenkinsimage_types.go b/pkg/apis/jenkins/v1alpha2/jenkinsimage_types.go new file mode 100644 index 00000000..4cd63654 --- /dev/null +++ b/pkg/apis/jenkins/v1alpha2/jenkinsimage_types.go @@ -0,0 +1,58 @@ +package v1alpha2 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// JenkinsImageSpec defines the desired state of JenkinsImage +type JenkinsImageSpec struct { + BaseImage Image `json:"image"` + Plugins []JenkinsPlugin `json:"plugins"` // Plugins list +} + +// Defines Jenkins Plugin structure +type JenkinsPlugin struct { + Name string `json:"name"` + Version string `json:"version,omitempty"` +} + +// Defines Jenkins Plugin structure +type Image struct { + Name string `json:"name"` + Tag string `json:"version,omitempty"` +} + +// JenkinsImageStatus defines the observed state of JenkinsImage +type JenkinsImageStatus struct { + Image string `json:"image,omitempty"` + MD5Sum string `json:"md5sum,omitempty"` + InstalledPlugins []JenkinsPlugin `json:"installedPlugins,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// JenkinsImage is the Schema for the jenkinsimages API +// +kubebuilder:subresource:status +// +kubebuilder:resource:path=jenkinsimages,scope=Namespaced +type JenkinsImage struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec JenkinsImageSpec `json:"spec,omitempty"` + Status JenkinsImageStatus `json:"status,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// JenkinsImageList contains a list of JenkinsImage +type JenkinsImageList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []JenkinsImage `json:"items"` +} + +func init() { + SchemeBuilder.Register(&JenkinsImage{}, &JenkinsImageList{}) +} diff --git a/pkg/apis/jenkins/v1alpha2/register.go b/pkg/apis/jenkins/v1alpha2/register.go index f9268b15..57aa81e4 100644 --- a/pkg/apis/jenkins/v1alpha2/register.go +++ b/pkg/apis/jenkins/v1alpha2/register.go @@ -47,4 +47,5 @@ func JenkinsTypeMeta() metav1.TypeMeta { func init() { SchemeBuilder.Register(&Jenkins{}, &JenkinsList{}) + SchemeBuilder.Register(&JenkinsImage{}, &JenkinsImageList{}) } diff --git a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go index ec7b4a32..4f228623 100644 --- a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go @@ -209,6 +209,22 @@ func (in *Handler) DeepCopy() *Handler { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Image) DeepCopyInto(out *Image) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Image. +func (in *Image) DeepCopy() *Image { + if in == nil { + return nil + } + out := new(Image) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Jenkins) DeepCopyInto(out *Jenkins) { *out = *in @@ -253,6 +269,110 @@ func (in *JenkinsAPISettings) DeepCopy() *JenkinsAPISettings { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JenkinsImage) DeepCopyInto(out *JenkinsImage) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JenkinsImage. +func (in *JenkinsImage) DeepCopy() *JenkinsImage { + if in == nil { + return nil + } + out := new(JenkinsImage) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *JenkinsImage) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JenkinsImageList) DeepCopyInto(out *JenkinsImageList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]JenkinsImage, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JenkinsImageList. +func (in *JenkinsImageList) DeepCopy() *JenkinsImageList { + if in == nil { + return nil + } + out := new(JenkinsImageList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *JenkinsImageList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JenkinsImageSpec) DeepCopyInto(out *JenkinsImageSpec) { + *out = *in + out.BaseImage = in.BaseImage + if in.Plugins != nil { + in, out := &in.Plugins, &out.Plugins + *out = make([]JenkinsPlugin, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JenkinsImageSpec. +func (in *JenkinsImageSpec) DeepCopy() *JenkinsImageSpec { + if in == nil { + return nil + } + out := new(JenkinsImageSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JenkinsImageStatus) DeepCopyInto(out *JenkinsImageStatus) { + *out = *in + if in.InstalledPlugins != nil { + in, out := &in.InstalledPlugins, &out.InstalledPlugins + *out = make([]JenkinsPlugin, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JenkinsImageStatus. +func (in *JenkinsImageStatus) DeepCopy() *JenkinsImageStatus { + if in == nil { + return nil + } + out := new(JenkinsImageStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JenkinsList) DeepCopyInto(out *JenkinsList) { *out = *in @@ -371,6 +491,22 @@ func (in *JenkinsMaster) DeepCopy() *JenkinsMaster { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JenkinsPlugin) DeepCopyInto(out *JenkinsPlugin) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JenkinsPlugin. +func (in *JenkinsPlugin) DeepCopy() *JenkinsPlugin { + if in == nil { + return nil + } + out := new(JenkinsPlugin) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JenkinsSpec) DeepCopyInto(out *JenkinsSpec) { *out = *in diff --git a/pkg/configuration/base/resources/builder.go b/pkg/configuration/base/resources/builder.go new file mode 100644 index 00000000..9ad27de8 --- /dev/null +++ b/pkg/configuration/base/resources/builder.go @@ -0,0 +1,128 @@ +package resources + +import ( + "fmt" + + jenkinsv1alpha2 "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + logf "sigs.k8s.io/controller-runtime/pkg/log" +) + +const ( + NameWithSuffixFormat = "%s-%s" + PluginDefinitionFormat = "%s:%s" + BuilderDockerfileArg = "--dockerfile=/workspace/dockerfile/Dockerfile" + BuilderContextDirArg = "--context=dir://workspace/" + BuilderPushArg = "--no-push" + BuilderDigestFileArg = "--digest-file=/dev/termination-log" + BuilderSuffix = "builder" + DockerfileStorageSuffix = "dockerfile-storage" + DockerfileNameSuffix = "dockerfile" + JenkinsImageBuilderImage = "gcr.io/kaniko-project/executor:latest" + JenkinsImageBuilderName = "jenkins-image-builder" + JenkinsImageDefaultBaseImage = "jenkins/jenkins:lts" + DockerfileName = "Dockerfile" + DockerfileTemplate = `FROM %s +RUN curl -o /tmp/install-plugins.sh https://raw.githubusercontent.com/jenkinsci/docker/master/install-plugins.sh +RUN chmod +x /tmp/install-plugins.sh +RUN install-plugins.sh %s ` +) + +var log = logf.Log.WithName("controller_jenkinsimage") + +// NewBuilderPod returns a busybox pod with the same name/namespace as the cr. +func NewBuilderPod(cr *jenkinsv1alpha2.JenkinsImage) *corev1.Pod { + name := fmt.Sprintf(NameWithSuffixFormat, cr.Name, BuilderSuffix) + args := []string{BuilderDockerfileArg, BuilderContextDirArg, BuilderPushArg, BuilderDigestFileArg} + volumes := getVolumes(cr) + volumeMounts := getVolumesMounts(cr) + p := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: cr.Namespace, + }, + Spec: corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyNever, + Containers: []corev1.Container{ + { + Name: JenkinsImageBuilderName, + Image: JenkinsImageBuilderImage, + Args: args, + VolumeMounts: volumeMounts, + }, + }, + Volumes: volumes, + }, + } + return p +} + +// NewDockerfileConfigMap returns a busybox pod with the same name/namespace as the cr. +func NewDockerfileConfigMap(cr *jenkinsv1alpha2.JenkinsImage) *corev1.ConfigMap { + dockerfileContent := fmt.Sprintf(DockerfileTemplate, getDefaultedBaseImage(cr), getPluginsList(cr)) + name := fmt.Sprintf(NameWithSuffixFormat, cr.Name, DockerfileNameSuffix) + data := map[string]string{DockerfileName: dockerfileContent} + dockerfile := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: cr.Namespace, + }, + Data: data, + } + return dockerfile +} + +func getPluginsList(cr *jenkinsv1alpha2.JenkinsImage) string { + logger := log.WithName("jenkinsimage_getPluginsList") + plugins := "" + for _, v := range cr.Spec.Plugins { + plugins += fmt.Sprintf(PluginDefinitionFormat, v.Name, v.Version) + " " + logger.Info(fmt.Sprintf("Adding plugin %s:%s ", v.Name, v.Version)) + } + return plugins +} + +func getDefaultedBaseImage(cr *jenkinsv1alpha2.JenkinsImage) string { + if len(cr.Spec.BaseImage.Name) != 0 { + return cr.Spec.BaseImage.Name + } + return JenkinsImageDefaultBaseImage +} + +func getVolumes(cr *jenkinsv1alpha2.JenkinsImage) []corev1.Volume { + name := fmt.Sprintf(NameWithSuffixFormat, cr.Name, DockerfileStorageSuffix) + storage := corev1.Volume{ + Name: name, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + } + + name = fmt.Sprintf(NameWithSuffixFormat, cr.Name, DockerfileNameSuffix) + config := corev1.Volume{ + Name: name, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: name}, + }, + }, + } + volumes := []corev1.Volume{storage, config} + return volumes +} + +func getVolumesMounts(cr *jenkinsv1alpha2.JenkinsImage) []corev1.VolumeMount { + name := fmt.Sprintf(NameWithSuffixFormat, cr.Name, DockerfileStorageSuffix) + storage := corev1.VolumeMount{ + Name: name, + MountPath: "/workspace", + } + name = fmt.Sprintf(NameWithSuffixFormat, cr.Name, DockerfileNameSuffix) + config := corev1.VolumeMount{ + Name: name, + MountPath: "/workspace/dockerfile", + } + volumeMounts := []corev1.VolumeMount{storage, config} + return volumeMounts +} diff --git a/pkg/controller/jenkinsimage/jenkinsimage_controller.go b/pkg/controller/jenkinsimage/jenkinsimage_controller.go new file mode 100644 index 00000000..92d81c25 --- /dev/null +++ b/pkg/controller/jenkinsimage/jenkinsimage_controller.go @@ -0,0 +1,150 @@ +package jenkinsimage + +import ( + "context" + + jenkinsv1alpha2 "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" + + "github.com/jenkinsci/kubernetes-operator/pkg/configuration/base/resources" + + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/handler" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" +) + +var log = logf.Log.WithName("controller_jenkinsimage") + +// Add creates a new JenkinsImage Controller and adds it to the Manager. The Manager will set fields on the Controller +// and Start it when the Manager is Started. +func Add(mgr manager.Manager) error { + r := &ReconcileJenkinsImage{client: mgr.GetClient(), scheme: mgr.GetScheme()} + return add(mgr, r) +} + +// add adds a new Controller to mgr with r as the reconcile.Reconciler +func add(mgr manager.Manager, r reconcile.Reconciler) error { + // Create a new controller + c, err := controller.New("jenkinsimage-controller", mgr, controller.Options{Reconciler: r}) + if err != nil { + return err + } + + // Watch for changes to primary resource JenkinsImage + eventHandlerForObject := &handler.EnqueueRequestForObject{} + src := &source.Kind{Type: &jenkinsv1alpha2.JenkinsImage{}} + err = c.Watch(src, eventHandlerForObject) + if err != nil { + return errors.WithStack(err) + } + + // Watch for changes to secondary resource Pods and requeue the owner JenkinsImage + eventHandlerForOwner := &handler.EnqueueRequestForOwner{ + IsController: true, + OwnerType: &jenkinsv1alpha2.JenkinsImage{}, + } + err = c.Watch(&source.Kind{Type: &corev1.Pod{}}, eventHandlerForOwner) + if err != nil { + return errors.WithStack(err) + } + // Watch for changes to secondary ConfigMap and requeue the owner JenkinsImage + err = c.Watch(&source.Kind{Type: &corev1.ConfigMap{}}, eventHandlerForOwner) + if err != nil { + return errors.WithStack(err) + } + return nil +} + +// blank assignment to verify that ReconcileJenkinsImage implements reconcile.Reconciler +var _ reconcile.Reconciler = &ReconcileJenkinsImage{} + +// ReconcileJenkinsImage reconciles a JenkinsImage object +type ReconcileJenkinsImage struct { + // This client, initialized using mgr.Client() above, is a split client + // that reads objects from the cache and writes to the apiserver + client client.Client + scheme *runtime.Scheme +} + +// Reconcile reads that state of the cluster for a JenkinsImage object and makes changes based on the state read +// and what is in the JenkinsImage.Spec +// The Controller will requeue the Request to be processed again if the returned error is non-nil or +// Result.Requeue is true, otherwise upon completion it will remove the work from the queue. +func (r *ReconcileJenkinsImage) Reconcile(request reconcile.Request) (reconcile.Result, error) { + reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) + reqLogger.Info("Reconciling JenkinsImage") + + // Fetch the JenkinsImage instance + instance := &jenkinsv1alpha2.JenkinsImage{} + err := r.client.Get(context.TODO(), request.NamespacedName, instance) + if err != nil { + if apierrors.IsNotFound(err) { + // Request object not found, could have been deleted after reconcile request. + // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. + // Return and don't requeue + return reconcile.Result{}, nil + } + // Error reading the object - requeue the request. + return reconcile.Result{}, err + } + + // Define a new ConfigMap containing the Dockerfile used to build the image + dockerfile := resources.NewDockerfileConfigMap(instance) + // Set JenkinsImage instance as the owner and controller + if err := controllerutil.SetControllerReference(instance, dockerfile, r.scheme); err != nil { + return reconcile.Result{}, err + } + + // Check if this ConfigMap already exists + foundConfigMap := &corev1.ConfigMap{} + err = r.client.Get(context.TODO(), types.NamespacedName{Name: dockerfile.Name, Namespace: dockerfile.Namespace}, foundConfigMap) + if err != nil && apierrors.IsNotFound(err) { + reqLogger.Info("Creating a new ConfigMap", "ConfigMap.Namespace", dockerfile.Namespace, "ConfigMap.Name", dockerfile.Name) + err = r.client.Create(context.TODO(), dockerfile) + if err != nil { + return reconcile.Result{}, err + } + // ConfigMap created successfully - don't requeue + return reconcile.Result{}, nil + } else if err != nil { + return reconcile.Result{}, err + } + // ConfigMap already exists - don't requeue + reqLogger.Info("Skip reconcile: ConfigMap already exists", "ConfigMap.Namespace", foundConfigMap.Namespace, "ConfigMap.Name", foundConfigMap.Name) + + // Define a new Pod object + pod := resources.NewBuilderPod(instance) + // Set JenkinsImage instance as the owner and controller + if err := controllerutil.SetControllerReference(instance, pod, r.scheme); err != nil { + return reconcile.Result{}, err + } + + // Check if this Pod already exists + foundPod := &corev1.Pod{} + err = r.client.Get(context.TODO(), types.NamespacedName{Name: pod.Name, Namespace: pod.Namespace}, foundPod) + if err != nil && apierrors.IsNotFound(err) { + reqLogger.Info("Creating a new Pod", "Pod.Namespace", pod.Namespace, "Pod.Name", pod.Name) + err = r.client.Create(context.TODO(), pod) + if err != nil { + return reconcile.Result{}, err + } + + // Pod created successfully - don't requeue + return reconcile.Result{}, nil + } else if err != nil { + return reconcile.Result{}, err + } + // Pod already exists - don't requeue + reqLogger.Info("Skip reconcile: Pod already exists", "Pod.Namespace", foundPod.Namespace, "Pod.Name", foundPod.Name) + + return reconcile.Result{}, nil +}