Skip to content
Open
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
34 changes: 34 additions & 0 deletions pkg/argocd/argocd.go
Original file line number Diff line number Diff line change
Expand Up @@ -981,6 +981,22 @@ func getApplicationSourceType(app *argocdapi.Application, wbc *WriteBackConfig)
return argocdapi.ApplicationSourceTypeDirectory
}

// For SourceHydrator apps, Status.SourceType reflects the sync source (typically
// "Directory" since it syncs rendered manifests), not the dry source. If the DrySource
// has explicit Helm/Kustomize/Plugin config, use that to determine the actual type.
if app.Spec.SourceHydrator != nil {
ds := app.Spec.SourceHydrator.DrySource
if ds.Helm != nil {
return argocdapi.ApplicationSourceTypeHelm
}
if ds.Kustomize != nil {
return argocdapi.ApplicationSourceTypeKustomize
}
if ds.Plugin != nil {
return argocdapi.ApplicationSourceTypePlugin
}
}

return app.Status.SourceType
}

Expand Down Expand Up @@ -1015,6 +1031,24 @@ func getApplicationSource(ctx context.Context, app *argocdapi.Application, wbc *
return &app.Spec.Sources[0]
}

// If the application uses a SourceHydrator, derive the source from its DrySource.
// The dry source is the write-back target: it holds the original (unhydrated) manifests.
// We construct the ApplicationSource manually (rather than using GetDrySource()) to
// include the Helm/Kustomize/Directory/Plugin fields, which downstream code relies on.
if app.Spec.SourceHydrator != nil {
ds := app.Spec.SourceHydrator.DrySource
source := argocdapi.ApplicationSource{
RepoURL: ds.RepoURL,
Path: ds.Path,
TargetRevision: ds.TargetRevision,
Helm: ds.Helm,
Kustomize: ds.Kustomize,
Directory: ds.Directory,
Plugin: ds.Plugin,
}
return &source
}

return app.Spec.Source
}

Expand Down
158 changes: 158 additions & 0 deletions pkg/argocd/argocd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,164 @@ func Test_GetApplicationSource(t *testing.T) {

}

func Test_GetApplicationSource_SourceHydrator(t *testing.T) {
t.Run("Get source with Helm config from DrySource", func(t *testing.T) {
application := &v1alpha1.Application{
ObjectMeta: v1.ObjectMeta{Name: "test-app", Namespace: "testns"},
Spec: v1alpha1.ApplicationSpec{
SourceHydrator: &v1alpha1.SourceHydrator{
DrySource: v1alpha1.DrySource{
RepoURL: "https://example.com/repo.git",
Path: "charts/myapp",
TargetRevision: "main",
Helm: &v1alpha1.ApplicationSourceHelm{
Parameters: []v1alpha1.HelmParameter{{Name: "image.tag", Value: "1.0.0"}},
},
},
},
},
}

appSource := GetApplicationSource(context.Background(), application, nil)
require.NotNil(t, appSource)
assert.Equal(t, "https://example.com/repo.git", appSource.RepoURL)
assert.Equal(t, "charts/myapp", appSource.Path)
assert.Equal(t, "main", appSource.TargetRevision)
require.NotNil(t, appSource.Helm)
assert.Equal(t, "1.0.0", appSource.Helm.Parameters[0].Value)
})

t.Run("Get source with Kustomize config from DrySource", func(t *testing.T) {
application := &v1alpha1.Application{
ObjectMeta: v1.ObjectMeta{Name: "test-app", Namespace: "testns"},
Spec: v1alpha1.ApplicationSpec{
SourceHydrator: &v1alpha1.SourceHydrator{
DrySource: v1alpha1.DrySource{
RepoURL: "https://example.com/repo.git",
Path: "overlays/hub",
Kustomize: &v1alpha1.ApplicationSourceKustomize{},
},
},
},
}

appSource := GetApplicationSource(context.Background(), application, nil)
require.NotNil(t, appSource)
assert.NotNil(t, appSource.Kustomize)
})

t.Run("Get source from DrySource without explicit type config", func(t *testing.T) {
application := &v1alpha1.Application{
ObjectMeta: v1.ObjectMeta{Name: "test-app", Namespace: "testns"},
Spec: v1alpha1.ApplicationSpec{
SourceHydrator: &v1alpha1.SourceHydrator{
DrySource: v1alpha1.DrySource{
RepoURL: "https://example.com/repo.git",
Path: "overlays/hub",
TargetRevision: "main",
},
},
},
}

appSource := GetApplicationSource(context.Background(), application, nil)
require.NotNil(t, appSource)
assert.Equal(t, "main", appSource.TargetRevision)
assert.Nil(t, appSource.Helm)
assert.Nil(t, appSource.Kustomize)
})

t.Run("SourceHydrator takes precedence over nil Source", func(t *testing.T) {
application := &v1alpha1.Application{
ObjectMeta: v1.ObjectMeta{Name: "test-app", Namespace: "testns"},
Spec: v1alpha1.ApplicationSpec{
Source: nil,
SourceHydrator: &v1alpha1.SourceHydrator{
DrySource: v1alpha1.DrySource{
RepoURL: "https://example.com/repo.git",
TargetRevision: "develop",
},
},
},
}

appSource := GetApplicationSource(context.Background(), application, nil)
require.NotNil(t, appSource)
assert.Equal(t, "develop", appSource.TargetRevision)
})
}

