New: Helm Chart v4 resource with new features and languages

Posted on

Today we’re happy to announce a new “v4” version of the Chart resource, available now in v4.13 of the Pulumi Kubernetes provider. The new kubernetes.helm.sh/v4.Chart resource is provided side-by-side with the existing kubernetes.helm.sh/v3.Chart resource. We expect to deprecate v3 in the future.

When you need to install a third-party application into your Kubernetes cluster, you’re likely to find a Helm chart for that in Artifact Hub or other registry. Pulumi provides two ways to apply a Helm chart, as outlined in Choosing the Right Helm Resource For Your Use Case. The Chart resource offers deeper integration with Pulumi and better drift remediation. v4 brings a host of new features, including enhanced SDK support across all Pulumi SDKs, full OCI registry support, improved handling of chart values, better connectivity for cluster interactions, and improved resource ordering. Let’s dig in.

What’s new

Let’s look at what’s new with Chart v4.

New language support - Java SDK & YAML SDK

The Chart v4 resource is a Pulumi Component that works consistently across all Pulumi SDKs. Earlier versions were implemented separately for each SDK, leading to various inconsistencies and limited SDK support.

Here, for example, is a simple deployment of cert-manager, a well-known Kubernetes add-on:

import * as pulumi from "@pulumi/pulumi";
import * as kubernetes from "@pulumi/kubernetes";

const certman = new kubernetes.helm.v4.Chart("cert-manager", {
    namespace: "cert-manager",
    chart: "oci://registry-1.docker.io/bitnamicharts/cert-manager",
    version: "1.3.1",
});
import pulumi
import pulumi_kubernetes as kubernetes

certman = kubernetes.helm.v4.Chart("cert-manager",
    namespace="cert-manager",
    chart="oci://registry-1.docker.io/bitnamicharts/cert-manager",
    version="1.3.1")
package main

