github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/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  	"fmt"
    31  	"io"
    32  	"io/ioutil"
    33  	"log"
    34  	"os"
    35  	"path/filepath"
    36  	"reflect"
    37  	"regexp"
    38  	"strings"
    39  
    40  	flag "github.com/spf13/pflag"
    41  	"sigs.k8s.io/yaml"
    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 outputPath = flag.String("output", "", "path to output the generated jobs to, defaults to $PWD/generated-security-jobs.yaml")
    53  
    54  // remove merged presets from a podspec
    55  func undoPreset(preset *config.Preset, labels map[string]string, pod *v1.PodSpec) {
    56  	// skip presets that do not match the job labels
    57  	for l, v := range preset.Labels {
    58  		if v2, ok := labels[l]; !ok || v2 != v {
    59  			return
    60  		}
    61  	}
    62  
    63  	// collect up preset created keys
    64  	removeEnvNames := sets.NewString()
    65  	for _, e1 := range preset.Env {
    66  		removeEnvNames.Insert(e1.Name)
    67  	}
    68  	removeVolumeNames := sets.NewString()
    69  	for _, volume := range preset.Volumes {
    70  		removeVolumeNames.Insert(volume.Name)
    71  	}
    72  	removeVolumeMountNames := sets.NewString()
    73  	for _, volumeMount := range preset.VolumeMounts {
    74  		removeVolumeMountNames.Insert(volumeMount.Name)
    75  	}
    76  
    77  	// remove volumes from spec
    78  	filteredVolumes := []v1.Volume{}
    79  	for _, volume := range pod.Volumes {
    80  		if !removeVolumeNames.Has(volume.Name) {
    81  			filteredVolumes = append(filteredVolumes, volume)
    82  		}
    83  	}
    84  	pod.Volumes = filteredVolumes
    85  
    86  	// remove env and volume mounts from containers
    87  	for i := range pod.Containers {
    88  		filteredEnv := []v1.EnvVar{}
    89  		for _, env := range pod.Containers[i].Env {
    90  			if !removeEnvNames.Has(env.Name) {
    91  				filteredEnv = append(filteredEnv, env)
    92  			}
    93  		}
    94  		pod.Containers[i].Env = filteredEnv
    95  
    96  		filteredVolumeMounts := []v1.VolumeMount{}
    97  		for _, mount := range pod.Containers[i].VolumeMounts {
    98  			if !removeVolumeMountNames.Has(mount.Name) {
    99  				filteredVolumeMounts = append(filteredVolumeMounts, mount)
   100  			}
   101  		}
   102  		pod.Containers[i].VolumeMounts = filteredVolumeMounts
   103  	}
   104  }
   105  
   106  // undo merged presets from loaded presubmit and its children
   107  func undoPresubmitPresets(presets []config.Preset, presubmit *config.Presubmit) {
   108  	if presubmit.Spec == nil {
   109  		return
   110  	}
   111  	for _, preset := range presets {
   112  		undoPreset(&preset, presubmit.Labels, presubmit.Spec)
   113  	}
   114  	// do the same for any run after success children
   115  	for i := range presubmit.RunAfterSuccess {
   116  		undoPresubmitPresets(presets, &presubmit.RunAfterSuccess[i])
   117  	}
   118  }
   119  
   120  // convert a kubernetes/kubernetes job to a kubernetes-security/kubernetes job
   121  // dropLabels should be a set of "k: v" strings
   122  // xref: prow/config/config_test.go replace(...)
   123  // it will return the same job mutated, or nil if the job should be removed
   124  func convertJobToSecurityJob(j *config.Presubmit, dropLabels sets.String, defaultDecoration *kube.DecorationConfig, podNamespace string) *config.Presubmit {
   125  	// if a GKE job, disable it
   126  	if strings.Contains(j.Name, "gke") {
   127  		return nil
   128  	}
   129  
   130  	// filter out the unwanted labels
   131  	if len(j.Labels) > 0 {
   132  		filteredLabels := make(map[string]string)
   133  		for k, v := range j.Labels {
   134  			if !dropLabels.Has(fmt.Sprintf("%s: %s", k, v)) {
   135  				filteredLabels[k] = v
   136  			}
   137  		}
   138  		j.Labels = filteredLabels
   139  	}
   140  
   141  	originalName := j.Name
   142  
   143  	// fix name and triggers for all jobs
   144  	j.Name = strings.Replace(originalName, "pull-kubernetes", "pull-security-kubernetes", -1)
   145  	j.RerunCommand = strings.Replace(j.RerunCommand, "pull-kubernetes", "pull-security-kubernetes", -1)
   146  	j.Trigger = strings.Replace(j.Trigger, "pull-kubernetes", "pull-security-kubernetes", -1)
   147  	j.Context = strings.Replace(j.Context, "pull-kubernetes", "pull-security-kubernetes", -1)
   148  	if j.Namespace != nil && *j.Namespace == podNamespace {
   149  		j.Namespace = nil
   150  	}
   151  	if j.DecorationConfig != nil && reflect.DeepEqual(j.DecorationConfig, defaultDecoration) {
   152  		j.DecorationConfig = nil
   153  	}
   154  
   155  	// handle k8s job args, volumes etc
   156  	if j.Agent == "kubernetes" {
   157  		j.Cluster = "security"
   158  		container := &j.Spec.Containers[0]
   159  		// check for args that need hijacking
   160  		endsWithScenarioArgs := false
   161  		needGCSFlag := false
   162  		needGCSSharedFlag := false
   163  		needStagingFlag := false
   164  		isGCPe2e := false
   165  		for i, arg := range container.Args {
   166  			if arg == "--" {
   167  				endsWithScenarioArgs = true
   168  
   169  				// handle --repo substitution for main repo
   170  			} else if arg == "--repo=k8s.io/kubernetes" || strings.HasPrefix(arg, "--repo=k8s.io/kubernetes=") || arg == "--repo=k8s.io/$(REPO_NAME)" || strings.HasPrefix(arg, "--repo=k8s.io/$(REPO_NAME)=") {
   171  				container.Args[i] = strings.Replace(arg, "k8s.io/", "github.com/kubernetes-security/", 1)
   172  
   173  				// handle upload bucket
   174  			} else if strings.HasPrefix(arg, "--upload=") {
   175  				container.Args[i] = "--upload=gs://kubernetes-security-prow/pr-logs"
   176  				// check if we need to change staging artifact location for bazel-build and e2es
   177  			} else if strings.HasPrefix(arg, "--release") {
   178  				needGCSFlag = true
   179  				needGCSSharedFlag = true
   180  			} else if strings.HasPrefix(arg, "--stage") {
   181  				needStagingFlag = true
   182  			} else if strings.HasPrefix(arg, "--use-shared-build") {
   183  				needGCSSharedFlag = true
   184  			}
   185  		}
   186  		// NOTE: this needs to be before the bare -- and then bootstrap args so we prepend it
   187  		container.Args = append([]string{"--ssh=/etc/ssh-security/ssh-security"}, container.Args...)
   188  
   189  		// check for scenario specific tweaks
   190  		// NOTE: jobs are remapped to their original name in bootstrap to de-dupe config
   191  
   192  		scenario := ""
   193  		for _, arg := range container.Args {
   194  			if strings.HasPrefix(arg, "--scenario=") {
   195  				scenario = strings.TrimPrefix(arg, "--scenario=")
   196  			}
   197  		}
   198  		// check if we need to change staging artifact location for bazel-build and e2es
   199  		if scenario == "kubernetes_bazel" {
   200  			for _, arg := range container.Args {
   201  				if strings.HasPrefix(arg, "--release") {
   202  					needGCSFlag = true
   203  					needGCSSharedFlag = true
   204  					break
   205  				}
   206  			}
   207  		}
   208  
   209  		if scenario == "kubernetes_e2e" {
   210  			for _, arg := range container.Args {
   211  				if strings.Contains(arg, "gcp") {
   212  					isGCPe2e = true
   213  				}
   214  				if strings.HasPrefix(arg, "--stage") {
   215  					needStagingFlag = true
   216  				} else if strings.HasPrefix(arg, "--use-shared-build") {
   217  					needGCSSharedFlag = true
   218  				}
   219  			}
   220  		}
   221  
   222  		// NOTE: these needs to be at the end and after a -- if there is none (it's a scenario arg)
   223  		if !endsWithScenarioArgs && (needGCSFlag || needGCSSharedFlag || needStagingFlag) {
   224  			container.Args = append(container.Args, "--")
   225  		}
   226  		if needGCSFlag {
   227  			container.Args = append(container.Args, "--gcs=gs://kubernetes-security-prow/ci/"+j.Name)
   228  		}
   229  		if needGCSSharedFlag {
   230  			container.Args = append(container.Args, "--gcs-shared=gs://kubernetes-security-prow/bazel")
   231  		}
   232  		if needStagingFlag {
   233  			container.Args = append(container.Args, "--stage=gs://kubernetes-security-prow/ci/"+j.Name)
   234  		}
   235  		// GCP e2e use a fixed project for security testing
   236  		if isGCPe2e {
   237  			container.Args = append(container.Args, "--gcp-project=k8s-jkns-pr-gce-etcd3")
   238  		}
   239  
   240  		// add ssh key volume / mount
   241  		container.VolumeMounts = append(
   242  			container.VolumeMounts,
   243  			kube.VolumeMount{
   244  				Name:      "ssh-security",
   245  				MountPath: "/etc/ssh-security",
   246  			},
   247  		)
   248  		defaultMode := int32(0400)
   249  		j.Spec.Volumes = append(
   250  			j.Spec.Volumes,
   251  			kube.Volume{
   252  				Name: "ssh-security",
   253  				VolumeSource: kube.VolumeSource{
   254  					Secret: &kube.SecretSource{
   255  						SecretName:  "ssh-security",
   256  						DefaultMode: &defaultMode,
   257  					},
   258  				},
   259  			},
   260  		)
   261  	}
   262  	// done with this job, check for run_after_success
   263  	if len(j.RunAfterSuccess) > 0 {
   264  		filteredRunAfterSucces := []config.Presubmit{}
   265  		for i := range j.RunAfterSuccess {
   266  			newJob := convertJobToSecurityJob(&j.RunAfterSuccess[i], dropLabels, defaultDecoration, podNamespace)
   267  			if newJob != nil {
   268  				filteredRunAfterSucces = append(filteredRunAfterSucces, *newJob)
   269  			}
   270  		}
   271  		j.RunAfterSuccess = filteredRunAfterSucces
   272  	}
   273  	return j
   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 *outputPath == "" {
   335  		*outputPath = pwd + "/generated-security-jobs.yaml"
   336  	}
   337  	// read in current prow config
   338  	parsed, err := config.Load(*configPath, *jobsPath)
   339  	if err != nil {
   340  		log.Fatalf("Failed to read config file: %v", err)
   341  	}
   342  
   343  	// create temp file to write updated config
   344  	f, err := ioutil.TempFile(filepath.Dir(*configPath), "temp")
   345  	if err != nil {
   346  		log.Fatalf("Failed to create temp file: %v", err)
   347  	}
   348  	defer os.Remove(f.Name())
   349  
   350  	// write the header
   351  	io.WriteString(f, "# Autogenerated by genjobs.go, do NOT edit!\n")
   352  	io.WriteString(f, "# see genjobs.go, which you can run with hack/update-config.sh\n")
   353  	io.WriteString(f, "presubmits:\n  kubernetes-security/kubernetes:\n")
   354  
   355  	// this is the set of preset labels we want to remove
   356  	// we remove the bazel remote cache because we do not deploy one to this build cluster
   357  	dropLabels := sets.NewString("preset-bazel-remote-cache-enabled: true")
   358  
   359  	// convert each kubernetes/kubernetes presubmit to a
   360  	// kubernetes-security/kubernetes presubmit and write to the file
   361  	for i := range parsed.Presubmits["kubernetes/kubernetes"] {
   362  		job := &parsed.Presubmits["kubernetes/kubernetes"][i]
   363  		// undo merged presets, this needs to occur first!
   364  		undoPresubmitPresets(parsed.Presets, job)
   365  		// now convert the job
   366  		job = convertJobToSecurityJob(job, dropLabels, parsed.Plank.DefaultDecorationConfig, parsed.PodNamespace)
   367  		if job == nil {
   368  			continue
   369  		}
   370  		jobBytes, err := yaml.Marshal(job)
   371  		if err != nil {
   372  			log.Fatalf("Failed to marshal job: %v", err)
   373  		}
   374  		// write, properly indented, and stripped of `foo: null`
   375  		jobBytes = yamlBytesStripNulls(jobBytes)
   376  		f.Write(yamlBytesToEntry(jobBytes, 4))
   377  	}
   378  	f.Sync()
   379  
   380  	// move file to replace original
   381  	f.Close()
   382  	err = os.Rename(f.Name(), *outputPath)
   383  	if err != nil {
   384  		// fallback to copying the file instead
   385  		err = copyFile(f.Name(), *outputPath)
   386  		if err != nil {
   387  			log.Fatalf("Failed to replace config with updated version: %v", err)
   388  		}
   389  	}
   390  }