pulumi-kubernetes: Can't install Prometheus Operator via Helm3: Duplicate resource URN

Problem description

Using Pulumi 2.7.1, I’ve been trying to install the prometheus-operator Helm chart from the stable repo, version 9.2.2.

I’m using the following Pulumi definition to do it:

public static void DeployPrometheus(CustomResourceOptions k8sCustomResourceOptions)
{
    var appName = "prometheus";
    var appLabels = new InputMap<string>
    {
        {"app", appName}
    };

    /**
     * Enable Prometheus data storage on a persistent volume claim
     */
    // See https://docs.microsoft.com/en-us/azure/aks/azure-disks-dynamic-pv for more details on provisioning
    var prometheusVolumeClaim = new Pulumi.Kubernetes.Core.V1.PersistentVolumeClaim("promtheus-volume-claim", new PersistentVolumeClaimArgs()
    {
        Metadata = new ObjectMetaArgs
        {
            Name = $"prometheus",
        },
        ApiVersion = "v1",
        Spec = new PersistentVolumeClaimSpecArgs
        {
            AccessModes = "ReadWriteOnce", // can only be mounted and written to by a single 
            StorageClassName = "default", // standard disk,
            Resources = new ResourceRequirementsArgs
            {
                Requests =
                {
                    // TODO: make storage amounts configurable
                    { "storage", "15Gi"} // 15Gb of storage
                }
            }
        }
    }, k8sCustomResourceOptions);

    // need to work around CRD bug here https://github.com/pulumi/pulumi-kubernetes/issues/1023
    //var configGroup = new ConfigGroup("prometheus-crds", new ConfigGroupArgs
    //{
    //    Files = new[]{ "https://raw.githubusercontent.com/coreos/prometheus-operator/release-0.38/example/prometheus-operator-crd/monitoring.coreos.com_alertmanagers.yaml",
    //    "https://raw.githubusercontent.com/coreos/prometheus-operator/release-0.38/example/prometheus-operator-crd/monitoring.coreos.com_podmonitors.yaml",
    //    "https://raw.githubusercontent.com/coreos/prometheus-operator/release-0.38/example/prometheus-operator-crd/monitoring.coreos.com_prometheuses.yaml",
    //    "https://raw.githubusercontent.com/coreos/prometheus-operator/release-0.38/example/prometheus-operator-crd/monitoring.coreos.com_prometheusrules.yaml",
    //    "https://raw.githubusercontent.com/coreos/prometheus-operator/release-0.38/example/prometheus-operator-crd/monitoring.coreos.com_servicemonitors.yaml",
    //    "https://raw.githubusercontent.com/coreos/prometheus-operator/release-0.38/example/prometheus-operator-crd/monitoring.coreos.com_thanosrulers.yaml"},
    //}, new ComponentResourceOptions()
    //{
    //    Provider = k8sCustomResourceOptions.Provider
    //});

    var prometheusChartValues = prometheusVolumeClaim.Metadata.Apply(f => new Dictionary<string, object>()
    {
        /* define Kubelet metrics scraping */
        ["kubelet"] = new Dictionary<string, object>
        {
            ["enabled"] = true,
            ["serviceMonitor"] = new Dictionary<string, object>
            {
                ["https"] = false
            },
        },
        ["kubeControllerManager"] = new Dictionary<string, object>
        {
            ["enabled"] = false
        },
        ["kubeScheduler"] = new Dictionary<string, object>
        {
            ["enabled"] = false
        },
        ["kubeEtcd"] = new Dictionary<string, object>
        {
            ["enabled"] = false
        },
        ["kubeProxy"] = new Dictionary<string, object>
        {
            ["enabled"] = false
        },
        ["prometheusOperator"] = new 
        {
            createCustomResource = false
        },
        ["prometheus"] = new Dictionary<string, object>
        {
            ["prometheusSpec"] = new Dictionary<string, object>
            {
                ["volumes"] = new[]
                {
                    new Dictionary<string, object>
                    {
                        ["name"] = "prometheus-storage",
                        ["persistentVolumeClaim"] = new Dictionary<string, object>{
                            ["claimName"] = f.Name
                        },
                    }
                },
                ["volumeMounts"] = new[] {
                    new Dictionary<string, object>
                    {
                        ["name"] = "prometheus-storage",
                        ["mountPath"] = "/prometheus/"
                    }
                }
            }
        },
    });

    var objectNames = new ConcurrentBag<string>();

    ImmutableDictionary<string, object> OmitCustomResource(ImmutableDictionary<string, object> obj, CustomResourceOptions opts)
    {
        var metadata = (ImmutableDictionary<string, object>)obj["metadata"];
        var resourceName = $"{obj["kind"]}.{metadata["name"]}";

        if (objectNames.Contains(resourceName))
        {
            Console.WriteLine($"Deduplicated [{resourceName}]");
            return new Dictionary<string, object>
            {
                ["apiVersion"] = "v1",
                ["kind"] = "List",
                ["items"] = new Dictionary<string, object>(),
            }.ToImmutableDictionary();
        }
        objectNames.Add(resourceName);
        Console.WriteLine(resourceName);
        return obj;
    }

    var prometheusOperatorChart = new Chart("pm", new Pulumi.Kubernetes.Helm.ChartArgs
    {
        Repo = "stable",
        Chart = "prometheus-operator",
        Version = "9.2.2",
        Namespace = "monitoring",
        Values = prometheusChartValues,
        Transformations = { OmitCustomResource }
    }, new ComponentResourceOptions()
    {
        Provider = k8sCustomResourceOptions.Provider,
        //DependsOn = configGroup
    });
}

