Repository: pantsbuild/pants

Issue:
Support optional 3rd party dependencies in helm
**Is your feature request related to a problem? Please describe.**
I am generating a helm chart that would ordinarily have chart dependencies that are optional based on a condition. The method of defining helm artifacts doesn't seem to allow for this.

**Describe the solution you'd like**
Preferably defining a 3rd party helm artifact would simply allow it to be used manually inside the chart.yaml instead of injecting the chart automatically (or at least have the option) so that you could manually configure the chart yaml as you would like.

**Describe alternatives you've considered**
I think I could make proxy 1st party chart that references the 3rd party chart and then make that optional but that is very awkward.

**Additional context**
Add any other context or screenshots about the feature request here.



Discussion:
You should be able to conditionally include helm charts using `condition` like in normal helm charts :
```
# Chart.yaml
apiVersion: v2
name: actual-project
version: "0.1.0"
dependencies:
  - name: cert-manager
    version: "1.13.2"
    condition: cert-manager.enabled
    repository: "https://charts.jetstack.io"
```
You still need to define the dependency in a BUILD file, so Pants knows what you're referencing:
```
# BUILD
helm_chart()

helm_artifact(
    name="cert-manager",
    repository="https://charts.jetstack.io",
    artifact="cert-manager",
    version="v1.13.2"
)
```
I think we could add a feature to automatically create the `helm_artifact`.
@lilatomic It turns out that this doesn't work if you keep a trailing slash on the end of the repository, which is what I had done. I was confused as to why your example worked for me but didn't work for my real life usecase, trial and error narrowed it down to me having my repository as `https://charts.jetstack.io/` not `https://charts.jetstack.io` in the Chart.yaml. Trailing slash in the helm_artifact makes no difference.
Oh, then that's a bug on our part. a trailing `/` is valid in the Chart.yaml for dependency repositories (although helm too had to add this as a feature https://github.com/helm/helm/pull/1603)

Resolving patch:
diff --git a/src/python/pants/backend/helm/dependency_inference/chart.py b/src/python/pants/backend/helm/dependency_inference/chart.py
--- a/src/python/pants/backend/helm/dependency_inference/chart.py
+++ b/src/python/pants/backend/helm/dependency_inference/chart.py
@@ -4,11 +4,13 @@
 from __future__ import annotations
 
 import logging
+import os
 from dataclasses import dataclass
 from typing import Iterable
 
 from pants.backend.helm.resolve import artifacts
 from pants.backend.helm.resolve.artifacts import ThirdPartyHelmArtifactMapping
+from pants.backend.helm.resolve.remotes import HelmRemotes
 from pants.backend.helm.subsystems.helm import HelmSubsystem
 from pants.backend.helm.target_types import (
     AllHelmChartTargets,
@@ -96,6 +98,16 @@ class InferHelmChartDependenciesRequest(InferDependenciesRequest):
     infer_from = HelmChartDependenciesInferenceFieldSet
 
 
+def resolve_dependency_url(remotes: HelmRemotes, dependency: HelmChartDependency) -> str | None:
+    if not dependency.repository:
+        registry = remotes.default_registry
+        if registry:
+            return os.path.join(registry.address, dependency.name)
+        return None
+    else:
+        return os.path.join(dependency.repository, dependency.name)
+
+
 @rule(desc="Inferring Helm chart dependencies", level=LogLevel.DEBUG)
 async def infer_chart_dependencies_via_metadata(
     request: InferHelmChartDependenciesRequest,
@@ -113,15 +125,6 @@ async def infer_chart_dependencies_via_metadata(
 
     remotes = subsystem.remotes()
 
-    def resolve_dependency_url(dependency: HelmChartDependency) -> str | None:
-        if not dependency.repository:
-            registry = remotes.default_registry
-            if registry:
-                return f"{registry.address}/{dependency.name}"
-            return None
-        else:
-            return f"{dependency.repository}/{dependency.name}"
-
     # Associate dependencies in Chart.yaml with addresses.
     dependencies: OrderedSet[Address] = OrderedSet()
     for chart_dep in metadata.dependencies:
@@ -131,7 +134,7 @@ def resolve_dependency_url(dependency: HelmChartDependency) -> str | None:
         if first_party_dep:
             candidate_addrs.append(first_party_dep)
 
-        dependency_url = resolve_dependency_url(chart_dep)
+        dependency_url = resolve_dependency_url(remotes, chart_dep)
         third_party_dep = third_party_mapping.get(dependency_url) if dependency_url else None
         if third_party_dep:
             candidate_addrs.append(third_party_dep)


Test patch:
diff --git a/src/python/pants/backend/helm/dependency_inference/chart_test.py b/src/python/pants/backend/helm/dependency_inference/chart_test.py
--- a/src/python/pants/backend/helm/dependency_inference/chart_test.py
+++ b/src/python/pants/backend/helm/dependency_inference/chart_test.py
@@ -9,17 +9,21 @@
     FirstPartyHelmChartMapping,
     HelmChartDependenciesInferenceFieldSet,
     InferHelmChartDependenciesRequest,
+    resolve_dependency_url,
 )
 from pants.backend.helm.dependency_inference.chart import rules as chart_infer_rules
 from pants.backend.helm.resolve import artifacts
+from pants.backend.helm.resolve.remotes import HelmRemotes
 from pants.backend.helm.target_types import HelmArtifactTarget, HelmChartTarget
 from pants.backend.helm.target_types import rules as target_types_rules
 from pants.backend.helm.util_rules import chart
+from pants.backend.helm.util_rules.chart_metadata import HelmChartDependency
 from pants.engine.addresses import Address
 from pants.engine.internals.scheduler import ExecutionError
 from pants.engine.rules import QueryRule
 from pants.engine.target import InferredDependencies
 from pants.testutil.rule_runner import RuleRunner
+from pants.util.frozendict import FrozenDict
 from pants.util.strutil import bullet_list
 
 
@@ -215,3 +219,21 @@ def test_raise_error_when_unknown_dependency_is_found(rule_runner: RuleRunner) -
             InferredDependencies,
             [InferHelmChartDependenciesRequest(HelmChartDependenciesInferenceFieldSet.create(tgt))],
         )
+
+
+@pytest.mark.parametrize(
+    "dependency,expected",
+    [
+        (
+            HelmChartDependency(repository="https://repo.example.com", name="name"),
+            "https://repo.example.com/name",
+        ),
+        (
+            HelmChartDependency(repository="https://repo.example.com/", name="name"),
+            "https://repo.example.com/name",
+        ),
+    ],
+)
+def test_18629(dependency, expected) -> None:
+    """Test that we properly resolve dependency urls."""
+    assert resolve_dependency_url(HelmRemotes(tuple(), FrozenDict()), dependency) == expected