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 }