diff --git a/docs/getting-started.md b/docs/getting-started.md index 7bee7304..43008181 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -213,7 +213,30 @@ You can verify if your pipelines were successfully configured in Jenkins Seed Jo ## Jenkins Customisation Jenkins can be customized using groovy scripts or configuration as code plugin. All custom configuration is stored in -the **jenkins-operator-user-configuration-example** ConfigMap which is automatically created by **jenkins-operator**. +the **jenkins-operator-user-configuration-example** ConfigMap which is automatically created by **jenkins-operator**. + +**jenkins-operator** creates **jenkins-operator-user-configuration-example** secret where user can store sensitive +information used for custom configuration. If you have entry in secret named `PASSWORD` then you can use it in +Configuration as Plugin as `adminAddress: "${PASSWORD}"`. + +``` +kubectl get secret jenkins-operator-user-configuration-example -o yaml + +apiVersion: v1 +data: + SECRET_JENKINS_ADMIN_ADDRESS: YXNkZgo= +kind: Secret +metadata: + creationTimestamp: 2019-03-03T11:54:36Z + labels: + app: jenkins-operator + jenkins-cr: example + watch: "true" + name: jenkins-operator-user-configuration-example + namespace: default +type: Opaque + +``` ``` kubectl get configmap jenkins-operator-user-configuration-example -o yaml @@ -243,6 +266,7 @@ data: 1-system-message.yaml: |2 jenkins: systemMessage: "Configuration as Code integration works!!!" + adminAddress: "${SECRET_JENKINS_ADMIN_ADDRESS}" kind: ConfigMap metadata: labels: diff --git a/pkg/controller/jenkins/configuration/base/reconcile.go b/pkg/controller/jenkins/configuration/base/reconcile.go index 2386179c..12ba7a2c 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile.go +++ b/pkg/controller/jenkins/configuration/base/reconcile.go @@ -126,6 +126,11 @@ func (r *ReconcileJenkinsBaseConfiguration) ensureResourcesRequiredForJenkinsPod } r.logger.V(log.VDebug).Info("User configuration config map is present") + if err := r.createUserConfigurationSecret(metaObject); err != nil { + return err + } + r.logger.V(log.VDebug).Info("User configuration secret is present") + if err := r.createRBAC(metaObject); err != nil { return err } @@ -252,6 +257,23 @@ func (r *ReconcileJenkinsBaseConfiguration) createUserConfigurationConfigMap(met return nil } +func (r *ReconcileJenkinsBaseConfiguration) createUserConfigurationSecret(meta metav1.ObjectMeta) error { + currentSecret := &corev1.Secret{} + err := r.k8sClient.Get(context.TODO(), types.NamespacedName{Name: resources.GetUserConfigurationSecretNameFromJenkins(r.jenkins), Namespace: r.jenkins.Namespace}, currentSecret) + if err != nil && errors.IsNotFound(err) { + return stackerr.WithStack(r.k8sClient.Create(context.TODO(), resources.NewUserConfigurationSecret(r.jenkins))) + } else if err != nil { + return stackerr.WithStack(err) + } + valid := r.verifyLabelsForWatchedResource(currentSecret) + if !valid { + currentSecret.ObjectMeta.Labels = resources.BuildLabelsForWatchedResources(r.jenkins) + return stackerr.WithStack(r.k8sClient.Update(context.TODO(), currentSecret)) + } + + return nil +} + func (r *ReconcileJenkinsBaseConfiguration) createRBAC(meta metav1.ObjectMeta) error { serviceAccount := resources.NewServiceAccount(meta) err := r.createResource(serviceAccount) diff --git a/pkg/controller/jenkins/configuration/base/resources/pod.go b/pkg/controller/jenkins/configuration/base/resources/pod.go index acf17fe4..e1b68d3d 100644 --- a/pkg/controller/jenkins/configuration/base/resources/pod.go +++ b/pkg/controller/jenkins/configuration/base/resources/pod.go @@ -31,10 +31,14 @@ const ( JenkinsBaseConfigurationVolumePath = jenkinsPath + "/base-configuration" jenkinsUserConfigurationVolumeName = "user-configuration" - // JenkinsUserConfigurationVolumePath is a path where are groovy scripts used to configure Jenkins - // this scripts are provided by user + // JenkinsUserConfigurationVolumePath is a path where are groovy scripts and CasC configs used to configure Jenkins + // this script is provided by user JenkinsUserConfigurationVolumePath = jenkinsPath + "/user-configuration" + userConfigurationSecretVolumeName = "user-configuration-secrets" + // UserConfigurationSecretVolumePath is a path where are secrets used for groovy scripts and CasC configs + UserConfigurationSecretVolumePath = jenkinsPath + "/user-configuration-secrets" + httpPortName = "http" slavePortName = "slavelistener" // HTTPPortInt defines Jenkins master HTTP port @@ -121,6 +125,10 @@ func NewJenkinsMasterPod(objectMeta metav1.ObjectMeta, jenkins *v1alpha1.Jenkins Name: "JAVA_OPTS", Value: "-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=1 -Djenkins.install.runSetupWizard=false -Djava.awt.headless=true", }, + { + Name: "SECRETS", // https://github.com/jenkinsci/configuration-as-code-plugin/blob/master/demos/kubernetes-secrets/README.md + Value: UserConfigurationSecretVolumePath, + }, }, Resources: jenkins.Spec.Master.Resources, VolumeMounts: []corev1.VolumeMount{ @@ -154,6 +162,11 @@ func NewJenkinsMasterPod(objectMeta metav1.ObjectMeta, jenkins *v1alpha1.Jenkins MountPath: jenkinsOperatorCredentialsVolumePath, ReadOnly: true, }, + { + Name: userConfigurationSecretVolumeName, + MountPath: UserConfigurationSecretVolumePath, + ReadOnly: true, + }, }, }, }, @@ -212,6 +225,14 @@ func NewJenkinsMasterPod(objectMeta metav1.ObjectMeta, jenkins *v1alpha1.Jenkins }, }, }, + { + Name: userConfigurationSecretVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: GetUserConfigurationSecretNameFromJenkins(jenkins), + }, + }, + }, }, }, } diff --git a/pkg/controller/jenkins/configuration/base/resources/user_configuration_configmap.go b/pkg/controller/jenkins/configuration/base/resources/user_configuration_configmap.go index f68438a3..0bca52b5 100644 --- a/pkg/controller/jenkins/configuration/base/resources/user_configuration_configmap.go +++ b/pkg/controller/jenkins/configuration/base/resources/user_configuration_configmap.go @@ -44,15 +44,13 @@ func GetUserConfigurationConfigMapName(jenkinsCRName string) string { // NewUserConfigurationConfigMap builds Kubernetes config map used to user configuration func NewUserConfigurationConfigMap(jenkins *v1alpha1.Jenkins) *corev1.ConfigMap { - meta := metav1.ObjectMeta{ - Name: GetUserConfigurationConfigMapNameFromJenkins(jenkins), - Namespace: jenkins.ObjectMeta.Namespace, - Labels: BuildLabelsForWatchedResources(jenkins), - } - return &corev1.ConfigMap{ - TypeMeta: buildConfigMapTypeMeta(), - ObjectMeta: meta, + TypeMeta: buildConfigMapTypeMeta(), + ObjectMeta: metav1.ObjectMeta{ + Name: GetUserConfigurationConfigMapNameFromJenkins(jenkins), + Namespace: jenkins.ObjectMeta.Namespace, + Labels: BuildLabelsForWatchedResources(jenkins), + }, Data: map[string]string{ "1-configure-theme.groovy": configureTheme, }, diff --git a/pkg/controller/jenkins/configuration/base/resources/user_configuration_secret.go b/pkg/controller/jenkins/configuration/base/resources/user_configuration_secret.go new file mode 100644 index 00000000..01d3776d --- /dev/null +++ b/pkg/controller/jenkins/configuration/base/resources/user_configuration_secret.go @@ -0,0 +1,33 @@ +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" +) + +// GetUserConfigurationSecretNameFromJenkins returns name of Kubernetes secret used to store jenkins operator credentials +func GetUserConfigurationSecretNameFromJenkins(jenkins *v1alpha1.Jenkins) string { + return fmt.Sprintf("%s-user-configuration-%s", constants.OperatorName, jenkins.Name) +} + +// GetUserConfigurationSecretName returns name of Kubernetes secret used to store jenkins operator credentials +func GetUserConfigurationSecretName(jenkinsCRName string) string { + return fmt.Sprintf("%s-user-configuration-%s", constants.OperatorName, jenkinsCRName) +} + +// NewUserConfigurationSecret builds the Kubernetes secret resource which is used to store user sensitive data for Jenkins configuration +func NewUserConfigurationSecret(jenkins *v1alpha1.Jenkins) *corev1.Secret { + return &corev1.Secret{ + TypeMeta: buildServiceTypeMeta(), + ObjectMeta: metav1.ObjectMeta{ + Name: GetUserConfigurationSecretNameFromJenkins(jenkins), + Namespace: jenkins.ObjectMeta.Namespace, + Labels: BuildLabelsForWatchedResources(jenkins), + }, + } +} diff --git a/pkg/controller/jenkins/configuration/user/casc/caac.go b/pkg/controller/jenkins/configuration/user/casc/caac.go index 6648fe66..07da86bc 100644 --- a/pkg/controller/jenkins/configuration/user/casc/caac.go +++ b/pkg/controller/jenkins/configuration/user/casc/caac.go @@ -1,6 +1,7 @@ package casc import ( + "context" "crypto/sha256" "encoding/base64" "fmt" @@ -9,14 +10,19 @@ import ( "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkinsio/v1alpha1" jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client" + "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/jobs" "github.com/go-logr/logr" + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" k8s "sigs.k8s.io/controller-runtime/pkg/client" ) const ( - jobHashParameterName = "hash" + userConfigurationHashParameterName = "userConfigurationHash" + userConfigurationSecretHashParameterName = "userConfigurationSecretHash" ) // ConfigurationAsCode defines API which configures Jenkins with help Configuration as a code plugin @@ -25,23 +31,23 @@ type ConfigurationAsCode struct { k8sClient k8s.Client logger logr.Logger jobName string - configsPath string } // New creates new instance of ConfigurationAsCode -func New(jenkinsClient jenkinsclient.Jenkins, k8sClient k8s.Client, logger logr.Logger, jobName, configsPath string) *ConfigurationAsCode { +func New(jenkinsClient jenkinsclient.Jenkins, k8sClient k8s.Client, logger logr.Logger, jobName string) *ConfigurationAsCode { return &ConfigurationAsCode{ jenkinsClient: jenkinsClient, k8sClient: k8sClient, logger: logger, jobName: jobName, - configsPath: configsPath, } } // ConfigureJob configures jenkins job which configures Jenkins with help Configuration as a code plugin func (g *ConfigurationAsCode) ConfigureJob() error { - _, created, err := g.jenkinsClient.CreateOrUpdateJob(fmt.Sprintf(configurationJobXMLFmt, g.configsPath), g.jobName) + _, created, err := g.jenkinsClient.CreateOrUpdateJob( + fmt.Sprintf(configurationJobXMLFmt, resources.UserConfigurationSecretVolumePath, resources.JenkinsUserConfigurationVolumePath), + g.jobName) if err != nil { return err } @@ -52,29 +58,67 @@ func (g *ConfigurationAsCode) ConfigureJob() error { } // Ensure configures Jenkins with help Configuration as a code plugin -func (g *ConfigurationAsCode) Ensure(secretOrConfigMapData map[string]string, jenkins *v1alpha1.Jenkins) (bool, error) { +func (g *ConfigurationAsCode) Ensure(jenkins *v1alpha1.Jenkins) (bool, error) { jobsClient := jobs.New(g.jenkinsClient, g.k8sClient, g.logger) - hash := g.calculateHash(secretOrConfigMapData) - done, err := jobsClient.EnsureBuildJob(g.jobName, hash, map[string]string{jobHashParameterName: hash}, jenkins, true) + configuration := &corev1.ConfigMap{} + namespaceName := types.NamespacedName{Namespace: jenkins.Namespace, Name: resources.GetUserConfigurationConfigMapNameFromJenkins(jenkins)} + err := g.k8sClient.Get(context.TODO(), namespaceName, configuration) + if err != nil { + return false, errors.WithStack(err) + } + + secret := &corev1.Secret{} + namespaceName = types.NamespacedName{Namespace: jenkins.Namespace, Name: resources.GetUserConfigurationSecretNameFromJenkins(jenkins)} + err = g.k8sClient.Get(context.TODO(), namespaceName, configuration) + if err != nil { + return false, errors.WithStack(err) + } + + userConfigurationSecretHash := g.calculateUserConfigurationSecretHash(secret) + userConfigurationHash := g.calculateUserConfigurationHash(configuration) + done, err := jobsClient.EnsureBuildJob( + g.jobName, + userConfigurationSecretHash+userConfigurationHash, + map[string]string{ + userConfigurationHashParameterName: userConfigurationHash, + userConfigurationSecretHashParameterName: userConfigurationSecretHash, + }, + jenkins, + true) if err != nil { return false, err } return done, nil } -func (g *ConfigurationAsCode) calculateHash(secretOrConfigMapData map[string]string) string { +func (g *ConfigurationAsCode) calculateUserConfigurationSecretHash(userConfigurationSecret *corev1.Secret) string { hash := sha256.New() var keys []string - for key := range secretOrConfigMapData { + for key := range userConfigurationSecret.Data { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + hash.Write([]byte(key)) + hash.Write([]byte(userConfigurationSecret.Data[key])) + } + return base64.StdEncoding.EncodeToString(hash.Sum(nil)) +} + +func (g *ConfigurationAsCode) calculateUserConfigurationHash(userConfiguration *corev1.ConfigMap) string { + hash := sha256.New() + + var keys []string + for key := range userConfiguration.Data { keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { if strings.HasSuffix(key, ".yaml") { hash.Write([]byte(key)) - hash.Write([]byte(secretOrConfigMapData[key])) + hash.Write([]byte(userConfiguration.Data[key])) } } return base64.StdEncoding.EncodeToString(hash.Sum(nil)) @@ -90,9 +134,15 @@ const configurationJobXMLFmt = ` - ` + jobHashParameterName + ` - - + ` + userConfigurationSecretHashParameterName + ` + + + false + + + ` + userConfigurationHashParameterName + ` + + false @@ -101,28 +151,23 @@ const configurationJobXMLFmt = `