Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
fix(bundler): resolve ArgoCD RepoURL placeholder in child applications (
#519)

Replace escaped Go template placeholder {{ .RepoURL }} in child
application.yaml with actual repo URL passed through ApplicationData.
ArgoCD's directory-based app-of-apps does not template-render discovered
files, so the literal string was used as a Git URL causing
ComparisonError.

Also auto-derive RepoURL and TargetRevision from OCI reference when
using --deployer argocd with OCI output, making --repo optional.
  • Loading branch information
mchmarny committed Apr 9, 2026
commit 0b5d630674f4b40abde0798a1e36759d06ab3511
1 change: 1 addition & 0 deletions pkg/bundler/bundler.go
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ func (b *DefaultBundler) makeArgoCD(ctx context.Context, recipeResult *recipe.Re
ComponentValues: componentValues,
Version: b.Config.Version(),
RepoURL: b.Config.RepoURL(),
TargetRevision: b.Config.TargetRevision(),
IncludeChecksums: b.Config.IncludeChecksums(),
}

Expand Down
15 changes: 15 additions & 0 deletions pkg/bundler/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ type Config struct {
// repoURL specifies the Git repository URL for ArgoCD applications.
repoURL string

// targetRevision specifies the target revision for the ArgoCD repo (default: "main").
targetRevision string

// workloadGateTaint specifies the taint for skyhook-operator runtime required feature.
workloadGateTaint *corev1.Taint

Expand Down Expand Up @@ -209,6 +212,11 @@ func (c *Config) RepoURL() string {
return c.repoURL
}

// TargetRevision returns the target revision for the ArgoCD repo.
func (c *Config) TargetRevision() string {
return c.targetRevision
}

// WorkloadGateTaint returns a copy of the workload gate taint.
func (c *Config) WorkloadGateTaint() *corev1.Taint {
if c.workloadGateTaint == nil {
Expand Down Expand Up @@ -362,6 +370,13 @@ func WithRepoURL(repoURL string) Option {
}
}

// WithTargetRevision sets the target revision for the ArgoCD repo.
func WithTargetRevision(targetRevision string) Option {
return func(c *Config) {
c.targetRevision = targetRevision
}
}

// WithWorkloadGateTaint sets the taint for skyhook-operator runtime required feature.
func WithWorkloadGateTaint(taint *corev1.Taint) Option {
return func(c *Config) {
Expand Down
14 changes: 14 additions & 0 deletions pkg/bundler/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,20 @@ func TestDeployerOptions(t *testing.T) {
t.Errorf("RepoURL() = %s, want empty string", cfg.RepoURL())
}
})

t.Run("WithTargetRevision sets target revision", func(t *testing.T) {
cfg := NewConfig(WithTargetRevision("v1.0.0"))
if cfg.TargetRevision() != "v1.0.0" {
t.Errorf("TargetRevision() = %s, want v1.0.0", cfg.TargetRevision())
}
})

t.Run("default TargetRevision is empty", func(t *testing.T) {
cfg := NewConfig()
if cfg.TargetRevision() != "" {
t.Errorf("TargetRevision() = %s, want empty string", cfg.TargetRevision())
}
})
}

func TestParseValueOverrides(t *testing.T) {
Expand Down
65 changes: 42 additions & 23 deletions pkg/bundler/deployer/argocd/argocd.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,17 @@ var readmeTemplate string

// ApplicationData contains data for rendering an ArgoCD Application.
type ApplicationData struct {
Name string
Namespace string
Repository string
Chart string
Version string
SyncWave int
IsKustomize bool // True when the component uses Kustomize instead of Helm
Tag string // Git ref for Kustomize components (tag, branch, or commit)
Path string // Path within the repository to the kustomization
Name string
Namespace string
Repository string
Chart string
Version string
SyncWave int
IsKustomize bool // True when the component uses Kustomize instead of Helm
Tag string // Git ref for Kustomize components (tag, branch, or commit)
Path string // Path within the repository to the kustomization
RepoURL string // Values repo URL for multi-source Helm apps
TargetRevision string // Target revision for values repo
}

// AppOfAppsData contains data for rendering the App of Apps manifest.
Expand Down Expand Up @@ -80,6 +82,9 @@ type GeneratorInput struct {
// If empty, a placeholder URL will be used.
RepoURL string

// TargetRevision is the target revision for the repo (default: "main").
TargetRevision string

// IncludeChecksums indicates whether to generate a checksums.txt file.
IncludeChecksums bool
}
Expand Down Expand Up @@ -110,6 +115,20 @@ func NewGenerator() *Generator {
return &Generator{}
}

// resolveRepoSettings returns the effective repoURL and targetRevision,
// applying defaults when the input values are empty.
func resolveRepoSettings(input *GeneratorInput) (repoURL, targetRevision string) {
repoURL = input.RepoURL
if repoURL == "" {
repoURL = "https://github.com/YOUR-ORG/YOUR-REPO.git"
}
targetRevision = input.TargetRevision
if targetRevision == "" {
targetRevision = "main"
}
return repoURL, targetRevision
}

// Generate creates ArgoCD Applications from the given input.
func (g *Generator) Generate(ctx context.Context, input *GeneratorInput, outputDir string) (*GeneratorOutput, error) {
start := time.Now()
Expand All @@ -128,6 +147,8 @@ func (g *Generator) Generate(ctx context.Context, input *GeneratorInput, outputD
"failed to create output directory", err)
}

repoURL, targetRevision := resolveRepoSettings(input)

// Sort components by deployment order
components := shared.SortComponentRefsByDeploymentOrder(
input.RecipeResult.ComponentRefs,
Expand All @@ -150,15 +171,17 @@ func (g *Generator) Generate(ctx context.Context, input *GeneratorInput, outputD
}

appData := ApplicationData{
Name: comp.Name,
Namespace: comp.Namespace,
Repository: comp.Source,
Chart: chartName,
Version: shared.NormalizeVersion(comp.Version),
SyncWave: i, // Use index as sync wave
IsKustomize: isKustomize,
Tag: comp.Tag,
Path: comp.Path,
Name: comp.Name,
Namespace: comp.Namespace,
Repository: comp.Source,
Chart: chartName,
Version: shared.NormalizeVersion(comp.Version),
SyncWave: i, // Use index as sync wave
IsKustomize: isKustomize,
Tag: comp.Tag,
Path: comp.Path,
RepoURL: repoURL,
TargetRevision: targetRevision,
}
appDataList = append(appDataList, appData)
}
Expand Down Expand Up @@ -206,13 +229,9 @@ func (g *Generator) Generate(ctx context.Context, input *GeneratorInput, outputD
}

// Generate app-of-apps.yaml
repoURL := input.RepoURL
if repoURL == "" {
repoURL = "https://github.com/YOUR-ORG/YOUR-REPO.git"
}
appOfAppsData := AppOfAppsData{
RepoURL: repoURL,
TargetRevision: "main",
TargetRevision: targetRevision,
Path: ".",
}
appOfAppsPath, appOfAppsSize, err := shared.GenerateFromTemplate(appOfAppsTemplate, appOfAppsData, outputDir, "app-of-apps.yaml")
Expand Down
115 changes: 115 additions & 0 deletions pkg/bundler/deployer/argocd/argocd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,121 @@ func TestGenerate_WithRepoURL(t *testing.T) {
if !strings.Contains(string(content), customRepoURL) {
t.Error("app-of-apps.yaml should contain custom repo URL")
}

// Verify child application.yaml contains custom repo URL in values source
gpuOperatorApp := filepath.Join(outputDir, "gpu-operator", "application.yaml")
appContent, err := os.ReadFile(gpuOperatorApp)
if err != nil {
t.Fatalf("Failed to read gpu-operator application.yaml: %v", err)
}
if !strings.Contains(string(appContent), customRepoURL) {
t.Errorf("application.yaml should contain custom repo URL %s, got:\n%s", customRepoURL, string(appContent))
}
}

func TestGenerate_WithOCIRepoURL(t *testing.T) {
g := NewGenerator()
ctx := context.Background()
outputDir := t.TempDir()

ociRepoURL := "nvcr.io/foo/aicr-bundles"
ociTag := "v0.0.1"

recipeResult := &recipe.RecipeResult{}
recipeResult.Metadata.Version = testVersion
recipeResult.ComponentRefs = []recipe.ComponentRef{
{
Name: "gpu-operator",
Namespace: "gpu-operator",
Chart: "gpu-operator",
Version: "v25.3.3",
Type: "helm",
Source: "https://helm.ngc.nvidia.com/nvidia",
},
}

input := &GeneratorInput{
RecipeResult: recipeResult,
ComponentValues: map[string]map[string]any{"gpu-operator": {}},
Version: "v0.9.0",
RepoURL: ociRepoURL,
TargetRevision: ociTag,
}

_, err := g.Generate(ctx, input, outputDir)
if err != nil {
t.Fatalf("Generate() error = %v", err)
}

// Verify app-of-apps uses OCI repo URL and tag
appOfApps, err := os.ReadFile(filepath.Join(outputDir, "app-of-apps.yaml"))
if err != nil {
t.Fatalf("Failed to read app-of-apps.yaml: %v", err)
}
if !strings.Contains(string(appOfApps), ociRepoURL) {
t.Error("app-of-apps.yaml should contain OCI repo URL")
}
if !strings.Contains(string(appOfApps), ociTag) {
t.Error("app-of-apps.yaml should contain OCI tag as targetRevision")
}

// Verify child application uses OCI repo URL and tag
gpuApp, err := os.ReadFile(filepath.Join(outputDir, "gpu-operator", "application.yaml"))
if err != nil {
t.Fatalf("Failed to read gpu-operator application.yaml: %v", err)
}
gpuAppStr := string(gpuApp)
if !strings.Contains(gpuAppStr, ociRepoURL) {
t.Errorf("application.yaml should contain OCI repo URL, got:\n%s", gpuAppStr)
}
if !strings.Contains(gpuAppStr, ociTag) {
t.Errorf("application.yaml should contain OCI tag as targetRevision, got:\n%s", gpuAppStr)
}
if strings.Contains(gpuAppStr, "{{ .RepoURL }}") {
t.Error("application.yaml should not contain literal {{ .RepoURL }} placeholder")
}
}

func TestGenerate_DefaultRepoURL_InChildApplications(t *testing.T) {
g := NewGenerator()
ctx := context.Background()
outputDir := t.TempDir()

recipeResult := &recipe.RecipeResult{}
recipeResult.Metadata.Version = testVersion
recipeResult.ComponentRefs = []recipe.ComponentRef{
{
Name: "gpu-operator",
Namespace: "gpu-operator",
Chart: "gpu-operator",
Version: "v25.3.3",
Type: "helm",
Source: "https://helm.ngc.nvidia.com/nvidia",
},
}

input := &GeneratorInput{
RecipeResult: recipeResult,
ComponentValues: map[string]map[string]any{"gpu-operator": {}},
Version: "v0.9.0",
}

_, err := g.Generate(ctx, input, outputDir)
if err != nil {
t.Fatalf("Generate() error = %v", err)
}

gpuApp, err := os.ReadFile(filepath.Join(outputDir, "gpu-operator", "application.yaml"))
if err != nil {
t.Fatalf("Failed to read application.yaml: %v", err)
}
gpuAppStr := string(gpuApp)
if !strings.Contains(gpuAppStr, "YOUR-ORG/YOUR-REPO") {
t.Errorf("application.yaml should contain placeholder URL, got:\n%s", gpuAppStr)
}
if strings.Contains(gpuAppStr, "{{ .RepoURL }}") {
t.Error("application.yaml should not contain literal {{ .RepoURL }} placeholder")
}
}

func TestGenerate_WithChecksums(t *testing.T) {
Expand Down
4 changes: 2 additions & 2 deletions pkg/bundler/deployer/argocd/templates/application.yaml.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ spec:
helm:
valueFiles:
- $values/{{ .Name }}/values.yaml
- repoURL: '{{ `{{ .RepoURL }}` }}'
targetRevision: main
- repoURL: '{{ .RepoURL }}'
targetRevision: {{ .TargetRevision }}
ref: values
{{- end }}
destination:
Expand Down
13 changes: 13 additions & 0 deletions pkg/cli/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type bundleCmdOptions struct {
workloadGateTaint *corev1.Taint
workloadSelector map[string]string
estimatedNodeCount int
targetRevision string

// attest enables bundle attestation and binary verification.
attest bool
Expand Down Expand Up @@ -138,6 +139,17 @@ func parseBundleCmdOptions(cmd *cli.Command) (*bundleCmdOptions, error) {
opts.outputDir = absOut
}

// When using ArgoCD deployer with OCI output and no explicit --repo,
// auto-populate repoURL from the OCI reference (issue #519).
if opts.deployer == config.DeployerArgoCD && opts.ociRef != nil && opts.repoURL == "" {
opts.repoURL = opts.ociRef.Registry + "/" + opts.ociRef.Repository
}

// Derive target revision: use OCI tag when available
if opts.ociRef != nil && opts.ociRef.Tag != "" {
opts.targetRevision = opts.ociRef.Tag
}

// Parse value overrides from --set flags
opts.valueOverrides, err = config.ParseValueOverrides(cmd.StringSlice("set"))
if err != nil {
Expand Down Expand Up @@ -394,6 +406,7 @@ func runBundleCmd(ctx context.Context, cmd *cli.Command) error {
config.WithVersion(version),
config.WithDeployer(opts.deployer),
config.WithRepoURL(opts.repoURL),
config.WithTargetRevision(opts.targetRevision),
config.WithAttest(opts.attest),
config.WithCertificateIdentityRegexp(opts.certificateIdentityRegexp),
config.WithValueOverrides(opts.valueOverrides),
Expand Down