From 1d10d629cedc677144ce15c287207f5fcbc87e55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20S=C4=99k?= Date: Fri, 15 Mar 2019 18:30:07 +0100 Subject: [PATCH] #14 Add username/password authentication for seed jobs --- pkg/apis/jenkinsio/v1alpha1/jenkins_types.go | 37 +- .../v1alpha1/zz_generated.deepcopy.go | 26 +- .../jenkins/configuration/base/reconcile.go | 21 +- .../resources/base_configuration_configmap.go | 13 +- .../configuration/base/resources/meta.go | 13 +- .../resources/user_configuration_configmap.go | 2 +- .../resources/user_configuration_secret.go | 2 +- .../configuration/user/seedjobs/seedjobs.go | 130 ++-- .../user/seedjobs/seedjobs_test.go | 11 +- .../jenkins/configuration/user/validate.go | 123 +++- .../configuration/user/validate_test.go | 580 ++++++++++++------ pkg/controller/jenkins/jobs/jobs_test.go | 11 +- test/e2e/configuration_test.go | 113 +--- test/e2e/jenkins.go | 18 +- test/e2e/main_test.go | 9 +- test/e2e/restart_test.go | 4 +- test/e2e/seedjobs_test.go | 153 +++++ 17 files changed, 826 insertions(+), 440 deletions(-) create mode 100644 test/e2e/seedjobs_test.go diff --git a/pkg/apis/jenkinsio/v1alpha1/jenkins_types.go b/pkg/apis/jenkinsio/v1alpha1/jenkins_types.go index ac7fa218..78b35633 100644 --- a/pkg/apis/jenkinsio/v1alpha1/jenkins_types.go +++ b/pkg/apis/jenkinsio/v1alpha1/jenkins_types.go @@ -107,19 +107,34 @@ type JenkinsList struct { Items []Jenkins `json:"items"` } -// SeedJob defined configuration for seed jobs and deploy keys -type SeedJob struct { - ID string `json:"id"` - Description string `json:"description,omitempty"` - Targets string `json:"targets,omitempty"` - RepositoryBranch string `json:"repositoryBranch,omitempty"` - RepositoryURL string `json:"repositoryUrl"` - PrivateKey PrivateKey `json:"privateKey,omitempty"` +// JenkinsCredentialType defines type of Jenkins credential used to seed job mechanisms +type JenkinsCredentialType string + +const ( + // NoJenkinsCredentialCredentialType define none Jenkins credential type + NoJenkinsCredentialCredentialType JenkinsCredentialType = "" + // BasicSSHCredentialType define basic SSH Jenkins credential type + BasicSSHCredentialType JenkinsCredentialType = "basicSSHUserPrivateKey" + // UsernamePasswordCredentialType define username & password Jenkins credential type + UsernamePasswordCredentialType JenkinsCredentialType = "usernamePassword" +) + +// AllowedJenkinsCredentialMap contains all allowed Jenkins credentials types +var AllowedJenkinsCredentialMap = map[string]string{ + string(NoJenkinsCredentialCredentialType): "", + string(BasicSSHCredentialType): "", + string(UsernamePasswordCredentialType): "", } -// PrivateKey contains a private key -type PrivateKey struct { - SecretKeyRef *corev1.SecretKeySelector `json:"secretKeyRef"` +// SeedJob defined configuration for seed jobs and deploy keys +type SeedJob struct { + ID string `json:"id,omitempty"` + CredentialID string `json:"credentialID,omitempty"` + Description string `json:"description,omitempty"` + Targets string `json:"targets,omitempty"` + RepositoryBranch string `json:"repositoryBranch,omitempty"` + RepositoryURL string `json:"repositoryUrl,omitempty"` + JenkinsCredentialType JenkinsCredentialType `json:"credentialType,omitempty"` } func init() { diff --git a/pkg/apis/jenkinsio/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/jenkinsio/v1alpha1/zz_generated.deepcopy.go index edd8b3a0..c53b799c 100644 --- a/pkg/apis/jenkinsio/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/jenkinsio/v1alpha1/zz_generated.deepcopy.go @@ -185,9 +185,7 @@ func (in *JenkinsSpec) DeepCopyInto(out *JenkinsSpec) { if in.SeedJobs != nil { in, out := &in.SeedJobs, &out.SeedJobs *out = make([]SeedJob, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } + copy(*out, *in) } in.Service.DeepCopyInto(&out.Service) in.SlaveService.DeepCopyInto(&out.SlaveService) @@ -239,31 +237,9 @@ func (in *JenkinsStatus) DeepCopy() *JenkinsStatus { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PrivateKey) DeepCopyInto(out *PrivateKey) { - *out = *in - if in.SecretKeyRef != nil { - in, out := &in.SecretKeyRef, &out.SecretKeyRef - *out = new(v1.SecretKeySelector) - (*in).DeepCopyInto(*out) - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrivateKey. -func (in *PrivateKey) DeepCopy() *PrivateKey { - if in == nil { - return nil - } - out := new(PrivateKey) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SeedJob) DeepCopyInto(out *SeedJob) { *out = *in - in.PrivateKey.DeepCopyInto(&out.PrivateKey) return } diff --git a/pkg/controller/jenkins/configuration/base/reconcile.go b/pkg/controller/jenkins/configuration/base/reconcile.go index 96e7642f..a65f20e6 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile.go +++ b/pkg/controller/jenkins/configuration/base/reconcile.go @@ -253,9 +253,8 @@ func (r *ReconcileJenkinsBaseConfiguration) createUserConfigurationConfigMap(met } else if err != nil { return stackerr.WithStack(err) } - valid := r.verifyLabelsForWatchedResource(currentConfigMap) - if !valid { - currentConfigMap.ObjectMeta.Labels = resources.BuildLabelsForWatchedResources(r.jenkins) + if !resources.VerifyIfLabelsAreSet(currentConfigMap, resources.BuildLabelsForWatchedResources(*r.jenkins)) { + currentConfigMap.ObjectMeta.Labels = resources.BuildLabelsForWatchedResources(*r.jenkins) return stackerr.WithStack(r.k8sClient.Update(context.TODO(), currentConfigMap)) } @@ -270,9 +269,8 @@ func (r *ReconcileJenkinsBaseConfiguration) createUserConfigurationSecret(meta m } else if err != nil { return stackerr.WithStack(err) } - valid := r.verifyLabelsForWatchedResource(currentSecret) - if !valid { - currentSecret.ObjectMeta.Labels = resources.BuildLabelsForWatchedResources(r.jenkins) + if !resources.VerifyIfLabelsAreSet(currentSecret, resources.BuildLabelsForWatchedResources(*r.jenkins)) { + currentSecret.ObjectMeta.Labels = resources.BuildLabelsForWatchedResources(*r.jenkins) return stackerr.WithStack(r.k8sClient.Update(context.TODO(), currentSecret)) } @@ -547,14 +545,3 @@ func (r *ReconcileJenkinsBaseConfiguration) ensureBaseConfiguration(jenkinsClien return reconcile.Result{}, nil } - -func (r *ReconcileJenkinsBaseConfiguration) verifyLabelsForWatchedResource(object metav1.Object) bool { - requiredLabels := resources.BuildLabelsForWatchedResources(r.jenkins) - for key, value := range requiredLabels { - if object.GetLabels()[key] != value { - return false - } - } - - return true -} 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 b14b9ce2..bf5682d0 100644 --- a/pkg/controller/jenkins/configuration/base/resources/base_configuration_configmap.go +++ b/pkg/controller/jenkins/configuration/base/resources/base_configuration_configmap.go @@ -155,6 +155,16 @@ if (jenkins.getView(jenkinsViewName) == null) { jenkins.save() ` +const disableJobDSLScriptApproval = ` +import jenkins.model.Jenkins +import javaposse.jobdsl.plugin.GlobalJobDslSecurityConfiguration +import jenkins.model.GlobalConfiguration + +// disable Job DSL script approval +GlobalConfiguration.all().get(GlobalJobDslSecurityConfiguration.class).useScriptSecurity=false +GlobalConfiguration.all().get(GlobalJobDslSecurityConfiguration.class).save() +` + // GetBaseConfigurationConfigMapName returns name of Kubernetes config map used to base configuration func GetBaseConfigurationConfigMapName(jenkins *v1alpha1.Jenkins) string { return fmt.Sprintf("%s-base-configuration-%s", constants.OperatorName, jenkins.ObjectMeta.Name) @@ -178,7 +188,8 @@ func NewBaseConfigurationConfigMap(meta metav1.ObjectMeta, jenkins *v1alpha1.Jen 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, + "7-configure-views.groovy": configureViews, + "8-disable-job-dsl-script-approval.groovy": disableJobDSLScriptApproval, }, } } diff --git a/pkg/controller/jenkins/configuration/base/resources/meta.go b/pkg/controller/jenkins/configuration/base/resources/meta.go index 5db914df..ceee93d8 100644 --- a/pkg/controller/jenkins/configuration/base/resources/meta.go +++ b/pkg/controller/jenkins/configuration/base/resources/meta.go @@ -29,7 +29,7 @@ func BuildResourceLabels(jenkins *v1alpha1.Jenkins) map[string]string { // BuildLabelsForWatchedResources returns labels for Kubernetes resources which operator want to watch // resources with that labels should not be deleted after Jenkins CR deletion, to prevent this situation don't set // any owner -func BuildLabelsForWatchedResources(jenkins *v1alpha1.Jenkins) map[string]string { +func BuildLabelsForWatchedResources(jenkins v1alpha1.Jenkins) map[string]string { return map[string]string{ constants.LabelAppKey: constants.LabelAppValue, constants.LabelJenkinsCRKey: jenkins.Name, @@ -41,3 +41,14 @@ func BuildLabelsForWatchedResources(jenkins *v1alpha1.Jenkins) map[string]string func GetResourceName(jenkins *v1alpha1.Jenkins) string { return fmt.Sprintf("%s-%s", constants.LabelAppValue, jenkins.ObjectMeta.Name) } + +// VerifyIfLabelsAreSet check is selected labels are set for specific resource +func VerifyIfLabelsAreSet(object metav1.Object, requiredLabels map[string]string) bool { + for key, value := range requiredLabels { + if object.GetLabels()[key] != value { + return false + } + } + + return true +} 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 0bca52b5..dc1c75b0 100644 --- a/pkg/controller/jenkins/configuration/base/resources/user_configuration_configmap.go +++ b/pkg/controller/jenkins/configuration/base/resources/user_configuration_configmap.go @@ -49,7 +49,7 @@ func NewUserConfigurationConfigMap(jenkins *v1alpha1.Jenkins) *corev1.ConfigMap ObjectMeta: metav1.ObjectMeta{ Name: GetUserConfigurationConfigMapNameFromJenkins(jenkins), Namespace: jenkins.ObjectMeta.Namespace, - Labels: BuildLabelsForWatchedResources(jenkins), + 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 index 01d3776d..bb9f1c92 100644 --- a/pkg/controller/jenkins/configuration/base/resources/user_configuration_secret.go +++ b/pkg/controller/jenkins/configuration/base/resources/user_configuration_secret.go @@ -27,7 +27,7 @@ func NewUserConfigurationSecret(jenkins *v1alpha1.Jenkins) *corev1.Secret { ObjectMeta: metav1.ObjectMeta{ Name: GetUserConfigurationSecretNameFromJenkins(jenkins), Namespace: jenkins.ObjectMeta.Namespace, - Labels: BuildLabelsForWatchedResources(jenkins), + Labels: BuildLabelsForWatchedResources(*jenkins), }, } } diff --git a/pkg/controller/jenkins/configuration/user/seedjobs/seedjobs.go b/pkg/controller/jenkins/configuration/user/seedjobs/seedjobs.go index e4c4ed70..7f030fdc 100644 --- a/pkg/controller/jenkins/configuration/user/seedjobs/seedjobs.go +++ b/pkg/controller/jenkins/configuration/user/seedjobs/seedjobs.go @@ -8,12 +8,14 @@ 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/constants" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/jobs" "github.com/jenkinsci/kubernetes-operator/pkg/log" "github.com/go-logr/logr" - "k8s.io/api/core/v1" + stackerr "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" k8s "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -22,12 +24,23 @@ const ( // ConfigureSeedJobsName this is the fixed seed job name ConfigureSeedJobsName = constants.OperatorName + "-configure-seed-job" - deployKeyIDParameterName = "DEPLOY_KEY_ID" - privateKeyParameterName = "PRIVATE_KEY" + idParameterName = "ID" + credentialIDParameterName = "CREDENTIAL_ID" repositoryURLParameterName = "REPOSITORY_URL" repositoryBranchParameterName = "REPOSITORY_BRANCH" targetsParameterName = "TARGETS" displayNameParameterName = "SEED_JOB_DISPLAY_NAME" + + // UsernameSecretKey is username data key in Kubernetes secret used to create Jenkins username/password credential + UsernameSecretKey = "username" + // PasswordSecretKey is password data key in Kubernetes secret used to create Jenkins username/password credential + PasswordSecretKey = "password" + // PrivateKeySecretKey is private key data key in Kubernetes secret used to create Jenkins SSH credential + PrivateKeySecretKey = "privateKey" + + // JenkinsCredentialTypeLabelName is label for kubernetes-credentials-provider-plugin which determine Jenkins + // credential type + JenkinsCredentialTypeLabelName = "jenkins.io/credentials-type" ) // SeedJobs defines API for configuring and ensuring Jenkins Seed Jobs and Deploy Keys @@ -48,11 +61,15 @@ func New(jenkinsClient jenkinsclient.Jenkins, k8sClient k8s.Client, logger logr. // EnsureSeedJobs configures seed job and runs it for every entry from Jenkins.Spec.SeedJobs func (s *SeedJobs) EnsureSeedJobs(jenkins *v1alpha1.Jenkins) (done bool, err error) { - err = s.createJob() - if err != nil { + if err = s.createJob(); err != nil { s.logger.V(log.VWarn).Info("Couldn't create jenkins seed job") return false, err } + + if err = s.ensureLabelsForSecrets(*jenkins); err != nil { + return false, err + } + done, err = s.buildJobs(jenkins) if err != nil { s.logger.V(log.VWarn).Info("Couldn't build jenkins seed job") @@ -73,18 +90,46 @@ func (s *SeedJobs) createJob() error { return nil } +// ensureLabelsForSecrets adds labels to Kubernetes secrets where are Jenkins credentials used for seed jobs, +// thanks to them kubernetes-credentials-provider-plugin will create Jenkins credentials in Jenkins and +// Operator will able to watch any changes made to them +func (s *SeedJobs) ensureLabelsForSecrets(jenkins v1alpha1.Jenkins) error { + for _, seedJob := range jenkins.Spec.SeedJobs { + if seedJob.JenkinsCredentialType == v1alpha1.BasicSSHCredentialType || seedJob.JenkinsCredentialType == v1alpha1.UsernamePasswordCredentialType { + requiredLabels := resources.BuildLabelsForWatchedResources(jenkins) + requiredLabels[JenkinsCredentialTypeLabelName] = string(seedJob.JenkinsCredentialType) + + secret := &corev1.Secret{} + namespaceName := types.NamespacedName{Namespace: jenkins.ObjectMeta.Namespace, Name: seedJob.CredentialID} + err := s.k8sClient.Get(context.TODO(), namespaceName, secret) + if err != nil { + return stackerr.WithStack(err) + } + + if !resources.VerifyIfLabelsAreSet(secret, requiredLabels) { + secret.ObjectMeta.Labels = requiredLabels + err = stackerr.WithStack(s.k8sClient.Update(context.TODO(), secret)) + if err != nil { + return err + } + } + } + } + + return nil +} + // buildJobs is responsible for running jenkins builds which configures jenkins seed jobs and deploy keys func (s *SeedJobs) buildJobs(jenkins *v1alpha1.Jenkins) (done bool, err error) { allDone := true - seedJobs := jenkins.Spec.SeedJobs - for _, seedJob := range seedJobs { - privateKey, err := s.privateKeyFromSecret(jenkins.Namespace, seedJob) + for _, seedJob := range jenkins.Spec.SeedJobs { + credentialValue, err := s.credentialValue(jenkins.Namespace, seedJob) if err != nil { return false, err } parameters := map[string]string{ - deployKeyIDParameterName: seedJob.ID, - privateKeyParameterName: privateKey, + idParameterName: seedJob.ID, + credentialIDParameterName: seedJob.CredentialID, repositoryURLParameterName: seedJob.RepositoryURL, repositoryBranchParameterName: seedJob.RepositoryBranch, targetsParameterName: seedJob.Targets, @@ -92,8 +137,9 @@ func (s *SeedJobs) buildJobs(jenkins *v1alpha1.Jenkins) (done bool, err error) { } hash := sha256.New() - hash.Write([]byte(parameters[deployKeyIDParameterName])) - hash.Write([]byte(parameters[privateKeyParameterName])) + hash.Write([]byte(parameters[idParameterName])) + hash.Write([]byte(parameters[credentialIDParameterName])) + hash.Write([]byte(credentialValue)) hash.Write([]byte(parameters[repositoryURLParameterName])) hash.Write([]byte(parameters[repositoryBranchParameterName])) hash.Write([]byte(parameters[targetsParameterName])) @@ -112,21 +158,23 @@ func (s *SeedJobs) buildJobs(jenkins *v1alpha1.Jenkins) (done bool, err error) { return allDone, nil } -// privateKeyFromSecret it's utility function which extracts deploy key from the kubernetes secret -func (s *SeedJobs) privateKeyFromSecret(namespace string, seedJob v1alpha1.SeedJob) (string, error) { - if seedJob.PrivateKey.SecretKeyRef != nil { - deployKeySecret := &v1.Secret{} - namespaceName := types.NamespacedName{Namespace: namespace, Name: seedJob.PrivateKey.SecretKeyRef.Name} - err := s.k8sClient.Get(context.TODO(), namespaceName, deployKeySecret) +func (s *SeedJobs) credentialValue(namespace string, seedJob v1alpha1.SeedJob) (string, error) { + if seedJob.JenkinsCredentialType == v1alpha1.BasicSSHCredentialType || seedJob.JenkinsCredentialType == v1alpha1.UsernamePasswordCredentialType { + secret := &corev1.Secret{} + namespaceName := types.NamespacedName{Namespace: namespace, Name: seedJob.CredentialID} + err := s.k8sClient.Get(context.TODO(), namespaceName, secret) if err != nil { return "", err } - return string(deployKeySecret.Data[seedJob.PrivateKey.SecretKeyRef.Key]), nil + + if seedJob.JenkinsCredentialType == v1alpha1.BasicSSHCredentialType { + return string(secret.Data[PrivateKeySecretKey]), nil + } + return string(secret.Data[UsernameSecretKey]) + string(secret.Data[PasswordSecretKey]), nil } return "", nil } -// FIXME(antoniaklja) use mask-password plugin for params.PRIVATE_KEY // seedJobConfigXML this is the XML representation of seed job var seedJobConfigXML = ` @@ -137,15 +185,16 @@ var seedJobConfigXML = ` - ` + deployKeyIDParameterName + ` + ` + idParameterName + ` false - ` + privateKeyParameterName + ` + ` + credentialIDParameterName + ` + false ` + repositoryURLParameterName + ` @@ -175,11 +224,7 @@ var seedJobConfigXML = ` - false diff --git a/pkg/controller/jenkins/configuration/user/seedjobs/seedjobs_test.go b/pkg/controller/jenkins/configuration/user/seedjobs/seedjobs_test.go index 34e35214..d0eb7466 100644 --- a/pkg/controller/jenkins/configuration/user/seedjobs/seedjobs_test.go +++ b/pkg/controller/jenkins/configuration/user/seedjobs/seedjobs_test.go @@ -134,11 +134,12 @@ func jenkinsCustomResource() *v1alpha1.Jenkins { }, SeedJobs: []v1alpha1.SeedJob{ { - ID: "jenkins-operator-e2e", - Targets: "cicd/jobs/*.jenkins", - Description: "Jenkins Operator e2e tests repository", - RepositoryBranch: "master", - RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git", + ID: "jenkins-operator-e2e", + JenkinsCredentialType: v1alpha1.NoJenkinsCredentialCredentialType, + Targets: "cicd/jobs/*.jenkins", + Description: "Jenkins Operator e2e tests repository", + RepositoryBranch: "master", + RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git", }, }, }, diff --git a/pkg/controller/jenkins/configuration/user/validate.go b/pkg/controller/jenkins/configuration/user/validate.go index 224d9764..fd25ec5b 100644 --- a/pkg/controller/jenkins/configuration/user/validate.go +++ b/pkg/controller/jenkins/configuration/user/validate.go @@ -8,8 +8,10 @@ import ( "strings" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkinsio/v1alpha1" + "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/user/seedjobs" "github.com/jenkinsci/kubernetes-operator/pkg/log" + "github.com/go-logr/logr" stackerr "github.com/pkg/errors" "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -28,45 +30,69 @@ func (r *ReconcileUserConfiguration) Validate(jenkins *v1alpha1.Jenkins) (bool, func (r *ReconcileUserConfiguration) validateSeedJobs(jenkins *v1alpha1.Jenkins) (bool, error) { valid := true + + // TODO id must be unique if jenkins.Spec.SeedJobs != nil { for _, seedJob := range jenkins.Spec.SeedJobs { logger := r.logger.WithValues("seedJob", fmt.Sprintf("%+v", seedJob)).V(log.VWarn) - // validate seed job id is not empty if len(seedJob.ID) == 0 { - logger.Info("seed job id can't be empty") + logger.Info("id can't be empty") + valid = false + } + + if len(seedJob.RepositoryBranch) == 0 { + logger.Info("repository branch can't be empty") + valid = false + } + + if len(seedJob.RepositoryURL) == 0 { + logger.Info("repository URL branch can't be empty") + valid = false + } + + if len(seedJob.Targets) == 0 { + logger.Info("targets can't be empty") + valid = false + } + + if _, ok := v1alpha1.AllowedJenkinsCredentialMap[string(seedJob.JenkinsCredentialType)]; !ok { + logger.Info("unknown credential type") + return false, nil + } + + if (seedJob.JenkinsCredentialType == v1alpha1.BasicSSHCredentialType || + seedJob.JenkinsCredentialType == v1alpha1.UsernamePasswordCredentialType) && len(seedJob.CredentialID) == 0 { + logger.Info("credential ID can't be empty") valid = false } // validate repository url match private key - if strings.Contains(seedJob.RepositoryURL, "git@") { - if seedJob.PrivateKey.SecretKeyRef == nil { - logger.Info("private key can't be empty while using ssh repository url") - valid = false - } + if strings.Contains(seedJob.RepositoryURL, "git@") && seedJob.JenkinsCredentialType == v1alpha1.NoJenkinsCredentialCredentialType { + logger.Info("Jenkins credential must be set while using ssh repository url") + valid = false } - // validate private key from secret - if seedJob.PrivateKey.SecretKeyRef != nil { - deployKeySecret := &v1.Secret{} - namespaceName := types.NamespacedName{Namespace: jenkins.Namespace, Name: seedJob.PrivateKey.SecretKeyRef.Name} - err := r.k8sClient.Get(context.TODO(), namespaceName, deployKeySecret) + if seedJob.JenkinsCredentialType == v1alpha1.BasicSSHCredentialType || seedJob.JenkinsCredentialType == v1alpha1.UsernamePasswordCredentialType { + secret := &v1.Secret{} + namespaceName := types.NamespacedName{Namespace: jenkins.Namespace, Name: seedJob.CredentialID} + err := r.k8sClient.Get(context.TODO(), namespaceName, secret) if err != nil && apierrors.IsNotFound(err) { - logger.Info("secret not found") - valid = false + logger.Info(fmt.Sprintf("required secret '%s' with Jenkins credential not found", seedJob.CredentialID)) + return false, nil } else if err != nil { return false, stackerr.WithStack(err) } - privateKey := string(deployKeySecret.Data[seedJob.PrivateKey.SecretKeyRef.Key]) - if privateKey == "" { - logger.Info("private key is empty") - valid = false + if seedJob.JenkinsCredentialType == v1alpha1.BasicSSHCredentialType { + if ok := validateBasicSSHSecret(logger, *secret); !ok { + valid = false + } } - - if err := validatePrivateKey(privateKey); err != nil { - logger.Info(fmt.Sprintf("private key is invalid: %s", err)) - valid = false + if seedJob.JenkinsCredentialType == v1alpha1.UsernamePasswordCredentialType { + if ok := validateUsernamePasswordSecret(logger, *secret); !ok { + valid = false + } } } } @@ -74,6 +100,59 @@ func (r *ReconcileUserConfiguration) validateSeedJobs(jenkins *v1alpha1.Jenkins) return valid, nil } +func validateBasicSSHSecret(logger logr.InfoLogger, secret v1.Secret) bool { + valid := true + username, exists := secret.Data[seedjobs.UsernameSecretKey] + if !exists { + logger.Info(fmt.Sprintf("required data '%s' not found in secret '%s'", seedjobs.UsernameSecretKey, secret.ObjectMeta.Name)) + valid = false + } + if len(username) == 0 { + logger.Info(fmt.Sprintf("required data '%s' is empty in secret '%s'", seedjobs.UsernameSecretKey, secret.ObjectMeta.Name)) + valid = false + } + + privateKey, exists := secret.Data[seedjobs.PrivateKeySecretKey] + if !exists { + logger.Info(fmt.Sprintf("required data '%s' not found in secret '%s'", seedjobs.PrivateKeySecretKey, secret.ObjectMeta.Name)) + valid = false + } + if len(string(privateKey)) == 0 { + logger.Info(fmt.Sprintf("required data '%s' not found in secret '%s'", seedjobs.PrivateKeySecretKey, secret.ObjectMeta.Name)) + return false + } + if err := validatePrivateKey(string(privateKey)); err != nil { + logger.Info(fmt.Sprintf("private key '%s' invalid in secret '%s': %s", seedjobs.PrivateKeySecretKey, secret.ObjectMeta.Name, err)) + valid = false + } + + return valid +} + +func validateUsernamePasswordSecret(logger logr.InfoLogger, secret v1.Secret) bool { + valid := true + username, exists := secret.Data[seedjobs.UsernameSecretKey] + if !exists { + logger.Info(fmt.Sprintf("required data '%s' not found in secret '%s'", seedjobs.UsernameSecretKey, secret.ObjectMeta.Name)) + valid = false + } + if len(username) == 0 { + logger.Info(fmt.Sprintf("required data '%s' is empty in secret '%s'", seedjobs.UsernameSecretKey, secret.ObjectMeta.Name)) + valid = false + } + password, exists := secret.Data[seedjobs.PasswordSecretKey] + if !exists { + logger.Info(fmt.Sprintf("required data '%s' not found in secret '%s'", seedjobs.PasswordSecretKey, secret.ObjectMeta.Name)) + valid = false + } + if len(password) == 0 { + logger.Info(fmt.Sprintf("required data '%s' is empty in secret '%s'", seedjobs.PasswordSecretKey, secret.ObjectMeta.Name)) + valid = false + } + + return valid +} + func validatePrivateKey(privateKey string) error { block, _ := pem.Decode([]byte(privateKey)) if block == nil { diff --git a/pkg/controller/jenkins/configuration/user/validate_test.go b/pkg/controller/jenkins/configuration/user/validate_test.go index 6c8c2324..bf700423 100644 --- a/pkg/controller/jenkins/configuration/user/validate_test.go +++ b/pkg/controller/jenkins/configuration/user/validate_test.go @@ -2,10 +2,10 @@ package user import ( "context" - "fmt" "testing" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkinsio/v1alpha1" + "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/user/seedjobs" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" @@ -40,8 +40,7 @@ JjihnIfoDu9MfU24GWDw49wGPTn+eI7GQC+8yxGg7fd24kohHSaCowoW16pbYVco 6iLr5rkCgYBt0bcYJ3AOTH0UXS8kvJvnyce/RBIAMoUABwvdkZt9r5B4UzsoLq5e WrrU6fSRsE6lSsBd83pOAQ46tv+vntQ+0EihD9/0INhkQM99lBw1TFdFTgGSAs1e ns4JGP6f5uIuwqu/nbqPqMyDovjkGbX2znuGBcvki90Pi97XL7MMWw== ------END RSA PRIVATE KEY----- -` +-----END RSA PRIVATE KEY-----` var fakeInvalidPrivateKey = `-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEArK4ld6i2iqW6L3jaTZaKD/v7PjDn+Ik9MXp+kvLcUw/+wEGm @@ -50,189 +49,398 @@ SwiLd8TWAvXkxdXm8fDOGAZbYK2alMV+M+9E2OpZsBUCxmb/3FAofF6JccKoJOH8 ` func TestValidateSeedJobs(t *testing.T) { - data := []struct { - description string - jenkins *v1alpha1.Jenkins - secret *corev1.Secret - expectedResult bool - }{ - { - description: "Valid with public repository and without private key", - jenkins: &v1alpha1.Jenkins{ - Spec: v1alpha1.JenkinsSpec{ - SeedJobs: []v1alpha1.SeedJob{ - { - ID: "jenkins-operator-e2e", - Targets: "cicd/jobs/*.jenkins", - Description: "Jenkins Operator e2e tests repository", - RepositoryBranch: "master", - RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git", - }, - }, - }, - }, - expectedResult: true, - }, - { - description: "Invalid without id", - jenkins: &v1alpha1.Jenkins{ - Spec: v1alpha1.JenkinsSpec{ - SeedJobs: []v1alpha1.SeedJob{ - { - Targets: "cicd/jobs/*.jenkins", - Description: "Jenkins Operator e2e tests repository", - RepositoryBranch: "master", - RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git", - }, - }, - }, - }, - expectedResult: false, - }, - { - description: "Valid with private key and secret", - jenkins: &v1alpha1.Jenkins{ - Spec: v1alpha1.JenkinsSpec{ - SeedJobs: []v1alpha1.SeedJob{ - { - ID: "jenkins-operator-e2e", - Targets: "cicd/jobs/*.jenkins", - Description: "Jenkins Operator e2e tests repository", - RepositoryBranch: "master", - RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git", - PrivateKey: v1alpha1.PrivateKey{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: "deploy-keys", - }, - Key: "jenkins-operator-e2e", - }, - }, - }, - }, - }, - }, - secret: &corev1.Secret{ - TypeMeta: metav1.TypeMeta{ - Kind: "Secret", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "deploy-keys", - Namespace: "default", - }, - Data: map[string][]byte{ - "jenkins-operator-e2e": []byte(fakePrivateKey), - }, - }, - expectedResult: true, - }, - { - description: "Invalid private key in secret", - jenkins: &v1alpha1.Jenkins{ - Spec: v1alpha1.JenkinsSpec{ - SeedJobs: []v1alpha1.SeedJob{ - { - ID: "jenkins-operator-e2e", - Targets: "cicd/jobs/*.jenkins", - Description: "Jenkins Operator e2e tests repository", - RepositoryBranch: "master", - RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git", - PrivateKey: v1alpha1.PrivateKey{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: "deploy-keys", - }, - Key: "jenkins-operator-e2e", - }, - }, - }, - }, - }, - }, - secret: &corev1.Secret{ - TypeMeta: metav1.TypeMeta{ - Kind: "Secret", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "deploy-keys", - Namespace: "default", - }, - Data: map[string][]byte{ - "jenkins-operator-e2e": []byte(fakeInvalidPrivateKey), - }, - }, - expectedResult: false, - }, - { - description: "Invalid with PrivateKey and empty Secret data", - jenkins: &v1alpha1.Jenkins{ - Spec: v1alpha1.JenkinsSpec{ - SeedJobs: []v1alpha1.SeedJob{ - { - ID: "jenkins-operator-e2e", - Targets: "cicd/jobs/*.jenkins", - Description: "Jenkins Operator e2e tests repository", - RepositoryBranch: "master", - RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git", - PrivateKey: v1alpha1.PrivateKey{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: "deploy-keys", - }, - Key: "jenkins-operator-e2e", - }, - }, - }, - }, - }, - }, - secret: &corev1.Secret{ - TypeMeta: metav1.TypeMeta{ - Kind: "Secret", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "deploy-keys", - Namespace: "default", - }, - Data: map[string][]byte{ - "jenkins-operator-e2e": []byte(""), - }, - }, - expectedResult: false, - }, - { - description: "Invalid with ssh RepositoryURL and empty PrivateKey", - jenkins: &v1alpha1.Jenkins{ - Spec: v1alpha1.JenkinsSpec{ - SeedJobs: []v1alpha1.SeedJob{ - { - ID: "jenkins-operator-e2e", - Targets: "cicd/jobs/*.jenkins", - Description: "Jenkins Operator e2e tests repository", - RepositoryBranch: "master", - RepositoryURL: "git@github.com:jenkinsci/kubernetes-operator.git", - }, - }, - }, - }, - expectedResult: false, - }, + secretTypeMeta := metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", } + secretObjectMeta := metav1.ObjectMeta{ + Name: "deploy-keys", + Namespace: "default", + } + t.Run("Valid with public repository and without private key", func(t *testing.T) { + jenkins := &v1alpha1.Jenkins{ + Spec: v1alpha1.JenkinsSpec{ + SeedJobs: []v1alpha1.SeedJob{ + { + ID: "example", + CredentialID: "jenkins-operator-e2e", + JenkinsCredentialType: v1alpha1.NoJenkinsCredentialCredentialType, + Targets: "cicd/jobs/*.jenkins", + RepositoryBranch: "master", + RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git", + }, + }, + }, + } - for _, testingData := range data { - t.Run(fmt.Sprintf("Testing '%s'", testingData.description), func(t *testing.T) { - fakeClient := fake.NewFakeClient() - if testingData.secret != nil { - err := fakeClient.Create(context.TODO(), testingData.secret) - assert.NoError(t, err) - } - userReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), nil) - result, err := userReconcileLoop.validateSeedJobs(testingData.jenkins) - assert.NoError(t, err) - assert.Equal(t, testingData.expectedResult, result) - }) - } + userReconcileLoop := New(fake.NewFakeClient(), nil, logf.ZapLogger(false), nil) + result, err := userReconcileLoop.validateSeedJobs(jenkins) + + assert.NoError(t, err) + assert.Equal(t, true, result) + }) + t.Run("Invalid without id", func(t *testing.T) { + jenkins := &v1alpha1.Jenkins{ + Spec: v1alpha1.JenkinsSpec{ + SeedJobs: []v1alpha1.SeedJob{ + { + JenkinsCredentialType: v1alpha1.NoJenkinsCredentialCredentialType, + Targets: "cicd/jobs/*.jenkins", + RepositoryBranch: "master", + RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git", + }, + }, + }, + } + + userReconcileLoop := New(fake.NewFakeClient(), nil, logf.ZapLogger(false), nil) + result, err := userReconcileLoop.validateSeedJobs(jenkins) + + assert.NoError(t, err) + assert.Equal(t, false, result) + }) + t.Run("Valid with private key and secret", func(t *testing.T) { + jenkins := &v1alpha1.Jenkins{ + Spec: v1alpha1.JenkinsSpec{ + SeedJobs: []v1alpha1.SeedJob{ + { + ID: "example", + CredentialID: "deploy-keys", + JenkinsCredentialType: v1alpha1.BasicSSHCredentialType, + Targets: "cicd/jobs/*.jenkins", + RepositoryBranch: "master", + RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git", + }, + }, + }, + } + secret := &corev1.Secret{ + TypeMeta: secretTypeMeta, + ObjectMeta: secretObjectMeta, + Data: map[string][]byte{ + seedjobs.UsernameSecretKey: []byte("username"), + seedjobs.PrivateKeySecretKey: []byte(fakePrivateKey), + }, + } + fakeClient := fake.NewFakeClient() + err := fakeClient.Create(context.TODO(), secret) + assert.NoError(t, err) + + userReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), nil) + result, err := userReconcileLoop.validateSeedJobs(jenkins) + + assert.NoError(t, err) + assert.Equal(t, true, result) + }) + t.Run("Invalid private key in secret", func(t *testing.T) { + jenkins := &v1alpha1.Jenkins{ + Spec: v1alpha1.JenkinsSpec{ + SeedJobs: []v1alpha1.SeedJob{ + { + ID: "example", + CredentialID: "deploy-keys", + JenkinsCredentialType: v1alpha1.BasicSSHCredentialType, + Targets: "cicd/jobs/*.jenkins", + RepositoryBranch: "master", + RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git", + }, + }, + }, + } + secret := &corev1.Secret{ + TypeMeta: secretTypeMeta, + ObjectMeta: secretObjectMeta, + Data: map[string][]byte{ + seedjobs.UsernameSecretKey: []byte("username"), + seedjobs.PrivateKeySecretKey: []byte(fakeInvalidPrivateKey), + }, + } + fakeClient := fake.NewFakeClient() + err := fakeClient.Create(context.TODO(), secret) + assert.NoError(t, err) + + userReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), nil) + result, err := userReconcileLoop.validateSeedJobs(jenkins) + + assert.NoError(t, err) + assert.Equal(t, false, result) + }) + t.Run("Invalid with PrivateKey and empty Secret data", func(t *testing.T) { + jenkins := &v1alpha1.Jenkins{ + Spec: v1alpha1.JenkinsSpec{ + SeedJobs: []v1alpha1.SeedJob{ + { + ID: "example", + CredentialID: "deploy-keys", + JenkinsCredentialType: v1alpha1.BasicSSHCredentialType, + Targets: "cicd/jobs/*.jenkins", + RepositoryBranch: "master", + RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git", + }, + }, + }, + } + secret := &corev1.Secret{ + TypeMeta: secretTypeMeta, + ObjectMeta: secretObjectMeta, + Data: map[string][]byte{ + seedjobs.UsernameSecretKey: []byte("username"), + seedjobs.PrivateKeySecretKey: []byte(""), + }, + } + fakeClient := fake.NewFakeClient() + err := fakeClient.Create(context.TODO(), secret) + assert.NoError(t, err) + + userReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), nil) + result, err := userReconcileLoop.validateSeedJobs(jenkins) + + assert.NoError(t, err) + assert.Equal(t, false, result) + }) + t.Run("Invalid with ssh RepositoryURL and empty PrivateKey", func(t *testing.T) { + jenkins := &v1alpha1.Jenkins{ + Spec: v1alpha1.JenkinsSpec{ + SeedJobs: []v1alpha1.SeedJob{ + { + ID: "example", + CredentialID: "jenkins-operator-e2e", + JenkinsCredentialType: v1alpha1.BasicSSHCredentialType, + Targets: "cicd/jobs/*.jenkins", + RepositoryBranch: "master", + RepositoryURL: "git@github.com:jenkinsci/kubernetes-operator.git", + }, + }, + }, + } + + userReconcileLoop := New(fake.NewFakeClient(), nil, logf.ZapLogger(false), nil) + result, err := userReconcileLoop.validateSeedJobs(jenkins) + + assert.NoError(t, err) + assert.Equal(t, false, result) + }) + t.Run("Invalid without targets", func(t *testing.T) { + jenkins := &v1alpha1.Jenkins{ + Spec: v1alpha1.JenkinsSpec{ + SeedJobs: []v1alpha1.SeedJob{ + { + ID: "example", + JenkinsCredentialType: v1alpha1.NoJenkinsCredentialCredentialType, + RepositoryBranch: "master", + RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git", + }, + }, + }, + } + + userReconcileLoop := New(fake.NewFakeClient(), nil, logf.ZapLogger(false), nil) + result, err := userReconcileLoop.validateSeedJobs(jenkins) + + assert.NoError(t, err) + assert.Equal(t, false, result) + }) + t.Run("Invalid without repository URL", func(t *testing.T) { + jenkins := &v1alpha1.Jenkins{ + Spec: v1alpha1.JenkinsSpec{ + SeedJobs: []v1alpha1.SeedJob{ + { + ID: "example", + JenkinsCredentialType: v1alpha1.NoJenkinsCredentialCredentialType, + Targets: "cicd/jobs/*.jenkins", + RepositoryBranch: "master", + }, + }, + }, + } + + userReconcileLoop := New(fake.NewFakeClient(), nil, logf.ZapLogger(false), nil) + result, err := userReconcileLoop.validateSeedJobs(jenkins) + + assert.NoError(t, err) + assert.Equal(t, false, result) + }) + t.Run("Invalid without repository branch", func(t *testing.T) { + jenkins := &v1alpha1.Jenkins{ + Spec: v1alpha1.JenkinsSpec{ + SeedJobs: []v1alpha1.SeedJob{ + { + ID: "example", + JenkinsCredentialType: v1alpha1.NoJenkinsCredentialCredentialType, + Targets: "cicd/jobs/*.jenkins", + RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git", + }, + }, + }, + } + + userReconcileLoop := New(fake.NewFakeClient(), nil, logf.ZapLogger(false), nil) + result, err := userReconcileLoop.validateSeedJobs(jenkins) + + assert.NoError(t, err) + assert.Equal(t, false, result) + }) + t.Run("Valid with username and password", func(t *testing.T) { + jenkins := &v1alpha1.Jenkins{ + Spec: v1alpha1.JenkinsSpec{ + SeedJobs: []v1alpha1.SeedJob{ + { + ID: "example", + CredentialID: "deploy-keys", + JenkinsCredentialType: v1alpha1.UsernamePasswordCredentialType, + Targets: "cicd/jobs/*.jenkins", + RepositoryBranch: "master", + RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git", + }, + }, + }, + } + secret := &corev1.Secret{ + TypeMeta: secretTypeMeta, + ObjectMeta: secretObjectMeta, + Data: map[string][]byte{ + seedjobs.UsernameSecretKey: []byte("some-username"), + seedjobs.PasswordSecretKey: []byte("some-password"), + }, + } + fakeClient := fake.NewFakeClient() + err := fakeClient.Create(context.TODO(), secret) + assert.NoError(t, err) + + userReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), nil) + result, err := userReconcileLoop.validateSeedJobs(jenkins) + + assert.NoError(t, err) + assert.Equal(t, true, result) + }) + t.Run("Invalid with empty username", func(t *testing.T) { + jenkins := &v1alpha1.Jenkins{ + Spec: v1alpha1.JenkinsSpec{ + SeedJobs: []v1alpha1.SeedJob{ + { + ID: "example", + CredentialID: "deploy-keys", + JenkinsCredentialType: v1alpha1.UsernamePasswordCredentialType, + Targets: "cicd/jobs/*.jenkins", + RepositoryBranch: "master", + RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git", + }, + }, + }, + } + secret := &corev1.Secret{ + TypeMeta: secretTypeMeta, + ObjectMeta: secretObjectMeta, + Data: map[string][]byte{ + seedjobs.UsernameSecretKey: []byte(""), + seedjobs.PasswordSecretKey: []byte("some-password"), + }, + } + fakeClient := fake.NewFakeClient() + err := fakeClient.Create(context.TODO(), secret) + assert.NoError(t, err) + + userReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), nil) + result, err := userReconcileLoop.validateSeedJobs(jenkins) + + assert.NoError(t, err) + assert.Equal(t, false, result) + }) + t.Run("Invalid with empty password", func(t *testing.T) { + jenkins := &v1alpha1.Jenkins{ + Spec: v1alpha1.JenkinsSpec{ + SeedJobs: []v1alpha1.SeedJob{ + { + ID: "example", + CredentialID: "deploy-keys", + JenkinsCredentialType: v1alpha1.UsernamePasswordCredentialType, + Targets: "cicd/jobs/*.jenkins", + RepositoryBranch: "master", + RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git", + }, + }, + }, + } + secret := &corev1.Secret{ + TypeMeta: secretTypeMeta, + ObjectMeta: secretObjectMeta, + Data: map[string][]byte{ + seedjobs.UsernameSecretKey: []byte("some-username"), + seedjobs.PasswordSecretKey: []byte(""), + }, + } + fakeClient := fake.NewFakeClient() + err := fakeClient.Create(context.TODO(), secret) + assert.NoError(t, err) + + userReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), nil) + result, err := userReconcileLoop.validateSeedJobs(jenkins) + + assert.NoError(t, err) + assert.Equal(t, false, result) + }) + t.Run("Invalid without username", func(t *testing.T) { + jenkins := &v1alpha1.Jenkins{ + Spec: v1alpha1.JenkinsSpec{ + SeedJobs: []v1alpha1.SeedJob{ + { + ID: "example", + CredentialID: "deploy-keys", + JenkinsCredentialType: v1alpha1.UsernamePasswordCredentialType, + Targets: "cicd/jobs/*.jenkins", + RepositoryBranch: "master", + RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git", + }, + }, + }, + } + secret := &corev1.Secret{ + TypeMeta: secretTypeMeta, + ObjectMeta: secretObjectMeta, + Data: map[string][]byte{ + seedjobs.PasswordSecretKey: []byte("some-password"), + }, + } + fakeClient := fake.NewFakeClient() + err := fakeClient.Create(context.TODO(), secret) + assert.NoError(t, err) + + userReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), nil) + result, err := userReconcileLoop.validateSeedJobs(jenkins) + + assert.NoError(t, err) + assert.Equal(t, false, result) + }) + t.Run("Invalid without password", func(t *testing.T) { + jenkins := &v1alpha1.Jenkins{ + Spec: v1alpha1.JenkinsSpec{ + SeedJobs: []v1alpha1.SeedJob{ + { + ID: "example", + CredentialID: "deploy-keys", + JenkinsCredentialType: v1alpha1.UsernamePasswordCredentialType, + Targets: "cicd/jobs/*.jenkins", + RepositoryBranch: "master", + RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git", + }, + }, + }, + } + secret := &corev1.Secret{ + TypeMeta: secretTypeMeta, + ObjectMeta: secretObjectMeta, + Data: map[string][]byte{ + seedjobs.UsernameSecretKey: []byte("some-username"), + }, + } + fakeClient := fake.NewFakeClient() + err := fakeClient.Create(context.TODO(), secret) + assert.NoError(t, err) + + userReconcileLoop := New(fakeClient, nil, logf.ZapLogger(false), nil) + result, err := userReconcileLoop.validateSeedJobs(jenkins) + + assert.NoError(t, err) + assert.Equal(t, false, result) + }) } diff --git a/pkg/controller/jenkins/jobs/jobs_test.go b/pkg/controller/jenkins/jobs/jobs_test.go index b49b3061..88582259 100644 --- a/pkg/controller/jenkins/jobs/jobs_test.go +++ b/pkg/controller/jenkins/jobs/jobs_test.go @@ -402,11 +402,12 @@ func jenkinsCustomResource() *v1alpha1.Jenkins { }, SeedJobs: []v1alpha1.SeedJob{ { - ID: "jenkins-operator-e2e", - Targets: "cicd/jobs/*.jenkins", - Description: "Jenkins Operator e2e tests repository", - RepositoryBranch: "master", - RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git", + ID: "jenkins-operator-e2e", + JenkinsCredentialType: v1alpha1.NoJenkinsCredentialCredentialType, + Targets: "cicd/jobs/*.jenkins", + Description: "Jenkins Operator e2e tests repository", + RepositoryBranch: "master", + RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git", }, }, }, diff --git a/test/e2e/configuration_test.go b/test/e2e/configuration_test.go index d71f693f..c32676e4 100644 --- a/test/e2e/configuration_test.go +++ b/test/e2e/configuration_test.go @@ -5,12 +5,10 @@ import ( "fmt" "reflect" "testing" - "time" "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/configuration/user/seedjobs" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/plugins" "github.com/bndr/gojenkins" @@ -19,8 +17,6 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/wait" ) func TestConfiguration(t *testing.T) { @@ -33,14 +29,24 @@ func TestConfiguration(t *testing.T) { numberOfExecutors := 6 systemMessage := "Configuration as Code integration works!!!" systemMessageEnvName := "SYSTEM_MESSAGE" - jenkinsCredentialName := "kubernetes-credentials-provider-plugin" + mySeedJob := seedJobConfig{ + SeedJob: v1alpha1.SeedJob{ + ID: "jenkins-operator", + CredentialID: "jenkins-operator", + JenkinsCredentialType: v1alpha1.NoJenkinsCredentialCredentialType, + Targets: "cicd/jobs/*.jenkins", + Description: "Jenkins Operator repository", + RepositoryBranch: "master", + RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git", + }, + } // base createUserConfigurationSecret(t, jenkinsCRName, namespace, systemMessageEnvName, systemMessage) createUserConfigurationConfigMap(t, jenkinsCRName, namespace, numberOfExecutors, fmt.Sprintf("${%s}", systemMessageEnvName)) - jenkins := createJenkinsCR(t, jenkinsCRName, namespace) + jenkins := createJenkinsCR(t, jenkinsCRName, namespace, &[]v1alpha1.SeedJob{mySeedJob.SeedJob}) createDefaultLimitsForContainersInNamespace(t, namespace) - createKubernetesCredentialsProviderSecret(t, namespace, jenkinsCredentialName) + createKubernetesCredentialsProviderSecret(t, namespace, mySeedJob) waitForJenkinsBaseConfigurationToComplete(t, jenkins) verifyJenkinsMasterPodAttributes(t, jenkins) @@ -49,9 +55,8 @@ func TestConfiguration(t *testing.T) { // user waitForJenkinsUserConfigurationToComplete(t, jenkins) - verifyJenkinsSeedJobs(t, client, jenkins) verifyUserConfiguration(t, client, numberOfExecutors, systemMessage) - verifyIfJenkinsCredentialExists(t, client, jenkinsCredentialName) + verifyJenkinsSeedJobs(t, client, []seedJobConfig{mySeedJob}) } func createUserConfigurationSecret(t *testing.T, jenkinsCRName string, namespace string, systemMessageEnvName, systemMessage string) { @@ -71,30 +76,6 @@ func createUserConfigurationSecret(t *testing.T, jenkinsCRName string, namespace } } -func createKubernetesCredentialsProviderSecret(t *testing.T, namespace, name string) { - secret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - Annotations: map[string]string{ - "jenkins.io/credentials-description": "credentials from Kubernetes", - }, - Labels: map[string]string{ - "jenkins.io/credentials-type": "usernamePassword", - }, - }, - StringData: map[string]string{ - "username": "user", - "password": "pass", - }, - } - - t.Logf("Secret for Kubernetes credentials provider plugin %+v", *secret) - if err := framework.Global.Client.Create(context.TODO(), secret, nil); err != nil { - t.Fatal(err) - } -} - func createUserConfigurationConfigMap(t *testing.T, jenkinsCRName string, namespace string, numberOfExecutors int, systemMessage string) { userConfiguration := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ @@ -223,43 +204,6 @@ func isPluginValid(plugins *gojenkins.Plugins, requiredPlugin plugins.Plugin) (* return p, requiredPlugin.Version == p.Version } -func verifyJenkinsSeedJobs(t *testing.T, client jenkinsclient.Jenkins, jenkins *v1alpha1.Jenkins) { - t.Logf("Attempting to get configure seed job status '%v'", seedjobs.ConfigureSeedJobsName) - - configureSeedJobs, err := client.GetJob(seedjobs.ConfigureSeedJobsName) - assert.NoError(t, err) - assert.NotNil(t, configureSeedJobs) - build, err := configureSeedJobs.GetLastSuccessfulBuild() - assert.NoError(t, err) - assert.NotNil(t, build) - - seedJobName := "jenkins-operator-configure-seed-job" - t.Logf("Attempting to verify if seed job has been created '%v'", seedJobName) - seedJob, err := client.GetJob(seedJobName) - assert.NoError(t, err) - assert.NotNil(t, seedJob) - - build, err = seedJob.GetLastSuccessfulBuild() - assert.NoError(t, err) - assert.NotNil(t, build) - - err = framework.Global.Client.Get(context.TODO(), types.NamespacedName{Namespace: jenkins.Namespace, Name: jenkins.Name}, jenkins) - assert.NoError(t, err, "couldn't get jenkins custom resource") - assert.NotNil(t, jenkins.Status.Builds) - assert.NotEmpty(t, jenkins.Status.Builds) - - jobCreatedByDSLPluginName := "build-jenkins-operator" - err = wait.Poll(time.Second*10, time.Minute*2, func() (bool, error) { - t.Logf("Attempting to verify if job '%s' has been created ", jobCreatedByDSLPluginName) - seedJob, err := client.GetJob(jobCreatedByDSLPluginName) - if err != nil || seedJob == nil { - return false, nil - } - return true, nil - }) - assert.NoError(t, err) -} - func verifyUserConfiguration(t *testing.T, jenkinsClient jenkinsclient.Jenkins, amountOfExecutors int, systemMessage string) { checkConfigurationViaGroovyScript := fmt.Sprintf(` if (!new Integer(%d).equals(Jenkins.instance.numExecutors)) { @@ -275,32 +219,3 @@ if (!"%s".equals(Jenkins.instance.systemMessage)) { logs, err = jenkinsClient.ExecuteScript(checkConfigurationAsCode) assert.NoError(t, err, logs) } - -func verifyIfJenkinsCredentialExists(t *testing.T, jenkinsClient jenkinsclient.Jenkins, credentialName string) { - groovyScriptFmt := `import com.cloudbees.plugins.credentials.Credentials - -Set allCredentials = new HashSet(); - -def creds = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials( - com.cloudbees.plugins.credentials.Credentials.class -); - -allCredentials.addAll(creds) - -Jenkins.instance.getAllItems(com.cloudbees.hudson.plugins.folder.Folder.class).each{ f -> - creds = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials( - com.cloudbees.plugins.credentials.Credentials.class, f) - allCredentials.addAll(creds) -} - -def found = false -for (c in allCredentials) { - if("%s".equals(c.id)) found = true -} -if(!found) { - throw new Exception("Expected credential not found") -}` - groovyScript := fmt.Sprintf(groovyScriptFmt, credentialName) - logs, err := jenkinsClient.ExecuteScript(groovyScript) - assert.NoError(t, err, logs) -} diff --git a/test/e2e/jenkins.go b/test/e2e/jenkins.go index 85abda5b..66ae8574 100644 --- a/test/e2e/jenkins.go +++ b/test/e2e/jenkins.go @@ -58,7 +58,12 @@ func createJenkinsAPIClient(jenkins *v1alpha1.Jenkins) (jenkinsclient.Jenkins, e ) } -func createJenkinsCR(t *testing.T, name, namespace string) *v1alpha1.Jenkins { +func createJenkinsCR(t *testing.T, name, namespace string, seedJob *[]v1alpha1.SeedJob) *v1alpha1.Jenkins { + var seedJobs []v1alpha1.SeedJob + if seedJob != nil { + seedJobs = append(seedJobs, *seedJob...) + } + jenkins := &v1alpha1.Jenkins{ ObjectMeta: metav1.ObjectMeta{ Name: name, @@ -80,16 +85,7 @@ func createJenkinsCR(t *testing.T, name, namespace string) *v1alpha1.Jenkins { }, }, }, - //TODO(bantoniak) add seed job with private key - SeedJobs: []v1alpha1.SeedJob{ - { - ID: "jenkins-operator", - Targets: "cicd/jobs/*.jenkins", - Description: "Jenkins Operator repository", - RepositoryBranch: "master", - RepositoryURL: "https://github.com/jenkinsci/kubernetes-operator.git", - }, - }, + SeedJobs: seedJobs, }, } diff --git a/test/e2e/main_test.go b/test/e2e/main_test.go index d3c89737..8143a940 100644 --- a/test/e2e/main_test.go +++ b/test/e2e/main_test.go @@ -1,6 +1,7 @@ package e2e import ( + "flag" "testing" "github.com/jenkinsci/kubernetes-operator/pkg/apis" @@ -14,10 +15,16 @@ import ( ) const ( - jenkinsOperatorDeploymentName = constants.OperatorName + jenkinsOperatorDeploymentName = constants.OperatorName + seedJobConfigurationParameterName = "seed-job-config" +) + +var ( + seedJobConfigurationFile *string ) func TestMain(m *testing.M) { + seedJobConfigurationFile = flag.String(seedJobConfigurationParameterName, "", "path to seed job config") f.MainEntry(m) } diff --git a/test/e2e/restart_test.go b/test/e2e/restart_test.go index 46084958..04c79a12 100644 --- a/test/e2e/restart_test.go +++ b/test/e2e/restart_test.go @@ -21,7 +21,7 @@ func TestJenkinsMasterPodRestart(t *testing.T) { // Deletes test namespace defer ctx.Cleanup() - jenkins := createJenkinsCR(t, "e2e", namespace) + jenkins := createJenkinsCR(t, "e2e", namespace, nil) waitForJenkinsBaseConfigurationToComplete(t, jenkins) restartJenkinsMasterPod(t, jenkins) waitForRecreateJenkinsMasterPod(t, jenkins) @@ -37,7 +37,7 @@ func TestSafeRestart(t *testing.T) { jenkinsCRName := "e2e" configureAuthorizationToUnSecure(t, jenkinsCRName, namespace) - jenkins := createJenkinsCR(t, jenkinsCRName, namespace) + jenkins := createJenkinsCR(t, jenkinsCRName, namespace, nil) waitForJenkinsBaseConfigurationToComplete(t, jenkins) waitForJenkinsUserConfigurationToComplete(t, jenkins) jenkinsClient := verifyJenkinsAPIConnection(t, jenkins) diff --git a/test/e2e/seedjobs_test.go b/test/e2e/seedjobs_test.go new file mode 100644 index 00000000..3fa4667f --- /dev/null +++ b/test/e2e/seedjobs_test.go @@ -0,0 +1,153 @@ +package e2e + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "testing" + "time" + + "github.com/jenkinsci/kubernetes-operator/internal/try" + "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/user/seedjobs" + + framework "github.com/operator-framework/operator-sdk/pkg/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type seedJobConfig struct { + v1alpha1.SeedJob + JobNames []string `json:"jobNames,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + PrivateKey string `json:"privateKey,omitempty"` +} + +type seedJobsConfig struct { + SeedJobs []seedJobConfig `json:"seedJobs,omitempty"` +} + +func TestSeedJobs(t *testing.T) { + t.Parallel() + if seedJobConfigurationFile == nil || len(*seedJobConfigurationFile) == 0 { + t.Skipf("Skipping test because flag '%+v' is not set", seedJobConfigurationFile) + } + seedJobsConfig := loadSeedJobsConfig(t) + namespace, ctx := setupTest(t) + // Deletes test namespace + defer ctx.Cleanup() + + jenkinsCRName := "e2e" + var seedJobs []v1alpha1.SeedJob + + // base + for _, seedJobConfig := range seedJobsConfig.SeedJobs { + createKubernetesCredentialsProviderSecret(t, namespace, seedJobConfig) + seedJobs = append(seedJobs, seedJobConfig.SeedJob) + } + jenkins := createJenkinsCR(t, jenkinsCRName, namespace, &seedJobs) + waitForJenkinsBaseConfigurationToComplete(t, jenkins) + + verifyJenkinsMasterPodAttributes(t, jenkins) + client := verifyJenkinsAPIConnection(t, jenkins) + verifyPlugins(t, client, jenkins) + + // user + waitForJenkinsUserConfigurationToComplete(t, jenkins) + verifyJenkinsSeedJobs(t, client, seedJobsConfig.SeedJobs) +} + +func loadSeedJobsConfig(t *testing.T) seedJobsConfig { + jsonFile, err := os.Open(*seedJobConfigurationFile) + assert.NoError(t, err) + defer func() { _ = jsonFile.Close() }() + + byteValue, err := ioutil.ReadAll(jsonFile) + assert.NoError(t, err) + + var result seedJobsConfig + err = json.Unmarshal([]byte(byteValue), &result) + assert.NoError(t, err) + assert.NotEmpty(t, result.SeedJobs) + return result +} + +func createKubernetesCredentialsProviderSecret(t *testing.T, namespace string, config seedJobConfig) { + if config.JenkinsCredentialType == v1alpha1.NoJenkinsCredentialCredentialType { + return + } + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: config.CredentialID, + Namespace: namespace, + Annotations: map[string]string{ + "jenkins.io/credentials-description": "credentials from Kubernetes " + config.ID, + }, + Labels: map[string]string{ + seedjobs.JenkinsCredentialTypeLabelName: string(config.CredentialID), + }, + }, + StringData: map[string]string{ + seedjobs.UsernameSecretKey: config.Username, + seedjobs.PasswordSecretKey: config.Password, + seedjobs.PrivateKeySecretKey: config.PrivateKey, + }, + } + + err := framework.Global.Client.Create(context.TODO(), secret, nil) + require.NoError(t, err) +} + +func verifyJenkinsSeedJobs(t *testing.T, jenkinsClient jenkinsclient.Jenkins, seedJobs []seedJobConfig) { + var err error + for _, seedJob := range seedJobs { + if seedJob.JenkinsCredentialType == v1alpha1.BasicSSHCredentialType || seedJob.JenkinsCredentialType == v1alpha1.UsernamePasswordCredentialType { + err = verifyIfJenkinsCredentialExists(jenkinsClient, seedJob.CredentialID) + assert.NoErrorf(t, err, "Jenkins credential '%s' not created for seed job ID '%s'", seedJob.CredentialID, seedJob.ID) + } + + for _, requireJobName := range seedJob.JobNames { + err = try.Until(func() (end bool, err error) { + _, err = jenkinsClient.GetJob(requireJobName) + return err == nil, err + }, time.Second*2, time.Minute*2) + assert.NoErrorf(t, err, "Jenkins job '%s' not created by seed job ID '%s'", requireJobName, seedJob.ID) + } + } +} + +func verifyIfJenkinsCredentialExists(jenkinsClient jenkinsclient.Jenkins, credentialName string) error { + groovyScriptFmt := `import com.cloudbees.plugins.credentials.Credentials + +Set allCredentials = new HashSet(); + +def creds = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials( + com.cloudbees.plugins.credentials.Credentials.class +); + +allCredentials.addAll(creds) + +Jenkins.instance.getAllItems(com.cloudbees.hudson.plugins.folder.Folder.class).each{ f -> + creds = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials( + com.cloudbees.plugins.credentials.Credentials.class, f) + allCredentials.addAll(creds) +} + +def found = false +for (c in allCredentials) { + if("%s".equals(c.id)) found = true +} +if(!found) { + throw new Exception("Expected credential not found") +}` + groovyScript := fmt.Sprintf(groovyScriptFmt, credentialName) + _, err := jenkinsClient.ExecuteScript(groovyScript) + return err +}