I’m using the transformation mechanism for the Helm chart to attempt to resolve the error

Errors & Logs

I get a similar error to this one each time I attempt to run this chart installation:

 error: Duplicate resource URN 'urn:pulumi:dev::sdkbin-resources::kubernetes:helm.sh/v2:Chart$kubernetes:apiextensions.k8s.io/v1:CustomResourceDefinition::monitoring/pm-prometheus-operator-alertmanager'; try giving it a unique name

It’s always one of the custom resource definitions - it’s not always the same one that throws the error though. This time it was the alertmanager CRD that caused the error - other times it’s been the prometheus CRD or the node-exporter.

I attempted to fix this issue by adding the OmitCustomResource filtering code to my application to omit any duplicates - I also added some logging to see what those filtered / unfiltered outputs were:

  1. CustomResourceDefinition.alertmanagers.monitoring.coreos.com
  2. CustomResourceDefinition.podmonitors.monitoring.coreos.com
  3. CustomResourceDefinition.prometheuses.monitoring.coreos.com
  4. CustomResourceDefinition.prometheusrules.monitoring.coreos.com
  5. CustomResourceDefinition.servicemonitors.monitoring.coreos.com
  6. CustomResourceDefinition.thanosrulers.monitoring.coreos.com
  7. PodSecurityPolicy.pm-grafana
  8. PodSecurityPolicy.pm-grafana-test
  9. PodSecurityPolicy.pm-kube-state-metrics
  10. PodSecurityPolicy.pm-prometheus-node-exporter
  11. PodSecurityPolicy.pm-prometheus-operator-alertmanager
  12. PodSecurityPolicy.pm-prometheus-operator-operator
  13. PodSecurityPolicy.pm-prometheus-operator-prometheus
  14. ServiceAccount.pm-grafana
  15. ServiceAccount.pm-grafana-test
  16. ServiceAccount.pm-kube-state-metrics
  17. ServiceAccount.pm-prometheus-node-exporter
  18. ServiceAccount.pm-prometheus-operator-alertmanager
  19. ServiceAccount.pm-prometheus-operator-operator
  20. ServiceAccount.pm-prometheus-operator-prometheus
  21. Secret.pm-grafana
  22. Secret.alertmanager-pm-prometheus-operator-alertmanager
  23. ConfigMap.pm-grafana-config-dashboards
  24. ConfigMap.pm-grafana
  25. ConfigMap.pm-grafana-test
  26. ConfigMap.pm-prometheus-operator-grafana-datasource
  27. ConfigMap.pm-prometheus-operator-apiserver
  28. ConfigMap.pm-prometheus-operator-cluster-total
  29. ConfigMap.pm-prometheus-operator-k8s-coredns
  30. ConfigMap.pm-prometheus-operator-k8s-resources-cluster
  31. ConfigMap.pm-prometheus-operator-k8s-resources-namespace
  32. ConfigMap.pm-prometheus-operator-k8s-resources-node
  33. ConfigMap.pm-prometheus-operator-k8s-resources-pod
  34. ConfigMap.pm-prometheus-operator-k8s-resources-workload
  35. ConfigMap.pm-prometheus-operator-k8s-resources-workloads-namespace
  36. ConfigMap.pm-prometheus-operator-kubelet
  37. ConfigMap.pm-prometheus-operator-namespace-by-pod
  38. ConfigMap.pm-prometheus-operator-namespace-by-workload
  39. ConfigMap.pm-prometheus-operator-node-cluster-rsrc-use
  40. ConfigMap.pm-prometheus-operator-node-rsrc-use
  41. ConfigMap.pm-prometheus-operator-nodes
  42. ConfigMap.pm-prometheus-operator-persistentvolumesusage
  43. ConfigMap.pm-prometheus-operator-pod-total
  44. ConfigMap.pm-prometheus-operator-prometheus
  45. ConfigMap.pm-prometheus-operator-statefulset
  46. ConfigMap.pm-prometheus-operator-workload-total
  47. ClusterRole.pm-grafana-clusterrole
  48. ClusterRole.pm-kube-state-metrics
  49. ClusterRole.psp-pm-kube-state-metrics
  50. ClusterRole.psp-pm-prometheus-node-exporter
  51. ClusterRole.pm-prometheus-operator-operator
  52. ClusterRole.pm-prometheus-operator-operator-psp
  53. ClusterRole.pm-prometheus-operator-prometheus
  54. ClusterRole.pm-prometheus-operator-prometheus-psp
  55. ClusterRoleBinding.pm-grafana-clusterrolebinding
  56. ClusterRoleBinding.pm-kube-state-metrics
  57. ClusterRoleBinding.psp-pm-kube-state-metrics
  58. ClusterRoleBinding.psp-pm-prometheus-node-exporter
  59. ClusterRoleBinding.pm-prometheus-operator-operator
  60. ClusterRoleBinding.pm-prometheus-operator-operator-psp
  61. ClusterRoleBinding.pm-prometheus-operator-prometheus
  62. ClusterRoleBinding.pm-prometheus-operator-prometheus-psp
  63. Role.pm-grafana
  64. Role.pm-grafana-test
  65. Role.pm-prometheus-operator-alertmanager
  66. RoleBinding.pm-grafana
  67. RoleBinding.pm-grafana-test
  68. RoleBinding.pm-prometheus-operator-alertmanager
  69. Service.pm-grafana
  70. Service.pm-kube-state-metrics
  71. Service.pm-prometheus-node-exporter
  72. Service.pm-prometheus-operator-alertmanager
  73. Service.pm-prometheus-operator-coredns
  74. Service.pm-prometheus-operator-operator
  75. Service.pm-prometheus-operator-prometheus
  76. DaemonSet.pm-prometheus-node-exporter
  77. Deployment.pm-grafana
  78. Deployment.pm-kube-state-metrics
  79. Deployment.pm-prometheus-operator-operator
  80. Alertmanager.pm-prometheus-operator-alertmanager
  81. MutatingWebhookConfiguration.pm-prometheus-operator-admission
  82. Prometheus.pm-prometheus-operator-prometheus
  83. PrometheusRule.pm-prometheus-operator-alertmanager.rules
  84. PrometheusRule.pm-prometheus-operator-general.rules
  85. PrometheusRule.pm-prometheus-operator-k8s.rules
  86. PrometheusRule.pm-prometheus-operator-kube-apiserver-availability.rules
  87. PrometheusRule.pm-prometheus-operator-kube-apiserver-slos
  88. PrometheusRule.pm-prometheus-operator-kube-apiserver.rules
  89. PrometheusRule.pm-prometheus-operator-kube-prometheus-general.rules
  90. PrometheusRule.pm-prometheus-operator-kube-prometheus-node-recording.rules
  91. PrometheusRule.pm-prometheus-operator-kube-state-metrics
  92. PrometheusRule.pm-prometheus-operator-kubelet.rules
  93. PrometheusRule.pm-prometheus-operator-kubernetes-apps
  94. PrometheusRule.pm-prometheus-operator-kubernetes-resources
  95. PrometheusRule.pm-prometheus-operator-kubernetes-storage
  96. PrometheusRule.pm-prometheus-operator-kubernetes-system-apiserver
  97. PrometheusRule.pm-prometheus-operator-kubernetes-system-kubelet
  98. PrometheusRule.pm-prometheus-operator-kubernetes-system
  99. PrometheusRule.pm-prometheus-operator-node-exporter.rules
  100. PrometheusRule.pm-prometheus-operator-node-exporter
  101. PrometheusRule.pm-prometheus-operator-node-network
  102. PrometheusRule.pm-prometheus-operator-node.rules
  103. PrometheusRule.pm-prometheus-operator-prometheus-operator
  104. PrometheusRule.pm-prometheus-operator-prometheus
  105. ServiceMonitor.pm-prometheus-operator-alertmanager
  106. ServiceMonitor.pm-prometheus-operator-coredns
  107. ServiceMonitor.pm-prometheus-operator-apiserver
  108. ServiceMonitor.pm-prometheus-operator-kube-state-metrics
  109. ServiceMonitor.pm-prometheus-operator-kubelet
  110. ServiceMonitor.pm-prometheus-operator-node-exporter
  111. ServiceMonitor.pm-prometheus-operator-grafana
  112. ServiceMonitor.pm-prometheus-operator-operator
  113. ServiceMonitor.pm-prometheus-operator-prometheus
  114. ValidatingWebhookConfiguration.pm-prometheus-operator-admission
  115. PodSecurityPolicy.pm-prometheus-operator-admission
  116. ServiceAccount.pm-prometheus-operator-admission
  117. ClusterRole.pm-prometheus-operator-admission
  118. ClusterRoleBinding.pm-prometheus-operator-admission
  119. Role.pm-prometheus-operator-admission
  120. RoleBinding.pm-prometheus-operator-admission
  121. Pod.pm-grafana-test
  122. Job.pm-prometheus-operator-admission-create
  123. Job.pm-prometheus-operator-admission-patch

As you can see, there were no duplicates included in this list - nor did my logging code for finding duplicates catch any.

Affected product version(s)

2.7.1

Reproducing the issue

The code I included above will reproduce it.

Suggestions for a fix

I’ve tried looking at other similar issues in this repo, such as https://github.com/pulumi/pulumi-kubernetes/issues/1023 - it’s not clear to me what the issue is here but I suspect it has something to do with the prometheus-operator chart pulling in other charts that all create the same custom resources under the covers. If my transform could also apply to those dependent charts that would be wonderful.

Or is my issue something else?

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 30 (13 by maintainers)

Most upvoted comments

The problem here isn’t with the Kubernetes CustomResourceDefinition, but with the URN generation of Pulumi.

Specifically in @Aaronontheweb case, there is an AlertManager generated by the helm chart called monitoring/pm-prometheus-operator-alertmanager. But there’s also a ServiceMonitor called monitoring/pm-prometheus-operator-alertmanager.

Because these are not standard Kubernetes resource kinds, Pulumi generates a URN for a CustomResourceDefinition type (is this the fallback type for unknown resource kinds?). The result is 2 resources with URN

urn:pulumi:dev::sdkbin-resources::kubernetes:helm.sh/v2:Chart$kubernetes:apiextensions.k8s.io/v1:CustomResourceDefinition::monitoring/pm-prometheus-operator-alertmanager

