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 }