diff --git a/integration-test/README.md b/integration-test/README.md
index c3c7a69a..c900f37a 100644
--- a/integration-test/README.md
+++ b/integration-test/README.md
@@ -183,11 +183,12 @@ Each `branch-N/target[-suffix]/` directory contains expected output files (`outp
### Branch 15: Additional Coverage
- `target`: Basic diff (no special options)
-### Branch 16-19: repo-server-api Render Method
+### Branch 16-20: repo-server-api Render Method
- `branch-16/target`: Config Management Plugin (kustomize-build-with-helm) via `repo-server-api`
- `branch-17/target-1` / `target-2`: App-of-apps traversal via `repo-server-api`
- `branch-18/target`: Single-source local Helm chart whose `helm.valueFiles` reference a file OUTSIDE the chart directory via a repo-root-absolute path (`/examples/out-of-chart-values/env/values.yaml`). Regression test for `repo-server-api`: the tool must stream the whole branch folder so the out-of-chart value file is reachable, otherwise the render fails with `no such file or directory`.
- `branch-19/target`: Kustomize resource outside the application directory via `repo-server-api`. It requires global `kustomize.buildOptions: --load-restrictor LoadRestrictionsNone` from `argocd-cm`, proving the direct repo-server request forwards that setting.
+- `branch-20/target`: Local Helm chart that gates a ServiceMonitor on the kind-level capability `monitoring.coreos.com/v1/ServiceMonitor`, via `repo-server-api` against a cluster with the ServiceMonitor CRD. The target enables it, so the diff must show the ServiceMonitor added, as a real Argo CD sync would render it.
## Prerequisites
diff --git a/integration-test/branch-20/target/output.html b/integration-test/branch-20/target/output.html
new file mode 100644
index 00000000..95c6778b
--- /dev/null
+++ b/integration-test/branch-20/target/output.html
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
Argo CD Diff Preview
+
+
Summary:
+
Modified (1):
+± kind-level-capabilities (+12|-1)
+
+
+
+
+kind-level-capabilities (examples/kind-level-capabilities/application.yaml)
+
+
+
+
+
+
+ +apiVersion: monitoring.coreos.com/v1 |
+ +kind: ServiceMonitor |
+ +metadata: |
+ + name: kind-level-capabilities |
+ + namespace: default |
+ +spec: |
+ + endpoints: |
+ + - port: metrics |
+ + selector: |
+ + matchLabels: |
+ + app.kubernetes.io/name: kind-level-capabilities |
+
+
+
+
+
+
+
+ apiVersion: v1 |
+ data: |
+ - serviceMonitorEnabled: "false" |
+ + serviceMonitorEnabled: "true" |
+ kind: ConfigMap |
+ metadata: |
+ name: kind-level-capabilities |
+ namespace: default |
+
+
+
+
+
+
+
_Stats_:
+[Applications: 2], [Full Run: Xs], [Rendering: Xs], [Cluster: Xs], [Argo CD: Xs]
+
+
+
diff --git a/integration-test/branch-20/target/output.md b/integration-test/branch-20/target/output.md
new file mode 100644
index 00000000..ce5725a7
--- /dev/null
+++ b/integration-test/branch-20/target/output.md
@@ -0,0 +1,41 @@
+## Argo CD Diff Preview
+
+Summary:
+```yaml
+Modified (1):
+± kind-level-capabilities (+12|-1)
+```
+
+
+kind-level-capabilities (examples/kind-level-capabilities/application.yaml)
+
+
+#### ServiceMonitor: default/kind-level-capabilities
+```diff
++apiVersion: monitoring.coreos.com/v1
++kind: ServiceMonitor
++metadata:
++ name: kind-level-capabilities
++ namespace: default
++spec:
++ endpoints:
++ - port: metrics
++ selector:
++ matchLabels:
++ app.kubernetes.io/name: kind-level-capabilities
+```
+#### ConfigMap: default/kind-level-capabilities
+```diff
+ apiVersion: v1
+ data:
+- serviceMonitorEnabled: "false"
++ serviceMonitorEnabled: "true"
+ kind: ConfigMap
+ metadata:
+ name: kind-level-capabilities
+ namespace: default
+```
+
+
+_Stats_:
+[Applications: 2], [Full Run: Xs], [Rendering: Xs], [Cluster: Xs], [Argo CD: Xs]
diff --git a/integration-test/integration_test.go b/integration-test/integration_test.go
index 85125cc2..6ca74673 100644
--- a/integration-test/integration_test.go
+++ b/integration-test/integration_test.go
@@ -360,6 +360,19 @@ var testCases = []TestCase{
FileRegex: "examples/(kustomize-build-options|resource-application-boundary|cluster-rbac-repro)/.*",
WatchIfNoWatchPatternFound: "false",
},
+ // Regression test for kind-level .Capabilities.APIVersions entries in
+ // repo-server-api mode. The chart gates a ServiceMonitor on
+ // "monitoring.coreos.com/v1/ServiceMonitor", which Argo CD includes next to
+ // the group version. The target enables it, so the diff must show it added.
+ {
+ Name: "branch-20/target",
+ TargetBranch: "integration-test/branch-20/target",
+ BaseBranch: "integration-test/branch-20/base",
+ RenderMethod: "repo-server-api",
+ ArgocdConfigDir: "with-servicemonitor-crd-no-cluster-roles",
+ FileRegex: "examples/kind-level-capabilities/.*",
+ WatchIfNoWatchPatternFound: "false",
+ },
// This test verifies that disabling cluster roles without using the API fails.
// When createClusterRoles: false is set but --render-method=cli is used,
// the tool should fail because it can't access cluster resources via CLI.
diff --git a/pkg/k8s/discovery.go b/pkg/k8s/discovery.go
index b1a25ff8..5d3c54d1 100644
--- a/pkg/k8s/discovery.go
+++ b/pkg/k8s/discovery.go
@@ -19,9 +19,11 @@ func (c *Client) GetServerVersion() (string, error) {
}
// GetAPIVersions returns all unique GroupVersion strings (e.g. "v1", "apps/v1",
-// "monitoring.coreos.com/v1") available in the cluster. This is the format
-// consumed by the Argo CD repo server's ManifestRequest.ApiVersions field and
-// exposed to Helm templates via .Capabilities.APIVersions.
+// "monitoring.coreos.com/v1") available in the cluster, plus a
+// "/" entry for every resource kind (e.g.
+// "monitoring.coreos.com/v1/ServiceMonitor"), as Argo CD sends them. This is the
+// format consumed by the Argo CD repo server's ManifestRequest.ApiVersions field
+// and exposed to Helm templates via .Capabilities.APIVersions.
func (c *Client) GetAPIVersions() ([]string, error) {
_, apiVersions, err := c.GetClusterScopedResourcesAndAPIVersions()
if err != nil {
@@ -31,7 +33,8 @@ func (c *Client) GetAPIVersions() ([]string, error) {
}
// GetClusterScopedResourcesAndAPIVersions returns metadata about all cluster-scoped
-// resource types and all unique GroupVersion strings in one discovery pass.
+// resource types and all unique API version strings (see GetAPIVersions) in one
+// discovery pass.
func (c *Client) GetClusterScopedResourcesAndAPIVersions() (map[schema.GroupKind]bool, []string, error) {
_, apiResourceLists, err := c.discoveryClient.ServerGroupsAndResources()
if err != nil {
@@ -55,14 +58,22 @@ func (c *Client) GetClusterScopedResourcesAndAPIVersions() (map[schema.GroupKind
// Check each resource in the API group
for _, apiResource := range apiResourceList.APIResources {
- // Only retain cluster-scoped resources. A kind missing from this map is
- // treated as namespaced, which is the safe default for undiscovered CRDs.
- if apiResource.Namespaced {
+ // Skip subresources (e.g., "pods/log", "deployments/scale")
+ if strings.Contains(apiResource.Name, "/") {
continue
}
- // Skip subresources (e.g., "pods/log", "deployments/scale")
- if strings.Contains(apiResource.Name, "/") {
+ // Argo CD also sends every kind as "/", and charts gate
+ // on that form, e.g. `.Capabilities.APIVersions.Has "monitoring.coreos.com/v1/ServiceMonitor"`.
+ kindVersion := apiResourceList.GroupVersion + "/" + apiResource.Kind
+ if !seen[kindVersion] {
+ seen[kindVersion] = true
+ apiVersions = append(apiVersions, kindVersion)
+ }
+
+ // Only retain cluster-scoped resources. A kind missing from this map is
+ // treated as namespaced, which is the safe default for undiscovered CRDs.
+ if apiResource.Namespaced {
continue
}
diff --git a/pkg/k8s/discovery_test.go b/pkg/k8s/discovery_test.go
new file mode 100644
index 00000000..4f1393c3
--- /dev/null
+++ b/pkg/k8s/discovery_test.go
@@ -0,0 +1,58 @@
+package k8s
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ fakediscovery "k8s.io/client-go/discovery/fake"
+ clienttesting "k8s.io/client-go/testing"
+)
+
+func newFakeDiscoveryClient(resources []*metav1.APIResourceList) *Client {
+ return &Client{
+ discoveryClient: &fakediscovery.FakeDiscovery{Fake: &clienttesting.Fake{Resources: resources}},
+ }
+}
+
+func TestGetClusterScopedResourcesAndAPIVersions(t *testing.T) {
+ client := newFakeDiscoveryClient([]*metav1.APIResourceList{
+ {
+ GroupVersion: "v1",
+ APIResources: []metav1.APIResource{
+ {Name: "pods", Kind: "Pod", Namespaced: true},
+ {Name: "pods/log", Kind: "Pod", Namespaced: true},
+ {Name: "namespaces", Kind: "Namespace", Namespaced: false},
+ },
+ },
+ {
+ GroupVersion: "apps/v1",
+ APIResources: []metav1.APIResource{
+ {Name: "deployments", Kind: "Deployment", Namespaced: true},
+ {Name: "deployments/scale", Kind: "Scale", Namespaced: true},
+ },
+ },
+ {
+ GroupVersion: "monitoring.coreos.com/v1",
+ APIResources: []metav1.APIResource{
+ {Name: "servicemonitors", Kind: "ServiceMonitor", Namespaced: true},
+ },
+ },
+ })
+
+ clusterScoped, apiVersions, err := client.GetClusterScopedResourcesAndAPIVersions()
+ require.NoError(t, err)
+
+ assert.Equal(t, []string{
+ "apps/v1",
+ "apps/v1/Deployment",
+ "monitoring.coreos.com/v1",
+ "monitoring.coreos.com/v1/ServiceMonitor",
+ "v1",
+ "v1/Namespace",
+ "v1/Pod",
+ }, apiVersions)
+ assert.Equal(t, map[schema.GroupKind]bool{{Group: "", Kind: "Namespace"}: true}, clusterScoped)
+}