import (
	helmv4 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v4"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := helmv4.NewChart(ctx, "cert-manager", &helmv4.ChartArgs{
			Namespace: pulumi.String("cert-manager"),
			Chart:     pulumi.String("oci://registry-1.docker.io/bitnamicharts/cert-manager"),
			Version:   pulumi.String("1.3.1"),
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Kubernetes = Pulumi.Kubernetes;

return await Deployment.RunAsync(() =>
{
    var certman = new Kubernetes.Helm.V4.Chart("cert-manager", new()
    {
        Namespace = "cert-manager",
        Chart = "oci://registry-1.docker.io/bitnamicharts/cert-manager",
        Version = "1.3.1",
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.kubernetes.helm.sh_v4.Chart;
import com.pulumi.kubernetes.helm.sh_v4.ChartArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var certman = new Chart("cert-manager", ChartArgs.builder()
            .namespace("cert-manager")
            .chart("oci://registry-1.docker.io/bitnamicharts/cert-manager")
            .version("1.3.1")
            .build());

    }
}
resources:
  cert-manager:
    type: kubernetes:helm.sh/v4:Chart
    properties:
      namespace: cert-manager
      chart: oci://registry-1.docker.io/bitnamicharts/cert-manager
      version: "1.3.1"

OCI registry support

You can use container registries with OCI support such as Docker Hub to store and share Helm chart packages. The Chart v4 resource now has full support for OCI, bringing it to parity with the Release resource.

To use an authenticated OCI registry, you must first login using helm registry login or docker login.

Chart v4 also supports the use of Helm chart repositories, and adopts the same repositoryOpts API as was introduced in the Release resource.

Lock file support

Helm has support for lock files (Chart.lock) to control a chart’s dependencies. When deploying a chart from a local directory, Pulumi automatically rebuilds the chart’s dependencies if a lock file is present. See Helm Dependency Build for details.

Better handling of chart values

Chart v4 offers new ways to work with Chart values. It is now possible to use multiple values files and to use Pulumi Assets. Of course you can also use output values from other resources as chart values.

It is also possible to set a chart value to the contents of a text file, similarly to using Helm’s --set-file argument. To do that, simply use a Pulumi Asset as a value within the values map. This is useful for injecting large values into a chart, such as additional templates as supported by the extraDeploy parameter of some Bitnami charts.

Here’s an example of using a combination of values from different sources.

import * as pulumi from "@pulumi/pulumi";
import * as kubernetes from "@pulumi/kubernetes";

const certManager = new kubernetes.helm.v4.Chart("cert-manager", {
    namespace: "cert-manager",
    chart: "oci://registry-1.docker.io/bitnamicharts/cert-manager",
    version: "1.3.1",
    valueYamlFiles: [new pulumi.asset.FileAsset("values.yaml")],
    values: {
        commonLabels: {
            "pulumi.com/stackName": pulumi.getStack(),
        },
        extraDeploy: [new pulumi.asset.FileAsset("manifest.yaml")],
    },
});
import pulumi
import pulumi_kubernetes as kubernetes

cert_manager = kubernetes.helm.v4.Chart("cert-manager",
    namespace="cert-manager",
    chart="oci://registry-1.docker.io/bitnamicharts/cert-manager",
    version="1.3.1",
    value_yaml_files=[pulumi.FileAsset("values.yaml")],
    values={
        "commonLabels": {
            "pulumi.com/stackName": pulumi.get_stack(),
        },
        "extraDeploy": [pulumi.FileAsset("manifest.yaml")],
    })
package main

import (
	helmv4 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v4"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := helmv4.NewChart(ctx, "cert-manager", &helmv4.ChartArgs{
			Namespace: pulumi.String("cert-manager"),
			Chart:     pulumi.String("oci://registry-1.docker.io/bitnamicharts/cert-manager"),
			Version:   pulumi.String("1.3.1"),
			ValueYamlFiles: pulumi.AssetOrArchiveArray{
				pulumi.NewFileAsset("values.yaml"),
			},
			Values: pulumi.Map{
				"commonLabels": pulumi.Any(map[string]interface{}{
					"pulumi.com/stackName": ctx.Stack(),
				}),
				"extraDeploy": pulumi.Array{
					pulumi.NewFileAsset("manifest.yaml"),
				},
			},
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Kubernetes = Pulumi.Kubernetes;

return await Deployment.RunAsync(() =>
{
    var certManager = new Kubernetes.Helm.V4.Chart("cert-manager", new()
    {
        Namespace = "cert-manager",
        Chart = "oci://registry-1.docker.io/bitnamicharts/cert-manager",
        Version = "1.3.1",
        ValueYamlFiles = new[]
        {
            new FileAsset("values.yaml"),
        },
        Values =
        {
            { "commonLabels", new Dictionary<string, object?>
            {
                ["pulumi.com/stackName"] = Deployment.Instance.StackName,
            } },
            { "extraDeploy", new[]
            {
                new FileAsset("manifest.yaml"),
            } },
        },
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.deployment.Deployment;
import com.pulumi.kubernetes.helm.v4.Chart;
import com.pulumi.kubernetes.helm.v4.ChartArgs;
import com.pulumi.asset.FileAsset;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var certManager = new Chart("cert-manager", ChartArgs.builder()
            .namespace("cert-manager")
            .chart("oci://registry-1.docker.io/bitnamicharts/cert-manager")
            .version("1.3.1")
            .valueYamlFiles(new FileAsset("values.yaml"))
            .values(Map.ofEntries(
                Map.entry("commonLabels", Map.of("pulumi.com/stackName", Deployment.getInstance().getStackName())),
                Map.entry("extraDeploy", List.of(new FileAsset("manifest.yaml")))
            ))
            .build());

    }
}
resources:
  cert-manager:
    type: kubernetes:helm.sh/v4:Chart
    properties:
      namespace: cert-manager
      chart: oci://registry-1.docker.io/bitnamicharts/cert-manager
      version: "1.3.1"
      valueYamlFiles:
      - fn::fileAsset: values.yaml
      values:
        commonLabels:
          pulumi.com/stackName: ${pulumi.stack}
        extraDeploy:
        - fn::fileAsset: manifest.yaml

Better connectivity

You may now use charts that use chart functions requiring a connection to the cluster, e.g. to:

Note that the lookup function is executed in both the preview and the non-preview mode, and keep in mind that the expected object may not exist during a preview.

Improved resource ordering

It’s now easy to wait for a chart’s resources to be installed before installing other resources, simply by using the dependsOn option. In earlier versions, we relied on a ready output property.

The Chart resource automatically detects dependencies between resources in the manifest(s). For example, it knows to install namespaces and Custom Resource Definitions (CRDs) first.

Use the config.kubernetes.io/depends-on annotation to declare an explicit resource dependency. The annotation accepts a list of resource references, delimited by commas.

It consists of the group, kind, name, and optionally the namespace, delimited by forward slashes.

Resource ScopeFormat
namespace-scoped<group>/namespaces/<namespace>/<kind>/<name>
cluster-scoped<group>/<kind>/<name>

For resources in the “core” group, the empty string is used instead (for example: /namespaces/test/Pod/pod-a).

New-style Pulumi transformations

Pulumi has a new way to transform component resources and their children, the transforms options. The older transformations option doesn’t work with multi-language components like Chart v4. See Resource Option: transforms for more details.

Note: you cannot change an object’s namespace or name using a Pulumi transformation, and you cannot add or discard an object.

Here’s an example of using the transforms option to add the pulumi.com/patchForce annotation to a chart’s resources.

const applyPatchForceAnnotation = async (args: pulumi.ResourceTransformArgs) => {
    switch(args.type) {
        case "kubernetes:helm.sh/v4:Chart":
            break;
        default:
            args.props.metadata.annotations = {
                "pulumi.com/patchForce": "true",
                ...args.props.metadata.annotations
            }
    }
    return {
        props: args.props,
        opts: args.opts,
    };
};

const ingressController = new kubernetes.helm.v4.Chart("ingresscontroller", {
    chart: "nginx-ingress",
    namespace: ingressNs.metadata.name,
    repositoryOpts: {
        repo: "https://helm.nginx.com/stable",
    },
    version: "0.14.1",
}, {transforms: [applyPatchForceAnnotation]});
def apply_patchforce_annotation(args: ResourceTransformArgs):
    if not args.type_ == "kubernetes:helm.sh/v4:Chart":
        if not 'metadata' in args.props:
            args.props['metadata'] = {}
        if not 'annotations' in args.props['metadata']:
            args.props['metadata']['annotations'] = {}
        args.props['metadata']['annotations']['pulumi.com/patchForce'] = 'true'

    return ResourceTransformResult(
        props=args.props,
        opts=args.opts)

ingresscontroller = kubernetes.helm.v4.Chart(
    "ingresscontroller",
    chart="nginx-ingress",
    namespace=ingress_ns.metadata.name,
    repository_opts=kubernetes.helm.v3.RepositoryOptsArgs(
        repo="https://helm.nginx.com/stable",
    ),
    version="0.14.1",
    opts=pulumi.ResourceOptions(transforms=[apply_patchforce_annotation])
)
applyPatchForceAnnotation := func(ctx context.Context, rta *pulumi.ResourceTransformArgs) *pulumi.ResourceTransformResult {
	transform := func(applier interface{}) {
		o := rta.Props.ToMapOutputWithContext(ctx).ApplyT(applier)
		r, err := internals.UnsafeAwaitOutput(ctx, o)
		if err != nil {
			panic(err)
		}
		rta.Props = r.Value.(pulumi.Map)
	}

	switch rta.Type {
	case "kubernetes:helm.sh/v4:Chart":
		// Do nothing for Helm charts
	default:
		transform(func(obj map[string]any) pulumi.Map {
			// note: obj is an ordinary Unstructured object at this point.
			unstructured.SetNestedField(obj, "true", "metadata", "annotations", "pulumi.com/patchForce")
			return pulumi.ToMap(obj)
		})
	}
	return &pulumi.ResourceTransformResult{
		Props: rta.Props,
		Opts:  rta.Opts,
	}
}

// Use Helm to install the Nginx ingress controller
_, err = helmv4.NewChart(ctx, "ingresscontroller", &helmv4.ChartArgs{
	Chart:     pulumi.String("nginx-ingress"),
	Namespace: ingressNs.Metadata.Name(),
	RepositoryOpts: &helmv4.RepositoryOptsArgs{
		Repo: pulumi.String("https://helm.nginx.com/stable"),
	},
	Version: pulumi.String("0.14.1"),
}, pulumi.Transforms([]pulumi.ResourceTransform{applyPatchForceAnnotation}))
if err != nil {
	return err
}

Post-rendering support

New to v4 is support for a post-rendering command, with optional arguments, to be applied to the rendered manifest. See Advanced Helm Techniques: Post Rendering for details.

“Keep” policy

The Chart v4 resource now understands Helm resource policies, specifically “keep” which instructs Pulumi not to delete a given object when the resource is destroyed. Simply apply the helm.sh/resource-policy: keep annotation to the object. See Tell Helm Not To Uninstall a Resource for details.

Release name

Charts typically generate object names based on the Helm release name. By default, Chart v4 uses its own resource name as the release name. To customize the release name, set the name property on the Chart.

Some charts also accept a fullnameOverride value to control object naming, use that if possible.

Limitations

Not supported: Kubernetes transformations

Chart v4 does not support the transformations argument as seen in Chart v3, that facilitates a Kubernetes-centric transformation and/or discarding of objects from the rendered manifest.

One alternative is to use use Pulumi transformations to transform the object and resource options. Another is to use post-rendering, which we’ll cover next.

Not supported: Helm hooks

Some charts use hooks to perform pre- and post-installation tasks, and we recommend using the Release resource to install such charts. Another option is to emulate the hook by performing the task directly, using other Pulumi resources such as kubernetes.batch/v1.Job and kubernetes.core/v1.Secret.

Note that Chart v3 does not support hooks either but, for historical reasons, does emit the hooks as ordinary child resources.

Evolving: Resource outputs

The child resources created by the chart are presented to your program via the resources output, as an array of Pulumi Resource objects. In some SDKs, you can cast the resource into the appropriate resource type, e.g. corev1.ConfigMap. Not all SDKs support this (yet); see pulumi/pulumi#15788.

Example: Argo CD

Here’s a real-world example of installing Argo CD into a Kubernetes cluster, and of using Argo CD’s Application custom resource to deploy the ‘guestbook’ example.

import * as pulumi from "@pulumi/pulumi";
import * as kubernetes from "@pulumi/kubernetes";
import * as random from "@pulumi/random";

const ns = new kubernetes.core.v1.Namespace("ns", {metadata: {
    name: "argocd",
}});
const redisPasswordResource = new random.RandomPassword("redis-password", {length: 16});
const redisSecret = new kubernetes.core.v1.Secret("redis-secret", {
    metadata: {
        name: "argocd-redis",
        namespace: ns.metadata.apply(metadata => metadata.name),
    },
    type: "Opaque",
    stringData: {
        auth: redisPasswordResource.result,
    },
});
const argocd = new kubernetes.helm.v4.Chart("argocd", {
    chart: "argo-cd",
    version: "6.11.1",
    namespace: ns.metadata.apply(metadata => metadata.name),
    repositoryOpts: {
        repo: "https://argoproj.github.io/argo-helm",
    },
    values: {
        fullnameOverride: "",
    },
});
const guestbook = new kubernetes.yaml.v2.ConfigGroup("guestbook", {objs: [{
    apiVersion: "argoproj.io/v1alpha1",
    kind: "Application",
    metadata: {
        name: "guestbook",
        namespace: ns.metadata.apply(metadata => metadata.name),
    },
    spec: {
        project: "default",
        source: {
            repoURL: "https://github.com/argoproj/argocd-example-apps.git",
            targetRevision: "HEAD",
            path: "guestbook",
        },
        destination: {
            server: "https://kubernetes.default.svc",
            namespace: "default",
        },
    },
}]}, {
    dependsOn: [argocd],
});
export const redisPassword = redisPasswordResource.result;
import pulumi
import pulumi_kubernetes as kubernetes
import pulumi_random as random

ns = kubernetes.core.v1.Namespace("ns", metadata=kubernetes.meta.v1.ObjectMetaArgs(
    name="argocd",
))
redis_password_resource = random.RandomPassword("redis-password", length=16)
redis_secret = kubernetes.core.v1.Secret("redis-secret",
    metadata=kubernetes.meta.v1.ObjectMetaArgs(
        name="argocd-redis",
        namespace=ns.metadata.name,
    ),
    type="Opaque",
    string_data={
        "auth": redis_password_resource.result,
    })
argocd = kubernetes.helm.v4.Chart("argocd",
    chart="argo-cd",
    version="6.11.1",
    namespace=ns.metadata.name,
    repository_opts=kubernetes.helm.v4.RepositoryOptsArgs(
        repo="https://argoproj.github.io/argo-helm",
    ),
    values={
        "fullnameOverride": "",
    })
guestbook = kubernetes.yaml.v2.ConfigGroup("guestbook", objs=[{
    "apiVersion": "argoproj.io/v1alpha1",
    "kind": "Application",
    "metadata": {
        "name": "guestbook",
        "namespace": ns.metadata.name,
    },
    "spec": {
        "project": "default",
        "source": {
            "repoURL": "https://github.com/argoproj/argocd-example-apps.git",
            "targetRevision": "HEAD",
            "path": "guestbook",
        },
        "destination": {
            "server": "https://kubernetes.default.svc",
            "namespace": "default",
        },
    },
}],
opts=pulumi.ResourceOptions(depends_on=[argocd]))
pulumi.export("redisPassword", redis_password_resource.result)
package main

import (
	corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1"
	helmv4 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v4"
	metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/meta/v1"
	yamlv2 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/yaml/v2"
	"github.com/pulumi/pulumi-random/sdk/v4/go/random"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		ns, err := corev1.NewNamespace(ctx, "ns", &corev1.NamespaceArgs{
			Metadata: &metav1.ObjectMetaArgs{
				Name: pulumi.String("argocd"),
			},
		})
		if err != nil {
			return err
		}
		redisPasswordResource, err := random.NewRandomPassword(ctx, "redis-password", &random.RandomPasswordArgs{
			Length: pulumi.Int(16),
		})
		if err != nil {
			return err
		}
		_, err = corev1.NewSecret(ctx, "redis-secret", &corev1.SecretArgs{
			Metadata: &metav1.ObjectMetaArgs{
				Name: pulumi.String("argocd-redis"),
				Namespace: ns.Metadata.ApplyT(func(metadata metav1.ObjectMeta) (*string, error) {
					return metadata.Name, nil
				}).(pulumi.StringPtrOutput),
			},
			Type: pulumi.String("Opaque"),
			StringData: pulumi.StringMap{
				"auth": redisPasswordResource.Result,
			},
		})
		if err != nil {
			return err
		}
		argocd, err := helmv4.NewChart(ctx, "argocd", &helmv4.ChartArgs{
			Chart:   pulumi.String("argo-cd"),
			Version: pulumi.String("6.11.1"),
			Namespace: ns.Metadata.ApplyT(func(metadata metav1.ObjectMeta) (*string, error) {
				return metadata.Name, nil
			}).(pulumi.StringPtrOutput),
			RepositoryOpts: &helmv4.RepositoryOptsArgs{
				Repo: pulumi.String("https://argoproj.github.io/argo-helm"),
			},
			Values: pulumi.Map{
				"fullnameOverride": pulumi.Any(""),
			},
		})
		if err != nil {
			return err
		}
		_, err = yamlv2.NewConfigGroup(ctx, "guestbook", &yamlv2.ConfigGroupArgs{
			Objs: pulumi.Array{
				pulumi.Any(map[string]interface{}{
					"apiVersion": "argoproj.io/v1alpha1",
					"kind":       "Application",
					"metadata": map[string]interface{}{
						"name": "guestbook",
						"namespace": ns.Metadata.ApplyT(func(metadata metav1.ObjectMeta) (*string, error) {
							return metadata.Name, nil
						}).(pulumi.StringPtrOutput),
					},
					"spec": map[string]interface{}{
						"project": "default",
						"source": map[string]interface{}{
							"repoURL":        "https://github.com/argoproj/argocd-example-apps.git",
							"targetRevision": "HEAD",
							"path":           "guestbook",
						},
						"destination": map[string]interface{}{
							"server":    "https://kubernetes.default.svc",
							"namespace": "default",
						},
					},
				}),
			},
		}, pulumi.DependsOn([]pulumi.Resource{
			argocd,
		}))
		if err != nil {
			return err
		}
		ctx.Export("redisPassword", redisPasswordResource.Result)
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Kubernetes = Pulumi.Kubernetes;
using Random = Pulumi.Random;

return await Deployment.RunAsync(() =>
{
    var ns = new Kubernetes.Core.V1.Namespace("ns", new()
    {
        Metadata = new Kubernetes.Types.Inputs.Meta.V1.ObjectMetaArgs
        {
            Name = "argocd",
        },
    });

    var redisPasswordResource = new Random.RandomPassword("redis-password", new()
    {
        Length = 16,
    });

    var redisSecret = new Kubernetes.Core.V1.Secret("redis-secret", new()
    {
        Metadata = new Kubernetes.Types.Inputs.Meta.V1.ObjectMetaArgs
        {
            Name = "argocd-redis",
            Namespace = ns.Metadata.Apply(metadata => metadata.Name),
        },
        Type = "Opaque",
        StringData =
        {
            { "auth", redisPasswordResource.Result },
        },
    });

    var argocd = new Kubernetes.Helm.V4.Chart("argocd", new()
    {
        Chart = "argo-cd",
        Version = "6.11.1",
        Namespace = ns.Metadata.Apply(metadata => metadata.Name),
        RepositoryOpts = new Kubernetes.Types.Inputs.Helm.V4.RepositoryOptsArgs
        {
            Repo = "https://argoproj.github.io/argo-helm",
        },
        Values =
        {
            { "fullnameOverride", "" },
        },
    });

    var guestbook = new Kubernetes.Yaml.V2.ConfigGroup("guestbook", new()
    {
        Objs = new[]
        {
            new Dictionary<string, object?>
            {
                ["apiVersion"] = "argoproj.io/v1alpha1",
                ["kind"] = "Application",
                ["metadata"] = new Dictionary<string, object?>
                {
                    ["name"] = "guestbook",
                    ["namespace"] = ns.Metadata.Apply(metadata => metadata.Name),
                },
                ["spec"] = new Dictionary<string, object?>
                {
                    ["project"] = "default",
                    ["source"] = new Dictionary<string, object?>
                    {
                        ["repoURL"] = "https://github.com/argoproj/argocd-example-apps.git",
                        ["targetRevision"] = "HEAD",
                        ["path"] = "guestbook",
                    },
                    ["destination"] = new Dictionary<string, object?>
                    {
                        ["server"] = "https://kubernetes.default.svc",
                        ["namespace"] = "default",
                    },
                },
            },
        },
    }, new ComponentResourceOptions
    {
        DependsOn =
        {
            argocd,
        },
    });

    return new Dictionary<string, object?>
    {
        ["redisPassword"] = redisPasswordResource.Result,
    };
});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.kubernetes.core.v1.Namespace;
import com.pulumi.kubernetes.core.v1.NamespaceArgs;
import com.pulumi.kubernetes.meta.v1.inputs.ObjectMetaArgs;
import com.pulumi.random.RandomPassword;
import com.pulumi.random.RandomPasswordArgs;
import com.pulumi.kubernetes.core.v1.Secret;
import com.pulumi.kubernetes.core.v1.SecretArgs;
import com.pulumi.kubernetes.helm.v4.Chart;
import com.pulumi.kubernetes.helm.v4.ChartArgs;
import com.pulumi.kubernetes.helm.v4.inputs.RepositoryOptsArgs;
import com.pulumi.kubernetes.yaml.v2.ConfigGroup;
import com.pulumi.kubernetes.yaml.v2.ConfigGroupArgs;
import com.pulumi.resources.ComponentResourceOptions;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var ns = new Namespace("ns", NamespaceArgs.builder()
            .metadata(ObjectMetaArgs.builder()
                .name("argocd")
                .build())
            .build());

        var redisPasswordResource = new RandomPassword("redisPasswordResource", RandomPasswordArgs.builder()
            .length(16)
            .build());

        var redisSecret = new Secret("redisSecret", SecretArgs.builder()
            .metadata(ObjectMetaArgs.builder()
                .name("argocd-redis")
                .namespace(ns.metadata().applyValue(metadata -> metadata.name().get()))
                .build())
            .type("Opaque")
            .stringData(redisPasswordResource.result().applyValue(password -> Map.of("auth", password)))
            .build());

        var argocd = new Chart("argocd", ChartArgs.builder()
            .chart("argo-cd")
            .version("6.11.1")
            .namespace(ns.metadata().applyValue(metadata -> metadata.name().get()))
            .repositoryOpts(RepositoryOptsArgs.builder()
                .repo("https://argoproj.github.io/argo-helm")
                .build())
            .values(Map.of("fullnameOverride", ""))
            .build());

        var guestbook = new ConfigGroup("guestbook", ConfigGroupArgs.builder()
            .objs(Map.ofEntries(
                Map.entry("apiVersion", "argoproj.io/v1alpha1"),
                Map.entry("kind", "Application"),
                Map.entry("metadata", Map.ofEntries(
                    Map.entry("name", "guestbook"),
                    Map.entry("namespace", ns.metadata().applyValue(metadata -> metadata.name().get()))
                )),
                Map.entry("spec", Map.ofEntries(
                    Map.entry("project", "default"),
                    Map.entry("source", Map.ofEntries(
                        Map.entry("repoURL", "https://github.com/argoproj/argocd-example-apps.git"),
                        Map.entry("targetRevision", "HEAD"),
                        Map.entry("path", "guestbook")
                    )),
                    Map.entry("destination", Map.ofEntries(
                        Map.entry("server", "https://kubernetes.default.svc"),
                        Map.entry("namespace", "default")
                    ))
                ))
            ))
            .build(), ComponentResourceOptions.builder()
                .dependsOn(argocd)
                .build());

        ctx.export("redisPassword", redisPasswordResource.result());
    }
}
resources:
  ns:
    type: kubernetes:core/v1:Namespace
    properties:
      metadata:
        name: argocd

  redis-password:
    type: random:RandomPassword
    properties:
      length: 16

  redis-secret:
    type: kubernetes:core/v1:Secret
    properties:
      metadata:
        name: argocd-redis
        namespace: ${ns.metadata.name}
      type: Opaque
      stringData:
        auth: ${redis-password.result}
    options:
      retainOnDelete: true

  argocd:
    type: kubernetes:helm.sh/v4:Chart
    properties:
      chart: argo-cd
      version: "6.11.1"
      namespace: ${ns.metadata.name}
      repositoryOpts:
        repo: https://argoproj.github.io/argo-helm
      values:
        fullnameOverride: ""

  guestbook:
    type: kubernetes:yaml/v2:ConfigGroup
    properties:
      objs:
      - apiVersion: argoproj.io/v1alpha1
        kind: Application
        metadata:
          name: guestbook
          namespace: ${ns.metadata.name}
        spec:
          project: default
          source:
            repoURL: https://github.com/argoproj/argocd-example-apps.git
            targetRevision: HEAD
            path: guestbook
          destination:
            server: https://kubernetes.default.svc
            namespace: default
    options:
      dependsOn:
      - ${argocd}

outputs:
  redisPassword: ${redis-password.result}

The program creates the argocd namespace, installs the ArgoCD server, and then creates an ArgoCD Application resource. Observe how the program installs and uses a Custom Resource Definition (CRD) successfully, and uses dependsOn to ensure that the CRD is installed before using it.

The argo-cd chart normally makes use of a Helm hook to initialize a password for the redis server. Since the Chart v4 resource doesn’t support Helm hooks, this program creates the password directly.

Conclusion

Pulumi loves empowering developers to use the best tools for the job, and we recommend using Helm charts to install third-party Kubernetes applications. Pulumi complements Helm by handling the cloud resources that are often required, such as an IAM Role or cloud storage bucket. Such combinations make for great reusable componentry.

Check out the following links to learn more about Pulumi Kubernetes today!