Files
41536738c8 refactor(cd): hard-fork - rename CRD API group argoproj.io -> deploy.hanzo.ai (#1)
* feat(api): hard-fork CRD API group argoproj.io -> deploy.hanzo.ai

Hanzo CD hard-fork, phase 1.1. Single source of truth is
pkg/apis/application/register.go's Group constant (Application/AppProject/
ApplicationSet all derive their FullName from it); every other Go call-site
that hardcoded the literal group string is updated to match: the 3 generated
informer GVRs, the app-of-apps nested-Application detector in types.go, the
resource-tree Application-kind cache populator, the 3 NotFound error
constructors in server/application, and the applicationset-controller's
leader-election Lease name/ID (kept in lockstep across Go source + RBAC
resourceNames, or leader election would get RBAC-denied on startup).

CRD YAMLs (application/appproject/applicationset) and every RBAC apiGroups
rule that actually governs our 3 CRDs are repointed to deploy.hanzo.ai, across
both manifests/base/* (namespaced Roles) and manifests/cluster-rbac/* (cluster
scope). Two adjacent things that share the "argoproj.io" string but are NOT
ours are deliberately left untouched: gitops-engine's health-check dispatch
for Argo Workflows' `Workflow` kind, and argocd-server-clusterrole's second
apiGroups block granting read on Argo Workflows' `workflows` resource — both
belong to a separate, unrelated Argoproj-family project we are not forking.

Also untouched, per explicit scope: annotation/label keys (argocd.argoproj.io/*)
and finalizers (*.argoproj.io) that track already-applied live resources, and
argocd-cm.yaml's resource.customizations.ignoreResourceUpdates keys (a narrow
app-of-apps diffing nicety, not build- or RBAC-relevant).

CGO_ENABLED=0 go build ./... verified green.

* ci: wire canonical hanzo.yml + cicd.yml (Hanzo CD image build)

Phase 1.3. Follows the same one-way pattern every hanzo/lux/zoo repo uses:
hanzo.yml declares the image (root Dockerfile -> ghcr.io/hanzoai/deploy) and a
fast go-build sanity test (the multi-stage Dockerfile build is the slow, real
gate); cicd.yml is the ~10-line trigger shim importing hanzoai/ci's reusable
build workflow on the arc pool, secrets from KMS only.

No `deploy:` block yet, matching the hanzoai/cms build-only-first-cutover
pattern: this repo's own live hanzo-cd namespace runs the whole Hanzo fleet's
GitOps, so the prod image/CRD swap is a deliberate separate action (see the
[HELD] universe PR), never an automatic rollout on merge. Every PR/push still
builds and pushes an immutable ghcr.io/hanzoai/deploy:sha-<7>-amd64 image.

* feat(ui): rebrand Argo CD -> Hanzo CD (title, favicon, logo, sidebar)

Phase 1.2. User-facing "Argo CD"/"ArgoCD" text replaced with "Hanzo CD"
across the login page, help modal, settings screens, version-info panel,
sync-options warning, fullscreen-logs title, app.tsx page titles, and
index.html's <title>/noscript text (dropped the now-wrong upstream CLI
doc link rather than invent a hanzo.ai URL that doesn't exist).

Logo/favicon assets replaced with the canonical Hanzo mark (never
hand-drawn, copied from ~/work/hanzo/logo/dist): the two real usages of
Argo's logo collapse onto one Hanzo glyph (mono + white variants) at the
same 5 call sites (login icon + footer, login.scss $logoPath/
$logoNegativePath, sidebar text-logo), preserving the existing CSS
invert() filters so both the light login panel and dark sidebar still
render correctly. Old argologo.svg/argo_o.svg/argo.png deleted (no
longer referenced). favicon-{16x16,32x32}.png added at
ui/src/assets/favicon/ (index.html's links to that path were previously
dead - the directory didn't exist in this checkout). package.json name
argo-cd-ui -> hanzo-cd-ui.

Left alone, deliberately: the two Kubernetes resource-kind icon sets
under ui/src/assets/images/resources/ (unrelated to branding), and
upstream doc/project links (argoproj.io, argo-cd.readthedocs.io) since
we have no equivalent hanzo.ai docs to point to yet - except the login
footer logo's outbound link, which now points to hanzo.ai instead of
argoproj.io (a Hanzo-branded mark linking to the upstream project it
was forked from read as an obvious leftover).

* feat(module): rename Go module github.com/argoproj/argo-cd/v3 -> github.com/hanzoai/deploy

Phase 2. No version suffix on the new path (org policy: never publish our
own Go modules above v1.x.x / with a v2+ suffix) — this fork starts a fresh
v0/v1 module identity rather than carrying the v3 the upstream fork point
had reached.

Mechanical, symmetric rewrite: 704 .go files (import statements only; two
disjoint substitutions applied specific-first so the nested gitops-engine
module path never double-matches), plus go.mod (module line, the
gitops-engine require+replace pair, version reset to v0.0.0-... since a
suffix-less path can't carry a v3 semver), gitops-engine/go.mod, Makefile
(ldflags PACKAGE + 2 TEST_MODULE filters), .golangci.yaml, .goreleaser.yaml,
.mockery.yaml (+ gitops-engine's), Procfile, the codegen/proto generation
scripts (hack/generate-proto.sh, update-codegen.sh, update-openapi.sh), and
20 .proto files' go_package options. Also one OpenTelemetry tracer name
string in gitops-engine/pkg/sync/sync_context.go (a cosmestic label, not an
import, but worth keeping consistent).

CGO_ENABLED=0 go build ./... verified green for both the root module and
the nested gitops-engine module; go vet clean on both; go mod tidy run
(network available) to reconcile go.sum, dependency set unchanged.

Left alone, deliberately: doc-only mentions (docs/snyk/*.html scan reports,
gitops-engine/README.md), a comment-only reference in
.github/workflows/release.yaml, and one Renovate bot config line — none are
build- or codegen-load-bearing. Also left alone: comments citing upstream
GitHub issue URLs (github.com/argoproj/argo-cd/issues/NNNNN) and a handful
of test fixtures using "https://github.com/argoproj/argo-cd.git" as a
representative example repo URL — both are accurate historical/test-data
references, not stale self-references.

---------

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-29 18:27:58 -07:00

2534 lines
77 KiB
Go

package generators
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/hanzoai/deploy/applicationset/services/mocks"
"github.com/hanzoai/deploy/pkg/apis/application/v1alpha1"
)
func Test_generateParamsFromGitFile(t *testing.T) {
defaultContent := []byte(`
foo:
bar: baz
`)
type args struct {
filePath string
fileContent []byte
values map[string]string
useGoTemplate bool
goTemplateOptions []string
pathParamPrefix string
}
tests := []struct {
name string
args args
want []map[string]any
wantErr bool
}{
{
name: "empty file returns path parameters",
args: args{
filePath: "path/dir/file_name.yaml",
fileContent: []byte(""),
values: map[string]string{},
useGoTemplate: false,
},
want: []map[string]any{
{
"path": "path/dir",
"path.basename": "dir",
"path.filename": "file_name.yaml",
"path.basenameNormalized": "dir",
"path.filenameNormalized": "file-name.yaml",
"path[0]": "path",
"path[1]": "dir",
},
},
},
{
name: "invalid json/yaml file returns error",
args: args{
filePath: "path/dir/file_name.yaml",
fileContent: []byte("this is not json or yaml"),
values: map[string]string{},
useGoTemplate: false,
},
wantErr: true,
},
{
name: "file parameters are added to params",
args: args{
filePath: "path/dir/file_name.yaml",
fileContent: defaultContent,
values: map[string]string{},
useGoTemplate: false,
},
want: []map[string]any{
{
"foo.bar": "baz",
"path": "path/dir",
"path.basename": "dir",
"path.filename": "file_name.yaml",
"path.basenameNormalized": "dir",
"path.filenameNormalized": "file-name.yaml",
"path[0]": "path",
"path[1]": "dir",
},
},
},
{
name: "path parameter are prefixed",
args: args{
filePath: "path/dir/file_name.yaml",
fileContent: defaultContent,
values: map[string]string{},
useGoTemplate: false,
pathParamPrefix: "myRepo",
},
want: []map[string]any{
{
"foo.bar": "baz",
"myRepo.path": "path/dir",
"myRepo.path.basename": "dir",
"myRepo.path.filename": "file_name.yaml",
"myRepo.path.basenameNormalized": "dir",
"myRepo.path.filenameNormalized": "file-name.yaml",
"myRepo.path[0]": "path",
"myRepo.path[1]": "dir",
},
},
},
{
name: "file parameters are added to params with go template",
args: args{
filePath: "path/dir/file_name.yaml",
fileContent: defaultContent,
values: map[string]string{
"somekey": "{{.path.basename}}",
},
useGoTemplate: true,
},
want: []map[string]any{
{
"values": map[string]string{
"somekey": "dir",
},
"foo": map[string]any{
"bar": "baz",
},
"path": map[string]any{
"path": "path/dir",
"basename": "dir",
"filename": "file_name.yaml",
"basenameNormalized": "dir",
"filenameNormalized": "file-name.yaml",
"segments": []string{
"path",
"dir",
},
},
},
},
},
{
name: "path parameter are prefixed with go template",
args: args{
filePath: "path/dir/file_name.yaml",
fileContent: defaultContent,
values: map[string]string{},
useGoTemplate: true,
pathParamPrefix: "myRepo",
},
want: []map[string]any{
{
"foo": map[string]any{
"bar": "baz",
},
"myRepo": map[string]any{
"path": map[string]any{
"path": "path/dir",
"basename": "dir",
"filename": "file_name.yaml",
"basenameNormalized": "dir",
"filenameNormalized": "file-name.yaml",
"segments": []string{
"path",
"dir",
},
},
},
},
},
},
{
name: "path parameter are prefixed with go template and values",
args: args{
filePath: "path/dir/file_name.yaml",
fileContent: defaultContent,
values: map[string]string{
"somekey": "{{.myRepo.path.basename}}",
},
useGoTemplate: true,
pathParamPrefix: "myRepo",
},
want: []map[string]any{
{
"foo": map[string]any{
"bar": "baz",
},
"myRepo": map[string]any{
"path": map[string]any{
"path": "path/dir",
"basename": "dir",
"filename": "file_name.yaml",
"basenameNormalized": "dir",
"filenameNormalized": "file-name.yaml",
"segments": []string{
"path",
"dir",
},
},
},
"values": map[string]string{
"somekey": "dir",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
params, err := (*GitGenerator)(nil).generateParamsFromGitFile(tt.args.filePath, tt.args.fileContent, tt.args.values, tt.args.useGoTemplate, tt.args.goTemplateOptions, tt.args.pathParamPrefix)
if tt.wantErr {
assert.Error(t, err, "GitGenerator.generateParamsFromGitFile()")
} else {
require.NoError(t, err)
assert.Equal(t, tt.want, params)
}
})
}
}
func TestGitGenerateParamsFromDirectories(t *testing.T) {
t.Parallel()
cases := []struct {
name string
directories []v1alpha1.GitDirectoryGeneratorItem
pathParamPrefix string
repoApps []string
repoError error
values map[string]string
expected []map[string]any
expectedError error
}{
{
name: "happy flow - created apps",
directories: []v1alpha1.GitDirectoryGeneratorItem{{Path: "*"}},
repoApps: []string{
"app1",
"app2",
"app_3",
"p1/app4",
},
expected: []map[string]any{
{"path": "app1", "path.basename": "app1", "path.basenameNormalized": "app1", "path[0]": "app1"},
{"path": "app2", "path.basename": "app2", "path.basenameNormalized": "app2", "path[0]": "app2"},
{"path": "app_3", "path.basename": "app_3", "path.basenameNormalized": "app-3", "path[0]": "app_3"},
},
expectedError: nil,
},
{
name: "It prefixes path parameters with PathParamPrefix",
directories: []v1alpha1.GitDirectoryGeneratorItem{{Path: "*"}},
pathParamPrefix: "myRepo",
repoApps: []string{
"app1",
"app2",
"app_3",
"p1/app4",
},
repoError: nil,
expected: []map[string]any{
{"myRepo.path": "app1", "myRepo.path.basename": "app1", "myRepo.path.basenameNormalized": "app1", "myRepo.path[0]": "app1"},
{"myRepo.path": "app2", "myRepo.path.basename": "app2", "myRepo.path.basenameNormalized": "app2", "myRepo.path[0]": "app2"},
{"myRepo.path": "app_3", "myRepo.path.basename": "app_3", "myRepo.path.basenameNormalized": "app-3", "myRepo.path[0]": "app_3"},
},
expectedError: nil,
},
{
name: "It filters application according to the paths",
directories: []v1alpha1.GitDirectoryGeneratorItem{{Path: "p1/*"}, {Path: "p1/*/*"}},
repoApps: []string{
"app1",
"p1/app2",
"p1/p2/app3",
"p1/p2/p3/app4",
},
expected: []map[string]any{
{"path": "p1/app2", "path.basename": "app2", "path[0]": "p1", "path[1]": "app2", "path.basenameNormalized": "app2"},
{"path": "p1/p2/app3", "path.basename": "app3", "path[0]": "p1", "path[1]": "p2", "path[2]": "app3", "path.basenameNormalized": "app3"},
},
expectedError: nil,
},
{
name: "It filters application according to the paths with Exclude",
directories: []v1alpha1.GitDirectoryGeneratorItem{{Path: "p1/*", Exclude: true}, {Path: "*"}, {Path: "*/*"}},
repoApps: []string{
"app1",
"app2",
"p1/app2",
"p1/app3",
"p2/app3",
},
repoError: nil,
expected: []map[string]any{
{"path": "app1", "path.basename": "app1", "path[0]": "app1", "path.basenameNormalized": "app1"},
{"path": "app2", "path.basename": "app2", "path[0]": "app2", "path.basenameNormalized": "app2"},
{"path": "p2/app3", "path.basename": "app3", "path[0]": "p2", "path[1]": "app3", "path.basenameNormalized": "app3"},
},
expectedError: nil,
},
{
name: "Expecting same exclude behavior with different order",
directories: []v1alpha1.GitDirectoryGeneratorItem{{Path: "*"}, {Path: "*/*"}, {Path: "p1/*", Exclude: true}},
repoApps: []string{
"app1",
"app2",
"p1/app2",
"p1/app3",
"p2/app3",
},
repoError: nil,
expected: []map[string]any{
{"path": "app1", "path.basename": "app1", "path[0]": "app1", "path.basenameNormalized": "app1"},
{"path": "app2", "path.basename": "app2", "path[0]": "app2", "path.basenameNormalized": "app2"},
{"path": "p2/app3", "path.basename": "app3", "path[0]": "p2", "path[1]": "app3", "path.basenameNormalized": "app3"},
},
expectedError: nil,
},
{
name: "Value variable interpolation",
directories: []v1alpha1.GitDirectoryGeneratorItem{{Path: "*"}, {Path: "*/*"}},
repoApps: []string{
"app1",
"p1/app2",
},
repoError: nil,
values: map[string]string{
"foo": "bar",
"aaa": "{{ path[0] }}",
"no-op": "{{ this-does-not-exist }}",
},
expected: []map[string]any{
{"values.foo": "bar", "values.no-op": "{{ this-does-not-exist }}", "values.aaa": "app1", "path": "app1", "path.basename": "app1", "path[0]": "app1", "path.basenameNormalized": "app1"},
{"values.foo": "bar", "values.no-op": "{{ this-does-not-exist }}", "values.aaa": "p1", "path": "p1/app2", "path.basename": "app2", "path[0]": "p1", "path[1]": "app2", "path.basenameNormalized": "app2"},
},
expectedError: nil,
},
{
name: "handles empty response from repo server",
directories: []v1alpha1.GitDirectoryGeneratorItem{{Path: "*"}},
repoApps: []string{},
repoError: nil,
expected: []map[string]any{},
expectedError: nil,
},
{
name: "handles error from repo server",
directories: []v1alpha1.GitDirectoryGeneratorItem{{Path: "*"}},
repoApps: []string{},
repoError: errors.New("error"),
expected: []map[string]any{},
expectedError: errors.New("error generating params from git: error getting directories from repo: error"),
},
}
for _, testCase := range cases {
testCaseCopy := testCase
t.Run(testCaseCopy.name, func(t *testing.T) {
t.Parallel()
argoCDServiceMock := mocks.NewRepos(t)
argoCDServiceMock.EXPECT().GetDirectories(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(testCaseCopy.repoApps, testCaseCopy.repoError)
gitGenerator := NewGitGenerator(argoCDServiceMock, "")
applicationSetInfo := v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "set",
},
Spec: v1alpha1.ApplicationSetSpec{
Generators: []v1alpha1.ApplicationSetGenerator{{
Git: &v1alpha1.GitGenerator{
RepoURL: "RepoURL",
Revision: "Revision",
Directories: testCaseCopy.directories,
PathParamPrefix: testCaseCopy.pathParamPrefix,
Values: testCaseCopy.values,
},
}},
},
}
scheme := runtime.NewScheme()
err := v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
appProject := v1alpha1.AppProject{}
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&appProject).Build()
got, err := gitGenerator.GenerateParams(&applicationSetInfo.Spec.Generators[0], &applicationSetInfo, client)
if testCaseCopy.expectedError != nil {
require.EqualError(t, err, testCaseCopy.expectedError.Error())
} else {
require.NoError(t, err)
assert.Equal(t, testCaseCopy.expected, got)
}
})
}
}
func TestGitGenerateParamsFromDirectoriesGoTemplate(t *testing.T) {
t.Parallel()
cases := []struct {
name string
directories []v1alpha1.GitDirectoryGeneratorItem
pathParamPrefix string
repoApps []string
repoError error
expected []map[string]any
expectedError error
}{
{
name: "happy flow - created apps",
directories: []v1alpha1.GitDirectoryGeneratorItem{{Path: "*"}},
repoApps: []string{
"app1",
"app2",
"app_3",
"p1/app4",
},
repoError: nil,
expected: []map[string]any{
{
"path": map[string]any{
"path": "app1",
"basename": "app1",
"basenameNormalized": "app1",
"segments": []string{
"app1",
},
},
},
{
"path": map[string]any{
"path": "app2",
"basename": "app2",
"basenameNormalized": "app2",
"segments": []string{
"app2",
},
},
},
{
"path": map[string]any{
"path": "app_3",
"basename": "app_3",
"basenameNormalized": "app-3",
"segments": []string{
"app_3",
},
},
},
},
expectedError: nil,
},
{
name: "It prefixes path parameters with PathParamPrefix",
directories: []v1alpha1.GitDirectoryGeneratorItem{{Path: "*"}},
pathParamPrefix: "myRepo",
repoApps: []string{
"app1",
"app2",
"app_3",
"p1/app4",
},
repoError: nil,
expected: []map[string]any{
{
"myRepo": map[string]any{
"path": map[string]any{
"path": "app1",
"basename": "app1",
"basenameNormalized": "app1",
"segments": []string{
"app1",
},
},
},
},
{
"myRepo": map[string]any{
"path": map[string]any{
"path": "app2",
"basename": "app2",
"basenameNormalized": "app2",
"segments": []string{
"app2",
},
},
},
},
{
"myRepo": map[string]any{
"path": map[string]any{
"path": "app_3",
"basename": "app_3",
"basenameNormalized": "app-3",
"segments": []string{
"app_3",
},
},
},
},
},
expectedError: nil,
},
{
name: "It filters application according to the paths",
directories: []v1alpha1.GitDirectoryGeneratorItem{{Path: "p1/*"}, {Path: "p1/*/*"}},
repoApps: []string{
"app1",
"p1/app2",
"p1/p2/app3",
"p1/p2/p3/app4",
},
repoError: nil,
expected: []map[string]any{
{
"path": map[string]any{
"path": "p1/app2",
"basename": "app2",
"basenameNormalized": "app2",
"segments": []string{
"p1",
"app2",
},
},
},
{
"path": map[string]any{
"path": "p1/p2/app3",
"basename": "app3",
"basenameNormalized": "app3",
"segments": []string{
"p1",
"p2",
"app3",
},
},
},
},
expectedError: nil,
},
{
name: "It filters application according to the paths with Exclude",
directories: []v1alpha1.GitDirectoryGeneratorItem{{Path: "p1/*", Exclude: true}, {Path: "*"}, {Path: "*/*"}},
repoApps: []string{
"app1",
"app2",
"p1/app2",
"p1/app3",
"p2/app3",
},
repoError: nil,
expected: []map[string]any{
{
"path": map[string]any{
"path": "app1",
"basename": "app1",
"basenameNormalized": "app1",
"segments": []string{
"app1",
},
},
},
{
"path": map[string]any{
"path": "app2",
"basename": "app2",
"basenameNormalized": "app2",
"segments": []string{
"app2",
},
},
},
{
"path": map[string]any{
"path": "p2/app3",
"basename": "app3",
"basenameNormalized": "app3",
"segments": []string{
"p2",
"app3",
},
},
},
},
expectedError: nil,
},
{
name: "Expecting same exclude behavior with different order",
directories: []v1alpha1.GitDirectoryGeneratorItem{{Path: "*"}, {Path: "*/*"}, {Path: "p1/*", Exclude: true}},
repoApps: []string{
"app1",
"app2",
"p1/app2",
"p1/app3",
"p2/app3",
},
repoError: nil,
expected: []map[string]any{
{
"path": map[string]any{
"path": "app1",
"basename": "app1",
"basenameNormalized": "app1",
"segments": []string{
"app1",
},
},
},
{
"path": map[string]any{
"path": "app2",
"basename": "app2",
"basenameNormalized": "app2",
"segments": []string{
"app2",
},
},
},
{
"path": map[string]any{
"path": "p2/app3",
"basename": "app3",
"basenameNormalized": "app3",
"segments": []string{
"p2",
"app3",
},
},
},
},
expectedError: nil,
},
{
name: "handles empty response from repo server",
directories: []v1alpha1.GitDirectoryGeneratorItem{{Path: "*"}},
repoApps: []string{},
repoError: nil,
expected: []map[string]any{},
expectedError: nil,
},
{
name: "handles error from repo server",
directories: []v1alpha1.GitDirectoryGeneratorItem{{Path: "*"}},
repoApps: []string{},
repoError: errors.New("error"),
expected: []map[string]any{},
expectedError: errors.New("error generating params from git: error getting directories from repo: error"),
},
}
for _, testCase := range cases {
testCaseCopy := testCase
t.Run(testCaseCopy.name, func(t *testing.T) {
t.Parallel()
argoCDServiceMock := mocks.NewRepos(t)
argoCDServiceMock.EXPECT().GetDirectories(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(testCaseCopy.repoApps, testCaseCopy.repoError)
gitGenerator := NewGitGenerator(argoCDServiceMock, "")
applicationSetInfo := v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "set",
},
Spec: v1alpha1.ApplicationSetSpec{
GoTemplate: true,
Generators: []v1alpha1.ApplicationSetGenerator{{
Git: &v1alpha1.GitGenerator{
RepoURL: "RepoURL",
Revision: "Revision",
Directories: testCaseCopy.directories,
PathParamPrefix: testCaseCopy.pathParamPrefix,
},
}},
},
}
scheme := runtime.NewScheme()
err := v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
appProject := v1alpha1.AppProject{}
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&appProject).Build()
got, err := gitGenerator.GenerateParams(&applicationSetInfo.Spec.Generators[0], &applicationSetInfo, client)
if testCaseCopy.expectedError != nil {
require.EqualError(t, err, testCaseCopy.expectedError.Error())
} else {
require.NoError(t, err)
assert.Equal(t, testCaseCopy.expected, got)
}
})
}
}
func TestGitGenerateParamsFromFiles(t *testing.T) {
t.Parallel()
cases := []struct {
name string
// files is the list of paths/globs to match
files []v1alpha1.GitFileGeneratorItem
// repoFileContents maps repo path to the literal contents of that path
repoFileContents map[string][]byte
// if repoPathsError is non-nil, the call to GetPaths(...) will return this error value
repoPathsError error
values map[string]string
expected []map[string]any
expectedError error
}{
{
name: "happy flow: create params from git files",
files: []v1alpha1.GitFileGeneratorItem{{Path: "**/config.json"}},
repoFileContents: map[string][]byte{
"cluster-config/production/config.json": []byte(`{
"cluster": {
"owner": "[email protected]",
"name": "production",
"address": "https://kubernetes.default.svc"
},
"key1": "val1",
"key2": {
"key2_1": "val2_1",
"key2_2": {
"key2_2_1": "val2_2_1"
}
},
"key3": 123
}`),
"cluster-config/staging/config.json": []byte(`{
"cluster": {
"owner": "[email protected]",
"name": "staging",
"address": "https://kubernetes.default.svc"
}
}`),
},
repoPathsError: nil,
expected: []map[string]any{
{
"cluster.owner": "[email protected]",
"cluster.name": "production",
"cluster.address": "https://kubernetes.default.svc",
"key1": "val1",
"key2.key2_1": "val2_1",
"key2.key2_2.key2_2_1": "val2_2_1",
"key3": "123",
"path": "cluster-config/production",
"path.basename": "production",
"path[0]": "cluster-config",
"path[1]": "production",
"path.basenameNormalized": "production",
"path.filename": "config.json",
"path.filenameNormalized": "config.json",
},
{
"cluster.owner": "[email protected]",
"cluster.name": "staging",
"cluster.address": "https://kubernetes.default.svc",
"path": "cluster-config/staging",
"path.basename": "staging",
"path[0]": "cluster-config",
"path[1]": "staging",
"path.basenameNormalized": "staging",
"path.filename": "config.json",
"path.filenameNormalized": "config.json",
},
},
expectedError: nil,
},
{
name: "Value variable interpolation",
files: []v1alpha1.GitFileGeneratorItem{{Path: "**/config.json"}},
repoFileContents: map[string][]byte{
"cluster-config/production/config.json": []byte(`{
"cluster": {
"owner": "[email protected]",
"name": "production",
"address": "https://kubernetes.default.svc"
},
"key1": "val1",
"key2": {
"key2_1": "val2_1",
"key2_2": {
"key2_2_1": "val2_2_1"
}
},
"key3": 123
}`),
"cluster-config/staging/config.json": []byte(`{
"cluster": {
"owner": "[email protected]",
"name": "staging",
"address": "https://kubernetes.default.svc"
}
}`),
},
repoPathsError: nil,
values: map[string]string{
"aaa": "{{ cluster.owner }}",
"no-op": "{{ this-does-not-exist }}",
},
expected: []map[string]any{
{
"cluster.owner": "[email protected]",
"cluster.name": "production",
"cluster.address": "https://kubernetes.default.svc",
"key1": "val1",
"key2.key2_1": "val2_1",
"key2.key2_2.key2_2_1": "val2_2_1",
"key3": "123",
"path": "cluster-config/production",
"path.basename": "production",
"path[0]": "cluster-config",
"path[1]": "production",
"path.basenameNormalized": "production",
"path.filename": "config.json",
"path.filenameNormalized": "config.json",
"values.aaa": "[email protected]",
"values.no-op": "{{ this-does-not-exist }}",
},
{
"cluster.owner": "[email protected]",
"cluster.name": "staging",
"cluster.address": "https://kubernetes.default.svc",
"path": "cluster-config/staging",
"path.basename": "staging",
"path[0]": "cluster-config",
"path[1]": "staging",
"path.basenameNormalized": "staging",
"path.filename": "config.json",
"path.filenameNormalized": "config.json",
"values.aaa": "[email protected]",
"values.no-op": "{{ this-does-not-exist }}",
},
},
expectedError: nil,
},
{
name: "handles error during getting repo paths",
files: []v1alpha1.GitFileGeneratorItem{{Path: "**/config.json"}},
repoFileContents: map[string][]byte{},
repoPathsError: errors.New("paths error"),
expected: []map[string]any{},
expectedError: errors.New("error generating params from git: paths error"),
},
{
name: "test invalid JSON file returns error",
files: []v1alpha1.GitFileGeneratorItem{{Path: "**/config.json"}},
repoFileContents: map[string][]byte{
"cluster-config/production/config.json": []byte(`invalid json file`),
},
repoPathsError: nil,
expected: []map[string]any{},
expectedError: errors.New("error generating params from git: unable to process file 'cluster-config/production/config.json' from repository 'RepoURL': unable to parse file: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type []map[string]interface {}"),
},
{
name: "test JSON array",
files: []v1alpha1.GitFileGeneratorItem{{Path: "**/config.json"}},
repoFileContents: map[string][]byte{
"cluster-config/production/config.json": []byte(`
[
{
"cluster": {
"owner": "[email protected]",
"name": "production",
"address": "https://kubernetes.default.svc",
"inner": {
"one" : "two"
}
}
},
{
"cluster": {
"owner": "[email protected]",
"name": "staging",
"address": "https://kubernetes.default.svc"
}
}
]`),
},
repoPathsError: nil,
expected: []map[string]any{
{
"cluster.owner": "[email protected]",
"cluster.name": "production",
"cluster.address": "https://kubernetes.default.svc",
"cluster.inner.one": "two",
"path": "cluster-config/production",
"path.basename": "production",
"path[0]": "cluster-config",
"path[1]": "production",
"path.basenameNormalized": "production",
"path.filename": "config.json",
"path.filenameNormalized": "config.json",
},
{
"cluster.owner": "[email protected]",
"cluster.name": "staging",
"cluster.address": "https://kubernetes.default.svc",
"path": "cluster-config/production",
"path.basename": "production",
"path[0]": "cluster-config",
"path[1]": "production",
"path.basenameNormalized": "production",
"path.filename": "config.json",
"path.filenameNormalized": "config.json",
},
},
expectedError: nil,
},
{
name: "Test YAML flow",
files: []v1alpha1.GitFileGeneratorItem{{Path: "**/config.yaml"}},
repoFileContents: map[string][]byte{
"cluster-config/production/config.yaml": []byte(`
cluster:
owner: [email protected]
name: production
address: https://kubernetes.default.svc
key1: val1
key2:
key2_1: val2_1
key2_2:
key2_2_1: val2_2_1
`),
"cluster-config/staging/config.yaml": []byte(`
cluster:
owner: [email protected]
name: staging
address: https://kubernetes.default.svc
`),
},
repoPathsError: nil,
expected: []map[string]any{
{
"cluster.owner": "[email protected]",
"cluster.name": "production",
"cluster.address": "https://kubernetes.default.svc",
"key1": "val1",
"key2.key2_1": "val2_1",
"key2.key2_2.key2_2_1": "val2_2_1",
"path": "cluster-config/production",
"path.basename": "production",
"path[0]": "cluster-config",
"path[1]": "production",
"path.basenameNormalized": "production",
"path.filename": "config.yaml",
"path.filenameNormalized": "config.yaml",
},
{
"cluster.owner": "[email protected]",
"cluster.name": "staging",
"cluster.address": "https://kubernetes.default.svc",
"path": "cluster-config/staging",
"path.basename": "staging",
"path[0]": "cluster-config",
"path[1]": "staging",
"path.basenameNormalized": "staging",
"path.filename": "config.yaml",
"path.filenameNormalized": "config.yaml",
},
},
expectedError: nil,
},
{
name: "test YAML array",
files: []v1alpha1.GitFileGeneratorItem{{Path: "**/config.yaml"}},
repoFileContents: map[string][]byte{
"cluster-config/production/config.yaml": []byte(`
- cluster:
owner: [email protected]
name: production
address: https://kubernetes.default.svc
inner:
one: two
- cluster:
owner: [email protected]
name: staging
address: https://kubernetes.default.svc`),
},
repoPathsError: nil,
expected: []map[string]any{
{
"cluster.owner": "[email protected]",
"cluster.name": "production",
"cluster.address": "https://kubernetes.default.svc",
"cluster.inner.one": "two",
"path": "cluster-config/production",
"path.basename": "production",
"path[0]": "cluster-config",
"path[1]": "production",
"path.basenameNormalized": "production",
"path.filename": "config.yaml",
"path.filenameNormalized": "config.yaml",
},
{
"cluster.owner": "[email protected]",
"cluster.name": "staging",
"cluster.address": "https://kubernetes.default.svc",
"path": "cluster-config/production",
"path.basename": "production",
"path[0]": "cluster-config",
"path[1]": "production",
"path.basenameNormalized": "production",
"path.filename": "config.yaml",
"path.filenameNormalized": "config.yaml",
},
},
expectedError: nil,
},
{
name: "test empty YAML array",
files: []v1alpha1.GitFileGeneratorItem{{Path: "**/config.yaml"}},
repoFileContents: map[string][]byte{
"cluster-config/production/config.yaml": []byte(`[]`),
},
repoPathsError: nil,
expected: []map[string]any{},
expectedError: nil,
},
}
for _, testCase := range cases {
testCaseCopy := testCase
t.Run(testCaseCopy.name, func(t *testing.T) {
t.Parallel()
argoCDServiceMock := mocks.NewRepos(t)
argoCDServiceMock.EXPECT().GetFiles(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Return(testCaseCopy.repoFileContents, testCaseCopy.repoPathsError)
gitGenerator := NewGitGenerator(argoCDServiceMock, "")
applicationSetInfo := v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "set",
},
Spec: v1alpha1.ApplicationSetSpec{
Generators: []v1alpha1.ApplicationSetGenerator{{
Git: &v1alpha1.GitGenerator{
RepoURL: "RepoURL",
Revision: "Revision",
Files: testCaseCopy.files,
Values: testCaseCopy.values,
},
}},
},
}
scheme := runtime.NewScheme()
err := v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
appProject := v1alpha1.AppProject{}
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&appProject).Build()
got, err := gitGenerator.GenerateParams(&applicationSetInfo.Spec.Generators[0], &applicationSetInfo, client)
if testCaseCopy.expectedError != nil {
require.EqualError(t, err, testCaseCopy.expectedError.Error())
} else {
require.NoError(t, err)
assert.ElementsMatch(t, testCaseCopy.expected, got)
}
})
}
}
// TestGitGeneratorParamsFromFilesWithExcludeOptionWithNewGlobbing tests the params values generated by git file generator
// when exclude option is set to true. It gives the result files based on new globbing pattern - doublestar package
func TestGitGeneratorParamsFromFilesWithExcludeOptionWithNewGlobbing(t *testing.T) {
t.Parallel()
cases := []struct {
name string
// files is the list of paths/globs to match
files []v1alpha1.GitFileGeneratorItem
// includePattern contains a list of file patterns that needs to be included
includePattern []string
// excludePattern contains a list of file patterns that needs to be excluded
excludePattern []string
// includeFiles is a map with key as absolute path to file and value as the content in bytes that satisfies the includePattern
includeFiles map[string][]byte
// excludeFiles is a map with key as absolute path to file and value as the content in bytes that satisfies the excludePattern
// This means all the files should be excluded
excludeFiles map[string][]byte
// noMatchFiles contains all the files that neither match include pattern nor exclude pattern
// Instead of keeping those files in the excludeFiles map, it is better to keep those files separately
// in a separate field like 'noMatchFiles' to avoid confusion.
noMatchFiles map[string][]byte
// if repoPathsError is non-nil, the call to GetPaths(...) will return this error value
repoPathsError error
values map[string]string
expected []map[string]any
expectedError error
}{
{
name: "filter files according to file-path with exclude",
files: []v1alpha1.GitFileGeneratorItem{
{
Path: "**/config.json",
},
{
Path: "p1/**/config.json",
Exclude: true,
},
},
includePattern: []string{"**/config.json"},
excludePattern: []string{"p1/**/config.json"},
includeFiles: map[string][]byte{
"cluster-config/production/config.json": []byte(`{
"cluster": {
"owner": "[email protected]",
"name": "production",
"address": "https://kubernetes.default.svc"
},
"key1": "val1",
"key2": {
"key2_1": "val2_1",
"key2_2": {
"key2_2_1": "val2_2_1"
}
},
"key3": 123
}
`),
"p1/config.json": []byte(`{
"database": {
"admin": "[email protected]",
"name": "user-data",
"host": "db.internal.local",
"settings": {
"replicas": 3,
"backup": "daily"
}
}
}
`),
"p1/p2/config.json": []byte(``),
},
excludeFiles: map[string][]byte{
"p1/config.json": []byte(`{
"database": {
"admin": "[email protected]",
"name": "user-data",
"host": "db.internal.local",
"settings": {
"replicas": 3,
"backup": "daily"
}
}
}
`),
"p1/p2/config.json": []byte(``),
},
repoPathsError: nil,
expected: []map[string]any{
{
"cluster.owner": "[email protected]",
"cluster.name": "production",
"cluster.address": "https://kubernetes.default.svc",
"key1": "val1",
"key2.key2_1": "val2_1",
"key2.key2_2.key2_2_1": "val2_2_1",
"key3": "123",
"path": "cluster-config/production",
"path.basename": "production",
"path[0]": "cluster-config",
"path[1]": "production",
"path.basenameNormalized": "production",
"path.filename": "config.json",
"path.filenameNormalized": "config.json",
},
},
},
{
name: "filter files according to multiple file-paths with exclude",
files: []v1alpha1.GitFileGeneratorItem{
{Path: "**/config.json"},
{Path: "p1/app2/config.json", Exclude: true},
{Path: "p1/app3/config.json", Exclude: true},
},
includePattern: []string{"**/config.json"},
excludePattern: []string{"p1/app2/config.json", "p1/app3/config.json"},
includeFiles: map[string][]byte{
"p1/config.json": []byte(`{
"cluster": {
"owner": "[email protected]",
"name": "production",
"address": "https://kubernetes.default.svc",
"inner": {
"one" : "two"
}
}
}`),
"p1/app2/config.json": []byte(`{}`),
"p1/app3/config.json": []byte(`{}`),
},
excludeFiles: map[string][]byte{
"p1/app2/config.json": []byte(`{}`),
"p1/app3/config.json": []byte(`{}`),
},
repoPathsError: nil,
expected: []map[string]any{
{
"cluster.owner": "[email protected]",
"cluster.name": "production",
"cluster.address": "https://kubernetes.default.svc",
"cluster.inner.one": "two",
"path": "p1",
"path.basename": "p1",
"path[0]": "p1",
"path.basenameNormalized": "p1",
"path.filename": "config.json",
"path.filenameNormalized": "config.json",
},
},
},
{
name: "docs example test case to filter files according to multiple file-paths with exclude",
files: []v1alpha1.GitFileGeneratorItem{{Path: "cluster-config/**/config.json"}, {Path: "cluster-config/*/dev/config.json", Exclude: true}},
includePattern: []string{"cluster-config/**/config.json"},
excludePattern: []string{"cluster-config/*/dev/config.json"},
includeFiles: map[string][]byte{
"cluster-config/engineering/prod/config.json": []byte(`
cluster:
owner: [email protected]
name: production
address: https://kubernetes.default.svc
`),
"cluster-config/engineering/dev/config.json": []byte(`
cluster:
owner: [email protected]
name: staging
address: https://kubernetes.default.svc
`),
},
excludeFiles: map[string][]byte{
"cluster-config/engineering/dev/config.json": []byte(`
cluster:
owner: [email protected]
name: staging
address: https://kubernetes.default.svc
`),
},
repoPathsError: nil,
expected: []map[string]any{
{
"cluster.owner": "[email protected]",
"cluster.name": "production",
"cluster.address": "https://kubernetes.default.svc",
"path": "cluster-config/engineering/prod",
"path.basename": "prod",
"path[0]": "cluster-config",
"path[1]": "engineering",
"path[2]": "prod",
"path.basenameNormalized": "prod",
"path.filename": "config.json",
"path.filenameNormalized": "config.json",
},
},
},
{
name: "testcase to verify new globbing pattern without any exclude",
files: []v1alpha1.GitFileGeneratorItem{{Path: "some-path/*.yaml"}},
includePattern: []string{"some-path/*.yaml"},
excludePattern: nil,
includeFiles: map[string][]byte{
"some-path/values.yaml": []byte(`
cluster:
owner: [email protected]
name: production
address: https://kubernetes.default.svc
`),
},
excludeFiles: map[string][]byte{},
noMatchFiles: map[string][]byte{
"some-path/staging/values.yaml": []byte(`
cluster:
owner: [email protected]
name: staging
address: https://kubernetes.default.svc
`),
},
repoPathsError: nil,
expected: []map[string]any{
{
"cluster.owner": "[email protected]",
"cluster.name": "production",
"cluster.address": "https://kubernetes.default.svc",
"path": "some-path",
"path.basename": "some-path",
"path[0]": "some-path",
"path.basenameNormalized": "some-path",
"path.filename": "values.yaml",
"path.filenameNormalized": "values.yaml",
},
},
expectedError: nil,
},
{
name: "test to verify the solution for Git File Generator Problem", // https://github.com/argoproj/argo-cd/blob/master/docs/operator-manual/applicationset/Generators-Git-File-Globbing.md#git-file-generator-globbing
files: []v1alpha1.GitFileGeneratorItem{{Path: "cluster-charts/*/*/values.yaml"}, {Path: "cluster-charts/*/values.yaml", Exclude: true}},
includePattern: []string{"cluster-charts/*/*/values.yaml"},
excludePattern: []string{"cluster-charts/*/values.yaml"},
includeFiles: map[string][]byte{
"cluster-charts/cluster1/mychart/values.yaml": []byte(`
env: staging
`),
"cluster-charts/cluster1/myotherchart/values.yaml": []byte(`
env: prod
`),
},
excludeFiles: map[string][]byte{
"cluster-charts/cluster2/values.yaml": []byte(`
env: dev
`),
},
noMatchFiles: map[string][]byte{
"cluster-charts/cluster1/mychart/charts/mysubchart/values.yaml": []byte(`
env: testing
`),
},
repoPathsError: nil,
expected: []map[string]any{
{
"env": "staging",
"path": "cluster-charts/cluster1/mychart",
"path.filenameNormalized": "values.yaml",
"path[0]": "cluster-charts",
"path[1]": "cluster1",
"path[2]": "mychart",
"path.basename": "mychart",
"path.filename": "values.yaml",
"path.basenameNormalized": "mychart",
},
{
"env": "prod",
"path": "cluster-charts/cluster1/myotherchart",
"path.filenameNormalized": "values.yaml",
"path[0]": "cluster-charts",
"path[1]": "cluster1",
"path[2]": "myotherchart",
"path.basename": "myotherchart",
"path.filename": "values.yaml",
"path.basenameNormalized": "myotherchart",
},
},
expectedError: nil,
},
}
for _, testCase := range cases {
testCaseCopy := testCase
t.Run(testCaseCopy.name, func(t *testing.T) {
t.Parallel()
argoCDServiceMock := mocks.NewRepos(t)
// IMPORTANT: we try to get the files from the repo server that matches the patterns
// If we find those files also satisfy the exclude pattern, we remove them from map
// This is generally done by the g.repos.GetFiles() function.
// With the below mock setup, we make sure that if the GetFiles() function gets called
// for a include or exclude pattern, it should always return the includeFiles or excludeFiles.
for _, pattern := range testCaseCopy.excludePattern {
argoCDServiceMock.EXPECT().GetFiles(mock.Anything, mock.Anything, mock.Anything, mock.Anything, pattern, mock.Anything, mock.Anything).
Return(testCaseCopy.excludeFiles, testCaseCopy.repoPathsError)
}
for _, pattern := range testCaseCopy.includePattern {
argoCDServiceMock.EXPECT().GetFiles(mock.Anything, mock.Anything, mock.Anything, mock.Anything, pattern, mock.Anything, mock.Anything).
Return(testCaseCopy.includeFiles, testCaseCopy.repoPathsError)
}
gitGenerator := NewGitGenerator(argoCDServiceMock, "")
applicationSetInfo := v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "set",
},
Spec: v1alpha1.ApplicationSetSpec{
Generators: []v1alpha1.ApplicationSetGenerator{{
Git: &v1alpha1.GitGenerator{
RepoURL: "RepoURL",
Revision: "Revision",
Files: testCaseCopy.files,
Values: testCaseCopy.values,
},
}},
},
}
scheme := runtime.NewScheme()
err := v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
appProject := v1alpha1.AppProject{}
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&appProject).Build()
got, err := gitGenerator.GenerateParams(&applicationSetInfo.Spec.Generators[0], &applicationSetInfo, client)
if testCaseCopy.expectedError != nil {
require.EqualError(t, err, testCaseCopy.expectedError.Error())
} else {
require.NoError(t, err)
assert.ElementsMatch(t, testCaseCopy.expected, got)
}
})
}
}
// TestGitGeneratorParamsFromFilesWithExcludeOptionWithOldGlobbing tests the params values generated by git file generator
// // when exclude option is set to true. It gives the result files based on old globbing pattern - git ls-files
func TestGitGeneratorParamsFromFilesWithExcludeOptionWithOldGlobbing(t *testing.T) {
t.Parallel()
cases := []struct {
name string
// files is the list of paths/globs to match
files []v1alpha1.GitFileGeneratorItem
// includePattern contains a list of file patterns that needs to be included
includePattern []string
// excludePattern contains a list of file patterns that needs to be excluded
excludePattern []string
// includeFiles is a map with key as absolute path to file and value as the content in bytes that satisfies the includePattern
includeFiles map[string][]byte
// excludeFiles is a map with key as absolute path to file and value as the content in bytes that satisfies the excludePattern
// This means all the files should be excluded
excludeFiles map[string][]byte
// noMatchFiles contains all the files that neither match include pattern nor exclude pattern
// Instead of keeping those files in the excludeFiles map, it is better to keep those files separately
// in a separate field like 'noMatchFiles' to avoid confusion.
noMatchFiles map[string][]byte
// if repoPathsError is non-nil, the call to GetPaths(...) will return this error value
repoPathsError error
values map[string]string
expected []map[string]any
expectedError error
}{
{
name: "filter files according to file-path with exclude",
files: []v1alpha1.GitFileGeneratorItem{
{
Path: "**/config.json",
},
{
Path: "p1/**/config.json",
Exclude: true,
},
},
includePattern: []string{"**/config.json"},
excludePattern: []string{"p1/**/config.json"},
includeFiles: map[string][]byte{
"cluster-config/production/config.json": []byte(`{
"cluster": {
"owner": "[email protected]",
"name": "production",
"address": "https://kubernetes.default.svc"
},
"key1": "val1",
"key2": {
"key2_1": "val2_1",
"key2_2": {
"key2_2_1": "val2_2_1"
}
},
"key3": 123
}
`),
"p1/config.json": []byte(`{
"database": {
"admin": "[email protected]",
"name": "user-data",
"host": "db.internal.local",
"settings": {
"replicas": 3,
"backup": "daily"
}
}
}
`),
"p1/p2/config.json": []byte(``),
},
excludeFiles: map[string][]byte{
"p1/config.json": []byte(`{
"database": {
"admin": "[email protected]",
"name": "user-data",
"host": "db.internal.local",
"settings": {
"replicas": 3,
"backup": "daily"
}
}
}
`),
"p1/p2/config.json": []byte(``),
},
repoPathsError: nil,
expected: []map[string]any{
{
"cluster.owner": "[email protected]",
"cluster.name": "production",
"cluster.address": "https://kubernetes.default.svc",
"key1": "val1",
"key2.key2_1": "val2_1",
"key2.key2_2.key2_2_1": "val2_2_1",
"key3": "123",
"path": "cluster-config/production",
"path.basename": "production",
"path[0]": "cluster-config",
"path[1]": "production",
"path.basenameNormalized": "production",
"path.filename": "config.json",
"path.filenameNormalized": "config.json",
},
},
},
{
name: "filter files according to multiple file-paths with exclude",
files: []v1alpha1.GitFileGeneratorItem{
{Path: "**/config.json"},
{Path: "p1/app2/config.json", Exclude: true},
{Path: "p1/app3/config.json", Exclude: true},
},
includePattern: []string{"**/config.json"},
excludePattern: []string{"p1/app2/config.json", "p1/app3/config.json"},
includeFiles: map[string][]byte{
"p1/config.json": []byte(`{
"cluster": {
"owner": "[email protected]",
"name": "production",
"address": "https://kubernetes.default.svc",
"inner": {
"one" : "two"
}
}
}`),
"p1/app2/config.json": []byte(`{}`),
"p1/app3/config.json": []byte(`{}`),
},
excludeFiles: map[string][]byte{
"p1/app2/config.json": []byte(`{}`),
"p1/app3/config.json": []byte(`{}`),
},
repoPathsError: nil,
expected: []map[string]any{
{
"cluster.owner": "[email protected]",
"cluster.name": "production",
"cluster.address": "https://kubernetes.default.svc",
"cluster.inner.one": "two",
"path": "p1",
"path.basename": "p1",
"path[0]": "p1",
"path.basenameNormalized": "p1",
"path.filename": "config.json",
"path.filenameNormalized": "config.json",
},
},
},
{
name: "docs example test case to filter files according to multiple file-paths with exclude",
files: []v1alpha1.GitFileGeneratorItem{{Path: "cluster-config/**/config.json"}, {Path: "cluster-config/*/dev/config.json", Exclude: true}},
includePattern: []string{"cluster-config/**/config.json"},
excludePattern: []string{"cluster-config/*/dev/config.json"},
includeFiles: map[string][]byte{
"cluster-config/engineering/prod/config.json": []byte(`
cluster:
owner: [email protected]
name: production
address: https://kubernetes.default.svc
`),
"cluster-config/engineering/dev/config.json": []byte(`
cluster:
owner: [email protected]
name: staging
address: https://kubernetes.default.svc
`),
},
excludeFiles: map[string][]byte{
"cluster-config/engineering/dev/config.json": []byte(`
cluster:
owner: [email protected]
name: staging
address: https://kubernetes.default.svc
`),
},
repoPathsError: nil,
expected: []map[string]any{
{
"cluster.owner": "[email protected]",
"cluster.name": "production",
"cluster.address": "https://kubernetes.default.svc",
"path": "cluster-config/engineering/prod",
"path.basename": "prod",
"path[0]": "cluster-config",
"path[1]": "engineering",
"path[2]": "prod",
"path.basenameNormalized": "prod",
"path.filename": "config.json",
"path.filenameNormalized": "config.json",
},
},
},
{
name: "testcase to verify new globbing pattern without any exclude",
files: []v1alpha1.GitFileGeneratorItem{{Path: "some-path/*.yaml"}},
includePattern: []string{"some-path/*.yaml"},
excludePattern: nil,
includeFiles: map[string][]byte{
"some-path/values.yaml": []byte(`
cluster:
owner: [email protected]
name: production
address: https://kubernetes.default.svc
`),
"some-path/staging/values.yaml": []byte(`
cluster:
owner: [email protected]
name: staging
address: https://kubernetes.default.svc
`),
},
excludeFiles: map[string][]byte{},
repoPathsError: nil,
expected: []map[string]any{
{
"cluster.owner": "[email protected]",
"cluster.name": "production",
"cluster.address": "https://kubernetes.default.svc",
"path": "some-path",
"path.basename": "some-path",
"path[0]": "some-path",
"path.basenameNormalized": "some-path",
"path.filename": "values.yaml",
"path.filenameNormalized": "values.yaml",
},
{
"cluster.owner": "[email protected]",
"cluster.name": "staging",
"cluster.address": "https://kubernetes.default.svc",
"path": "some-path/staging",
"path.basename": "staging",
"path[0]": "some-path",
"path[1]": "staging",
"path.basenameNormalized": "staging",
"path.filename": "values.yaml",
"path.filenameNormalized": "values.yaml",
},
},
expectedError: nil,
},
{
name: "test to verify the solution for Git File Generator Problem",
files: []v1alpha1.GitFileGeneratorItem{{Path: "cluster-charts/*/*/values.yaml"}, {Path: "cluster-charts/*/values.yaml", Exclude: true}},
includePattern: []string{"cluster-charts/*/*/values.yaml"},
excludePattern: []string{"cluster-charts/*/values.yaml"},
includeFiles: map[string][]byte{
"cluster-charts/cluster1/mychart/values.yaml": []byte(`
env: staging
`),
"cluster-charts/cluster1/myotherchart/values.yaml": []byte(`
env: prod
`),
"cluster-charts/cluster1/mychart/charts/mysubchart/values.yaml": []byte(``),
},
excludeFiles: map[string][]byte{
"cluster-charts/cluster2/values.yaml": []byte(`
env: dev
`),
"cluster-charts/cluster1/mychart/values.yaml": []byte(`
env: staging
`),
"cluster-charts/cluster1/myotherchart/values.yaml": []byte(`
env: prod
`),
"cluster-charts/cluster1/mychart/charts/mysubchart/values.yaml": []byte(``),
},
noMatchFiles: map[string][]byte{
"cluster-charts/cluster1/mychart/charts/mysubchart/values.yaml": []byte(`
env: testing
`),
},
repoPathsError: nil,
expected: []map[string]any{},
expectedError: nil,
},
}
for _, testCase := range cases {
testCaseCopy := testCase
t.Run(testCaseCopy.name, func(t *testing.T) {
t.Parallel()
argoCDServiceMock := mocks.NewRepos(t)
// IMPORTANT: we try to get the files from the repo server that matches the patterns
// If we find those files also satisfy the exclude pattern, we remove them from map
// This is generally done by the g.repos.GetFiles() function.
// With the below mock setup, we make sure that if the GetFiles() function gets called
// for a include or exclude pattern, it should always return the includeFiles or excludeFiles.
for _, pattern := range testCaseCopy.excludePattern {
argoCDServiceMock.EXPECT().GetFiles(mock.Anything, mock.Anything, mock.Anything, mock.Anything, pattern, mock.Anything, mock.Anything).
Return(testCaseCopy.excludeFiles, testCaseCopy.repoPathsError)
}
for _, pattern := range testCaseCopy.includePattern {
argoCDServiceMock.EXPECT().GetFiles(mock.Anything, mock.Anything, mock.Anything, mock.Anything, pattern, mock.Anything, mock.Anything).
Return(testCaseCopy.includeFiles, testCaseCopy.repoPathsError)
}
gitGenerator := NewGitGenerator(argoCDServiceMock, "")
applicationSetInfo := v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "set",
},
Spec: v1alpha1.ApplicationSetSpec{
Generators: []v1alpha1.ApplicationSetGenerator{{
Git: &v1alpha1.GitGenerator{
RepoURL: "RepoURL",
Revision: "Revision",
Files: testCaseCopy.files,
Values: testCaseCopy.values,
},
}},
},
}
scheme := runtime.NewScheme()
err := v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
appProject := v1alpha1.AppProject{}
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&appProject).Build()
got, err := gitGenerator.GenerateParams(&applicationSetInfo.Spec.Generators[0], &applicationSetInfo, client)
if testCaseCopy.expectedError != nil {
require.EqualError(t, err, testCaseCopy.expectedError.Error())
} else {
require.NoError(t, err)
assert.ElementsMatch(t, testCaseCopy.expected, got)
}
})
}
}
// TestGitGeneratorParamsFromFilesWithExcludeOption tests the params values generated by git file generator
// when exclude option is set to true. It gives the result files based on new globbing pattern - doublestar package
func TestGitGeneratorParamsFromFilesWithExcludeOptionGoTemplate(t *testing.T) {
t.Parallel()
cases := []struct {
name string
// files is the list of paths/globs to match
files []v1alpha1.GitFileGeneratorItem
// includePattern contains a list of file patterns that needs to be included
includePattern []string
// excludePattern contains a list of file patterns that needs to be excluded
excludePattern []string
// includeFiles is a map with key as absolute path to file and value as the content in bytes that satisfies the includePattern
includeFiles map[string][]byte
// excludeFiles is a map with key as absolute path to file and value as the content in bytes that satisfies the excludePattern
// This means all the files should be excluded
excludeFiles map[string][]byte
// if repoPathsError is non-nil, the call to GetPaths(...) will return this error value
repoPathsError error
values map[string]string
expected []map[string]any
expectedError error
}{
{
name: "filter files according to file-path with exclude",
files: []v1alpha1.GitFileGeneratorItem{
{
Path: "**/config.json",
},
{
Path: "p1/**/config.json",
Exclude: true,
},
},
includePattern: []string{"**/config.json"},
excludePattern: []string{"p1/**/config.json"},
includeFiles: map[string][]byte{
"cluster-config/production/config.json": []byte(`{
"cluster": {
"owner": "[email protected]",
"name": "production",
"address": "https://kubernetes.default.svc"
},
"key1": "val1",
"key2": {
"key2_1": "val2_1",
"key2_2": {
"key2_2_1": "val2_2_1"
}
},
"key3": 123
}
`),
},
excludeFiles: map[string][]byte{
"p1/p2/config.json": []byte(`{
"service": {
"maintainer": "[email protected]",
"serviceName": "auth-service",
"endpoint": "http://auth.internal.svc",
"config": {
"retries": 5,
"timeout": "30s"
}
}
}
`),
},
repoPathsError: nil,
expected: []map[string]any{
{
"cluster": map[string]any{
"owner": "[email protected]",
"name": "production",
"address": "https://kubernetes.default.svc",
},
"key1": "val1",
"key2": map[string]any{
"key2_1": "val2_1",
"key2_2": map[string]any{
"key2_2_1": "val2_2_1",
},
},
"key3": float64(123),
"path": map[string]any{
"path": "cluster-config/production",
"basename": "production",
"filename": "config.json",
"basenameNormalized": "production",
"filenameNormalized": "config.json",
"segments": []string{
"cluster-config",
"production",
},
},
},
},
expectedError: nil,
},
{
name: "filter files according to multiple file-paths with exclude",
files: []v1alpha1.GitFileGeneratorItem{
{Path: "**/config.json"},
{Path: "p1/app2/config.json", Exclude: true},
{Path: "p1/app3/config.json", Exclude: true},
},
includePattern: []string{"**/config.json"},
excludePattern: []string{"p1/app2/config.json", "p1/app3/config.json"},
includeFiles: map[string][]byte{
"p1/config.json": []byte(`{
"cluster": {
"owner": "[email protected]",
"name": "production",
"address": "https://kubernetes.default.svc",
"inner": {
"one" : "two"
}
}
}`),
},
excludeFiles: map[string][]byte{
"p1/app2/config.json": []byte(`{
"database": {
"admin": "[email protected]",
"env": "staging",
"url": "postgres://db.internal.svc:5432",
"settings": {
"replicas": 3,
"backup": "enabled"
}
}
}
`),
"p1/app3/config.json": []byte(`{
"storage": {
"owner": "[email protected]",
"bucketName": "app-assets",
"region": "us-west-2",
"options": {
"versioning": true,
"encryption": "AES256"
}
}
}
`),
},
repoPathsError: nil,
expected: []map[string]any{
{
"cluster": map[string]any{
"owner": "[email protected]",
"name": "production",
"address": "https://kubernetes.default.svc",
"inner": map[string]any{
"one": "two",
},
},
"path": map[string]any{
"path": "p1",
"basename": "p1",
"filename": "config.json",
"basenameNormalized": "p1",
"filenameNormalized": "config.json",
"segments": []string{
"p1",
},
},
},
},
expectedError: nil,
},
}
for _, testCase := range cases {
testCaseCopy := testCase
t.Run(testCaseCopy.name, func(t *testing.T) {
t.Parallel()
argoCDServiceMock := mocks.NewRepos(t)
// IMPORTANT: we try to get the files from the repo server that matches the patterns
// If we find those files also satisfy the exclude pattern, we remove them from map
// This is generally done by the g.repos.GetFiles() function.
// With the below mock setup, we make sure that if the GetFiles() function gets called
// for a include or exclude pattern, it should always return the includeFiles or excludeFiles.
for _, pattern := range testCaseCopy.excludePattern {
argoCDServiceMock.EXPECT().GetFiles(mock.Anything, mock.Anything, mock.Anything, mock.Anything, pattern, mock.Anything, mock.Anything).
Return(testCaseCopy.excludeFiles, testCaseCopy.repoPathsError)
}
for _, pattern := range testCaseCopy.includePattern {
argoCDServiceMock.EXPECT().GetFiles(mock.Anything, mock.Anything, mock.Anything, mock.Anything, pattern, mock.Anything, mock.Anything).
Return(testCaseCopy.includeFiles, testCaseCopy.repoPathsError)
}
gitGenerator := NewGitGenerator(argoCDServiceMock, "")
applicationSetInfo := v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "set",
},
Spec: v1alpha1.ApplicationSetSpec{
GoTemplate: true,
Generators: []v1alpha1.ApplicationSetGenerator{{
Git: &v1alpha1.GitGenerator{
RepoURL: "RepoURL",
Revision: "Revision",
Files: testCaseCopy.files,
},
}},
},
}
scheme := runtime.NewScheme()
err := v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
appProject := v1alpha1.AppProject{}
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&appProject).Build()
got, err := gitGenerator.GenerateParams(&applicationSetInfo.Spec.Generators[0], &applicationSetInfo, client)
if testCaseCopy.expectedError != nil {
require.EqualError(t, err, testCaseCopy.expectedError.Error())
} else {
require.NoError(t, err)
assert.ElementsMatch(t, testCaseCopy.expected, got)
}
})
}
}
func TestGitGenerateParamsFromFilesGoTemplate(t *testing.T) {
t.Parallel()
cases := []struct {
name string
// files is the list of paths/globs to match
files []v1alpha1.GitFileGeneratorItem
// repoFileContents maps repo path to the literal contents of that path
repoFileContents map[string][]byte
// if repoPathsError is non-nil, the call to GetPaths(...) will return this error value
repoPathsError error
expected []map[string]any
expectedError error
}{
{
name: "happy flow: create params from git files",
files: []v1alpha1.GitFileGeneratorItem{{Path: "**/config.json"}},
repoFileContents: map[string][]byte{
"cluster-config/production/config.json": []byte(`{
"cluster": {
"owner": "[email protected]",
"name": "production",
"address": "https://kubernetes.default.svc"
},
"key1": "val1",
"key2": {
"key2_1": "val2_1",
"key2_2": {
"key2_2_1": "val2_2_1"
}
},
"key3": 123
}`),
"cluster-config/staging/config.json": []byte(`{
"cluster": {
"owner": "[email protected]",
"name": "staging",
"address": "https://kubernetes.default.svc"
}
}`),
},
repoPathsError: nil,
expected: []map[string]any{
{
"cluster": map[string]any{
"owner": "[email protected]",
"name": "production",
"address": "https://kubernetes.default.svc",
},
"key1": "val1",
"key2": map[string]any{
"key2_1": "val2_1",
"key2_2": map[string]any{
"key2_2_1": "val2_2_1",
},
},
"key3": float64(123),
"path": map[string]any{
"path": "cluster-config/production",
"basename": "production",
"filename": "config.json",
"basenameNormalized": "production",
"filenameNormalized": "config.json",
"segments": []string{
"cluster-config",
"production",
},
},
},
{
"cluster": map[string]any{
"owner": "[email protected]",
"name": "staging",
"address": "https://kubernetes.default.svc",
},
"path": map[string]any{
"path": "cluster-config/staging",
"basename": "staging",
"filename": "config.json",
"basenameNormalized": "staging",
"filenameNormalized": "config.json",
"segments": []string{
"cluster-config",
"staging",
},
},
},
},
expectedError: nil,
},
{
name: "handles error during getting repo paths",
files: []v1alpha1.GitFileGeneratorItem{{Path: "**/config.json"}},
repoFileContents: map[string][]byte{},
repoPathsError: errors.New("paths error"),
expected: []map[string]any{},
expectedError: errors.New("error generating params from git: paths error"),
},
{
name: "test invalid JSON file returns error",
files: []v1alpha1.GitFileGeneratorItem{{Path: "**/config.json"}},
repoFileContents: map[string][]byte{
"cluster-config/production/config.json": []byte(`invalid json file`),
},
repoPathsError: nil,
expected: []map[string]any{},
expectedError: errors.New("error generating params from git: unable to process file 'cluster-config/production/config.json' from repository 'RepoURL': unable to parse file: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type []map[string]interface {}"),
},
{
name: "test JSON array",
files: []v1alpha1.GitFileGeneratorItem{{Path: "**/config.json"}},
repoFileContents: map[string][]byte{
"cluster-config/production/config.json": []byte(`
[
{
"cluster": {
"owner": "[email protected]",
"name": "production",
"address": "https://kubernetes.default.svc",
"inner": {
"one" : "two"
}
}
},
{
"cluster": {
"owner": "[email protected]",
"name": "staging",
"address": "https://kubernetes.default.svc"
}
}
]`),
},
repoPathsError: nil,
expected: []map[string]any{
{
"cluster": map[string]any{
"owner": "[email protected]",
"name": "production",
"address": "https://kubernetes.default.svc",
"inner": map[string]any{
"one": "two",
},
},
"path": map[string]any{
"path": "cluster-config/production",
"basename": "production",
"filename": "config.json",
"basenameNormalized": "production",
"filenameNormalized": "config.json",
"segments": []string{
"cluster-config",
"production",
},
},
},
{
"cluster": map[string]any{
"owner": "[email protected]",
"name": "staging",
"address": "https://kubernetes.default.svc",
},
"path": map[string]any{
"path": "cluster-config/production",
"basename": "production",
"filename": "config.json",
"basenameNormalized": "production",
"filenameNormalized": "config.json",
"segments": []string{
"cluster-config",
"production",
},
},
},
},
expectedError: nil,
},
{
name: "Test YAML flow",
files: []v1alpha1.GitFileGeneratorItem{{Path: "**/config.yaml"}},
repoFileContents: map[string][]byte{
"cluster-config/production/config.yaml": []byte(`
cluster:
owner: [email protected]
name: production
address: https://kubernetes.default.svc
key1: val1
key2:
key2_1: val2_1
key2_2:
key2_2_1: val2_2_1
`),
"cluster-config/staging/config.yaml": []byte(`
cluster:
owner: [email protected]
name: staging
address: https://kubernetes.default.svc
`),
},
repoPathsError: nil,
expected: []map[string]any{
{
"cluster": map[string]any{
"owner": "[email protected]",
"name": "production",
"address": "https://kubernetes.default.svc",
},
"key1": "val1",
"key2": map[string]any{
"key2_1": "val2_1",
"key2_2": map[string]any{
"key2_2_1": "val2_2_1",
},
},
"path": map[string]any{
"path": "cluster-config/production",
"basename": "production",
"filename": "config.yaml",
"basenameNormalized": "production",
"filenameNormalized": "config.yaml",
"segments": []string{
"cluster-config",
"production",
},
},
},
{
"cluster": map[string]any{
"owner": "[email protected]",
"name": "staging",
"address": "https://kubernetes.default.svc",
},
"path": map[string]any{
"path": "cluster-config/staging",
"basename": "staging",
"filename": "config.yaml",
"basenameNormalized": "staging",
"filenameNormalized": "config.yaml",
"segments": []string{
"cluster-config",
"staging",
},
},
},
},
expectedError: nil,
},
{
name: "test YAML array",
files: []v1alpha1.GitFileGeneratorItem{{Path: "**/config.yaml"}},
repoFileContents: map[string][]byte{
"cluster-config/production/config.yaml": []byte(`
- cluster:
owner: [email protected]
name: production
address: https://kubernetes.default.svc
inner:
one: two
- cluster:
owner: [email protected]
name: staging
address: https://kubernetes.default.svc`),
},
repoPathsError: nil,
expected: []map[string]any{
{
"cluster": map[string]any{
"owner": "[email protected]",
"name": "production",
"address": "https://kubernetes.default.svc",
"inner": map[string]any{
"one": "two",
},
},
"path": map[string]any{
"path": "cluster-config/production",
"basename": "production",
"filename": "config.yaml",
"basenameNormalized": "production",
"filenameNormalized": "config.yaml",
"segments": []string{
"cluster-config",
"production",
},
},
},
{
"cluster": map[string]any{
"owner": "[email protected]",
"name": "staging",
"address": "https://kubernetes.default.svc",
},
"path": map[string]any{
"path": "cluster-config/production",
"basename": "production",
"filename": "config.yaml",
"basenameNormalized": "production",
"filenameNormalized": "config.yaml",
"segments": []string{
"cluster-config",
"production",
},
},
},
},
expectedError: nil,
},
}
for _, testCase := range cases {
testCaseCopy := testCase
t.Run(testCaseCopy.name, func(t *testing.T) {
t.Parallel()
argoCDServiceMock := mocks.NewRepos(t)
argoCDServiceMock.EXPECT().GetFiles(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Return(testCaseCopy.repoFileContents, testCaseCopy.repoPathsError)
gitGenerator := NewGitGenerator(argoCDServiceMock, "")
applicationSetInfo := v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "set",
},
Spec: v1alpha1.ApplicationSetSpec{
GoTemplate: true,
Generators: []v1alpha1.ApplicationSetGenerator{{
Git: &v1alpha1.GitGenerator{
RepoURL: "RepoURL",
Revision: "Revision",
Files: testCaseCopy.files,
},
}},
},
}
scheme := runtime.NewScheme()
err := v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
appProject := v1alpha1.AppProject{}
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&appProject).Build()
got, err := gitGenerator.GenerateParams(&applicationSetInfo.Spec.Generators[0], &applicationSetInfo, client)
if testCaseCopy.expectedError != nil {
require.EqualError(t, err, testCaseCopy.expectedError.Error())
} else {
require.NoError(t, err)
assert.ElementsMatch(t, testCaseCopy.expected, got)
}
})
}
}
func TestGitGenerator_GenerateParams(t *testing.T) {
cases := []struct {
name string
appProject v1alpha1.AppProject
directories []v1alpha1.GitDirectoryGeneratorItem
pathParamPrefix string
repoApps []string
repoPathsError error
repoFileContents map[string][]byte
values map[string]string
expected []map[string]any
expectedError error
expectedProject *string
appset v1alpha1.ApplicationSet
callGetDirectories bool
}{
{
name: "Signature Verification - ignores templated project field",
repoApps: []string{
"app1",
},
repoPathsError: nil,
appset: v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "set",
Namespace: "namespace",
},
Spec: v1alpha1.ApplicationSetSpec{
Generators: []v1alpha1.ApplicationSetGenerator{{
Git: &v1alpha1.GitGenerator{
RepoURL: "RepoURL",
Revision: "Revision",
Directories: []v1alpha1.GitDirectoryGeneratorItem{{Path: "*"}},
PathParamPrefix: "",
Values: map[string]string{
"foo": "bar",
},
},
}},
Template: v1alpha1.ApplicationSetTemplate{
Spec: v1alpha1.ApplicationSpec{
Project: "{{.project}}",
},
},
},
},
callGetDirectories: true,
expected: []map[string]any{{"path": "app1", "path.basename": "app1", "path.basenameNormalized": "app1", "path[0]": "app1", "values.foo": "bar"}},
expectedError: nil,
},
{
name: "Signature Verification - Checks for non-templated project field",
repoApps: []string{
"app1",
},
repoPathsError: nil,
appset: v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "set",
Namespace: "namespace",
},
Spec: v1alpha1.ApplicationSetSpec{
Generators: []v1alpha1.ApplicationSetGenerator{{
Git: &v1alpha1.GitGenerator{
RepoURL: "RepoURL",
Revision: "Revision",
Directories: []v1alpha1.GitDirectoryGeneratorItem{{Path: "*"}},
PathParamPrefix: "",
Values: map[string]string{
"foo": "bar",
},
},
}},
Template: v1alpha1.ApplicationSetTemplate{
Spec: v1alpha1.ApplicationSpec{
Project: "project",
},
},
},
},
callGetDirectories: false,
expected: []map[string]any{{"path": "app1", "path.basename": "app1", "path.basenameNormalized": "app1", "path[0]": "app1", "values.foo": "bar"}},
expectedError: errors.New("error getting project project: appprojects.argoproj.io \"project\" not found"),
},
{
name: "Project field is not templated - verify that project is passed through to repo-server as-is",
repoApps: []string{
"app1",
},
callGetDirectories: true,
appProject: v1alpha1.AppProject{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{
Name: "project",
Namespace: "argocd",
},
},
appset: v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "set",
Namespace: "namespace",
},
Spec: v1alpha1.ApplicationSetSpec{
Generators: []v1alpha1.ApplicationSetGenerator{{
Git: &v1alpha1.GitGenerator{
RepoURL: "RepoURL",
Revision: "Revision",
Directories: []v1alpha1.GitDirectoryGeneratorItem{{Path: "*"}},
PathParamPrefix: "",
Values: map[string]string{
"foo": "bar",
},
},
}},
Template: v1alpha1.ApplicationSetTemplate{
Spec: v1alpha1.ApplicationSpec{
Project: "project",
},
},
},
},
expected: []map[string]any{{"path": "app1", "path.basename": "app1", "path.basenameNormalized": "app1", "path[0]": "app1", "values.foo": "bar"}},
expectedProject: new("project"),
expectedError: nil,
},
{
name: "Project field is templated - verify that project is passed through to repo-server as empty string",
repoApps: []string{
"app1",
},
callGetDirectories: true,
appset: v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "set",
Namespace: "namespace",
},
Spec: v1alpha1.ApplicationSetSpec{
Generators: []v1alpha1.ApplicationSetGenerator{{
Git: &v1alpha1.GitGenerator{
RepoURL: "RepoURL",
Revision: "Revision",
Directories: []v1alpha1.GitDirectoryGeneratorItem{{Path: "*"}},
PathParamPrefix: "",
Values: map[string]string{
"foo": "bar",
},
},
}},
Template: v1alpha1.ApplicationSetTemplate{
Spec: v1alpha1.ApplicationSpec{
Project: "{{.project}}",
},
},
},
},
expected: []map[string]any{{"path": "app1", "path.basename": "app1", "path.basenameNormalized": "app1", "path[0]": "app1", "values.foo": "bar"}},
expectedProject: new(""),
expectedError: nil,
},
}
for _, testCase := range cases {
argoCDServiceMock := mocks.NewRepos(t)
if testCase.callGetDirectories {
var project any
if testCase.expectedProject != nil {
project = *testCase.expectedProject
} else {
project = mock.Anything
}
argoCDServiceMock.EXPECT().GetDirectories(mock.Anything, mock.Anything, mock.Anything, project, mock.Anything, mock.Anything).Return(testCase.repoApps, testCase.repoPathsError)
}
gitGenerator := NewGitGenerator(argoCDServiceMock, "argocd")
scheme := runtime.NewScheme()
err := v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&testCase.appProject).Build()
got, err := gitGenerator.GenerateParams(&testCase.appset.Spec.Generators[0], &testCase.appset, client)
if testCase.expectedError != nil {
require.EqualError(t, err, testCase.expectedError.Error())
} else {
require.NoError(t, err)
assert.Equal(t, testCase.expected, got)
}
}
}