github.com/abayer/test-infra@v0.0.5/prow/pod-utils/decorate/podspec.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 package decorate 18 19 import ( 20 "fmt" 21 "path" 22 "sort" 23 24 "k8s.io/api/core/v1" 25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 27 "k8s.io/test-infra/prow/clonerefs" 28 "k8s.io/test-infra/prow/entrypoint" 29 "k8s.io/test-infra/prow/gcsupload" 30 "k8s.io/test-infra/prow/initupload" 31 "k8s.io/test-infra/prow/kube" 32 "k8s.io/test-infra/prow/pod-utils/clone" 33 "k8s.io/test-infra/prow/pod-utils/downwardapi" 34 "k8s.io/test-infra/prow/pod-utils/wrapper" 35 "k8s.io/test-infra/prow/sidecar" 36 ) 37 38 const ( 39 logMountName = "logs" 40 logMountPath = "/logs" 41 artifactsEnv = "ARTIFACTS" 42 artifactsPath = logMountPath + "/artifacts" 43 codeMountName = "code" 44 codeMountPath = "/home/prow/go" 45 gopathEnv = "GOPATH" 46 toolsMountName = "tools" 47 toolsMountPath = "/tools" 48 gcsCredentialsMountName = "gcs-credentials" 49 gcsCredentialsMountPath = "/secrets/gcs" 50 sshKeysMountNamePrefix = "ssh-keys" 51 sshKeysMountPathPrefix = "/secrets/ssh" 52 ) 53 54 // Labels returns a string slice with label consts from kube. 55 func Labels() []string { 56 return []string{kube.ProwJobTypeLabel, kube.CreatedByProw, kube.ProwJobIDLabel} 57 } 58 59 // VolumeMounts returns a string slice with *MountName consts in it. 60 func VolumeMounts() []string { 61 return []string{logMountName, codeMountName, toolsMountName, gcsCredentialsMountName} 62 } 63 64 // VolumeMountPaths returns a string slice with *MountPath consts in it. 65 func VolumeMountPaths() []string { 66 return []string{logMountPath, codeMountPath, toolsMountPath, gcsCredentialsMountPath} 67 } 68 69 // ProwJobToPod converts a ProwJob to a Pod that will run the tests. 70 func ProwJobToPod(pj kube.ProwJob, buildID string) (*v1.Pod, error) { 71 if pj.Spec.PodSpec == nil { 72 return nil, fmt.Errorf("prowjob %q lacks a pod spec", pj.Name) 73 } 74 75 rawEnv, err := downwardapi.EnvForSpec(downwardapi.NewJobSpec(pj.Spec, buildID, pj.Name)) 76 if err != nil { 77 return nil, err 78 } 79 80 spec := pj.Spec.PodSpec.DeepCopy() 81 spec.RestartPolicy = "Never" 82 spec.Containers[0].Name = kube.TestContainerName 83 84 if pj.Spec.DecorationConfig == nil { 85 spec.Containers[0].Env = append(spec.Containers[0].Env, kubeEnv(rawEnv)...) 86 } else { 87 if err := decorate(spec, &pj, rawEnv); err != nil { 88 return nil, fmt.Errorf("error decorating podspec: %v", err) 89 } 90 } 91 92 podLabels := make(map[string]string) 93 for k, v := range pj.ObjectMeta.Labels { 94 podLabels[k] = v 95 } 96 podLabels[kube.CreatedByProw] = "true" 97 podLabels[kube.ProwJobTypeLabel] = string(pj.Spec.Type) 98 podLabels[kube.ProwJobIDLabel] = pj.ObjectMeta.Name 99 return &v1.Pod{ 100 ObjectMeta: metav1.ObjectMeta{ 101 Name: pj.ObjectMeta.Name, 102 Labels: podLabels, 103 Annotations: map[string]string{ 104 kube.ProwJobAnnotation: pj.Spec.Job, 105 }, 106 }, 107 Spec: *spec, 108 }, nil 109 } 110 111 func decorate(spec *kube.PodSpec, pj *kube.ProwJob, rawEnv map[string]string) error { 112 rawEnv[artifactsEnv] = artifactsPath 113 rawEnv[gopathEnv] = codeMountPath 114 logMount := kube.VolumeMount{ 115 Name: logMountName, 116 MountPath: logMountPath, 117 } 118 logVolume := kube.Volume{ 119 Name: logMountName, 120 VolumeSource: kube.VolumeSource{ 121 EmptyDir: &kube.EmptyDirVolumeSource{}, 122 }, 123 } 124 125 codeMount := kube.VolumeMount{ 126 Name: codeMountName, 127 MountPath: codeMountPath, 128 } 129 codeVolume := kube.Volume{ 130 Name: codeMountName, 131 VolumeSource: kube.VolumeSource{ 132 EmptyDir: &kube.EmptyDirVolumeSource{}, 133 }, 134 } 135 136 toolsMount := kube.VolumeMount{ 137 Name: toolsMountName, 138 MountPath: toolsMountPath, 139 } 140 toolsVolume := kube.Volume{ 141 Name: toolsMountName, 142 VolumeSource: kube.VolumeSource{ 143 EmptyDir: &kube.EmptyDirVolumeSource{}, 144 }, 145 } 146 147 gcsCredentialsMount := kube.VolumeMount{ 148 Name: gcsCredentialsMountName, 149 MountPath: gcsCredentialsMountPath, 150 } 151 gcsCredentialsVolume := kube.Volume{ 152 Name: gcsCredentialsMountName, 153 VolumeSource: kube.VolumeSource{ 154 Secret: &kube.SecretSource{ 155 SecretName: pj.Spec.DecorationConfig.GCSCredentialsSecret, 156 }, 157 }, 158 } 159 160 var sshKeysVolumes []kube.Volume 161 var cloneLog string 162 var refs []*kube.Refs 163 if pj.Spec.Refs != nil { 164 refs = append(refs, pj.Spec.Refs) 165 } 166 refs = append(refs, pj.Spec.ExtraRefs...) 167 willCloneRefs := len(refs) > 0 && !pj.Spec.DecorationConfig.SkipCloning 168 if willCloneRefs { 169 var sshKeyMode int32 = 0400 // this is octal, so symbolic ref is `u+r` 170 var sshKeysMounts []kube.VolumeMount 171 var sshKeyPaths []string 172 for _, secret := range pj.Spec.DecorationConfig.SSHKeySecrets { 173 name := fmt.Sprintf("%s-%s", sshKeysMountNamePrefix, secret) 174 keyPath := path.Join(sshKeysMountPathPrefix, secret) 175 sshKeyPaths = append(sshKeyPaths, keyPath) 176 sshKeysMounts = append(sshKeysMounts, kube.VolumeMount{ 177 Name: name, 178 MountPath: keyPath, 179 ReadOnly: true, 180 }) 181 sshKeysVolumes = append(sshKeysVolumes, kube.Volume{ 182 Name: name, 183 VolumeSource: kube.VolumeSource{ 184 Secret: &kube.SecretSource{ 185 SecretName: secret, 186 DefaultMode: &sshKeyMode, 187 }, 188 }, 189 }) 190 } 191 192 cloneLog = fmt.Sprintf("%s/clone.json", logMountPath) 193 cloneConfigEnv, err := clonerefs.Encode(clonerefs.Options{ 194 SrcRoot: codeMountPath, 195 Log: cloneLog, 196 GitUserName: clonerefs.DefaultGitUserName, 197 GitUserEmail: clonerefs.DefaultGitUserEmail, 198 GitRefs: refs, 199 KeyFiles: sshKeyPaths, 200 }) 201 if err != nil { 202 return fmt.Errorf("could not encode clone configuration as JSON: %v", err) 203 } 204 205 spec.InitContainers = append(spec.InitContainers, kube.Container{ 206 Name: "clonerefs", 207 Image: pj.Spec.DecorationConfig.UtilityImages.CloneRefs, 208 Command: []string{"/clonerefs"}, 209 Env: kubeEnv(map[string]string{clonerefs.JSONConfigEnvVar: cloneConfigEnv}), 210 VolumeMounts: append([]kube.VolumeMount{logMount, codeMount}, sshKeysMounts...), 211 }) 212 } 213 gcsOptions := gcsupload.Options{ 214 // TODO: pass the artifact dir here too once we figure that out 215 GCSConfiguration: pj.Spec.DecorationConfig.GCSConfiguration, 216 GcsCredentialsFile: fmt.Sprintf("%s/service-account.json", gcsCredentialsMountPath), 217 DryRun: false, 218 } 219 220 initUploadOptions := initupload.Options{ 221 Options: &gcsOptions, 222 } 223 if willCloneRefs { 224 initUploadOptions.Log = cloneLog 225 } 226 initUploadConfigEnv, err := initupload.Encode(initUploadOptions) 227 if err != nil { 228 return fmt.Errorf("could not encode initupload configuration as JSON: %v", err) 229 } 230 231 entrypointLocation := fmt.Sprintf("%s/entrypoint", toolsMountPath) 232 233 spec.InitContainers = append(spec.InitContainers, 234 kube.Container{ 235 Name: "initupload", 236 Image: pj.Spec.DecorationConfig.UtilityImages.InitUpload, 237 Command: []string{"/initupload"}, 238 Env: kubeEnv(map[string]string{ 239 initupload.JSONConfigEnvVar: initUploadConfigEnv, 240 downwardapi.JobSpecEnv: rawEnv[downwardapi.JobSpecEnv], // TODO: shouldn't need this? 241 }), 242 VolumeMounts: []kube.VolumeMount{logMount, gcsCredentialsMount}, 243 }, 244 kube.Container{ 245 Name: "place-tools", 246 Image: pj.Spec.DecorationConfig.UtilityImages.Entrypoint, 247 Command: []string{"/bin/cp"}, 248 Args: []string{"/entrypoint", entrypointLocation}, 249 VolumeMounts: []kube.VolumeMount{toolsMount}, 250 }, 251 ) 252 253 wrapperOptions := wrapper.Options{ 254 ProcessLog: fmt.Sprintf("%s/process-log.txt", logMountPath), 255 MarkerFile: fmt.Sprintf("%s/marker-file.txt", logMountPath), 256 } 257 entrypointConfigEnv, err := entrypoint.Encode(entrypoint.Options{ 258 Args: append(spec.Containers[0].Command, spec.Containers[0].Args...), 259 Options: &wrapperOptions, 260 Timeout: pj.Spec.DecorationConfig.Timeout, 261 GracePeriod: pj.Spec.DecorationConfig.GracePeriod, 262 ArtifactDir: artifactsPath, 263 }) 264 if err != nil { 265 return fmt.Errorf("could not encode entrypoint configuration as JSON: %v", err) 266 } 267 allEnv := rawEnv 268 allEnv[entrypoint.JSONConfigEnvVar] = entrypointConfigEnv 269 270 spec.Containers[0].Command = []string{entrypointLocation} 271 spec.Containers[0].Args = []string{} 272 spec.Containers[0].Env = append(spec.Containers[0].Env, kubeEnv(allEnv)...) 273 spec.Containers[0].VolumeMounts = append(spec.Containers[0].VolumeMounts, logMount, toolsMount) 274 275 gcsOptions.Items = append(gcsOptions.Items, artifactsPath) 276 sidecarConfigEnv, err := sidecar.Encode(sidecar.Options{ 277 GcsOptions: &gcsOptions, 278 WrapperOptions: &wrapperOptions, 279 }) 280 if err != nil { 281 return fmt.Errorf("could not encode sidecar configuration as JSON: %v", err) 282 } 283 284 spec.Containers = append(spec.Containers, kube.Container{ 285 Name: "sidecar", 286 Image: pj.Spec.DecorationConfig.UtilityImages.Sidecar, 287 Command: []string{"/sidecar"}, 288 Env: kubeEnv(map[string]string{ 289 sidecar.JSONConfigEnvVar: sidecarConfigEnv, 290 downwardapi.JobSpecEnv: rawEnv[downwardapi.JobSpecEnv], // TODO: shouldn't need this? 291 }), 292 VolumeMounts: []kube.VolumeMount{logMount, gcsCredentialsMount}, 293 }) 294 spec.Volumes = append(spec.Volumes, logVolume, toolsVolume, gcsCredentialsVolume) 295 296 if willCloneRefs { 297 spec.Containers[0].WorkingDir = clone.PathForRefs(codeMountPath, refs[0]) 298 spec.Containers[0].VolumeMounts = append(spec.Containers[0].VolumeMounts, codeMount) 299 spec.Volumes = append(spec.Volumes, append(sshKeysVolumes, codeVolume)...) 300 } 301 302 return nil 303 } 304 305 // kubeEnv transforms a mapping of environment variables 306 // into their serialized form for a PodSpec, sorting by 307 // the name of the env vars 308 func kubeEnv(environment map[string]string) []v1.EnvVar { 309 var keys []string 310 for key := range environment { 311 keys = append(keys, key) 312 } 313 sort.Strings(keys) 314 315 var kubeEnvironment []v1.EnvVar 316 for _, key := range keys { 317 kubeEnvironment = append(kubeEnvironment, v1.EnvVar{ 318 Name: key, 319 Value: environment[key], 320 }) 321 } 322 323 return kubeEnvironment 324 }