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  }