-
Notifications
You must be signed in to change notification settings - Fork 239
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implemented validation logic for the webhook #593
Changes from 27 commits
b407e51
52fe5fe
db0978c
800c1a7
35dfd47
7ce9d1f
463cad3
bd32624
9d5c525
935b60b
dfd25e8
17dba08
37d0eac
8453b3e
858f0f4
b82fc7c
b400a42
1d2651d
853f485
9106582
90b685d
b11ca32
5ca4e0a
e2ec2ea
f527a8c
9594c8e
95c29d4
ba66ba4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -5,7 +5,7 @@ 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 | ||||||
http://www.apache.org/lictenses/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, | ||||||
|
@@ -17,14 +17,33 @@ limitations under the License. | |||||
package v1alpha2 | ||||||
|
||||||
import ( | ||||||
"compress/gzip" | ||||||
"encoding/json" | ||||||
"errors" | ||||||
"io" | ||||||
"io/ioutil" | ||||||
"net/http" | ||||||
"os" | ||||||
"time" | ||||||
|
||||||
//"github.com/jenkinsci/kubernetes-operator/pkg/configuration/base/resources" | ||||||
|
||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Don't leave commented parts in code. |
||||||
"github.com/jenkinsci/kubernetes-operator/pkg/plugins" | ||||||
|
||||||
"golang.org/x/mod/semver" | ||||||
"k8s.io/apimachinery/pkg/runtime" | ||||||
ctrl "sigs.k8s.io/controller-runtime" | ||||||
logf "sigs.k8s.io/controller-runtime/pkg/log" | ||||||
"sigs.k8s.io/controller-runtime/pkg/webhook" | ||||||
) | ||||||
|
||||||
// log is for logging in this package. | ||||||
var jenkinslog = logf.Log.WithName("jenkins-resource") | ||||||
var ( | ||||||
jenkinslog = logf.Log.WithName("jenkins-resource") // log is for logging in this package. | ||||||
PluginsMgr PluginDataManager = *NewPluginsDataManager("/tmp/plugins.json.gzip", "/tmp/plugins.json", false, time.Duration(1000)*time.Second) | ||||||
_ webhook.Validator = &Jenkins{} | ||||||
) | ||||||
|
||||||
const Hosturl = "https://ci.jenkins.io/job/Infra/job/plugin-site-api/job/generate-data/lastSuccessfulBuild/artifact/plugins.json.gzip" | ||||||
|
||||||
func (in *Jenkins) SetupWebhookWithManager(mgr ctrl.Manager) error { | ||||||
return ctrl.NewWebhookManagedBy(mgr). | ||||||
|
@@ -37,25 +56,291 @@ func (in *Jenkins) SetupWebhookWithManager(mgr ctrl.Manager) error { | |||||
// 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} | ||||||
|
||||||
var _ webhook.Validator = &Jenkins{} | ||||||
|
||||||
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type | ||||||
func (in *Jenkins) ValidateCreate() error { | ||||||
jenkinslog.Info("validate create", "name", in.Name) | ||||||
if in.Spec.ValidateSecurityWarnings { | ||||||
jenkinslog.Info("validate create", "name", in.Name) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are these logs useful in any way? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah for debugging purposes I was checking if the function is being called and get a look the the reconciller object. |
||||||
return Validate(*in) | ||||||
} | ||||||
|
||||||
// TODO(user): fill in your validation logic upon object creation. | ||||||
return nil | ||||||
} | ||||||
|
||||||
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type | ||||||
func (in *Jenkins) ValidateUpdate(old runtime.Object) error { | ||||||
jenkinslog.Info("validate update", "name", in.Name) | ||||||
if in.Spec.ValidateSecurityWarnings { | ||||||
jenkinslog.Info("validate update", "name", in.Name) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are these logs useful in any way? |
||||||
return Validate(*in) | ||||||
} | ||||||
|
||||||
// TODO(user): fill in your validation logic upon object update. | ||||||
return nil | ||||||
} | ||||||
|
||||||
func (in *Jenkins) ValidateDelete() error { | ||||||
// TODO(user): fill in your validation logic upon object deletion. | ||||||
return nil | ||||||
} | ||||||
|
||||||
type PluginDataManager struct { | ||||||
PluginDataCache PluginsInfo | ||||||
Timeout time.Duration | ||||||
CompressedFilePath string | ||||||
PluginDataFile string | ||||||
IsCached bool | ||||||
Attempts int | ||||||
SleepTime time.Duration | ||||||
} | ||||||
|
||||||
type PluginsInfo struct { | ||||||
Plugins []PluginInfo `json:"plugins"` | ||||||
} | ||||||
|
||||||
type PluginInfo struct { | ||||||
Name string `json:"name"` | ||||||
SecurityWarnings []Warning `json:"securityWarnings"` | ||||||
} | ||||||
|
||||||
type Warning struct { | ||||||
Versions []Version `json:"versions"` | ||||||
ID string `json:"id"` | ||||||
Message string `json:"message"` | ||||||
URL string `json:"url"` | ||||||
Active bool `json:"active"` | ||||||
} | ||||||
|
||||||
type Version struct { | ||||||
FirstVersion string `json:"firstVersion"` | ||||||
LastVersion string `json:"lastVersion"` | ||||||
} | ||||||
|
||||||
type PluginData struct { | ||||||
Version string | ||||||
Kind string | ||||||
} | ||||||
|
||||||
// Validates security warnings for both updating and creating a Jenkins CR | ||||||
func Validate(r Jenkins) error { | ||||||
if !PluginsMgr.IsCached { | ||||||
return errors.New("plugins data has not been fetched") | ||||||
} | ||||||
|
||||||
pluginSet := make(map[string]PluginData) | ||||||
var faultyBasePlugins string | ||||||
var faultyUserPlugins string | ||||||
basePlugins := plugins.BasePlugins() | ||||||
|
||||||
for _, plugin := range basePlugins { | ||||||
// Only Update the map if the plugin is not present or a lower version is being used | ||||||
if pluginData, ispresent := pluginSet[plugin.Name]; !ispresent || semver.Compare(makeSemanticVersion(plugin.Version), pluginData.Version) == 1 { | ||||||
pluginSet[plugin.Name] = PluginData{Version: plugin.Version, Kind: "base"} | ||||||
} | ||||||
} | ||||||
|
||||||
for _, plugin := range r.Spec.Master.Plugins { | ||||||
if pluginData, ispresent := pluginSet[plugin.Name]; !ispresent || semver.Compare(makeSemanticVersion(plugin.Version), pluginData.Version) == 1 { | ||||||
pluginSet[plugin.Name] = PluginData{Version: plugin.Version, Kind: "user-defined"} | ||||||
} | ||||||
} | ||||||
|
||||||
for _, plugin := range PluginsMgr.PluginDataCache.Plugins { | ||||||
if pluginData, ispresent := pluginSet[plugin.Name]; ispresent { | ||||||
var hasVulnerabilities bool | ||||||
for _, warning := range plugin.SecurityWarnings { | ||||||
for _, version := range warning.Versions { | ||||||
firstVersion := version.FirstVersion | ||||||
lastVersion := version.LastVersion | ||||||
if len(firstVersion) == 0 { | ||||||
firstVersion = "0" // setting default value in case of empty string | ||||||
} | ||||||
if len(lastVersion) == 0 { | ||||||
lastVersion = pluginData.Version // setting default value in case of empty string | ||||||
} | ||||||
// checking if this warning applies to our version as well | ||||||
if compareVersions(firstVersion, lastVersion, pluginData.Version) { | ||||||
jenkinslog.Info("Security Vulnerability detected in "+pluginData.Kind+" "+plugin.Name+":"+pluginData.Version, "Warning message", warning.Message, "For more details,check security advisory", warning.URL) | ||||||
hasVulnerabilities = true | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
if hasVulnerabilities { | ||||||
if pluginData.Kind == "base" { | ||||||
faultyBasePlugins += "\n" + plugin.Name + ":" + pluginData.Version | ||||||
} else { | ||||||
faultyUserPlugins += "\n" + plugin.Name + ":" + pluginData.Version | ||||||
} | ||||||
} | ||||||
} | ||||||
} | ||||||
if len(faultyBasePlugins) > 0 || len(faultyUserPlugins) > 0 { | ||||||
var errormsg string | ||||||
if len(faultyBasePlugins) > 0 { | ||||||
errormsg += "security vulnerabilities detected in the following base plugins: " + faultyBasePlugins | ||||||
} | ||||||
if len(faultyUserPlugins) > 0 { | ||||||
errormsg += "security vulnerabilities detected in the following user-defined plugins: " + faultyUserPlugins | ||||||
} | ||||||
return errors.New(errormsg) | ||||||
} | ||||||
|
||||||
return nil | ||||||
} | ||||||
|
||||||
func NewPluginsDataManager(compressedFilePath string, pluginDataFile string, isCached bool, timeout time.Duration) *PluginDataManager { | ||||||
return &PluginDataManager{ | ||||||
CompressedFilePath: compressedFilePath, | ||||||
PluginDataFile: pluginDataFile, | ||||||
IsCached: isCached, | ||||||
Timeout: timeout, | ||||||
} | ||||||
} | ||||||
|
||||||
func (in *PluginDataManager) ManagePluginData(sig chan bool) { | ||||||
var isInit bool // checks if the operator | ||||||
var retryInterval time.Duration | ||||||
for { | ||||||
SylwiaBrant marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
isCached := in.fetchPluginData() | ||||||
if !isInit { | ||||||
sig <- isCached // sending signal to main to continue | ||||||
isInit = false | ||||||
} | ||||||
|
||||||
in.IsCached = in.IsCached || isCached | ||||||
if !isCached { | ||||||
retryInterval = time.Duration(1) * time.Hour | ||||||
} else { | ||||||
retryInterval = time.Duration(12) * time.Hour | ||||||
} | ||||||
time.Sleep(retryInterval) | ||||||
} | ||||||
} | ||||||
|
||||||
// Downloads extracts and reads the JSON data in every 12 hours | ||||||
func (in *PluginDataManager) fetchPluginData() bool { | ||||||
jenkinslog.Info("Initializing/Updating the plugin data cache") | ||||||
var err error | ||||||
for in.Attempts = 0; in.Attempts < 5; in.Attempts++ { | ||||||
err = in.download() | ||||||
if err == nil { | ||||||
break | ||||||
} | ||||||
jenkinslog.V(1).Info("Cache Plugin Data", "failed to download file", err) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
We have a constant to not use magic numbers. |
||||||
} | ||||||
|
||||||
if err != nil { | ||||||
jenkinslog.Info("Cache Plugin Data", "failed to download file", err) | ||||||
return false | ||||||
} | ||||||
|
||||||
for in.Attempts = 0; in.Attempts < 5; in.Attempts++ { | ||||||
err = in.extract() | ||||||
if err == nil { | ||||||
break | ||||||
} | ||||||
jenkinslog.V(1).Info("Cache Plugin Data", "failed to extract file", err) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
} | ||||||
|
||||||
if err != nil { | ||||||
jenkinslog.Info("Cache Plugin Data", "failed to extract file", err) | ||||||
return false | ||||||
} | ||||||
|
||||||
for in.Attempts = 0; in.Attempts < 5; in.Attempts++ { | ||||||
err = in.cache() | ||||||
if err == nil { | ||||||
break | ||||||
} | ||||||
jenkinslog.V(1).Info("Cache Plugin Data", "failed to read plugin data file", err) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
} | ||||||
|
||||||
if err != nil { | ||||||
jenkinslog.Info("Cache Plugin Data", "failed to read plugin data file", err) | ||||||
return false | ||||||
} | ||||||
|
||||||
return true | ||||||
} | ||||||
|
||||||
func (in *PluginDataManager) download() error { | ||||||
out, err := os.Create(in.CompressedFilePath) | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
defer out.Close() | ||||||
|
||||||
client := http.Client{ | ||||||
Timeout: in.Timeout, | ||||||
} | ||||||
|
||||||
resp, err := client.Get(Hosturl) | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
defer resp.Body.Close() | ||||||
|
||||||
_, err = io.Copy(out, resp.Body) | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
|
||||||
return nil | ||||||
} | ||||||
|
||||||
func (in *PluginDataManager) extract() error { | ||||||
reader, err := os.Open(in.CompressedFilePath) | ||||||
|
||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
defer reader.Close() | ||||||
archive, err := gzip.NewReader(reader) | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
|
||||||
defer archive.Close() | ||||||
writer, err := os.Create(in.PluginDataFile) | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
defer writer.Close() | ||||||
|
||||||
_, err = io.Copy(writer, archive) | ||||||
return err | ||||||
} | ||||||
|
||||||
// Loads the JSON data into memory and stores it | ||||||
func (in *PluginDataManager) cache() error { | ||||||
jsonFile, err := os.Open(in.PluginDataFile) | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
defer jsonFile.Close() | ||||||
byteValue, err := ioutil.ReadAll(jsonFile) | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
err = json.Unmarshal(byteValue, &in.PluginDataCache) | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
return nil | ||||||
} | ||||||
|
||||||
// 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 { | ||||||
if version[0] != 'v' { | ||||||
version = "v" + version | ||||||
} | ||||||
return semver.Canonical(version) | ||||||
} | ||||||
|
||||||
// Compare if the current version lies between first version and last version | ||||||
func compareVersions(firstVersion string, lastVersion string, pluginVersion string) bool { | ||||||
firstSemVer := makeSemanticVersion(firstVersion) | ||||||
lastSemVer := makeSemanticVersion(lastVersion) | ||||||
pluginSemVer := makeSemanticVersion(pluginVersion) | ||||||
if semver.Compare(pluginSemVer, firstSemVer) == -1 || semver.Compare(pluginSemVer, lastSemVer) == 1 { | ||||||
return false | ||||||
} | ||||||
return true | ||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please revert this