diff --git a/docs/getting-started.md b/docs/getting-started.md index de7c29df..aff5ecf2 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -6,9 +6,8 @@ This document describes a getting started guide for **jenkins-operator** and an 2. [Deploy Jenkins](#deploy-jenkins) 3. [Configure Seed Jobs and Pipelines](#configure-seed-jobs-and-pipelines) 4. [Install Plugins](#install-plugins) -5. [Configure Authorization](#configure-authorization) -6. [Configure Backup & Restore](#configure-backup-&-restore) -7. [Debugging](#debugging) +5. [Configure Backup & Restore](#configure-backup-&-restore) +6. [Debugging](#debugging) ## First Steps @@ -256,6 +255,27 @@ When **jenkins-operator-user-configuration-example** ConfigMap is updated Jenkin ## Install Plugins +### Via CR + +Edit CR under `spec.master.plugins`: + +``` +apiVersion: jenkins.io/v1alpha1 +kind: Jenkins +metadata: + name: example +spec: + master: + image: jenkins/jenkins:lts + plugins: + configuration-as-code:1.4: + - configuration-as-code-support:1.4 +``` + +Then **jenkins-operator** will automatically install plugins after Jenkins master pod restart. + +### Via groovy script + To install a plugin please add **2-install-slack-plugin.groovy** script to the **jenkins-operator-user-configuration-example** ConfigMap: ``` @@ -318,7 +338,7 @@ data: Then **jenkins-operator** will automatically trigger **jenkins-operator-user-configuration** Jenkins Job again. -## Configure Backup & Restore (work in progress) +## Configure Backup & Restore Not implemented yet. diff --git a/pkg/apis/jenkinsio/v1alpha1/jenkins_types.go b/pkg/apis/jenkinsio/v1alpha1/jenkins_types.go index e4dd8cda..8fe05885 100644 --- a/pkg/apis/jenkinsio/v1alpha1/jenkins_types.go +++ b/pkg/apis/jenkinsio/v1alpha1/jenkins_types.go @@ -22,7 +22,10 @@ type JenkinsMaster struct { Image string `json:"image,omitempty"` Annotations map[string]string `json:"masterAnnotations,omitempty"` Resources corev1.ResourceRequirements `json:"resources,omitempty"` - Plugins map[string][]string `json:"plugins,omitempty"` + // OperatorPlugins contains plugins required by operator + OperatorPlugins map[string][]string `json:"basePlugins,omitempty"` + // Plugins contains plugins required by user + Plugins map[string][]string `json:"plugins,omitempty"` } // JenkinsStatus defines the observed state of Jenkins diff --git a/pkg/apis/jenkinsio/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/jenkinsio/v1alpha1/zz_generated.deepcopy.go index f283db4f..2aa2f6c9 100644 --- a/pkg/apis/jenkinsio/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/jenkinsio/v1alpha1/zz_generated.deepcopy.go @@ -121,6 +121,21 @@ func (in *JenkinsMaster) DeepCopyInto(out *JenkinsMaster) { } } in.Resources.DeepCopyInto(&out.Resources) + if in.OperatorPlugins != nil { + in, out := &in.OperatorPlugins, &out.OperatorPlugins + *out = make(map[string][]string, len(*in)) + for key, val := range *in { + var outVal []string + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = make([]string, len(*in)) + copy(*out, *in) + } + (*out)[key] = outVal + } + } if in.Plugins != nil { in, out := &in.Plugins, &out.Plugins *out = make(map[string][]string, len(*in)) diff --git a/pkg/controller/jenkins/configuration/base/reconcile.go b/pkg/controller/jenkins/configuration/base/reconcile.go index 0ecb2099..b08bdb22 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile.go +++ b/pkg/controller/jenkins/configuration/base/reconcile.go @@ -85,12 +85,13 @@ func (r *ReconcileJenkinsBaseConfiguration) Reconcile() (reconcile.Result, jenki } r.logger.V(log.VDebug).Info("Jenkins API client set") - ok, err := r.verifyPlugins(jenkinsClient, plugins.BasePluginsMap) + ok, err := r.verifyPlugins(jenkinsClient) if err != nil { return reconcile.Result{}, nil, err } if !ok { - r.logger.V(log.VWarn).Info("Please correct Jenkins CR (spec.master.plugins)") + r.logger.V(log.VWarn).Info("Please correct Jenkins CR(spec.master.OperatorPlugins or spec.master.plugins)") + // TODO inform user via Admin Monitor and don't restart Jenkins return reconcile.Result{Requeue: true}, nil, r.restartJenkinsMasterPod(metaObject) } @@ -137,7 +138,7 @@ func (r *ReconcileJenkinsBaseConfiguration) ensureResourcesRequiredForJenkinsPod return nil } -func (r *ReconcileJenkinsBaseConfiguration) verifyPlugins(jenkinsClient jenkinsclient.Jenkins, allRequiredPlugins ...map[string][]plugins.Plugin) (bool, error) { +func (r *ReconcileJenkinsBaseConfiguration) verifyPlugins(jenkinsClient jenkinsclient.Jenkins) (bool, error) { allPluginsInJenkins, err := jenkinsClient.GetPlugins(fetchAllPlugins) if err != nil { return false, err @@ -151,7 +152,21 @@ func (r *ReconcileJenkinsBaseConfiguration) verifyPlugins(jenkinsClient jenkinsc } r.logger.V(log.VDebug).Info(fmt.Sprintf("Installed plugins '%+v'", installedPlugins)) + userPlugins := map[string][]plugins.Plugin{} + for rootPlugin, dependentPluginNames := range r.jenkins.Spec.Master.Plugins { + var dependentPlugins []plugins.Plugin + for _, pluginNameWithVersion := range dependentPluginNames { + plugin, err := plugins.New(pluginNameWithVersion) + if err != nil { + return false, err + } + dependentPlugins = append(dependentPlugins, *plugin) + } + userPlugins[rootPlugin] = dependentPlugins + } + status := true + allRequiredPlugins := []map[string][]plugins.Plugin{plugins.BasePluginsMap, userPlugins} for _, requiredPlugins := range allRequiredPlugins { for rootPluginName, p := range requiredPlugins { rootPlugin, _ := plugins.New(rootPluginName) diff --git a/pkg/controller/jenkins/configuration/base/resources/scripts_configmap.go b/pkg/controller/jenkins/configuration/base/resources/scripts_configmap.go index f88ac74f..a0d6e6b4 100644 --- a/pkg/controller/jenkins/configuration/base/resources/scripts_configmap.go +++ b/pkg/controller/jenkins/configuration/base/resources/scripts_configmap.go @@ -254,12 +254,19 @@ chmod +x {{ .JenkinsHomePath }}/scripts/*.sh {{- $jenkinsHomePath := .JenkinsHomePath }} {{- $installPluginsCommand := .InstallPluginsCommand }} -echo "Installing plugins - begin" -{{- range $rootPluginName, $plugins := .Plugins }} +echo "Installing plugins required by Operator - begin" +{{- range $rootPluginName, $plugins := .OperatorPlugins }} echo "Installing required plugins for '{{ $rootPluginName }}'" {{ $jenkinsHomePath }}/scripts/{{ $installPluginsCommand }} {{ $rootPluginName }} {{ range $index, $plugin := $plugins }}{{ . }} {{ end }} {{- end }} -echo "Installing plugins - end" +echo "Installing plugins required by Operator - end" + +echo "Installing plugins required by user - begin" +{{- range $rootPluginName, $plugins := .UserPlugins }} +echo "Installing required plugins for '{{ $rootPluginName }}'" +{{ $jenkinsHomePath }}/scripts/{{ $installPluginsCommand }} {{ $rootPluginName }} {{ range $index, $plugin := $plugins }}{{ . }} {{ end }} +{{- end }} +echo "Installing plugins required by user - end" /sbin/tini -s -- /usr/local/bin/jenkins.sh `)) @@ -271,17 +278,19 @@ func buildConfigMapTypeMeta() metav1.TypeMeta { } } -func buildInitBashScript(pluginsToInstall map[string][]string) (*string, error) { +func buildInitBashScript(jenkins *v1alpha1.Jenkins) (*string, error) { data := struct { JenkinsHomePath string InitConfigurationPath string InstallPluginsCommand string JenkinsScriptsVolumePath string - Plugins map[string][]string + OperatorPlugins map[string][]string + UserPlugins map[string][]string }{ JenkinsHomePath: jenkinsHomePath, InitConfigurationPath: jenkinsInitConfigurationVolumePath, - Plugins: pluginsToInstall, + OperatorPlugins: jenkins.Spec.Master.OperatorPlugins, + UserPlugins: jenkins.Spec.Master.Plugins, InstallPluginsCommand: installPluginsCommand, JenkinsScriptsVolumePath: jenkinsScriptsVolumePath, } @@ -302,7 +311,7 @@ func getScriptsConfigMapName(jenkins *v1alpha1.Jenkins) string { func NewScriptsConfigMap(meta metav1.ObjectMeta, jenkins *v1alpha1.Jenkins) (*corev1.ConfigMap, error) { meta.Name = getScriptsConfigMapName(jenkins) - initBashScript, err := buildInitBashScript(jenkins.Spec.Master.Plugins) + initBashScript, err := buildInitBashScript(jenkins) if err != nil { return nil, err } diff --git a/pkg/controller/jenkins/configuration/base/validate.go b/pkg/controller/jenkins/configuration/base/validate.go index d5f37766..4bd7f1b1 100644 --- a/pkg/controller/jenkins/configuration/base/validate.go +++ b/pkg/controller/jenkins/configuration/base/validate.go @@ -28,34 +28,39 @@ func (r *ReconcileJenkinsBaseConfiguration) Validate(jenkins *v1alpha1.Jenkins) } - if !r.validatePlugins(jenkins.Spec.Master.Plugins) { + if !r.validatePlugins(jenkins.Spec.Master.OperatorPlugins, jenkins.Spec.Master.Plugins) { return false, nil } return true, nil } -func (r *ReconcileJenkinsBaseConfiguration) validatePlugins(pluginsWithVersions map[string][]string) bool { +func (r *ReconcileJenkinsBaseConfiguration) validatePlugins(pluginsWithVersionSlice ...map[string][]string) bool { valid := true - allPlugins := map[string][]plugins.Plugin{} + allPlugins := map[plugins.Plugin][]plugins.Plugin{} - for rootPluginName, dependentPluginNames := range pluginsWithVersions { - if _, err := plugins.New(rootPluginName); err != nil { - r.logger.V(log.VWarn).Info(fmt.Sprintf("Invalid root plugin name '%s'", rootPluginName)) - valid = false - } - - dependentPlugins := []plugins.Plugin{} - for _, pluginName := range dependentPluginNames { - if p, err := plugins.New(pluginName); err != nil { - r.logger.V(log.VWarn).Info(fmt.Sprintf("Invalid dependent plugin name '%s' in root plugin '%s'", pluginName, rootPluginName)) + for _, pluginsWithVersions := range pluginsWithVersionSlice { + for rootPluginName, dependentPluginNames := range pluginsWithVersions { + rootPlugin, err := plugins.New(rootPluginName) + if err != nil { + r.logger.V(log.VWarn).Info(fmt.Sprintf("Invalid root plugin name '%s'", rootPluginName)) valid = false - } else { - dependentPlugins = append(dependentPlugins, *p) + } + + var dependentPlugins []plugins.Plugin + for _, pluginName := range dependentPluginNames { + if p, err := plugins.New(pluginName); err != nil { + r.logger.V(log.VWarn).Info(fmt.Sprintf("Invalid dependent plugin name '%s' in root plugin '%s'", pluginName, rootPluginName)) + valid = false + } else { + dependentPlugins = append(dependentPlugins, *p) + } + } + + if rootPlugin != nil { + allPlugins[*rootPlugin] = dependentPlugins } } - - allPlugins[rootPluginName] = dependentPlugins } if valid { diff --git a/pkg/controller/jenkins/configuration/base/validate_test.go b/pkg/controller/jenkins/configuration/base/validate_test.go index a44bf5c7..6df2c8e2 100644 --- a/pkg/controller/jenkins/configuration/base/validate_test.go +++ b/pkg/controller/jenkins/configuration/base/validate_test.go @@ -1,7 +1,6 @@ package base import ( - "fmt" "testing" "github.com/stretchr/testify/assert" @@ -9,50 +8,60 @@ import ( ) func TestValidatePlugins(t *testing.T) { - data := []struct { - plugins map[string][]string - expectedResult bool - }{ - { - plugins: map[string][]string{ - "valid-plugin-name:1.0": { - "valid-plugin-name:1.0", - }, - }, - expectedResult: true, - }, - { - plugins: map[string][]string{ - "invalid-plugin-name": { - "invalid-plugin-name", - }, - }, - expectedResult: false, - }, - { - plugins: map[string][]string{ - "valid-plugin-name:1.0": { - "valid-plugin-name:1.0", - "valid-plugin-name2:1.0", - }, - }, - expectedResult: true, - }, - { - plugins: map[string][]string{ - "valid-plugin-name:1.0": {}, - }, - expectedResult: true, - }, - } - baseReconcileLoop := New(nil, nil, logf.ZapLogger(false), nil, false, false) + t.Run("happy", func(t *testing.T) { + plugins := map[string][]string{ + "valid-plugin-name:1.0": { + "valid-plugin-name:1.0", + }, + } + got := baseReconcileLoop.validatePlugins(plugins) + assert.Equal(t, true, got) + }) + t.Run("fail, no version in plugin name", func(t *testing.T) { + plugins := map[string][]string{ + "invalid-plugin-name": { + "invalid-plugin-name", + }, + } + got := baseReconcileLoop.validatePlugins(plugins) + assert.Equal(t, false, got) + }) + t.Run("fail, no version in root plugin name", func(t *testing.T) { + plugins := map[string][]string{ + "invalid-plugin-name": { + "invalid-plugin-name:1.0", + }, + } + got := baseReconcileLoop.validatePlugins(plugins) + assert.Equal(t, false, got) + }) + t.Run("fail, no version in plugin name", func(t *testing.T) { + plugins := map[string][]string{ + "invalid-plugin-name:1.0": { + "invalid-plugin-name", + }, + } + got := baseReconcileLoop.validatePlugins(plugins) + assert.Equal(t, false, got) + }) + t.Run("happy", func(t *testing.T) { + plugins := map[string][]string{ + "valid-plugin-name:1.0": { + "valid-plugin-name:1.0", + "valid-plugin-name2:1.0", + }, + } + got := baseReconcileLoop.validatePlugins(plugins) + assert.Equal(t, true, got) + }) + t.Run("hapy", func(t *testing.T) { + plugins := map[string][]string{ + "valid-plugin-name:1.0": {}, + } + got := baseReconcileLoop.validatePlugins(plugins) + assert.Equal(t, true, got) + }) - for index, testingData := range data { - t.Run(fmt.Sprintf("Testing %d plugins set", index), func(t *testing.T) { - result := baseReconcileLoop.validatePlugins(testingData.plugins) - assert.Equal(t, testingData.expectedResult, result) - }) - } } diff --git a/pkg/controller/jenkins/jenkins_controller.go b/pkg/controller/jenkins/jenkins_controller.go index 6d12d368..09398bf1 100644 --- a/pkg/controller/jenkins/jenkins_controller.go +++ b/pkg/controller/jenkins/jenkins_controller.go @@ -212,10 +212,14 @@ func (r *ReconcileJenkins) setDefaults(jenkins *v1alpha1.Jenkins, logger logr.Lo changed = true jenkins.Spec.Master.Image = constants.DefaultJenkinsMasterImage } - if len(jenkins.Spec.Master.Plugins) == 0 { + if len(jenkins.Spec.Master.OperatorPlugins) == 0 { logger.Info("Setting default base plugins") changed = true - jenkins.Spec.Master.Plugins = plugins.BasePlugins() + jenkins.Spec.Master.OperatorPlugins = plugins.BasePlugins() + } + if len(jenkins.Spec.Master.Plugins) == 0 { + changed = true + jenkins.Spec.Master.Plugins = map[string][]string{"simple-theme-plugin:0.5.1": {}} } _, requestCPUSet := jenkins.Spec.Master.Resources.Requests[corev1.ResourceCPU] _, requestMemporySet := jenkins.Spec.Master.Resources.Requests[corev1.ResourceMemory] diff --git a/pkg/controller/jenkins/plugins/base_plugins.go b/pkg/controller/jenkins/plugins/base_plugins.go index f962d273..a2432d4a 100644 --- a/pkg/controller/jenkins/plugins/base_plugins.go +++ b/pkg/controller/jenkins/plugins/base_plugins.go @@ -1,56 +1,72 @@ package plugins const ( - // ApacheComponentsClientPlugin is apache-httpcomponents-client-4-api Jenkins plugin with version - ApacheComponentsClientPlugin = "apache-httpcomponents-client-4-api:4.5.5-3.0" - // Jackson2ADIPlugin is jackson2-api-httpcomponents-client-4-api Jenkins plugin with version - Jackson2ADIPlugin = "jackson2-api:2.9.8" + apacheComponentsClientPlugin = "apache-httpcomponents-client-4-api:4.5.5-3.0" + jackson2ADIPlugin = "jackson2-api:2.9.8" + credentialsPlugin = "credentials:2.1.18" + cloudBeesFolderPlugin = "cloudbees-folder:6.7" + durableTaskPlugin = "durable-task:1.28" + plainCredentialsPlugin = "plain-credentials:1.5" + structsPlugin = "structs:1.17" + workflowStepAPIPlugin = "workflow-step-api:2.17" + scmAPIPlugin = "scm-api:2.3.0" + workflowAPIPlugin = "workflow-api:2.33" + workflowSupportPlugin = "workflow-support:3.0" + displayURLAPIPlugin = "display-url-api:2.3.0" + gitClientPlugin = "git-client:2.7.6" + jschPlugin = "jsch:0.1.55" + junitPlugin = "junit:1.26.1" + mailerPlugin = "mailer:1.23" + matrixProjectPlugin = "matrix-project:1.13" + scriptSecurityPlugin = "script-security:1.50" + sshCredentialsPlugin = "ssh-credentials:1.14" + workflowSCMStepPlugin = "workflow-scm-step:2.7" ) // BasePluginsMap contains plugins to install by operator var BasePluginsMap = map[string][]Plugin{ Must(New("kubernetes:1.13.8")).String(): { - Must(New(ApacheComponentsClientPlugin)), - Must(New("cloudbees-folder:6.7")), - Must(New("credentials:2.1.18")), - Must(New("durable-task:1.28")), - Must(New(Jackson2ADIPlugin)), + Must(New(apacheComponentsClientPlugin)), + Must(New(cloudBeesFolderPlugin)), + Must(New(credentialsPlugin)), + Must(New(durableTaskPlugin)), + Must(New(jackson2ADIPlugin)), Must(New("kubernetes-credentials:0.4.0")), - Must(New("plain-credentials:1.5")), - Must(New("structs:1.17")), + Must(New(plainCredentialsPlugin)), + Must(New(structsPlugin)), Must(New("variant:1.1")), - Must(New("workflow-step-api:2.17")), + Must(New(workflowStepAPIPlugin)), }, Must(New("workflow-job:2.31")).String(): { - Must(New("scm-api:2.3.0")), - Must(New("script-security:1.50")), - Must(New("structs:1.17")), - Must(New("workflow-api:2.33")), - Must(New("workflow-step-api:2.17")), - Must(New("workflow-support:3.0")), + Must(New(scmAPIPlugin)), + Must(New(scriptSecurityPlugin)), + Must(New(structsPlugin)), + Must(New(workflowAPIPlugin)), + Must(New(workflowStepAPIPlugin)), + Must(New(workflowSupportPlugin)), }, Must(New("workflow-aggregator:2.6")).String(): { Must(New("ace-editor:1.1")), - Must(New(ApacheComponentsClientPlugin)), + Must(New(apacheComponentsClientPlugin)), Must(New("authentication-tokens:1.3")), Must(New("branch-api:2.1.2")), - Must(New("cloudbees-folder:6.7")), + Must(New(cloudBeesFolderPlugin)), Must(New("credentials-binding:1.17")), - Must(New("credentials:2.1.18")), - Must(New("display-url-api:2.3.0")), + Must(New(credentialsPlugin)), + Must(New(displayURLAPIPlugin)), Must(New("docker-commons:1.13")), Must(New("docker-workflow:1.17")), - Must(New("durable-task:1.28")), - Must(New("git-client:2.7.6")), + Must(New(durableTaskPlugin)), + Must(New(gitClientPlugin)), Must(New("git-server:1.7")), Must(New("handlebars:1.1.1")), - Must(New(Jackson2ADIPlugin)), + Must(New(jackson2ADIPlugin)), Must(New("jquery-detached:1.2.1")), - Must(New("jsch:0.1.55")), - Must(New("junit:1.26.1")), + Must(New(jschPlugin)), + Must(New(junitPlugin)), Must(New("lockable-resources:2.3")), - Must(New("mailer:1.23")), - Must(New("matrix-project:1.13")), + Must(New(mailerPlugin)), + Must(New(matrixProjectPlugin)), Must(New("momentjs:1.1.1")), Must(New("pipeline-build-step:2.7")), Must(New("pipeline-graph-analysis:1.9")), @@ -64,48 +80,46 @@ var BasePluginsMap = map[string][]Plugin{ Must(New("pipeline-stage-step:2.3")), Must(New("pipeline-stage-tags-metadata:1.3.4.1")), Must(New("pipeline-stage-view:2.10")), - Must(New("plain-credentials:1.5")), - Must(New("scm-api:2.3.0")), - Must(New("script-security:1.50")), - Must(New("ssh-credentials:1.14")), - Must(New("structs:1.17")), - Must(New("workflow-api:2.33")), + Must(New(plainCredentialsPlugin)), + Must(New(scmAPIPlugin)), + Must(New(scriptSecurityPlugin)), + Must(New(sshCredentialsPlugin)), + Must(New(structsPlugin)), + Must(New(workflowAPIPlugin)), Must(New("workflow-basic-steps:2.13")), Must(New("workflow-cps-global-lib:2.12")), Must(New("workflow-cps:2.61.1")), Must(New("workflow-durable-task-step:2.27")), Must(New("workflow-job:2.31")), Must(New("workflow-multibranch:2.20")), - Must(New("workflow-scm-step:2.7")), - Must(New("workflow-step-api:2.17")), - Must(New("workflow-support:3.0")), + Must(New(workflowSCMStepPlugin)), + Must(New(workflowStepAPIPlugin)), + Must(New(workflowSupportPlugin)), }, Must(New("git:3.9.1")).String(): { - Must(New(ApacheComponentsClientPlugin)), - Must(New("credentials:2.1.18")), - Must(New("display-url-api:2.3.0")), - Must(New("git-client:2.7.6")), - Must(New("jsch:0.1.55")), - Must(New("junit:1.26.1")), - Must(New("mailer:1.23")), - Must(New("matrix-project:1.13")), - Must(New("scm-api:2.3.0")), - Must(New("script-security:1.50")), - Must(New("ssh-credentials:1.14")), - Must(New("structs:1.17")), - Must(New("workflow-api:2.33")), - Must(New("workflow-scm-step:2.7")), - Must(New("workflow-step-api:2.17")), + Must(New(apacheComponentsClientPlugin)), + Must(New(credentialsPlugin)), + Must(New(displayURLAPIPlugin)), + Must(New(gitClientPlugin)), + Must(New(jschPlugin)), + Must(New(junitPlugin)), + Must(New(mailerPlugin)), + Must(New(matrixProjectPlugin)), + Must(New(scmAPIPlugin)), + Must(New(scriptSecurityPlugin)), + Must(New(sshCredentialsPlugin)), + Must(New(structsPlugin)), + Must(New(workflowAPIPlugin)), + Must(New(workflowSCMStepPlugin)), + Must(New(workflowStepAPIPlugin)), }, Must(New("job-dsl:1.71")).String(): { - Must(New("script-security:1.50")), - Must(New("structs:1.17")), + Must(New(scriptSecurityPlugin)), + Must(New(structsPlugin)), }, - Must(New("jobConfigHistory:2.19")).String(): {}, Must(New("configuration-as-code:1.4")).String(): { Must(New("configuration-as-code-support:1.4")), }, - Must(New("simple-theme-plugin:0.5.1")).String(): {}, } // BasePlugins returns map of plugins to install by operator diff --git a/pkg/controller/jenkins/plugins/plugin.go b/pkg/controller/jenkins/plugins/plugin.go index 45694ec7..6f56e098 100644 --- a/pkg/controller/jenkins/plugins/plugin.go +++ b/pkg/controller/jenkins/plugins/plugin.go @@ -40,26 +40,22 @@ func Must(plugin *Plugin, err error) Plugin { } // VerifyDependencies checks if all plugins have compatible versions -func VerifyDependencies(values ...map[string][]Plugin) bool { +func VerifyDependencies(values ...map[Plugin][]Plugin) bool { // key - plugin name, value array of versions allPlugins := make(map[string][]Plugin) valid := true for _, value := range values { - for rootPluginNameAndVersion, plugins := range value { - if rootPlugin, err := New(rootPluginNameAndVersion); err != nil { - valid = false - } else { - allPlugins[rootPlugin.Name] = append(allPlugins[rootPlugin.Name], Plugin{ - Name: rootPlugin.Name, - Version: rootPlugin.Version, - rootPluginNameAndVersion: rootPluginNameAndVersion}) - } + for rootPlugin, plugins := range value { + allPlugins[rootPlugin.Name] = append(allPlugins[rootPlugin.Name], Plugin{ + Name: rootPlugin.Name, + Version: rootPlugin.Version, + rootPluginNameAndVersion: rootPlugin.String()}) for _, plugin := range plugins { allPlugins[plugin.Name] = append(allPlugins[plugin.Name], Plugin{ Name: plugin.Name, Version: plugin.Version, - rootPluginNameAndVersion: rootPluginNameAndVersion}) + rootPluginNameAndVersion: rootPlugin.String()}) } } } diff --git a/pkg/controller/jenkins/plugins/plugin_test.go b/pkg/controller/jenkins/plugins/plugin_test.go index c588f136..d56915da 100644 --- a/pkg/controller/jenkins/plugins/plugin_test.go +++ b/pkg/controller/jenkins/plugins/plugin_test.go @@ -1,90 +1,88 @@ package plugins import ( - "fmt" - "github.com/jenkinsci/kubernetes-operator/pkg/log" "testing" + "github.com/jenkinsci/kubernetes-operator/pkg/log" + "github.com/stretchr/testify/assert" ) func TestVerifyDependencies(t *testing.T) { - data := []struct { - basePlugins map[string][]Plugin - extraPlugins map[string][]Plugin - expectedResult bool - }{ - { - basePlugins: map[string][]Plugin{ - "first-root-plugin:1.0.0": { - Must(New("first-plugin:0.0.1")), - }, - }, - expectedResult: true, - }, - { - basePlugins: map[string][]Plugin{ - "first-root-plugin:1.0.0": { - Must(New("first-plugin:0.0.1")), - }, - "second-root-plugin:1.0.0": { - Must(New("first-plugin:0.0.1")), - }, - }, - expectedResult: true, - }, - { - basePlugins: map[string][]Plugin{ - "first-root-plugin:1.0.0": { - Must(New("first-plugin:0.0.1")), - }, - }, - extraPlugins: map[string][]Plugin{ - "second-root-plugin:2.0.0": { - Must(New("first-plugin:0.0.1")), - }, - }, - expectedResult: true, - }, - { - basePlugins: map[string][]Plugin{ - "first-root-plugin:1.0.0": { - Must(New("first-plugin:0.0.1")), - }, - "first-root-plugin:2.0.0": { - Must(New("first-plugin:0.0.2")), - }, - }, - expectedResult: false, - }, - { - basePlugins: map[string][]Plugin{ - "first-root-plugin:1.0.0": { - Must(New("first-plugin:0.0.1")), - }, - }, - extraPlugins: map[string][]Plugin{ - "first-root-plugin:2.0.0": { - Must(New("first-plugin:0.0.2")), - }, - }, - expectedResult: false, - }, - { - basePlugins: map[string][]Plugin{ - "invalid-plugin-name": {}, - }, - expectedResult: false, - }, - } - debug := false log.SetupLogger(&debug) - for index, testingData := range data { - t.Run(fmt.Sprintf("Testing %d data", index), func(t *testing.T) { - result := VerifyDependencies(testingData.basePlugins, testingData.extraPlugins) - assert.Equal(t, testingData.expectedResult, result) - }) - } + t.Run("happy, single root plugin with one dependent plugin", func(t *testing.T) { + basePlugins := map[Plugin][]Plugin{ + Must(New("first-root-plugin:1.0.0")): { + Must(New("first-plugin:0.0.1")), + }, + } + got := VerifyDependencies(basePlugins) + assert.Equal(t, true, got) + }) + t.Run("happy, two root plugins with one depended plugin with the same version", func(t *testing.T) { + basePlugins := map[Plugin][]Plugin{ + Must(New("first-root-plugin:1.0.0")): { + Must(New("first-plugin:0.0.1")), + }, + Must(New("second-root-plugin:1.0.0")): { + Must(New("first-plugin:0.0.1")), + }, + } + got := VerifyDependencies(basePlugins) + assert.Equal(t, true, got) + }) + t.Run("fail, two root plugins have different versions", func(t *testing.T) { + basePlugins := map[Plugin][]Plugin{ + Must(New("first-root-plugin:1.0.0")): { + Must(New("first-plugin:0.0.1")), + }, + Must(New("first-root-plugin:2.0.0")): { + Must(New("first-plugin:0.0.1")), + }, + } + got := VerifyDependencies(basePlugins) + assert.Equal(t, false, got) + }) + t.Run("happy, no version collision with two sperate plugins lists", func(t *testing.T) { + basePlugins := map[Plugin][]Plugin{ + Must(New("first-root-plugin:1.0.0")): { + Must(New("first-plugin:0.0.1")), + }, + } + extraPlugins := map[Plugin][]Plugin{ + Must(New("second-root-plugin:2.0.0")): { + Must(New("first-plugin:0.0.1")), + }, + } + got := VerifyDependencies(basePlugins, extraPlugins) + assert.Equal(t, true, got) + }) + t.Run("fail, dependent plugins have different versions", func(t *testing.T) { + basePlugins := map[Plugin][]Plugin{ + Must(New("first-root-plugin:1.0.0")): { + Must(New("first-plugin:0.0.1")), + }, + Must(New("first-root-plugin:2.0.0")): { + Must(New("first-plugin:0.0.2")), + }, + } + got := VerifyDependencies(basePlugins) + assert.Equal(t, false, got) + }) + t.Run("fail, root and dependent plugins have different versions", func(t *testing.T) { + basePlugins := map[Plugin][]Plugin{ + Must(New("first-root-plugin:1.0.0")): { + Must(New("first-plugin:0.0.1")), + }, + } + extraPlugins := map[Plugin][]Plugin{ + Must(New("first-root-plugin:2.0.0")): { + Must(New("first-plugin:0.0.2")), + }, + } + got := VerifyDependencies(basePlugins, extraPlugins) + assert.Equal(t, false, got) + }) } diff --git a/test/e2e/configuration_test.go b/test/e2e/configuration_test.go index e9530503..2b5addde 100644 --- a/test/e2e/configuration_test.go +++ b/test/e2e/configuration_test.go @@ -33,7 +33,7 @@ func TestConfiguration(t *testing.T) { verifyJenkinsMasterPodAttributes(t, jenkins) client := verifyJenkinsAPIConnection(t, jenkins) - verifyBasePlugins(t, client) + verifyPlugins(t, client, jenkins) // user waitForJenkinsUserConfigurationToComplete(t, jenkins) @@ -90,28 +90,48 @@ func verifyJenkinsMasterPodAttributes(t *testing.T, jenkins *v1alpha1.Jenkins) { t.Log("Jenkins pod attributes are valid") } -func verifyBasePlugins(t *testing.T, jenkinsClient *gojenkins.Jenkins) { +func verifyPlugins(t *testing.T, jenkinsClient *gojenkins.Jenkins, jenkins *v1alpha1.Jenkins) { installedPlugins, err := jenkinsClient.GetPlugins(1) if err != nil { t.Fatal(err) } - for rootPluginName, p := range plugins.BasePluginsMap { - rootPlugin, err := plugins.New(rootPluginName) - if err != nil { - t.Fatal(err) - } - if found, ok := isPluginValid(installedPlugins, *rootPlugin); !ok { - t.Fatalf("Invalid plugin '%s', actual '%+v'", rootPlugin, found) - } - for _, requiredPlugin := range p { - if found, ok := isPluginValid(installedPlugins, requiredPlugin); !ok { - t.Fatalf("Invalid plugin '%s', actual '%+v'", requiredPlugin, found) + requiredPlugins := []map[string][]string{plugins.BasePlugins(), jenkins.Spec.Master.Plugins} + for _, p := range requiredPlugins { + for rootPluginName, dependentPlugins := range p { + rootPlugin, err := plugins.New(rootPluginName) + if err != nil { + t.Fatal(err) + } + if found, ok := isPluginValid(installedPlugins, *rootPlugin); !ok { + t.Fatalf("Invalid plugin '%s', actual '%+v'", rootPlugin, found) + } + for _, pluginName := range dependentPlugins { + plugin, err := plugins.New(pluginName) + if err != nil { + t.Fatal(err) + } + if found, ok := isPluginValid(installedPlugins, *plugin); !ok { + t.Fatalf("Invalid plugin '%s', actual '%+v'", rootPlugin, found) + } } } } - t.Log("Base plugins have been installed") + t.Log("All plugins have been installed") +} + +func isPluginValid(plugins *gojenkins.Plugins, requiredPlugin plugins.Plugin) (*gojenkins.Plugin, bool) { + p := plugins.Contains(requiredPlugin.Name) + if p == nil { + return p, false + } + + if !p.Active || !p.Enabled || p.Deleted { + return p, false + } + + return p, requiredPlugin.Version == p.Version } func verifyJenkinsSeedJobs(t *testing.T, client *gojenkins.Jenkins, jenkins *v1alpha1.Jenkins) { @@ -150,16 +170,3 @@ func verifyJenkinsSeedJobs(t *testing.T, client *gojenkins.Jenkins, jenkins *v1a }) assert.NoError(t, err) } - -func isPluginValid(plugins *gojenkins.Plugins, requiredPlugin plugins.Plugin) (*gojenkins.Plugin, bool) { - p := plugins.Contains(requiredPlugin.Name) - if p == nil { - return p, false - } - - if !p.Active || !p.Enabled || p.Deleted { - return p, false - } - - return p, requiredPlugin.Version == p.Version -} diff --git a/test/e2e/jenkins.go b/test/e2e/jenkins.go index bc055e28..a9ab7b07 100644 --- a/test/e2e/jenkins.go +++ b/test/e2e/jenkins.go @@ -85,6 +85,10 @@ func createJenkinsCR(t *testing.T, namespace string) *v1alpha1.Jenkins { Master: v1alpha1.JenkinsMaster{ Image: "jenkins/jenkins", Annotations: map[string]string{"test": "label"}, + Plugins: map[string][]string{ + "audit-trail:2.4": {}, + "simple-theme-plugin:0.5.1": {}, + }, }, //TODO(bantoniak) add seed job with private key SeedJobs: []v1alpha1.SeedJob{