From e1aba3ed9f7d0792beb0efe5c0e605908085eb54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20S=C4=99k?= Date: Tue, 11 Jun 2019 19:51:49 +0200 Subject: [PATCH] Improve plugins management --- .../jenkins/v1alpha1/zz_generated.deepcopy.go | 126 ++++++++---- pkg/apis/jenkins/v1alpha2/jenkins_types.go | 12 +- .../jenkins/v1alpha2/zz_generated.deepcopy.go | 163 ++++++++++------ .../jenkins/configuration/base/reconcile.go | 50 +---- .../configuration/base/reconcile_test.go | 143 +++++++++----- .../resources/base_configuration_configmap.go | 4 +- .../base/resources/scripts_configmap.go | 16 +- .../jenkins/configuration/base/validate.go | 72 ++++--- .../configuration/base/validate_test.go | 165 ++++++++++------ pkg/controller/jenkins/jenkins_controller.go | 15 +- .../jenkins/plugins/base_plugins.go | 183 ++---------------- pkg/controller/jenkins/plugins/plugin.go | 39 +++- test/e2e/configuration_test.go | 29 +-- test/e2e/jenkins.go | 6 +- 14 files changed, 554 insertions(+), 469 deletions(-) diff --git a/pkg/apis/jenkins/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/jenkins/v1alpha1/zz_generated.deepcopy.go index f7ccff13..81a2e5f5 100644 --- a/pkg/apis/jenkins/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/jenkins/v1alpha1/zz_generated.deepcopy.go @@ -1,21 +1,5 @@ // +build !ignore_autogenerated -/* -Copyright The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - // Code generated by deepcopy-gen. DO NOT EDIT. package v1alpha1 @@ -49,6 +33,79 @@ func (in *Build) DeepCopy() *Build { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Container) DeepCopyInto(out *Container) { + *out = *in + if in.Command != nil { + in, out := &in.Command, &out.Command + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Args != nil { + in, out := &in.Args, &out.Args + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Ports != nil { + in, out := &in.Ports, &out.Ports + *out = make([]v1.ContainerPort, len(*in)) + copy(*out, *in) + } + if in.EnvFrom != nil { + in, out := &in.EnvFrom, &out.EnvFrom + *out = make([]v1.EnvFromSource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = make([]v1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + in.Resources.DeepCopyInto(&out.Resources) + if in.VolumeMounts != nil { + in, out := &in.VolumeMounts, &out.VolumeMounts + *out = make([]v1.VolumeMount, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.LivenessProbe != nil { + in, out := &in.LivenessProbe, &out.LivenessProbe + *out = new(v1.Probe) + (*in).DeepCopyInto(*out) + } + if in.ReadinessProbe != nil { + in, out := &in.ReadinessProbe, &out.ReadinessProbe + *out = new(v1.Probe) + (*in).DeepCopyInto(*out) + } + if in.Lifecycle != nil { + in, out := &in.Lifecycle, &out.Lifecycle + *out = new(v1.Lifecycle) + (*in).DeepCopyInto(*out) + } + if in.SecurityContext != nil { + in, out := &in.SecurityContext, &out.SecurityContext + *out = new(v1.SecurityContext) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Container. +func (in *Container) DeepCopy() *Container { + if in == nil { + return nil + } + out := new(Container) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Jenkins) DeepCopyInto(out *Jenkins) { *out = *in @@ -113,13 +170,7 @@ func (in *JenkinsList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JenkinsMaster) DeepCopyInto(out *JenkinsMaster) { *out = *in - if in.NodeSelector != nil { - in, out := &in.NodeSelector, &out.NodeSelector - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } + in.Container.DeepCopyInto(&out.Container) if in.Annotations != nil { in, out := &in.Annotations, &out.Annotations *out = make(map[string]string, len(*in)) @@ -127,23 +178,26 @@ func (in *JenkinsMaster) DeepCopyInto(out *JenkinsMaster) { (*out)[key] = val } } - in.Resources.DeepCopyInto(&out.Resources) - if in.Env != nil { - in, out := &in.Env, &out.Env - *out = make([]v1.EnvVar, len(*in)) + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Containers != nil { + in, out := &in.Containers, &out.Containers + *out = make([]Container, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } - if in.LivenessProbe != nil { - in, out := &in.LivenessProbe, &out.LivenessProbe - *out = new(v1.Probe) - (*in).DeepCopyInto(*out) - } - if in.ReadinessProbe != nil { - in, out := &in.ReadinessProbe, &out.ReadinessProbe - *out = new(v1.Probe) - (*in).DeepCopyInto(*out) + if in.Volumes != nil { + in, out := &in.Volumes, &out.Volumes + *out = make([]v1.Volume, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } if in.OperatorPlugins != nil { in, out := &in.OperatorPlugins, &out.OperatorPlugins diff --git a/pkg/apis/jenkins/v1alpha2/jenkins_types.go b/pkg/apis/jenkins/v1alpha2/jenkins_types.go index e541a50a..7257c8c4 100644 --- a/pkg/apis/jenkins/v1alpha2/jenkins_types.go +++ b/pkg/apis/jenkins/v1alpha2/jenkins_types.go @@ -38,6 +38,12 @@ type Container struct { SecurityContext *corev1.SecurityContext `json:"securityContext,omitempty"` } +// Plugin defines Jenkins plugin +type Plugin struct { + Name string `json:"name"` + Version string `json:"version"` +} + // JenkinsMaster defines the Jenkins master pod attributes and plugins, // every single change requires Jenkins master pod restart type JenkinsMaster struct { @@ -49,10 +55,10 @@ type JenkinsMaster struct { Containers []Container `json:"containers,omitempty"` Volumes []corev1.Volume `json:"volumes,omitempty"` - // OperatorPlugins contains plugins required by operator - OperatorPlugins map[string][]string `json:"basePlugins,omitempty"` + // BasePlugins contains plugins required by operator + BasePlugins []Plugin `json:"basePlugins,omitempty"` // Plugins contains plugins required by user - Plugins map[string][]string `json:"plugins,omitempty"` + Plugins []Plugin `json:"plugins,omitempty"` } // Service defines Kubernetes service attributes which Operator will manage diff --git a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go index 61dd8a73..dad9c4ad 100644 --- a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go @@ -1,21 +1,5 @@ // +build !ignore_autogenerated -/* -Copyright The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - // Code generated by deepcopy-gen. DO NOT EDIT. package v1alpha2 @@ -49,6 +33,79 @@ func (in *Build) DeepCopy() *Build { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Container) DeepCopyInto(out *Container) { + *out = *in + if in.Command != nil { + in, out := &in.Command, &out.Command + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Args != nil { + in, out := &in.Args, &out.Args + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Ports != nil { + in, out := &in.Ports, &out.Ports + *out = make([]v1.ContainerPort, len(*in)) + copy(*out, *in) + } + if in.EnvFrom != nil { + in, out := &in.EnvFrom, &out.EnvFrom + *out = make([]v1.EnvFromSource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = make([]v1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + in.Resources.DeepCopyInto(&out.Resources) + if in.VolumeMounts != nil { + in, out := &in.VolumeMounts, &out.VolumeMounts + *out = make([]v1.VolumeMount, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.LivenessProbe != nil { + in, out := &in.LivenessProbe, &out.LivenessProbe + *out = new(v1.Probe) + (*in).DeepCopyInto(*out) + } + if in.ReadinessProbe != nil { + in, out := &in.ReadinessProbe, &out.ReadinessProbe + *out = new(v1.Probe) + (*in).DeepCopyInto(*out) + } + if in.Lifecycle != nil { + in, out := &in.Lifecycle, &out.Lifecycle + *out = new(v1.Lifecycle) + (*in).DeepCopyInto(*out) + } + if in.SecurityContext != nil { + in, out := &in.SecurityContext, &out.SecurityContext + *out = new(v1.SecurityContext) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Container. +func (in *Container) DeepCopy() *Container { + if in == nil { + return nil + } + out := new(Container) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Jenkins) DeepCopyInto(out *Jenkins) { *out = *in @@ -120,60 +177,36 @@ func (in *JenkinsMaster) DeepCopyInto(out *JenkinsMaster) { (*out)[key] = val } } - if in.Annotations != nil { - in, out := &in.Annotations, &out.Annotations + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector *out = make(map[string]string, len(*in)) for key, val := range *in { (*out)[key] = val } } - in.Resources.DeepCopyInto(&out.Resources) - if in.Env != nil { - in, out := &in.Env, &out.Env - *out = make([]v1.EnvVar, len(*in)) + if in.Containers != nil { + in, out := &in.Containers, &out.Containers + *out = make([]Container, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } - if in.LivenessProbe != nil { - in, out := &in.LivenessProbe, &out.LivenessProbe - *out = new(v1.Probe) - (*in).DeepCopyInto(*out) - } - if in.ReadinessProbe != nil { - in, out := &in.ReadinessProbe, &out.ReadinessProbe - *out = new(v1.Probe) - (*in).DeepCopyInto(*out) - } - 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.Volumes != nil { + in, out := &in.Volumes, &out.Volumes + *out = make([]v1.Volume, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.BasePlugins != nil { + in, out := &in.BasePlugins, &out.BasePlugins + *out = make([]Plugin, len(*in)) + copy(*out, *in) + } if in.Plugins != nil { in, out := &in.Plugins, &out.Plugins - *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 - } + *out = make([]Plugin, len(*in)) + copy(*out, *in) } return } @@ -247,6 +280,22 @@ func (in *JenkinsStatus) DeepCopy() *JenkinsStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Plugin) DeepCopyInto(out *Plugin) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Plugin. +func (in *Plugin) DeepCopy() *Plugin { + if in == nil { + return nil + } + out := new(Plugin) + 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 diff --git a/pkg/controller/jenkins/configuration/base/reconcile.go b/pkg/controller/jenkins/configuration/base/reconcile.go index 16a5bf5e..74914d44 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile.go +++ b/pkg/controller/jenkins/configuration/base/reconcile.go @@ -106,7 +106,7 @@ 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 } @@ -187,7 +187,7 @@ func (r *ReconcileJenkinsBaseConfiguration) waitForVolumes() (reconcile.Result, return reconcile.Result{}, nil } -func (r *ReconcileJenkinsBaseConfiguration) verifyPlugins(jenkinsClient jenkinsclient.Jenkins, basePlugins 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, stackerr.WithStack(err) @@ -201,46 +201,16 @@ 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{basePlugins, userPlugins} + allRequiredPlugins := [][]v1alpha2.Plugin{r.jenkins.Spec.Master.BasePlugins, r.jenkins.Spec.Master.Plugins} for _, requiredPlugins := range allRequiredPlugins { - for rootPluginName, p := range requiredPlugins { - rootPlugin, _ := plugins.New(rootPluginName) - if found, ok := isPluginInstalled(allPluginsInJenkins, *rootPlugin); !ok { - r.logger.V(log.VWarn).Info(fmt.Sprintf("Missing plugin '%s', actual '%+v'", rootPlugin, found)) + for _, plugin := range requiredPlugins { + if found, ok := isPluginInstalled(allPluginsInJenkins, plugin); !ok { + r.logger.V(log.VWarn).Info(fmt.Sprintf("Missing plugin '%s', actual '%+v'", plugin, found)) status = false } - for _, requiredPlugin := range p { - if found, ok := isPluginInstalled(allPluginsInJenkins, requiredPlugin); !ok { - r.logger.V(log.VWarn).Info(fmt.Sprintf("Missing plugin '%s', actual '%+v'", requiredPlugin, found)) - status = false - } - } - } - } - - for rootPluginName, p := range userPlugins { - rootPlugin, _ := plugins.New(rootPluginName) - if found, ok := isPluginVersionCompatible(allPluginsInJenkins, *rootPlugin); !ok { - r.logger.V(log.VWarn).Info(fmt.Sprintf("Incompatible plugin '%s' version, actual '%+v'", rootPlugin, found.Version)) - status = false - } - for _, requiredPlugin := range p { - if found, ok := isPluginInstalled(allPluginsInJenkins, requiredPlugin); !ok { - r.logger.V(log.VWarn).Info(fmt.Sprintf("Incompatible plugin '%s' version, actual '%+v'", requiredPlugin, found.Version)) + if found, ok := isPluginVersionCompatible(allPluginsInJenkins, plugin); !ok { + r.logger.V(log.VWarn).Info(fmt.Sprintf("Incompatible plugin '%s' version, actual '%+v'", plugin, found.Version)) status = false } } @@ -249,7 +219,7 @@ func (r *ReconcileJenkinsBaseConfiguration) verifyPlugins(jenkinsClient jenkinsc return status, nil } -func isPluginVersionCompatible(plugins *gojenkins.Plugins, plugin plugins.Plugin) (gojenkins.Plugin, bool) { +func isPluginVersionCompatible(plugins *gojenkins.Plugins, plugin v1alpha2.Plugin) (gojenkins.Plugin, bool) { p := plugins.Contains(plugin.Name) if p == nil { return gojenkins.Plugin{}, false @@ -262,7 +232,7 @@ func isValidPlugin(plugin gojenkins.Plugin) bool { return plugin.Active && plugin.Enabled && !plugin.Deleted } -func isPluginInstalled(plugins *gojenkins.Plugins, requiredPlugin plugins.Plugin) (gojenkins.Plugin, bool) { +func isPluginInstalled(plugins *gojenkins.Plugins, requiredPlugin v1alpha2.Plugin) (gojenkins.Plugin, bool) { p := plugins.Contains(requiredPlugin.Name) if p == nil { return gojenkins.Plugin{}, false diff --git a/pkg/controller/jenkins/configuration/base/reconcile_test.go b/pkg/controller/jenkins/configuration/base/reconcile_test.go index 2d9853db..ae49947e 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile_test.go +++ b/pkg/controller/jenkins/configuration/base/reconcile_test.go @@ -6,7 +6,6 @@ import ( "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources" - "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/plugins" "github.com/jenkinsci/kubernetes-operator/pkg/log" "github.com/bndr/gojenkins" @@ -167,19 +166,67 @@ func TestReconcileJenkinsBaseConfiguration_verifyPlugins(t *testing.T) { pluginsInJenkins := &gojenkins.Plugins{ Raw: &gojenkins.PluginResponse{}, } - basePlugins := map[string][]plugins.Plugin{} ctrl := gomock.NewController(t) defer ctrl.Finish() jenkinsClient := client.NewMockJenkins(ctrl) jenkinsClient.EXPECT().GetPlugins(fetchAllPlugins).Return(pluginsInJenkins, nil) - got, err := r.verifyPlugins(jenkinsClient, basePlugins) + got, err := r.verifyPlugins(jenkinsClient) + + assert.NoError(t, err) + assert.True(t, got) + }) + t.Run("happy, not empty base and user plugins", func(t *testing.T) { + jenkins := &v1alpha2.Jenkins{ + Spec: v1alpha2.JenkinsSpec{ + Master: v1alpha2.JenkinsMaster{ + BasePlugins: []v1alpha2.Plugin{{Name: "plugin-name1", Version: "0.0.1"}}, + Plugins: []v1alpha2.Plugin{{Name: "plugin-name2", Version: "0.0.1"}}, + }, + }, + } + r := ReconcileJenkinsBaseConfiguration{ + logger: log.Log, + jenkins: jenkins, + } + pluginsInJenkins := &gojenkins.Plugins{ + Raw: &gojenkins.PluginResponse{ + Plugins: []gojenkins.Plugin{ + { + ShortName: "plugin-name1", + Active: true, + Deleted: false, + Enabled: true, + Version: "0.0.1", + }, + { + ShortName: "plugin-name2", + Active: true, + Deleted: false, + Enabled: true, + Version: "0.0.1", + }, + }, + }, + } + ctrl := gomock.NewController(t) + defer ctrl.Finish() + jenkinsClient := client.NewMockJenkins(ctrl) + jenkinsClient.EXPECT().GetPlugins(fetchAllPlugins).Return(pluginsInJenkins, nil) + + got, err := r.verifyPlugins(jenkinsClient) assert.NoError(t, err) assert.True(t, got) }) t.Run("happy, not empty base and empty user plugins", func(t *testing.T) { - jenkins := &v1alpha2.Jenkins{} + jenkins := &v1alpha2.Jenkins{ + Spec: v1alpha2.JenkinsSpec{ + Master: v1alpha2.JenkinsMaster{ + BasePlugins: []v1alpha2.Plugin{{Name: "plugin-name", Version: "0.0.1"}}, + }, + }, + } r := ReconcileJenkinsBaseConfiguration{ logger: log.Log, jenkins: jenkins, @@ -197,15 +244,12 @@ func TestReconcileJenkinsBaseConfiguration_verifyPlugins(t *testing.T) { }, }, } - basePlugins := map[string][]plugins.Plugin{ - "plugin-name:0.0.1": {}, - } ctrl := gomock.NewController(t) defer ctrl.Finish() jenkinsClient := client.NewMockJenkins(ctrl) jenkinsClient.EXPECT().GetPlugins(fetchAllPlugins).Return(pluginsInJenkins, nil) - got, err := r.verifyPlugins(jenkinsClient, basePlugins) + got, err := r.verifyPlugins(jenkinsClient) assert.NoError(t, err) assert.True(t, got) @@ -214,7 +258,7 @@ func TestReconcileJenkinsBaseConfiguration_verifyPlugins(t *testing.T) { jenkins := &v1alpha2.Jenkins{ Spec: v1alpha2.JenkinsSpec{ Master: v1alpha2.JenkinsMaster{ - Plugins: map[string][]string{"plugin-name:0.0.1": {}}, + Plugins: []v1alpha2.Plugin{{Name: "plugin-name", Version: "0.0.1"}}, }, }, } @@ -235,19 +279,24 @@ func TestReconcileJenkinsBaseConfiguration_verifyPlugins(t *testing.T) { }, }, } - basePlugins := map[string][]plugins.Plugin{} ctrl := gomock.NewController(t) defer ctrl.Finish() jenkinsClient := client.NewMockJenkins(ctrl) jenkinsClient.EXPECT().GetPlugins(fetchAllPlugins).Return(pluginsInJenkins, nil) - got, err := r.verifyPlugins(jenkinsClient, basePlugins) + got, err := r.verifyPlugins(jenkinsClient) assert.NoError(t, err) assert.True(t, got) }) - t.Run("happy, plugin version doesn't matter for base plugins", func(t *testing.T) { - jenkins := &v1alpha2.Jenkins{} + t.Run("happy, plugin version matter for base plugins", func(t *testing.T) { + jenkins := &v1alpha2.Jenkins{ + Spec: v1alpha2.JenkinsSpec{ + Master: v1alpha2.JenkinsMaster{ + BasePlugins: []v1alpha2.Plugin{{Name: "plugin-name", Version: "0.0.1"}}, + }, + }, + } r := ReconcileJenkinsBaseConfiguration{ logger: log.Log, jenkins: jenkins, @@ -265,24 +314,21 @@ func TestReconcileJenkinsBaseConfiguration_verifyPlugins(t *testing.T) { }, }, } - basePlugins := map[string][]plugins.Plugin{ - "plugin-name:0.0.1": {}, - } ctrl := gomock.NewController(t) defer ctrl.Finish() jenkinsClient := client.NewMockJenkins(ctrl) jenkinsClient.EXPECT().GetPlugins(fetchAllPlugins).Return(pluginsInJenkins, nil) - got, err := r.verifyPlugins(jenkinsClient, basePlugins) + got, err := r.verifyPlugins(jenkinsClient) assert.NoError(t, err) - assert.True(t, got) + assert.False(t, got) }) t.Run("plugin version matter for user plugins", func(t *testing.T) { jenkins := &v1alpha2.Jenkins{ Spec: v1alpha2.JenkinsSpec{ Master: v1alpha2.JenkinsMaster{ - Plugins: map[string][]string{"plugin-name:0.0.2": {}}, + Plugins: []v1alpha2.Plugin{{Name: "plugin-name", Version: "0.0.2"}}, }, }, } @@ -303,46 +349,21 @@ func TestReconcileJenkinsBaseConfiguration_verifyPlugins(t *testing.T) { }, }, } - basePlugins := map[string][]plugins.Plugin{} ctrl := gomock.NewController(t) defer ctrl.Finish() jenkinsClient := client.NewMockJenkins(ctrl) jenkinsClient.EXPECT().GetPlugins(fetchAllPlugins).Return(pluginsInJenkins, nil) - got, err := r.verifyPlugins(jenkinsClient, basePlugins) + got, err := r.verifyPlugins(jenkinsClient) assert.NoError(t, err) assert.False(t, got) }) t.Run("missing base plugin", func(t *testing.T) { - jenkins := &v1alpha2.Jenkins{} - r := ReconcileJenkinsBaseConfiguration{ - logger: log.Log, - jenkins: jenkins, - } - pluginsInJenkins := &gojenkins.Plugins{ - Raw: &gojenkins.PluginResponse{ - Plugins: []gojenkins.Plugin{}, - }, - } - basePlugins := map[string][]plugins.Plugin{ - "plugin-name:0.0.2": {}, - } - ctrl := gomock.NewController(t) - defer ctrl.Finish() - jenkinsClient := client.NewMockJenkins(ctrl) - jenkinsClient.EXPECT().GetPlugins(fetchAllPlugins).Return(pluginsInJenkins, nil) - - got, err := r.verifyPlugins(jenkinsClient, basePlugins) - - assert.NoError(t, err) - assert.False(t, got) - }) - t.Run("missing user plugin", func(t *testing.T) { jenkins := &v1alpha2.Jenkins{ Spec: v1alpha2.JenkinsSpec{ Master: v1alpha2.JenkinsMaster{ - Plugins: map[string][]string{"plugin-name:0.0.2": {}}, + BasePlugins: []v1alpha2.Plugin{{Name: "plugin-name", Version: "0.0.2"}}, }, }, } @@ -355,13 +376,39 @@ func TestReconcileJenkinsBaseConfiguration_verifyPlugins(t *testing.T) { Plugins: []gojenkins.Plugin{}, }, } - basePlugins := map[string][]plugins.Plugin{} ctrl := gomock.NewController(t) defer ctrl.Finish() jenkinsClient := client.NewMockJenkins(ctrl) jenkinsClient.EXPECT().GetPlugins(fetchAllPlugins).Return(pluginsInJenkins, nil) - got, err := r.verifyPlugins(jenkinsClient, basePlugins) + got, err := r.verifyPlugins(jenkinsClient) + + assert.NoError(t, err) + assert.False(t, got) + }) + t.Run("missing user plugin", func(t *testing.T) { + jenkins := &v1alpha2.Jenkins{ + Spec: v1alpha2.JenkinsSpec{ + Master: v1alpha2.JenkinsMaster{ + Plugins: []v1alpha2.Plugin{{Name: "plugin-name", Version: "0.0.2"}}, + }, + }, + } + r := ReconcileJenkinsBaseConfiguration{ + logger: log.Log, + jenkins: jenkins, + } + pluginsInJenkins := &gojenkins.Plugins{ + Raw: &gojenkins.PluginResponse{ + Plugins: []gojenkins.Plugin{}, + }, + } + ctrl := gomock.NewController(t) + defer ctrl.Finish() + jenkinsClient := client.NewMockJenkins(ctrl) + jenkinsClient.EXPECT().GetPlugins(fetchAllPlugins).Return(pluginsInJenkins, nil) + + got, err := r.verifyPlugins(jenkinsClient) assert.NoError(t, err) assert.False(t, got) diff --git a/pkg/controller/jenkins/configuration/base/resources/base_configuration_configmap.go b/pkg/controller/jenkins/configuration/base/resources/base_configuration_configmap.go index 0aa655df..18ec0b2e 100644 --- a/pkg/controller/jenkins/configuration/base/resources/base_configuration_configmap.go +++ b/pkg/controller/jenkins/configuration/base/resources/base_configuration_configmap.go @@ -94,7 +94,9 @@ def remove = { list -> remove(jenkins.getExtensionList(RootAction.class)) remove(jenkins.actions) -jenkins.getDescriptor("jenkins.CLI").get().setEnabled(false) +if (jenkins.getDescriptor("jenkins.CLI") != null) { + jenkins.getDescriptor("jenkins.CLI").get().setEnabled(false) +} jenkins.save() ` diff --git a/pkg/controller/jenkins/configuration/base/resources/scripts_configmap.go b/pkg/controller/jenkins/configuration/base/resources/scripts_configmap.go index 85ffbeaf..c9949b4a 100644 --- a/pkg/controller/jenkins/configuration/base/resources/scripts_configmap.go +++ b/pkg/controller/jenkins/configuration/base/resources/scripts_configmap.go @@ -255,17 +255,11 @@ chmod +x {{ .JenkinsHomePath }}/scripts/*.sh {{- $installPluginsCommand := .InstallPluginsCommand }} 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 }} +{{ $installPluginsCommand }} {{ range $index, $plugin := .BasePlugins }}{{ $plugin.Name }}:{{ $plugin.Version }} {{ 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 }} +{{ $installPluginsCommand }} {{ range $index, $plugin := .UserPlugins }}{{ $plugin.Name }}:{{ $plugin.Version }} {{ end }} echo "Installing plugins required by user - end" /sbin/tini -s -- /usr/local/bin/jenkins.sh @@ -284,12 +278,12 @@ func buildInitBashScript(jenkins *v1alpha2.Jenkins) (*string, error) { InitConfigurationPath string InstallPluginsCommand string JenkinsScriptsVolumePath string - OperatorPlugins map[string][]string - UserPlugins map[string][]string + BasePlugins []v1alpha2.Plugin + UserPlugins []v1alpha2.Plugin }{ JenkinsHomePath: jenkinsHomePath, InitConfigurationPath: jenkinsInitConfigurationVolumePath, - OperatorPlugins: jenkins.Spec.Master.OperatorPlugins, + BasePlugins: jenkins.Spec.Master.BasePlugins, UserPlugins: jenkins.Spec.Master.Plugins, InstallPluginsCommand: installPluginsCommand, JenkinsScriptsVolumePath: jenkinsScriptsVolumePath, diff --git a/pkg/controller/jenkins/configuration/base/validate.go b/pkg/controller/jenkins/configuration/base/validate.go index d8d5c688..6542ea91 100644 --- a/pkg/controller/jenkins/configuration/base/validate.go +++ b/pkg/controller/jenkins/configuration/base/validate.go @@ -42,7 +42,7 @@ func (r *ReconcileJenkinsBaseConfiguration) Validate(jenkins *v1alpha2.Jenkins) } } - if !r.validatePlugins(jenkins.Spec.Master.OperatorPlugins, jenkins.Spec.Master.Plugins) { + if !r.validatePlugins(plugins.BasePlugins(), jenkins.Spec.Master.BasePlugins, jenkins.Spec.Master.Plugins) { return false, nil } @@ -215,36 +215,60 @@ func (r *ReconcileJenkinsBaseConfiguration) validateJenkinsMasterPodEnvs() bool return valid } -func (r *ReconcileJenkinsBaseConfiguration) validatePlugins(pluginsWithVersionSlice ...map[string][]string) bool { +func (r *ReconcileJenkinsBaseConfiguration) validatePlugins(requiredBasePlugins []plugins.Plugin, basePlugins, userPlugins []v1alpha2.Plugin) bool { valid := true allPlugins := map[plugins.Plugin][]plugins.Plugin{} - 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 - } + for _, jenkinsPlugin := range basePlugins { + plugin, err := plugins.NewPlugin(jenkinsPlugin.Name, jenkinsPlugin.Version) + if err != nil { + r.logger.V(log.VWarn).Info(err.Error()) + valid = false + } - 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 - } + if plugin != nil { + allPlugins[*plugin] = []plugins.Plugin{} } } - if valid { - return plugins.VerifyDependencies(allPlugins) + for _, jenkinsPlugin := range userPlugins { + plugin, err := plugins.NewPlugin(jenkinsPlugin.Name, jenkinsPlugin.Version) + if err != nil { + r.logger.V(log.VWarn).Info(err.Error()) + valid = false + } + + if plugin != nil { + allPlugins[*plugin] = []plugins.Plugin{} + } + } + + if !plugins.VerifyDependencies(allPlugins) { + valid = false + } + + if !r.verifyBasePlugins(requiredBasePlugins, basePlugins) { + valid = false + } + + return valid +} + +func (r *ReconcileJenkinsBaseConfiguration) verifyBasePlugins(requiredBasePlugins []plugins.Plugin, basePlugins []v1alpha2.Plugin) bool { + valid := true + + for _, requiredBasePlugin := range requiredBasePlugins { + found := false + for _, basePlugin := range basePlugins { + if requiredBasePlugin.Name == basePlugin.Name { + found = true + break + } + } + if !found { + valid = false + r.logger.V(log.VWarn).Info(fmt.Sprintf("Missing plugin '%s' in spec.master.basePlugins", requiredBasePlugin.Name)) + } } return valid diff --git a/pkg/controller/jenkins/configuration/base/validate_test.go b/pkg/controller/jenkins/configuration/base/validate_test.go index 2c6b70f2..01109322 100644 --- a/pkg/controller/jenkins/configuration/base/validate_test.go +++ b/pkg/controller/jenkins/configuration/base/validate_test.go @@ -6,6 +6,8 @@ import ( "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources" + "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/plugins" + "github.com/jenkinsci/kubernetes-operator/pkg/log" "github.com/stretchr/testify/assert" "k8s.io/api/core/v1" @@ -16,62 +18,117 @@ import ( ) func TestValidatePlugins(t *testing.T) { - baseReconcileLoop := New(nil, nil, logf.ZapLogger(false), + log.SetupLogger(true) + baseReconcileLoop := New(nil, nil, log.Log, 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) - }) + t.Run("empty", func(t *testing.T) { + var requiredBasePlugins []plugins.Plugin + var basePlugins []v1alpha2.Plugin + var userPlugins []v1alpha2.Plugin + got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins) + + assert.True(t, got) + }) + t.Run("valid user plugin", func(t *testing.T) { + var requiredBasePlugins []plugins.Plugin + var basePlugins []v1alpha2.Plugin + userPlugins := []v1alpha2.Plugin{{Name: "simple-plugin", Version: "0.0.1"}} + + got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins) + + assert.True(t, got) + }) + t.Run("invalid user plugin name", func(t *testing.T) { + var requiredBasePlugins []plugins.Plugin + var basePlugins []v1alpha2.Plugin + userPlugins := []v1alpha2.Plugin{{Name: "INVALID", Version: "0.0.1"}} + + got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins) + + assert.False(t, got) + }) + t.Run("invalid user plugin version", func(t *testing.T) { + var requiredBasePlugins []plugins.Plugin + var basePlugins []v1alpha2.Plugin + userPlugins := []v1alpha2.Plugin{{Name: "simple-plugin", Version: "invalid"}} + + got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins) + + assert.False(t, got) + }) + t.Run("valid base plugin", func(t *testing.T) { + var requiredBasePlugins []plugins.Plugin + basePlugins := []v1alpha2.Plugin{{Name: "simple-plugin", Version: "0.0.1"}} + var userPlugins []v1alpha2.Plugin + + got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins) + + assert.True(t, got) + }) + t.Run("invalid base plugin name", func(t *testing.T) { + var requiredBasePlugins []plugins.Plugin + basePlugins := []v1alpha2.Plugin{{Name: "INVALID", Version: "0.0.1"}} + var userPlugins []v1alpha2.Plugin + + got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins) + + assert.False(t, got) + }) + t.Run("invalid base plugin version", func(t *testing.T) { + var requiredBasePlugins []plugins.Plugin + basePlugins := []v1alpha2.Plugin{{Name: "simple-plugin", Version: "invalid"}} + var userPlugins []v1alpha2.Plugin + + got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins) + + assert.False(t, got) + }) + t.Run("valid user and base plugin version", func(t *testing.T) { + var requiredBasePlugins []plugins.Plugin + basePlugins := []v1alpha2.Plugin{{Name: "simple-plugin", Version: "0.0.1"}} + userPlugins := []v1alpha2.Plugin{{Name: "simple-plugin", Version: "0.0.1"}} + + got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins) + + assert.True(t, got) + }) + t.Run("invalid user and base plugin version", func(t *testing.T) { + var requiredBasePlugins []plugins.Plugin + basePlugins := []v1alpha2.Plugin{{Name: "simple-plugin", Version: "0.0.1"}} + userPlugins := []v1alpha2.Plugin{{Name: "simple-plugin", Version: "0.0.2"}} + + got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins) + + assert.False(t, got) + }) + t.Run("required base plugin set with the same version", func(t *testing.T) { + requiredBasePlugins := []plugins.Plugin{{Name: "simple-plugin", Version: "0.0.1"}} + basePlugins := []v1alpha2.Plugin{{Name: "simple-plugin", Version: "0.0.1"}} + var userPlugins []v1alpha2.Plugin + + got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins) + + assert.True(t, got) + }) + t.Run("required base plugin set with different version", func(t *testing.T) { + requiredBasePlugins := []plugins.Plugin{{Name: "simple-plugin", Version: "0.0.1"}} + basePlugins := []v1alpha2.Plugin{{Name: "simple-plugin", Version: "0.0.2"}} + var userPlugins []v1alpha2.Plugin + + got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins) + + assert.True(t, got) + }) + t.Run("missign required base plugin", func(t *testing.T) { + requiredBasePlugins := []plugins.Plugin{{Name: "simple-plugin", Version: "0.0.1"}} + var basePlugins []v1alpha2.Plugin + var userPlugins []v1alpha2.Plugin + + got := baseReconcileLoop.validatePlugins(requiredBasePlugins, basePlugins, userPlugins) + + assert.False(t, got) + }) } func TestValidateJenkinsMasterPodEnvs(t *testing.T) { diff --git a/pkg/controller/jenkins/jenkins_controller.go b/pkg/controller/jenkins/jenkins_controller.go index d2940303..bf743c00 100644 --- a/pkg/controller/jenkins/jenkins_controller.go +++ b/pkg/controller/jenkins/jenkins_controller.go @@ -262,15 +262,15 @@ func (r *ReconcileJenkins) setDefaults(jenkins *v1alpha2.Jenkins, logger logr.Lo FailureThreshold: int32(12), } } - if len(jenkins.Spec.Master.OperatorPlugins) == 0 { + if len(jenkins.Spec.Master.BasePlugins) == 0 { logger.Info("Setting default operator plugins") changed = true - jenkins.Spec.Master.OperatorPlugins = plugins.BasePlugins() + jenkins.Spec.Master.BasePlugins = basePlugins() } if len(jenkins.Status.OperatorVersion) > 0 && version.Version != jenkins.Status.OperatorVersion { logger.Info("Setting default operator plugins after Operator version change") changed = true - jenkins.Spec.Master.OperatorPlugins = plugins.BasePlugins() + jenkins.Spec.Master.BasePlugins = basePlugins() } if len(jenkins.Status.OperatorVersion) == 0 { logger.Info("Setting operator version") @@ -279,7 +279,7 @@ func (r *ReconcileJenkins) setDefaults(jenkins *v1alpha2.Jenkins, logger logr.Lo } if len(jenkins.Spec.Master.Plugins) == 0 { changed = true - jenkins.Spec.Master.Plugins = map[string][]string{"simple-theme-plugin:0.5.1": {}} + jenkins.Spec.Master.Plugins = []v1alpha2.Plugin{{Name: "simple-theme-plugin", Version: "0.5.1"}} } if isResourceRequirementsNotSet(jenkins.Spec.Master.Resources) { logger.Info("Setting default Jenkins master container resource requirements") @@ -365,3 +365,10 @@ func isResourceRequirementsNotSet(requirements corev1.ResourceRequirements) bool return !limitCPUSet || !limitMemorySet || !requestCPUSet || !requestMemporySet } + +func basePlugins() (result []v1alpha2.Plugin) { + for _, value := range plugins.BasePlugins() { + result = append(result, v1alpha2.Plugin{Name: value.Name, Version: value.Version}) + } + return +} diff --git a/pkg/controller/jenkins/plugins/base_plugins.go b/pkg/controller/jenkins/plugins/base_plugins.go index 071392b8..c8b402ce 100644 --- a/pkg/controller/jenkins/plugins/base_plugins.go +++ b/pkg/controller/jenkins/plugins/base_plugins.go @@ -1,180 +1,29 @@ package plugins const ( - aceEditorPlugin = "ace-editor:1.1" - apacheComponentsClientPlugin = "apache-httpcomponents-client-4-api:4.5.5-3.0" - authenticationTokensPlugin = "authentication-tokens:1.3" - branchAPIPlugin = "branch-api:2.5.2" - cloudBeesFolderPlugin = "cloudbees-folder:6.8" - configurationAsCodePlugin = "configuration-as-code:1.17" - configurationAsCodeSupportPlugin = "configuration-as-code-support:1.17" - credentialsBindingPlugin = "credentials-binding:1.18" - credentialsPlugin = "credentials:2.1.19" - displayURLAPIPlugin = "display-url-api:2.3.1" - dockerCommonsPlugin = "docker-commons:1.15" - dockerWorkflowPlugin = "docker-workflow:1.18" - durableTaskPlugin = "durable-task:1.29" - gitClientPlugin = "git-client:2.7.7" + configurationAsCodePlugin = "configuration-as-code:1.19" + configurationAsCodeSupportPlugin = "configuration-as-code-support:1.19" gitPlugin = "git:3.10.0" - gitServerPlugin = "git-server:1.7" - handlebarsPlugin = "handlebars:1.1.1" - jackson2ADIPlugin = "jackson2-api:2.9.9" jobDslPlugin = "job-dsl:1.74" - jqueryDetachedPlugin = "jquery-detached:1.2.1" - jschPlugin = "jsch:0.1.55" - junitPlugin = "junit:1.28" - kubernetesCredentialsPlugin = "kubernetes-credentials:0.4.0" kubernetesCredentialsProviderPlugin = "kubernetes-credentials-provider:0.12.1" - kubernetesPlugin = "kubernetes:1.15.5" - lockableResourcesPlugin = "lockable-resources:2.5" - mailerPlugin = "mailer:1.23" - matrixProjectPlugin = "matrix-project:1.14" - momentjsPlugin = "momentjs:1.1.1" - pipelineBuildStepPlugin = "pipeline-build-step:2.9" - pipelineGraphAnalysisPlugin = "pipeline-graph-analysis:1.10" - pipelineInputStepPlugin = "pipeline-input-step:2.10" - pipelineMilestoneStepPlugin = "pipeline-milestone-step:1.3.1" - pipelineModelAPIPlugin = "pipeline-model-api:1.3.8" - pipelineModelDeclarativeAgentPlugin = "pipeline-model-declarative-agent:1.1.1" - pipelineModelDefinitionPlugin = "pipeline-model-definition:1.3.8" - pipelineModelExtensionsPlugin = "pipeline-model-extensions:1.3.8" - pipelineRestAPIPlugin = "pipeline-rest-api:2.11" - pipelineStageStepPlugin = "pipeline-stage-step:2.3" - pipelineStageTagsMetadataPlugin = "pipeline-stage-tags-metadata:1.3.8" - pipelineStageViewPlugin = "pipeline-stage-view:2.11" - plainCredentialsPlugin = "plain-credentials:1.5" - scmAPIPlugin = "scm-api:2.4.1" - scriptSecurityPlugin = "script-security:1.59" - sshCredentialsPlugin = "ssh-credentials:1.16" - structsPlugin = "structs:1.19" - variantPlugin = "variant:1.2" + kubernetesPlugin = "kubernetes:1.15.7" workflowAggregatorPlugin = "workflow-aggregator:2.6" - workflowAPIPlugin = "workflow-api:2.34" - workflowBasicStepsPlugin = "workflow-basic-steps:2.16" - workflowCpsGlobalLibPlugin = "workflow-cps-global-lib:2.13" - workflowCpsPlugin = "workflow-cps:2.69" - workflowDurableTaskStepPlugin = "workflow-durable-task-step:2.30" workflowJobPlugin = "workflow-job:2.32" - workflowMultibranchPlugin = "workflow-multibranch:2.21" - workflowSCMStepPlugin = "workflow-scm-step:2.7" - workflowStepAPIPlugin = "workflow-step-api:2.19" - workflowSupportPlugin = "workflow-support:3.3" ) -// BasePluginsMap contains plugins to install by operator -var BasePluginsMap = map[string][]Plugin{ - Must(New(kubernetesPlugin)).String(): { - Must(New(apacheComponentsClientPlugin)), - Must(New(cloudBeesFolderPlugin)), - Must(New(credentialsPlugin)), - Must(New(durableTaskPlugin)), - Must(New(jackson2ADIPlugin)), - Must(New(kubernetesCredentialsPlugin)), - Must(New(plainCredentialsPlugin)), - Must(New(structsPlugin)), - Must(New(variantPlugin)), - Must(New(workflowStepAPIPlugin)), - }, - Must(New(workflowJobPlugin)).String(): { - Must(New(scmAPIPlugin)), - Must(New(scriptSecurityPlugin)), - Must(New(structsPlugin)), - Must(New(workflowAPIPlugin)), - Must(New(workflowStepAPIPlugin)), - Must(New(workflowSupportPlugin)), - }, - Must(New(workflowAggregatorPlugin)).String(): { - Must(New(aceEditorPlugin)), - Must(New(apacheComponentsClientPlugin)), - Must(New(authenticationTokensPlugin)), - Must(New(branchAPIPlugin)), - Must(New(cloudBeesFolderPlugin)), - Must(New(credentialsBindingPlugin)), - Must(New(credentialsPlugin)), - Must(New(displayURLAPIPlugin)), - Must(New(dockerCommonsPlugin)), - Must(New(dockerWorkflowPlugin)), - Must(New(durableTaskPlugin)), - Must(New(gitClientPlugin)), - Must(New(gitServerPlugin)), - Must(New(handlebarsPlugin)), - Must(New(jackson2ADIPlugin)), - Must(New(jqueryDetachedPlugin)), - Must(New(jschPlugin)), - Must(New(junitPlugin)), - Must(New(lockableResourcesPlugin)), - Must(New(mailerPlugin)), - Must(New(matrixProjectPlugin)), - Must(New(momentjsPlugin)), - Must(New(pipelineBuildStepPlugin)), - Must(New(pipelineGraphAnalysisPlugin)), - Must(New(pipelineInputStepPlugin)), - Must(New(pipelineMilestoneStepPlugin)), - Must(New(pipelineModelAPIPlugin)), - Must(New(pipelineModelDeclarativeAgentPlugin)), - Must(New(pipelineModelDefinitionPlugin)), - Must(New(pipelineModelExtensionsPlugin)), - Must(New(pipelineRestAPIPlugin)), - Must(New(pipelineStageStepPlugin)), - Must(New(pipelineStageTagsMetadataPlugin)), - Must(New(pipelineStageViewPlugin)), - Must(New(plainCredentialsPlugin)), - Must(New(scmAPIPlugin)), - Must(New(scriptSecurityPlugin)), - Must(New(sshCredentialsPlugin)), - Must(New(structsPlugin)), - Must(New(workflowAPIPlugin)), - Must(New(workflowBasicStepsPlugin)), - Must(New(workflowCpsGlobalLibPlugin)), - Must(New(workflowCpsPlugin)), - Must(New(workflowDurableTaskStepPlugin)), - Must(New(workflowJobPlugin)), - Must(New(workflowMultibranchPlugin)), - Must(New(workflowSCMStepPlugin)), - Must(New(workflowStepAPIPlugin)), - Must(New(workflowSupportPlugin)), - }, - Must(New(gitPlugin)).String(): { - 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(jobDslPlugin)).String(): { - Must(New(scriptSecurityPlugin)), - Must(New(structsPlugin)), - }, - Must(New(configurationAsCodePlugin)).String(): { - Must(New(configurationAsCodeSupportPlugin)), - }, - Must(New(kubernetesCredentialsProviderPlugin)).String(): { - Must(New(credentialsPlugin)), - Must(New(structsPlugin)), - Must(New(variantPlugin)), - }, +// basePluginsList contains plugins to install by operator +var basePluginsList = []Plugin{ + Must(New(kubernetesPlugin)), + Must(New(workflowJobPlugin)), + Must(New(workflowAggregatorPlugin)), + Must(New(gitPlugin)), + Must(New(jobDslPlugin)), + Must(New(configurationAsCodePlugin)), + Must(New(configurationAsCodeSupportPlugin)), + Must(New(kubernetesCredentialsProviderPlugin)), } -// BasePlugins returns map of plugins to install by operator -func BasePlugins() (plugins map[string][]string) { - plugins = map[string][]string{} - - for rootPluginName, dependentPlugins := range BasePluginsMap { - plugins[rootPluginName] = []string{} - for _, pluginName := range dependentPlugins { - plugins[rootPluginName] = append(plugins[rootPluginName], pluginName.String()) - } - } - - return +// BasePlugins returns list of plugins to install by operator +func BasePlugins() []Plugin { + return basePluginsList } diff --git a/pkg/controller/jenkins/plugins/plugin.go b/pkg/controller/jenkins/plugins/plugin.go index 12d7368a..2fcc8c3c 100644 --- a/pkg/controller/jenkins/plugins/plugin.go +++ b/pkg/controller/jenkins/plugins/plugin.go @@ -2,6 +2,7 @@ package plugins import ( "fmt" + "regexp" "strings" "github.com/jenkinsci/kubernetes-operator/pkg/log" @@ -20,18 +21,52 @@ func (p Plugin) String() string { return fmt.Sprintf("%s:%s", p.Name, p.Version) } +var ( + namePattern = regexp.MustCompile(`^[0-9a-z-]+$`) + versionPattern = regexp.MustCompile(`^[0-9\\.]+$`) +) + // New creates plugin from string, for example "name-of-plugin:0.0.1" func New(nameWithVersion string) (*Plugin, error) { val := strings.SplitN(nameWithVersion, ":", 2) if val == nil || len(val) != 2 { return nil, errors.Errorf("invalid plugin format '%s'", nameWithVersion) } + name := val[0] + version := val[1] + + if err := validatePlugin(name, version); err != nil { + return nil, err + } + return &Plugin{ - Name: val[0], - Version: val[1], + Name: name, + Version: version, }, nil } +// NewPlugin creates plugin from name and version, for example "name-of-plugin:0.0.1" +func NewPlugin(name, version string) (*Plugin, error) { + if err := validatePlugin(name, version); err != nil { + return nil, err + } + + return &Plugin{ + Name: name, + Version: version, + }, nil +} + +func validatePlugin(name, version string) error { + if ok := namePattern.MatchString(name); !ok { + return errors.Errorf("invalid plugin name '%s:%s', must follow pattern '%s'", name, version, namePattern.String()) + } + if ok := versionPattern.MatchString(version); !ok { + return errors.Errorf("invalid plugin version '%s:%s', must follow pattern '%s'", name, version, versionPattern.String()) + } + return nil +} + // Must returns plugin from pointer and throws panic when error is set func Must(plugin *Plugin, err error) Plugin { if err != nil { diff --git a/test/e2e/configuration_test.go b/test/e2e/configuration_test.go index 5e83fb1c..b1321fcd 100644 --- a/test/e2e/configuration_test.go +++ b/test/e2e/configuration_test.go @@ -228,25 +228,16 @@ func verifyPlugins(t *testing.T, jenkinsClient jenkinsclient.Jenkins, jenkins *v t.Fatal(err) } - 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) - } - } + for _, basePlugin := range plugins.BasePlugins() { + if found, ok := isPluginValid(installedPlugins, basePlugin); !ok { + t.Fatalf("Invalid plugin '%s', actual '%+v'", basePlugin, found) + } + } + + for _, userPlugin := range jenkins.Spec.Master.Plugins { + plugin := plugins.Plugin{Name: userPlugin.Name, Version: userPlugin.Version} + if found, ok := isPluginValid(installedPlugins, plugin); !ok { + t.Fatalf("Invalid plugin '%s', actual '%+v'", plugin, found) } } diff --git a/test/e2e/jenkins.go b/test/e2e/jenkins.go index b18eaa80..7d3e4cf3 100644 --- a/test/e2e/jenkins.go +++ b/test/e2e/jenkins.go @@ -113,9 +113,9 @@ func createJenkinsCR(t *testing.T, name, namespace string, seedJob *[]v1alpha2.S Image: "envoyproxy/envoy-alpine", }, }, - Plugins: map[string][]string{ - "audit-trail:2.4": {}, - "simple-theme-plugin:0.5.1": {}, + Plugins: []v1alpha2.Plugin{ + {Name: "audit-trail:", Version: "2.4"}, + {Name: "simple-theme-plugin", Version: "0.5.1"}, }, NodeSelector: map[string]string{"kubernetes.io/hostname": "minikube"}, Volumes: volumes,