diff --git a/api/v1alpha2/jenkins_webhook.go b/api/v1alpha2/jenkins_webhook.go index 97af23c9..56750fc2 100644 --- a/api/v1alpha2/jenkins_webhook.go +++ b/api/v1alpha2/jenkins_webhook.go @@ -36,14 +36,9 @@ import ( ) var ( - jenkinslog = logf.Log.WithName("jenkins-resource") // log is for logging in this package. - isRetrieved bool = false // For checking whether the data file is downloaded and extracted or not -) - -const ( - hosturl string = "https://ci.jenkins.io/job/Infra/job/plugin-site-api/job/generate-data/lastSuccessfulBuild/artifact/plugins.json.gzip" // Url for downloading the plugins file - compressedFile string = "/tmp/plugins.json.gzip" // location where the gzip file will be downloaded - pluginDataFile string = "/tmp/plugins.json" // location where the file will be extracted + jenkinslog = logf.Log.WithName("jenkins-resource") // log is for logging in this package. + PluginsDataManager PluginDataManager = *NewPluginsDataManager() + _ webhook.Validator = &Jenkins{} ) func (in *Jenkins) SetupWebhookWithManager(mgr ctrl.Manager) error { @@ -57,8 +52,6 @@ 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 { if in.Spec.ValidateSecurityWarnings { @@ -83,6 +76,15 @@ func (in *Jenkins) ValidateDelete() error { return nil } +type PluginDataManager struct { + pluginDataCache PluginsInfo + hosturl string + compressedFilePath string + pluginDataFile string + iscached bool + maxattempts int +} + type PluginsInfo struct { Plugins []PluginInfo `json:"plugins"` } @@ -112,36 +114,27 @@ type PluginData struct { // Validates security warnings for both updating and creating a Jenkins CR func Validate(r Jenkins) error { - pluginset := make(map[string]PluginData) - var warningmsg string + var faultybaseplugins string + var faultyuserplugins string basePlugins := plugins.BasePlugins() - temp, err := NewPluginsInfo() - AllPluginData := *temp - if err != nil { - return err - } 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 { - jenkinslog.Info("Validate", plugin.Name, plugin.Version) 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 { - jenkinslog.Info("Validate", plugin.Name, plugin.Version) pluginset[plugin.Name] = PluginData{Version: plugin.Version, Kind: "user-defined"} } } - jenkinslog.Info("Checking through all the warnings") - for _, plugin := range AllPluginData.Plugins { - + for _, plugin := range PluginsDataManager.pluginDataCache.Plugins { if pluginData, ispresent := pluginset[plugin.Name]; ispresent { - jenkinslog.Info("Checking for plugin", "name", plugin.Name) + var hasvulnerabilities bool for _, warning := range plugin.SecurityWarnings { for _, version := range warning.Versions { firstVersion := version.FirstVersion @@ -154,113 +147,121 @@ func Validate(r Jenkins) error { } if CompareVersions(firstVersion, lastVersion, pluginData.Version) { - jenkinslog.Info("security Vulnerabilities detected", "message", warning.Message, "Check security Advisory", warning.URL) - warningmsg += "Security Vulnerabilities detected in " + pluginData.Kind + " plugin " + plugin.Name + "\n" - + 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 += plugin.Name + ":" + pluginData.Version + "\n" + } else { + faultyuserplugins += plugin.Name + ":" + pluginData.Version + "\n" + } + } } - } - - if len(warningmsg) > 0 { - return errors.New(warningmsg) + 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 + } + if len(faultyuserplugins) > 0 { + errormsg += "Security vulnerabilities detected in the following user-defined plugins: \n" + faultyuserplugins + } + return errors.New(errormsg) } return nil - } -// Returns an object containing information of all the plugins present in the security center -func NewPluginsInfo() (*PluginsInfo, error) { - var AllPluginData PluginsInfo - for i := 0; i < 28; i++ { - if isRetrieved { - break - } - time.Sleep(1 * time.Second) +func NewPluginsDataManager() *PluginDataManager { + return &PluginDataManager{ + hosturl: "https://ci.jenkins.io/job/Infra/job/plugin-site-api/job/generate-data/lastSuccessfulBuild/artifact/plugins.json.gzip", + compressedFilePath: "/tmp/plugins.json.gzip", + pluginDataFile: "/tmp/plugins.json", + iscached: false, + maxattempts: 5, } - if !isRetrieved { - jenkinslog.Info("Plugins Data file hasn't been downloaded and extracted") - return &AllPluginData, errors.New("plugins data file not found") - } - - jsonFile, err := os.Open(pluginDataFile) - if err != nil { - jenkinslog.Info("Failed to open the Plugins Data File") - return &AllPluginData, err - } - defer jsonFile.Close() - - byteValue, err := ioutil.ReadAll(jsonFile) - if err != nil { - jenkinslog.Info("Failed to convert the JSON file into a byte array") - return &AllPluginData, err - } - err = json.Unmarshal(byteValue, &AllPluginData) - if err != nil { - jenkinslog.Info("Failed to decode the Plugin JSON data file") - return &AllPluginData, err - } - - return &AllPluginData, nil } -// Downloads and extracts the JSON file in every 12 hours -func RetrieveDataFile() { +// Downloads extracts and caches the JSON data in every 12 hours +func (in *PluginDataManager) CachePluginData(ch chan bool) { for { - jenkinslog.Info("Retreiving file", "Host Url", hosturl) - err := Download() - if err != nil { - jenkinslog.Info("Retrieving File", "Error while downloading", err) - continue + jenkinslog.Info("Initializing/Updating the plugin data cache") + var isdownloaded, isextracted, iscached bool + var err error + for i := 0; i < in.maxattempts; i++ { + err = in.Download() + if err == nil { + isdownloaded = true + break + } } - jenkinslog.Info("Retrieve File", "Successfully downloaded", compressedFile) - err = Extract() - if err != nil { - jenkinslog.Info("Retreive File", "Error while extracting", err) - continue + if isdownloaded { + for i := 0; i < in.maxattempts; i++ { + err = in.Extract() + if err == nil { + isextracted = true + break + } + } + } else { + jenkinslog.Info("Cache Plugin Data", "failed to download file", err) } - jenkinslog.Info("Retreive File", "Successfully extracted", pluginDataFile) - isRetrieved = true + + if isextracted { + for i := 0; i < in.maxattempts; i++ { + err = in.Cache() + if err == nil { + iscached = true + break + } + } + + if !iscached { + jenkinslog.Info("Cache Plugin Data", "failed to read plugin data file", err) + } + } else { + jenkinslog.Info("Cache Plugin Data", "failed to extract file", err) + } + + if !in.iscached { + ch <- iscached + } + in.iscached = in.iscached || iscached time.Sleep(12 * time.Hour) - } } -func Download() error { - - out, err := os.Create(compressedFile) +func (in *PluginDataManager) Download() error { + out, err := os.Create(in.compressedFilePath) if err != nil { return err } defer out.Close() client := http.Client{ - Timeout: 2000 * time.Second, + Timeout: 1000 * time.Second, } - resp, err := client.Get(hosturl) + resp, err := client.Get(in.hosturl) if err != nil { return err } defer resp.Body.Close() - jenkinslog.Info("Successfully Downloaded") _, err = io.Copy(out, resp.Body) if err != nil { return err } return nil - } -func Extract() error { - reader, err := os.Open(compressedFile) +func (in *PluginDataManager) Extract() error { + reader, err := os.Open(in.compressedFilePath) if err != nil { return err @@ -272,7 +273,7 @@ func Extract() error { } defer archive.Close() - writer, err := os.Create(pluginDataFile) + writer, err := os.Create(in.pluginDataFile) if err != nil { return err } @@ -280,10 +281,28 @@ func Extract() error { _, err = io.Copy(writer, archive) return err - } -// returns a semantic version that can be used for comparision +// 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 func MakeSemanticVersion(version string) string { version = "v" + version return semver.Canonical(version) diff --git a/config/samples/jenkins.io_v1alpha2_jenkins.yaml b/config/samples/jenkins.io_v1alpha2_jenkins.yaml index be33732a..49f39c93 100644 --- a/config/samples/jenkins.io_v1alpha2_jenkins.yaml +++ b/config/samples/jenkins.io_v1alpha2_jenkins.yaml @@ -1,8 +1,9 @@ + apiVersion: jenkins.io/v1alpha2 kind: Jenkins metadata: - name: example-8 - namespace: default + name: example + namespace: default spec: configurationAsCode: configurations: [] @@ -14,7 +15,6 @@ spec: name: "" jenkinsAPISettings: authorizationStrategy: createUser - ValidateSecurityWarnings: true master: disableCSRFProtection: false containers: @@ -48,75 +48,9 @@ spec: requests: cpu: "1" memory: 500Mi - plugins: - - name: mailer - version: "1.19" - - name: script-security - version: "1.18" - - name: google-login - version: "1.2" - - name: google-login - version: "1.2" - - name: google-login - version: "1.2" - - name: google-login - version: "1.2" - - name: google-login - version: "1.2" - - name: google-login - version: "1.2" - - name: google-login - version: "1.2" - - name: google-login - version: "1.2" - - name: google-login - version: "1.2" - - name: google-login - version: "1.2" - - name: google-login - version: "1.2" - - name: google-login - version: "1.2" - - name: google-login - version: "1.2" - - name: google-login - version: "1.2" - - name: google-login - version: "1.2" - - name: google-login - version: "1.2" - - name: google-login - version: "1.2" - - name: google-login - version: "1.2" - - name: google-login - version: "1.2" - - name: google-login - version: "1.2" - - name: google-login - version: "1.2" - - name: google-login - version: "1.2" - - name: credentials - version: "2.1" - - name: ssh-credentials - version: "1.1" - - name: junit - version: "1.2" - - name: matrix-project - version: "1.1" - - name: git-client - version: "2.8.4" - - name: pipeline-model-definition - version: "1.3.0" - - name: favorite - version: "2" - - name: workflow-cps - version: "2" - seedJobs: - id: jenkins-operator targets: "cicd/jobs/*.jenkins" description: "Jenkins Operator repository" repositoryBranch: master - repositoryUrl: https://github.com/jenkinsci/kubernetes-operator.git + repositoryUrl: https://github.com/jenkinsci/kubernetes-operator.git \ No newline at end of file diff --git a/main.go b/main.go index 3cd399f6..936862f1 100644 --- a/main.go +++ b/main.go @@ -78,6 +78,7 @@ func main() { var metricsAddr string var enableLeaderElection bool var probeAddr string + var ValidateSecurityWarnings bool isRunningInCluster, err := resources.IsRunningInCluster() if err != nil { @@ -88,6 +89,7 @@ func main() { flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") flag.BoolVar(&enableLeaderElection, "leader-elect", isRunningInCluster, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") + flag.BoolVar(&ValidateSecurityWarnings, "validate-security-warnings", false, "Enable validation for potential security warnings in jenkins custom resource plugins") hostname := flag.String("jenkins-api-hostname", "", "Hostname or IP of Jenkins API. It can be service name, node IP or localhost.") port := flag.Int("jenkins-api-port", 0, "The port on which Jenkins API is running. Note: If you want to use nodePort don't set this setting and --jenkins-api-use-nodeport must be true.") useNodePort := flag.Bool("jenkins-api-use-nodeport", false, "Connect to Jenkins API using the service nodePort instead of service port. If you want to set this as true - don't set --jenkins-api-port.") @@ -95,9 +97,6 @@ func main() { opts := zap.Options{ Development: true, } - - go v1alpha2.RetrieveDataFile() - opts.BindFlags(flag.CommandLine) flag.Parse() @@ -112,6 +111,15 @@ func main() { } logger.Info(fmt.Sprintf("Watch namespace: %v", namespace)) + if ValidateSecurityWarnings { + ispluginsdatainitialized := make(chan bool) + go v1alpha2.PluginsDataManager.CachePluginData(ispluginsdatainitialized) + + if !<-ispluginsdatainitialized { + fatal(errors.New("Unable to get the plugins data"), *debug) + } + } + // get a config to talk to the API server cfg, err := config.GetConfig() if err != nil { diff --git a/webhook/all_in_one_v1alpha2.yaml b/webhook/all_in_one_v1alpha2.yaml index 0d878dd2..18afb801 100644 --- a/webhook/all_in_one_v1alpha2.yaml +++ b/webhook/all_in_one_v1alpha2.yaml @@ -247,7 +247,7 @@ spec: - /manager args: - --leader-elect - image: jenkins-operator:52fe5fe9-dirty + image: jenkins-operator:37d0eac4-dirty name: jenkins-operator imagePullPolicy: IfNotPresent securityContext: @@ -266,11 +266,11 @@ spec: periodSeconds: 10 resources: limits: - cpu: 100m - memory: 30Mi + cpu: 200m + memory: 200Mi requests: cpu: 100m - memory: 20Mi + memory: 80Mi env: - name: WATCH_NAMESPACE valueFrom: @@ -278,8 +278,7 @@ spec: fieldPath: metadata.namespace volumeMounts: - mountPath: /tmp/k8s-webhook-server/serving-certs - name: cert - readOnly: false + name: cert volumes: - name: cert secret: diff --git a/webhook/operator.yaml b/webhook/operator.yaml index c056ba8e..01093af2 100644 --- a/webhook/operator.yaml +++ b/webhook/operator.yaml @@ -23,6 +23,7 @@ spec: - /manager args: - --leader-elect + - --validate-security-warnings image: {DOCKER_REGISTRY}:{GITCOMMIT} name: jenkins-operator imagePullPolicy: IfNotPresent