diff --git a/api/v1alpha2/jenkins_types.go b/api/v1alpha2/jenkins_types.go index 1f73d8ea..3de5e3e2 100644 --- a/api/v1alpha2/jenkins_types.go +++ b/api/v1alpha2/jenkins_types.go @@ -20,7 +20,7 @@ type JenkinsSpec struct { // ValidateSecurityWarnings enables or disables validating potential security warnings in Jenkins plugins via admission webhooks. //+optional - ValidateSecurityWarnings bool `json:"ValidateSecurityWarnings,omitempty"` + ValidateSecurityWarnings bool `json:"validateSecurityWarnings,omitempty"` // Notifications defines list of a services which are used to inform about Jenkins status // Can be used to integrate chat services like Slack, Microsoft Teams or Mailgun diff --git a/api/v1alpha2/jenkins_webhook.go b/api/v1alpha2/jenkins_webhook.go index 2eac25c5..c481501c 100644 --- a/api/v1alpha2/jenkins_webhook.go +++ b/api/v1alpha2/jenkins_webhook.go @@ -37,12 +37,19 @@ import ( ) var ( - jenkinslog = logf.Log.WithName("jenkins-resource") // log is for logging in this package. - PluginsMgr PluginDataManager = *NewPluginsDataManager("/tmp/plugins.json.gzip", "/tmp/plugins.json", false, time.Duration(1000)*time.Second) - _ webhook.Validator = &Jenkins{} + jenkinslog = logf.Log.WithName("jenkins-resource") // log is for logging in this package. + SecValidator = *NewSecurityValidator() + _ webhook.Validator = &Jenkins{} + initialSecurityWarningsDownloadSucceded = false ) -const Hosturl = "https://ci.jenkins.io/job/Infra/job/plugin-site-api/job/generate-data/lastSuccessfulBuild/artifact/plugins.json.gzip" +const ( + Hosturl = "https://ci.jenkins.io/job/Infra/job/plugin-site-api/job/generate-data/lastSuccessfulBuild/artifact/plugins.json.gzip" + CompressedFilePath = "/tmp/plugins.json.gzip" + PluginDataFile = "/tmp/plugins.json" + shortenedCheckingPeriod = 1 * time.Hour + defaultCheckingPeriod = 12 * time.Minute +) func (in *Jenkins) SetupWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr). @@ -50,8 +57,6 @@ func (in *Jenkins) SetupWebhookWithManager(mgr ctrl.Manager) error { Complete() } -// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! - // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. // +kubebuilder:webhook:path=/validate-jenkins-io-jenkins-io-v1alpha2-jenkins,mutating=false,failurePolicy=fail,sideEffects=None,groups=jenkins.io.jenkins.io,resources=jenkins,verbs=create;update,versions=v1alpha2,name=vjenkins.kb.io,admissionReviewVersions={v1,v1beta1} @@ -79,14 +84,11 @@ func (in *Jenkins) ValidateDelete() error { return nil } -type PluginDataManager struct { - PluginDataCache PluginsInfo - Timeout time.Duration - CompressedFilePath string - PluginDataFile string - IsCached bool - Attempts int - SleepTime time.Duration +type SecurityValidator struct { + PluginDataCache PluginsInfo + isCached bool + Attempts int + checkingPeriod time.Duration } type PluginsInfo struct { @@ -118,7 +120,7 @@ type PluginData struct { // Validates security warnings for both updating and creating a Jenkins CR func Validate(r Jenkins) error { - if !PluginsMgr.IsCached { + if !SecValidator.isCached { return errors.New("plugins data has not been fetched") } @@ -140,7 +142,7 @@ func Validate(r Jenkins) error { } } - for _, plugin := range PluginsMgr.PluginDataCache.Plugins { + for _, plugin := range SecValidator.PluginDataCache.Plugins { if pluginData, ispresent := pluginSet[plugin.Name]; ispresent { var hasVulnerabilities bool for _, warning := range plugin.SecurityWarnings { @@ -184,44 +186,42 @@ func Validate(r Jenkins) error { return nil } -func NewPluginsDataManager(compressedFilePath string, pluginDataFile string, isCached bool, timeout time.Duration) *PluginDataManager { - return &PluginDataManager{ - CompressedFilePath: compressedFilePath, - PluginDataFile: pluginDataFile, - IsCached: isCached, - Timeout: timeout, +// NewMonitor creates a new worker and instantiates all the data structures required +func NewSecurityValidator() *SecurityValidator { + return &SecurityValidator{ + isCached: false, + Attempts: 0, + checkingPeriod: shortenedCheckingPeriod, } } -func (in *PluginDataManager) ManagePluginData(sig chan bool) { - var isInit bool - var retryInterval time.Duration +func (in *SecurityValidator) MonitorSecurityWarnings(securityWarningsFetched chan bool) { + jenkinslog.Info("Security warnings check: enabled\n") for { - var isCached bool - err := in.fetchPluginData() - if err == nil { - isCached = true - } else { - jenkinslog.Info("Cache plugin data", "failed to fetch plugin data", err) - } - // should only be executed once when the operator starts - if !isInit { - sig <- isCached // sending signal to main to continue - isInit = true - } + in.checkForSecurityVulnerabilities(securityWarningsFetched) + <-time.After(in.checkingPeriod) + } +} - in.IsCached = in.IsCached || isCached - if !isCached { - retryInterval = time.Duration(1) * time.Hour - } else { - retryInterval = time.Duration(12) * time.Hour - } - time.Sleep(retryInterval) +func (in *SecurityValidator) checkForSecurityVulnerabilities(securityWarningsFetched chan bool) { + err := in.fetchPluginData() + if err != nil { + jenkinslog.Info("Cache plugin data", "failed to fetch plugin data", err) + in.checkingPeriod = shortenedCheckingPeriod + return + } + in.isCached = true + in.checkingPeriod = defaultCheckingPeriod + + // should only be executed once when the operator starts + if !initialSecurityWarningsDownloadSucceded { + securityWarningsFetched <- in.isCached + initialSecurityWarningsDownloadSucceded = true } } // Downloads extracts and reads the JSON data in every 12 hours -func (in *PluginDataManager) fetchPluginData() error { +func (in *SecurityValidator) fetchPluginData() error { jenkinslog.Info("Initializing/Updating the plugin data cache") var err error for in.Attempts = 0; in.Attempts < 5; in.Attempts++ { @@ -262,29 +262,36 @@ func (in *PluginDataManager) fetchPluginData() error { return err } -func (in *PluginDataManager) download() error { - out, err := os.Create(in.CompressedFilePath) +func (in *SecurityValidator) download() error { + out, err := os.Create(CompressedFilePath) if err != nil { return err } defer out.Close() - client := http.Client{ - Timeout: in.Timeout, - } - - resp, err := client.Get(Hosturl) + req, err := http.NewRequest(http.MethodGet, Hosturl, nil) if err != nil { return err } - defer resp.Body.Close() + req.Header.Set("Content-Type", "application/json") - _, err = io.Copy(out, resp.Body) + Client := http.Client{ + Timeout: 1 * time.Minute, + } + + response, err := Client.Do(req) + if err != nil { + return err + } + + defer response.Body.Close() + + _, err = io.Copy(out, response.Body) return err } -func (in *PluginDataManager) extract() error { - reader, err := os.Open(in.CompressedFilePath) +func (in *SecurityValidator) extract() error { + reader, err := os.Open(CompressedFilePath) if err != nil { return err @@ -296,7 +303,7 @@ func (in *PluginDataManager) extract() error { } defer archive.Close() - writer, err := os.Create(in.PluginDataFile) + writer, err := os.Create(PluginDataFile) if err != nil { return err } @@ -307,8 +314,8 @@ func (in *PluginDataManager) extract() error { } // Loads the JSON data into memory and stores it -func (in *PluginDataManager) cache() error { - jsonFile, err := os.Open(in.PluginDataFile) +func (in *SecurityValidator) cache() error { + jsonFile, err := os.Open(PluginDataFile) if err != nil { return err } diff --git a/api/v1alpha2/jenkins_webhook_test.go b/api/v1alpha2/jenkins_webhook_test.go index 439a1960..eb664ce7 100644 --- a/api/v1alpha2/jenkins_webhook_test.go +++ b/api/v1alpha2/jenkins_webhook_test.go @@ -83,9 +83,9 @@ func TestValidate(t *testing.T) { assert.Equal(t, got, errors.New("plugins data has not been fetched")) }) - PluginsMgr.IsCached = true + SecValidator.isCached = true t.Run("Validating a Jenkins CR with plugins not having security warnings and validation is turned on", func(t *testing.T) { - PluginsMgr.PluginDataCache = PluginsInfo{Plugins: []PluginInfo{ + SecValidator.PluginDataCache = PluginsInfo{Plugins: []PluginInfo{ {Name: "security-script"}, {Name: "git-client"}, {Name: "git"}, @@ -100,7 +100,7 @@ func TestValidate(t *testing.T) { }) t.Run("Validating a Jenkins CR with some of the plugins having security warnings and validation is turned on", func(t *testing.T) { - PluginsMgr.PluginDataCache = PluginsInfo{Plugins: []PluginInfo{ + SecValidator.PluginDataCache = PluginsInfo{Plugins: []PluginInfo{ {Name: "security-script", SecurityWarnings: createSecurityWarnings("1.2", "2.2")}, {Name: "workflow-cps", SecurityWarnings: createSecurityWarnings("2.59", "")}, {Name: "git-client"}, @@ -118,7 +118,7 @@ func TestValidate(t *testing.T) { }) t.Run("Updating a Jenkins CR with some of the plugins having security warnings and validation is turned on", func(t *testing.T) { - PluginsMgr.PluginDataCache = PluginsInfo{Plugins: []PluginInfo{ + SecValidator.PluginDataCache = PluginsInfo{Plugins: []PluginInfo{ {Name: "handy-uri-templates-2-api", SecurityWarnings: createSecurityWarnings("2.1.8-1.0", "2.2.8-1.0")}, {Name: "workflow-cps", SecurityWarnings: createSecurityWarnings("2.59", "")}, {Name: "resource-disposer", SecurityWarnings: createSecurityWarnings("0.7", "1.2")}, diff --git a/api/v1alpha2/zz_generated.deepcopy.go b/api/v1alpha2/zz_generated.deepcopy.go index 566f8d1d..c44318c7 100644 --- a/api/v1alpha2/zz_generated.deepcopy.go +++ b/api/v1alpha2/zz_generated.deepcopy.go @@ -23,7 +23,7 @@ package v1alpha2 import ( corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" - runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -356,6 +356,13 @@ func (in *JenkinsMaster) DeepCopyInto(out *JenkinsMaster) { *out = make([]Plugin, len(*in)) copy(*out, *in) } + if in.HostAliases != nil { + in, out := &in.HostAliases, &out.HostAliases + *out = make([]corev1.HostAlias, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JenkinsMaster. @@ -528,6 +535,65 @@ func (in *Plugin) DeepCopy() *Plugin { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PluginData) DeepCopyInto(out *PluginData) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PluginData. +func (in *PluginData) DeepCopy() *PluginData { + if in == nil { + return nil + } + out := new(PluginData) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PluginInfo) DeepCopyInto(out *PluginInfo) { + *out = *in + if in.SecurityWarnings != nil { + in, out := &in.SecurityWarnings, &out.SecurityWarnings + *out = make([]Warning, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PluginInfo. +func (in *PluginInfo) DeepCopy() *PluginInfo { + if in == nil { + return nil + } + out := new(PluginInfo) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PluginsInfo) DeepCopyInto(out *PluginsInfo) { + *out = *in + if in.Plugins != nil { + in, out := &in.Plugins, &out.Plugins + *out = make([]PluginInfo, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PluginsInfo. +func (in *PluginsInfo) DeepCopy() *PluginsInfo { + if in == nil { + return nil + } + out := new(PluginsInfo) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Restore) DeepCopyInto(out *Restore) { *out = *in @@ -593,6 +659,22 @@ func (in *SecretRef) DeepCopy() *SecretRef { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecurityValidator) DeepCopyInto(out *SecurityValidator) { + *out = *in + in.PluginDataCache.DeepCopyInto(&out.PluginDataCache) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecurityValidator. +func (in *SecurityValidator) DeepCopy() *SecurityValidator { + if in == nil { + return nil + } + out := new(SecurityValidator) + 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 @@ -679,3 +761,38 @@ func (in *Slack) DeepCopy() *Slack { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Version) DeepCopyInto(out *Version) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Version. +func (in *Version) DeepCopy() *Version { + if in == nil { + return nil + } + out := new(Version) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Warning) DeepCopyInto(out *Warning) { + *out = *in + if in.Versions != nil { + in, out := &in.Versions, &out.Versions + *out = make([]Version, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Warning. +func (in *Warning) DeepCopy() *Warning { + if in == nil { + return nil + } + out := new(Warning) + in.DeepCopyInto(out) + return out +} diff --git a/chart/jenkins-operator/crds/jenkins-crd.yaml b/chart/jenkins-operator/crds/jenkins-crd.yaml index 95350902..8fb5afab 100644 --- a/chart/jenkins-operator/crds/jenkins-crd.yaml +++ b/chart/jenkins-operator/crds/jenkins-crd.yaml @@ -36,10 +36,6 @@ spec: spec: description: Spec defines the desired state of the Jenkins properties: - ValidateSecurityWarnings: - description: ValidateSecurityWarnings enables or disables validating - potential security warnings in Jenkins plugins via admission webhooks. - type: boolean backup: description: 'Backup defines configuration of Jenkins backup More info: https://jenkinsci.github.io/kubernetes-operator/docs/getting-started/latest/configure-backup-and-restore/' @@ -1108,6 +1104,22 @@ spec: description: DisableCSRFProtection allows you to toggle CSRF Protection on Jenkins type: boolean + hostAliases: + description: HostAliases for Jenkins master pod and SeedJob agent + items: + description: HostAlias holds the mapping between IP and hostnames + that will be injected as an entry in the pod's hosts file. + properties: + hostnames: + description: Hostnames for the above IP address. + items: + type: string + type: array + ip: + description: IP address of the host file entry. + type: string + type: object + type: array imagePullSecrets: description: 'ImagePullSecrets is an optional list of references to secrets in the same namespace to use for pulling any of the @@ -3320,6 +3332,10 @@ spec: More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services---service-types' type: string type: object + validateSecurityWarnings: + description: ValidateSecurityWarnings enables or disables validating + potential security warnings in Jenkins plugins via admission webhooks. + type: boolean required: - jenkinsAPISettings - master diff --git a/chart/jenkins-operator/templates/jenkins.yaml b/chart/jenkins-operator/templates/jenkins.yaml index aae8f6ac..f9dd763d 100644 --- a/chart/jenkins-operator/templates/jenkins.yaml +++ b/chart/jenkins-operator/templates/jenkins.yaml @@ -145,7 +145,7 @@ spec: securityContext: {{- toYaml . | nindent 6 }} {{- end }} - ValidateSecurityWarnings: {{ .Values.jenkins.ValidateSecurityWarnings }} + validateSecurityWarnings: {{ .Values.jenkins.validateSecurityWarnings }} {{- with .Values.jenkins.seedJobs }} seedJobs: {{- toYaml . | nindent 4 }} {{- end }} diff --git a/chart/jenkins-operator/values.yaml b/chart/jenkins-operator/values.yaml index e4412fc8..a16f8e4f 100644 --- a/chart/jenkins-operator/values.yaml +++ b/chart/jenkins-operator/values.yaml @@ -48,8 +48,8 @@ jenkins: disableCSRFProtection: false - # ValidateSecurityWarnings enables or disables validating potential security warnings in Jenkins plugins via admission webhooks. - ValidateSecurityWarnings: false + # validateSecurityWarnings enables or disables validating potential security warnings in Jenkins plugins via admission webhooks. + validateSecurityWarnings: false # imagePullSecrets is used if you want to pull images from private repository # See https://jenkinsci.github.io/kubernetes-operator/docs/getting-started/latest/configuration/#pulling-docker-images-from-private-repositories for more info @@ -110,7 +110,7 @@ jenkins: # See https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/ for details resources: limits: - cpu: 1500m + cpu: 1000m memory: 3Gi requests: cpu: 1 @@ -146,26 +146,26 @@ jenkins: # LivenessProbe for Jenkins Master pod livenessProbe: - failureThreshold: 12 + failureThreshold: 20 httpGet: path: /login port: http scheme: HTTP - initialDelaySeconds: 80 + initialDelaySeconds: 100 periodSeconds: 10 successThreshold: 1 - timeoutSeconds: 5 + timeoutSeconds: 8 # ReadinessProbe for Jenkins Master pod readinessProbe: - failureThreshold: 3 + failureThreshold: 60 httpGet: path: /login port: http scheme: HTTP - initialDelaySeconds: 30 + initialDelaySeconds: 120 periodSeconds: 10 successThreshold: 1 - timeoutSeconds: 1 + timeoutSeconds: 8 # backup is section for configuring operator's backup feature # By default backup feature is enabled and pre-configured @@ -215,11 +215,11 @@ jenkins: # resources used by backup container resources: limits: - cpu: 1500m - memory: 1Gi + cpu: 1000m + memory: 2Gi requests: cpu: 100m - memory: 256Mi + memory: 500Mi # env contains container environment variables # PVC backup provider handles these variables: diff --git a/config/crd/bases/jenkins.io_jenkins.yaml b/config/crd/bases/jenkins.io_jenkins.yaml index 72fdcbbd..8fb5afab 100644 --- a/config/crd/bases/jenkins.io_jenkins.yaml +++ b/config/crd/bases/jenkins.io_jenkins.yaml @@ -36,10 +36,6 @@ spec: spec: description: Spec defines the desired state of the Jenkins properties: - ValidateSecurityWarnings: - description: ValidateSecurityWarnings enables or disables validating - potential security warnings in Jenkins plugins via admission webhooks. - type: boolean backup: description: 'Backup defines configuration of Jenkins backup More info: https://jenkinsci.github.io/kubernetes-operator/docs/getting-started/latest/configure-backup-and-restore/' @@ -3336,6 +3332,10 @@ spec: More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services---service-types' type: string type: object + validateSecurityWarnings: + description: ValidateSecurityWarnings enables or disables validating + potential security warnings in Jenkins plugins via admission webhooks. + type: boolean required: - jenkinsAPISettings - master diff --git a/main.go b/main.go index 778a019b..11175b6f 100644 --- a/main.go +++ b/main.go @@ -78,7 +78,7 @@ func main() { var metricsAddr string var enableLeaderElection bool var probeAddr string - var ValidateSecurityWarnings bool + var validateSecurityWarnings bool isRunningInCluster, err := resources.IsRunningInCluster() if err != nil { @@ -89,7 +89,7 @@ func main() { flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") flag.BoolVar(&enableLeaderElection, "leader-elect", isRunningInCluster, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") - flag.BoolVar(&ValidateSecurityWarnings, "validate-security-warnings", false, "Enable validation for potential security warnings in jenkins custom resource plugins") + flag.BoolVar(&validateSecurityWarnings, "validate-security-warnings", false, "Enable validation for potential security warnings in jenkins custom resource plugins") hostname := flag.String("jenkins-api-hostname", "", "Hostname or IP of Jenkins API. It can be service name, node IP or localhost.") port := flag.Int("jenkins-api-port", 0, "The port on which Jenkins API is running. Note: If you want to use nodePort don't set this setting and --jenkins-api-use-nodeport must be true.") useNodePort := flag.Bool("jenkins-api-use-nodeport", false, "Connect to Jenkins API using the service nodePort instead of service port. If you want to set this as true - don't set --jenkins-api-port.") @@ -111,11 +111,11 @@ func main() { } logger.Info(fmt.Sprintf("Watch namespace: %v", namespace)) - if ValidateSecurityWarnings { - isInitialized := make(chan bool) - go v1alpha2.PluginsMgr.ManagePluginData(isInitialized) + if validateSecurityWarnings { + securityWarningsFetched := make(chan bool) + go v1alpha2.SecValidator.MonitorSecurityWarnings(securityWarningsFetched) - if !<-isInitialized { + if !<-securityWarningsFetched { logger.Info("Unable to get the plugins data") } } @@ -180,7 +180,7 @@ func main() { fatal(errors.Wrap(err, "unable to create Jenkins controller"), *debug) } - if ValidateSecurityWarnings { + if validateSecurityWarnings { if err = (&v1alpha2.Jenkins{}).SetupWebhookWithManager(mgr); err != nil { fatal(errors.Wrap(err, "unable to create Webhook"), *debug) } diff --git a/test/e2e/jenkins_configuration_test.go b/test/e2e/jenkins_configuration_test.go index e5553ff9..638536cb 100644 --- a/test/e2e/jenkins_configuration_test.go +++ b/test/e2e/jenkins_configuration_test.go @@ -1,11 +1,13 @@ package e2e import ( + "context" "fmt" "github.com/jenkinsci/kubernetes-operator/api/v1alpha2" . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" // +kubebuilder:scaffold:imports ) @@ -74,10 +76,10 @@ var _ = Describe("Jenkins controller configuration", func() { BeforeEach(func() { namespace = CreateNamespace() - createUserConfigurationSecret(namespace.Name, userConfigurationSecretData) createUserConfigurationConfigMap(namespace.Name, numberOfExecutorsEnvName, fmt.Sprintf("${%s}", systemMessageEnvName)) - jenkins = createJenkinsCR(jenkinsCRName, namespace.Name, &[]v1alpha2.SeedJob{mySeedJob.SeedJob}, groovyScripts, casc, priorityClassName) + jenkins = RenderJenkinsCR(jenkinsCRName, namespace.Name, &[]v1alpha2.SeedJob{mySeedJob.SeedJob}, groovyScripts, casc, priorityClassName) + Expect(K8sClient.Create(context.TODO(), jenkins)).Should(Succeed()) createDefaultLimitsForContainersInNamespace(namespace.Name) createKubernetesCredentialsProviderSecret(namespace.Name, mySeedJob) }) @@ -125,7 +127,8 @@ var _ = Describe("Jenkins controller priority class", func() { BeforeEach(func() { namespace = CreateNamespace() - jenkins = createJenkinsCR(jenkinsCRName, namespace.Name, nil, groovyScripts, casc, priorityClassName) + jenkins = RenderJenkinsCR(jenkinsCRName, namespace.Name, nil, groovyScripts, casc, priorityClassName) + Expect(K8sClient.Create(context.TODO(), jenkins)).Should(Succeed()) }) AfterEach(func() { @@ -177,7 +180,8 @@ var _ = Describe("Jenkins controller plugins test", func() { BeforeEach(func() { namespace = CreateNamespace() - jenkins = createJenkinsCR(jenkinsCRName, namespace.Name, &[]v1alpha2.SeedJob{mySeedJob.SeedJob}, groovyScripts, casc, priorityClassName) + jenkins = RenderJenkinsCR(jenkinsCRName, namespace.Name, &[]v1alpha2.SeedJob{mySeedJob.SeedJob}, groovyScripts, casc, priorityClassName) + Expect(K8sClient.Create(context.TODO(), jenkins)).Should(Succeed()) }) AfterEach(func() { diff --git a/test/e2e/jenkins_pod_restart_test.go b/test/e2e/jenkins_pod_restart_test.go index 4d94a597..ef21e67b 100644 --- a/test/e2e/jenkins_pod_restart_test.go +++ b/test/e2e/jenkins_pod_restart_test.go @@ -1,6 +1,8 @@ package e2e import ( + "context" + "github.com/jenkinsci/kubernetes-operator/api/v1alpha2" . "github.com/onsi/ginkgo" @@ -35,7 +37,8 @@ var _ = Describe("Jenkins controller", func() { namespace = CreateNamespace() configureAuthorizationToUnSecure(namespace.Name, userConfigurationConfigMapName) - jenkins = createJenkinsCR(jenkinsCRName, namespace.Name, nil, groovyScripts, casc, priorityClassName) + jenkins = RenderJenkinsCR(jenkinsCRName, namespace.Name, nil, groovyScripts, casc, priorityClassName) + Expect(K8sClient.Create(context.TODO(), jenkins)).Should(Succeed()) }) AfterEach(func() { diff --git a/test/e2e/jenkins_test.go b/test/e2e/jenkins_test.go index a80b6ae2..972e0c9b 100644 --- a/test/e2e/jenkins_test.go +++ b/test/e2e/jenkins_test.go @@ -15,6 +15,7 @@ import ( . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" @@ -46,112 +47,6 @@ func getJenkinsMasterPod(jenkins *v1alpha2.Jenkins) *corev1.Pod { return &pods.Items[0] } -func createJenkinsCR(name, namespace string, seedJob *[]v1alpha2.SeedJob, groovyScripts v1alpha2.GroovyScripts, casc v1alpha2.ConfigurationAsCode, priorityClassName string) *v1alpha2.Jenkins { - var seedJobs []v1alpha2.SeedJob - if seedJob != nil { - seedJobs = append(seedJobs, *seedJob...) - } - - jenkins := &v1alpha2.Jenkins{ - TypeMeta: v1alpha2.JenkinsTypeMeta(), - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - Spec: v1alpha2.JenkinsSpec{ - GroovyScripts: groovyScripts, - ConfigurationAsCode: casc, - Master: v1alpha2.JenkinsMaster{ - Annotations: map[string]string{"test": "label"}, - Containers: []v1alpha2.Container{ - { - Name: resources.JenkinsMasterContainerName, - Env: []corev1.EnvVar{ - { - Name: "TEST_ENV", - Value: "test_env_value", - }, - }, - ReadinessProbe: &corev1.Probe{ - Handler: corev1.Handler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/login", - Port: intstr.FromString("http"), - Scheme: corev1.URISchemeHTTP, - }, - }, - InitialDelaySeconds: int32(100), - TimeoutSeconds: int32(4), - FailureThreshold: int32(12), - SuccessThreshold: int32(1), - PeriodSeconds: int32(1), - }, - LivenessProbe: &corev1.Probe{ - Handler: corev1.Handler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/login", - Port: intstr.FromString("http"), - Scheme: corev1.URISchemeHTTP, - }, - }, - InitialDelaySeconds: int32(80), - TimeoutSeconds: int32(4), - FailureThreshold: int32(30), - SuccessThreshold: int32(1), - PeriodSeconds: int32(5), - }, - VolumeMounts: []corev1.VolumeMount{ - { - Name: "plugins-cache", - MountPath: "/usr/share/jenkins/ref/plugins", - }, - }, - }, - { - Name: "envoyproxy", - Image: "envoyproxy/envoy-alpine:v1.14.1", - }, - }, - Plugins: []v1alpha2.Plugin{ - {Name: "audit-trail", Version: "3.10"}, - {Name: "simple-theme-plugin", Version: "0.7"}, - {Name: "github", Version: "1.34.1"}, - {Name: "devoptics", Version: "1.1934", DownloadURL: "https://jenkins-updates.cloudbees.com/download/plugins/devoptics/1.1934/devoptics.hpi"}, - }, - PriorityClassName: priorityClassName, - NodeSelector: map[string]string{"kubernetes.io/os": "linux"}, - Volumes: []corev1.Volume{ - { - Name: "plugins-cache", - VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{}, - }, - }, - }, - }, - SeedJobs: seedJobs, - Service: v1alpha2.Service{ - Type: corev1.ServiceTypeNodePort, - Port: constants.DefaultHTTPPortInt32, - }, - }, - } - jenkins.Spec.Roles = []rbacv1.RoleRef{ - { - APIGroup: "rbac.authorization.k8s.io", - Kind: "Role", - Name: resources.GetResourceName(jenkins), - }, - } - updateJenkinsCR(jenkins) - - _, _ = fmt.Fprintf(GinkgoWriter, "Jenkins CR %+v\n", *jenkins) - - Expect(K8sClient.Create(context.TODO(), jenkins)).Should(Succeed()) - - return jenkins -} - func createJenkinsCRSafeRestart(name, namespace string, seedJob *[]v1alpha2.SeedJob, groovyScripts v1alpha2.GroovyScripts, casc v1alpha2.ConfigurationAsCode, priorityClassName string) *v1alpha2.Jenkins { var seedJobs []v1alpha2.SeedJob if seedJob != nil { @@ -171,7 +66,8 @@ func createJenkinsCRSafeRestart(name, namespace string, seedJob *[]v1alpha2.Seed Annotations: map[string]string{"test": "label"}, Containers: []v1alpha2.Container{ { - Name: resources.JenkinsMasterContainerName, + Name: resources.JenkinsMasterContainerName, + Image: JenkinsTestImage, Env: []corev1.EnvVar{ { Name: "TEST_ENV", @@ -206,6 +102,16 @@ func createJenkinsCRSafeRestart(name, namespace string, seedJob *[]v1alpha2.Seed SuccessThreshold: int32(1), PeriodSeconds: int32(5), }, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("1"), + corev1.ResourceMemory: resource.MustParse("500Mi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("1000m"), + corev1.ResourceMemory: resource.MustParse("3Gi"), + }, + }, VolumeMounts: []corev1.VolumeMount{ { Name: "plugins-cache", diff --git a/test/e2e/logging.go b/test/e2e/logging.go index 21566c6e..a2dd1696 100644 --- a/test/e2e/logging.go +++ b/test/e2e/logging.go @@ -8,7 +8,6 @@ import ( "sort" "github.com/onsi/ginkgo" - corev1 "k8s.io/api/core/v1" "k8s.io/api/events/v1beta1" "k8s.io/apimachinery/pkg/labels" @@ -109,17 +108,18 @@ func printKubernetesEvents(namespace string) { _, _ = fmt.Fprintf(ginkgo.GinkgoWriter, "Last %d events from kubernetes:\n", kubernetesEventsLimit) for _, event := range events { - _, _ = fmt.Fprintf(ginkgo.GinkgoWriter, "%+v\n\n", event) + _, _ = fmt.Fprintf(ginkgo.GinkgoWriter, "Event CreationTime: %s, Type: %s, Reason: %s, Object: %s %s/%s, Action: %s\n", + event.CreationTimestamp, event.Type, event.Reason, event.Regarding.Kind, event.Regarding.Namespace, event.Regarding.Name, event.Note) } } } func printKubernetesPods(namespace string) { - _, _ = fmt.Fprintf(ginkgo.GinkgoWriter, "All pods in '%s' namespace:\n", namespace) + _, _ = fmt.Fprintf(ginkgo.GinkgoWriter, "\nAll pods in '%s' namespace:\n", namespace) pod, err := getOperatorPod(namespace) if err == nil { - _, _ = fmt.Fprintf(ginkgo.GinkgoWriter, "%+v\n\n", pod) + _, _ = fmt.Fprintf(ginkgo.GinkgoWriter, "%s: %+v \n", pod.Name, pod.Status.Conditions) } } diff --git a/test/e2e/restorebackup_test.go b/test/e2e/restorebackup_test.go index fc6d335e..9d710ace 100644 --- a/test/e2e/restorebackup_test.go +++ b/test/e2e/restorebackup_test.go @@ -108,7 +108,8 @@ func createJenkinsWithBackupAndRestoreConfigured(name, namespace string) *v1alph Master: v1alpha2.JenkinsMaster{ Containers: []v1alpha2.Container{ { - Name: resources.JenkinsMasterContainerName, + Name: resources.JenkinsMasterContainerName, + Image: JenkinsTestImage, VolumeMounts: []corev1.VolumeMount{ { Name: "plugins-cache", @@ -143,6 +144,16 @@ func createJenkinsWithBackupAndRestoreConfigured(name, namespace string) *v1alph SuccessThreshold: int32(1), PeriodSeconds: int32(5), }, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("1"), + corev1.ResourceMemory: resource.MustParse("500Mi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("1000m"), + corev1.ResourceMemory: resource.MustParse("3Gi"), + }, + }, }, { Name: containerName, diff --git a/test/e2e/test_utility.go b/test/e2e/test_utility.go index 4006b791..ee22d02d 100644 --- a/test/e2e/test_utility.go +++ b/test/e2e/test_utility.go @@ -5,15 +5,24 @@ import ( "fmt" "time" + "github.com/jenkinsci/kubernetes-operator/api/v1alpha2" + "github.com/jenkinsci/kubernetes-operator/pkg/configuration/base/resources" + "github.com/jenkinsci/kubernetes-operator/pkg/constants" + "github.com/onsi/ginkgo" "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" ) +const JenkinsTestImage = "jenkins/jenkins:2.303.2-lts" + var ( Cfg *rest.Config K8sClient client.Client @@ -59,3 +68,113 @@ func DestroyNamespace(namespace *corev1.Namespace) { return !exists, nil }, time.Second*120, time.Second).Should(gomega.BeTrue()) } + +func RenderJenkinsCR(name, namespace string, seedJob *[]v1alpha2.SeedJob, groovyScripts v1alpha2.GroovyScripts, casc v1alpha2.ConfigurationAsCode, priorityClassName string) *v1alpha2.Jenkins { + var seedJobs []v1alpha2.SeedJob + if seedJob != nil { + seedJobs = append(seedJobs, *seedJob...) + } + + return &v1alpha2.Jenkins{ + TypeMeta: v1alpha2.JenkinsTypeMeta(), + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: v1alpha2.JenkinsSpec{ + GroovyScripts: groovyScripts, + ConfigurationAsCode: casc, + Master: v1alpha2.JenkinsMaster{ + Annotations: map[string]string{"test": "label"}, + Containers: []v1alpha2.Container{ + { + Name: resources.JenkinsMasterContainerName, + Image: JenkinsTestImage, + Env: []corev1.EnvVar{ + { + Name: "TEST_ENV", + Value: "test_env_value", + }, + }, + ReadinessProbe: &corev1.Probe{ + Handler: corev1.Handler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/login", + Port: intstr.FromString("http"), + Scheme: corev1.URISchemeHTTP, + }, + }, + InitialDelaySeconds: int32(100), + TimeoutSeconds: int32(4), + FailureThreshold: int32(12), + SuccessThreshold: int32(1), + PeriodSeconds: int32(1), + }, + LivenessProbe: &corev1.Probe{ + Handler: corev1.Handler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/login", + Port: intstr.FromString("http"), + Scheme: corev1.URISchemeHTTP, + }, + }, + InitialDelaySeconds: int32(80), + TimeoutSeconds: int32(4), + FailureThreshold: int32(30), + SuccessThreshold: int32(1), + PeriodSeconds: int32(5), + }, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("1"), + corev1.ResourceMemory: resource.MustParse("500Mi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("1000m"), + corev1.ResourceMemory: resource.MustParse("3Gi"), + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "plugins-cache", + MountPath: "/usr/share/jenkins/ref/plugins", + }, + }, + }, + { + Name: "envoyproxy", + Image: "envoyproxy/envoy-alpine:v1.14.1", + }, + }, + Plugins: []v1alpha2.Plugin{ + {Name: "audit-trail", Version: "3.10"}, + {Name: "simple-theme-plugin", Version: "0.7"}, + {Name: "github", Version: "1.34.1"}, + {Name: "devoptics", Version: "1.1934", DownloadURL: "https://jenkins-updates.cloudbees.com/download/plugins/devoptics/1.1934/devoptics.hpi"}, + }, + PriorityClassName: priorityClassName, + NodeSelector: map[string]string{"kubernetes.io/os": "linux"}, + Volumes: []corev1.Volume{ + { + Name: "plugins-cache", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + }, + }, + SeedJobs: seedJobs, + Service: v1alpha2.Service{ + Type: corev1.ServiceTypeNodePort, + Port: constants.DefaultHTTPPortInt32, + }, + Roles: []rbacv1.RoleRef{ + { + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: "jenkins-operator-jenkins", + }, + }, + }, + } +} diff --git a/test/helm/helm_test.go b/test/helm/helm_test.go index 7bae087b..bfc983d0 100644 --- a/test/helm/helm_test.go +++ b/test/helm/helm_test.go @@ -7,20 +7,18 @@ import ( "time" "github.com/jenkinsci/kubernetes-operator/api/v1alpha2" - "github.com/jenkinsci/kubernetes-operator/pkg/configuration/base/resources" - "github.com/jenkinsci/kubernetes-operator/pkg/constants" "github.com/jenkinsci/kubernetes-operator/test/e2e" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" // +kubebuilder:scaffold:imports ) -var _ = Describe("Jenkins Controller with webhook", func() { +const jenkinsCRName = "jenkins" +var _ = Describe("Jenkins Controller", func() { var ( namespace *corev1.Namespace ) @@ -42,14 +40,15 @@ var _ = Describe("Jenkins Controller with webhook", func() { jenkins := &v1alpha2.Jenkins{ TypeMeta: v1alpha2.JenkinsTypeMeta(), ObjectMeta: metav1.ObjectMeta{ - Name: "jenkins", + Name: jenkinsCRName, Namespace: namespace.Name, }, } cmd := exec.Command("../../bin/helm", "upgrade", "jenkins", "../../chart/jenkins-operator", "--namespace", namespace.Name, "--debug", "--set-string", fmt.Sprintf("jenkins.namespace=%s", namespace.Name), - "--set-string", fmt.Sprintf("operator.image=%s", *imageName), "--install", "--wait") + "--set-string", fmt.Sprintf("jenkins.image=%s", "jenkins/jenkins:2.303.2-lts"), + "--set-string", fmt.Sprintf("operator.image=%s", *imageName), "--install") output, err := cmd.CombinedOutput() Expect(err).NotTo(HaveOccurred(), string(output)) @@ -58,47 +57,77 @@ var _ = Describe("Jenkins Controller with webhook", func() { }) }) +}) - Context("Deploys jenkins operator with helm charts with validating webhook and jenkins instance disabled", func() { - It("Deploys operator,denies creating a jenkins cr and creates jenkins cr with validation turned off", func() { +var _ = Describe("Jenkins Controller with security validator", func() { + var ( + namespace *corev1.Namespace + seedJobs = &[]v1alpha2.SeedJob{} + groovyScripts = v1alpha2.GroovyScripts{ + Customization: v1alpha2.Customization{ + Configurations: []v1alpha2.ConfigMapRef{}, + }, + } + casc = v1alpha2.ConfigurationAsCode{ + Customization: v1alpha2.Customization{ + Configurations: []v1alpha2.ConfigMapRef{}, + }, + } + invalidPlugins = []v1alpha2.Plugin{ + {Name: "simple-theme-plugin", Version: "0.6"}, + {Name: "audit-trail", Version: "3.5"}, + {Name: "github", Version: "1.29.0"}, + } + validPlugins = []v1alpha2.Plugin{ + {Name: "simple-theme-plugin", Version: "0.6"}, + {Name: "audit-trail", Version: "3.8"}, + {Name: "github", Version: "1.31.0"}, + } + ) + + BeforeEach(func() { + namespace = e2e.CreateNamespace() + }) + AfterEach(func() { + cmd := exec.Command("../../bin/helm", "delete", "jenkins", "--namespace", namespace.Name) + output, err := cmd.CombinedOutput() + Expect(err).NotTo(HaveOccurred(), string(output)) + + e2e.ShowLogsIfTestHasFailed(CurrentGinkgoTestDescription().Failed, namespace.Name) + e2e.DestroyNamespace(namespace) + }) + + Context("When Jenkins CR contains plugins with security warnings", func() { + It("Denies creating a jenkins CR with a warning", func() { By("Deploying the operator along with webhook and cert-manager") cmd := exec.Command("../../bin/helm", "upgrade", "jenkins", "../../chart/jenkins-operator", "--namespace", namespace.Name, "--debug", - "--set-string", fmt.Sprintf("jenkins.namespace=%s", namespace.Name), "--set-string", fmt.Sprintf("operator.image=%s", *imageName), - "--set", fmt.Sprintf("webhook.enabled=%t", true), "--set", fmt.Sprintf("jenkins.enabled=%t", false), "--install", "--wait") + "--set-string", fmt.Sprintf("jenkins.namespace=%s", namespace.Name), + "--set-string", fmt.Sprintf("operator.image=%s", *imageName), + "--set", fmt.Sprintf("jenkins.securityValidator=%t", true), + "--set", fmt.Sprintf("jenkins.enabled=%t", false), + "--set", fmt.Sprintf("webhook.enabled=%t", true), "--install") output, err := cmd.CombinedOutput() Expect(err).NotTo(HaveOccurred(), string(output)) - By("Waiting for the operator to fetch the plugin data ") + By("Waiting for the operator to fetch the plugin data") time.Sleep(time.Duration(200) * time.Second) - By("Denying a create request for a Jenkins custom resource with some plugins having security warnings and validation is turned on") - userplugins := []v1alpha2.Plugin{ - {Name: "simple-theme-plugin", Version: "0.6"}, - {Name: "audit-trail", Version: "3.5"}, - {Name: "github", Version: "1.29.0"}, - } - jenkins := CreateJenkinsCR("jenkins", namespace.Name, userplugins, true) + By("Denying a create request for a Jenkins custom resource") + jenkins := e2e.RenderJenkinsCR(jenkinsCRName, namespace.Name, seedJobs, groovyScripts, casc, "") + jenkins.Spec.Master.Plugins = invalidPlugins + jenkins.Spec.ValidateSecurityWarnings = true Expect(e2e.K8sClient.Create(context.TODO(), jenkins)).Should(MatchError("admission webhook \"vjenkins.kb.io\" denied the request: security vulnerabilities detected in the following user-defined plugins: \naudit-trail:3.5\ngithub:1.29.0")) - - By("Creating the Jenkins resource with plugins not having any security warnings and validation is turned on") - userplugins = []v1alpha2.Plugin{ - {Name: "simple-theme-plugin", Version: "0.6"}, - {Name: "audit-trail", Version: "3.8"}, - {Name: "github", Version: "1.31.0"}, - } - jenkins = CreateJenkinsCR("jenkins", namespace.Name, userplugins, true) - Expect(e2e.K8sClient.Create(context.TODO(), jenkins)).Should(Succeed()) - e2e.WaitForJenkinsBaseConfigurationToComplete(jenkins) - e2e.WaitForJenkinsUserConfigurationToComplete(jenkins) - }) - - It("Deploys operator, creates a jenkins cr and denies update request for another one", func() { + }) + Context("When Jenkins CR doesn't contain plugins with security warnings", func() { + It("Jenkins instance is successfully created", func() { By("Deploying the operator along with webhook and cert-manager") cmd := exec.Command("../../bin/helm", "upgrade", "jenkins", "../../chart/jenkins-operator", "--namespace", namespace.Name, "--debug", - "--set-string", fmt.Sprintf("jenkins.namespace=%s", namespace.Name), "--set-string", fmt.Sprintf("operator.image=%s", *imageName), - "--set", fmt.Sprintf("webhook.enabled=%t", true), "--set", fmt.Sprintf("jenkins.enabled=%t", false), "--install", "--wait") + "--set-string", fmt.Sprintf("jenkins.namespace=%s", namespace.Name), + "--set-string", fmt.Sprintf("operator.image=%s", *imageName), + "--set", fmt.Sprintf("webhook.enabled=%t", true), + "--set", fmt.Sprintf("jenkins.enabled=%t", false), "--install") output, err := cmd.CombinedOutput() Expect(err).NotTo(HaveOccurred(), string(output)) @@ -106,129 +135,12 @@ var _ = Describe("Jenkins Controller with webhook", func() { time.Sleep(time.Duration(200) * time.Second) By("Creating a Jenkins custom resource with some plugins having security warnings but validation is turned off") - userplugins := []v1alpha2.Plugin{ - {Name: "simple-theme-plugin", Version: "0.6"}, - {Name: "audit-trail", Version: "3.5"}, - {Name: "github", Version: "1.29.0"}, - } - jenkins := CreateJenkinsCR("jenkins", namespace.Name, userplugins, false) + jenkins := e2e.RenderJenkinsCR(jenkinsCRName, namespace.Name, seedJobs, groovyScripts, casc, "") + jenkins.Spec.Master.Plugins = validPlugins + jenkins.Spec.ValidateSecurityWarnings = true Expect(e2e.K8sClient.Create(context.TODO(), jenkins)).Should(Succeed()) e2e.WaitForJenkinsBaseConfigurationToComplete(jenkins) e2e.WaitForJenkinsUserConfigurationToComplete(jenkins) - - By("Failing to update the Jenkins custom resource because some plugins have security warnings and validation is turned on") - userplugins = []v1alpha2.Plugin{ - {Name: "vncviewer", Version: "1.7"}, - {Name: "build-timestamp", Version: "1.0.3"}, - {Name: "deployit-plugin", Version: "7.5.5"}, - {Name: "github-branch-source", Version: "2.0.7"}, - {Name: "aws-lambda-cloud", Version: "0.4"}, - {Name: "groovy", Version: "1.31"}, - {Name: "google-login", Version: "1.2"}, - } - jenkins.Spec.Master.Plugins = userplugins - jenkins.Spec.ValidateSecurityWarnings = true - Expect(e2e.K8sClient.Update(context.TODO(), jenkins)).Should(MatchError("admission webhook \"vjenkins.kb.io\" denied the request: security vulnerabilities detected in the following user-defined plugins: \nvncviewer:1.7\ndeployit-plugin:7.5.5\ngithub-branch-source:2.0.7\ngroovy:1.31\ngoogle-login:1.2")) - }) }) - }) - -func CreateJenkinsCR(name string, namespace string, userPlugins []v1alpha2.Plugin, validateSecurityWarnings bool) *v1alpha2.Jenkins { - jenkins := &v1alpha2.Jenkins{ - TypeMeta: v1alpha2.JenkinsTypeMeta(), - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - Spec: v1alpha2.JenkinsSpec{ - GroovyScripts: v1alpha2.GroovyScripts{ - Customization: v1alpha2.Customization{ - Configurations: []v1alpha2.ConfigMapRef{}, - Secret: v1alpha2.SecretRef{ - Name: "", - }, - }, - }, - ConfigurationAsCode: v1alpha2.ConfigurationAsCode{ - Customization: v1alpha2.Customization{ - Configurations: []v1alpha2.ConfigMapRef{}, - Secret: v1alpha2.SecretRef{ - Name: "", - }, - }, - }, - Master: v1alpha2.JenkinsMaster{ - Containers: []v1alpha2.Container{ - { - Name: resources.JenkinsMasterContainerName, - Env: []corev1.EnvVar{ - { - Name: "TEST_ENV", - Value: "test_env_value", - }, - }, - ReadinessProbe: &corev1.Probe{ - Handler: corev1.Handler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/login", - Port: intstr.FromString("http"), - Scheme: corev1.URISchemeHTTP, - }, - }, - InitialDelaySeconds: int32(100), - TimeoutSeconds: int32(4), - FailureThreshold: int32(40), - SuccessThreshold: int32(1), - PeriodSeconds: int32(10), - }, - LivenessProbe: &corev1.Probe{ - Handler: corev1.Handler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/login", - Port: intstr.FromString("http"), - Scheme: corev1.URISchemeHTTP, - }, - }, - InitialDelaySeconds: int32(80), - TimeoutSeconds: int32(4), - FailureThreshold: int32(30), - SuccessThreshold: int32(1), - PeriodSeconds: int32(5), - }, - VolumeMounts: []corev1.VolumeMount{ - { - Name: "plugins-cache", - MountPath: "/usr/share/jenkins/ref/plugins", - }, - }, - }, - { - Name: "envoyproxy", - Image: "envoyproxy/envoy-alpine:v1.14.1", - }, - }, - Plugins: userPlugins, - DisableCSRFProtection: false, - NodeSelector: map[string]string{"kubernetes.io/os": "linux"}, - Volumes: []corev1.Volume{ - { - Name: "plugins-cache", - VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{}, - }, - }, - }, - }, - ValidateSecurityWarnings: validateSecurityWarnings, - Service: v1alpha2.Service{ - Type: corev1.ServiceTypeNodePort, - Port: constants.DefaultHTTPPortInt32, - }, - JenkinsAPISettings: v1alpha2.JenkinsAPISettings{AuthorizationStrategy: v1alpha2.CreateUserAuthorizationStrategy}, - }, - } - - return jenkins -}