diff --git a/api/v1alpha2/jenkins_webhook.go b/api/v1alpha2/jenkins_webhook.go index 51a7e54a..222aab2d 100644 --- a/api/v1alpha2/jenkins_webhook.go +++ b/api/v1alpha2/jenkins_webhook.go @@ -79,6 +79,7 @@ func (in *Jenkins) ValidateDelete() error { type PluginDataManager struct { PluginDataCache PluginsInfo Hosturl string + Timeout time.Duration CompressedFilePath string PluginDataFile string IsCached bool @@ -160,9 +161,9 @@ func Validate(r Jenkins) error { if hasVulnerabilities { if pluginData.Kind == "base" { - faultyBasePlugins += plugin.Name + ":" + pluginData.Version + "\n" + faultyBasePlugins += "\n" + plugin.Name + ":" + pluginData.Version } else { - faultyUserPlugins += plugin.Name + ":" + pluginData.Version + "\n" + faultyUserPlugins += "\n" + plugin.Name + ":" + pluginData.Version } } } @@ -170,10 +171,10 @@ func Validate(r Jenkins) error { if len(faultyBasePlugins) > 0 || len(faultyUserPlugins) > 0 { var errormsg string if len(faultyBasePlugins) > 0 { - errormsg += "Security vulnerabilities detected in the following base plugins: \n" + faultyBasePlugins + errormsg += "security vulnerabilities detected in the following base plugins: " + faultyBasePlugins } if len(faultyUserPlugins) > 0 { - errormsg += "Security vulnerabilities detected in the following user-defined plugins: \n" + faultyUserPlugins + errormsg += "security vulnerabilities detected in the following user-defined plugins: " + faultyUserPlugins } return errors.New(errormsg) } @@ -188,6 +189,7 @@ func NewPluginsDataManager() *PluginDataManager { PluginDataFile: "/tmp/plugins.json", IsCached: false, Attempts: 0, + Timeout: time.Duration(1000) * time.Second, } } @@ -256,7 +258,7 @@ func (in *PluginDataManager) download() error { defer out.Close() client := http.Client{ - Timeout: 1000 * time.Second, + Timeout: in.Timeout, } resp, err := client.Get(in.Hosturl) @@ -314,9 +316,11 @@ func (in *PluginDataManager) cache() error { return nil } -// returns a semantic version that can be used for comparison +// returns a semantic version that can be used for comparison, allowed versioning format vMAJOR.MINOR.PATCH or MAJOR.MINOR.PATCH func makeSemanticVersion(version string) string { - version = "v" + version + if version[0] != 'v' { + version = "v" + version + } return semver.Canonical(version) } diff --git a/api/v1alpha2/jenkins_webhook_test.go b/api/v1alpha2/jenkins_webhook_test.go new file mode 100644 index 00000000..c04ae2c6 --- /dev/null +++ b/api/v1alpha2/jenkins_webhook_test.go @@ -0,0 +1,163 @@ +package v1alpha2 + +import ( + "errors" + "testing" + "time" + + "github.com/jenkinsci/kubernetes-operator/pkg/constants" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestMakeSemanticVersion(t *testing.T) { + t.Run("only major version specified", func(t *testing.T) { + got := makeSemanticVersion("1") + assert.Equal(t, got, "v1.0.0") + }) + + t.Run("major and minor version specified", func(t *testing.T) { + got := makeSemanticVersion("1.2") + assert.Equal(t, got, "v1.2.0") + }) + + t.Run("major,minor and patch version specified", func(t *testing.T) { + got := makeSemanticVersion("1.2.3") + assert.Equal(t, got, "v1.2.3") + }) + + t.Run("semantic versions begin with a leading v and no patch version", func(t *testing.T) { + got := makeSemanticVersion("v2.5") + assert.Equal(t, got, "v2.5.0") + }) + + t.Run("semantic versions with prerelease versions", func(t *testing.T) { + got := makeSemanticVersion("2.1.2-alpha.1") + assert.Equal(t, got, "v2.1.2-alpha.1") + }) + + t.Run("semantic versions with prerelease versions", func(t *testing.T) { + got := makeSemanticVersion("0.11.2-9.c8b45b8bb173") + assert.Equal(t, got, "v0.11.2-9.c8b45b8bb173") + }) + + t.Run("semantic versions with build suffix", func(t *testing.T) { + got := makeSemanticVersion("1.7.9+meta") + assert.Equal(t, got, "v1.7.9") + }) + + t.Run("invalid semantic version", func(t *testing.T) { + got := makeSemanticVersion("google-login-1.2") + assert.Equal(t, got, "") + }) +} + +func TestCompareVersions(t *testing.T) { + t.Run("Plugin Version lies between first and last version", func(t *testing.T) { + got := compareVersions("1.2", "1.6", "1.4") + assert.Equal(t, got, true) + }) + t.Run("Plugin Version is greater than the last version", func(t *testing.T) { + got := compareVersions("1", "2", "3") + assert.Equal(t, got, false) + }) + t.Run("Plugin Version is less than the first version", func(t *testing.T) { + got := compareVersions("1.4", "2.5", "1.1") + assert.Equal(t, got, false) + }) + + t.Run("Plugins Versions have prerelease version and it lies between first and last version", func(t *testing.T) { + got := compareVersions("1.2.1-alpha", "1.2.1", "1.2.1-beta") + assert.Equal(t, got, true) + }) + + t.Run("Plugins Versions have prerelease version and it is greater than the last version", func(t *testing.T) { + got := compareVersions("v2.2.1-alpha", "v2.5.1-beta.1", "v2.5.1-beta.2") + assert.Equal(t, got, false) + }) +} + +func TestValidate(t *testing.T) { + t.Run("Validating when plugins data file is not fetched", func(t *testing.T) { + userplugins := []Plugin{{Name: "script-security", Version: "1.77"}, {Name: "git-client", Version: "3.9"}, {Name: "git", Version: "4.8.1"}, {Name: "plain-credentials", Version: "1.7"}} + jenkinscr := *createJenkinsCR(userplugins, true) + got := jenkinscr.ValidateCreate() + assert.Equal(t, got, errors.New("plugins data has not been fetched")) + }) + + isInitialized := make(chan bool) + go PluginsMgr.FetchPluginData(isInitialized) + if <-isInitialized { + t.Run("Validating a Jenkins CR with plugins not having security warnings and validation is turned on", func(t *testing.T) { + userplugins := []Plugin{{Name: "script-security", Version: "1.77"}, {Name: "git-client", Version: "3.9"}, {Name: "git", Version: "4.8.1"}, {Name: "plain-credentials", Version: "1.7"}} + jenkinscr := *createJenkinsCR(userplugins, true) + got := jenkinscr.ValidateCreate() + assert.Nil(t, got) + }) + + t.Run("Validating a Jenkins CR with some of the plugins having security warnings and validation is turned on", func(t *testing.T) { + userplugins := []Plugin{{Name: "google-login", Version: "1.2"}, {Name: "mailer", Version: "1.1"}, {Name: "git", Version: "4.8.1"}, {Name: "command-launcher", Version: "1.6"}, {Name: "workflow-cps", Version: "2.59"}} + jenkinscr := *createJenkinsCR(userplugins, true) + got := jenkinscr.ValidateCreate() + assert.Equal(t, got, errors.New("security vulnerabilities detected in the following user-defined plugins: \nworkflow-cps:2.59\ngoogle-login:1.2\nmailer:1.1")) + }) + + t.Run("Updating a Jenkins CR with some of the plugins having security warnings and validation is turned on", func(t *testing.T) { + userplugins := []Plugin{{Name: "google-login", Version: "1.2"}, {Name: "mailer", Version: "1.1"}, {Name: "git", Version: "4.8.1"}, {Name: "command-launcher", Version: "1.6"}, {Name: "workflow-cps", Version: "2.59"}} + oldjenkinscr := *createJenkinsCR(userplugins, true) + + userplugins = []Plugin{{Name: "handy-uri-templates-2-api", Version: "2.1.8-1.0"}, {Name: "resource-disposer", Version: "0.8"}, {Name: "jjwt-api", Version: "0.11.2-9.c8b45b8bb173"}, {Name: "blueocean-github-pipeline", Version: "1.2.0-beta-3"}, {Name: "ghprb", Version: "1.39"}} + newjenkinscr := *createJenkinsCR(userplugins, true) + got := newjenkinscr.ValidateUpdate(&oldjenkinscr) + assert.Equal(t, got, errors.New("security vulnerabilities detected in the following user-defined plugins: \nresource-disposer:0.8\nblueocean-github-pipeline:1.2.0-beta-3\nghprb:1.39")) + }) + + t.Run("Validation is turned off", func(t *testing.T) { + userplugins := []Plugin{{Name: "google-login", Version: "1.2"}, {Name: "mailer", Version: "1.1"}, {Name: "git", Version: "4.8.1"}, {Name: "command-launcher", Version: "1.6"}, {Name: "workflow-cps", Version: "2.59"}} + jenkinscr := *createJenkinsCR(userplugins, false) + got := jenkinscr.ValidateCreate() + assert.Nil(t, got) + }) + } else { + t.Fatal("Plugin Data File is not Downloaded") + } +} + +func TestFetchPluginData(t *testing.T) { + t.Run("Timeout error while downloading plugins data file", func(t *testing.T) { + pluginsDataMgr := *NewPluginsDataManager() + pluginsDataMgr.Timeout = time.Duration(1) * time.Nanosecond + got := pluginsDataMgr.download() + assert.NotNil(t, got) + }) + t.Run("Successfully fetching plugins data file", func(t *testing.T) { + isInitialized := make(chan bool) + pluginsDataMgr := *NewPluginsDataManager() + go pluginsDataMgr.FetchPluginData(isInitialized) + assert.Equal(t, <-isInitialized, true) + }) +} + +func createJenkinsCR(userPlugins []Plugin, validateSecurityWarnings bool) *Jenkins { + jenkins := &Jenkins{ + TypeMeta: JenkinsTypeMeta(), + ObjectMeta: metav1.ObjectMeta{ + Name: "Jenkins", + Namespace: "test", + }, + Spec: JenkinsSpec{ + Master: JenkinsMaster{ + Annotations: map[string]string{"test": "label"}, + Plugins: userPlugins, + }, + ValidateSecurityWarnings: validateSecurityWarnings, + Service: Service{ + Type: corev1.ServiceTypeNodePort, + Port: constants.DefaultHTTPPortInt32, + }, + }, + } + + return jenkins +}