github.com/yrj2011/jx-test-infra@v0.0.0-20190529031832-7a2065ee98eb/config/jobs/kubernetes-security/genjobs.go (about)

     1  /*
     2  Copyright 2018 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  /*
    18  genjobs automatically generates the security repo presubmits from the
    19  kubernetes presubmits
    20  
    21  NOTE: this makes a few assumptions
    22  - $PWD/../../prow/config.yaml is where the config lives (unless you supply --config=)
    23  - $PWD/.. is where the job configs live (unless you supply --jobs=)
    24  - the output is job configs ($PWD/..) + /kubernetes-security/generated-security-jobs.yaml (unless you supply --output)
    25  */
    26  package main
    27  
    28  import (
    29  	"bytes"
    30  	"encoding/json"
    31  	"fmt"
    32  	"io"
    33  	"io/ioutil"
    34  	"log"
    35  	"os"
    36  	"path/filepath"
    37  	"regexp"
    38  	"strings"
    39  
    40  	"github.com/ghodss/yaml"
    41  	flag "github.com/spf13/pflag"
    42  
    43  	"k8s.io/api/core/v1"
    44  	"k8s.io/apimachinery/pkg/util/sets"
    45  
    46  	"k8s.io/test-infra/prow/config"
    47  	"k8s.io/test-infra/prow/kube"
    48  )
    49  
    50  var configPath = flag.String("config", "", "path to prow/config.yaml, defaults to $PWD/../../prow/config.yaml")
    51  var jobsPath = flag.String("jobs", "", "path to prowjobs, defaults to $PWD/../")
    52  var configJSONPath = flag.String("config-json", "", "path to jobs/config.json, defaults to $PWD/../../jobs/config.json")
    53  var outputPath = flag.String("output", "", "path to output the generated jobs to, defaults to $PWD/generated-security-jobs.json")
    54  
    55  // config.json is the worst but contains useful information :-(
    56  type configJSON map[string]map[string]interface{}
    57  
    58  func (c configJSON) ScenarioForJob(jobName string) string {
    59  	if scenario, ok := c[jobName]["scenario"]; ok {
    60  		return scenario.(string)
    61  	}
    62  	return ""
    63  }
    64  
    65  func (c configJSON) ArgsForJob(jobName string) []string {
    66  	res := []string{}
    67  	if args, ok := c[jobName]["args"]; ok {
    68  		for _, arg := range args.([]interface{}) {
    69  			res = append(res, arg.(string))
    70  		}
    71  	}
    72  	return res
    73  }
    74  
    75  func readConfigJSON(path string) (config configJSON, err error) {
    76  	raw, err := ioutil.ReadFile(path)
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  	config = configJSON{}
    81  	err = json.Unmarshal(raw, &config)
    82  	if err != nil {
    83  		return nil, err
    84  	}
    85  	return config, nil
    86  }
    87  
    88  // remove merged presets from a podspec
    89  func undoPreset(preset *config.Preset, labels map[string]string, pod *v1.PodSpec) {
    90  	// skip presets that do not match the job labels
    91  	for l, v := range preset.Labels {
    92  		if v2, ok := labels[l]; !ok || v2 != v {
    93  			return
    94  		}
    95  	}
    96  
    97  	// collect up preset created keys
    98  	removeEnvNames := sets.NewString()
    99  	for _, e1 := range preset.Env {
   100  		removeEnvNames.Insert(e1.Name)
   101  	}
   102  	removeVolumeNames := sets.NewString()
   103  	for _, volume := range preset.Volumes {
   104  		removeVolumeNames.Insert(volume.Name)
   105  	}
   106  	removeVolumeMountNames := sets.NewString()
   107  	for _, volumeMount := range preset.VolumeMounts {
   108  		removeVolumeMountNames.Insert(volumeMount.Name)
   109  	}
   110  
   111  	// remove volumes from spec
   112  	filteredVolumes := []v1.Volume{}
   113  	for _, volume := range pod.Volumes {
   114  		if !removeVolumeNames.Has(volume.Name) {
   115  			filteredVolumes = append(filteredVolumes, volume)
   116  		}
   117  	}
   118  	pod.Volumes = filteredVolumes
   119  
   120  	// remove env and volume mounts from containers
   121  	for i := range pod.Containers {
   122  		filteredEnv := []v1.EnvVar{}
   123  		for _, env := range pod.Containers[i].Env {
   124  			if !removeEnvNames.Has(env.Name) {
   125  				filteredEnv = append(filteredEnv, env)
   126  			}
   127  		}
   128  		pod.Containers[i].Env = filteredEnv
   129  
   130  		filteredVolumeMounts := []v1.VolumeMount{}
   131  		for _, mount := range pod.Containers[i].VolumeMounts {
   132  			if !removeVolumeMountNames.Has(mount.Name) {
   133  				filteredVolumeMounts = append(filteredVolumeMounts, mount)
   134  			}
   135  		}
   136  		pod.Containers[i].VolumeMounts = filteredVolumeMounts
   137  	}
   138  }
   139  
   140  // undo merged presets from loaded presubmit and its children
   141  func undoPresubmitPresets(presets []config.Preset, presubmit *config.Presubmit) {
   142  	if presubmit.Spec == nil {
   143  		return
   144  	}
   145  	for _, preset := range presets {
   146  		undoPreset(&preset, presubmit.Labels, presubmit.Spec)
   147  	}
   148  	// do the same for any run after success children
   149  	for i := range presubmit.RunAfterSuccess {
   150  		undoPresubmitPresets(presets, &presubmit.RunAfterSuccess[i])
   151  	}
   152  }
   153  
   154  // convert a kubernetes/kubernetes job to a kubernetes-security/kubernetes job
   155  // dropLabels should be a set of "k: v" strings
   156  // xref: prow/config/config_test.go replace(...)
   157  func convertJobToSecurityJob(j *config.Presubmit, dropLabels sets.String, jobsConfig configJSON) {
   158  	// filter out the unwanted labels
   159  	if len(j.Labels) > 0 {
   160  		filteredLabels := make(map[string]string)
   161  		for k, v := range j.Labels {
   162  			if !dropLabels.Has(fmt.Sprintf("%s: %s", k, v)) {
   163  				filteredLabels[k] = v
   164  			}
   165  		}
   166  		j.Labels = filteredLabels
   167  	}
   168  
   169  	originalName := j.Name
   170  
   171  	// fix name and triggers for all jobs
   172  	j.Name = strings.Replace(originalName, "pull-kubernetes", "pull-security-kubernetes", -1)
   173  	j.RerunCommand = strings.Replace(j.RerunCommand, "pull-kubernetes", "pull-security-kubernetes", -1)
   174  	j.Trigger = strings.Replace(j.Trigger, "pull-kubernetes", "pull-security-kubernetes", -1)
   175  	j.Context = strings.Replace(j.Context, "pull-kubernetes", "pull-security-kubernetes", -1)
   176  
   177  	// handle k8s job args, volumes etc
   178  	if j.Agent == "kubernetes" {
   179  		j.Cluster = "security"
   180  		container := &j.Spec.Containers[0]
   181  		// check for args that need hijacking
   182  		endsWithScenarioArgs := false
   183  		needGCSFlag := false
   184  		needGCSSharedFlag := false
   185  		needStagingFlag := false
   186  		for i, arg := range container.Args {
   187  			if arg == "--" {
   188  				endsWithScenarioArgs = true
   189  
   190  				// handle --repo substitution for main repo
   191  			} else if strings.HasPrefix(arg, "--repo=k8s.io/kubernetes") || strings.HasPrefix(arg, "--repo=k8s.io/$(REPO_NAME)") {
   192  				container.Args[i] = strings.Replace(arg, "k8s.io/", "github.com/kubernetes-security/", 1)
   193  
   194  				// handle upload bucket
   195  			} else if strings.HasPrefix(arg, "--upload=") {
   196  				container.Args[i] = "--upload=gs://kubernetes-security-prow/pr-logs"
   197  				// check if we need to change staging artifact location for bazel-build and e2es
   198  			} else if strings.HasPrefix(arg, "--release") {
   199  				needGCSFlag = true
   200  				needGCSSharedFlag = true
   201  			} else if strings.HasPrefix(arg, "--stage") {
   202  				needStagingFlag = true
   203  			} else if strings.HasPrefix(arg, "--use-shared-build") {
   204  				needGCSSharedFlag = true
   205  			}
   206  		}
   207  		// NOTE: this needs to be before the bare -- and then bootstrap args so we prepend it
   208  		container.Args = append([]string{"--ssh=/etc/ssh-security/ssh-security"}, container.Args...)
   209  
   210  		// check for scenario specific tweaks
   211  		// NOTE: jobs are remapped to their original name in bootstrap to de-dupe config
   212  
   213  		// check if we need to change staging artifact location for bazel-build and e2es
   214  		if jobsConfig.ScenarioForJob(originalName) == "kubernetes_bazel" {
   215  			for _, arg := range jobsConfig.ArgsForJob(originalName) {
   216  				if strings.HasPrefix(arg, "--release") {
   217  					needGCSFlag = true
   218  					needGCSSharedFlag = true
   219  					break
   220  				}
   221  			}
   222  		}
   223  
   224  		if jobsConfig.ScenarioForJob(originalName) == "kubernetes_e2e" {
   225  			for _, arg := range jobsConfig.ArgsForJob(originalName) {
   226  				if strings.HasPrefix(arg, "--stage") {
   227  					needStagingFlag = true
   228  				} else if strings.HasPrefix(arg, "--use-shared-build") {
   229  					needGCSSharedFlag = true
   230  				}
   231  			}
   232  		}
   233  
   234  		// NOTE: these needs to be at the end and after a -- if there is none (it's a scenario arg)
   235  		if !endsWithScenarioArgs && (needGCSFlag || needGCSSharedFlag || needStagingFlag) {
   236  			container.Args = append(container.Args, "--")
   237  		}
   238  		if needGCSFlag {
   239  			container.Args = append(container.Args, "--gcs=gs://kubernetes-security-prow/ci/"+j.Name)
   240  		}
   241  		if needGCSSharedFlag {
   242  			container.Args = append(container.Args, "--gcs-shared=gs://kubernetes-security-prow/bazel")
   243  		}
   244  		if needStagingFlag {
   245  			container.Args = append(container.Args, "--stage=gs://kubernetes-security-prow/ci/"+j.Name)
   246  		}
   247  
   248  		// add ssh key volume / mount
   249  		container.VolumeMounts = append(
   250  			container.VolumeMounts,
   251  			kube.VolumeMount{
   252  				Name:      "ssh-security",
   253  				MountPath: "/etc/ssh-security",
   254  			},
   255  		)
   256  		defaultMode := int32(0400)
   257  		j.Spec.Volumes = append(
   258  			j.Spec.Volumes,
   259  			kube.Volume{
   260  				Name: "ssh-security",
   261  				VolumeSource: kube.VolumeSource{
   262  					Secret: &kube.SecretSource{
   263  						SecretName:  "ssh-security",
   264  						DefaultMode: &defaultMode,
   265  					},
   266  				},
   267  			},
   268  		)
   269  	}
   270  	// done with this job, check for run_after_success
   271  	for i := range j.RunAfterSuccess {
   272  		convertJobToSecurityJob(&j.RunAfterSuccess[i], dropLabels, jobsConfig)
   273  	}
   274  }
   275  
   276  // these are unnecessary, and make the config larger so we strip them out
   277  func yamlBytesStripNulls(yamlBytes []byte) []byte {
   278  	nullRE := regexp.MustCompile("(?m)[\n]+^[^\n]+: null$")
   279  	return nullRE.ReplaceAll(yamlBytes, []byte{})
   280  }
   281  
   282  func yamlBytesToEntry(yamlBytes []byte, indent int) []byte {
   283  	var buff bytes.Buffer
   284  	// spaces of length indent
   285  	prefix := bytes.Repeat([]byte{32}, indent)
   286  	// `- ` before the first field of a yaml entry
   287  	prefix[len(prefix)-2] = byte(45)
   288  	buff.Write(prefix)
   289  	// put back space
   290  	prefix[len(prefix)-2] = byte(32)
   291  	for i, b := range yamlBytes {
   292  		buff.WriteByte(b)
   293  		// indent after newline, except the last one
   294  		if b == byte(10) && i+1 != len(yamlBytes) {
   295  			buff.Write(prefix)
   296  		}
   297  	}
   298  	return buff.Bytes()
   299  }
   300  
   301  func copyFile(srcPath, destPath string) error {
   302  	// fallback to copying the file instead
   303  	src, err := os.Open(srcPath)
   304  	if err != nil {
   305  		return err
   306  	}
   307  	dst, err := os.OpenFile(destPath, os.O_WRONLY, 0666)
   308  	if err != nil {
   309  		return err
   310  	}
   311  	_, err = io.Copy(dst, src)
   312  	if err != nil {
   313  		return err
   314  	}
   315  	dst.Sync()
   316  	dst.Close()
   317  	src.Close()
   318  	return nil
   319  }
   320  
   321  func main() {
   322  	flag.Parse()
   323  	// default to $PWD/prow/config.yaml
   324  	pwd, err := os.Getwd()
   325  	if err != nil {
   326  		log.Fatalf("Failed to get $PWD: %v", err)
   327  	}
   328  	if *configPath == "" {
   329  		*configPath = pwd + "/../../prow/config.yaml"
   330  	}
   331  	if *jobsPath == "" {
   332  		*jobsPath = pwd + "/../"
   333  	}
   334  	if *configJSONPath == "" {
   335  		*configJSONPath = pwd + "/../../jobs/config.json"
   336  	}
   337  	if *outputPath == "" {
   338  		*outputPath = pwd + "/generated-security-jobs.yaml"
   339  	}
   340  	// read in current prow config
   341  	parsed, err := config.Load(*configPath, *jobsPath)
   342  	if err != nil {
   343  		log.Fatalf("Failed to read config file: %v", err)
   344  	}
   345  	// read in jobs config
   346  	jobsConfig, err := readConfigJSON(*configJSONPath)
   347  
   348  	// create temp file to write updated config
   349  	f, err := ioutil.TempFile(filepath.Dir(*configPath), "temp")
   350  	if err != nil {
   351  		log.Fatalf("Failed to create temp file: %v", err)
   352  	}
   353  	defer os.Remove(f.Name())
   354  
   355  	// write the header
   356  	io.WriteString(f, "# Autogenerated by genjobs.go, do NOT edit!\n")
   357  	io.WriteString(f, "# see genjobs.go, which you can run with hack/update-config.sh\n")
   358  	io.WriteString(f, "presubmits:\n  kubernetes-security/kubernetes:\n")
   359  
   360  	// this is the set of preset labels we want to remove
   361  	// we remove the bazel remote cache because we do not deploy one to this build cluster
   362  	dropLabels := sets.NewString("preset-bazel-remote-cache-enabled: true")
   363  
   364  	// convert each kubernetes/kubernetes presubmit to a
   365  	// kubernetes-security/kubernetes presubmit and write to the file
   366  	for i := range parsed.Presubmits["kubernetes/kubernetes"] {
   367  		job := &parsed.Presubmits["kubernetes/kubernetes"][i]
   368  		// undo merged presets, this needs to occur first!
   369  		undoPresubmitPresets(parsed.Presets, job)
   370  		// now convert the job
   371  		convertJobToSecurityJob(job, dropLabels, jobsConfig)
   372  		jobBytes, err := yaml.Marshal(job)
   373  		if err != nil {
   374  			log.Fatalf("Failed to marshal job: %v", err)
   375  		}
   376  		// write, properly indented, and stripped of `foo: null`
   377  		jobBytes = yamlBytesStripNulls(jobBytes)
   378  		f.Write(yamlBytesToEntry(jobBytes, 4))
   379  	}
   380  	f.Sync()
   381  
   382  	// move file to replace original
   383  	f.Close()
   384  	err = os.Rename(f.Name(), *outputPath)
   385  	if err != nil {
   386  		// fallback to copying the file instead
   387  		err = copyFile(f.Name(), *outputPath)
   388  		if err != nil {
   389  			log.Fatalf("Failed to replace config with updated version: %v", err)
   390  		}
   391  	}
   392  }