diff --git a/README.md b/README.md index 7e6a6ec2..4558823f 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ Run the following commands for available flags and subcommands: ### Custom Packages Idpbuilder supports specifying custom packages using the flag `--package-dir` flag. -This flag expects a directory (local or remote) containing ArgoCD application files. +This flag expects a directory (local or remote) containing ArgoCD application files and / or ArgoCD application set files. In case of a remote directory, it must be a directory in a git repository, and the URL format must be a [kustomize remote URL format](https://github.com/kubernetes-sigs/kustomize/blob/master/examples/remoteBuild.md). diff --git a/api/v1alpha1/custom_package_types.go b/api/v1alpha1/custom_package_types.go index 71c39c57..69d5ade5 100644 --- a/api/v1alpha1/custom_package_types.go +++ b/api/v1alpha1/custom_package_types.go @@ -56,6 +56,8 @@ type ArgoCDPackageSpec struct { ApplicationFile string `json:"applicationFile"` Name string `json:"name"` Namespace string `json:"namespace"` + // +kubebuilder:validation:Enum:=Application;ApplicationSet + Type string `json:"type"` } type CustomPackageStatus struct { diff --git a/go.mod b/go.mod index fcab099a..e9e77b26 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.21.3 require ( code.gitea.io/sdk/gitea v0.16.0 - github.com/cnoe-io/argocd-api v0.0.0-20240125015729-416a35fe855d + github.com/cnoe-io/argocd-api v0.0.0-20240529230753-bb9d05f9e1d8 github.com/docker/docker v24.0.7+incompatible github.com/go-git/go-billy/v5 v5.5.0 github.com/go-git/go-git/v5 v5.12.0 @@ -17,7 +17,7 @@ require ( k8s.io/apiextensions-apiserver v0.29.1 k8s.io/apimachinery v0.29.1 k8s.io/client-go v0.29.1 - k8s.io/klog/v2 v2.110.1 + k8s.io/klog/v2 v2.120.1 sigs.k8s.io/controller-runtime v0.17.1 sigs.k8s.io/kind v0.23.0 sigs.k8s.io/kustomize/kyaml v0.16.0 @@ -52,7 +52,7 @@ require ( github.com/go-openapi/swag v0.23.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect @@ -97,7 +97,7 @@ require ( golang.org/x/crypto v0.21.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/mod v0.14.0 // indirect - golang.org/x/net v0.22.0 // indirect + golang.org/x/net v0.23.0 // indirect golang.org/x/oauth2 v0.16.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/term v0.18.0 // indirect diff --git a/go.sum b/go.sum index 6698b730..4c96898c 100644 --- a/go.sum +++ b/go.sum @@ -25,8 +25,8 @@ github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= -github.com/cnoe-io/argocd-api v0.0.0-20240125015729-416a35fe855d h1:cmcSrS0OYTLlGsBFshaAG29qC+PC5LBtKRhnEknlzgU= -github.com/cnoe-io/argocd-api v0.0.0-20240125015729-416a35fe855d/go.mod h1:IXG3LiEAeckMfjdwJnt6qC0ee4J4U5bleMuk1HN82ZA= +github.com/cnoe-io/argocd-api v0.0.0-20240529230753-bb9d05f9e1d8 h1:BYnQciuk2mQl3rE2B+P3u4Od7PK7PMUxUfRlz3cXIeA= +github.com/cnoe-io/argocd-api v0.0.0-20240529230753-bb9d05f9e1d8/go.mod h1:vc5mQ/ctD9dFYbWrYHyOclTdbbYU5p+QoFgJBG21RQQ= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= @@ -69,7 +69,6 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= -github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= @@ -240,8 +239,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -335,8 +334,8 @@ k8s.io/client-go v0.29.1 h1:19B/+2NGEwnFLzt0uB5kNJnfTsbV8w6TgQRz9l7ti7A= k8s.io/client-go v0.29.1/go.mod h1:TDG/psL9hdet0TI9mGyHJSgRkW3H9JZk2dNEUS7bRks= k8s.io/component-base v0.29.1 h1:MUimqJPCRnnHsskTTjKD+IC1EHBbRCVyi37IoFBrkYw= k8s.io/component-base v0.29.1/go.mod h1:fP9GFjxYrLERq1GcWWZAE3bqbNcDKDytn2srWuHTtKc= -k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= -k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= +k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= diff --git a/pkg/controllers/custompackage/controller.go b/pkg/controllers/custompackage/controller.go index b53d9f1e..9fb878ef 100644 --- a/pkg/controllers/custompackage/controller.go +++ b/pkg/controllers/custompackage/controller.go @@ -8,6 +8,7 @@ import ( "strings" "time" + argocdapplication "github.com/cnoe-io/argocd-api/api/argo/application" argov1alpha1 "github.com/cnoe-io/argocd-api/api/argo/application/v1alpha1" "github.com/cnoe-io/idpbuilder/api/v1alpha1" "github.com/cnoe-io/idpbuilder/pkg/k8s" @@ -77,12 +78,7 @@ func (r *Reconciler) reconcileCustomPackage(ctx context.Context, resource *v1alp return ctrl.Result{}, fmt.Errorf("reading file %s: %w", resource.Spec.ArgoCD.ApplicationFile, err) } - var returnedRawResource []byte - if returnedRawResource, err = util.ApplyTemplate(b, r.Config); err != nil { - return ctrl.Result{}, err - } - - objs, err := k8s.ConvertYamlToObjects(r.Scheme, returnedRawResource) + objs, err := k8s.ConvertYamlToObjects(r.Scheme, b) if err != nil { return ctrl.Result{}, fmt.Errorf("converting yaml to object %w", err) } @@ -90,42 +86,93 @@ func (r *Reconciler) reconcileCustomPackage(ctx context.Context, resource *v1alp return ctrl.Result{}, fmt.Errorf("file contained 0 kubernetes objects %s", resource.Spec.ArgoCD.ApplicationFile) } - app, ok := objs[0].(*argov1alpha1.Application) - if !ok { - return ctrl.Result{}, fmt.Errorf("object is not an ArgoCD application %s", resource.Spec.ArgoCD.ApplicationFile) - } + switch resource.Spec.ArgoCD.Type { + case argocdapplication.ApplicationKind: + app, ok := objs[0].(*argov1alpha1.Application) + if !ok { + return ctrl.Result{}, fmt.Errorf("object is not an ArgoCD application %s", resource.Spec.ArgoCD.ApplicationFile) + } + + res, err := r.reconcileArgoCDApp(ctx, resource, app) + if err != nil { + return ctrl.Result{}, err + } - appName := app.GetName() - if resource.Spec.Replicate { - synced := true - repoRefs := make([]v1alpha1.ObjectRef, 0, 1) - if app.Spec.HasMultipleSources() { - for j := range app.Spec.Sources { - s := &app.Spec.Sources[j] - res, repo, sErr := r.reconcileArgoCDSource(ctx, resource, s, appName) - if sErr != nil { - return res, sErr + foundAppObj := argov1alpha1.Application{} + err = r.Client.Get(ctx, client.ObjectKeyFromObject(app), &foundAppObj) + if err != nil { + if errors.IsNotFound(err) { + err = r.Client.Create(ctx, app) + if err != nil { + return ctrl.Result{}, fmt.Errorf("creating %s app CR: %w", app.Name, err) } - if repo != nil { - if synced { - synced = repo.Status.InternalGitRepositoryUrl != "" - } - s.RepoURL = repo.Status.InternalGitRepositoryUrl - repoRefs = append(repoRefs, v1alpha1.ObjectRef{ - Namespace: repo.Namespace, - Name: repo.Name, - UID: string(repo.ObjectMeta.UID), - }) + + return ctrl.Result{RequeueAfter: requeueTime}, nil + } + return ctrl.Result{}, fmt.Errorf("getting argocd application object: %w", err) + } + + foundAppObj.Spec = app.Spec + foundAppObj.ObjectMeta.Annotations = app.GetAnnotations() + foundAppObj.ObjectMeta.Labels = app.GetLabels() + err = r.Client.Update(ctx, &foundAppObj) + if err != nil { + return ctrl.Result{}, fmt.Errorf("updating argocd application object %s: %w", app.Name, err) + } + return res, nil + + case argocdapplication.ApplicationSetKind: + // application set embeds application spec. extract it then handle git generator repoURLs. + appSet, ok := objs[0].(*argov1alpha1.ApplicationSet) + if !ok { + return ctrl.Result{}, fmt.Errorf("object is not an ArgoCD application set %s", resource.Spec.ArgoCD.ApplicationFile) + } + res, err := r.reconcileArgoCDAppSet(ctx, resource, appSet) + if err != nil { + return ctrl.Result{}, err + } + foundAppSetObj := argov1alpha1.ApplicationSet{} + err = r.Client.Get(ctx, client.ObjectKeyFromObject(appSet), &foundAppSetObj) + if err != nil { + if errors.IsNotFound(err) { + err = r.Client.Create(ctx, appSet) + if err != nil { + return ctrl.Result{}, fmt.Errorf("creating %s argocd application set CR: %w", appSet.Name, err) } + return ctrl.Result{RequeueAfter: requeueTime}, nil } - } else { - s := app.Spec.Source - res, repo, sErr := r.reconcileArgoCDSource(ctx, resource, s, appName) + return ctrl.Result{}, fmt.Errorf("getting argocd application set object: %w", err) + } + + foundAppSetObj.Spec = appSet.Spec + foundAppSetObj.ObjectMeta.Annotations = appSet.GetAnnotations() + foundAppSetObj.ObjectMeta.Labels = appSet.GetLabels() + err = r.Client.Update(ctx, &foundAppSetObj) + if err != nil { + return ctrl.Result{}, fmt.Errorf("updating argocd application object %s: %w", appSet.Name, err) + } + return res, nil + + default: + return ctrl.Result{}, fmt.Errorf("file is not a supported argocd kind %s", resource.Spec.ArgoCD.ApplicationFile) + } +} + +func (r *Reconciler) reconcileArgoCDApp(ctx context.Context, resource *v1alpha1.CustomPackage, app *argov1alpha1.Application) (ctrl.Result, error) { + appSourcesSynced := true + repoRefs := make([]v1alpha1.ObjectRef, 0, 1) + if app.Spec.HasMultipleSources() { + notSyncedRepos := 0 + for j := range app.Spec.Sources { + s := &app.Spec.Sources[j] + res, repo, sErr := r.reconcileArgoCDSource(ctx, resource, s.RepoURL, app.Name) if sErr != nil { return res, sErr } if repo != nil { - synced = repo.Status.InternalGitRepositoryUrl != "" + if repo.Status.InternalGitRepositoryUrl == "" { + notSyncedRepos += 1 + } s.RepoURL = repo.Status.InternalGitRepositoryUrl repoRefs = append(repoRefs, v1alpha1.ObjectRef{ Namespace: repo.Namespace, @@ -134,40 +181,69 @@ func (r *Reconciler) reconcileCustomPackage(ctx context.Context, resource *v1alp }) } } - resource.Status.GitRepositoryRefs = repoRefs - resource.Status.Synced = synced + appSourcesSynced = notSyncedRepos == 0 + } else { + s := app.Spec.Source + 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 + repoRefs = append(repoRefs, v1alpha1.ObjectRef{ + Namespace: repo.Namespace, + Name: repo.Name, + UID: string(repo.ObjectMeta.UID), + }) + } } + resource.Status.GitRepositoryRefs = repoRefs + resource.Status.Synced = appSourcesSynced + return ctrl.Result{RequeueAfter: requeueTime}, nil +} - foundAppObj := argov1alpha1.Application{} - err = r.Client.Get(ctx, client.ObjectKeyFromObject(app), &foundAppObj) - if err != nil { - if errors.IsNotFound(err) { - err = r.Client.Create(ctx, app) - if err != nil { - return ctrl.Result{}, fmt.Errorf("creating %s app CR: %w", appName, err) +func (r *Reconciler) reconcileArgoCDAppSet(ctx context.Context, resource *v1alpha1.CustomPackage, appSet *argov1alpha1.ApplicationSet) (ctrl.Result, error) { + notSyncedRepos := 0 + for i := range appSet.Spec.Generators { + g := appSet.Spec.Generators[i] + if g.Git != nil { + res, repo, gErr := r.reconcileArgoCDSource(ctx, resource, g.Git.RepoURL, appSet.GetName()) + if gErr != nil { + return res, fmt.Errorf("reconciling git generator URL %s, %s: %w", g.Git.RepoURL, resource.Spec.ArgoCD.ApplicationFile, gErr) + } + if repo != nil { + g.Git.RepoURL = repo.Status.InternalGitRepositoryUrl + if repo.Status.InternalGitRepositoryUrl == "" { + notSyncedRepos += 1 + } } - - return ctrl.Result{RequeueAfter: requeueTime}, nil } - return ctrl.Result{}, fmt.Errorf("getting argocd application object: %w", err) } - foundAppObj.Spec = app.Spec - foundAppObj.ObjectMeta.Annotations = app.Annotations - foundAppObj.ObjectMeta.Labels = app.Labels - err = r.Client.Update(ctx, &foundAppObj) + gitGeneratorsSynced := notSyncedRepos == 0 + app := argov1alpha1.Application{ + ObjectMeta: metav1.ObjectMeta{Name: appSet.GetName(), Namespace: appSet.Namespace}, + } + app.Spec = appSet.Spec.Template.Spec + + _, err := r.reconcileArgoCDApp(ctx, resource, &app) if err != nil { - return ctrl.Result{}, fmt.Errorf("updating argocd application object %s: %w", appName, err) + return ctrl.Result{}, fmt.Errorf("reconciling application set %s %w", resource.Spec.ArgoCD.ApplicationFile, err) } + + resource.Status.Synced = resource.Status.Synced && gitGeneratorsSynced + return ctrl.Result{RequeueAfter: requeueTime}, nil } -func (r *Reconciler) reconcileArgoCDSource(ctx context.Context, resource *v1alpha1.CustomPackage, appSource *argov1alpha1.ApplicationSource, appName string) (ctrl.Result, *v1alpha1.GitRepository, error) { - if isCNOEScheme(appSource.RepoURL) { +// create a gitrepository custom resource, then let the git repository controller take care of the rest +func (r *Reconciler) reconcileArgoCDSource(ctx context.Context, resource *v1alpha1.CustomPackage, repoUrl, appName string) (ctrl.Result, *v1alpha1.GitRepository, error) { + if isCNOEScheme(repoUrl) { if resource.Spec.RemoteRepository.Url == "" { - return r.reconcileArgoCDSourceFromLocal(ctx, resource, appName, appSource.RepoURL) + return r.reconcileArgoCDSourceFromLocal(ctx, resource, appName, repoUrl) } - return r.reconcileArgoCDSourceFromRemote(ctx, resource, appName, appSource.RepoURL) + return r.reconcileArgoCDSourceFromRemote(ctx, resource, appName, repoUrl) } return ctrl.Result{}, nil, nil } @@ -281,8 +357,10 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { } func (r *Reconciler) getArgoCDAppFile(ctx context.Context, resource *v1alpha1.CustomPackage) ([]byte, error) { + filePath := resource.Spec.ArgoCD.ApplicationFile + if resource.Spec.RemoteRepository.Url == "" { - return os.ReadFile(resource.Spec.ArgoCD.ApplicationFile) + return os.ReadFile(filePath) } cloneDir := util.RepoDir(resource.Spec.RemoteRepository.Url, r.TempDir) @@ -293,7 +371,7 @@ func (r *Reconciler) getArgoCDAppFile(ctx context.Context, resource *v1alpha1.Cu if err != nil { return nil, fmt.Errorf("cloning repo, %s: %w", resource.Spec.RemoteRepository.Url, err) } - return util.ReadWorktreeFile(wt, resource.Spec.ArgoCD.ApplicationFile) + return util.ReadWorktreeFile(wt, filePath) } func localRepoName(appName, dir string) string { diff --git a/pkg/controllers/custompackage/controller_test.go b/pkg/controllers/custompackage/controller_test.go index 8fa97ed8..d69085bd 100644 --- a/pkg/controllers/custompackage/controller_test.go +++ b/pkg/controllers/custompackage/controller_test.go @@ -22,6 +22,12 @@ import ( "sigs.k8s.io/controller-runtime/pkg/envtest" ) +type testCase struct { + expectedGitRepo v1alpha1.GitRepository + expectedApplicationSet argov1alpha1.ApplicationSet + input v1alpha1.CustomPackage +} + func TestReconcileCustomPkg(t *testing.T) { s := k8sruntime.NewScheme() sb := k8sruntime.NewSchemeBuilder( @@ -38,7 +44,7 @@ func TestReconcileCustomPkg(t *testing.T) { ErrorIfCRDPathMissing: true, Scheme: s, BinaryAssetsDirectory: filepath.Join("..", "..", "..", "bin", "k8s", - fmt.Sprintf("1.27.1-%s-%s", runtime.GOOS, runtime.GOARCH)), + fmt.Sprintf("1.29.1-%s-%s", runtime.GOOS, runtime.GOARCH)), } cfg, err := testEnv.Start() @@ -94,6 +100,7 @@ func TestReconcileCustomPkg(t *testing.T) { ApplicationFile: filepath.Join(cwd, "test/resources/customPackages/testDir/app.yaml"), Name: "my-app", Namespace: "argocd", + Type: "Application", }, }, }, @@ -111,6 +118,7 @@ func TestReconcileCustomPkg(t *testing.T) { ApplicationFile: filepath.Join(cwd, "test/resources/customPackages/testDir2/exampleApp.yaml"), Name: "guestbook", Namespace: "argocd", + Type: "Application", }, }, }, @@ -128,6 +136,7 @@ func TestReconcileCustomPkg(t *testing.T) { ApplicationFile: filepath.Join(cwd, "test/resources/customPackages/testDir/app2.yaml"), Name: "my-app2", Namespace: "argocd", + Type: "Application", }, }, }, @@ -223,3 +232,277 @@ func TestReconcileCustomPkg(t *testing.T) { t.Fatalf("%s prefix should be removed", v1alpha1.CNOEURIScheme) } } + +func TestReconcileCustomPkgAppSet(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() + assert.Nil(t, err) + defer testEnv.Stop() + + mgr, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: s, + }) + assert.Nil(t, 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() + } + }() + + r := &Reconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorderFor("test-custompkg-controller"), + } + cwd, err := os.Getwd() + assert.Nil(t, err) + + for _, n := range []string{"argocd", "test"} { + ns := v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: n, + }, + } + err = mgr.GetClient().Create(context.Background(), &ns) + assert.Nil(t, err) + } + + cases := []testCase{ + { + input: 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/applicationSet/generator-single-source.yaml"), + Type: "ApplicationSet", + }, + }, + }, + expectedGitRepo: v1alpha1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: localRepoName("generator-single-source", "test/resources/customPackages/applicationSet/test1"), + Namespace: "test", + }, + Spec: v1alpha1.GitRepositorySpec{ + Source: v1alpha1.GitRepositorySource{ + Type: "local", + Path: filepath.Join(cwd, "test/resources/customPackages/applicationSet/test1"), + }, + Provider: v1alpha1.Provider{ + Name: v1alpha1.GitProviderGitea, + GitURL: "https://cnoe.io", + InternalGitURL: "http://internal.cnoe.io", + OrganizationName: v1alpha1.GiteaAdminUserName, + }, + }, + }, + expectedApplicationSet: argov1alpha1.ApplicationSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "generator-single-source", + Namespace: "argocd", + }, + Spec: argov1alpha1.ApplicationSetSpec{ + Generators: []argov1alpha1.ApplicationSetGenerator{ + { + Git: &argov1alpha1.GitGenerator{ + RepoURL: "", + }, + }, + }, + Template: argov1alpha1.ApplicationSetTemplate{ + Spec: argov1alpha1.ApplicationSpec{ + Source: &argov1alpha1.ApplicationSource{ + RepoURL: "", + }, + }, + }, + }, + }, + }, + { + input: v1alpha1.CustomPackage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test2", + Namespace: "test", + UID: "test2", + }, + Spec: v1alpha1.CustomPackageSpec{ + Replicate: true, + GitServerURL: "https://cnoe.io", + InternalGitServeURL: "http://internal.cnoe.io", + ArgoCD: v1alpha1.ArgoCDPackageSpec{ + ApplicationFile: filepath.Join(cwd, "test/resources/customPackages/applicationSet/generator-multi-sources.yaml"), + Type: "ApplicationSet", + }, + }, + }, + expectedGitRepo: v1alpha1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: localRepoName("generator-multi-sources", "test/resources/customPackages/applicationSet/test1"), + Namespace: "test", + }, + Spec: v1alpha1.GitRepositorySpec{ + Source: v1alpha1.GitRepositorySource{ + Type: "local", + Path: filepath.Join(cwd, "test/resources/customPackages/applicationSet/test1"), + }, + Provider: v1alpha1.Provider{ + Name: v1alpha1.GitProviderGitea, + GitURL: "https://cnoe.io", + InternalGitURL: "http://internal.cnoe.io", + OrganizationName: v1alpha1.GiteaAdminUserName, + }, + }, + }, + expectedApplicationSet: argov1alpha1.ApplicationSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "generator-multi-sources", + Namespace: "argocd", + }, + Spec: argov1alpha1.ApplicationSetSpec{ + Generators: []argov1alpha1.ApplicationSetGenerator{ + { + Git: &argov1alpha1.GitGenerator{ + RepoURL: "", + }, + }, + }, + Template: argov1alpha1.ApplicationSetTemplate{ + Spec: argov1alpha1.ApplicationSpec{ + Sources: []argov1alpha1.ApplicationSource{ + { + RepoURL: "", + }, + }, + }, + }, + }, + }, + }, + { + input: v1alpha1.CustomPackage{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test3", + Namespace: "test", + UID: "test3", + }, + Spec: v1alpha1.CustomPackageSpec{ + Replicate: true, + GitServerURL: "https://cnoe.io", + InternalGitServeURL: "http://internal.cnoe.io", + ArgoCD: v1alpha1.ArgoCDPackageSpec{ + ApplicationFile: filepath.Join(cwd, "test/resources/customPackages/applicationSet/no-generator-single-source.yaml"), + Type: "ApplicationSet", + }, + }, + }, + expectedGitRepo: v1alpha1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: localRepoName("no-generator-single-source", "test/resources/customPackages/applicationSet/test1"), + Namespace: "test", + }, + Spec: v1alpha1.GitRepositorySpec{ + Source: v1alpha1.GitRepositorySource{ + Type: "local", + Path: filepath.Join(cwd, "test/resources/customPackages/applicationSet/test1"), + }, + Provider: v1alpha1.Provider{ + Name: v1alpha1.GitProviderGitea, + GitURL: "https://cnoe.io", + InternalGitURL: "http://internal.cnoe.io", + OrganizationName: v1alpha1.GiteaAdminUserName, + }, + }, + }, + expectedApplicationSet: argov1alpha1.ApplicationSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "no-generator-single-source", + Namespace: "argocd", + }, + Spec: argov1alpha1.ApplicationSetSpec{ + Template: argov1alpha1.ApplicationSetTemplate{ + Spec: argov1alpha1.ApplicationSpec{ + Source: &argov1alpha1.ApplicationSource{ + RepoURL: "", + }, + }, + }, + }, + }, + }, + } + + for i := range cases { + tc := cases[i] + _, err = r.reconcileCustomPackage(context.Background(), &tc.input) + assert.Nil(t, err) + time.Sleep(1 * time.Second) + + c := mgr.GetClient() + repo := v1alpha1.GitRepository{} + err = c.Get(context.Background(), client.ObjectKeyFromObject(&tc.expectedGitRepo), &repo) + assert.Nil(t, err) + + assert.Equal(t, tc.expectedGitRepo.Spec, repo.Spec) + + // verify argocd applicationSet + appset := argov1alpha1.ApplicationSet{} + err = c.Get(context.Background(), client.ObjectKeyFromObject(&tc.expectedApplicationSet), &appset) + assert.Nil(t, err) + + if len(tc.expectedApplicationSet.Spec.Template.Spec.Sources) > 0 { + for j := range tc.expectedApplicationSet.Spec.Template.Spec.Sources { + exs := tc.expectedApplicationSet.Spec.Template.Spec.Sources[j] + assert.Equal(t, exs.RepoURL, appset.Spec.Template.Spec.Sources[j].RepoURL) + assert.False(t, strings.HasPrefix(appset.Spec.Template.Spec.Sources[j].RepoURL, v1alpha1.CNOEURIScheme)) + } + } else { + assert.Equal(t, tc.expectedApplicationSet.Spec.Template.Spec.Source.RepoURL, appset.Spec.Template.Spec.Source.RepoURL) + assert.False(t, strings.HasPrefix(appset.Spec.Template.Spec.Source.RepoURL, v1alpha1.CNOEURIScheme)) + } + + if len(tc.expectedApplicationSet.Spec.Generators) > 0 { + for j := range tc.expectedApplicationSet.Spec.Generators { + exg := tc.expectedApplicationSet.Spec.Generators[j] + if exg.Git != nil { + assert.Equal(t, exg.Git.RepoURL, appset.Spec.Generators[j].Git.RepoURL) + } + } + } + } +} diff --git a/pkg/controllers/custompackage/test/resources/customPackages/applicationSet/generator-multi-sources.yaml b/pkg/controllers/custompackage/test/resources/customPackages/applicationSet/generator-multi-sources.yaml new file mode 100644 index 00000000..2872ee79 --- /dev/null +++ b/pkg/controllers/custompackage/test/resources/customPackages/applicationSet/generator-multi-sources.yaml @@ -0,0 +1,27 @@ +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: generator-multi-sources + namespace: argocd +spec: + generators: + - git: + repoURL: cnoe://test1 + revision: HEAD + directories: + - path: apps/* + template: + metadata: + name: '{{path.basename}}' + spec: + project: default + sources: + - repoURL: cnoe://test1 + targetRevision: HEAD + path: '{{path}}' + destination: + server: https://kubernetes.default.svc + namespace: '{{path.basename}}' + syncPolicy: + syncOptions: + - CreateNamespace=true diff --git a/pkg/controllers/custompackage/test/resources/customPackages/applicationSet/generator-single-source.yaml b/pkg/controllers/custompackage/test/resources/customPackages/applicationSet/generator-single-source.yaml new file mode 100644 index 00000000..3e2372e9 --- /dev/null +++ b/pkg/controllers/custompackage/test/resources/customPackages/applicationSet/generator-single-source.yaml @@ -0,0 +1,27 @@ +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: generator-single-source + namespace: argocd +spec: + generators: + - git: + repoURL: cnoe://test1 + revision: HEAD + directories: + - path: apps/* + template: + metadata: + name: '{{path.basename}}' + spec: + project: default + source: + repoURL: cnoe://test1 + targetRevision: HEAD + path: '{{path}}' + destination: + server: https://kubernetes.default.svc + namespace: '{{path.basename}}' + syncPolicy: + syncOptions: + - CreateNamespace=true diff --git a/pkg/controllers/custompackage/test/resources/customPackages/applicationSet/no-generator-single-source.yaml b/pkg/controllers/custompackage/test/resources/customPackages/applicationSet/no-generator-single-source.yaml new file mode 100644 index 00000000..d40a7b0e --- /dev/null +++ b/pkg/controllers/custompackage/test/resources/customPackages/applicationSet/no-generator-single-source.yaml @@ -0,0 +1,23 @@ +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: no-generator-single-source + namespace: argocd +spec: + generators: + - clusters: { } + template: + metadata: + name: '{{path.basename}}' + spec: + project: default + source: + repoURL: cnoe://test1 + targetRevision: HEAD + path: '{{path}}' + destination: + server: https://kubernetes.default.svc + namespace: '{{path.basename}}' + syncPolicy: + syncOptions: + - CreateNamespace=true diff --git a/pkg/controllers/custompackage/test/resources/customPackages/applicationSet/test1/apps/guestbook/guestbook-ui-deployment.yaml b/pkg/controllers/custompackage/test/resources/customPackages/applicationSet/test1/apps/guestbook/guestbook-ui-deployment.yaml new file mode 100644 index 00000000..8a0975e3 --- /dev/null +++ b/pkg/controllers/custompackage/test/resources/customPackages/applicationSet/test1/apps/guestbook/guestbook-ui-deployment.yaml @@ -0,0 +1,20 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: guestbook-ui +spec: + replicas: 1 + revisionHistoryLimit: 3 + selector: + matchLabels: + app: guestbook-ui + template: + metadata: + labels: + app: guestbook-ui + spec: + containers: + - image: gcr.io/heptio-images/ks-guestbook-demo:0.2 + name: guestbook-ui + ports: + - containerPort: 80 diff --git a/pkg/controllers/custompackage/test/resources/customPackages/applicationSet/test1/apps/guestbook/guestbook-ui-svc.yaml b/pkg/controllers/custompackage/test/resources/customPackages/applicationSet/test1/apps/guestbook/guestbook-ui-svc.yaml new file mode 100644 index 00000000..e619b5cd --- /dev/null +++ b/pkg/controllers/custompackage/test/resources/customPackages/applicationSet/test1/apps/guestbook/guestbook-ui-svc.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Service +metadata: + name: guestbook-ui +spec: + ports: + - port: 80 + targetPort: 80 + selector: + app: guestbook-ui diff --git a/pkg/controllers/custompackage/test/resources/customPackages/applicationSet/test1/apps/guestbook/kustomization.yaml b/pkg/controllers/custompackage/test/resources/customPackages/applicationSet/test1/apps/guestbook/kustomization.yaml new file mode 100644 index 00000000..cbaba902 --- /dev/null +++ b/pkg/controllers/custompackage/test/resources/customPackages/applicationSet/test1/apps/guestbook/kustomization.yaml @@ -0,0 +1,7 @@ +namePrefix: kustomize- + +resources: +- guestbook-ui-deployment.yaml +- guestbook-ui-svc.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization diff --git a/pkg/controllers/custompackage/test/resources/customPackages/applicationSet/test1/apps/guestbook2/guestbook-ui-deployment.yaml b/pkg/controllers/custompackage/test/resources/customPackages/applicationSet/test1/apps/guestbook2/guestbook-ui-deployment.yaml new file mode 100644 index 00000000..8a0975e3 --- /dev/null +++ b/pkg/controllers/custompackage/test/resources/customPackages/applicationSet/test1/apps/guestbook2/guestbook-ui-deployment.yaml @@ -0,0 +1,20 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: guestbook-ui +spec: + replicas: 1 + revisionHistoryLimit: 3 + selector: + matchLabels: + app: guestbook-ui + template: + metadata: + labels: + app: guestbook-ui + spec: + containers: + - image: gcr.io/heptio-images/ks-guestbook-demo:0.2 + name: guestbook-ui + ports: + - containerPort: 80 diff --git a/pkg/controllers/custompackage/test/resources/customPackages/applicationSet/test1/apps/guestbook2/guestbook-ui-svc.yaml b/pkg/controllers/custompackage/test/resources/customPackages/applicationSet/test1/apps/guestbook2/guestbook-ui-svc.yaml new file mode 100644 index 00000000..e619b5cd --- /dev/null +++ b/pkg/controllers/custompackage/test/resources/customPackages/applicationSet/test1/apps/guestbook2/guestbook-ui-svc.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Service +metadata: + name: guestbook-ui +spec: + ports: + - port: 80 + targetPort: 80 + selector: + app: guestbook-ui diff --git a/pkg/controllers/custompackage/test/resources/customPackages/applicationSet/test1/apps/guestbook2/kustomization.yaml b/pkg/controllers/custompackage/test/resources/customPackages/applicationSet/test1/apps/guestbook2/kustomization.yaml new file mode 100644 index 00000000..cbaba902 --- /dev/null +++ b/pkg/controllers/custompackage/test/resources/customPackages/applicationSet/test1/apps/guestbook2/kustomization.yaml @@ -0,0 +1,7 @@ +namePrefix: kustomize- + +resources: +- guestbook-ui-deployment.yaml +- guestbook-ui-svc.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization diff --git a/pkg/controllers/localbuild/controller.go b/pkg/controllers/localbuild/controller.go index aaeb22f8..3b9d68b0 100644 --- a/pkg/controllers/localbuild/controller.go +++ b/pkg/controllers/localbuild/controller.go @@ -306,6 +306,7 @@ func (r *LocalbuildReconciler) reconcileCustomPkg( } if isSupportedArgoCDTypes(gvk) { + kind := o.GetKind() appName := o.GetName() appNS := o.GetNamespace() customPkg := &v1alpha1.CustomPackage{ @@ -339,6 +340,7 @@ func (r *LocalbuildReconciler) reconcileCustomPkg( ApplicationFile: filePath, Name: appName, Namespace: appNS, + Type: kind, }, } @@ -497,7 +499,7 @@ func isSupportedArgoCDTypes(gvk *schema.GroupVersionKind) bool { if gvk == nil { return false } - return gvk.Kind == argocdapp.ApplicationKind && gvk.Group == argocdapp.Group + return gvk.Group == argocdapp.Group && (gvk.Kind == argocdapp.ApplicationKind || gvk.Kind == argocdapp.ApplicationSetKind) } func GetEmbeddedRawInstallResources(name string, templateData any, config v1alpha1.PackageCustomization, scheme *runtime.Scheme) ([][]byte, error) { diff --git a/pkg/controllers/resources/idpbuilder.cnoe.io_custompackages.yaml b/pkg/controllers/resources/idpbuilder.cnoe.io_custompackages.yaml index 507dd2f8..f22b5ec8 100644 --- a/pkg/controllers/resources/idpbuilder.cnoe.io_custompackages.yaml +++ b/pkg/controllers/resources/idpbuilder.cnoe.io_custompackages.yaml @@ -49,10 +49,16 @@ spec: type: string namespace: type: string + type: + enum: + - Application + - ApplicationSet + type: string required: - applicationFile - name - namespace + - type type: object gitServerAuthSecretRef: properties: