github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/controllers/core/tiltfile/api_test.go (about)

     1  package tiltfile
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"testing"
     7  
     8  	"github.com/stretchr/testify/assert"
     9  	"github.com/stretchr/testify/require"
    10  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    11  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    12  	"k8s.io/apimachinery/pkg/runtime/schema"
    13  	"k8s.io/apimachinery/pkg/types"
    14  	ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
    15  
    16  	"github.com/tilt-dev/tilt/internal/controllers/fake"
    17  	"github.com/tilt-dev/tilt/internal/k8s/testyaml"
    18  	"github.com/tilt-dev/tilt/internal/store"
    19  	"github.com/tilt-dev/tilt/internal/testutils/configmap"
    20  	"github.com/tilt-dev/tilt/internal/testutils/manifestbuilder"
    21  	"github.com/tilt-dev/tilt/internal/testutils/tempdir"
    22  	"github.com/tilt-dev/tilt/internal/tiltfile"
    23  	"github.com/tilt-dev/tilt/pkg/apis"
    24  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    25  	"github.com/tilt-dev/tilt/pkg/model"
    26  )
    27  
    28  func TestAPICreate(t *testing.T) {
    29  	f := newAPIFixture(t)
    30  	fe := manifestbuilder.New(f, "fe").WithK8sYAML(testyaml.SanchoYAML).Build()
    31  	nn := types.NamespacedName{Name: "tiltfile"}
    32  	tf := &v1alpha1.Tiltfile{ObjectMeta: metav1.ObjectMeta{Name: "tiltfile"}}
    33  	err := f.updateOwnedObjects(nn, tf,
    34  		&tiltfile.TiltfileLoadResult{Manifests: []model.Manifest{fe}})
    35  	assert.NoError(t, err)
    36  
    37  	var ka v1alpha1.KubernetesApply
    38  	assert.NoError(t, f.Get(types.NamespacedName{Name: "fe"}, &ka))
    39  	assert.Contains(t, ka.Spec.YAML, "name: sancho")
    40  }
    41  
    42  func TestAPIDelete(t *testing.T) {
    43  	f := newAPIFixture(t)
    44  	fe := manifestbuilder.New(f, "fe").WithK8sYAML(testyaml.SanchoYAML).Build()
    45  	nn := types.NamespacedName{Name: "tiltfile"}
    46  	tf := &v1alpha1.Tiltfile{ObjectMeta: metav1.ObjectMeta{Name: "tiltfile"}}
    47  	err := f.updateOwnedObjects(nn, tf,
    48  		&tiltfile.TiltfileLoadResult{Manifests: []model.Manifest{fe}})
    49  	assert.NoError(t, err)
    50  
    51  	var ka1 v1alpha1.KubernetesApply
    52  	assert.NoError(t, f.Get(types.NamespacedName{Name: "fe"}, &ka1))
    53  
    54  	err = f.updateOwnedObjects(nn, tf,
    55  		&tiltfile.TiltfileLoadResult{Manifests: []model.Manifest{}})
    56  	assert.NoError(t, err)
    57  
    58  	var ka2 v1alpha1.KubernetesApply
    59  	err = f.Get(types.NamespacedName{Name: "fe"}, &ka2)
    60  	if assert.Error(t, err) {
    61  		assert.True(t, apierrors.IsNotFound(err))
    62  	}
    63  }
    64  
    65  func TestAPINoGarbageCollectOnError(t *testing.T) {
    66  	f := newAPIFixture(t)
    67  	fe := manifestbuilder.New(f, "fe").WithK8sYAML(testyaml.SanchoYAML).Build()
    68  	nn := types.NamespacedName{Name: "tiltfile"}
    69  	tf := &v1alpha1.Tiltfile{ObjectMeta: metav1.ObjectMeta{Name: "tiltfile"}}
    70  	err := f.updateOwnedObjects(nn, tf,
    71  		&tiltfile.TiltfileLoadResult{Manifests: []model.Manifest{fe}})
    72  	assert.NoError(t, err)
    73  
    74  	var ka1 v1alpha1.KubernetesApply
    75  	assert.NoError(t, f.Get(types.NamespacedName{Name: "fe"}, &ka1))
    76  
    77  	err = f.updateOwnedObjects(nn, tf, &tiltfile.TiltfileLoadResult{
    78  		Error:     fmt.Errorf("random failure"),
    79  		Manifests: []model.Manifest{},
    80  	})
    81  	assert.NoError(t, err)
    82  
    83  	var ka2 v1alpha1.KubernetesApply
    84  	assert.NoError(t, f.Get(types.NamespacedName{Name: "fe"}, &ka2))
    85  	assert.Equal(t, ka1, ka2)
    86  }
    87  
    88  func TestAPIUpdate(t *testing.T) {
    89  	f := newAPIFixture(t)
    90  	fe := manifestbuilder.New(f, "fe").WithK8sYAML(testyaml.SanchoYAML).Build()
    91  	nn := types.NamespacedName{Name: "tiltfile"}
    92  	tf := &v1alpha1.Tiltfile{ObjectMeta: metav1.ObjectMeta{Name: "tiltfile"}}
    93  	err := f.updateOwnedObjects(nn, tf,
    94  		&tiltfile.TiltfileLoadResult{Manifests: []model.Manifest{fe}})
    95  	assert.NoError(t, err)
    96  
    97  	var ka v1alpha1.KubernetesApply
    98  	assert.NoError(t, f.Get(types.NamespacedName{Name: "fe"}, &ka))
    99  	assert.Contains(t, ka.Spec.YAML, "name: sancho")
   100  	assert.NotContains(t, ka.Spec.YAML, "sidecar")
   101  
   102  	fe = manifestbuilder.New(f, "fe").WithK8sYAML(testyaml.SanchoSidecarYAML).Build()
   103  	err = f.updateOwnedObjects(nn, tf,
   104  		&tiltfile.TiltfileLoadResult{Manifests: []model.Manifest{fe}})
   105  	assert.NoError(t, err)
   106  
   107  	err = f.Get(types.NamespacedName{Name: "fe"}, &ka)
   108  	assert.NoError(t, err)
   109  	assert.Contains(t, ka.Spec.YAML, "sidecar")
   110  }
   111  
   112  func TestImageMapCreate(t *testing.T) {
   113  	f := newAPIFixture(t)
   114  	fe := manifestbuilder.New(f, "fe").
   115  		WithImageTarget(NewSanchoDockerBuildImageTarget(f)).
   116  		WithK8sYAML(testyaml.SanchoYAML).
   117  		Build()
   118  	nn := types.NamespacedName{Name: "tiltfile"}
   119  	tf := &v1alpha1.Tiltfile{ObjectMeta: metav1.ObjectMeta{Name: "tiltfile"}}
   120  	err := f.updateOwnedObjects(nn, tf,
   121  		&tiltfile.TiltfileLoadResult{Manifests: []model.Manifest{fe}})
   122  	assert.NoError(t, err)
   123  
   124  	name := apis.SanitizeName(SanchoRef.String())
   125  
   126  	var im v1alpha1.ImageMap
   127  	assert.NoError(t, f.Get(types.NamespacedName{Name: name}, &im))
   128  	assert.Contains(t, im.Spec.Selector, SanchoRef.String())
   129  
   130  	diName := apis.SanitizeName(fmt.Sprintf("fe:%s", SanchoRef.String()))
   131  	var di v1alpha1.DockerImage
   132  	assert.NoError(t, f.Get(types.NamespacedName{Name: diName}, &di))
   133  	assert.Contains(t, di.Spec.Ref, SanchoRef.String())
   134  }
   135  
   136  func TestCmdImageCreate(t *testing.T) {
   137  	f := newAPIFixture(t)
   138  	target := model.MustNewImageTarget(SanchoRef).
   139  		WithBuildDetails(model.CustomBuild{
   140  			CmdImageSpec: v1alpha1.CmdImageSpec{Args: []string{"echo"}},
   141  			Deps:         []string{f.Path()},
   142  		})
   143  	fe := manifestbuilder.New(f, "fe").
   144  		WithImageTarget(target).
   145  		WithK8sYAML(testyaml.SanchoYAML).
   146  		Build()
   147  	nn := types.NamespacedName{Name: "tiltfile"}
   148  	tf := &v1alpha1.Tiltfile{ObjectMeta: metav1.ObjectMeta{Name: "tiltfile"}}
   149  	err := f.updateOwnedObjects(nn, tf,
   150  		&tiltfile.TiltfileLoadResult{Manifests: []model.Manifest{fe}})
   151  	assert.NoError(t, err)
   152  
   153  	name := apis.SanitizeName(SanchoRef.String())
   154  
   155  	var im v1alpha1.ImageMap
   156  	assert.NoError(t, f.Get(types.NamespacedName{Name: name}, &im))
   157  	assert.Contains(t, im.Spec.Selector, SanchoRef.String())
   158  
   159  	ciName := apis.SanitizeName(fmt.Sprintf("fe:%s", SanchoRef.String()))
   160  	var ci v1alpha1.CmdImage
   161  	assert.NoError(t, f.Get(types.NamespacedName{Name: ciName}, &ci))
   162  	assert.Contains(t, ci.Spec.Ref, SanchoRef.String())
   163  }
   164  
   165  func TestTwoManifestsShareImage(t *testing.T) {
   166  	f := newAPIFixture(t)
   167  	target := model.MustNewImageTarget(SanchoRef).
   168  		WithBuildDetails(model.CustomBuild{
   169  			CmdImageSpec: v1alpha1.CmdImageSpec{Args: []string{"echo"}},
   170  			Deps:         []string{f.Path()},
   171  		})
   172  	fe1 := manifestbuilder.New(f, "fe1").
   173  		WithImageTarget(target).
   174  		WithK8sYAML(testyaml.SanchoYAML).
   175  		Build()
   176  	fe2 := manifestbuilder.New(f, "fe2").
   177  		WithImageTarget(target).
   178  		WithK8sYAML(testyaml.SanchoYAML).
   179  		Build()
   180  	nn := types.NamespacedName{Name: "tiltfile"}
   181  	tf := &v1alpha1.Tiltfile{ObjectMeta: metav1.ObjectMeta{Name: "tiltfile"}}
   182  	err := f.updateOwnedObjects(nn, tf,
   183  		&tiltfile.TiltfileLoadResult{Manifests: []model.Manifest{fe1, fe2}})
   184  	assert.NoError(t, err)
   185  
   186  	name := apis.SanitizeName(fe1.ImageTargets[0].ID().String())
   187  
   188  	var fw v1alpha1.FileWatch
   189  	assert.NoError(t, f.Get(types.NamespacedName{Name: name}, &fw))
   190  	assert.Equal(t, fw.Spec.DisableSource, &v1alpha1.DisableSource{
   191  		EveryConfigMap: []v1alpha1.ConfigMapDisableSource{
   192  			{Name: "fe1-disable", Key: "isDisabled"},
   193  			{Name: "fe2-disable", Key: "isDisabled"},
   194  		},
   195  	})
   196  }
   197  
   198  func TestAPITwoTiltfiles(t *testing.T) {
   199  	f := newAPIFixture(t)
   200  	feA := manifestbuilder.New(f, "fe-a").WithK8sYAML(testyaml.SanchoYAML).Build()
   201  	nnA := types.NamespacedName{Name: "tiltfile-a"}
   202  	tfA := &v1alpha1.Tiltfile{ObjectMeta: metav1.ObjectMeta{Name: "tiltfile-a"}}
   203  
   204  	feB := manifestbuilder.New(f, "fe-b").WithK8sYAML(testyaml.SanchoYAML).Build()
   205  	nnB := types.NamespacedName{Name: "tiltfile-b"}
   206  	tfB := &v1alpha1.Tiltfile{ObjectMeta: metav1.ObjectMeta{Name: "tiltfile-b"}}
   207  
   208  	err := f.updateOwnedObjects(nnA, tfA,
   209  		&tiltfile.TiltfileLoadResult{Manifests: []model.Manifest{feA}})
   210  	assert.NoError(t, err)
   211  
   212  	err = f.updateOwnedObjects(nnB, tfB,
   213  		&tiltfile.TiltfileLoadResult{Manifests: []model.Manifest{feB}})
   214  	assert.NoError(t, err)
   215  
   216  	var ka v1alpha1.KubernetesApply
   217  	assert.NoError(t, f.Get(types.NamespacedName{Name: "fe-a"}, &ka))
   218  	assert.Contains(t, ka.Name, "fe-a")
   219  	assert.NoError(t, f.Get(types.NamespacedName{Name: "fe-b"}, &ka))
   220  	assert.Contains(t, ka.Name, "fe-b")
   221  
   222  	err = f.updateOwnedObjects(nnA, nil, nil)
   223  	assert.NoError(t, err)
   224  
   225  	// Assert that fe-a was deleted but fe-b was not.
   226  	assert.NoError(t, f.Get(types.NamespacedName{Name: "fe-b"}, &ka))
   227  	assert.Contains(t, ka.Name, "fe-b")
   228  
   229  	err = f.Get(types.NamespacedName{Name: "fe-a"}, &ka)
   230  	if assert.Error(t, err) {
   231  		assert.True(t, apierrors.IsNotFound(err))
   232  	}
   233  }
   234  
   235  func TestCreateUiResourceForTiltfile(t *testing.T) {
   236  	f := newAPIFixture(t)
   237  	fe := manifestbuilder.New(f, "fe").
   238  		WithImageTarget(NewSanchoDockerBuildImageTarget(f)).
   239  		WithK8sYAML(testyaml.SanchoYAML).
   240  		Build()
   241  	lr := manifestbuilder.New(f, "be").WithLocalResource("ls", []string{"be"}).Build()
   242  	nn := types.NamespacedName{Name: "tiltfile"}
   243  	tf := &v1alpha1.Tiltfile{ObjectMeta: metav1.ObjectMeta{Name: "tiltfile", Labels: map[string]string{"some": "sweet-label"}}}
   244  	err := f.updateOwnedObjects(nn, tf,
   245  		&tiltfile.TiltfileLoadResult{Manifests: []model.Manifest{fe, lr}})
   246  	assert.NoError(t, err)
   247  
   248  	var uir v1alpha1.UIResource
   249  	require.NoError(t, f.Get(types.NamespacedName{Name: "tiltfile"}, &uir))
   250  	require.Equal(t, map[string]string{"some": "sweet-label"}, uir.ObjectMeta.Labels)
   251  	require.Equal(t, "tiltfile", uir.ObjectMeta.Name)
   252  }
   253  
   254  func TestCreateClusterDefaultRegistry(t *testing.T) {
   255  	f := newAPIFixture(t)
   256  	fe := manifestbuilder.New(f, "fe").
   257  		WithImageTarget(NewSanchoDockerBuildImageTarget(f)).
   258  		WithK8sYAML(testyaml.SanchoYAML).
   259  		Build()
   260  	tf := &v1alpha1.Tiltfile{
   261  		ObjectMeta: metav1.ObjectMeta{Name: model.MainTiltfileManifestName.String()},
   262  	}
   263  	nn := apis.Key(tf)
   264  	reg := &v1alpha1.RegistryHosting{
   265  		Host:       "registry.example.com",
   266  		SingleName: "fake-repo",
   267  	}
   268  	tlr := &tiltfile.TiltfileLoadResult{
   269  		Manifests:       []model.Manifest{fe},
   270  		DefaultRegistry: reg,
   271  	}
   272  	err := f.updateOwnedObjects(nn, tf, tlr)
   273  	assert.NoError(t, err)
   274  
   275  	var cluster v1alpha1.Cluster
   276  	require.NoError(t, f.Get(types.NamespacedName{Name: "default"}, &cluster))
   277  	require.NotNil(t, cluster.Spec.DefaultRegistry, ".Spec.DefaultRegistry was nil")
   278  	require.Equal(t, "registry.example.com", cluster.Spec.DefaultRegistry.Host, "Default registry host")
   279  	require.Equal(t, "fake-repo", cluster.Spec.DefaultRegistry.SingleName, "Default registry single name")
   280  }
   281  
   282  // Ensure that we emit disable-related objects/field appropriately
   283  func TestDisableObjects(t *testing.T) {
   284  	f := newAPIFixture(t)
   285  	fe := manifestbuilder.New(f, "fe").
   286  		WithImageTarget(NewSanchoDockerBuildImageTarget(f)).
   287  		WithK8sYAML(testyaml.SanchoYAML).
   288  		Build()
   289  	lr := manifestbuilder.New(f, "be").WithLocalResource("ls", []string{"be"}).Build()
   290  	nn := types.NamespacedName{Name: "tiltfile"}
   291  	tf := &v1alpha1.Tiltfile{ObjectMeta: metav1.ObjectMeta{Name: "tiltfile"}}
   292  	err := f.updateOwnedObjects(nn, tf,
   293  		&tiltfile.TiltfileLoadResult{
   294  			Manifests: []model.Manifest{fe, lr},
   295  		})
   296  	assert.NoError(t, err)
   297  
   298  	feDisable := &v1alpha1.DisableSource{
   299  		ConfigMap: &v1alpha1.ConfigMapDisableSource{
   300  			Name: "fe-disable",
   301  			Key:  "isDisabled",
   302  		},
   303  	}
   304  
   305  	var cm v1alpha1.ConfigMap
   306  	require.NoError(t, f.Get(types.NamespacedName{Name: feDisable.ConfigMap.Name}, &cm))
   307  	require.Equal(t, "true", cm.Data[feDisable.ConfigMap.Key])
   308  
   309  	name := apis.SanitizeName(SanchoRef.String())
   310  	var im v1alpha1.ImageMap
   311  	require.NoError(t, f.Get(types.NamespacedName{Name: name}, &im))
   312  
   313  	var ka v1alpha1.KubernetesApply
   314  	require.NoError(t, f.Get(types.NamespacedName{Name: "fe"}, &ka))
   315  	require.Equal(t, feDisable, ka.Spec.DisableSource)
   316  
   317  	beDisable := &v1alpha1.DisableSource{
   318  		ConfigMap: &v1alpha1.ConfigMapDisableSource{
   319  			Name: "be-disable",
   320  			Key:  "isDisabled",
   321  		},
   322  	}
   323  
   324  	var fw v1alpha1.FileWatch
   325  	require.NoError(t, f.Get(types.NamespacedName{Name: "local:be"}, &fw))
   326  	require.Equal(t, beDisable, fw.Spec.DisableSource)
   327  
   328  	var cmd v1alpha1.Cmd
   329  	require.NoError(t, f.Get(types.NamespacedName{Name: "be:update"}, &cmd))
   330  	require.Equal(t, beDisable, cmd.Spec.DisableSource)
   331  
   332  	var uir v1alpha1.UIResource
   333  	require.NoError(t, f.Get(types.NamespacedName{Name: "be"}, &uir))
   334  	require.Equal(t, []v1alpha1.DisableSource{*beDisable}, uir.Status.DisableStatus.Sources)
   335  
   336  	var tb v1alpha1.ToggleButton
   337  	err = f.Get(types.NamespacedName{Name: "fe-disable"}, &tb)
   338  	require.NoError(t, err)
   339  	require.Equal(t, feDisable.ConfigMap.Name, tb.Spec.StateSource.ConfigMap.Name)
   340  
   341  	err = f.Get(types.NamespacedName{Name: "be-disable"}, &tb)
   342  	require.NoError(t, err)
   343  	require.Equal(t, beDisable.ConfigMap.Name, tb.Spec.StateSource.ConfigMap.Name)
   344  }
   345  
   346  // If a DisableSource ConfigMap already exists, don't replace its data
   347  func TestUpdateDisableSource(t *testing.T) {
   348  	f := newAPIFixture(t)
   349  	fe := manifestbuilder.New(f, "fe").WithK8sYAML(testyaml.SanchoYAML).Build()
   350  	nn := types.NamespacedName{Name: "tiltfile"}
   351  	tf := &v1alpha1.Tiltfile{ObjectMeta: metav1.ObjectMeta{Name: "tiltfile"}}
   352  	err := f.updateOwnedObjects(nn, tf,
   353  		&tiltfile.TiltfileLoadResult{Manifests: []model.Manifest{fe}})
   354  	assert.NoError(t, err)
   355  
   356  	err = configmap.UpsertDisableConfigMap(f.ctx, f.c, "fe-disable", "isDisabled", true)
   357  	require.NoError(t, err)
   358  
   359  	err = f.updateOwnedObjects(nn, tf,
   360  		&tiltfile.TiltfileLoadResult{Manifests: []model.Manifest{fe}})
   361  	assert.NoError(t, err)
   362  
   363  	var cm v1alpha1.ConfigMap
   364  	require.NoError(t, f.Get(types.NamespacedName{Name: "fe-disable"}, &cm))
   365  	require.Equal(t, "true", cm.Data["isDisabled"])
   366  }
   367  
   368  // make sure that objects created by the Tiltfile are included in typesToReconcile, so that
   369  // they get cleaned up when they go away
   370  // note: this test is not exhaustive, since not all branches generate all types that are possibly
   371  // generated by a Tiltfile, but hopefully it at least catches most common cases
   372  func TestReconciledTypesCompleteness(t *testing.T) {
   373  	f := newAPIFixture(t)
   374  	nn := types.NamespacedName{Name: "tiltfile"}
   375  	tf := &v1alpha1.Tiltfile{ObjectMeta: metav1.ObjectMeta{Name: "tiltfile"}}
   376  	fe := manifestbuilder.New(f, "fe").WithK8sYAML(testyaml.SanchoYAML).Build()
   377  	tlr := &tiltfile.TiltfileLoadResult{Manifests: []model.Manifest{fe}}
   378  	ds := toDisableSources(tlr)
   379  	objs := toAPIObjects(nn, tf, tlr, 0, store.EngineModeCI, &v1alpha1.KubernetesClusterConnection{}, ds)
   380  
   381  	reconciledTypes := make(map[schema.GroupVersionResource]bool)
   382  	for _, t := range typesToReconcile {
   383  		reconciledTypes[t.GetGroupVersionResource()] = true
   384  	}
   385  
   386  	for _, os := range objs {
   387  		for _, v := range os {
   388  			require.Truef(t,
   389  				reconciledTypes[v.GetGroupVersionResource()],
   390  				"object %q of type %q was generated by the Tiltfile, but is not listed in typesToReconcile.\n"+
   391  					"either add the type to typesToReconcile or change the Tiltfile reconciler to not generate it.",
   392  				v.GetName(),
   393  				v.GetGroupVersionResource())
   394  		}
   395  	}
   396  }
   397  
   398  type apiFixture struct {
   399  	ctx context.Context
   400  	c   ctrlclient.Client
   401  	*tempdir.TempDirFixture
   402  }
   403  
   404  func newAPIFixture(t testing.TB) *apiFixture {
   405  	f := tempdir.NewTempDirFixture(t)
   406  
   407  	ctx := context.Background()
   408  	c := fake.NewFakeTiltClient()
   409  	return &apiFixture{
   410  		ctx:            ctx,
   411  		c:              c,
   412  		TempDirFixture: f,
   413  	}
   414  }
   415  
   416  func (f *apiFixture) updateOwnedObjects(nn types.NamespacedName, tf *v1alpha1.Tiltfile, tlr *tiltfile.TiltfileLoadResult) error {
   417  	return updateOwnedObjects(f.ctx, f.c, nn, tf, tlr, false, 0, store.EngineModeUp,
   418  		&v1alpha1.KubernetesClusterConnection{})
   419  }
   420  
   421  func (f *apiFixture) Get(nn types.NamespacedName, obj ctrlclient.Object) error {
   422  	return f.c.Get(f.ctx, nn, obj)
   423  }