#14 Add username/password authentication for seed jobs

This commit is contained in:
Tomasz Sęk 2019-03-15 18:30:07 +01:00
parent 6285f22170
commit 1d10d629ce
No known key found for this signature in database
GPG Key ID: DC356D23F6A644D0
17 changed files with 826 additions and 440 deletions

View File

@ -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() {

View File

@ -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
}

View File

@ -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
}

View File

@ -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,
},
}
}

View File

@ -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
}

View File

@ -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,

View File

@ -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),
},
}
}

View File

@ -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 = `
<flow-definition plugin="workflow-job@2.30">
@ -137,15 +185,16 @@ var seedJobConfigXML = `
<hudson.model.ParametersDefinitionProperty>
<parameterDefinitions>
<hudson.model.StringParameterDefinition>
<name>` + deployKeyIDParameterName + `</name>
<name>` + idParameterName + `</name>
<description></description>
<defaultValue></defaultValue>
<trim>false</trim>
</hudson.model.StringParameterDefinition>
<hudson.model.StringParameterDefinition>
<name>` + privateKeyParameterName + `</name>
<name>` + credentialIDParameterName + `</name>
<description></description>
<defaultValue></defaultValue>
<trim>false</trim>
</hudson.model.StringParameterDefinition>
<hudson.model.StringParameterDefinition>
<name>` + repositoryURLParameterName + `</name>
@ -175,11 +224,7 @@ var seedJobConfigXML = `
</hudson.model.ParametersDefinitionProperty>
</properties>
<definition class="org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition" plugin="workflow-cps@2.61">
<script>import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey
import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey.DirectEntryPrivateKeySource
import com.cloudbees.plugins.credentials.CredentialsScope
import com.cloudbees.plugins.credentials.SystemCredentialsProvider
import com.cloudbees.plugins.credentials.domains.Domain
<script>
import hudson.model.FreeStyleProject
import hudson.model.labels.LabelAtom
import hudson.plugins.git.BranchSpec
@ -190,36 +235,19 @@ import javaposse.jobdsl.plugin.ExecuteDslScripts
import javaposse.jobdsl.plugin.LookupStrategy
import javaposse.jobdsl.plugin.RemovedJobAction
import javaposse.jobdsl.plugin.RemovedViewAction
import jenkins.model.Jenkins
import javaposse.jobdsl.plugin.GlobalJobDslSecurityConfiguration
import jenkins.model.GlobalConfiguration
import static com.google.common.collect.Lists.newArrayList
// https://javadoc.jenkins.io/plugin/ssh-credentials/com/cloudbees/jenkins/plugins/sshcredentials/impl/BasicSSHUserPrivateKey.html
BasicSSHUserPrivateKey deployKeyPrivate = new BasicSSHUserPrivateKey(
CredentialsScope.GLOBAL,
&quot;${params.DEPLOY_KEY_ID}&quot;,
&quot;git&quot;,
new DirectEntryPrivateKeySource(&quot;${params.PRIVATE_KEY}&quot;),
&quot;&quot;,
&quot;${params.DEPLOY_KEY_ID}&quot;
)
// https://javadoc.jenkins.io/plugin/credentials/index.html?com/cloudbees/plugins/credentials/SystemCredentialsProvider.html
SystemCredentialsProvider.getInstance().getStore().addCredentials(Domain.global(), deployKeyPrivate)
Jenkins jenkins = Jenkins.instance
def jobDslSeedName = &quot;${params.DEPLOY_KEY_ID}-` + constants.SeedJobSuffix + `&quot;
def jobDslDeployKeyName = &quot;${params.DEPLOY_KEY_ID}&quot;
def jobDslSeedName = &quot;${params.` + idParameterName + `}-` + constants.SeedJobSuffix + `&quot;
def jobRef = jenkins.getItem(jobDslSeedName)
def repoList = GitSCM.createRepoList(&quot;${params.REPOSITORY_URL}&quot;, jobDslDeployKeyName)
def repoList = GitSCM.createRepoList(&quot;${params.` + repositoryURLParameterName + `}&quot;, &quot;${params.` + credentialIDParameterName + `}&quot;)
def gitExtensions = [new CloneOption(true, true, &quot;&quot;, 10)]
def scm = new GitSCM(
repoList,
newArrayList(new BranchSpec(&quot;${params.REPOSITORY_BRANCH}&quot;)),
newArrayList(new BranchSpec(&quot;${params.` + repositoryBranchParameterName + `}&quot;)),
false,
Collections.&lt;SubmoduleConfig&gt; emptyList(),
null,
@ -228,7 +256,7 @@ def scm = new GitSCM(
)
def executeDslScripts = new ExecuteDslScripts()
executeDslScripts.setTargets(&quot;${params.TARGETS}&quot;)
executeDslScripts.setTargets(&quot;${params.` + targetsParameterName + `}&quot;)
executeDslScripts.setSandbox(false)
executeDslScripts.setRemovedJobAction(RemovedJobAction.DELETE)
executeDslScripts.setRemovedViewAction(RemovedViewAction.DELETE)
@ -240,13 +268,11 @@ if (jobRef == null) {
}
jobRef.getBuildersList().clear()
jobRef.getBuildersList().add(executeDslScripts)
jobRef.setDisplayName(&quot;${params.SEED_JOB_DISPLAY_NAME}&quot;)
jobRef.setDisplayName(&quot;${params.` + displayNameParameterName + `}&quot;)
jobRef.setScm(scm)
// TODO don't use master executors
jobRef.setAssignedLabel(new LabelAtom(&quot;master&quot;))
// disable Job DSL script approval
GlobalConfiguration.all().get(GlobalJobDslSecurityConfiguration.class).useScriptSecurity=false
GlobalConfiguration.all().get(GlobalJobDslSecurityConfiguration.class).save()
jenkins.getQueue().schedule(jobRef)
</script>
<sandbox>false</sandbox>

View File

@ -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",
},
},
},

View File

@ -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 {

View File

@ -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)
})
}

View File

@ -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",
},
},
},

View File

@ -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<Credentials> allCredentials = new HashSet<Credentials>();
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)
}

View File

@ -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,
},
}

View File

@ -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)
}

View File

@ -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)

153
test/e2e/seedjobs_test.go Normal file
View File

@ -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<Credentials> allCredentials = new HashSet<Credentials>();
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
}