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