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) + + +

ServiceMonitor: default/kind-level-capabilities

+
+ + + + + + + + + + + + + +
+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

+
+ + + + + + + + + + +
 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) +}