diff --git a/modules/helm/template.go b/modules/helm/template.go index f6b3624cf..a9070e12d 100644 --- a/modules/helm/template.go +++ b/modules/helm/template.go @@ -72,6 +72,44 @@ func RenderTemplateE(t testing.TestingT, options *Options, chartDir string, rele return RunHelmCommandAndGetStdOutE(t, options, "template", args...) } +// RenderTemplate runs `helm template` to render a *remote* chart given the provided options and returns stdout/stderr from +// the template command. If you pass in templateFiles, this will only render those templates. This function will fail +// the test if there is an error rendering the template. +func RenderRemoteTemplate(t testing.TestingT, options *Options, chartURL string, releaseName string, templateFiles []string, extraHelmArgs ...string) string { + out, err := RenderRemoteTemplateE(t, options, chartURL, releaseName, templateFiles, extraHelmArgs...) + require.NoError(t, err) + return out +} + +// RenderTemplate runs `helm template` to render a *remote* helm chart given the provided options and returns stdout/stderr from +// the template command. If you pass in templateFiles, this will only render those templates. +func RenderRemoteTemplateE(t testing.TestingT, options *Options, chartURL string, releaseName string, templateFiles []string, extraHelmArgs ...string) (string, error) { + // Now construct the args + // We first construct the template args + args := []string{} + if options.KubectlOptions != nil && options.KubectlOptions.Namespace != "" { + args = append(args, "--namespace", options.KubectlOptions.Namespace) + } + args, err := getValuesArgsE(t, options, args...) + if err != nil { + return "", err + } + for _, templateFile := range templateFiles { + // As the helm command fails if a non valid template is given as input + // we do not check if the template file exists or not as we do for local charts + // as it would add unecessary networking calls + args = append(args, "--show-only", templateFile) + } + // deal extraHelmArgs + args = append(args, extraHelmArgs...) + + // ... and add the helm chart name, the remote repo and chart URL at the end + args = append(args, releaseName, "--repo", chartURL) + + // Finally, call out to helm template command + return RunHelmCommandAndGetStdOutE(t, options, "template", args...) +} + // UnmarshalK8SYaml is the same as UnmarshalK8SYamlE, but will fail the test if there is an error. func UnmarshalK8SYaml(t testing.TestingT, yamlData string, destinationObj interface{}) { require.NoError(t, UnmarshalK8SYamlE(t, yamlData, destinationObj)) diff --git a/modules/helm/template_test.go b/modules/helm/template_test.go new file mode 100644 index 000000000..1c212099b --- /dev/null +++ b/modules/helm/template_test.go @@ -0,0 +1,67 @@ +//go:build kubeall || helm +// +build kubeall helm + +// NOTE: we have build tags to differentiate kubernetes tests from non-kubernetes tests, and further differentiate helm +// tests. This is done because minikube is heavy and can interfere with docker related tests in terratest. Similarly, +// helm can overload the minikube system and thus interfere with the other kubernetes tests. To avoid overloading the +// system, we run the kubernetes tests and helm tests separately from the others. + +package helm + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + + "github.com/gruntwork-io/terratest/modules/k8s" + "github.com/gruntwork-io/terratest/modules/random" +) + +// Test that we can render locally a remote chart (e.g bitnami/nginx) +func TestRemoteChartRender(t *testing.T) { + const ( + remoteChartSource = "https://charts.bitnami.com/bitnami" + remoteChartName = "nginx" + remoteChartVersion = "13.2.23" + ) + + t.Parallel() + + namespaceName := fmt.Sprintf( + "%s-%s", + strings.ToLower(t.Name()), + strings.ToLower(random.UniqueId()), + ) + + releaseName := remoteChartName + + options := &Options{ + SetValues: map[string]string{ + "image.repository": remoteChartName, + "image.registry": "", + "image.tag": remoteChartVersion, + }, + KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName), + } + + // Run RenderTemplate to render the template and capture the output. Note that we use the version without `E`, since + // we want to assert that the template renders without any errors. + output := RenderRemoteTemplate(t, options, remoteChartSource, releaseName, []string{"templates/deployment.yaml"}) + + // Now we use kubernetes/client-go library to render the template output into the Deployment struct. This will + // ensure the Deployment resource is rendered correctly. + var deployment appsv1.Deployment + UnmarshalK8SYaml(t, output, &deployment) + + // Verify the namespace matches the expected supplied namespace. + require.Equal(t, namespaceName, deployment.Namespace) + + // Finally, we verify the deployment pod template spec is set to the expected container image value + expectedContainerImage := remoteChartName + ":" + remoteChartVersion + deploymentContainers := deployment.Spec.Template.Spec.Containers + require.Equal(t, len(deploymentContainers), 1) + require.Equal(t, deploymentContainers[0].Image, expectedContainerImage) +} diff --git a/test/helm_keda_remote_example_template_test.go b/test/helm_keda_remote_example_template_test.go new file mode 100644 index 000000000..dbf6bbc59 --- /dev/null +++ b/test/helm_keda_remote_example_template_test.go @@ -0,0 +1,70 @@ +//go:build kubeall || helm +// +build kubeall helm + +// **NOTE**: we have build tags to differentiate kubernetes tests from non-kubernetes tests, and further differentiate helm +// tests. This is done because minikube is heavy and can interfere with docker related tests in terratest. Similarly, helm +// can overload the minikube system and thus interfere with the other kubernetes tests. Specifically, many of the tests +// start to fail with `connection refused` errors from `minikube`. To avoid overloading the system, we run the kubernetes +// tests and helm tests separately from the others. This may not be necessary if you have a sufficiently powerful machine. +// We recommend at least 4 cores and 16GB of RAM if you want to run all the tests together. + +package test + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + + "github.com/gruntwork-io/terratest/modules/helm" + "github.com/gruntwork-io/terratest/modules/k8s" + "github.com/gruntwork-io/terratest/modules/logger" + "github.com/gruntwork-io/terratest/modules/random" +) + +// This file contains an example of how to use terratest to test *remote* helm chart template logic by rendering the templates +// using `helm template`, and then reading in the rendered templates. +// - TestHelmKedaRemoteExampleTemplateRenderedDeployment: An example of how to read in the rendered object and check the +// computed values. + +// An example of how to verify the rendered template object of a Helm Chart given various inputs. +func TestHelmKedaRemoteExampleTemplateRenderedDeployment(t *testing.T) { + t.Parallel() + + // chart name + releaseName := "keda" + + // Set up the namespace; confirm that the template renders the expected value for the namespace. + namespaceName := "medieval-" + strings.ToLower(random.UniqueId()) + logger.Logf(t, "Namespace: %s\n", namespaceName) + + // Setup the args. For this test, we will set the following input values: + options := &helm.Options{ + SetValues: map[string]string{ + "metricsServer.replicaCount": "999", + "resources.metricServer.limits.memory": "1234Mi", + }, + KubectlOptions: k8s.NewKubectlOptions("", "", namespaceName), + } + + // Run RenderTemplate to render the *remote* template and capture the output. Note that we use the version without `E`, since + // we want to assert that the template renders without any errors. + // Additionally, we path a the templateFile for which we are setting test values to + // demonstrate how to select individual templates to render. + output := helm.RenderRemoteTemplate(t, options, "https://kedacore.github.io/charts", releaseName, []string{"templates/metrics-server/deployment.yaml"}) + + // Now we use kubernetes/client-go library to render the template output into the Deployment struct. This will + // ensure the Deployment resource is rendered correctly. + var deployment appsv1.Deployment + helm.UnmarshalK8SYaml(t, output, &deployment) + + // Verify the namespace matches the expected supplied namespace. + require.Equal(t, namespaceName, deployment.Namespace) + + // Finally, we verify the deployment pod template spec is set to the expected container image value + var expectedMetricsServerReplica int32 + expectedMetricsServerReplica = 999 + deploymentMetricsServerReplica := *deployment.Spec.Replicas + require.Equal(t, expectedMetricsServerReplica, deploymentMetricsServerReplica) +}