Add reconcile loop of Jenkins base configuration

This commit is contained in:
Tomasz Sęk
2018-12-06 09:29:38 +01:00
parent 14950293c5
commit 54aa39c502
37 changed files with 1769 additions and 137 deletions
+3 -3
View File
@@ -5,12 +5,12 @@ import (
)
// AddToManagerFuncs is a list of functions to add all Controllers to the Manager
var AddToManagerFuncs []func(manager.Manager) error
var AddToManagerFuncs []func(manager manager.Manager, local, minikube bool) error
// AddToManager adds all Controllers to the Manager
func AddToManager(m manager.Manager) error {
func AddToManager(m manager.Manager, local, minikube bool) error {
for _, f := range AddToManagerFuncs {
if err := f(m); err != nil {
if err := f(m, local, minikube); err != nil {
return err
}
}
+100
View File
@@ -0,0 +1,100 @@
package client
import (
"bytes"
"fmt"
"net/http"
"os/exec"
"strings"
"github.com/bndr/gojenkins"
)
// Jenkins defines Jenkins API
type Jenkins interface {
GenerateToken(userName, tokenName string) (*UserToken, error)
//Info() (*gojenkins.executorResponse, error)
CreateNode(name string, numExecutors int, description string, remoteFS string, options ...interface{}) (*gojenkins.Node, error)
CreateJob(config string, options ...interface{}) (*gojenkins.Job, error)
RenameJob(job string, name string) *gojenkins.Job
CopyJob(copyFrom string, newName string) (*gojenkins.Job, error)
DeleteJob(name string) (bool, error)
BuildJob(name string, options ...interface{}) (bool, error)
GetNode(name string) (*gojenkins.Node, error)
GetBuild(jobName string, number int64) (*gojenkins.Build, error)
GetJob(id string) (*gojenkins.Job, error)
GetAllNodes() ([]*gojenkins.Node, error)
//GetAllBuildIds(job string) ([]jobBuild, error)
//GetAllJobNames() ([]job, error)
GetAllJobs() ([]*gojenkins.Job, error)
GetQueue() (*gojenkins.Queue, error)
GetQueueUrl() string
//GetArtifactData(id string) (*fingerPrintResponse, error)
GetPlugins(depth int) (*gojenkins.Plugins, error)
HasPlugin(name string) (*gojenkins.Plugin, error)
ValidateFingerPrint(id string) (bool, error)
GetView(name string) (*gojenkins.View, error)
GetAllViews() ([]*gojenkins.View, error)
CreateView(name string, viewType string) (*gojenkins.View, error)
}
type jenkins struct {
gojenkins.Jenkins
}
// BuildJenkinsAPIUrl returns Jenkins API URL
func BuildJenkinsAPIUrl(namespace, serviceName string, portNumber int, local, minikube bool) (string, error) {
// Get Jenkins URL from minikube command
if local && minikube {
cmd := exec.Command("minikube", "service", "--url", "-n", namespace, serviceName)
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return "", err
}
lines := strings.Split(out.String(), "\n")
// First is for http, the second one is for Jenkins slaves communication
// see pkg/controller/jenkins/configuration/base/resources/service.go
url := lines[0]
return url, nil
}
if local {
// When run locally make port-forward to jenkins pod ('kubectl -n default port-forward jenkins-operator-example 8080')
return fmt.Sprintf("http://localhost:%d", portNumber), nil
}
// Connect through Kubernetes service, operator has to be run inside cluster
return fmt.Sprintf("http://%s:%d", serviceName, portNumber), nil
}
// New creates Jenkins API client
func New(url, user, passwordOrToken string) (Jenkins, error) {
if strings.HasSuffix(url, "/") {
url = url[:len(url)-1]
}
jenkinsClient := &jenkins{}
jenkinsClient.Server = url
jenkinsClient.Requester = &gojenkins.Requester{
Base: url,
SslVerify: true,
Client: http.DefaultClient,
BasicAuth: &gojenkins.BasicAuth{Username: user, Password: passwordOrToken},
Headers: http.Header{},
}
if _, err := jenkinsClient.Init(); err != nil {
return nil, err
}
status, err := jenkinsClient.Poll()
if err != nil {
return nil, err
}
if status != http.StatusOK {
return nil, fmt.Errorf("Invalid status code returned: %d", status)
}
return jenkinsClient, nil
}
+52
View File
@@ -0,0 +1,52 @@
package client
import (
"errors"
"fmt"
"net/http"
"strconv"
)
type userTokenResponseData struct {
Name string `json:"tokenName"`
UUID string `json:"tokenUuid"`
Value string `json:"tokenValue"`
}
type userTokenResponse struct {
Status string `json:"status"`
Data userTokenResponseData `json:"data"`
}
// UserToken defines user token for Jenkins API communication
type UserToken struct {
raw *userTokenResponse
base string
}
// GetToken returns user token
func (token *UserToken) GetToken() string {
return token.raw.Data.Value
}
func (jenkins *jenkins) GenerateToken(userName, tokenName string) (*UserToken, error) {
token := &UserToken{raw: new(userTokenResponse),
base: fmt.Sprintf("/user/%s/descriptorByName/jenkins.security.ApiTokenProperty/generateNewToken", userName)}
endpoint := token.base
data := map[string]string{"newTokenName": tokenName}
r, err := jenkins.Requester.Post(endpoint, nil, token.raw, data)
if err != nil {
return nil, err
}
if r.StatusCode == http.StatusOK {
if token.raw.Status == "ok" {
return token, nil
}
return nil, errors.New(token.raw.Status)
}
return nil, errors.New(strconv.Itoa(r.StatusCode))
}
@@ -0,0 +1,302 @@
package base
import (
"context"
"fmt"
"reflect"
"time"
virtuslabv1alpha1 "github.com/VirtusLab/jenkins-operator/pkg/apis/virtuslab/v1alpha1"
jenkinsclient "github.com/VirtusLab/jenkins-operator/pkg/controller/jenkins/client"
"github.com/VirtusLab/jenkins-operator/pkg/controller/jenkins/configuration/base/resources"
"github.com/VirtusLab/jenkins-operator/pkg/log"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
// ReconcileJenkinsBaseConfiguration defines values required for Jenkins base configuration
type ReconcileJenkinsBaseConfiguration struct {
client client.Client
scheme *runtime.Scheme
logger logr.Logger
jenkins *virtuslabv1alpha1.Jenkins
local, minikube bool
}
// New create structure which takes care of base configuration
func New(client client.Client, scheme *runtime.Scheme, logger logr.Logger,
jenkins *virtuslabv1alpha1.Jenkins, local, minikube bool) *ReconcileJenkinsBaseConfiguration {
return &ReconcileJenkinsBaseConfiguration{
client: client,
scheme: scheme,
logger: logger,
jenkins: jenkins,
local: local,
minikube: minikube,
}
}
// Reconcile takes care of base configuration
func (r *ReconcileJenkinsBaseConfiguration) Reconcile() (*reconcile.Result, error) {
if !r.validate(r.jenkins) {
r.logger.V(log.VWarn).Info("Please correct Jenkins CR")
return &reconcile.Result{}, nil
}
metaObject := resources.NewResourceObjectMeta(r.jenkins)
if err := r.createOperatorCredentialsSecret(metaObject); err != nil {
return &reconcile.Result{}, err
}
r.logger.V(log.VDebug).Info("Operator credentials secret is present")
if err := r.createScriptsConfigMap(metaObject); err != nil {
return &reconcile.Result{}, err
}
r.logger.V(log.VDebug).Info("Scripts config map is present")
if err := r.createBaseConfigurationConfigMap(metaObject); err != nil {
return &reconcile.Result{}, err
}
r.logger.V(log.VDebug).Info("Base configuration config map is present")
if err := r.createService(metaObject); err != nil {
return &reconcile.Result{}, err
}
r.logger.V(log.VDebug).Info("Service is present")
result, err := r.createJenkinsMasterPod(metaObject)
if err != nil {
return &reconcile.Result{}, err
}
if result != nil {
return result, nil
}
r.logger.V(log.VDebug).Info("Jenkins master pod is present")
result, err = r.waitForJenkins(metaObject)
if err != nil {
return &reconcile.Result{}, err
}
if result != nil {
return result, nil
}
r.logger.V(log.VDebug).Info("Jenkins master pod is ready")
_, err = r.getJenkinsClient(metaObject)
if err != nil {
return &reconcile.Result{}, err
}
r.logger.V(log.VDebug).Info("Jenkins API client set")
return nil, nil
}
func (r *ReconcileJenkinsBaseConfiguration) createOperatorCredentialsSecret(meta metav1.ObjectMeta) error {
found := &corev1.Secret{}
err := r.client.Get(context.TODO(), types.NamespacedName{Name: resources.GetOperatorCredentialsSecretName(r.jenkins), Namespace: r.jenkins.ObjectMeta.Namespace}, found)
if err != nil && apierrors.IsNotFound(err) {
return r.createResource(resources.NewOperatorCredentialsSecret(meta, r.jenkins))
} else if err != nil && !apierrors.IsNotFound(err) {
return err
}
if found.Data[resources.OperatorCredentialsSecretUserNameKey] != nil &&
found.Data[resources.OperatorCredentialsSecretPasswordKey] != nil {
return nil
}
return r.updateResource(resources.NewOperatorCredentialsSecret(meta, r.jenkins))
}
func (r *ReconcileJenkinsBaseConfiguration) createScriptsConfigMap(meta metav1.ObjectMeta) error {
scripts, err := resources.NewScriptsConfigMap(meta, r.jenkins)
if err != nil {
return err
}
return r.createOrUpdateResource(scripts)
}
func (r *ReconcileJenkinsBaseConfiguration) createBaseConfigurationConfigMap(meta metav1.ObjectMeta) error {
scripts, err := resources.NewBaseConfigurationConfigMap(meta, r.jenkins)
if err != nil {
return err
}
return r.createOrUpdateResource(scripts)
}
func (r *ReconcileJenkinsBaseConfiguration) createService(meta metav1.ObjectMeta) error {
err := r.createResource(resources.NewService(&meta, r.minikube))
if err != nil && !apierrors.IsAlreadyExists(err) {
return err
}
return nil
}
func (r *ReconcileJenkinsBaseConfiguration) getJenkinsMasterPod(meta metav1.ObjectMeta) (*corev1.Pod, error) {
jenkinsMasterPod := resources.NewJenkinsMasterPod(meta, r.jenkins)
currentJenkinsMasterPod := &corev1.Pod{}
err := r.client.Get(context.TODO(), types.NamespacedName{Name: jenkinsMasterPod.Name, Namespace: jenkinsMasterPod.Namespace}, currentJenkinsMasterPod)
if err != nil {
return nil, err
}
return currentJenkinsMasterPod, nil
}
func (r *ReconcileJenkinsBaseConfiguration) createJenkinsMasterPod(meta metav1.ObjectMeta) (*reconcile.Result, error) {
// Check if this Pod already exists
currentJenkinsMasterPod, err := r.getJenkinsMasterPod(meta)
if err != nil && errors.IsNotFound(err) {
jenkinsMasterPod := resources.NewJenkinsMasterPod(meta, r.jenkins)
r.logger.Info(fmt.Sprintf("Creating a new Jenkins Master Pod %s/%s", jenkinsMasterPod.Namespace, jenkinsMasterPod.Name))
err = r.createResource(jenkinsMasterPod)
if err != nil {
return nil, err
}
if r.jenkins.Status.BaseConfigurationCompletedTime != nil {
r.jenkins.Status.BaseConfigurationCompletedTime = nil
err = r.updateResource(r.jenkins)
if err != nil {
return nil, err
}
}
return nil, nil
} else if err != nil && !errors.IsNotFound(err) {
return nil, err
}
// Recreate pod
recreatePod := false
if currentJenkinsMasterPod != nil &&
(currentJenkinsMasterPod.Status.Phase == corev1.PodFailed ||
currentJenkinsMasterPod.Status.Phase == corev1.PodSucceeded ||
currentJenkinsMasterPod.Status.Phase == corev1.PodUnknown) {
r.logger.Info(fmt.Sprintf("Invalid Jenkins pod phase %v, recreating pod", currentJenkinsMasterPod.Status.Phase))
recreatePod = true
}
if currentJenkinsMasterPod != nil &&
r.jenkins.Spec.Master.Image != currentJenkinsMasterPod.Spec.Containers[0].Image {
r.logger.Info(fmt.Sprintf("Jenkins image has changed to %v, recreating pod", r.jenkins.Spec.Master.Image))
recreatePod = true
}
if currentJenkinsMasterPod != nil && len(r.jenkins.Spec.Master.Annotations) > 0 &&
!reflect.DeepEqual(r.jenkins.Spec.Master.Annotations, currentJenkinsMasterPod.ObjectMeta.Annotations) {
r.logger.Info(fmt.Sprintf("Jenkins pod annotations have changed to %v, recreating pod", r.jenkins.Spec.Master.Annotations))
recreatePod = true
}
if currentJenkinsMasterPod != nil &&
!reflect.DeepEqual(r.jenkins.Spec.Master.Resources, currentJenkinsMasterPod.Spec.Containers[0].Resources) {
r.logger.Info(fmt.Sprintf("Jenkins pod resources have changed to %v, recreating pod", r.jenkins.Spec.Master.Resources))
recreatePod = true
}
if currentJenkinsMasterPod != nil && recreatePod && currentJenkinsMasterPod.ObjectMeta.DeletionTimestamp == nil {
r.logger.Info(fmt.Sprintf("Terminating Jenkins Master Pod %s/%s", currentJenkinsMasterPod.Namespace, currentJenkinsMasterPod.Name))
if err := r.client.Delete(context.TODO(), currentJenkinsMasterPod); err != nil {
return nil, err
}
return &reconcile.Result{Requeue: true}, nil
}
return nil, nil
}
func (r *ReconcileJenkinsBaseConfiguration) waitForJenkins(meta metav1.ObjectMeta) (*reconcile.Result, error) {
jenkinsMasterPodStatus, err := r.getJenkinsMasterPod(meta)
if err != nil {
return nil, err
}
if jenkinsMasterPodStatus.ObjectMeta.DeletionTimestamp != nil {
r.logger.Info("Jenkins master pod is terminating")
return &reconcile.Result{Requeue: true, RequeueAfter: time.Second * 5}, nil
}
if jenkinsMasterPodStatus.Status.Phase != corev1.PodRunning {
r.logger.Info("Jenkins master pod not ready")
return &reconcile.Result{Requeue: true, RequeueAfter: time.Second * 5}, nil
}
for _, containerStatus := range jenkinsMasterPodStatus.Status.ContainerStatuses {
if !containerStatus.Ready {
r.logger.Info("Jenkins master pod not ready, readiness probe failed")
return &reconcile.Result{Requeue: true, RequeueAfter: time.Second * 5}, nil
}
}
return nil, nil
}
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)
if err != nil {
return nil, err
}
r.logger.V(log.VDebug).Info(fmt.Sprintf("Jenkins API URL %s", jenkinsURL))
credentialsSecret := &corev1.Secret{}
err = r.client.Get(context.TODO(), types.NamespacedName{Name: resources.GetOperatorCredentialsSecretName(r.jenkins), Namespace: r.jenkins.ObjectMeta.Namespace}, credentialsSecret)
if err != nil {
return nil, err
}
currentJenkinsMasterPod, err := r.getJenkinsMasterPod(meta)
if err != nil {
return nil, err
}
var tokenCreationTime *time.Time
tokenCreationTimeBytes := credentialsSecret.Data[resources.OperatorCredentialsSecretTokenCreationKey]
if tokenCreationTimeBytes != nil {
tokenCreationTime = &time.Time{}
err = tokenCreationTime.UnmarshalText(tokenCreationTimeBytes)
if err != nil {
tokenCreationTime = nil
}
}
if credentialsSecret.Data[resources.OperatorCredentialsSecretTokenKey] == nil ||
tokenCreationTimeBytes == nil || tokenCreationTime == nil ||
currentJenkinsMasterPod.ObjectMeta.CreationTimestamp.Time.UTC().After(tokenCreationTime.UTC()) {
r.logger.Info("Generating Jenkins API token for operator")
userName := string(credentialsSecret.Data[resources.OperatorCredentialsSecretUserNameKey])
jenkinsClient, err := jenkinsclient.New(
jenkinsURL,
userName,
string(credentialsSecret.Data[resources.OperatorCredentialsSecretPasswordKey]))
if err != nil {
return nil, err
}
token, err := jenkinsClient.GenerateToken(userName, "token")
if err != nil {
return nil, err
}
credentialsSecret.Data[resources.OperatorCredentialsSecretTokenKey] = []byte(token.GetToken())
now, _ := time.Now().UTC().MarshalText()
credentialsSecret.Data[resources.OperatorCredentialsSecretTokenCreationKey] = now
err = r.updateResource(credentialsSecret)
if err != nil {
return nil, err
}
}
return jenkinsclient.New(
jenkinsURL,
string(credentialsSecret.Data[resources.OperatorCredentialsSecretUserNameKey]),
string(credentialsSecret.Data[resources.OperatorCredentialsSecretTokenKey]))
}
@@ -0,0 +1,56 @@
package base
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)
func (r *ReconcileJenkinsBaseConfiguration) createResource(obj metav1.Object) error {
runtimeObj, ok := obj.(runtime.Object)
if !ok {
return fmt.Errorf("is not a %T a runtime.Object", obj)
}
// Set Jenkins instance as the owner and controller
if err := controllerutil.SetControllerReference(r.jenkins, obj, r.scheme); err != nil {
return err
}
return r.client.Create(context.TODO(), runtimeObj)
}
func (r *ReconcileJenkinsBaseConfiguration) updateResource(obj metav1.Object) error {
runtimeObj, ok := obj.(runtime.Object)
if !ok {
return fmt.Errorf("is not a %T a runtime.Object", obj)
}
// Set Jenkins instance as the owner and controller, don't check error(can be already set)
_ = controllerutil.SetControllerReference(r.jenkins, obj, r.scheme)
return r.client.Update(context.TODO(), runtimeObj)
}
func (r *ReconcileJenkinsBaseConfiguration) createOrUpdateResource(obj metav1.Object) error {
runtimeObj, ok := obj.(runtime.Object)
if !ok {
return fmt.Errorf("is not a %T a runtime.Object", obj)
}
// Set Jenkins instance as the owner and controller, don't check error(can be already set)
_ = controllerutil.SetControllerReference(r.jenkins, obj, r.scheme)
err := r.client.Create(context.TODO(), runtimeObj)
if err != nil && errors.IsAlreadyExists(err) {
return r.updateResource(obj)
} else if err != nil && !errors.IsAlreadyExists(err) {
return err
}
return nil
}
@@ -0,0 +1,72 @@
package resources
import (
"fmt"
"text/template"
virtuslabv1alpha1 "github.com/VirtusLab/jenkins-operator/pkg/apis/virtuslab/v1alpha1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const createOperatorUserFileName = "createOperatorUser.groovy"
var createOperatorUserGroovyFmtTemplate = template.Must(template.New(createOperatorUserFileName).Parse(`
import hudson.security.*
def jenkins = jenkins.model.Jenkins.getInstance()
def hudsonRealm = new HudsonPrivateSecurityRealm(false)
hudsonRealm.createAccount(
new File('{{ .OperatorCredentialsPath }}/{{ .OperatorUserNameFile }}').text,
new File('{{ .OperatorCredentialsPath }}/{{ .OperatorPasswordFile }}').text)
jenkins.setSecurityRealm(hudsonRealm)
def strategy = new FullControlOnceLoggedInAuthorizationStrategy()
strategy.setAllowAnonymousRead(false)
jenkins.setAuthorizationStrategy(strategy)
jenkins.save()
`))
func buildCreateJenkinsOperatorUserGroovyScript() (*string, error) {
data := struct {
OperatorCredentialsPath string
OperatorUserNameFile string
OperatorPasswordFile string
}{
OperatorCredentialsPath: jenkinsOperatorCredentialsVolumePath,
OperatorUserNameFile: OperatorCredentialsSecretUserNameKey,
OperatorPasswordFile: OperatorCredentialsSecretPasswordKey,
}
output, err := renderTemplate(createOperatorUserGroovyFmtTemplate, data)
if err != nil {
return nil, err
}
return &output, nil
}
// GetBaseConfigurationConfigMapName returns name of Kubernetes config map used to base configuration
func GetBaseConfigurationConfigMapName(jenkins *virtuslabv1alpha1.Jenkins) string {
return fmt.Sprintf("jenkins-operator-base-configuration-%s", jenkins.ObjectMeta.Name)
}
// NewBaseConfigurationConfigMap builds Kubernetes config map used to base configuration
func NewBaseConfigurationConfigMap(meta metav1.ObjectMeta, jenkins *virtuslabv1alpha1.Jenkins) (*corev1.ConfigMap, error) {
meta.Name = GetBaseConfigurationConfigMapName(jenkins)
createJenkinsOperatorUserGroovy, err := buildCreateJenkinsOperatorUserGroovyScript()
if err != nil {
return nil, err
}
return &corev1.ConfigMap{
TypeMeta: buildConfigMapTypeMeta(),
ObjectMeta: meta,
Data: map[string]string{
createOperatorUserFileName: *createJenkinsOperatorUserGroovy,
},
}, nil
}
@@ -0,0 +1,31 @@
package resources
import (
"fmt"
virtuslabv1alpha1 "github.com/VirtusLab/jenkins-operator/pkg/apis/virtuslab/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// NewResourceObjectMeta builds ObjectMeta for all Kubernetes resources created by operator
func NewResourceObjectMeta(jenkins *virtuslabv1alpha1.Jenkins) metav1.ObjectMeta {
return metav1.ObjectMeta{
Name: GetResourceName(jenkins),
Namespace: jenkins.ObjectMeta.Namespace,
Labels: BuildResourceLabels(jenkins),
}
}
// BuildResourceLabels returns labels for all Kubernetes resources created by operator
func BuildResourceLabels(jenkins *virtuslabv1alpha1.Jenkins) map[string]string {
return map[string]string{
"app": "jenkins-master",
"jenkins-cr": jenkins.Name,
}
}
// GetResourceName returns name of Kubernetes resource base on Jenkins CR
func GetResourceName(jenkins *virtuslabv1alpha1.Jenkins) string {
return fmt.Sprintf("jenkins-operator-%s", jenkins.ObjectMeta.Name)
}
@@ -0,0 +1,176 @@
package resources
import (
"fmt"
virtuslabv1alpha1 "github.com/VirtusLab/jenkins-operator/pkg/apis/virtuslab/v1alpha1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
)
const (
jenkinsHomeVolumeName = "home"
jenkinsHomePath = "/var/jenkins/home"
jenkinsScriptsVolumeName = "scripts"
jenkinsScriptsVolumePath = "/var/jenkins/scripts"
initScriptName = "init.sh"
jenkinsOperatorCredentialsVolumeName = "operator-credentials"
jenkinsOperatorCredentialsVolumePath = "/var/jenkins/operator-credentials"
jenkinsBaseConfigurationVolumeName = "base-configuration"
jenkinsBaseConfigurationVolumePath = "/var/jenkins/base-configuration"
httpPortName = "http"
slavePortName = "slavelistener"
// HTTPPortInt defines Jenkins master HTTP port
HTTPPortInt = 8080
slavePortInt = 50000
httpPortInt32 = int32(8080)
slavePortInt32 = int32(50000)
jenkinsUserUID = int64(1000) // build in Docker image jenkins user UID
)
func buildPodTypeMeta() metav1.TypeMeta {
return metav1.TypeMeta{
Kind: "Pod",
APIVersion: "v1",
}
}
// NewJenkinsMasterPod builds Jenkins Master Kubernetes Pod resource
func NewJenkinsMasterPod(objectMeta metav1.ObjectMeta, jenkins *virtuslabv1alpha1.Jenkins) *corev1.Pod {
initialDelaySeconds := int32(30)
timeoutSeconds := int32(5)
failureThreshold := int32(12)
runAsUser := jenkinsUserUID
objectMeta.Annotations = jenkins.Spec.Master.Annotations
return &corev1.Pod{
TypeMeta: buildPodTypeMeta(),
ObjectMeta: objectMeta,
Spec: corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyNever,
SecurityContext: &corev1.PodSecurityContext{
RunAsUser: &runAsUser,
RunAsGroup: &runAsUser,
},
Containers: []corev1.Container{
{
Name: "jenkins-master",
Image: jenkins.Spec.Master.Image,
Command: []string{
"bash",
fmt.Sprintf("%s/%s", jenkinsScriptsVolumePath, initScriptName),
},
LivenessProbe: &corev1.Probe{
Handler: corev1.Handler{
HTTPGet: &corev1.HTTPGetAction{
Path: "/login",
Port: intstr.FromString(httpPortName),
Scheme: corev1.URISchemeHTTP,
},
},
InitialDelaySeconds: initialDelaySeconds,
TimeoutSeconds: timeoutSeconds,
FailureThreshold: failureThreshold,
},
ReadinessProbe: &corev1.Probe{
Handler: corev1.Handler{
HTTPGet: &corev1.HTTPGetAction{
Path: "/login",
Port: intstr.FromString(httpPortName),
Scheme: corev1.URISchemeHTTP,
},
},
InitialDelaySeconds: initialDelaySeconds,
},
Ports: []corev1.ContainerPort{
{
Name: slavePortName,
ContainerPort: slavePortInt32,
},
{
Name: httpPortName,
ContainerPort: httpPortInt32,
},
},
Env: []corev1.EnvVar{
{
Name: "JENKINS_HOME",
Value: jenkinsHomePath,
},
{
Name: "JAVA_OPTS",
Value: "-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=1 -Djenkins.install.runSetupWizard=false -Djava.awt.headless=true",
},
},
Resources: jenkins.Spec.Master.Resources,
VolumeMounts: []corev1.VolumeMount{
{
Name: jenkinsHomeVolumeName,
MountPath: jenkinsHomePath,
ReadOnly: false,
},
{
Name: jenkinsScriptsVolumeName,
MountPath: jenkinsScriptsVolumePath,
ReadOnly: true,
},
{
Name: jenkinsBaseConfigurationVolumeName,
MountPath: jenkinsBaseConfigurationVolumePath,
ReadOnly: true,
},
{
Name: jenkinsOperatorCredentialsVolumeName,
MountPath: jenkinsOperatorCredentialsVolumePath,
ReadOnly: true,
},
},
},
},
Volumes: []corev1.Volume{
{
Name: jenkinsHomeVolumeName,
VolumeSource: corev1.VolumeSource{
EmptyDir: &corev1.EmptyDirVolumeSource{},
},
},
{
Name: jenkinsScriptsVolumeName,
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: getScriptsConfigMapName(jenkins),
},
},
},
},
{
Name: jenkinsBaseConfigurationVolumeName,
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: GetBaseConfigurationConfigMapName(jenkins),
},
},
},
},
{
Name: jenkinsOperatorCredentialsVolumeName,
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: GetOperatorCredentialsSecretName(jenkins),
},
},
},
},
},
}
}
@@ -0,0 +1,20 @@
package resources
import (
"math/rand"
"time"
)
var randomCharset = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
func randomString(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = randomCharset[rand.Intn(len(randomCharset))]
}
return string(b)
}
func init() {
rand.Seed(time.Now().UnixNano())
}
@@ -0,0 +1,68 @@
package resources
import (
"fmt"
"text/template"
virtuslabv1alpha1 "github.com/VirtusLab/jenkins-operator/pkg/apis/virtuslab/v1alpha1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var initBashTemplate = template.Must(template.New(initScriptName).Parse(`#!/usr/bin/env bash
set -e
set -x
# https://wiki.jenkins.io/display/JENKINS/Post-initialization+script
mkdir -p {{ .JenkinsHomePath }}/init.groovy.d
cp -n {{ .BaseConfigurationPath }}/*.groovy {{ .JenkinsHomePath }}/init.groovy.d
/sbin/tini -s -- /usr/local/bin/jenkins.sh
`))
func buildConfigMapTypeMeta() metav1.TypeMeta {
return metav1.TypeMeta{
Kind: "ConfigMap",
APIVersion: "v1",
}
}
func buildInitBashScript() (*string, error) {
data := struct {
JenkinsHomePath string
BaseConfigurationPath string
}{
JenkinsHomePath: jenkinsHomePath,
BaseConfigurationPath: jenkinsBaseConfigurationVolumePath,
}
output, err := renderTemplate(initBashTemplate, data)
if err != nil {
return nil, err
}
return &output, nil
}
func getScriptsConfigMapName(jenkins *virtuslabv1alpha1.Jenkins) string {
return fmt.Sprintf("jenkins-operator-scripts-%s", jenkins.ObjectMeta.Name)
}
// NewScriptsConfigMap builds Kubernetes config map used to store scripts
func NewScriptsConfigMap(meta metav1.ObjectMeta, jenkins *virtuslabv1alpha1.Jenkins) (*corev1.ConfigMap, error) {
meta.Name = getScriptsConfigMapName(jenkins)
initBashScript, err := buildInitBashScript()
if err != nil {
return nil, err
}
return &corev1.ConfigMap{
TypeMeta: buildConfigMapTypeMeta(),
ObjectMeta: meta,
Data: map[string]string{
initScriptName: *initBashScript,
},
}, nil
}
@@ -0,0 +1,50 @@
package resources
import (
"fmt"
virtuslabv1alpha1 "github.com/VirtusLab/jenkins-operator/pkg/apis/virtuslab/v1alpha1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
// OperatorUserName defines username for Jenkins API calls
OperatorUserName = "jenkins-operator"
// OperatorCredentialsSecretUserNameKey defines key of username in operator credentials secret
OperatorCredentialsSecretUserNameKey = "user"
// OperatorCredentialsSecretPasswordKey defines key of password in operator credentials secret
OperatorCredentialsSecretPasswordKey = "password"
// OperatorCredentialsSecretTokenKey defines key of token in operator credentials secret
OperatorCredentialsSecretTokenKey = "token"
// OperatorCredentialsSecretTokenCreationKey defines key of token creation time in operator credentials secret
OperatorCredentialsSecretTokenCreationKey = "tokenCreationTime"
)
func buildSecretTypeMeta() metav1.TypeMeta {
return metav1.TypeMeta{
Kind: "Secret",
APIVersion: "v1",
}
}
// GetOperatorCredentialsSecretName returns name of Kubernetes secret used to store jenkins operator credentials
// to allow calls to Jenkins API
func GetOperatorCredentialsSecretName(jenkins *virtuslabv1alpha1.Jenkins) string {
return fmt.Sprintf("jenkins-operator-credentials-%s", jenkins.Name)
}
// NewOperatorCredentialsSecret builds the Kubernetes secret used to store jenkins operator credentials
// to allow calls to Jenkins API
func NewOperatorCredentialsSecret(meta metav1.ObjectMeta, jenkins *virtuslabv1alpha1.Jenkins) *corev1.Secret {
meta.Name = GetOperatorCredentialsSecretName(jenkins)
return &corev1.Secret{
TypeMeta: buildSecretTypeMeta(),
ObjectMeta: meta,
Data: map[string][]byte{
OperatorCredentialsSecretUserNameKey: []byte(OperatorUserName),
OperatorCredentialsSecretPasswordKey: []byte(randomString(20)),
},
}
}
@@ -0,0 +1,49 @@
package resources
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
)
func buildServiceTypeMeta() metav1.TypeMeta {
return metav1.TypeMeta{
Kind: "Service",
APIVersion: "v1",
}
}
// 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),
},
},
},
}
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
}
@@ -0,0 +1,15 @@
package resources
import (
"bytes"
"text/template"
)
func renderTemplate(template *template.Template, data interface{}) (string, error) {
var buffer bytes.Buffer
if err := template.Execute(&buffer, data); err != nil {
return "", err
}
return buffer.String(), nil
}
@@ -0,0 +1,27 @@
package base
import (
"regexp"
virtuslabv1alpha1 "github.com/VirtusLab/jenkins-operator/pkg/apis/virtuslab/v1alpha1"
docker "github.com/docker/distribution/reference"
)
var (
dockerImageRegexp = regexp.MustCompile(`^` + docker.TagRegexp.String() + `$`)
)
func (r *ReconcileJenkinsBaseConfiguration) validate(jenkins *virtuslabv1alpha1.Jenkins) bool {
if jenkins.Spec.Master.Image == "" {
r.logger.V(0).Info("Image not set")
return false
}
if !dockerImageRegexp.MatchString(jenkins.Spec.Master.Image) && !docker.ReferenceRegexp.MatchString(jenkins.Spec.Master.Image) {
r.logger.V(0).Info("Invalid image")
return false
}
return true
}
+32 -62
View File
@@ -2,37 +2,38 @@ package jenkins
import (
"context"
"log"
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/log"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"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"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
)
/**
* USER ACTION REQUIRED: This is a scaffold file intended for the user to modify with their own Controller
* business logic. Delete these comments after modifying this file.*
*/
// Add creates a new Jenkins 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 {
return add(mgr, newReconciler(mgr))
func Add(mgr manager.Manager, local, minikube bool) error {
return add(mgr, newReconciler(mgr, local, minikube))
}
// newReconciler returns a new reconcile.Reconciler
func newReconciler(mgr manager.Manager) reconcile.Reconciler {
return &ReconcileJenkins{client: mgr.GetClient(), scheme: mgr.GetScheme()}
func newReconciler(mgr manager.Manager, local, minikube bool) reconcile.Reconciler {
return &ReconcileJenkins{
client: mgr.GetClient(),
scheme: mgr.GetScheme(),
local: local,
minikube: minikube,
}
}
// add adds a new Controller to mgr with r as the reconcile.Reconciler
@@ -68,23 +69,20 @@ var _ reconcile.Reconciler = &ReconcileJenkins{}
type ReconcileJenkins 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
client client.Client
scheme *runtime.Scheme
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
// TODO(user): Modify this Reconcile function to implement your Controller logic. This example creates
// a Pod as an example
// Note:
// 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 *ReconcileJenkins) Reconcile(request reconcile.Request) (reconcile.Result, error) {
log.Printf("Reconciling Jenkins %s/%s\n", request.Namespace, request.Name)
logger := r.buildLogger(request.Name)
logger.Info("Reconciling Jenkins")
// Fetch the Jenkins instance
instance := &virtuslabv1alpha1.Jenkins{}
err := r.client.Get(context.TODO(), request.NamespacedName, instance)
jenkins := &virtuslabv1alpha1.Jenkins{}
err := r.client.Get(context.TODO(), request.NamespacedName, jenkins)
if err != nil {
if errors.IsNotFound(err) {
// Request object not found, could have been deleted after reconcile request.
@@ -96,54 +94,26 @@ func (r *ReconcileJenkins) Reconcile(request reconcile.Request) (reconcile.Resul
return reconcile.Result{}, err
}
// Define a new Pod object
pod := newPodForCR(instance)
// Set Jenkins instance as the owner and controller
if err := controllerutil.SetControllerReference(instance, pod, r.scheme); err != nil {
baseConfiguration := base.New(r.client, r.scheme, logger, jenkins, r.local, r.minikube)
result, err := baseConfiguration.Reconcile()
if err != nil {
return reconcile.Result{}, err
}
// Check if this Pod already exists
found := &corev1.Pod{}
err = r.client.Get(context.TODO(), types.NamespacedName{Name: pod.Name, Namespace: pod.Namespace}, found)
if err != nil && errors.IsNotFound(err) {
log.Printf("Creating a new Pod %s/%s\n", pod.Namespace, pod.Name)
err = r.client.Create(context.TODO(), pod)
if result != nil {
return *result, nil
}
if err == nil && result == nil && jenkins.Status.BaseConfigurationCompletedTime == nil {
now := metav1.Now()
jenkins.Status.BaseConfigurationCompletedTime = &now
err = r.client.Update(context.TODO(), jenkins)
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
log.Printf("Skip reconcile: Pod %s/%s already exists", found.Namespace, found.Name)
return reconcile.Result{}, nil
}
// newPodForCR returns a busybox pod with the same name/namespace as the cr
func newPodForCR(cr *virtuslabv1alpha1.Jenkins) *corev1.Pod {
labels := map[string]string{
"app": cr.Name,
}
return &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: cr.Name + "-pod",
Namespace: cr.Namespace,
Labels: labels,
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "busybox",
Image: "busybox",
Command: []string{"sleep", "3600"},
},
},
},
}
func (r *ReconcileJenkins) buildLogger(jenkinsName string) logr.Logger {
return log.Log.WithValues("cr", jenkinsName)
}