Kubernetes has no issue with resources named the same, as long as they are not of the same Kind. From the helm chart perspective there is no issue. I would consider it a bug/missing feature in Pulumi. It would be nice if Pulumi just takes the Kubernetes resource Kind in the URN, also for unknown kinds.

I worked around it by applying this transform


Transformations = {
    (obj, opt) =>
    {
         // Transform the name with suffix to prevent URN collisions
         obj = AddNameSuffixForKind(
         kind: "ServiceMonitor",
         suffix: "service-monitor",
         obj,
         opt);

         // Transform the name with suffix to prevent URN collisions
         obj = AddNameSuffixForKind(
         kind: "AlertManager",
         suffix: "alert-manager",
         obj,
         opt);

        return obj;
    }
...
static ImmutableDictionary<string, object> AddNameSuffixForKind(
            string kind,
            string suffix,
            ImmutableDictionary<string, object> obj,
            CustomResourceOptions opts)
        {
            if ((string)obj["kind"] == kind)
            {
                var metaData = (ImmutableDictionary<string, object>)obj["metadata"];
                var name = metaData["name"];
                metaData = metaData.SetItem("name", $"{name}-{suffix}");
                obj = obj.SetItem("metadata", metaData);

                return obj;
            }

            return obj;
        }

Yes. This is extremely common. Most Charts have lots of bugs exactly like this.

If that’s the case then it severely limits Pulumi’s usefulness if we can’t install most Helm charts.

FWIW - I do not consider this “a bug in the charts” exactly. Since the charts do work correctly via the Helm CLI, it is a bug in Pulumi’s support for deploying Helm charts that we do not handle this smoothly as well - even if it is “poor design on behalf of the chart author”.

We should definitely look into what we can do to fix/improve Pulumi’s support for charts like these (especially as the majority of the most used charts have this sort of “issue”).

I’ve got this problem with cert-manager’s CRDs. I’m deploying multiple clusters in my pulumi stack and cert-manager to each cluster.

Pulumi throws a duplicate URN error because the helm chart uses a hardcoded name for it’s CRD “certificaterequests.cert-manager.io”.

Unfortunately I can’t work around this issue by using “resourcePrefix” because then my resource names start to exceed 53 characters (also it makes the naming very silly with a double {name}-{name} prefix).

I also can’t work around this problem by using a resource transformation to rename the CRD because the CRD name must be spec.names.plural+"."+spec.group as enforced by the k8s api.

I also can’t work around this problem by using k8s.yaml.ConfigFile or k8s.yaml.parse because they also map resource names to URNs.

I think pulumi needs to do something to better handle URN generation for helm charts.

Perhaps an urnPrefix would work?

@lblackstone and I are having a discussion on how best to support this. It seems this was introduced by #1102 and we do need to figure out a way to either A) let the user opt-out of installing the CRDs, because they might be installed out of band by another process B) detect if the crd is in the api and omit them if automatically.

The workaround at the moment is to use a transformation to drop the resource, something a bit like this:

transformations: [
          (obj: any, opts: pulumi.CustomResourceOptions) => {
            if (obj.kind === "CustomResourceDefinition" && (obj.metadata.name === "postgresqls.acid.zalan.do" || obj.metadata.name === "operatorconfigurations.acid.zalan.do")) {
              obj.apiVersion = "v1"
                obj.kind = "List"
            }
          },
        ],

I can confirm that release v3.8.0 with #1741 fixes this issue for me.

Specifically, I can remove the crd name transformation hacks I mention above and it works as expected! 🎉

@lblackstone and I are having a discussion on how best to support this. It seems this was introduced by #1102 and we do need to figure out a way to either A) let the user opt-out of installing the CRDs, because they might be installed out of band by another process B) detect if the crd is in the api and omit them if automatically.

The workaround at the moment is to use a transformation to drop the resource, something a bit like this:

transformations: [
          (obj: any, opts: pulumi.CustomResourceOptions) => {
            if (obj.kind === "CustomResourceDefinition" && (obj.metadata.name === "postgresqls.acid.zalan.do" || obj.metadata.name === "operatorconfigurations.acid.zalan.do")) {
              obj.apiVersion = "v1"
                obj.kind = "List"
            }
          },
        ],

Would be nice to at least have the flag --skip-crds exposed on the v3.Chart object, for now this workaround works for us. 👍

Shouldn’t this become an option so the user can decide to either use the --include-crds or --skip-crds flag?

Yes - I believe @jaxxstorm is working on a plan for how best to expose this option.

Yes. This is extremely common. Most Charts have lots of bugs exactly like this.

If that’s the case then it severely limits Pulumi’s usefulness if we can’t install most Helm charts.