func Test_GetApplicationSourceType_SourceHydrator(t *testing.T) {
t.Run("Detect Helm from DrySource", func(t *testing.T) {
application := &v1alpha1.Application{
ObjectMeta: v1.ObjectMeta{Name: "test-app", Namespace: "testns"},
Spec: v1alpha1.ApplicationSpec{
SourceHydrator: &v1alpha1.SourceHydrator{
DrySource: v1alpha1.DrySource{Helm: &v1alpha1.ApplicationSourceHelm{}},
},
},
Status: v1alpha1.ApplicationStatus{SourceType: v1alpha1.ApplicationSourceTypeDirectory},
}
assert.Equal(t, v1alpha1.ApplicationSourceTypeHelm, GetApplicationSourceType(application, nil))
})

t.Run("Detect Kustomize from DrySource", func(t *testing.T) {
application := &v1alpha1.Application{
ObjectMeta: v1.ObjectMeta{Name: "test-app", Namespace: "testns"},
Spec: v1alpha1.ApplicationSpec{
SourceHydrator: &v1alpha1.SourceHydrator{
DrySource: v1alpha1.DrySource{Kustomize: &v1alpha1.ApplicationSourceKustomize{}},
},
},
Status: v1alpha1.ApplicationStatus{SourceType: v1alpha1.ApplicationSourceTypeDirectory},
}
assert.Equal(t, v1alpha1.ApplicationSourceTypeKustomize, GetApplicationSourceType(application, nil))
})

t.Run("Detect Plugin from DrySource", func(t *testing.T) {
application := &v1alpha1.Application{
ObjectMeta: v1.ObjectMeta{Name: "test-app", Namespace: "testns"},
Spec: v1alpha1.ApplicationSpec{
SourceHydrator: &v1alpha1.SourceHydrator{
DrySource: v1alpha1.DrySource{Plugin: &v1alpha1.ApplicationSourcePlugin{}},
},
},
Status: v1alpha1.ApplicationStatus{SourceType: v1alpha1.ApplicationSourceTypeDirectory},
}
assert.Equal(t, v1alpha1.ApplicationSourceTypePlugin, GetApplicationSourceType(application, nil))
})

t.Run("Fall back to Status.SourceType when DrySource has no explicit type", func(t *testing.T) {
application := &v1alpha1.Application{
ObjectMeta: v1.ObjectMeta{Name: "test-app", Namespace: "testns"},
Spec: v1alpha1.ApplicationSpec{
SourceHydrator: &v1alpha1.SourceHydrator{
DrySource: v1alpha1.DrySource{},
},
},
Status: v1alpha1.ApplicationStatus{SourceType: v1alpha1.ApplicationSourceTypeDirectory},
}
assert.Equal(t, v1alpha1.ApplicationSourceTypeDirectory, GetApplicationSourceType(application, nil))
})

t.Run("DrySource Helm overrides misleading Status.SourceType Directory", func(t *testing.T) {
application := &v1alpha1.Application{
ObjectMeta: v1.ObjectMeta{Name: "test-app", Namespace: "testns"},
Spec: v1alpha1.ApplicationSpec{
SourceHydrator: &v1alpha1.SourceHydrator{
DrySource: v1alpha1.DrySource{
Helm: &v1alpha1.ApplicationSourceHelm{
Parameters: []v1alpha1.HelmParameter{{Name: "image.tag", Value: "1.0.0"}},
},
},
},
},
Status: v1alpha1.ApplicationStatus{SourceType: v1alpha1.ApplicationSourceTypeDirectory},
}
assert.Equal(t, v1alpha1.ApplicationSourceTypeHelm, GetApplicationSourceType(application, nil))
})
}

