package seedjobs import ( "context" "fmt" "strings" "github.com/jenkinsci/kubernetes-operator/api/v1alpha2" stackerr "github.com/pkg/errors" "golang.org/x/crypto/ssh" v1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" ) // ValidateSeedJobs verify seed jobs configuration func (s *seedJobs) ValidateSeedJobs(jenkins v1alpha2.Jenkins) ([]string, error) { messages := []string{} if msg := s.validateIfIDIsUnique(jenkins.Spec.SeedJobs); len(msg) > 0 { messages = append(messages, msg...) } for _, seedJob := range jenkins.Spec.SeedJobs { if len(seedJob.ID) == 0 { messages = append(messages, fmt.Sprintf("seedJob `%s` id can't be empty", seedJob.ID)) } if len(seedJob.RepositoryBranch) == 0 { messages = append(messages, fmt.Sprintf("seedJob `%s` repository branch can't be empty", seedJob.ID)) } if len(seedJob.RepositoryURL) == 0 { messages = append(messages, fmt.Sprintf("seedJob `%s` repository URL branch can't be empty", seedJob.ID)) } if len(seedJob.Targets) == 0 { messages = append(messages, fmt.Sprintf("seedJob `%s` targets can't be empty", seedJob.ID)) } if _, ok := v1alpha2.AllowedJenkinsCredentialMap[string(seedJob.JenkinsCredentialType)]; !ok { messages = append(messages, fmt.Sprintf("seedJob `%s` unknown credential type", seedJob.ID)) } if (seedJob.JenkinsCredentialType == v1alpha2.BasicSSHCredentialType || seedJob.JenkinsCredentialType == v1alpha2.UsernamePasswordCredentialType) && len(seedJob.CredentialID) == 0 { messages = append(messages, fmt.Sprintf("seedJob `%s` credential ID can't be empty", seedJob.ID)) } // validate repository url match private key if strings.Contains(seedJob.RepositoryURL, "git@") && seedJob.JenkinsCredentialType == v1alpha2.NoJenkinsCredentialCredentialType { messages = append(messages, fmt.Sprintf("seedJob `%s` Jenkins credential must be set while using ssh repository url", seedJob.ID)) } if seedJob.JenkinsCredentialType == v1alpha2.BasicSSHCredentialType || seedJob.JenkinsCredentialType == v1alpha2.UsernamePasswordCredentialType || seedJob.JenkinsCredentialType == v1alpha2.GithubAppCredentialType { secret := &v1.Secret{} namespaceName := types.NamespacedName{Namespace: jenkins.Namespace, Name: seedJob.CredentialID} err := s.Client.Get(context.TODO(), namespaceName, secret) if err != nil && apierrors.IsNotFound(err) { messages = append(messages, fmt.Sprintf("seedJob `%s` required secret '%s' with Jenkins credential not found", seedJob.ID, seedJob.CredentialID)) } else if err != nil { return nil, stackerr.WithStack(err) } if seedJob.JenkinsCredentialType == v1alpha2.BasicSSHCredentialType { if msg := validateBasicSSHSecret(*secret); len(msg) > 0 { for _, m := range msg { messages = append(messages, fmt.Sprintf("seedJob `%s` %s", seedJob.ID, m)) } } } if seedJob.JenkinsCredentialType == v1alpha2.UsernamePasswordCredentialType { if msg := validateUsernamePasswordSecret(*secret); len(msg) > 0 { for _, m := range msg { messages = append(messages, fmt.Sprintf("seedJob `%s` %s", seedJob.ID, m)) } } } if seedJob.JenkinsCredentialType == v1alpha2.GithubAppCredentialType { if msg := validateGithubAppSecret(*secret); len(msg) > 0 { for _, m := range msg { messages = append(messages, fmt.Sprintf("seedJob `%s` %s", seedJob.ID, m)) } } } } s.setSeedJobPushTriggers(seedJob, &messages, jenkins) } return messages, nil } func (s *seedJobs) setSeedJobPushTriggers(seedJob v1alpha2.SeedJob, messages *[]string, jenkins v1alpha2.Jenkins) { if seedJob.GitHubPushTrigger { if msg := s.validateGitHubPushTrigger(jenkins); len(msg) > 0 { for _, m := range msg { *messages = append(*messages, fmt.Sprintf("seedJob `%s` %s", seedJob.ID, m)) } } } if seedJob.BitbucketPushTrigger { if msg := s.validateBitbucketPushTrigger(jenkins); len(msg) > 0 { for _, m := range msg { *messages = append(*messages, fmt.Sprintf("seedJob `%s` %s", seedJob.ID, m)) } } } } func (s *seedJobs) validateGitHubPushTrigger(jenkins v1alpha2.Jenkins) []string { var messages []string if err := checkPluginExists(jenkins, "github"); err != nil { return append(messages, fmt.Sprintf("githubPushTrigger cannot be enabled: %s", err)) } return messages } func (s *seedJobs) validateBitbucketPushTrigger(jenkins v1alpha2.Jenkins) []string { var messages []string if err := checkPluginExists(jenkins, "bitbucket"); err != nil { return append(messages, fmt.Sprintf("bitbucketPushTrigger cannot be enabled: %s", err)) } return messages } func checkPluginExists(jenkins v1alpha2.Jenkins, name string) error { exists := false for _, plugin := range jenkins.Spec.Master.BasePlugins { if plugin.Name == name { exists = true } } userExists := false for _, plugin := range jenkins.Spec.Master.Plugins { if plugin.Name == name { userExists = true } } if !exists && !userExists { return fmt.Errorf("`%s` plugin not installed", name) } return nil } func (s *seedJobs) validateIfIDIsUnique(seedJobs []v1alpha2.SeedJob) []string { var messages []string ids := map[string]bool{} for _, seedJob := range seedJobs { if _, found := ids[seedJob.ID]; found { messages = append(messages, fmt.Sprintf("'%s' seed job ID is not unique", seedJob.ID)) } ids[seedJob.ID] = true } return messages } func validateBasicSSHSecret(secret v1.Secret) []string { var messages []string username, exists := secret.Data[UsernameSecretKey] if !exists { messages = append(messages, fmt.Sprintf("required data '%s' not found in secret '%s'", UsernameSecretKey, secret.ObjectMeta.Name)) } if len(username) == 0 { messages = append(messages, fmt.Sprintf("required data '%s' is empty in secret '%s'", UsernameSecretKey, secret.ObjectMeta.Name)) } privateKey, exists := secret.Data[PrivateKeySecretKey] if !exists { messages = append(messages, fmt.Sprintf("required data '%s' not found in secret '%s'", PrivateKeySecretKey, secret.ObjectMeta.Name)) } if len(string(privateKey)) == 0 { messages = append(messages, fmt.Sprintf("required data '%s' not found in secret '%s'", PrivateKeySecretKey, secret.ObjectMeta.Name)) } if err := validatePrivateKey(string(privateKey)); err != nil { messages = append(messages, fmt.Sprintf("private key '%s' invalid in secret '%s': %s", PrivateKeySecretKey, secret.ObjectMeta.Name, err)) } return messages } func validateUsernamePasswordSecret(secret v1.Secret) []string { var messages []string username, exists := secret.Data[UsernameSecretKey] if !exists { messages = append(messages, fmt.Sprintf("required data '%s' not found in secret '%s'", UsernameSecretKey, secret.ObjectMeta.Name)) } if len(username) == 0 { messages = append(messages, fmt.Sprintf("required data '%s' is empty in secret '%s'", UsernameSecretKey, secret.ObjectMeta.Name)) } password, exists := secret.Data[PasswordSecretKey] if !exists { messages = append(messages, fmt.Sprintf("required data '%s' not found in secret '%s'", PasswordSecretKey, secret.ObjectMeta.Name)) } if len(password) == 0 { messages = append(messages, fmt.Sprintf("required data '%s' is empty in secret '%s'", PasswordSecretKey, secret.ObjectMeta.Name)) } return messages } func validateGithubAppSecret(secret v1.Secret) []string { var messages []string appid, exists := secret.Data[AppIDSecretKey] if !exists { messages = append(messages, fmt.Sprintf("required data '%s' not found in secret '%s'", AppIDSecretKey, secret.ObjectMeta.Name)) } if len(appid) == 0 { messages = append(messages, fmt.Sprintf("required data '%s' is empty in secret '%s'", AppIDSecretKey, secret.ObjectMeta.Name)) } pkey, exists := secret.Data[PrivateKeySecretKey] if !exists { messages = append(messages, fmt.Sprintf("required data '%s' not found in secret '%s'", PrivateKeySecretKey, secret.ObjectMeta.Name)) } if len(pkey) == 0 { messages = append(messages, fmt.Sprintf("required data '%s' is empty in secret '%s'", PrivateKeySecretKey, secret.ObjectMeta.Name)) } return messages } func validatePrivateKey(privateKey string) error { _, err := ssh.ParseRawPrivateKey([]byte(privateKey)) if err != nil { return stackerr.Wrap(err, "failed to decode key") } return nil }