github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/k8s/image_test.go (about) 1 package k8s 2 3 import ( 4 "fmt" 5 "strings" 6 "testing" 7 8 "github.com/distribution/reference" 9 "github.com/opencontainers/go-digest" 10 "github.com/stretchr/testify/assert" 11 "github.com/stretchr/testify/require" 12 appsv1 "k8s.io/api/apps/v1" 13 v1 "k8s.io/api/core/v1" 14 15 "github.com/tilt-dev/tilt/internal/container" 16 "github.com/tilt-dev/tilt/internal/k8s/testyaml" 17 ) 18 19 func TestInjectDigestSanchoYAML(t *testing.T) { 20 entities, err := ParseYAMLFromString(testyaml.SanchoYAML) 21 if err != nil { 22 t.Fatal(err) 23 } 24 25 if len(entities) != 1 { 26 t.Fatalf("Unexpected entities: %+v", entities) 27 } 28 29 entity := entities[0] 30 name := "gcr.io/some-project-162817/sancho" 31 digest := "sha256:2baf1f40105d9501fe319a8ec463fdf4325a2a5df445adf3f572f626253678c9" 32 newEntity, replaced, err := InjectImageDigestWithStrings(entity, name, digest, nil, v1.PullIfNotPresent) 33 if err != nil { 34 t.Fatal(err) 35 } 36 37 if !replaced { 38 t.Errorf("Expected replaced: true. Actual: %v", replaced) 39 } 40 41 result, err := SerializeSpecYAML([]K8sEntity{newEntity}) 42 if err != nil { 43 t.Fatal(err) 44 } 45 46 if !strings.Contains(result, fmt.Sprintf("image: %s@%s", name, digest)) { 47 t.Errorf("image name did not appear in serialized yaml: %s", result) 48 } 49 } 50 51 func TestInjectDigestDoesNotMutateOriginal(t *testing.T) { 52 entities, err := ParseYAMLFromString(testyaml.SanchoYAML) 53 if err != nil { 54 t.Fatal(err) 55 } 56 57 if len(entities) != 1 { 58 t.Fatalf("Unexpected entities: %+v", entities) 59 } 60 61 entity := entities[0] 62 name := "gcr.io/some-project-162817/sancho" 63 digest := "sha256:2baf1f40105d9501fe319a8ec463fdf4325a2a5df445adf3f572f626253678c9" 64 _, replaced, err := InjectImageDigestWithStrings(entity, name, digest, nil, v1.PullIfNotPresent) 65 if err != nil { 66 t.Fatal(err) 67 } 68 69 if !replaced { 70 t.Errorf("Expected replaced: true. Actual: %v", replaced) 71 } 72 73 result, err := SerializeSpecYAML([]K8sEntity{entity}) 74 if err != nil { 75 t.Fatal(err) 76 } 77 78 if strings.Contains(result, fmt.Sprintf("image: %s@%s", name, digest)) { 79 t.Errorf("oops! accidentally mutated original entity: %s", result) 80 } 81 } 82 83 func TestInjectImagePullPolicy(t *testing.T) { 84 entities, err := ParseYAMLFromString(testyaml.BlorgBackendYAML) 85 if err != nil { 86 t.Fatal(err) 87 } 88 89 entity := entities[1] 90 newEntity, err := InjectImagePullPolicy(entity, v1.PullNever) 91 if err != nil { 92 t.Fatal(err) 93 } 94 95 result, err := SerializeSpecYAML([]K8sEntity{newEntity}) 96 if err != nil { 97 t.Fatal(err) 98 } 99 100 if !strings.Contains(result, "imagePullPolicy: Never") { 101 t.Errorf("image does not have correct pull policy: %s", result) 102 } 103 104 serializedOrigEntity, err := SerializeSpecYAML([]K8sEntity{entity}) 105 if err != nil { 106 t.Fatal(err) 107 } 108 109 if strings.Contains(serializedOrigEntity, "imagePullPolicy: Never") { 110 t.Errorf("oops! accidentally mutated original entity: %+v", entity) 111 } 112 } 113 114 func TestInjectImagePullPolicyDoesNotMutateOriginal(t *testing.T) { 115 entities, err := ParseYAMLFromString(testyaml.BlorgBackendYAML) 116 if err != nil { 117 t.Fatal(err) 118 } 119 120 entity := entities[1] 121 _, err = InjectImagePullPolicy(entity, v1.PullNever) 122 if err != nil { 123 t.Fatal(err) 124 } 125 126 result, err := SerializeSpecYAML([]K8sEntity{entity}) 127 if err != nil { 128 t.Fatal(err) 129 } 130 131 if strings.Contains(result, "imagePullPolicy: Never") { 132 t.Errorf("oops! accidentally mutated original entity: %+v", entity) 133 } 134 } 135 136 func TestErrorInjectDigestBlorgBackendYAML(t *testing.T) { 137 entities, err := ParseYAMLFromString(testyaml.BlorgBackendYAML) 138 if err != nil { 139 t.Fatal(err) 140 } 141 142 if len(entities) != 2 { 143 t.Fatalf("Unexpected entities: %+v", entities) 144 } 145 146 entity := entities[1] 147 name := "gcr.io/blorg-dev/blorg-backend" 148 digest := "sha256:2baf1f40105d9501fe319a8ec463fdf4325a2a5df445adf3f572f626253678c9" 149 _, _, err = InjectImageDigestWithStrings(entity, name, digest, nil, v1.PullNever) 150 if err == nil || !strings.Contains(err.Error(), "INTERNAL TILT ERROR") { 151 t.Errorf("Expected internal tilt error, actual: %v", err) 152 } 153 } 154 155 func TestInjectDigestBlorgBackendYAML(t *testing.T) { 156 entities, err := ParseYAMLFromString(testyaml.BlorgBackendYAML) 157 if err != nil { 158 t.Fatal(err) 159 } 160 161 if len(entities) != 2 { 162 t.Fatalf("Unexpected entities: %+v", entities) 163 } 164 165 entity := entities[1] 166 name := "gcr.io/blorg-dev/blorg-backend" 167 namedTagged, _ := reference.ParseNamed(fmt.Sprintf("%s:wm-tilt", name)) 168 newEntity, replaced, err := InjectImageDigest(entity, container.NameSelector(namedTagged), namedTagged, nil, false, v1.PullNever) 169 if err != nil { 170 t.Fatal(err) 171 } 172 173 if !replaced { 174 t.Errorf("Expected replaced: true. Actual: %v", replaced) 175 } 176 177 result, err := SerializeSpecYAML([]K8sEntity{newEntity}) 178 if err != nil { 179 t.Fatal(err) 180 } 181 182 if !strings.Contains(result, fmt.Sprintf("image: %s", namedTagged)) { 183 t.Errorf("image name did not appear in serialized yaml: %s", result) 184 } 185 186 if !strings.Contains(result, "imagePullPolicy: Never") { 187 t.Errorf("image does not have correct pull policy: %s", result) 188 } 189 } 190 191 // the same as InjectImageDigestInjectRefWithStrings, but with original == inject (the normal case with no default_registry) 192 func InjectImageDigestWithStrings(entity K8sEntity, original string, newDigest string, locators []ImageLocator, policy v1.PullPolicy) (K8sEntity, bool, error) { 193 return InjectImageDigestInjectRefWithStrings(entity, original, original, newDigest, locators, policy) 194 } 195 196 // Returns: the new entity, whether anything was replaced, and an error. 197 func InjectImageDigestInjectRefWithStrings(entity K8sEntity, original string, inject string, newDigest string, locators []ImageLocator, policy v1.PullPolicy) (K8sEntity, bool, error) { 198 originalRef, err := reference.ParseNamed(original) 199 if err != nil { 200 return K8sEntity{}, false, err 201 } 202 203 injectRef, err := reference.ParseNamed(inject) 204 if err != nil { 205 return K8sEntity{}, false, err 206 } 207 208 d, err := digest.Parse(newDigest) 209 if err != nil { 210 return K8sEntity{}, false, err 211 } 212 213 canonicalRef, err := reference.WithDigest(injectRef, d) 214 if err != nil { 215 return K8sEntity{}, false, err 216 } 217 218 return InjectImageDigest(entity, container.NameSelector(originalRef), canonicalRef, locators, false, policy) 219 } 220 221 func TestInjectSyncletImage(t *testing.T) { 222 entities, err := ParseYAMLFromString(testyaml.SyncletYAML) 223 if err != nil { 224 t.Fatal(err) 225 } 226 227 assert.Equal(t, 1, len(entities)) 228 entity := entities[0] 229 name := "gcr.io/windmill-public-containers/synclet" 230 namedTagged, _ := container.ParseNamedTagged(fmt.Sprintf("%s:tilt-deadbeef", name)) 231 newEntity, replaced, err := InjectImageDigest(entity, container.NameSelector(namedTagged), namedTagged, nil, false, v1.PullNever) 232 if err != nil { 233 t.Fatal(err) 234 } else if !replaced { 235 t.Errorf("Expected replacement in:\n%s", testyaml.SyncletYAML) 236 } 237 238 result, err := SerializeSpecYAML([]K8sEntity{newEntity}) 239 if err != nil { 240 t.Fatal(err) 241 } 242 243 if !strings.Contains(result, namedTagged.String()) { 244 t.Errorf("could not find image in yaml (%s):\n%s", namedTagged, result) 245 } 246 } 247 248 func TestEntityHasImage(t *testing.T) { 249 entities, err := ParseYAMLFromString(testyaml.BlorgBackendYAML) 250 if err != nil { 251 t.Fatal(err) 252 } 253 254 img := container.MustParseSelector("gcr.io/blorg-dev/blorg-backend:devel-nick") 255 wrongImg := container.MustParseSelector("gcr.io/blorg-dev/wrong-app-whoops:devel-nick") 256 257 match, err := entities[0].HasImage(img, nil, false) 258 if err != nil { 259 t.Fatal(err) 260 } 261 assert.False(t, match, "service yaml should not match (does not contain image)") 262 263 match, err = entities[1].HasImage(img, nil, false) 264 if err != nil { 265 t.Fatal(err) 266 } 267 assert.True(t, match, "deployment yaml should match image %s", img.String()) 268 269 match, err = entities[1].HasImage(wrongImg, nil, false) 270 if err != nil { 271 t.Fatal(err) 272 } 273 assert.False(t, match, "deployment yaml should not match image %s", img.String()) 274 } 275 276 func TestCRDExtract(t *testing.T) { 277 entities, err := ParseYAMLFromString(testyaml.CRDYAML) 278 if err != nil { 279 t.Fatal(err) 280 } 281 282 img := container.MustParseTaggedSelector("docker.io/bitnami/minideb:latest") 283 e := entities[0] 284 selector, err := NewPartialMatchObjectSelector("", "", "projects.example.martin-helmich.de", "") 285 require.NoError(t, err) 286 287 jp, err := NewJSONPathImageLocator( 288 selector, 289 "{.spec.validation.openAPIV3Schema.properties.spec.properties.image}") 290 require.NoError(t, err) 291 292 match, err := e.HasImage(img, []ImageLocator{jp}, false) 293 require.NoError(t, err) 294 295 assert.True(t, match, "CRD yaml should match image %s", img.String()) 296 } 297 298 func TestEnvExtract(t *testing.T) { 299 entities, err := ParseYAMLFromString(testyaml.SanchoImageInEnvYAML) 300 if err != nil { 301 t.Fatal(err) 302 } 303 img := container.MustParseSelector("gcr.io/some-project-162817/sancho") 304 e := entities[0] 305 match, err := e.HasImage(img, nil, false) 306 if err != nil { 307 t.Fatal(err) 308 } 309 assert.True(t, match, "deployment yaml should match image %s", img.String()) 310 img2 := container.MustParseSelector("gcr.io/some-project-162817/sancho2") 311 e = entities[0] 312 match, err = e.HasImage(img2, nil, true) 313 if err != nil { 314 t.Fatal(err) 315 } 316 assert.True(t, match, "CRD yaml should match image %s", img2.String()) 317 318 } 319 320 func testInjectDigestCRD(t *testing.T, yaml string, locator ImageLocator, expectedDigestPrefix string) { 321 entities, err := ParseYAMLFromString(yaml) 322 if err != nil { 323 t.Fatal(err) 324 } 325 326 if len(entities) != 1 { 327 t.Fatalf("Unexpected entities: %+v", entities) 328 } 329 330 entity := entities[0] 331 name := "gcr.io/foo" 332 digest := "sha256:2baf1f40105d9501fe319a8ec463fdf4325a2a5df445adf3f572f626253678c9" 333 newEntity, replaced, err := InjectImageDigestWithStrings(entity, name, digest, 334 []ImageLocator{locator}, v1.PullIfNotPresent) 335 if err != nil { 336 t.Fatal(err) 337 } 338 339 if !replaced { 340 t.Errorf("Expected replaced: true. Actual: %v", replaced) 341 } 342 343 result, err := SerializeSpecYAML([]K8sEntity{newEntity}) 344 if err != nil { 345 t.Fatal(err) 346 } 347 348 if !strings.Contains(result, fmt.Sprintf("%s%s@%s", expectedDigestPrefix, name, digest)) { 349 t.Errorf("image name did not appear in serialized yaml: %s", result) 350 } 351 } 352 353 // e.g., using a crd w/ a default_registry 354 func TestInjectDigestCRDSelectorDoesntMatchInjectRef(t *testing.T) { 355 yaml := ` 356 apiversion: foo/v1 357 kind: Foo 358 spec: 359 image: gcr.io/foo:stable 360 ` 361 362 selector := MustKindSelector("Foo") 363 locator := MustJSONPathImageLocator(selector, "{.spec.image}") 364 entities, err := ParseYAMLFromString(yaml) 365 require.NoError(t, err) 366 367 if len(entities) != 1 { 368 t.Fatalf("Unexpected entities: %+v", entities) 369 } 370 371 entity := entities[0] 372 originalName := "gcr.io/foo" 373 injectionName := "localhost:3000/gcr_io_foo" 374 digest := "sha256:2baf1f40105d9501fe319a8ec463fdf4325a2a5df445adf3f572f626253678c9" 375 newEntity, replaced, err := InjectImageDigestInjectRefWithStrings(entity, originalName, injectionName, digest, 376 []ImageLocator{locator}, v1.PullIfNotPresent) 377 require.NoError(t, err) 378 379 require.Truef(t, replaced, "expected replaced: true. actual: %v", replaced) 380 381 result, err := SerializeSpecYAML([]K8sEntity{newEntity}) 382 require.NoError(t, err) 383 384 if !strings.Contains(result, fmt.Sprintf("%s%s@%s", "image: ", injectionName, digest)) { 385 t.Errorf("image name did not appear in serialized yaml: %s", result) 386 } 387 } 388 389 func TestInjectDigestCRDMapValue(t *testing.T) { 390 locator := MustJSONPathImageLocator(MustKindSelector("Foo"), "{.spec.image}") 391 testInjectDigestCRD(t, ` 392 apiversion: foo/v1 393 kind: Foo 394 spec: 395 image: gcr.io/foo:stable 396 `, locator, "image: ") 397 } 398 399 func TestInjectDigestCRDListElement(t *testing.T) { 400 locator := MustJSONPathImageLocator(MustKindSelector("Foo"), "{.spec.images[0]}") 401 testInjectDigestCRD(t, ` 402 apiversion: foo/v1 403 kind: Foo 404 spec: 405 images: 406 - gcr.io/foo:stable 407 `, locator, "- ") 408 } 409 410 func TestInjectDigestCRDListOfMaps(t *testing.T) { 411 locator := MustJSONPathImageLocator(MustKindSelector("Foo"), "{.spec.args.image}") 412 testInjectDigestCRD(t, ` 413 apiversion: foo/v1 414 kind: Foo 415 spec: 416 args: 417 image: gcr.io/foo:stable 418 `, locator, "image: ") 419 } 420 421 func TestMatchInEnvVarsFalse(t *testing.T) { 422 entity := parseOneEntity(t, testyaml.SanchoImageInEnvYAML) 423 name := "gcr.io/some-project-162817/sancho" 424 digest := "2baf1f40105d9501fe319a8ec463fdf4325a2a5df445adf3f572f626253678c9" 425 namedTagged, err := reference.ParseNamed(fmt.Sprintf("%s:%s", name, digest)) 426 if err != nil { 427 t.Fatal(err) 428 } 429 newEntity, replaced, err := InjectImageDigest(entity, container.NameSelector(namedTagged), namedTagged, nil, false, v1.PullNever) 430 if err != nil { 431 t.Fatal(err) 432 } 433 assert.True(t, replaced) 434 d := newEntity.Obj.(*appsv1.Deployment) 435 if !assert.Equal(t, 1, len(d.Spec.Template.Spec.Containers)) { 436 return 437 } 438 c := d.Spec.Template.Spec.Containers[0] 439 // make sure we didn't inject to the env var 440 assert.Equal(t, namedTagged.String(), c.Image) 441 assert.Contains(t, c.Env, v1.EnvVar{Name: "bar", Value: name}) 442 } 443 444 func TestMatchInEnvVarsTrue(t *testing.T) { 445 entity := parseOneEntity(t, testyaml.SanchoImageInEnvYAML) 446 name := "gcr.io/some-project-162817/sancho" 447 digest := "2baf1f40105d9501fe319a8ec463fdf4325a2a5df445adf3f572f626253678c9" 448 namedTagged, err := reference.ParseNamed(fmt.Sprintf("%s:%s", name, digest)) 449 if err != nil { 450 t.Fatal(err) 451 } 452 newEntity, replaced, err := InjectImageDigest(entity, container.NameSelector(namedTagged), namedTagged, nil, true, v1.PullNever) 453 if err != nil { 454 t.Fatal(err) 455 } 456 assert.True(t, replaced) 457 d := newEntity.Obj.(*appsv1.Deployment) 458 if !assert.Equal(t, 1, len(d.Spec.Template.Spec.Containers)) { 459 return 460 } 461 c := d.Spec.Template.Spec.Containers[0] 462 // make sure we didn't inject to the env var 463 assert.Equal(t, namedTagged.String(), c.Image) 464 assert.Contains(t, c.Env, v1.EnvVar{Name: "bar", Value: namedTagged.String()}) 465 }