diff --git a/pkg/controller/jenkins/configuration/user/seedjobs/validate.go b/pkg/controller/jenkins/configuration/user/seedjobs/validate.go new file mode 100644 index 00000000..1626b4d8 --- /dev/null +++ b/pkg/controller/jenkins/configuration/user/seedjobs/validate.go @@ -0,0 +1,163 @@ +package seedjobs + +import ( + "context" + "crypto/x509" + "encoding/pem" + "fmt" + "strings" + + "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkinsio/v1alpha1" + "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" + "k8s.io/apimachinery/pkg/types" +) + +// ValidateSeedJobs verify seed jobs configuration +func (r *SeedJobs) 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) + + if len(seedJob.ID) == 0 { + 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@") && seedJob.JenkinsCredentialType == v1alpha1.NoJenkinsCredentialCredentialType { + logger.Info("Jenkins credential must be set while using ssh repository url") + valid = false + } + + 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(fmt.Sprintf("required secret '%s' with Jenkins credential not found", seedJob.CredentialID)) + return false, nil + } else if err != nil { + return false, stackerr.WithStack(err) + } + + if seedJob.JenkinsCredentialType == v1alpha1.BasicSSHCredentialType { + if ok := validateBasicSSHSecret(logger, *secret); !ok { + valid = false + } + } + if seedJob.JenkinsCredentialType == v1alpha1.UsernamePasswordCredentialType { + if ok := validateUsernamePasswordSecret(logger, *secret); !ok { + valid = false + } + } + } + } + } + return valid, nil +} + +func validateBasicSSHSecret(logger logr.InfoLogger, secret v1.Secret) bool { + valid := true + username, exists := secret.Data[UsernameSecretKey] + if !exists { + logger.Info(fmt.Sprintf("required data '%s' not found in secret '%s'", UsernameSecretKey, secret.ObjectMeta.Name)) + valid = false + } + if len(username) == 0 { + logger.Info(fmt.Sprintf("required data '%s' is empty in secret '%s'", UsernameSecretKey, secret.ObjectMeta.Name)) + valid = false + } + + privateKey, exists := secret.Data[PrivateKeySecretKey] + if !exists { + logger.Info(fmt.Sprintf("required data '%s' not found in secret '%s'", PrivateKeySecretKey, secret.ObjectMeta.Name)) + valid = false + } + if len(string(privateKey)) == 0 { + logger.Info(fmt.Sprintf("required data '%s' not found in secret '%s'", 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", PrivateKeySecretKey, secret.ObjectMeta.Name, err)) + valid = false + } + + return valid +} + +func validateUsernamePasswordSecret(logger logr.InfoLogger, secret v1.Secret) bool { + valid := true + username, exists := secret.Data[UsernameSecretKey] + if !exists { + logger.Info(fmt.Sprintf("required data '%s' not found in secret '%s'", UsernameSecretKey, secret.ObjectMeta.Name)) + valid = false + } + if len(username) == 0 { + logger.Info(fmt.Sprintf("required data '%s' is empty in secret '%s'", UsernameSecretKey, secret.ObjectMeta.Name)) + valid = false + } + password, exists := secret.Data[PasswordSecretKey] + if !exists { + logger.Info(fmt.Sprintf("required data '%s' not found in secret '%s'", PasswordSecretKey, secret.ObjectMeta.Name)) + valid = false + } + if len(password) == 0 { + logger.Info(fmt.Sprintf("required data '%s' is empty in secret '%s'", PasswordSecretKey, secret.ObjectMeta.Name)) + valid = false + } + + return valid +} + +func validatePrivateKey(privateKey string) error { + block, _ := pem.Decode([]byte(privateKey)) + if block == nil { + return stackerr.New("failed to decode PEM block") + } + + priv, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return stackerr.WithStack(err) + } + + err = priv.Validate() + if err != nil { + return stackerr.WithStack(err) + } + + return nil +} diff --git a/pkg/controller/jenkins/configuration/user/validate_test.go b/pkg/controller/jenkins/configuration/user/seedjobs/validate_test.go similarity index 81% rename from pkg/controller/jenkins/configuration/user/validate_test.go rename to pkg/controller/jenkins/configuration/user/seedjobs/validate_test.go index bf700423..f1ee651f 100644 --- a/pkg/controller/jenkins/configuration/user/validate_test.go +++ b/pkg/controller/jenkins/configuration/user/seedjobs/validate_test.go @@ -1,11 +1,10 @@ -package user +package seedjobs import ( "context" "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" @@ -73,8 +72,8 @@ func TestValidateSeedJobs(t *testing.T) { }, } - userReconcileLoop := New(fake.NewFakeClient(), nil, logf.ZapLogger(false), nil) - result, err := userReconcileLoop.validateSeedJobs(jenkins) + seedJobs := New(nil, fake.NewFakeClient(), logf.ZapLogger(false)) + result, err := seedJobs.ValidateSeedJobs(jenkins) assert.NoError(t, err) assert.Equal(t, true, result) @@ -93,8 +92,8 @@ func TestValidateSeedJobs(t *testing.T) { }, } - userReconcileLoop := New(fake.NewFakeClient(), nil, logf.ZapLogger(false), nil) - result, err := userReconcileLoop.validateSeedJobs(jenkins) + seedJobs := New(nil, fake.NewFakeClient(), logf.ZapLogger(false)) + result, err := seedJobs.ValidateSeedJobs(jenkins) assert.NoError(t, err) assert.Equal(t, false, result) @@ -118,16 +117,16 @@ func TestValidateSeedJobs(t *testing.T) { TypeMeta: secretTypeMeta, ObjectMeta: secretObjectMeta, Data: map[string][]byte{ - seedjobs.UsernameSecretKey: []byte("username"), - seedjobs.PrivateKeySecretKey: []byte(fakePrivateKey), + UsernameSecretKey: []byte("username"), + 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) + seedJobs := New(nil, fakeClient, logf.ZapLogger(false)) + result, err := seedJobs.ValidateSeedJobs(jenkins) assert.NoError(t, err) assert.Equal(t, true, result) @@ -151,16 +150,16 @@ func TestValidateSeedJobs(t *testing.T) { TypeMeta: secretTypeMeta, ObjectMeta: secretObjectMeta, Data: map[string][]byte{ - seedjobs.UsernameSecretKey: []byte("username"), - seedjobs.PrivateKeySecretKey: []byte(fakeInvalidPrivateKey), + UsernameSecretKey: []byte("username"), + 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) + seedJobs := New(nil, fakeClient, logf.ZapLogger(false)) + result, err := seedJobs.ValidateSeedJobs(jenkins) assert.NoError(t, err) assert.Equal(t, false, result) @@ -184,16 +183,16 @@ func TestValidateSeedJobs(t *testing.T) { TypeMeta: secretTypeMeta, ObjectMeta: secretObjectMeta, Data: map[string][]byte{ - seedjobs.UsernameSecretKey: []byte("username"), - seedjobs.PrivateKeySecretKey: []byte(""), + UsernameSecretKey: []byte("username"), + 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) + seedJobs := New(nil, fakeClient, logf.ZapLogger(false)) + result, err := seedJobs.ValidateSeedJobs(jenkins) assert.NoError(t, err) assert.Equal(t, false, result) @@ -214,8 +213,8 @@ func TestValidateSeedJobs(t *testing.T) { }, } - userReconcileLoop := New(fake.NewFakeClient(), nil, logf.ZapLogger(false), nil) - result, err := userReconcileLoop.validateSeedJobs(jenkins) + seedJobs := New(nil, fake.NewFakeClient(), logf.ZapLogger(false)) + result, err := seedJobs.ValidateSeedJobs(jenkins) assert.NoError(t, err) assert.Equal(t, false, result) @@ -234,8 +233,8 @@ func TestValidateSeedJobs(t *testing.T) { }, } - userReconcileLoop := New(fake.NewFakeClient(), nil, logf.ZapLogger(false), nil) - result, err := userReconcileLoop.validateSeedJobs(jenkins) + seedJobs := New(nil, fake.NewFakeClient(), logf.ZapLogger(false)) + result, err := seedJobs.ValidateSeedJobs(jenkins) assert.NoError(t, err) assert.Equal(t, false, result) @@ -254,8 +253,8 @@ func TestValidateSeedJobs(t *testing.T) { }, } - userReconcileLoop := New(fake.NewFakeClient(), nil, logf.ZapLogger(false), nil) - result, err := userReconcileLoop.validateSeedJobs(jenkins) + seedJobs := New(nil, fake.NewFakeClient(), logf.ZapLogger(false)) + result, err := seedJobs.ValidateSeedJobs(jenkins) assert.NoError(t, err) assert.Equal(t, false, result) @@ -274,8 +273,8 @@ func TestValidateSeedJobs(t *testing.T) { }, } - userReconcileLoop := New(fake.NewFakeClient(), nil, logf.ZapLogger(false), nil) - result, err := userReconcileLoop.validateSeedJobs(jenkins) + seedJobs := New(nil, fake.NewFakeClient(), logf.ZapLogger(false)) + result, err := seedJobs.ValidateSeedJobs(jenkins) assert.NoError(t, err) assert.Equal(t, false, result) @@ -299,16 +298,16 @@ func TestValidateSeedJobs(t *testing.T) { TypeMeta: secretTypeMeta, ObjectMeta: secretObjectMeta, Data: map[string][]byte{ - seedjobs.UsernameSecretKey: []byte("some-username"), - seedjobs.PasswordSecretKey: []byte("some-password"), + UsernameSecretKey: []byte("some-username"), + 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) + seedJobs := New(nil, fakeClient, logf.ZapLogger(false)) + result, err := seedJobs.ValidateSeedJobs(jenkins) assert.NoError(t, err) assert.Equal(t, true, result) @@ -332,16 +331,16 @@ func TestValidateSeedJobs(t *testing.T) { TypeMeta: secretTypeMeta, ObjectMeta: secretObjectMeta, Data: map[string][]byte{ - seedjobs.UsernameSecretKey: []byte(""), - seedjobs.PasswordSecretKey: []byte("some-password"), + UsernameSecretKey: []byte(""), + 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) + seedJobs := New(nil, fakeClient, logf.ZapLogger(false)) + result, err := seedJobs.ValidateSeedJobs(jenkins) assert.NoError(t, err) assert.Equal(t, false, result) @@ -365,16 +364,16 @@ func TestValidateSeedJobs(t *testing.T) { TypeMeta: secretTypeMeta, ObjectMeta: secretObjectMeta, Data: map[string][]byte{ - seedjobs.UsernameSecretKey: []byte("some-username"), - seedjobs.PasswordSecretKey: []byte(""), + UsernameSecretKey: []byte("some-username"), + 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) + seedJobs := New(nil, fakeClient, logf.ZapLogger(false)) + result, err := seedJobs.ValidateSeedJobs(jenkins) assert.NoError(t, err) assert.Equal(t, false, result) @@ -398,15 +397,15 @@ func TestValidateSeedJobs(t *testing.T) { TypeMeta: secretTypeMeta, ObjectMeta: secretObjectMeta, Data: map[string][]byte{ - seedjobs.PasswordSecretKey: []byte("some-password"), + 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) + seedJobs := New(nil, fakeClient, logf.ZapLogger(false)) + result, err := seedJobs.ValidateSeedJobs(jenkins) assert.NoError(t, err) assert.Equal(t, false, result) @@ -430,15 +429,15 @@ func TestValidateSeedJobs(t *testing.T) { TypeMeta: secretTypeMeta, ObjectMeta: secretObjectMeta, Data: map[string][]byte{ - seedjobs.UsernameSecretKey: []byte("some-username"), + 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) + seedJobs := New(nil, fakeClient, logf.ZapLogger(false)) + result, err := seedJobs.ValidateSeedJobs(jenkins) assert.NoError(t, err) assert.Equal(t, false, result) diff --git a/pkg/controller/jenkins/configuration/user/validate.go b/pkg/controller/jenkins/configuration/user/validate.go index fd25ec5b..bd8f6a69 100644 --- a/pkg/controller/jenkins/configuration/user/validate.go +++ b/pkg/controller/jenkins/configuration/user/validate.go @@ -1,173 +1,12 @@ package user import ( - "context" - "crypto/x509" - "encoding/pem" - "fmt" - "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" - "k8s.io/apimachinery/pkg/types" ) // Validate validates Jenkins CR Spec section func (r *ReconcileUserConfiguration) Validate(jenkins *v1alpha1.Jenkins) (bool, error) { - valid, err := r.validateSeedJobs(jenkins) - if !valid || err != nil { - return valid, err - } - - return true, nil -} - -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) - - if len(seedJob.ID) == 0 { - 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@") && seedJob.JenkinsCredentialType == v1alpha1.NoJenkinsCredentialCredentialType { - logger.Info("Jenkins credential must be set while using ssh repository url") - valid = false - } - - 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(fmt.Sprintf("required secret '%s' with Jenkins credential not found", seedJob.CredentialID)) - return false, nil - } else if err != nil { - return false, stackerr.WithStack(err) - } - - if seedJob.JenkinsCredentialType == v1alpha1.BasicSSHCredentialType { - if ok := validateBasicSSHSecret(logger, *secret); !ok { - valid = false - } - } - if seedJob.JenkinsCredentialType == v1alpha1.UsernamePasswordCredentialType { - if ok := validateUsernamePasswordSecret(logger, *secret); !ok { - valid = false - } - } - } - } - } - 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 { - return stackerr.New("failed to decode PEM block") - } - - priv, err := x509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { - return stackerr.WithStack(err) - } - - err = priv.Validate() - if err != nil { - return stackerr.WithStack(err) - } - - return nil + seedJobs := seedjobs.New(r.jenkinsClient, r.k8sClient, r.logger) + return seedJobs.ValidateSeedJobs(jenkins) }