From 6c944e27d95cb6eb4342e4808e94194a598a2ef9 Mon Sep 17 00:00:00 2001 From: Dag Andersen Date: Fri, 22 May 2026 09:35:16 +0200 Subject: [PATCH 1/7] Fix | repo-server-api render method does not pass ApiVersions in ManifestRequest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Query the cluster for KubeVersion and ApiVersions once per render run and populate them on every ManifestRequest sent to the Argo CD repo server. This ensures Helm charts that conditionally render resources based on .Capabilities.APIVersions (e.g. ServiceMonitor, VirtualService) produce the same output as a real ArgoCD sync. New helpers added to pkg/k8s/discovery.go: - GetServerVersion() – wraps discoveryClient.ServerVersion() - GetAPIVersions() – returns all unique GroupVersion strings from the cluster Both RenderApplicationsFromBothBranches and RenderApplicationsFromBothBranchesWithAppOfApps now fetch these values and thread them through renderApp → buildManifestRequestForSource. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pkg/k8s/discovery.go | 31 +++++++++++++++++++++++++++ pkg/reposerverextract/appofapps.go | 16 ++++++++++++-- pkg/reposerverextract/extract.go | 20 +++++++++++++++-- pkg/reposerverextract/extract_test.go | 24 ++++++++++----------- 4 files changed, 75 insertions(+), 16 deletions(-) diff --git a/pkg/k8s/discovery.go b/pkg/k8s/discovery.go index 71c9450a..723b59f6 100644 --- a/pkg/k8s/discovery.go +++ b/pkg/k8s/discovery.go @@ -8,6 +8,37 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" ) +// GetServerVersion returns the Kubernetes server version string (e.g. "v1.29.2"). +func (c *Client) GetServerVersion() (string, error) { + v, err := c.discoveryClient.ServerVersion() + if err != nil { + return "", fmt.Errorf("failed to get server version: %w", err) + } + return v.GitVersion, nil +} + +// 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. +func (c *Client) GetAPIVersions() ([]string, error) { + _, apiResourceLists, err := c.discoveryClient.ServerGroupsAndResources() + if err != nil { + return nil, fmt.Errorf("failed to discover API resources: %w", err) + } + + seen := make(map[string]bool) + var apiVersions []string + for _, apiResourceList := range apiResourceLists { + if seen[apiResourceList.GroupVersion] { + continue + } + seen[apiResourceList.GroupVersion] = true + apiVersions = append(apiVersions, apiResourceList.GroupVersion) + } + return apiVersions, nil +} + // GetListOfNamespacedScopedResources returns metadata about all namespaced resource types // Returns a map where the key is schema.GroupKind and the value is true (indicating the resource is namespaced) // This format matches the interface expected by Argo CD's kubeutil.ResourceInfoProvider diff --git a/pkg/reposerverextract/appofapps.go b/pkg/reposerverextract/appofapps.go index 7ef764dc..085f303c 100644 --- a/pkg/reposerverextract/appofapps.go +++ b/pkg/reposerverextract/appofapps.go @@ -166,6 +166,16 @@ func RenderApplicationsFromBothBranchesWithAppOfApps( return nil, nil, time.Since(startTime), fmt.Errorf("failed to get list of namespaced scoped resources: %w", err) } + kubeVersion, err := argocd.K8sClient.GetServerVersion() + if err != nil { + return nil, nil, time.Since(startTime), fmt.Errorf("failed to get server version: %w", err) + } + + apiVersions, err := argocd.K8sClient.GetAPIVersions() + if err != nil { + return nil, nil, time.Since(startTime), fmt.Errorf("failed to get API versions: %w", err) + } + // Collect all unique repository URLs referenced by the Applications so that // FetchRepoCreds can enrich them with credentials from repo-creds templates. appRepoURLs := collectRepoURLs(baseApps, targetApps) @@ -347,7 +357,7 @@ func RenderApplicationsFromBothBranchesWithAppOfApps( ctx, cancel := context.WithTimeout(context.Background(), time.Duration(remainingTime())*time.Second) defer cancel() - manifests, childApps, err := renderAppWithChildDiscovery(ctx, repoClient, argocd, item.app, branchFolderByType, branchByType, namespacedScopedResources, creds, &repoSelector, argocd.Namespace, tempFolder, item.depth) + manifests, childApps, err := renderAppWithChildDiscovery(ctx, repoClient, argocd, item.app, branchFolderByType, branchByType, namespacedScopedResources, creds, &repoSelector, argocd.Namespace, tempFolder, item.depth, kubeVersion, apiVersions) if err != nil { results <- renderResult{err: fmt.Errorf("failed to render app %s: %w", item.app.GetLongName(), err)} return @@ -412,8 +422,10 @@ func renderAppWithChildDiscovery( argocdNamespace string, tempFolder string, depth int, + kubeVersion string, + apiVersions []string, ) ([]unstructured.Unstructured, []argoapplication.ArgoResource, error) { - allManifests, err := renderApp(ctx, repoClient, app, branchFolderByType, namespacedScopedResources, creds, repoSelector) + allManifests, err := renderApp(ctx, repoClient, app, branchFolderByType, namespacedScopedResources, creds, repoSelector, kubeVersion, apiVersions) if err != nil { return nil, nil, err } diff --git a/pkg/reposerverextract/extract.go b/pkg/reposerverextract/extract.go index ac7e3d52..310d0b58 100644 --- a/pkg/reposerverextract/extract.go +++ b/pkg/reposerverextract/extract.go @@ -97,6 +97,16 @@ func RenderApplicationsFromBothBranches( return nil, nil, time.Since(startTime), fmt.Errorf("failed to get list of namespaced scoped resources: %w", err) } + kubeVersion, err := argocd.K8sClient.GetServerVersion() + if err != nil { + return nil, nil, time.Since(startTime), fmt.Errorf("failed to get server version: %w", err) + } + + apiVersions, err := argocd.K8sClient.GetAPIVersions() + if err != nil { + return nil, nil, time.Since(startTime), fmt.Errorf("failed to get API versions: %w", err) + } + // Collect all unique repository URLs referenced by the Applications so that // FetchRepoCreds can enrich them with credentials from repo-creds templates. appRepoURLs := collectRepoURLs(baseApps, targetApps) @@ -176,7 +186,7 @@ func RenderApplicationsFromBothBranches( ctx, cancel := context.WithTimeout(context.Background(), time.Duration(remainingTime())*time.Second) defer cancel() - manifests, err := renderApp(ctx, repoClient, app, branchFolderByType, namespacedScopedResources, creds, &repoSelector) + manifests, err := renderApp(ctx, repoClient, app, branchFolderByType, namespacedScopedResources, creds, &repoSelector, kubeVersion, apiVersions) if err != nil { results <- result{err: fmt.Errorf("failed to render app %s: %w", app.GetLongName(), err)} return @@ -251,6 +261,8 @@ func renderApp( namespacedScopedResources map[schema.GroupKind]bool, creds *RepoCreds, repoSelector *repository.Selector, + kubeVersion string, + apiVersions []string, ) ([]unstructured.Unstructured, error) { branchFolder, ok := branchFolderByType[app.Branch] if !ok { @@ -265,7 +277,7 @@ func renderApp( var allManifestStrings []string for i, contentSource := range contentSources { - request, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSource, refSources, hasMultipleSources, branchFolder, creds, repoSelector) + request, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSource, refSources, hasMultipleSources, branchFolder, creds, repoSelector, kubeVersion, apiVersions) if err != nil { return nil, fmt.Errorf("failed to build manifest request for content source %d: %w", i, err) } @@ -502,6 +514,8 @@ func buildManifestRequestForSource( branchFolder string, creds *RepoCreds, repoSelector *repository.Selector, + kubeVersion string, + apiVersions []string, ) (request *repoapiclient.ManifestRequest, streamDir string, cleanup func(), err error) { obj := app.Yaml.Object @@ -524,6 +538,8 @@ func buildManifestRequestForSource( AppName: app.Id, Namespace: namespace, ApplicationSource: source, + KubeVersion: kubeVersion, + ApiVersions: apiVersions, HasMultipleSources: hasMultipleSources, // Applications are patched to the default project before rendering. We // allow all source repos here so repo-server does not replace Helm diff --git a/pkg/reposerverextract/extract_test.go b/pkg/reposerverextract/extract_test.go index a2322768..020bde26 100644 --- a/pkg/reposerverextract/extract_test.go +++ b/pkg/reposerverextract/extract_test.go @@ -107,7 +107,7 @@ spec: require.Empty(t, refSources) assert.False(t, hasMultipleSources) - req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, "")) + req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, ""), "", nil) require.NoError(t, err) if cleanup != nil { defer cleanup() @@ -149,7 +149,7 @@ spec: require.NoError(t, err) require.Len(t, contentSources, 1) - req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, "")) + req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, ""), "", nil) require.NoError(t, err) if cleanup != nil { defer cleanup() @@ -217,7 +217,7 @@ spec: require.Len(t, contentSources, 1, "only the chart source is a content source") require.Len(t, refSources, 1) - req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, "")) + req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, ""), "", nil) require.NoError(t, err) if cleanup != nil { defer cleanup() @@ -299,7 +299,7 @@ spec: require.Len(t, contentSources, 1) require.Len(t, refSources, 1) - req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, "")) + req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, ""), "", nil) require.NoError(t, err) require.NotEmpty(t, streamDir, "local chart with refs must stream a temp dir") defer cleanup() @@ -410,7 +410,7 @@ spec: } require.NotEmpty(t, chartSource.Chart, "should find the chart content source") - req, streamDir, cleanup, err := buildManifestRequestForSource(app, chartSource, refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, "")) + req, streamDir, cleanup, err := buildManifestRequestForSource(app, chartSource, refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, ""), "", nil) require.NoError(t, err) if cleanup != nil { defer cleanup() @@ -470,7 +470,7 @@ spec: require.Len(t, contentSources, 1) require.Len(t, refSources, 1) - req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, "")) + req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, ""), "", nil) require.NoError(t, err) if cleanup != nil { defer cleanup() @@ -555,7 +555,7 @@ spec: // Capture requests so we can verify per-source paths without duplicate calls. reqs := make([]struct{ path string }, len(contentSources)) for i, cs := range contentSources { - req, streamDir, cleanup, buildErr := buildManifestRequestForSource(app, cs, refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, "")) + req, streamDir, cleanup, buildErr := buildManifestRequestForSource(app, cs, refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, ""), "", nil) require.NoError(t, buildErr, "content source %d should not error", i) if cleanup != nil { defer cleanup() @@ -615,7 +615,7 @@ spec: require.Len(t, contentSources, 1) require.Empty(t, refSources) - req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, prRepo)) + req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, prRepo), "", nil) require.NoError(t, err) if cleanup != nil { defer cleanup() @@ -665,7 +665,7 @@ spec: require.NoError(t, err) require.Len(t, contentSources, 1) - req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, prRepo)) + req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, prRepo), "", nil) require.NoError(t, err) if cleanup != nil { defer cleanup() @@ -706,7 +706,7 @@ spec: require.Len(t, contentSources, 1) require.Len(t, refSources, 1) - req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, prRepo)) + req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, prRepo), "", nil) require.NoError(t, err) if cleanup != nil { defer cleanup() @@ -753,7 +753,7 @@ spec: require.Len(t, contentSources, 1) require.Len(t, refSources, 1) - req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, prRepo)) + req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, prRepo), "", nil) require.NoError(t, err) if cleanup != nil { defer cleanup() @@ -859,7 +859,7 @@ spec: require.NoError(t, err) require.Len(t, contentSources, 1) - req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, prRepo)) + req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, prRepo), "", nil) require.NoError(t, err) if cleanup != nil { defer cleanup() From 9b7c38742fd81e79b67c6f830dd5bad68bfb9ad7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 May 2026 12:18:28 +0000 Subject: [PATCH 2/7] Reduce duplicate discovery calls and stabilize API versions --- pkg/k8s/discovery.go | 49 ++++++++++++++++-------------- pkg/reposerverextract/appofapps.go | 9 ++---- pkg/reposerverextract/extract.go | 9 ++---- 3 files changed, 31 insertions(+), 36 deletions(-) diff --git a/pkg/k8s/discovery.go b/pkg/k8s/discovery.go index 723b59f6..77620ea7 100644 --- a/pkg/k8s/discovery.go +++ b/pkg/k8s/discovery.go @@ -2,6 +2,7 @@ package k8s import ( "fmt" + "sort" "strings" "github.com/rs/zerolog/log" @@ -12,7 +13,7 @@ import ( func (c *Client) GetServerVersion() (string, error) { v, err := c.discoveryClient.ServerVersion() if err != nil { - return "", fmt.Errorf("failed to get server version: %w", err) + return "", err } return v.GitVersion, nil } @@ -22,37 +23,30 @@ func (c *Client) GetServerVersion() (string, error) { // 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) { - _, apiResourceLists, err := c.discoveryClient.ServerGroupsAndResources() + _, apiVersions, err := c.GetNamespacedScopedResourcesAndAPIVersions() if err != nil { - return nil, fmt.Errorf("failed to discover API resources: %w", err) - } - - seen := make(map[string]bool) - var apiVersions []string - for _, apiResourceList := range apiResourceLists { - if seen[apiResourceList.GroupVersion] { - continue - } - seen[apiResourceList.GroupVersion] = true - apiVersions = append(apiVersions, apiResourceList.GroupVersion) + return nil, err } return apiVersions, nil } -// GetListOfNamespacedScopedResources returns metadata about all namespaced resource types -// Returns a map where the key is schema.GroupKind and the value is true (indicating the resource is namespaced) -// This format matches the interface expected by Argo CD's kubeutil.ResourceInfoProvider -func (c *Client) GetListOfNamespacedScopedResources() (map[schema.GroupKind]bool, error) { - namespacedScopedResources := make(map[schema.GroupKind]bool) - - // Get all API resources from the cluster +// GetNamespacedScopedResourcesAndAPIVersions returns metadata about all namespaced +// resource types and all unique GroupVersion strings in one discovery pass. +func (c *Client) GetNamespacedScopedResourcesAndAPIVersions() (map[schema.GroupKind]bool, []string, error) { _, apiResourceLists, err := c.discoveryClient.ServerGroupsAndResources() if err != nil { - return nil, fmt.Errorf("failed to discover API resources: %w", err) + return nil, nil, fmt.Errorf("failed to discover API resources: %w", err) } - // Iterate through all resource groups and versions + namespacedScopedResources := make(map[schema.GroupKind]bool) + seen := make(map[string]bool) + var apiVersions []string for _, apiResourceList := range apiResourceLists { + if seen[apiResourceList.GroupVersion] { + } else { + seen[apiResourceList.GroupVersion] = true + apiVersions = append(apiVersions, apiResourceList.GroupVersion) + } // Parse GroupVersion to extract the group gv, err := schema.ParseGroupVersion(apiResourceList.GroupVersion) if err != nil { @@ -82,6 +76,17 @@ func (c *Client) GetListOfNamespacedScopedResources() (map[schema.GroupKind]bool namespacedScopedResources[gk] = true } } + sort.Strings(apiVersions) + return namespacedScopedResources, apiVersions, nil +} +// GetListOfNamespacedScopedResources returns metadata about all namespaced resource types +// Returns a map where the key is schema.GroupKind and the value is true (indicating the resource is namespaced) +// This format matches the interface expected by Argo CD's kubeutil.ResourceInfoProvider +func (c *Client) GetListOfNamespacedScopedResources() (map[schema.GroupKind]bool, error) { + namespacedScopedResources, _, err := c.GetNamespacedScopedResourcesAndAPIVersions() + if err != nil { + return nil, err + } return namespacedScopedResources, nil } diff --git a/pkg/reposerverextract/appofapps.go b/pkg/reposerverextract/appofapps.go index 085f303c..aa83c425 100644 --- a/pkg/reposerverextract/appofapps.go +++ b/pkg/reposerverextract/appofapps.go @@ -161,9 +161,9 @@ func RenderApplicationsFromBothBranchesWithAppOfApps( return nil, nil, time.Since(startTime), err } - namespacedScopedResources, err := argocd.K8sClient.GetListOfNamespacedScopedResources() + namespacedScopedResources, apiVersions, err := argocd.K8sClient.GetNamespacedScopedResourcesAndAPIVersions() if err != nil { - return nil, nil, time.Since(startTime), fmt.Errorf("failed to get list of namespaced scoped resources: %w", err) + return nil, nil, time.Since(startTime), fmt.Errorf("failed to discover API resources: %w", err) } kubeVersion, err := argocd.K8sClient.GetServerVersion() @@ -171,11 +171,6 @@ func RenderApplicationsFromBothBranchesWithAppOfApps( return nil, nil, time.Since(startTime), fmt.Errorf("failed to get server version: %w", err) } - apiVersions, err := argocd.K8sClient.GetAPIVersions() - if err != nil { - return nil, nil, time.Since(startTime), fmt.Errorf("failed to get API versions: %w", err) - } - // Collect all unique repository URLs referenced by the Applications so that // FetchRepoCreds can enrich them with credentials from repo-creds templates. appRepoURLs := collectRepoURLs(baseApps, targetApps) diff --git a/pkg/reposerverextract/extract.go b/pkg/reposerverextract/extract.go index 310d0b58..61f65ab7 100644 --- a/pkg/reposerverextract/extract.go +++ b/pkg/reposerverextract/extract.go @@ -92,9 +92,9 @@ func RenderApplicationsFromBothBranches( return nil, nil, time.Since(startTime), err } - namespacedScopedResources, err := argocd.K8sClient.GetListOfNamespacedScopedResources() + namespacedScopedResources, apiVersions, err := argocd.K8sClient.GetNamespacedScopedResourcesAndAPIVersions() if err != nil { - return nil, nil, time.Since(startTime), fmt.Errorf("failed to get list of namespaced scoped resources: %w", err) + return nil, nil, time.Since(startTime), fmt.Errorf("failed to discover API resources: %w", err) } kubeVersion, err := argocd.K8sClient.GetServerVersion() @@ -102,11 +102,6 @@ func RenderApplicationsFromBothBranches( return nil, nil, time.Since(startTime), fmt.Errorf("failed to get server version: %w", err) } - apiVersions, err := argocd.K8sClient.GetAPIVersions() - if err != nil { - return nil, nil, time.Since(startTime), fmt.Errorf("failed to get API versions: %w", err) - } - // Collect all unique repository URLs referenced by the Applications so that // FetchRepoCreds can enrich them with credentials from repo-creds templates. appRepoURLs := collectRepoURLs(baseApps, targetApps) From 34dce12db84fc39d0a78401d9cc73e5ac8c1bfab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 May 2026 13:19:18 +0000 Subject: [PATCH 3/7] Apply three review thread suggestions --- pkg/k8s/discovery.go | 3 +- pkg/reposerverextract/extract.go | 27 +++++++++++--- pkg/reposerverextract/extract_test.go | 52 ++++++++++++++++++++------- 3 files changed, 64 insertions(+), 18 deletions(-) diff --git a/pkg/k8s/discovery.go b/pkg/k8s/discovery.go index 77620ea7..4a8741fb 100644 --- a/pkg/k8s/discovery.go +++ b/pkg/k8s/discovery.go @@ -42,8 +42,7 @@ func (c *Client) GetNamespacedScopedResourcesAndAPIVersions() (map[schema.GroupK seen := make(map[string]bool) var apiVersions []string for _, apiResourceList := range apiResourceLists { - if seen[apiResourceList.GroupVersion] { - } else { + if !seen[apiResourceList.GroupVersion] { seen[apiResourceList.GroupVersion] = true apiVersions = append(apiVersions, apiResourceList.GroupVersion) } diff --git a/pkg/reposerverextract/extract.go b/pkg/reposerverextract/extract.go index 61f65ab7..84ff9f8f 100644 --- a/pkg/reposerverextract/extract.go +++ b/pkg/reposerverextract/extract.go @@ -272,7 +272,19 @@ func renderApp( var allManifestStrings []string for i, contentSource := range contentSources { - request, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSource, refSources, hasMultipleSources, branchFolder, creds, repoSelector, kubeVersion, apiVersions) + request, streamDir, cleanup, err := buildManifestRequestForSource( + app, + contentSource, + refSources, + hasMultipleSources, + branchFolder, + creds, + manifestRequestRenderContext{ + repoSelector: repoSelector, + kubeVersion: kubeVersion, + apiVersions: apiVersions, + }, + ) if err != nil { return nil, fmt.Errorf("failed to build manifest request for content source %d: %w", i, err) } @@ -501,6 +513,12 @@ func splitSources(app argoapplication.ArgoResource) ( // // cleanup must be called by the caller when the stream directory is no longer // needed. +type manifestRequestRenderContext struct { + repoSelector *repository.Selector + kubeVersion string + apiVersions []string +} + func buildManifestRequestForSource( app argoapplication.ArgoResource, primarySource v1alpha1.ApplicationSource, @@ -508,10 +526,11 @@ func buildManifestRequestForSource( hasMultipleSources bool, branchFolder string, creds *RepoCreds, - repoSelector *repository.Selector, - kubeVersion string, - apiVersions []string, + renderContext manifestRequestRenderContext, ) (request *repoapiclient.ManifestRequest, streamDir string, cleanup func(), err error) { + repoSelector := renderContext.repoSelector + kubeVersion := renderContext.kubeVersion + apiVersions := renderContext.apiVersions obj := app.Yaml.Object var specPath []string diff --git a/pkg/reposerverextract/extract_test.go b/pkg/reposerverextract/extract_test.go index 020bde26..92907e19 100644 --- a/pkg/reposerverextract/extract_test.go +++ b/pkg/reposerverextract/extract_test.go @@ -107,7 +107,21 @@ spec: require.Empty(t, refSources) assert.False(t, hasMultipleSources) - req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, ""), "", nil) + kubeVersion := "v1.30.1" + apiVersions := []string{"apps/v1", "v1"} + req, streamDir, cleanup, err := buildManifestRequestForSource( + app, + contentSources[0], + refSources, + hasMultipleSources, + branchFolder, + nil, + manifestRequestRenderContext{ + repoSelector: testRepoSelector(t, ""), + kubeVersion: kubeVersion, + apiVersions: apiVersions, + }, + ) require.NoError(t, err) if cleanup != nil { defer cleanup() @@ -118,6 +132,8 @@ spec: assert.Equal(t, "apps/my-app", req.ApplicationSource.Path) assert.Empty(t, req.ApplicationSource.Chart, "should not have a Chart field") assert.Equal(t, "production", req.Namespace) + assert.Equal(t, kubeVersion, req.KubeVersion) + assert.Equal(t, apiVersions, req.ApiVersions) assert.Nil(t, req.RefSources) assertDefaultProjectFields(t, req) } @@ -149,7 +165,8 @@ spec: require.NoError(t, err) require.Len(t, contentSources, 1) - req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, ""), "", nil) + req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, manifestRequestRenderContext{ + repoSelector: testRepoSelector(t, "")}) require.NoError(t, err) if cleanup != nil { defer cleanup() @@ -217,7 +234,8 @@ spec: require.Len(t, contentSources, 1, "only the chart source is a content source") require.Len(t, refSources, 1) - req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, ""), "", nil) + req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, manifestRequestRenderContext{ + repoSelector: testRepoSelector(t, "")}) require.NoError(t, err) if cleanup != nil { defer cleanup() @@ -299,7 +317,8 @@ spec: require.Len(t, contentSources, 1) require.Len(t, refSources, 1) - req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, ""), "", nil) + req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, manifestRequestRenderContext{ + repoSelector: testRepoSelector(t, "")}) require.NoError(t, err) require.NotEmpty(t, streamDir, "local chart with refs must stream a temp dir") defer cleanup() @@ -410,7 +429,8 @@ spec: } require.NotEmpty(t, chartSource.Chart, "should find the chart content source") - req, streamDir, cleanup, err := buildManifestRequestForSource(app, chartSource, refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, ""), "", nil) + req, streamDir, cleanup, err := buildManifestRequestForSource(app, chartSource, refSources, hasMultipleSources, branchFolder, nil, manifestRequestRenderContext{ + repoSelector: testRepoSelector(t, "")}) require.NoError(t, err) if cleanup != nil { defer cleanup() @@ -470,7 +490,8 @@ spec: require.Len(t, contentSources, 1) require.Len(t, refSources, 1) - req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, ""), "", nil) + req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, manifestRequestRenderContext{ + repoSelector: testRepoSelector(t, "")}) require.NoError(t, err) if cleanup != nil { defer cleanup() @@ -555,7 +576,8 @@ spec: // Capture requests so we can verify per-source paths without duplicate calls. reqs := make([]struct{ path string }, len(contentSources)) for i, cs := range contentSources { - req, streamDir, cleanup, buildErr := buildManifestRequestForSource(app, cs, refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, ""), "", nil) + req, streamDir, cleanup, buildErr := buildManifestRequestForSource(app, cs, refSources, hasMultipleSources, branchFolder, nil, manifestRequestRenderContext{ + repoSelector: testRepoSelector(t, "")}) require.NoError(t, buildErr, "content source %d should not error", i) if cleanup != nil { defer cleanup() @@ -615,7 +637,8 @@ spec: require.Len(t, contentSources, 1) require.Empty(t, refSources) - req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, prRepo), "", nil) + req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, manifestRequestRenderContext{ + repoSelector: testRepoSelector(t, prRepo)}) require.NoError(t, err) if cleanup != nil { defer cleanup() @@ -665,7 +688,8 @@ spec: require.NoError(t, err) require.Len(t, contentSources, 1) - req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, prRepo), "", nil) + req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, manifestRequestRenderContext{ + repoSelector: testRepoSelector(t, prRepo)}) require.NoError(t, err) if cleanup != nil { defer cleanup() @@ -706,7 +730,8 @@ spec: require.Len(t, contentSources, 1) require.Len(t, refSources, 1) - req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, prRepo), "", nil) + req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, manifestRequestRenderContext{ + repoSelector: testRepoSelector(t, prRepo)}) require.NoError(t, err) if cleanup != nil { defer cleanup() @@ -753,7 +778,9 @@ spec: require.Len(t, contentSources, 1) require.Len(t, refSources, 1) - req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, prRepo), "", nil) + req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, manifestRequestRenderContext{ + repoSelector: testRepoSelector(t, prRepo), + }) require.NoError(t, err) if cleanup != nil { defer cleanup() @@ -859,7 +886,8 @@ spec: require.NoError(t, err) require.Len(t, contentSources, 1) - req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, testRepoSelector(t, prRepo), "", nil) + req, streamDir, cleanup, err := buildManifestRequestForSource(app, contentSources[0], refSources, hasMultipleSources, branchFolder, nil, manifestRequestRenderContext{ + repoSelector: testRepoSelector(t, prRepo)}) require.NoError(t, err) if cleanup != nil { defer cleanup() From e822f40fc0396b9d3afce5425977e4bf798d653a Mon Sep 17 00:00:00 2001 From: Dag Andersen Date: Sat, 23 May 2026 17:04:15 +0200 Subject: [PATCH 4/7] Chore | Clarify manifest request render context --- pkg/reposerverextract/extract.go | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/pkg/reposerverextract/extract.go b/pkg/reposerverextract/extract.go index 84ff9f8f..cb3b055b 100644 --- a/pkg/reposerverextract/extract.go +++ b/pkg/reposerverextract/extract.go @@ -513,6 +513,10 @@ func splitSources(app argoapplication.ArgoResource) ( // // cleanup must be called by the caller when the stream directory is no longer // needed. +// manifestRequestRenderContext groups render-run settings that every +// ManifestRequest needs but that are not specific to a single source. Keeping +// these values together avoids a long positional parameter list where adjacent +// strings or slices can be accidentally swapped at call sites. type manifestRequestRenderContext struct { repoSelector *repository.Selector kubeVersion string @@ -528,9 +532,6 @@ func buildManifestRequestForSource( creds *RepoCreds, renderContext manifestRequestRenderContext, ) (request *repoapiclient.ManifestRequest, streamDir string, cleanup func(), err error) { - repoSelector := renderContext.repoSelector - kubeVersion := renderContext.kubeVersion - apiVersions := renderContext.apiVersions obj := app.Yaml.Object var specPath []string @@ -552,8 +553,8 @@ func buildManifestRequestForSource( AppName: app.Id, Namespace: namespace, ApplicationSource: source, - KubeVersion: kubeVersion, - ApiVersions: apiVersions, + KubeVersion: renderContext.kubeVersion, + ApiVersions: renderContext.apiVersions, HasMultipleSources: hasMultipleSources, // Applications are patched to the default project before rendering. We // allow all source repos here so repo-server does not replace Helm @@ -584,11 +585,11 @@ func buildManifestRequestForSource( // repository than the PR repo. Those files are not checked out // locally, so we cannot stream them. Fall back to the remote // GenerateManifest RPC and let the repo server fetch them itself. - if !repoSelector.Matches(primarySource.RepoURL) { + if !renderContext.repoSelector.Matches(primarySource.RepoURL) { log.Debug(). Str("App", app.GetLongName()). Str("sourceRepoURL", primarySource.RepoURL). - Str("prRepo", repoSelector.String()). + Str("prRepo", renderContext.repoSelector.String()). Msg("Source repoURL does not match PR repo - using remote RPC") return newManifestRequest(&primarySource), "", nil, nil } @@ -623,11 +624,11 @@ func buildManifestRequestForSource( // The same applies when a ref source lives outside the PR repository. Copying // the local branch folder into .refs would provide the wrong repository // content, especially for ref-only sources whose Path is empty. - if !repoSelector.Matches(primarySource.RepoURL) || hasExternalRefSource(refSources, repoSelector) { + if !renderContext.repoSelector.Matches(primarySource.RepoURL) || hasExternalRefSource(refSources, renderContext.repoSelector) { log.Debug(). Str("App", app.GetLongName()). Str("sourceRepoURL", primarySource.RepoURL). - Str("prRepo", repoSelector.String()). + Str("prRepo", renderContext.repoSelector.String()). Msg("Source or ref repoURL does not match PR repo (slow path) - using remote RPC") request = newManifestRequest(&primarySource) request.RefSources = buildRefSourcesMap(refSources, creds) From a0e5763147d0431c19eff801f1e9301caebff4a6 Mon Sep 17 00:00:00 2001 From: Dag Andersen Date: Sat, 23 May 2026 20:08:34 +0200 Subject: [PATCH 5/7] Tests | Cover repo-server API capabilities --- .../branch-12/target-1/output.html | 31 +++++++++++++- integration-test/branch-12/target-1/output.md | 40 +++++++++++++------ integration-test/integration_test.go | 20 ++++++++-- .../values.yaml | 26 ++++++++++++ .../with-servicemonitor-crd/values.yaml | 26 ++++++++++++ 5 files changed, 125 insertions(+), 18 deletions(-) create mode 100644 integration-test/with-servicemonitor-crd-no-cluster-roles/values.yaml create mode 100644 integration-test/with-servicemonitor-crd/values.yaml diff --git a/integration-test/branch-12/target-1/output.html b/integration-test/branch-12/target-1/output.html index 03829726..74d6ff8e 100644 --- a/integration-test/branch-12/target-1/output.html +++ b/integration-test/branch-12/target-1/output.html @@ -60,7 +60,7 @@

