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 }