// Test_MultiSourceHelmAndKustomize_SourceTypeDetection demonstrates the bug fixed by the
// write-back target check in getApplicationSourceType.
// The bug scenario:
Expand Down
14 changes: 10 additions & 4 deletions pkg/argocd/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -375,8 +375,11 @@ func marshalParamsOverride(ctx context.Context, applicationImages *ApplicationIm
mergeKustomizeOverride(&params, &newParams)
override, err = marshalWithIndent(params, defaultIndent)
case ApplicationTypeHelm:
if appSource.Helm == nil {
return []byte{}, nil
// Extract Helm parameters safely; Helm may be nil for SourceHydrator apps
// where the source type is auto-detected from files rather than set in the spec.
var helmParams []v1alpha1.HelmParameter
if appSource.Helm != nil {
helmParams = appSource.Helm.Parameters
}

if wbc != nil && !strings.HasPrefix(filepath.Base(wbc.Target), common.DefaultTargetFilePrefix) {
Expand Down Expand Up @@ -425,7 +428,7 @@ func marshalParamsOverride(ctx context.Context, applicationImages *ApplicationIm
}
} else {
// image-tag is present, so continue to process image-tag
helmParamVer := getHelmParam(appSource.Helm.Parameters, helmParamVersion)
helmParamVer := getHelmParam(helmParams, helmParamVersion)
var tagValue string
if helmParamVer == nil {
// Parameter not pre-defined in the Application - use the image's tag data as fallback
Expand All @@ -440,7 +443,7 @@ func marshalParamsOverride(ctx context.Context, applicationImages *ApplicationIm
}
}

helmParamN := getHelmParam(appSource.Helm.Parameters, helmParamName)
helmParamN := getHelmParam(helmParams, helmParamName)
// Determine which value to use for the image name parameter
var valueToSet string
if helmParamN == nil {
Expand Down Expand Up @@ -475,6 +478,9 @@ func marshalParamsOverride(ctx context.Context, applicationImages *ApplicationIm

override, err = marshalWithIndent(&helmNewValues, defaultIndent)
} else {
if appSource.Helm == nil {
return []byte{}, nil
}
var params helmOverride
newParams := helmOverride{
Helm: helmParameters{
Expand Down
83 changes: 83 additions & 0 deletions pkg/argocd/update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3402,6 +3402,89 @@ kustomize:
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(yamlOutput)),
"Output should be valid Kustomize override YAML with updated image")
})

t.Run("SourceHydrator app with nil Helm writes custom values file via fallback", func(t *testing.T) {
expected := `
# auto generated by argocd image updater

image:
tag: v0.0.0
name: nginx
`
app := v1alpha1.Application{
ObjectMeta: v1.ObjectMeta{Name: "hydrator-app"},
Spec: v1alpha1.ApplicationSpec{
SourceHydrator: &v1alpha1.SourceHydrator{
DrySource: v1alpha1.DrySource{},
},
},
Status: v1alpha1.ApplicationStatus{
SourceType: v1alpha1.ApplicationSourceTypeDirectory,
Summary: v1alpha1.ApplicationSummary{Images: []string{"nginx:v0.0.0"}},
},
}
im := NewImage(image.NewFromIdentifier("nginx"))
im.HelmImageName = "image.name"
im.HelmImageTag = "image.tag"
applicationImages := &ApplicationImages{
Application: app,
Images: ImageList{im},
WriteBackConfig: &WriteBackConfig{
Method: WriteBackGit,
Target: "./values.yaml",
},
}

yamlOutput, err := marshalParamsOverride(context.Background(), applicationImages, nil)
require.NoError(t, err)
assert.NotEmpty(t, yamlOutput)
assert.Equal(t, strings.TrimSpace(strings.ReplaceAll(expected, "\t", " ")), strings.TrimSpace(string(yamlOutput)))
})

t.Run("SourceHydrator app with Helm in DrySource writes default override file", func(t *testing.T) {
expected := `
helm:
parameters:
- name: image.name
value: nginx
forcestring: true
- name: image.tag
value: v1.0.0
forcestring: true
`
app := v1alpha1.Application{
ObjectMeta: v1.ObjectMeta{Name: "hydrator-app"},
Spec: v1alpha1.ApplicationSpec{
SourceHydrator: &v1alpha1.SourceHydrator{
DrySource: v1alpha1.DrySource{
Helm: &v1alpha1.ApplicationSourceHelm{
Parameters: []v1alpha1.HelmParameter{
{Name: "image.name", Value: "nginx", ForceString: true},
{Name: "image.tag", Value: "v1.0.0", ForceString: true},
},
},
},
},
},
Status: v1alpha1.ApplicationStatus{SourceType: v1alpha1.ApplicationSourceTypeDirectory},
}
im := NewImage(image.NewFromIdentifier("nginx"))
im.HelmImageName = "image.name"
im.HelmImageTag = "image.tag"
applicationImages := &ApplicationImages{
Application: app,
Images: ImageList{im},
WriteBackConfig: &WriteBackConfig{
Method: WriteBackGit,
Target: ".argocd-source-hydrator-app.yaml",
},
}

yamlOutput, err := marshalParamsOverride(context.Background(), applicationImages, nil)
require.NoError(t, err)
assert.NotEmpty(t, yamlOutput)
assert.Equal(t, strings.TrimSpace(strings.ReplaceAll(expected, "\t", " ")), strings.TrimSpace(string(yamlOutput)))
})
}

func Test_GetHelmValue(t *testing.T) {
Expand Down
Loading