kubernetes-operator/api/v1alpha2/jenkins_webhook.go

302 lines
8.4 KiB
Go

/*
Copyright 2021.
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/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,
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.
*/
package v1alpha2
import (
"compress/gzip"
"encoding/json"
"errors"
"io"
"io/ioutil"
"net/http"
"os"
"time"
"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"
)
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
)
func (in *Jenkins) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(in).
Complete()
}
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// 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 {
jenkinslog.Info("validate create", "name", in.Name)
return Validate(*in)
}
return nil
}
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (in *Jenkins) ValidateUpdate(old runtime.Object) error {
if in.Spec.ValidateSecurityWarnings {
jenkinslog.Info("validate update", "name", in.Name)
return Validate(*in)
}
return nil
}
func (in *Jenkins) ValidateDelete() error {
return nil
}
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 {
pluginset := make(map[string]PluginData)
var warningmsg 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 {
if pluginData, ispresent := pluginset[plugin.Name]; ispresent {
jenkinslog.Info("Checking for plugin", "name", plugin.Name)
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
}
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"
}
}
}
}
}
if len(warningmsg) > 0 {
return errors.New(warningmsg)
}
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)
}
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() {
for {
jenkinslog.Info("Retreiving file", "Host Url", hosturl)
err := Download()
if err != nil {
jenkinslog.Info("Retrieving File", "Error while downloading", err)
continue
}
jenkinslog.Info("Retrieve File", "Successfully downloaded", compressedFile)
err = Extract()
if err != nil {
jenkinslog.Info("Retreive File", "Error while extracting", err)
continue
}
jenkinslog.Info("Retreive File", "Successfully extracted", pluginDataFile)
isRetrieved = true
time.Sleep(12 * time.Hour)
}
}
func Download() error {
out, err := os.Create(compressedFile)
if err != nil {
return err
}
defer out.Close()
client := http.Client{
Timeout: 2000 * time.Second,
}
resp, err := client.Get(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)
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(pluginDataFile)
if err != nil {
return err
}
defer writer.Close()
_, err = io.Copy(writer, archive)
return err
}
// returns a semantic version that can be used for comparision
func MakeSemanticVersion(version string) string {
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
}