diff --git a/pkg/controllers/custompackage/controller.go b/pkg/controllers/custompackage/controller.go index 12480bef..ed093616 100644 --- a/pkg/controllers/custompackage/controller.go +++ b/pkg/controllers/custompackage/controller.go @@ -2,6 +2,7 @@ package custompackage import ( "context" + "encoding/json" "fmt" "os" "path/filepath" @@ -165,10 +166,16 @@ func (r *Reconciler) reconcileArgoCDApp(ctx context.Context, resource *v1alpha1. notSyncedRepos := 0 for j := range app.Spec.Sources { s := &app.Spec.Sources[j] + res, sErr := r.reconcileHelmValueObject(ctx, s, resource, app.Name) + if sErr != nil { + return res, sErr + } + res, repo, sErr := r.reconcileArgoCDSource(ctx, resource, s.RepoURL, app.Name) if sErr != nil { return res, sErr } + if repo != nil { if repo.Status.InternalGitRepositoryUrl == "" { notSyncedRepos += 1 @@ -184,10 +191,16 @@ func (r *Reconciler) reconcileArgoCDApp(ctx context.Context, resource *v1alpha1. appSourcesSynced = notSyncedRepos == 0 } else { s := app.Spec.Source + res, sErr := r.reconcileHelmValueObject(ctx, s, resource, app.Name) + if sErr != nil { + return res, sErr + } + res, repo, sErr := r.reconcileArgoCDSource(ctx, resource, s.RepoURL, app.Name) if sErr != nil { return res, sErr } + if repo != nil { appSourcesSynced = repo.Status.InternalGitRepositoryUrl != "" s.RepoURL = repo.Status.InternalGitRepositoryUrl @@ -391,6 +404,68 @@ func (r *Reconciler) getArgoCDAppFile(ctx context.Context, resource *v1alpha1.Cu return util.ReadWorktreeFile(wt, filePath) } +func (r *Reconciler) reconcileHelmValueObject(ctx context.Context, source *argov1alpha1.ApplicationSource, + resource *v1alpha1.CustomPackage, appName string, +) (ctrl.Result, error) { + if source.Helm == nil || source.Helm.ValuesObject == nil { + return ctrl.Result{}, nil + } + + var data any + err := json.Unmarshal(source.Helm.ValuesObject.Raw, &data) + if err != nil { + return ctrl.Result{}, fmt.Errorf("processing helm valuesObject: %w", err) + } + + res, err := r.reconcileHelmValueObjectSource(ctx, &data, resource, appName) + if err != nil { + return ctrl.Result{}, err + } + + raw, err := json.Marshal(data) + if err != nil { + return ctrl.Result{}, fmt.Errorf("converting helm valuesObject to json") + } + + source.Helm.ValuesObject.Raw = raw + return res, nil +} + +func (r *Reconciler) reconcileHelmValueObjectSource(ctx context.Context, + valueObject *any, resource *v1alpha1.CustomPackage, appName string, +) (ctrl.Result, error) { + + switch val := (*valueObject).(type) { + case string: + res, repo, err := r.reconcileArgoCDSource(ctx, resource, val, appName) + if err != nil { + return res, fmt.Errorf("processing %s in helmValueObject: %w", val, err) + } + if repo != nil { + *valueObject = repo.Status.InternalGitRepositoryUrl + } + case map[string]any: + for k := range val { + v := val[k] + res, err := r.reconcileHelmValueObjectSource(ctx, &v, resource, appName) + if err != nil { + return res, err + } + val[k] = v + } + case []any: + for k := range val { + v := val[k] + res, err := r.reconcileHelmValueObjectSource(ctx, &v, resource, appName) + if err != nil { + return res, err + } + val[k] = v + } + } + return ctrl.Result{RequeueAfter: requeueTime}, nil +} + func localRepoName(appName, dir string) string { return fmt.Sprintf("%s-%s", appName, filepath.Base(dir)) } diff --git a/pkg/controllers/custompackage/controller_test.go b/pkg/controllers/custompackage/controller_test.go index bf6d1248..578138b6 100644 --- a/pkg/controllers/custompackage/controller_test.go +++ b/pkg/controllers/custompackage/controller_test.go @@ -577,3 +577,126 @@ func TestReconcileCustomPkgAppSet(t *testing.T) { } } } + +func TestReconcileHelmValueObject(t *testing.T) { + s := k8sruntime.NewScheme() + sb := k8sruntime.NewSchemeBuilder( + v1.AddToScheme, + argov1alpha1.AddToScheme, + v1alpha1.AddToScheme, + ) + sb.AddToScheme(s) + testEnv := &envtest.Environment{ + CRDDirectoryPaths: []string{ + filepath.Join("..", "resources"), + "../localbuild/resources/argo/install.yaml", + }, + ErrorIfCRDPathMissing: true, + Scheme: s, + BinaryAssetsDirectory: filepath.Join("..", "..", "..", "bin", "k8s", + fmt.Sprintf("1.29.1-%s-%s", runtime.GOOS, runtime.GOARCH)), + } + + cfg, err := testEnv.Start() + if err != nil { + t.Fatalf("Starting testenv: %v", err) + } + defer testEnv.Stop() + + mgr, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: s, + }) + if err != nil { + t.Fatalf("getting manager: %v", err) + } + ctx, ctxCancel := context.WithCancel(context.Background()) + stoppedCh := make(chan error) + go func() { + err := mgr.Start(ctx) + stoppedCh <- err + }() + + defer func() { + ctxCancel() + err := <-stoppedCh + if err != nil { + t.Errorf("Starting controller manager: %v", err) + t.FailNow() + } + }() + + time.Sleep(1 * time.Second) + + for _, n := range []string{"argocd", "test"} { + ns := v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: n, + }, + } + err = mgr.GetClient().Create(context.Background(), &ns) + if err != nil { + t.Fatalf("creating test ns: %v", err) + } + } + + r := &Reconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorderFor("test-custompkg-controller"), + } + + cwd, _ := os.Getwd() + + resource := v1alpha1.CustomPackage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test1", + Namespace: "test", + UID: "abc", + }, + Spec: v1alpha1.CustomPackageSpec{ + Replicate: true, + GitServerURL: "https://cnoe.io", + InternalGitServeURL: "http://internal.cnoe.io", + ArgoCD: v1alpha1.ArgoCDPackageSpec{ + ApplicationFile: filepath.Join(cwd, "test/resources/customPackages/helm/app.yaml"), + Name: "my-app", + Namespace: "argocd", + Type: "Application", + }, + }, + } + + source := &argov1alpha1.ApplicationSource{ + Helm: &argov1alpha1.ApplicationSourceHelm{ + ValuesObject: &k8sruntime.RawExtension{ + Raw: []byte(`{ + "repoURLGit": "cnoe://test", + "nested": { + "repoURLGit": "cnoe://test", + "bool": true, + "int": 123 + }, + "bool": false, + "int": 456, + "arrayString": [ + "abc", + "cnoe://test" + ], + "arrayMap": [ + { + "test": "cnoe://test", + "nested": { + "test": "cnoe://test" + } + } + ] + }`), + }, + }, + } + + _, err = r.reconcileHelmValueObject(ctx, source, &resource, "test") + assert.NoError(t, err) + expectJson := `{"arrayMap":[{"nested":{"test":""},"test":""}],"arrayString":["abc",""],"bool":false,"int":456,"nested":{"bool":true,"int":123,"repoURLGit":""},"repoURLGit":""}` + assert.JSONEq(t, expectJson, string(source.Helm.ValuesObject.Raw)) +} diff --git a/pkg/controllers/custompackage/test/resources/customPackages/helm/app.yaml b/pkg/controllers/custompackage/test/resources/customPackages/helm/app.yaml new file mode 100644 index 00000000..770867e7 --- /dev/null +++ b/pkg/controllers/custompackage/test/resources/customPackages/helm/app.yaml @@ -0,0 +1,24 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: my-app-helm + namespace: argocd +spec: + destination: + namespace: my-app-helm + server: "https://kubernetes.default.svc" + source: + repoURL: cnoe://test + targetRevision: HEAD + path: "." + helm: + valuesObject: + repoURLGit: cnoe://test + nested: + repoURLGit: cnoe://test + project: default + syncPolicy: + automated: + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/pkg/controllers/custompackage/test/resources/customPackages/helm/test/Chart.yaml b/pkg/controllers/custompackage/test/resources/customPackages/helm/test/Chart.yaml new file mode 100644 index 00000000..3ebad470 --- /dev/null +++ b/pkg/controllers/custompackage/test/resources/customPackages/helm/test/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: test +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/pkg/controllers/custompackage/test/resources/customPackages/helm/test/templates/cm.yaml b/pkg/controllers/custompackage/test/resources/customPackages/helm/test/templates/cm.yaml new file mode 100644 index 00000000..db411caa --- /dev/null +++ b/pkg/controllers/custompackage/test/resources/customPackages/helm/test/templates/cm.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: config +data: + test1: "one" diff --git a/pkg/controllers/custompackage/test/resources/customPackages/helm/test/values.yaml b/pkg/controllers/custompackage/test/resources/customPackages/helm/test/values.yaml new file mode 100644 index 00000000..1bccfc78 --- /dev/null +++ b/pkg/controllers/custompackage/test/resources/customPackages/helm/test/values.yaml @@ -0,0 +1 @@ +some: value