Updated Validation logic
- Defined a security manager struct to cache all the plugin data - Added flag to make validating security warnings optional while deploying the operator
This commit is contained in:
parent
37d0eac4e3
commit
1d2651d43f
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
14
main.go
14
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 {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ spec:
|
|||
- /manager
|
||||
args:
|
||||
- --leader-elect
|
||||
- --validate-security-warnings
|
||||
image: {DOCKER_REGISTRY}:{GITCOMMIT}
|
||||
name: jenkins-operator
|
||||
imagePullPolicy: IfNotPresent
|
||||
|
|
|
|||
Loading…
Reference in New Issue