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 @@
+
+
+
+
+ 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 |
+
+
+
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/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 @@
+
+
+
+
+ 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 |
+
+
+
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 b2a260e9..06c5e84b 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,23 @@ 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
+}
+
+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\]`)
@@ -420,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 {
@@ -430,18 +453,27 @@ 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).
+ // 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
-
- if rbacMismatch {
- printToTTY("🔄 Deleting cluster due to RBAC configuration mismatch...\n")
+ configMismatch := 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 = ""
}
}
@@ -449,6 +481,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
@@ -861,8 +894,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 +1084,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
diff --git a/pkg/k8s/discovery.go b/pkg/k8s/discovery.go
index 71c9450a..4a8741fb 100644
--- a/pkg/k8s/discovery.go
+++ b/pkg/k8s/discovery.go
@@ -2,26 +2,50 @@ package k8s
import (
"fmt"
+ "sort"
"strings"
"github.com/rs/zerolog/log"
"k8s.io/apimachinery/pkg/runtime/schema"
)
-// 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)
+// 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 "", err
+ }
+ return v.GitVersion, nil
+}
- // Get all API resources from the cluster
+// 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) {
+ _, apiVersions, err := c.GetNamespacedScopedResourcesAndAPIVersions()
+ if err != nil {
+ return nil, err
+ }
+ return apiVersions, nil
+}
+
+// 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] {
+ seen[apiResourceList.GroupVersion] = true
+ apiVersions = append(apiVersions, apiResourceList.GroupVersion)
+ }
// Parse GroupVersion to extract the group
gv, err := schema.ParseGroupVersion(apiResourceList.GroupVersion)
if err != nil {
@@ -51,6 +75,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 7ef764dc..3b64f656 100644
--- a/pkg/reposerverextract/appofapps.go
+++ b/pkg/reposerverextract/appofapps.go
@@ -161,9 +161,14 @@ 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 initialize render context: %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)
}
// Collect all unique repository URLs referenced by the Applications so that
@@ -347,7 +352,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 +417,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..053b0c04 100644
--- a/pkg/reposerverextract/extract.go
+++ b/pkg/reposerverextract/extract.go
@@ -92,9 +92,14 @@ 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 initialize render context: %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)
}
// Collect all unique repository URLs referenced by the Applications so that
@@ -176,7 +181,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 +256,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 +272,19 @@ 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,
+ 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)
}
@@ -494,6 +513,16 @@ 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
+ apiVersions []string
+}
+
func buildManifestRequestForSource(
app argoapplication.ArgoResource,
primarySource v1alpha1.ApplicationSource,
@@ -501,7 +530,7 @@ func buildManifestRequestForSource(
hasMultipleSources bool,
branchFolder string,
creds *RepoCreds,
- repoSelector *repository.Selector,
+ renderContext manifestRequestRenderContext,
) (request *repoapiclient.ManifestRequest, streamDir string, cleanup func(), err error) {
obj := app.Yaml.Object
@@ -524,6 +553,8 @@ func buildManifestRequestForSource(
AppName: app.Id,
Namespace: namespace,
ApplicationSource: source,
+ 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
@@ -554,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
}
@@ -593,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)
diff --git a/pkg/reposerverextract/extract_test.go b/pkg/reposerverextract/extract_test.go
index a2322768..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, ""))
+ 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, ""))
+ 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, ""))
+ 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, ""))
+ 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, ""))
+ 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, ""))
+ 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, ""))
+ 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))
+ 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))
+ 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))
+ 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))
+ 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))
+ 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()