github.com/grahambrereton-form3/tilt@v0.10.18/internal/testutils/podbuilder/podbuilder.go (about) 1 package podbuilder 2 3 import ( 4 "fmt" 5 "testing" 6 "time" 7 8 "github.com/docker/distribution/reference" 9 appsv1 "k8s.io/api/apps/v1" 10 v1 "k8s.io/api/core/v1" 11 "k8s.io/apimachinery/pkg/api/validation" 12 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 "k8s.io/apimachinery/pkg/types" 14 15 "github.com/windmilleng/tilt/internal/container" 16 "github.com/windmilleng/tilt/internal/k8s" 17 "github.com/windmilleng/tilt/pkg/model" 18 ) 19 20 const fakeContainerID = container.ID("myTestContainer") 21 22 func FakeContainerID() container.ID { 23 return FakeContainerIDAtIndex(0) 24 } 25 26 func FakeContainerIDAtIndex(index int) container.ID { 27 indexSuffix := "" 28 if index != 0 { 29 indexSuffix = fmt.Sprintf("-%d", index) 30 } 31 return container.ID(fmt.Sprintf("%s%s", fakeContainerID, indexSuffix)) 32 } 33 34 func FakeContainerIDSet(size int) map[container.ID]bool { 35 result := container.NewIDSet() 36 for i := 0; i < size; i++ { 37 result[FakeContainerIDAtIndex(i)] = true 38 } 39 return result 40 } 41 42 // Builds Pod objects for testing 43 // 44 // The pod model should be internally well-formed (e.g., the containers 45 // in the PodSpec object should match the containers in the PodStatus object). 46 // 47 // The pod model should also be consistent with the Manifest (e.g., if the Manifest 48 // specifies a Deployment with labels in a PodTemplateSpec, then any Pods should also 49 // have those labels). 50 // 51 // The PodBuilder is responsible for making sure we create well-formed Pods for 52 // testing. Tests should never modify the pod directly, but instead use the PodBuilder 53 // methods to ensure that the pod is consistent. 54 type PodBuilder struct { 55 t testing.TB 56 manifest model.Manifest 57 58 podID string 59 phase string 60 creationTime time.Time 61 deletionTime time.Time 62 restartCount int 63 extraPodLabels map[string]string 64 deploymentUID types.UID 65 66 // keyed by container index -- i.e. the first container will have image: imageRefs[0] and ID: cIDs[0], etc. 67 // If there's no entry at index i, we'll use a dummy value. 68 imageRefs map[int]string 69 cIDs map[int]string 70 cReady map[int]bool 71 } 72 73 func New(t testing.TB, manifest model.Manifest) PodBuilder { 74 return PodBuilder{ 75 t: t, 76 manifest: manifest, 77 imageRefs: make(map[int]string), 78 cIDs: make(map[int]string), 79 cReady: make(map[int]bool), 80 extraPodLabels: make(map[string]string), 81 } 82 } 83 84 func (b PodBuilder) WithPodLabel(key, val string) PodBuilder { 85 b.extraPodLabels[key] = val 86 return b 87 } 88 89 func (b PodBuilder) ManifestName() model.ManifestName { 90 return b.manifest.Name 91 } 92 93 func (b PodBuilder) RestartCount() int { 94 return b.restartCount 95 } 96 97 func (b PodBuilder) WithRestartCount(restartCount int) PodBuilder { 98 b.restartCount = restartCount 99 return b 100 } 101 102 func (b PodBuilder) WithPodID(podID string) PodBuilder { 103 msgs := validation.NameIsDNSSubdomain(podID, false) 104 if len(msgs) != 0 { 105 b.t.Fatalf("pod id %q is invalid: %s", podID, msgs) 106 } 107 b.podID = podID 108 return b 109 } 110 111 func (b PodBuilder) WithPhase(phase string) PodBuilder { 112 b.phase = phase 113 return b 114 } 115 116 func (b PodBuilder) WithImage(image string) PodBuilder { 117 return b.WithImageAtIndex(image, 0) 118 } 119 120 func (b PodBuilder) WithImageAtIndex(image string, index int) PodBuilder { 121 b.imageRefs[index] = image 122 return b 123 } 124 125 func (b PodBuilder) WithContainerID(cID container.ID) PodBuilder { 126 return b.WithContainerIDAtIndex(cID, 0) 127 } 128 129 func (b PodBuilder) WithContainerIDAtIndex(cID container.ID, index int) PodBuilder { 130 if cID == "" { 131 b.cIDs[index] = "" 132 } else { 133 b.cIDs[index] = fmt.Sprintf("%s%s", k8s.ContainerIDPrefix, cID) 134 } 135 return b 136 } 137 138 func (b PodBuilder) WithContainerReady(ready bool) PodBuilder { 139 return b.WithContainerReadyAtIndex(ready, 0) 140 } 141 142 func (b PodBuilder) WithContainerReadyAtIndex(ready bool, index int) PodBuilder { 143 b.cReady[index] = ready 144 return b 145 } 146 147 func (b PodBuilder) WithCreationTime(creationTime time.Time) PodBuilder { 148 b.creationTime = creationTime 149 return b 150 } 151 152 func (b PodBuilder) WithDeletionTime(deletionTime time.Time) PodBuilder { 153 b.deletionTime = deletionTime 154 return b 155 } 156 157 func (b PodBuilder) PodID() string { 158 if b.podID != "" { 159 return b.podID 160 } 161 return "fakePodID" 162 } 163 164 func (b PodBuilder) buildPodUID() types.UID { 165 return types.UID(fmt.Sprintf("%s-fakeUID", b.PodID())) 166 } 167 168 func (b PodBuilder) WithDeploymentUID(deploymentUID types.UID) PodBuilder { 169 b.deploymentUID = deploymentUID 170 return b 171 } 172 173 func (b PodBuilder) buildReplicaSetName() string { 174 return fmt.Sprintf("%s-replicaset", b.manifest.Name) 175 } 176 177 func (b PodBuilder) buildReplicaSetUID() types.UID { 178 return types.UID(fmt.Sprintf("%s-fakeUID", b.buildReplicaSetName())) 179 } 180 181 func (b PodBuilder) buildDeploymentName() string { 182 return b.manifest.Name.String() 183 } 184 185 func (b PodBuilder) DeploymentUID() types.UID { 186 if b.deploymentUID != "" { 187 return b.deploymentUID 188 } 189 return types.UID(fmt.Sprintf("%s-fakeUID", b.buildDeploymentName())) 190 } 191 192 func (b PodBuilder) buildDeployment() *appsv1.Deployment { 193 return &appsv1.Deployment{ 194 ObjectMeta: metav1.ObjectMeta{ 195 Name: b.buildDeploymentName(), 196 Labels: k8s.NewTiltLabelMap(), 197 UID: b.DeploymentUID(), 198 }, 199 } 200 } 201 202 func (b PodBuilder) buildReplicaSet() *appsv1.ReplicaSet { 203 dep := b.buildDeployment() 204 return &appsv1.ReplicaSet{ 205 ObjectMeta: metav1.ObjectMeta{ 206 Name: b.buildReplicaSetName(), 207 UID: b.buildReplicaSetUID(), 208 Labels: k8s.NewTiltLabelMap(), 209 OwnerReferences: []metav1.OwnerReference{ 210 k8s.RuntimeObjToOwnerRef(dep), 211 }, 212 }, 213 } 214 } 215 216 func (b PodBuilder) buildCreationTime() metav1.Time { 217 if !b.creationTime.IsZero() { 218 return metav1.Time{Time: b.creationTime} 219 } 220 return metav1.Time{Time: time.Now()} 221 } 222 223 func (b PodBuilder) buildDeletionTime() *metav1.Time { 224 if !b.deletionTime.IsZero() { 225 return &metav1.Time{Time: b.deletionTime} 226 } 227 return nil 228 } 229 230 func (b PodBuilder) buildLabels(tSpec *v1.PodTemplateSpec) map[string]string { 231 labels := k8s.NewTiltLabelMap() 232 for k, v := range tSpec.Labels { 233 labels[k] = v 234 } 235 for k, v := range b.extraPodLabels { 236 labels[k] = v 237 } 238 return labels 239 } 240 241 func (b PodBuilder) buildImage(imageSpec string, index int) string { 242 image, ok := b.imageRefs[index] 243 if ok { 244 return image 245 } 246 247 imageSpecRef := container.MustParseNamed(imageSpec) 248 249 // Use the pod ID as the image tag. This is kind of weird, but gets at the semantics 250 // we want (e.g., a new pod ID indicates that this is a new build). 251 // Tests that don't want this behavior should replace the image with setImage(pod, imageName) 252 return fmt.Sprintf("%s:%s", imageSpecRef.Name(), b.PodID()) 253 } 254 255 func (b PodBuilder) buildContainerID(index int) string { 256 cID, ok := b.cIDs[index] 257 if ok { 258 return cID 259 } 260 261 return fmt.Sprintf("%s%s", k8s.ContainerIDPrefix, FakeContainerIDAtIndex(index)) 262 } 263 264 func (b PodBuilder) buildPhase() v1.PodPhase { 265 if b.phase == "" { 266 return v1.PodPhase("Running") 267 } 268 return v1.PodPhase(b.phase) 269 } 270 271 func (b PodBuilder) buildContainerStatuses(spec v1.PodSpec) []v1.ContainerStatus { 272 result := make([]v1.ContainerStatus, len(spec.Containers)) 273 for i, cSpec := range spec.Containers { 274 restartCount := 0 275 if i == 0 { 276 restartCount = b.restartCount 277 } 278 ready, ok := b.cReady[i] 279 // if not specified, default to true 280 ready = !ok || ready 281 282 result[i] = v1.ContainerStatus{ 283 Name: cSpec.Name, 284 Image: b.buildImage(cSpec.Image, i), 285 Ready: ready, 286 ContainerID: b.buildContainerID(i), 287 RestartCount: int32(restartCount), 288 } 289 } 290 return result 291 } 292 293 func (b PodBuilder) validateImageRefs(numContainers int) { 294 for index, img := range b.imageRefs { 295 if index >= numContainers { 296 b.t.Fatalf("Image %q specified at index %d. Pod only has %d containers", img, index, numContainers) 297 } 298 } 299 } 300 301 func (b PodBuilder) validateContainerIDs(numContainers int) { 302 for index, cID := range b.cIDs { 303 if index >= numContainers { 304 b.t.Fatalf("Container ID %q specified at index %d. Pod only has %d containers", cID, index, numContainers) 305 } 306 } 307 } 308 309 // Simulates a Pod -> ReplicaSet -> Deployment ref tree 310 func (b PodBuilder) ObjectTreeEntities() []k8s.K8sEntity { 311 pod := b.Build() 312 rs := b.buildReplicaSet() 313 dep := b.buildDeployment() 314 return []k8s.K8sEntity{ 315 k8s.NewK8sEntity(pod), 316 k8s.NewK8sEntity(rs), 317 k8s.NewK8sEntity(dep), 318 } 319 } 320 321 func (b PodBuilder) Build() *v1.Pod { 322 entities, err := parseYAMLFromManifest(b.manifest) 323 if err != nil { 324 b.t.Fatal(fmt.Errorf("PodBuilder YAML parser: %v", err)) 325 } 326 327 tSpecs, err := k8s.ExtractPodTemplateSpec(entities) 328 if err != nil { 329 b.t.Fatal(fmt.Errorf("PodBuilder extract pod templates: %v", err)) 330 } 331 332 if len(tSpecs) != 1 { 333 b.t.Fatalf("PodBuilder only works with Manifests with exactly 1 PodTemplateSpec: %v", tSpecs) 334 } 335 336 tSpec := tSpecs[0] 337 spec := tSpec.Spec 338 numContainers := len(spec.Containers) 339 b.validateImageRefs(numContainers) 340 b.validateContainerIDs(numContainers) 341 342 for i, container := range spec.Containers { 343 container.Image = b.buildImage(container.Image, i) 344 spec.Containers[i] = container 345 } 346 347 return &v1.Pod{ 348 ObjectMeta: metav1.ObjectMeta{ 349 Name: b.PodID(), 350 CreationTimestamp: b.buildCreationTime(), 351 DeletionTimestamp: b.buildDeletionTime(), 352 Labels: b.buildLabels(tSpec), 353 UID: b.buildPodUID(), 354 OwnerReferences: []metav1.OwnerReference{ 355 k8s.RuntimeObjToOwnerRef(b.buildReplicaSet()), 356 }, 357 }, 358 Spec: spec, 359 Status: v1.PodStatus{ 360 Phase: b.buildPhase(), 361 ContainerStatuses: b.buildContainerStatuses(spec), 362 }, 363 } 364 } 365 366 func imageNameForManifest(manifestName string) reference.Named { 367 return container.MustParseNamed(manifestName) 368 } 369 370 func parseYAMLFromManifest(m model.Manifest) ([]k8s.K8sEntity, error) { 371 return k8s.ParseYAMLFromString(m.K8sTarget().YAML) 372 }