228 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			228 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			Go
		
	
	
	
package seedjobs
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"crypto/x509"
 | 
						|
	"encoding/pem"
 | 
						|
	"fmt"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
 | 
						|
	"github.com/jenkinsci/kubernetes-operator/pkg/log"
 | 
						|
 | 
						|
	"github.com/go-logr/logr"
 | 
						|
	stackerr "github.com/pkg/errors"
 | 
						|
	"github.com/robfig/cron"
 | 
						|
	"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 v1alpha2.Jenkins) (bool, error) {
 | 
						|
	valid := true
 | 
						|
 | 
						|
	if !r.validateIfIDIsUnique(jenkins.Spec.SeedJobs) {
 | 
						|
		valid = false
 | 
						|
	}
 | 
						|
 | 
						|
	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 := v1alpha2.AllowedJenkinsCredentialMap[string(seedJob.JenkinsCredentialType)]; !ok {
 | 
						|
			logger.Info("unknown credential type")
 | 
						|
			return false, nil
 | 
						|
		}
 | 
						|
 | 
						|
		if (seedJob.JenkinsCredentialType == v1alpha2.BasicSSHCredentialType ||
 | 
						|
			seedJob.JenkinsCredentialType == v1alpha2.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 == v1alpha2.NoJenkinsCredentialCredentialType {
 | 
						|
			logger.Info("Jenkins credential must be set while using ssh repository url")
 | 
						|
			valid = false
 | 
						|
		}
 | 
						|
 | 
						|
		if seedJob.JenkinsCredentialType == v1alpha2.BasicSSHCredentialType || seedJob.JenkinsCredentialType == v1alpha2.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 == v1alpha2.BasicSSHCredentialType {
 | 
						|
				if ok := validateBasicSSHSecret(logger, *secret); !ok {
 | 
						|
					valid = false
 | 
						|
				}
 | 
						|
			}
 | 
						|
			if seedJob.JenkinsCredentialType == v1alpha2.UsernamePasswordCredentialType {
 | 
						|
				if ok := validateUsernamePasswordSecret(logger, *secret); !ok {
 | 
						|
					valid = false
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		if len(seedJob.BuildPeriodically) > 0 {
 | 
						|
			if !r.validateSchedule(seedJob, seedJob.BuildPeriodically, "buildPeriodically") {
 | 
						|
				valid = false
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		if len(seedJob.PollSCM) > 0 {
 | 
						|
			if !r.validateSchedule(seedJob, seedJob.PollSCM, "pollSCM") {
 | 
						|
				valid = false
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		if seedJob.GitHubPushTrigger {
 | 
						|
			if !r.validateGitHubPushTrigger(jenkins) {
 | 
						|
				valid = false
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return valid, nil
 | 
						|
}
 | 
						|
 | 
						|
func (r *SeedJobs) validateSchedule(job v1alpha2.SeedJob, str string, key string) bool {
 | 
						|
	_, err := cron.Parse(str)
 | 
						|
	if err != nil {
 | 
						|
		r.logger.V(log.VWarn).Info(fmt.Sprintf("`%s` schedule '%s' is invalid cron spec in `%s`", key, str, job.ID))
 | 
						|
		return false
 | 
						|
	}
 | 
						|
	return true
 | 
						|
}
 | 
						|
 | 
						|
func (r *SeedJobs) validateGitHubPushTrigger(jenkins v1alpha2.Jenkins) bool {
 | 
						|
	exists := false
 | 
						|
	for _, plugin := range jenkins.Spec.Master.BasePlugins {
 | 
						|
		if plugin.Name == "github" {
 | 
						|
			exists = true
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	userExists := false
 | 
						|
	for _, plugin := range jenkins.Spec.Master.Plugins {
 | 
						|
		if plugin.Name == "github" {
 | 
						|
			userExists = true
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if !exists && !userExists {
 | 
						|
		r.logger.V(log.VWarn).Info("githubPushTrigger is set. This function requires `github` plugin installed in .Spec.Master.Plugins because seed jobs Push Trigger function needs it")
 | 
						|
		return false
 | 
						|
	}
 | 
						|
	return true
 | 
						|
}
 | 
						|
 | 
						|
func (r *SeedJobs) validateIfIDIsUnique(seedJobs []v1alpha2.SeedJob) bool {
 | 
						|
	ids := map[string]bool{}
 | 
						|
	for _, seedJob := range seedJobs {
 | 
						|
		if _, found := ids[seedJob.ID]; found {
 | 
						|
			r.logger.V(log.VWarn).Info(fmt.Sprintf("'%s' seed job ID is not unique", seedJob.ID))
 | 
						|
			return false
 | 
						|
		}
 | 
						|
		ids[seedJob.ID] = true
 | 
						|
	}
 | 
						|
	return true
 | 
						|
}
 | 
						|
 | 
						|
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
 | 
						|
}
 |