Argo CD Diff Preview

Summary:

Modified (1):
-± argocd-helm-chart (+2434|-62)
+± argocd-helm-chart (+2437|-65)
@@ -1016,6 +1016,35 @@

StatefulSet: argocd/argocd-helm-chart-application-co

+

ServiceMonitor: argocd/argocd-helm-chart-application-controller

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
     app.kubernetes.io/part-of: argocd
-    app.kubernetes.io/version: v2.13.1
-    helm.sh/chart: argo-cd-7.7.7
+    app.kubernetes.io/version: v3.2.0
+    helm.sh/chart: argo-cd-9.1.4
   name: argocd-helm-chart-application-controller
   namespace: argocd
 spec:
   endpoints:
   - honorLabels: false
-    interval: 30s
+    interval: 15s
     path: /metrics
     port: http-metrics
   namespaceSelector:
     matchNames:
     - argocd
   selector:
     matchLabels:
       app.kubernetes.io/component: application-controller
       app.kubernetes.io/instance: argocd-helm-chart
       app.kubernetes.io/name: argocd-metrics
+
+

ClusterRole: argocd-helm-chart-server

diff --git a/integration-test/branch-12/target-1/output.md b/integration-test/branch-12/target-1/output.md index 25e3dbea..6f37b0ed 100644 --- a/integration-test/branch-12/target-1/output.md +++ b/integration-test/branch-12/target-1/output.md @@ -3,7 +3,7 @@ Summary: ```yaml Modified (1): -± argocd-helm-chart (+2434|-62) +± argocd-helm-chart (+2437|-65) ```
@@ -930,6 +930,31 @@ Modified (1): path: ca.crt optional: true ``` +#### ServiceMonitor: argocd/argocd-helm-chart-application-controller +```diff + app.kubernetes.io/part-of: argocd +- app.kubernetes.io/version: v2.13.1 +- helm.sh/chart: argo-cd-7.7.7 ++ app.kubernetes.io/version: v3.2.0 ++ helm.sh/chart: argo-cd-9.1.4 + name: argocd-helm-chart-application-controller + namespace: argocd + spec: + endpoints: + - honorLabels: false +- interval: 30s ++ interval: 15s + path: /metrics + port: http-metrics + namespaceSelector: + matchNames: + - argocd + selector: + matchLabels: + app.kubernetes.io/component: application-controller + app.kubernetes.io/instance: argocd-helm-chart + app.kubernetes.io/name: argocd-metrics +``` #### ClusterRole: argocd-helm-chart-server ```diff name: argocd-helm-chart-server @@ -1621,18 +1646,7 @@ Modified (1): description: Images is a list of Kustomize image override specifications items: - description: KustomizeImage represents a Kustomize image - definition in the format [old_image_name=]: - type: string - type: array - kubeVersion: - description: |- - KubeVersion specifies the Kubernetes API version to pass to Helm when templating manifests. By default, Argo CD - uses the Kubernetes version of the target cluster. - type: string -+ labelIncludeTemplates: -+ description: LabelIncludeTemplates specifies whether to -+ + description ``` 🚨 Diff is too long diff --git a/integration-test/integration_test.go b/integration-test/integration_test.go index b2a260e9..469d54d7 100644 --- a/integration-test/integration_test.go +++ b/integration-test/integration_test.go @@ -63,6 +63,7 @@ type TestCase struct { RenderMethod string // "cli", "server-api", "repo-server-api", or "" to use global flag DisableClusterRoles string // Use no-cluster-roles/values.yaml (sets createClusterRoles: false) ArgocdConfigDir string // Custom argocd-config directory (relative to integration-test/); overrides auto-derived path + ArgocdConfigDirAPIMode string // Custom argocd-config directory for API render modes; falls back to ArgocdConfigDir when empty ArgocdUIURL string // Argo CD URL for generating application links in diff output TraverseAppOfApps string // If "true", enables recursive child app discovery (--traverse-app-of-apps) RepoRegex string // If set, use --repo-regex instead of --repo @@ -259,6 +260,8 @@ var testCases = []TestCase{ Suffix: "-1", DiffIgnore: "annotations", WatchIfNoWatchPatternFound: "false", + ArgocdConfigDir: "with-servicemonitor-crd", + ArgocdConfigDirAPIMode: "with-servicemonitor-crd-no-cluster-roles", }, { Name: "branch-12/target-2", @@ -268,6 +271,8 @@ var testCases = []TestCase{ DiffIgnore: "annotations", WatchIfNoWatchPatternFound: "false", IgnoreResources: "*:CustomResourceDefinition:*,:ConfigMap:argocd-cm", + ArgocdConfigDir: "with-servicemonitor-crd", + ArgocdConfigDirAPIMode: "with-servicemonitor-crd-no-cluster-roles", }, { Name: "branch-13/target-1", @@ -368,6 +373,13 @@ func isAPIMode(tc TestCase) bool { return m == "server-api" || m == "repo-server-api" } +func effectiveArgocdConfigDir(tc TestCase) string { + if isAPIMode(tc) && tc.ArgocdConfigDirAPIMode != "" { + return tc.ArgocdConfigDirAPIMode + } + return tc.ArgocdConfigDir +} + // timePattern matches timing information in output that varies between runs // Matches patterns like "1m10s]", "24s]", "110s]" var timePattern = regexp.MustCompile(`\d+m?\d*s\]`) @@ -861,8 +873,8 @@ func runWithDocker(tc TestCase, createCluster bool, runDirs RunDirs) error { // When using a custom ArgoCD config directory, mount the entire directory. // Otherwise, when using API mode or DisableClusterRoles is set, mount only the values.yaml file // (which sets createClusterRoles: false) into the default argocd-config path in the container. - if tc.ArgocdConfigDir != "" { - args = append(args, "-v", fmt.Sprintf("%s/integration-test/%s:/argocd-config", repoRoot, tc.ArgocdConfigDir)) + if argocdConfigDir := effectiveArgocdConfigDir(tc); argocdConfigDir != "" { + args = append(args, "-v", fmt.Sprintf("%s/integration-test/%s:/argocd-config", repoRoot, argocdConfigDir)) } else if tc.DisableClusterRoles == "true" || isAPIMode(tc) { args = append(args, "-v", fmt.Sprintf("%s/integration-test/no-cluster-roles/values.yaml:/argocd-config/values.yaml", repoRoot)) } @@ -1051,8 +1063,8 @@ func buildArgs(tc TestCase, createCluster bool, runDirs RunDirs, repoRoot string // pass --argocd-config-dir pointing at the no-cluster-roles directory (createClusterRoles: false). // If ArgocdConfigDir is explicitly set, use that directory instead. // This avoids mutating the shared argocd-config/values.yaml on disk. - if tc.ArgocdConfigDir != "" { - args = append(args, "--argocd-config-dir", filepath.Join(repoRoot, "integration-test", tc.ArgocdConfigDir)) + if argocdConfigDir := effectiveArgocdConfigDir(tc); argocdConfigDir != "" { + args = append(args, "--argocd-config-dir", filepath.Join(repoRoot, "integration-test", argocdConfigDir)) } else if tc.DisableClusterRoles == "true" || isAPIMode(tc) { args = append(args, "--argocd-config-dir", filepath.Join(repoRoot, "integration-test", "no-cluster-roles")) } diff --git a/integration-test/with-servicemonitor-crd-no-cluster-roles/values.yaml b/integration-test/with-servicemonitor-crd-no-cluster-roles/values.yaml new file mode 100644 index 00000000..6aebb8ad --- /dev/null +++ b/integration-test/with-servicemonitor-crd-no-cluster-roles/values.yaml @@ -0,0 +1,26 @@ +# Same as with-servicemonitor-crd, but installs Argo CD without cluster roles +# for API render modes. +createClusterRoles: false + +extraObjects: + - apiVersion: apiextensions.k8s.io/v1 + kind: CustomResourceDefinition + metadata: + name: servicemonitors.monitoring.coreos.com + spec: + group: monitoring.coreos.com + scope: Namespaced + names: + plural: servicemonitors + singular: servicemonitor + kind: ServiceMonitor + shortNames: + - smon + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + x-kubernetes-preserve-unknown-fields: true diff --git a/integration-test/with-servicemonitor-crd/values.yaml b/integration-test/with-servicemonitor-crd/values.yaml new file mode 100644 index 00000000..14f6f8b9 --- /dev/null +++ b/integration-test/with-servicemonitor-crd/values.yaml @@ -0,0 +1,26 @@ +# Install the ServiceMonitor CRD in the test cluster so Argo CD discovers +# monitoring.coreos.com/v1 and passes it to Helm as a cluster capability. +# This lets tests cover charts that gate ServiceMonitor templates with +# .Capabilities.APIVersions.Has "monitoring.coreos.com/v1". +extraObjects: + - apiVersion: apiextensions.k8s.io/v1 + kind: CustomResourceDefinition + metadata: + name: servicemonitors.monitoring.coreos.com + spec: + group: monitoring.coreos.com + scope: Namespaced + names: + plural: servicemonitors + singular: servicemonitor + kind: ServiceMonitor + shortNames: + - smon + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + x-kubernetes-preserve-unknown-fields: true From 35bef4bfd9bcf1da4c398c8d2a1e9d11389b1719 Mon Sep 17 00:00:00 2001 From: Dag Andersen Date: Sun, 24 May 2026 23:27:39 +0200 Subject: [PATCH 6/7] Fix test data --- .../branch-12/target-2/output.html | 31 ++++++++++++++++++- integration-test/branch-12/target-2/output.md | 27 +++++++++++++++- integration-test/integration_test.go | 27 +++++++++++++--- 3 files changed, 79 insertions(+), 6 deletions(-) diff --git a/integration-test/branch-12/target-2/output.html b/integration-test/branch-12/target-2/output.html index d29324f5..214889cc 100644 --- a/integration-test/branch-12/target-2/output.html +++ b/integration-test/branch-12/target-2/output.html @@ -60,7 +60,7 @@

Argo CD Diff Preview

Summary:

Modified (1):
-± argocd-helm-chart (+212|-48)
+± argocd-helm-chart (+215|-51)
@@ -1016,6 +1016,35 @@

StatefulSet: argocd/argocd-helm-chart-application-co

+

ServiceMonitor: argocd/argocd-helm-chart-application-controller

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
     app.kubernetes.io/part-of: argocd
-    app.kubernetes.io/version: v2.13.1
-    helm.sh/chart: argo-cd-7.7.7
+    app.kubernetes.io/version: v3.2.0
+    helm.sh/chart: argo-cd-9.1.4
   name: argocd-helm-chart-application-controller
   namespace: argocd
 spec:
   endpoints:
   - honorLabels: false
-    interval: 30s
+    interval: 15s
     path: /metrics
     port: http-metrics
   namespaceSelector:
     matchNames:
     - argocd
   selector:
     matchLabels:
       app.kubernetes.io/component: application-controller
       app.kubernetes.io/instance: argocd-helm-chart
       app.kubernetes.io/name: argocd-metrics
+
+

ClusterRole: argocd-helm-chart-server

diff --git a/integration-test/branch-12/target-2/output.md b/integration-test/branch-12/target-2/output.md index c9155250..9bdb8137 100644 --- a/integration-test/branch-12/target-2/output.md +++ b/integration-test/branch-12/target-2/output.md @@ -3,7 +3,7 @@ Summary: ```yaml Modified (1): -± argocd-helm-chart (+212|-48) +± argocd-helm-chart (+215|-51) ```
@@ -930,6 +930,31 @@ Modified (1): path: ca.crt optional: true ``` +#### ServiceMonitor: argocd/argocd-helm-chart-application-controller +```diff + app.kubernetes.io/part-of: argocd +- app.kubernetes.io/version: v2.13.1 +- helm.sh/chart: argo-cd-7.7.7 ++ app.kubernetes.io/version: v3.2.0 ++ helm.sh/chart: argo-cd-9.1.4 + name: argocd-helm-chart-application-controller + namespace: argocd + spec: + endpoints: + - honorLabels: false +- interval: 30s ++ interval: 15s + path: /metrics + port: http-metrics + namespaceSelector: + matchNames: + - argocd + selector: + matchLabels: + app.kubernetes.io/component: application-controller + app.kubernetes.io/instance: argocd-helm-chart + app.kubernetes.io/name: argocd-metrics +``` #### ClusterRole: argocd-helm-chart-server ```diff name: argocd-helm-chart-server diff --git a/integration-test/integration_test.go b/integration-test/integration_test.go index 469d54d7..8c063b0f 100644 --- a/integration-test/integration_test.go +++ b/integration-test/integration_test.go @@ -380,6 +380,16 @@ func effectiveArgocdConfigDir(tc TestCase) string { return tc.ArgocdConfigDir } +func effectiveClusterConfig(tc TestCase) string { + if argocdConfigDir := effectiveArgocdConfigDir(tc); argocdConfigDir != "" { + return argocdConfigDir + } + if tc.DisableClusterRoles == "true" || isAPIMode(tc) { + return "no-cluster-roles" + } + return "default" +} + // timePattern matches timing information in output that varies between runs // Matches patterns like "1m10s]", "24s]", "110s]" var timePattern = regexp.MustCompile(`\d+m?\d*s\]`) @@ -432,6 +442,7 @@ func TestIntegration(t *testing.T) { // Track how many tests since last cluster creation testsSinceClusterCreation := 0 + currentClusterConfig := "" // Run each test case for i, tc := range shuffledCases { @@ -442,18 +453,25 @@ func TestIntegration(t *testing.T) { // Check current cluster state clusterExists := kindClusterExists() + requiredClusterConfig := effectiveClusterConfig(tc) - // Check for RBAC mismatch (only relevant if cluster exists) + // Check for cluster configuration mismatch (only relevant if cluster exists) if clusterExists { clusterHasRoles := clusterHasArgocdClusterRoles() // Mismatch if: test wants roles disabled but cluster has them, OR // test wants roles enabled but cluster doesn't have them rbacMismatch := testNeedsRolesDisabled == clusterHasRoles - - if rbacMismatch { - printToTTY("🔄 Deleting cluster due to RBAC configuration mismatch...\n") + configMismatch := currentClusterConfig != "" && currentClusterConfig != requiredClusterConfig + + if rbacMismatch || configMismatch { + reason := "RBAC configuration mismatch" + if configMismatch { + reason = fmt.Sprintf("Argo CD config mismatch: current=%s, required=%s", currentClusterConfig, requiredClusterConfig) + } + printToTTY(fmt.Sprintf("🔄 Deleting cluster due to %s...\n", reason)) _ = deleteKindCluster() clusterExists = false + currentClusterConfig = "" } } @@ -461,6 +479,7 @@ func TestIntegration(t *testing.T) { createCluster := testsSinceClusterCreation >= 15 || !clusterExists || tc.CreateCluster == "true" if createCluster { testsSinceClusterCreation = 0 + currentClusterConfig = requiredClusterConfig } // Print separator to TTY for visibility between test runs From d1e12ad0ce4432e1fa3e86ce002a751f5f064f4c Mon Sep 17 00:00:00 2001 From: Dag Andersen Date: Mon, 25 May 2026 16:47:54 +0200 Subject: [PATCH 7/7] Fix | Address repo-server API review comments --- integration-test/integration_test.go | 6 ++++-- pkg/reposerverextract/appofapps.go | 2 +- pkg/reposerverextract/extract.go | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/integration-test/integration_test.go b/integration-test/integration_test.go index 8c063b0f..06c5e84b 100644 --- a/integration-test/integration_test.go +++ b/integration-test/integration_test.go @@ -455,13 +455,15 @@ func TestIntegration(t *testing.T) { clusterExists := kindClusterExists() requiredClusterConfig := effectiveClusterConfig(tc) - // Check for cluster configuration mismatch (only relevant if cluster exists) + // Check for cluster configuration mismatch (only relevant if cluster exists). + // An empty currentClusterConfig means this test process did not create the + // existing cluster, so the Argo CD values are unknown and must not be reused. if clusterExists { clusterHasRoles := clusterHasArgocdClusterRoles() // Mismatch if: test wants roles disabled but cluster has them, OR // test wants roles enabled but cluster doesn't have them rbacMismatch := testNeedsRolesDisabled == clusterHasRoles - configMismatch := currentClusterConfig != "" && currentClusterConfig != requiredClusterConfig + configMismatch := currentClusterConfig != requiredClusterConfig if rbacMismatch || configMismatch { reason := "RBAC configuration mismatch" diff --git a/pkg/reposerverextract/appofapps.go b/pkg/reposerverextract/appofapps.go index aa83c425..3b64f656 100644 --- a/pkg/reposerverextract/appofapps.go +++ b/pkg/reposerverextract/appofapps.go @@ -163,7 +163,7 @@ func RenderApplicationsFromBothBranchesWithAppOfApps( namespacedScopedResources, apiVersions, err := argocd.K8sClient.GetNamespacedScopedResourcesAndAPIVersions() if err != nil { - return nil, nil, time.Since(startTime), fmt.Errorf("failed to discover API resources: %w", err) + return nil, nil, time.Since(startTime), fmt.Errorf("failed to initialize render context: %w", err) } kubeVersion, err := argocd.K8sClient.GetServerVersion() diff --git a/pkg/reposerverextract/extract.go b/pkg/reposerverextract/extract.go index cb3b055b..053b0c04 100644 --- a/pkg/reposerverextract/extract.go +++ b/pkg/reposerverextract/extract.go @@ -94,7 +94,7 @@ func RenderApplicationsFromBothBranches( namespacedScopedResources, apiVersions, err := argocd.K8sClient.GetNamespacedScopedResourcesAndAPIVersions() if err != nil { - return nil, nil, time.Since(startTime), fmt.Errorf("failed to discover API resources: %w", err) + return nil, nil, time.Since(startTime), fmt.Errorf("failed to initialize render context: %w", err) } kubeVersion, err := argocd.K8sClient.GetServerVersion()