Prepare for Security Validator release (#680)

* Tidy up k8s events logging
* Increase memory and decrease CPU in resources
* Standarize API fields
* Refactor tests
* Change image and resources for e2e tests
* Increase readability
This commit is contained in:
SylwiaBrant 2021-12-07 08:23:58 +01:00 committed by GitHub
parent 89fa53ae08
commit 7e94bc623f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 460 additions and 365 deletions

View File

@ -20,7 +20,7 @@ type JenkinsSpec struct {
// ValidateSecurityWarnings enables or disables validating potential security warnings in Jenkins plugins via admission webhooks. // ValidateSecurityWarnings enables or disables validating potential security warnings in Jenkins plugins via admission webhooks.
//+optional //+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 // 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 // Can be used to integrate chat services like Slack, Microsoft Teams or Mailgun

View File

@ -38,11 +38,18 @@ import (
var ( var (
jenkinslog = logf.Log.WithName("jenkins-resource") // log is for logging in this package. 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) SecValidator = *NewSecurityValidator()
_ webhook.Validator = &Jenkins{} _ 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 { func (in *Jenkins) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr). return ctrl.NewWebhookManagedBy(mgr).
@ -50,8 +57,6 @@ func (in *Jenkins) SetupWebhookWithManager(mgr ctrl.Manager) error {
Complete() 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. // 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} // +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 return nil
} }
type PluginDataManager struct { type SecurityValidator struct {
PluginDataCache PluginsInfo PluginDataCache PluginsInfo
Timeout time.Duration isCached bool
CompressedFilePath string
PluginDataFile string
IsCached bool
Attempts int Attempts int
SleepTime time.Duration checkingPeriod time.Duration
} }
type PluginsInfo struct { type PluginsInfo struct {
@ -118,7 +120,7 @@ type PluginData struct {
// Validates security warnings for both updating and creating a Jenkins CR // Validates security warnings for both updating and creating a Jenkins CR
func Validate(r Jenkins) error { func Validate(r Jenkins) error {
if !PluginsMgr.IsCached { if !SecValidator.isCached {
return errors.New("plugins data has not been fetched") 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 { if pluginData, ispresent := pluginSet[plugin.Name]; ispresent {
var hasVulnerabilities bool var hasVulnerabilities bool
for _, warning := range plugin.SecurityWarnings { for _, warning := range plugin.SecurityWarnings {
@ -184,44 +186,42 @@ func Validate(r Jenkins) error {
return nil return nil
} }
func NewPluginsDataManager(compressedFilePath string, pluginDataFile string, isCached bool, timeout time.Duration) *PluginDataManager { // NewMonitor creates a new worker and instantiates all the data structures required
return &PluginDataManager{ func NewSecurityValidator() *SecurityValidator {
CompressedFilePath: compressedFilePath, return &SecurityValidator{
PluginDataFile: pluginDataFile, isCached: false,
IsCached: isCached, Attempts: 0,
Timeout: timeout, checkingPeriod: shortenedCheckingPeriod,
} }
} }
func (in *PluginDataManager) ManagePluginData(sig chan bool) { func (in *SecurityValidator) MonitorSecurityWarnings(securityWarningsFetched chan bool) {
var isInit bool jenkinslog.Info("Security warnings check: enabled\n")
var retryInterval time.Duration
for { for {
var isCached bool in.checkForSecurityVulnerabilities(securityWarningsFetched)
err := in.fetchPluginData() <-time.After(in.checkingPeriod)
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.IsCached = in.IsCached || isCached func (in *SecurityValidator) checkForSecurityVulnerabilities(securityWarningsFetched chan bool) {
if !isCached { err := in.fetchPluginData()
retryInterval = time.Duration(1) * time.Hour if err != nil {
} else { jenkinslog.Info("Cache plugin data", "failed to fetch plugin data", err)
retryInterval = time.Duration(12) * time.Hour in.checkingPeriod = shortenedCheckingPeriod
return
} }
time.Sleep(retryInterval) 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 // 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") jenkinslog.Info("Initializing/Updating the plugin data cache")
var err error var err error
for in.Attempts = 0; in.Attempts < 5; in.Attempts++ { for in.Attempts = 0; in.Attempts < 5; in.Attempts++ {
@ -262,29 +262,36 @@ func (in *PluginDataManager) fetchPluginData() error {
return err return err
} }
func (in *PluginDataManager) download() error { func (in *SecurityValidator) download() error {
out, err := os.Create(in.CompressedFilePath) out, err := os.Create(CompressedFilePath)
if err != nil { if err != nil {
return err return err
} }
defer out.Close() defer out.Close()
client := http.Client{ req, err := http.NewRequest(http.MethodGet, Hosturl, nil)
Timeout: in.Timeout,
}
resp, err := client.Get(Hosturl)
if err != nil { if err != nil {
return err 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 return err
} }
func (in *PluginDataManager) extract() error { func (in *SecurityValidator) extract() error {
reader, err := os.Open(in.CompressedFilePath) reader, err := os.Open(CompressedFilePath)
if err != nil { if err != nil {
return err return err
@ -296,7 +303,7 @@ func (in *PluginDataManager) extract() error {
} }
defer archive.Close() defer archive.Close()
writer, err := os.Create(in.PluginDataFile) writer, err := os.Create(PluginDataFile)
if err != nil { if err != nil {
return err return err
} }
@ -307,8 +314,8 @@ func (in *PluginDataManager) extract() error {
} }
// Loads the JSON data into memory and stores it // Loads the JSON data into memory and stores it
func (in *PluginDataManager) cache() error { func (in *SecurityValidator) cache() error {
jsonFile, err := os.Open(in.PluginDataFile) jsonFile, err := os.Open(PluginDataFile)
if err != nil { if err != nil {
return err return err
} }

View File

@ -83,9 +83,9 @@ func TestValidate(t *testing.T) {
assert.Equal(t, got, errors.New("plugins data has not been fetched")) 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) { 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: "security-script"},
{Name: "git-client"}, {Name: "git-client"},
{Name: "git"}, {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) { 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: "security-script", SecurityWarnings: createSecurityWarnings("1.2", "2.2")},
{Name: "workflow-cps", SecurityWarnings: createSecurityWarnings("2.59", "")}, {Name: "workflow-cps", SecurityWarnings: createSecurityWarnings("2.59", "")},
{Name: "git-client"}, {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) { 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: "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: "workflow-cps", SecurityWarnings: createSecurityWarnings("2.59", "")},
{Name: "resource-disposer", SecurityWarnings: createSecurityWarnings("0.7", "1.2")}, {Name: "resource-disposer", SecurityWarnings: createSecurityWarnings("0.7", "1.2")},

View File

@ -23,7 +23,7 @@ package v1alpha2
import ( import (
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/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. // 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)) *out = make([]Plugin, len(*in))
copy(*out, *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. // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JenkinsMaster.
@ -528,6 +535,65 @@ func (in *Plugin) DeepCopy() *Plugin {
return out 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. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Restore) DeepCopyInto(out *Restore) { func (in *Restore) DeepCopyInto(out *Restore) {
*out = *in *out = *in
@ -593,6 +659,22 @@ func (in *SecretRef) DeepCopy() *SecretRef {
return out 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. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SeedJob) DeepCopyInto(out *SeedJob) { func (in *SeedJob) DeepCopyInto(out *SeedJob) {
*out = *in *out = *in
@ -679,3 +761,38 @@ func (in *Slack) DeepCopy() *Slack {
in.DeepCopyInto(out) in.DeepCopyInto(out)
return 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
}

View File

@ -36,10 +36,6 @@ spec:
spec: spec:
description: Spec defines the desired state of the Jenkins description: Spec defines the desired state of the Jenkins
properties: properties:
ValidateSecurityWarnings:
description: ValidateSecurityWarnings enables or disables validating
potential security warnings in Jenkins plugins via admission webhooks.
type: boolean
backup: backup:
description: 'Backup defines configuration of Jenkins backup More description: 'Backup defines configuration of Jenkins backup More
info: https://jenkinsci.github.io/kubernetes-operator/docs/getting-started/latest/configure-backup-and-restore/' 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 description: DisableCSRFProtection allows you to toggle CSRF Protection
on Jenkins on Jenkins
type: boolean 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: imagePullSecrets:
description: 'ImagePullSecrets is an optional list of references description: 'ImagePullSecrets is an optional list of references
to secrets in the same namespace to use for pulling any of the 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' More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services---service-types'
type: string type: string
type: object type: object
validateSecurityWarnings:
description: ValidateSecurityWarnings enables or disables validating
potential security warnings in Jenkins plugins via admission webhooks.
type: boolean
required: required:
- jenkinsAPISettings - jenkinsAPISettings
- master - master

View File

@ -145,7 +145,7 @@ spec:
securityContext: securityContext:
{{- toYaml . | nindent 6 }} {{- toYaml . | nindent 6 }}
{{- end }} {{- end }}
ValidateSecurityWarnings: {{ .Values.jenkins.ValidateSecurityWarnings }} validateSecurityWarnings: {{ .Values.jenkins.validateSecurityWarnings }}
{{- with .Values.jenkins.seedJobs }} {{- with .Values.jenkins.seedJobs }}
seedJobs: {{- toYaml . | nindent 4 }} seedJobs: {{- toYaml . | nindent 4 }}
{{- end }} {{- end }}

View File

@ -48,8 +48,8 @@ jenkins:
disableCSRFProtection: false disableCSRFProtection: false
# ValidateSecurityWarnings enables or disables validating potential security warnings in Jenkins plugins via admission webhooks. # validateSecurityWarnings enables or disables validating potential security warnings in Jenkins plugins via admission webhooks.
ValidateSecurityWarnings: false validateSecurityWarnings: false
# imagePullSecrets is used if you want to pull images from private repository # 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 # 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 # See https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/ for details
resources: resources:
limits: limits:
cpu: 1500m cpu: 1000m
memory: 3Gi memory: 3Gi
requests: requests:
cpu: 1 cpu: 1
@ -146,26 +146,26 @@ jenkins:
# LivenessProbe for Jenkins Master pod # LivenessProbe for Jenkins Master pod
livenessProbe: livenessProbe:
failureThreshold: 12 failureThreshold: 20
httpGet: httpGet:
path: /login path: /login
port: http port: http
scheme: HTTP scheme: HTTP
initialDelaySeconds: 80 initialDelaySeconds: 100
periodSeconds: 10 periodSeconds: 10
successThreshold: 1 successThreshold: 1
timeoutSeconds: 5 timeoutSeconds: 8
# ReadinessProbe for Jenkins Master pod # ReadinessProbe for Jenkins Master pod
readinessProbe: readinessProbe:
failureThreshold: 3 failureThreshold: 60
httpGet: httpGet:
path: /login path: /login
port: http port: http
scheme: HTTP scheme: HTTP
initialDelaySeconds: 30 initialDelaySeconds: 120
periodSeconds: 10 periodSeconds: 10
successThreshold: 1 successThreshold: 1
timeoutSeconds: 1 timeoutSeconds: 8
# backup is section for configuring operator's backup feature # backup is section for configuring operator's backup feature
# By default backup feature is enabled and pre-configured # By default backup feature is enabled and pre-configured
@ -215,11 +215,11 @@ jenkins:
# resources used by backup container # resources used by backup container
resources: resources:
limits: limits:
cpu: 1500m cpu: 1000m
memory: 1Gi memory: 2Gi
requests: requests:
cpu: 100m cpu: 100m
memory: 256Mi memory: 500Mi
# env contains container environment variables # env contains container environment variables
# PVC backup provider handles these variables: # PVC backup provider handles these variables:

View File

@ -36,10 +36,6 @@ spec:
spec: spec:
description: Spec defines the desired state of the Jenkins description: Spec defines the desired state of the Jenkins
properties: properties:
ValidateSecurityWarnings:
description: ValidateSecurityWarnings enables or disables validating
potential security warnings in Jenkins plugins via admission webhooks.
type: boolean
backup: backup:
description: 'Backup defines configuration of Jenkins backup More description: 'Backup defines configuration of Jenkins backup More
info: https://jenkinsci.github.io/kubernetes-operator/docs/getting-started/latest/configure-backup-and-restore/' 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' More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services---service-types'
type: string type: string
type: object type: object
validateSecurityWarnings:
description: ValidateSecurityWarnings enables or disables validating
potential security warnings in Jenkins plugins via admission webhooks.
type: boolean
required: required:
- jenkinsAPISettings - jenkinsAPISettings
- master - master

14
main.go
View File

@ -78,7 +78,7 @@ func main() {
var metricsAddr string var metricsAddr string
var enableLeaderElection bool var enableLeaderElection bool
var probeAddr string var probeAddr string
var ValidateSecurityWarnings bool var validateSecurityWarnings bool
isRunningInCluster, err := resources.IsRunningInCluster() isRunningInCluster, err := resources.IsRunningInCluster()
if err != nil { 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.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. "+ flag.BoolVar(&enableLeaderElection, "leader-elect", isRunningInCluster, "Enable leader election for controller manager. "+
"Enabling this will ensure there is only one active 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.") 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.") 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.") 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)) logger.Info(fmt.Sprintf("Watch namespace: %v", namespace))
if ValidateSecurityWarnings { if validateSecurityWarnings {
isInitialized := make(chan bool) securityWarningsFetched := make(chan bool)
go v1alpha2.PluginsMgr.ManagePluginData(isInitialized) go v1alpha2.SecValidator.MonitorSecurityWarnings(securityWarningsFetched)
if !<-isInitialized { if !<-securityWarningsFetched {
logger.Info("Unable to get the plugins data") logger.Info("Unable to get the plugins data")
} }
} }
@ -180,7 +180,7 @@ func main() {
fatal(errors.Wrap(err, "unable to create Jenkins controller"), *debug) fatal(errors.Wrap(err, "unable to create Jenkins controller"), *debug)
} }
if ValidateSecurityWarnings { if validateSecurityWarnings {
if err = (&v1alpha2.Jenkins{}).SetupWebhookWithManager(mgr); err != nil { if err = (&v1alpha2.Jenkins{}).SetupWebhookWithManager(mgr); err != nil {
fatal(errors.Wrap(err, "unable to create Webhook"), *debug) fatal(errors.Wrap(err, "unable to create Webhook"), *debug)
} }

View File

@ -1,11 +1,13 @@
package e2e package e2e
import ( import (
"context"
"fmt" "fmt"
"github.com/jenkinsci/kubernetes-operator/api/v1alpha2" "github.com/jenkinsci/kubernetes-operator/api/v1alpha2"
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
// +kubebuilder:scaffold:imports // +kubebuilder:scaffold:imports
) )
@ -74,10 +76,10 @@ var _ = Describe("Jenkins controller configuration", func() {
BeforeEach(func() { BeforeEach(func() {
namespace = CreateNamespace() namespace = CreateNamespace()
createUserConfigurationSecret(namespace.Name, userConfigurationSecretData) createUserConfigurationSecret(namespace.Name, userConfigurationSecretData)
createUserConfigurationConfigMap(namespace.Name, numberOfExecutorsEnvName, fmt.Sprintf("${%s}", systemMessageEnvName)) 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) createDefaultLimitsForContainersInNamespace(namespace.Name)
createKubernetesCredentialsProviderSecret(namespace.Name, mySeedJob) createKubernetesCredentialsProviderSecret(namespace.Name, mySeedJob)
}) })
@ -125,7 +127,8 @@ var _ = Describe("Jenkins controller priority class", func() {
BeforeEach(func() { BeforeEach(func() {
namespace = CreateNamespace() 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() { AfterEach(func() {
@ -177,7 +180,8 @@ var _ = Describe("Jenkins controller plugins test", func() {
BeforeEach(func() { BeforeEach(func() {
namespace = CreateNamespace() 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() { AfterEach(func() {

View File

@ -1,6 +1,8 @@
package e2e package e2e
import ( import (
"context"
"github.com/jenkinsci/kubernetes-operator/api/v1alpha2" "github.com/jenkinsci/kubernetes-operator/api/v1alpha2"
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
@ -35,7 +37,8 @@ var _ = Describe("Jenkins controller", func() {
namespace = CreateNamespace() namespace = CreateNamespace()
configureAuthorizationToUnSecure(namespace.Name, userConfigurationConfigMapName) 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() { AfterEach(func() {

View File

@ -15,6 +15,7 @@ import (
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1" rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
@ -46,112 +47,6 @@ func getJenkinsMasterPod(jenkins *v1alpha2.Jenkins) *corev1.Pod {
return &pods.Items[0] 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 { func createJenkinsCRSafeRestart(name, namespace string, seedJob *[]v1alpha2.SeedJob, groovyScripts v1alpha2.GroovyScripts, casc v1alpha2.ConfigurationAsCode, priorityClassName string) *v1alpha2.Jenkins {
var seedJobs []v1alpha2.SeedJob var seedJobs []v1alpha2.SeedJob
if seedJob != nil { if seedJob != nil {
@ -172,6 +67,7 @@ func createJenkinsCRSafeRestart(name, namespace string, seedJob *[]v1alpha2.Seed
Containers: []v1alpha2.Container{ Containers: []v1alpha2.Container{
{ {
Name: resources.JenkinsMasterContainerName, Name: resources.JenkinsMasterContainerName,
Image: JenkinsTestImage,
Env: []corev1.EnvVar{ Env: []corev1.EnvVar{
{ {
Name: "TEST_ENV", Name: "TEST_ENV",
@ -206,6 +102,16 @@ func createJenkinsCRSafeRestart(name, namespace string, seedJob *[]v1alpha2.Seed
SuccessThreshold: int32(1), SuccessThreshold: int32(1),
PeriodSeconds: int32(5), 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{ VolumeMounts: []corev1.VolumeMount{
{ {
Name: "plugins-cache", Name: "plugins-cache",

View File

@ -8,7 +8,6 @@ import (
"sort" "sort"
"github.com/onsi/ginkgo" "github.com/onsi/ginkgo"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/api/events/v1beta1" "k8s.io/api/events/v1beta1"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
@ -109,17 +108,18 @@ func printKubernetesEvents(namespace string) {
_, _ = fmt.Fprintf(ginkgo.GinkgoWriter, "Last %d events from kubernetes:\n", kubernetesEventsLimit) _, _ = fmt.Fprintf(ginkgo.GinkgoWriter, "Last %d events from kubernetes:\n", kubernetesEventsLimit)
for _, event := range events { 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) { 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) pod, err := getOperatorPod(namespace)
if err == nil { if err == nil {
_, _ = fmt.Fprintf(ginkgo.GinkgoWriter, "%+v\n\n", pod) _, _ = fmt.Fprintf(ginkgo.GinkgoWriter, "%s: %+v \n", pod.Name, pod.Status.Conditions)
} }
} }

View File

@ -109,6 +109,7 @@ func createJenkinsWithBackupAndRestoreConfigured(name, namespace string) *v1alph
Containers: []v1alpha2.Container{ Containers: []v1alpha2.Container{
{ {
Name: resources.JenkinsMasterContainerName, Name: resources.JenkinsMasterContainerName,
Image: JenkinsTestImage,
VolumeMounts: []corev1.VolumeMount{ VolumeMounts: []corev1.VolumeMount{
{ {
Name: "plugins-cache", Name: "plugins-cache",
@ -143,6 +144,16 @@ func createJenkinsWithBackupAndRestoreConfigured(name, namespace string) *v1alph
SuccessThreshold: int32(1), SuccessThreshold: int32(1),
PeriodSeconds: int32(5), 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, Name: containerName,

View File

@ -5,15 +5,24 @@ import (
"fmt" "fmt"
"time" "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/ginkgo"
"github.com/onsi/gomega" "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1" 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" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest" "sigs.k8s.io/controller-runtime/pkg/envtest"
) )
const JenkinsTestImage = "jenkins/jenkins:2.303.2-lts"
var ( var (
Cfg *rest.Config Cfg *rest.Config
K8sClient client.Client K8sClient client.Client
@ -59,3 +68,113 @@ func DestroyNamespace(namespace *corev1.Namespace) {
return !exists, nil return !exists, nil
}, time.Second*120, time.Second).Should(gomega.BeTrue()) }, 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",
},
},
},
}
}

View File

@ -7,20 +7,18 @@ import (
"time" "time"
"github.com/jenkinsci/kubernetes-operator/api/v1alpha2" "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" "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/ginkgo"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// +kubebuilder:scaffold:imports // +kubebuilder:scaffold:imports
) )
var _ = Describe("Jenkins Controller with webhook", func() { const jenkinsCRName = "jenkins"
var _ = Describe("Jenkins Controller", func() {
var ( var (
namespace *corev1.Namespace namespace *corev1.Namespace
) )
@ -42,14 +40,15 @@ var _ = Describe("Jenkins Controller with webhook", func() {
jenkins := &v1alpha2.Jenkins{ jenkins := &v1alpha2.Jenkins{
TypeMeta: v1alpha2.JenkinsTypeMeta(), TypeMeta: v1alpha2.JenkinsTypeMeta(),
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: "jenkins", Name: jenkinsCRName,
Namespace: namespace.Name, Namespace: namespace.Name,
}, },
} }
cmd := exec.Command("../../bin/helm", "upgrade", "jenkins", "../../chart/jenkins-operator", "--namespace", namespace.Name, "--debug", 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("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() output, err := cmd.CombinedOutput()
Expect(err).NotTo(HaveOccurred(), string(output)) 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() { var _ = Describe("Jenkins Controller with security validator", func() {
It("Deploys operator,denies creating a jenkins cr and creates jenkins cr with validation turned off", func() {
By("Deploying the operator along with webhook and cert-manager") var (
cmd := exec.Command("../../bin/helm", "upgrade", "jenkins", "../../chart/jenkins-operator", "--namespace", namespace.Name, "--debug", namespace *corev1.Namespace
"--set-string", fmt.Sprintf("jenkins.namespace=%s", namespace.Name), "--set-string", fmt.Sprintf("operator.image=%s", *imageName), seedJobs = &[]v1alpha2.SeedJob{}
"--set", fmt.Sprintf("webhook.enabled=%t", true), "--set", fmt.Sprintf("jenkins.enabled=%t", false), "--install", "--wait") groovyScripts = v1alpha2.GroovyScripts{
output, err := cmd.CombinedOutput() Customization: v1alpha2.Customization{
Expect(err).NotTo(HaveOccurred(), string(output)) Configurations: []v1alpha2.ConfigMapRef{},
},
By("Waiting for the operator to fetch the plugin data ") }
time.Sleep(time.Duration(200) * time.Second) casc = v1alpha2.ConfigurationAsCode{
Customization: v1alpha2.Customization{
By("Denying a create request for a Jenkins custom resource with some plugins having security warnings and validation is turned on") Configurations: []v1alpha2.ConfigMapRef{},
userplugins := []v1alpha2.Plugin{ },
}
invalidPlugins = []v1alpha2.Plugin{
{Name: "simple-theme-plugin", Version: "0.6"}, {Name: "simple-theme-plugin", Version: "0.6"},
{Name: "audit-trail", Version: "3.5"}, {Name: "audit-trail", Version: "3.5"},
{Name: "github", Version: "1.29.0"}, {Name: "github", Version: "1.29.0"},
} }
jenkins := CreateJenkinsCR("jenkins", namespace.Name, userplugins, true) validPlugins = []v1alpha2.Plugin{
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: "simple-theme-plugin", Version: "0.6"},
{Name: "audit-trail", Version: "3.8"}, {Name: "audit-trail", Version: "3.8"},
{Name: "github", Version: "1.31.0"}, {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)
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)
}) })
It("Deploys operator, creates a jenkins cr and denies update request for another one", func() { 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") By("Deploying the operator along with webhook and cert-manager")
cmd := exec.Command("../../bin/helm", "upgrade", "jenkins", "../../chart/jenkins-operator", "--namespace", namespace.Name, "--debug", 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-string", fmt.Sprintf("jenkins.namespace=%s", namespace.Name),
"--set", fmt.Sprintf("webhook.enabled=%t", true), "--set", fmt.Sprintf("jenkins.enabled=%t", false), "--install", "--wait") "--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")
time.Sleep(time.Duration(200) * time.Second)
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"))
})
})
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")
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
Expect(err).NotTo(HaveOccurred(), string(output)) Expect(err).NotTo(HaveOccurred(), string(output))
@ -106,129 +135,12 @@ var _ = Describe("Jenkins Controller with webhook", func() {
time.Sleep(time.Duration(200) * time.Second) time.Sleep(time.Duration(200) * time.Second)
By("Creating a Jenkins custom resource with some plugins having security warnings but validation is turned off") By("Creating a Jenkins custom resource with some plugins having security warnings but validation is turned off")
userplugins := []v1alpha2.Plugin{ jenkins := e2e.RenderJenkinsCR(jenkinsCRName, namespace.Name, seedJobs, groovyScripts, casc, "")
{Name: "simple-theme-plugin", Version: "0.6"}, jenkins.Spec.Master.Plugins = validPlugins
{Name: "audit-trail", Version: "3.5"}, jenkins.Spec.ValidateSecurityWarnings = true
{Name: "github", Version: "1.29.0"},
}
jenkins := CreateJenkinsCR("jenkins", namespace.Name, userplugins, false)
Expect(e2e.K8sClient.Create(context.TODO(), jenkins)).Should(Succeed()) Expect(e2e.K8sClient.Create(context.TODO(), jenkins)).Should(Succeed())
e2e.WaitForJenkinsBaseConfigurationToComplete(jenkins) e2e.WaitForJenkinsBaseConfigurationToComplete(jenkins)
e2e.WaitForJenkinsUserConfigurationToComplete(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
}