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

     1  //go:build !skiplargetiltfiletests
     2  // +build !skiplargetiltfiletests
     3  
     4  // On windows, running Helm can take ~0.5 seconds,
     5  // which starts to blow up test times.
     6  
     7  package tiltfile
     8  
     9  import (
    10  	"os/exec"
    11  	"strings"
    12  	"testing"
    13  
    14  	"github.com/stretchr/testify/assert"
    15  	"github.com/stretchr/testify/require"
    16  
    17  	"github.com/tilt-dev/tilt/internal/k8s"
    18  	"github.com/tilt-dev/tilt/internal/tiltfile/testdata"
    19  )
    20  
    21  func TestHelm(t *testing.T) {
    22  	f := newFixture(t)
    23  
    24  	f.setupHelm()
    25  
    26  	f.file("Tiltfile", `
    27  yml = helm('helm')
    28  k8s_yaml(yml)
    29  `)
    30  
    31  	f.load()
    32  
    33  	f.assertNextManifestUnresourced("chart-helloworld-chart")
    34  	f.assertConfigFiles(
    35  		"Tiltfile",
    36  		".tiltignore",
    37  		"helm",
    38  	)
    39  }
    40  
    41  func TestHelmArgs(t *testing.T) {
    42  	f := newFixture(t)
    43  
    44  	f.setupHelm()
    45  
    46  	f.file("Tiltfile", `
    47  yml = helm('./helm', name='rose-quartz', namespace='garnet', values=['./dev/helm/values-dev.yaml'])
    48  k8s_yaml(yml)
    49  `)
    50  
    51  	f.load()
    52  
    53  	m := f.assertNextManifestUnresourced("rose-quartz-helloworld-chart")
    54  	yaml := m.K8sTarget().YAML
    55  	assert.Contains(t, yaml, "release: rose-quartz")
    56  	assert.Contains(t, yaml, "namespace: garnet")
    57  	assert.Contains(t, yaml, "namespaceLabel: garnet")
    58  	assert.Contains(t, yaml, "name: nginx-dev")
    59  
    60  	entities, err := k8s.ParseYAMLFromString(yaml)
    61  	require.NoError(t, err)
    62  
    63  	names := k8s.UniqueNames(entities, 2)
    64  	expectedNames := []string{"rose-quartz-helloworld-chart:service"}
    65  	assert.ElementsMatch(t, expectedNames, names)
    66  
    67  	f.assertConfigFiles("./helm/", "./dev/helm/values-dev.yaml", ".tiltignore", "Tiltfile")
    68  }
    69  
    70  func TestHelmNamespaceFlagDoesNotInsertNSEntityIfNSInChart(t *testing.T) {
    71  	f := newFixture(t)
    72  
    73  	f.setupHelm()
    74  
    75  	valuesWithNamespace := `
    76  namespace:
    77    enabled: true
    78    name: foobarbaz`
    79  	f.file("helm/extra_values.yaml", valuesWithNamespace)
    80  
    81  	f.file("Tiltfile", `
    82  yml = helm('./helm', name='rose-quartz', namespace="foobarbaz", values=['./helm/extra_values.yaml'])
    83  k8s_yaml(yml)
    84  `)
    85  
    86  	f.load()
    87  
    88  	m := f.assertNextManifestUnresourced("foobarbaz", "rose-quartz-helloworld-chart")
    89  	yaml := m.K8sTarget().YAML
    90  
    91  	entities, err := k8s.ParseYAMLFromString(yaml)
    92  	require.NoError(t, err)
    93  	require.Len(t, entities, 2)
    94  	e := entities[0]
    95  	require.Equal(t, "Namespace", e.GVK().Kind)
    96  	assert.Equal(t, "foobarbaz", e.Name())
    97  	assert.Equal(t, "indeed", e.Labels()["somePersistedLabel"],
    98  		"label originally specified in chart YAML should persist")
    99  }
   100  
   101  func TestHelmNamespaceFlagInsertsNSEntityIfDifferentNSInChart(t *testing.T) {
   102  	f := newFixture(t)
   103  
   104  	f.setupHelm()
   105  
   106  	valuesWithNamespace := `
   107  namespace:
   108    enabled: true
   109    name: not-the-one-specified-in-flag` // what kind of jerk would do this?
   110  	f.file("helm/extra_values.yaml", valuesWithNamespace)
   111  
   112  	f.file("Tiltfile", `
   113  yml = helm('./helm', name='rose-quartz', namespace="foobarbaz", values=['./helm/extra_values.yaml'])
   114  k8s_yaml(yml)
   115  `)
   116  
   117  	f.load()
   118  
   119  	f.assertNextManifestUnresourced("not-the-one-specified-in-flag", "rose-quartz-helloworld-chart")
   120  }
   121  
   122  func TestHelmInvalidDirectory(t *testing.T) {
   123  	f := newFixture(t)
   124  
   125  	f.file("Tiltfile", `
   126  yml = helm('helm')
   127  k8s_yaml(yml)
   128  `)
   129  
   130  	f.loadErrString("Could not read Helm chart directory")
   131  }
   132  
   133  func TestHelmFromRepoPath(t *testing.T) {
   134  	f := newFixture(t)
   135  
   136  	f.gitInit(".")
   137  	f.setupHelm()
   138  
   139  	f.file("Tiltfile", `
   140  r = local_git_repo('.')
   141  yml = helm(r.paths('helm'))
   142  k8s_yaml(yml)
   143  `)
   144  
   145  	f.load()
   146  
   147  	f.assertNextManifestUnresourced("chart-helloworld-chart")
   148  	f.assertConfigFiles(
   149  		"Tiltfile",
   150  		".tiltignore",
   151  		"helm",
   152  	)
   153  }
   154  
   155  func TestHelmMalformedChart(t *testing.T) {
   156  	f := newFixture(t)
   157  
   158  	f.WriteFile("./helm/Chart.yaml", "brrrrr")
   159  
   160  	f.file("Tiltfile", `
   161  yml = helm('helm')
   162  k8s_yaml(yml)
   163  `)
   164  
   165  	f.loadErrString("error unmarshaling JSON")
   166  	f.assertConfigFiles(
   167  		"Tiltfile",
   168  		".tiltignore",
   169  		"helm",
   170  	)
   171  }
   172  
   173  func TestHelmNamespace(t *testing.T) {
   174  	f := newFixture(t)
   175  
   176  	f.setupHelm()
   177  	f.file("helm/templates/public-config.yaml", `apiVersion: v1
   178  kind: ConfigMap
   179  metadata:
   180    name: public-config
   181    namespace: kube-public
   182  data:
   183    noData: "true"
   184  `)
   185  
   186  	f.file("Tiltfile", `
   187  yml = helm('./helm', name='rose-quartz', namespace='garnet')
   188  k8s_yaml(yml)
   189  `)
   190  
   191  	f.load()
   192  
   193  	m := f.assertNextManifestUnresourced(
   194  		"public-config",
   195  		"rose-quartz-helloworld-chart")
   196  	yaml := m.K8sTarget().YAML
   197  
   198  	assert.Contains(t, yaml, "name: rose-quartz-helloworld-chart\n  namespace: garnet")
   199  	assert.Contains(t, yaml, "name: public-config\n  namespace: kube-public")
   200  }
   201  
   202  func TestHelmSetArgs(t *testing.T) {
   203  	f := newFixture(t)
   204  
   205  	f.setupHelm()
   206  
   207  	f.file("Tiltfile", `
   208  yml = helm('./helm', name='rose-quartz', namespace='garnet', set=[
   209    'ingress.enabled=true',
   210    'service.externalPort=1234',
   211    'service.internalPort=5678'
   212  ])
   213  k8s_yaml(yml)
   214  `)
   215  
   216  	f.load()
   217  
   218  	m := f.assertNextManifestUnresourced(
   219  		// A service and ingress with the same name
   220  		"rose-quartz-helloworld-chart",
   221  		"rose-quartz-helloworld-chart")
   222  	yaml := m.K8sTarget().YAML
   223  
   224  	// Set on the service
   225  	assert.Contains(t, yaml, "port: 1234")
   226  	assert.Contains(t, yaml, "targetPort: 5678")
   227  
   228  	// Set on the ingress
   229  	assert.Contains(t, yaml, "serviceName: rose-quartz-helloworld-chart")
   230  	assert.Contains(t, yaml, "servicePort: 1234")
   231  }
   232  
   233  func TestHelmSetArgsMap(t *testing.T) {
   234  	f := newFixture(t)
   235  
   236  	f.setupHelm()
   237  
   238  	f.file("Tiltfile", `
   239  yml = helm('./helm', name='rose-quartz', namespace='garnet', set={'a': 'b'})
   240  k8s_yaml(yml)
   241  `)
   242  
   243  	f.loadErrString("helm: for parameter \"set\"", "string", "List", "type dict")
   244  }
   245  
   246  const exampleHelmV2VersionOutput = `Client: v2.12.3geecf22f`
   247  const exampleHelmV3_0VersionOutput = `v3.0.0`
   248  const exampleHelmV3_1VersionOutput = `v3.1.0`
   249  const exampleHelmV3_2VersionOutput = `v3.2.4`
   250  
   251  // see https://github.com/tilt-dev/tilt/issues/3788
   252  const exampleHelmV3_3VersionOutput = `WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: /Users/someone/.kube/config
   253  WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: /Users/someone/.kube/config
   254  v3.3.3+g55e3ca0
   255  `
   256  
   257  func TestParseHelmV2Version(t *testing.T) {
   258  	expected := helmV2
   259  	assertHelmVersion(t, exampleHelmV2VersionOutput, expected)
   260  }
   261  
   262  func TestParseHelmV3Version(t *testing.T) {
   263  	expected := helmV3_0
   264  	assertHelmVersion(t, exampleHelmV3_0VersionOutput, expected)
   265  }
   266  
   267  func TestParseHelmV3_1Version(t *testing.T) {
   268  	expected := helmV3_1andAbove
   269  	assertHelmVersion(t, exampleHelmV3_1VersionOutput, expected)
   270  }
   271  
   272  func TestParseHelmV3_2Version(t *testing.T) {
   273  	expected := helmV3_1andAbove
   274  	assertHelmVersion(t, exampleHelmV3_2VersionOutput, expected)
   275  }
   276  
   277  func TestParseHelmV3_3Version(t *testing.T) {
   278  	expected := helmV3_1andAbove
   279  	assertHelmVersion(t, exampleHelmV3_3VersionOutput, expected)
   280  }
   281  
   282  func TestHelmUnknownVersionError(t *testing.T) {
   283  	_, err := parseVersion("v4.1.2")
   284  	require.Error(t, err)
   285  	require.Contains(t, err.Error(), "could not parse Helm version from string")
   286  }
   287  
   288  const fileRequirementsYAML = `dependencies:
   289    - name: foobar
   290      version: 1.0.1
   291      repository: file://./foobar`
   292  
   293  func TestLocalSubchartFileDependencies(t *testing.T) {
   294  	input := []byte(fileRequirementsYAML)
   295  	expected := "./foobar"
   296  	actual, err := localSubchartDependencies(input)
   297  	if err != nil {
   298  		t.Fatal(err)
   299  	}
   300  
   301  	assert.Contains(t, actual, expected)
   302  }
   303  
   304  const remoteRequirementsYAML = `
   305  dependencies:
   306  - name: etcd
   307    version: 0.6.2
   308    repository: https://kubernetes-charts-incubator.storage.googleapis.com/
   309    condition: etcd.deployChart`
   310  
   311  func TestSubchartRemoteDependencies(t *testing.T) {
   312  	input := []byte(remoteRequirementsYAML)
   313  	actual, err := localSubchartDependencies(input)
   314  	if err != nil {
   315  		t.Fatal(err)
   316  	}
   317  
   318  	assert.Empty(t, actual)
   319  }
   320  
   321  func TestHelmReleaseName(t *testing.T) {
   322  	f := newFixture(t)
   323  
   324  	f.file("helm/Chart.yaml", `apiVersion: v1
   325  description: grafana chart
   326  name: grafana
   327  version: 0.1.0`)
   328  
   329  	f.file("helm/values.yaml", testdata.GrafanaHelmValues)
   330  	f.file("helm/templates/_helpers.tpl", testdata.GrafanaHelmHelpers)
   331  	f.file("helm/templates/service-account.yaml", testdata.GrafanaHelmServiceAccount)
   332  
   333  	f.file("Tiltfile", `
   334  k8s_yaml(helm('./helm'))
   335  `)
   336  
   337  	f.load()
   338  
   339  	manifests := f.loadResult.Manifests
   340  	require.Equal(t, 1, len(manifests))
   341  
   342  	m := manifests[0]
   343  	yaml := m.K8sTarget().YAML
   344  	assert.NotContains(t, yaml, "RELEASE-NAME")
   345  	assert.Contains(t, yaml, "name: chart-grafana")
   346  }
   347  
   348  func TestHelm3CRD(t *testing.T) {
   349  	f := newFixture(t)
   350  
   351  	f.file("helm/Chart.yaml", `apiVersion: v1
   352  description: crd chart
   353  name: crd
   354  version: 0.1.0`)
   355  
   356  	f.file("helm/templates/service-account.yaml", `apiVersion: v1
   357  kind: ServiceAccount
   358  metadata:
   359    name: crd-sa`)
   360  
   361  	// Only works in Helm3
   362  	// https://helm.sh/docs/chart_best_practices/custom_resource_definitions/
   363  	f.file("helm/crds/um.yaml", `apiVersion: tilt.dev/v1alpha1
   364  kind: UselessMachine
   365  metadata:
   366    name: bobo
   367  spec:
   368    image: bobo`)
   369  
   370  	f.file("Tiltfile", `
   371  k8s_yaml(helm('./helm'))
   372  `)
   373  
   374  	f.load()
   375  
   376  	manifests := f.loadResult.Manifests
   377  	require.Equal(t, 1, len(manifests))
   378  
   379  	m := manifests[0]
   380  	yaml := m.K8sTarget().YAML
   381  	v, err := getHelmVersion()
   382  	assert.NoError(t, err)
   383  	assert.Contains(t, yaml, "kind: ServiceAccount")
   384  	if v == helmV3_0 || v == helmV3_1andAbove {
   385  		assert.Contains(t, yaml, "kind: UselessMachine")
   386  	} else {
   387  		assert.NotContains(t, yaml, "kind: UselessMachine")
   388  	}
   389  }
   390  
   391  func assertHelmVersion(t *testing.T, versionOutput string, expectedV helmVersion) {
   392  	actualV, err := parseVersion(versionOutput)
   393  	require.NoError(t, err, "parsing helm version")
   394  	require.Equal(t, expectedV, actualV)
   395  }
   396  
   397  func TestYamlErrorFromHelm(t *testing.T) {
   398  	f := newFixture(t)
   399  	f.setupHelm()
   400  	f.file("helm/templates/foo.yaml", "hi")
   401  	f.file("Tiltfile", `
   402  k8s_yaml(helm('helm'))
   403  `)
   404  
   405  	// TODO(dmiller): there should be a better assertion here
   406  
   407  	version, err := getHelmVersion()
   408  	if err != nil {
   409  		t.Fatal(err)
   410  	}
   411  	if version == helmV2 {
   412  		f.loadErrString("from helm")
   413  	} else {
   414  		f.loadErrString("in helm")
   415  	}
   416  }
   417  
   418  func TestHelmSkipsTests(t *testing.T) {
   419  	f := newFixture(t)
   420  
   421  	f.setupHelmWithTest()
   422  	f.file("Tiltfile", `
   423  yml = helm('helm')
   424  k8s_yaml(yml)
   425  `)
   426  
   427  	f.load()
   428  
   429  	f.assertNextManifestUnresourced("chart-helloworld-chart")
   430  	f.assertConfigFiles(
   431  		"Tiltfile",
   432  		".tiltignore",
   433  		"helm",
   434  	)
   435  }
   436  
   437  // There's a major helm regression that's breaking everything
   438  // https://github.com/helm/helm/issues/6708
   439  func isBuggyHelm(t *testing.T) bool {
   440  	cmd := exec.Command("helm", "version", "-c", "--short")
   441  	out, err := cmd.Output()
   442  	if err != nil {
   443  		t.Fatalf("Error running helm: %v", err)
   444  	}
   445  
   446  	return strings.Contains(string(out), "v2.15.0")
   447  }
   448  
   449  func TestHelmIncludesRequirements(t *testing.T) {
   450  	if isBuggyHelm(t) {
   451  		t.Skipf("Helm v2.15.0 has a major regression, skipping test. See: https://github.com/helm/helm/issues/6708")
   452  	}
   453  
   454  	f := newFixture(t)
   455  
   456  	f.setupHelmWithRequirements()
   457  	f.file("Tiltfile", `
   458  yml = helm('helm')
   459  k8s_yaml(yml)
   460  `)
   461  
   462  	f.load()
   463  	f.assertNextManifest("chart-nginx-ingress-controller")
   464  }