diff --git a/cmd/commands/app.go b/cmd/commands/app.go index bfe8b7da..c66d02ec 100644 --- a/cmd/commands/app.go +++ b/cmd/commands/app.go @@ -10,7 +10,6 @@ import ( "time" "github.com/argoproj-labs/argocd-autopilot/pkg/application" - "github.com/argoproj-labs/argocd-autopilot/pkg/argocd" "github.com/argoproj-labs/argocd-autopilot/pkg/fs" "github.com/argoproj-labs/argocd-autopilot/pkg/git" "github.com/argoproj-labs/argocd-autopilot/pkg/kube" @@ -199,13 +198,14 @@ func RunAppCreate(ctx context.Context, opts *AppCreateOptions) error { if opts.AppsCloneOpts != opts.CloneOpts { log.G(ctx).Info("committing changes to apps repo...") - if err = appsRepo.Persist(ctx, &git.PushOptions{CommitMsg: getCommitMsg(opts, appsfs)}); err != nil { + if _, err = appsRepo.Persist(ctx, &git.PushOptions{CommitMsg: getCommitMsg(opts, appsfs)}); err != nil { return fmt.Errorf("failed to push to apps repo: %w", err) } } log.G(ctx).Info("committing changes to gitops repo...") - if err = r.Persist(ctx, &git.PushOptions{CommitMsg: getCommitMsg(opts, repofs)}); err != nil { + revision, err := r.Persist(ctx, &git.PushOptions{CommitMsg: getCommitMsg(opts, repofs)}) + if err != nil { return fmt.Errorf("failed to push to gitops repo: %w", err) } @@ -219,7 +219,7 @@ func RunAppCreate(ctx context.Context, opts *AppCreateOptions) error { fullName := fmt.Sprintf("%s-%s", opts.ProjectName, opts.AppOpts.AppName) // wait for argocd to be ready before applying argocd-apps stop := util.WithSpinner(ctx, fmt.Sprintf("waiting for '%s' to be ready", fullName)) - if err = waitAppSynced(ctx, opts.KubeFactory, opts.Timeout, fullName, namespace); err != nil { + if err = waitAppSynced(ctx, opts.KubeFactory, opts.Timeout, fullName, namespace, revision, true); err != nil { stop() return fmt.Errorf("failed waiting for application to sync: %w", err) } @@ -298,20 +298,6 @@ func getCommitMsg(opts *AppCreateOptions, repofs fs.FS) string { return commitMsg } -func waitAppSynced(ctx context.Context, f kube.Factory, timeout time.Duration, appName, namespace string) error { - return f.Wait(ctx, &kube.WaitOptions{ - Interval: store.Default.WaitInterval, - Timeout: timeout, - Resources: []kube.Resource{ - { - Name: appName, - Namespace: namespace, - WaitFunc: argocd.CheckAppSynced, - }, - }, - }) -} - func NewAppListCommand(cloneOpts *git.CloneOptions) *cobra.Command { cmd := &cobra.Command{ Use: "list [PROJECT_NAME]", @@ -490,7 +476,7 @@ func RunAppDelete(ctx context.Context, opts *AppDeleteOptions) error { } log.G(ctx).Info("committing changes to gitops repo...") - if err = r.Persist(ctx, &git.PushOptions{CommitMsg: commitMsg}); err != nil { + if _, err = r.Persist(ctx, &git.PushOptions{CommitMsg: commitMsg}); err != nil { return fmt.Errorf("failed to push to repo: %w", err) } diff --git a/cmd/commands/app_test.go b/cmd/commands/app_test.go index dcdb7c58..91bedcfd 100644 --- a/cmd/commands/app_test.go +++ b/cmd/commands/app_test.go @@ -117,7 +117,7 @@ func TestRunAppCreate(t *testing.T) { mockRepo := &gitmocks.Repository{} mockRepo.On("Persist", mock.Anything, &git.PushOptions{ CommitMsg: "installed app 'app' on project 'project' installation-path: '/'", - }).Return(fmt.Errorf("some error")) + }).Return("", fmt.Errorf("some error")) return mockRepo, fs.Create(memfs.New()), nil }, }, @@ -129,7 +129,7 @@ func TestRunAppCreate(t *testing.T) { mockRepo := &gitmocks.Repository{} mockRepo.On("Persist", mock.Anything, &git.PushOptions{ CommitMsg: "installed app 'app' on project 'project' installation-path: '/'", - }).Return(fmt.Errorf("some error")) + }).Return("", fmt.Errorf("some error")) return mockRepo, fs.Create(memfs), nil }, }, @@ -142,7 +142,7 @@ func TestRunAppCreate(t *testing.T) { mockRepo := &gitmocks.Repository{} mockRepo.On("Persist", mock.Anything, &git.PushOptions{ CommitMsg: "installed app 'app' on project 'project' installation-path: '/'", - }).Return(nil) + }).Return("revision", nil) return mockRepo, fs.Create(memfs), nil }, getInstallationNamespace: func(repofs fs.FS) (string, error) { @@ -161,7 +161,7 @@ func TestRunAppCreate(t *testing.T) { mockRepo := &gitmocks.Repository{} mockRepo.On("Persist", mock.Anything, &git.PushOptions{ CommitMsg: "installed app 'app' on project 'project' installation-path: '/'", - }).Return(nil) + }).Return("revision", nil) return mockRepo, fs.Create(memfs), nil }, getInstallationNamespace: func(repofs fs.FS) (string, error) { @@ -177,14 +177,14 @@ func TestRunAppCreate(t *testing.T) { mockRepo := &gitmocks.Repository{} mockRepo.On("Persist", mock.Anything, &git.PushOptions{ CommitMsg: "installed app 'app' on project 'project' installation-path: '/'", - }).Return(nil) + }).Return("revision", nil) return mockRepo, fs.Create(memfs), nil }, getRepo: func(_ *testing.T, _ *git.CloneOptions) (git.Repository, fs.FS, error) { mockRepo := &gitmocks.Repository{} mockRepo.On("Persist", mock.Anything, &git.PushOptions{ CommitMsg: "installed app 'app' on project 'project' installation-path: '/'", - }).Return(nil) + }).Return("revision", nil) return mockRepo, fs.Create(memfs.New()), nil }, assertFn: func(t *testing.T, gitopsRepo git.Repository, appsRepo git.Repository) { @@ -200,7 +200,7 @@ func TestRunAppCreate(t *testing.T) { mockRepo := &gitmocks.Repository{} mockRepo.On("Persist", mock.Anything, &git.PushOptions{ CommitMsg: "installed app 'app' on project 'project' installation-path: '/'", - }).Return(nil) + }).Return("revision", nil) return mockRepo, fs.Create(memfs), nil }, assertFn: func(t *testing.T, gitopsRepo git.Repository, appsRepo git.Repository) { @@ -219,7 +219,7 @@ func TestRunAppCreate(t *testing.T) { mockRepo := &gitmocks.Repository{} mockRepo.On("Persist", mock.Anything, &git.PushOptions{ CommitMsg: "installed app 'app' on project 'project' installation-path: '/'", - }).Return(nil) + }).Return("revision", nil) return mockRepo, fs.Create(memfs), nil }, getInstallationNamespace: func(repofs fs.FS) (string, error) { @@ -462,7 +462,7 @@ func TestRunAppDelete(t *testing.T) { mockRepo := &gitmocks.Repository{} mockRepo.On("Persist", mock.Anything, &git.PushOptions{ CommitMsg: "Deleted app 'app'", - }).Return(nil) + }).Return("revision", nil) return mockRepo, fs.Create(memfs), nil }, assertFn: func(t *testing.T, repo git.Repository, repofs fs.FS) { @@ -506,7 +506,7 @@ func TestRunAppDelete(t *testing.T) { mockRepo := &gitmocks.Repository{} mockRepo.On("Persist", mock.Anything, &git.PushOptions{ CommitMsg: "Deleted app 'app' from project 'project'", - }).Return(nil) + }).Return("revision", nil) return mockRepo, fs.Create(memfs), nil }, assertFn: func(t *testing.T, repo git.Repository, repofs fs.FS) { @@ -524,7 +524,7 @@ func TestRunAppDelete(t *testing.T) { mockRepo := &gitmocks.Repository{} mockRepo.On("Persist", mock.Anything, &git.PushOptions{ CommitMsg: "Deleted app 'app'", - }).Return(nil) + }).Return("revision", nil) return mockRepo, fs.Create(memfs), nil }, assertFn: func(t *testing.T, repo git.Repository, repofs fs.FS) { @@ -542,7 +542,7 @@ func TestRunAppDelete(t *testing.T) { mockRepo := &gitmocks.Repository{} mockRepo.On("Persist", mock.Anything, &git.PushOptions{ CommitMsg: "Deleted app 'app' from project 'project'", - }).Return(nil) + }).Return("revision", nil) return mockRepo, fs.Create(memfs), nil }, assertFn: func(t *testing.T, repo git.Repository, repofs fs.FS) { @@ -560,7 +560,7 @@ func TestRunAppDelete(t *testing.T) { mockRepo := &gitmocks.Repository{} mockRepo.On("Persist", mock.Anything, &git.PushOptions{ CommitMsg: "Deleted app 'app'", - }).Return(nil) + }).Return("revision", nil) return mockRepo, fs.Create(memfs), nil }, assertFn: func(t *testing.T, repo git.Repository, repofs fs.FS) { @@ -578,7 +578,7 @@ func TestRunAppDelete(t *testing.T) { mockRepo := &gitmocks.Repository{} mockRepo.On("Persist", mock.Anything, &git.PushOptions{ CommitMsg: "Deleted app 'app'", - }).Return(fmt.Errorf("some error")) + }).Return("", fmt.Errorf("some error")) return mockRepo, fs.Create(memfs), nil }, assertFn: func(t *testing.T, repo git.Repository, repofs fs.FS) { diff --git a/cmd/commands/common.go b/cmd/commands/common.go index 9f2e618f..b90a6dfb 100644 --- a/cmd/commands/common.go +++ b/cmd/commands/common.go @@ -5,9 +5,12 @@ import ( _ "embed" "fmt" "os" + "time" + "github.com/argoproj-labs/argocd-autopilot/pkg/argocd" "github.com/argoproj-labs/argocd-autopilot/pkg/fs" "github.com/argoproj-labs/argocd-autopilot/pkg/git" + "github.com/argoproj-labs/argocd-autopilot/pkg/kube" "github.com/argoproj-labs/argocd-autopilot/pkg/log" "github.com/argoproj-labs/argocd-autopilot/pkg/store" "github.com/argoproj-labs/argocd-autopilot/pkg/util" @@ -40,20 +43,20 @@ var ( } prepareRepo = func(ctx context.Context, cloneOpts *git.CloneOptions, projectName string) (git.Repository, fs.FS, error) { - log.G().WithFields(log.Fields{ + log.G(ctx).WithFields(log.Fields{ "repoURL": cloneOpts.URL(), "revision": cloneOpts.Revision(), }).Debug("starting with options: ") // clone repo - log.G().Infof("cloning git repository: %s", cloneOpts.URL()) + log.G(ctx).Infof("cloning git repository: %s", cloneOpts.URL()) r, repofs, err := getRepo(ctx, cloneOpts) if err != nil { return nil, nil, fmt.Errorf("Failed cloning the repository: %w", err) } root := repofs.Root() - log.G().Infof("using revision: \"%s\", installation path: \"%s\"", cloneOpts.Revision(), root) + log.G(ctx).Infof("using revision: \"%s\", installation path: \"%s\"", cloneOpts.Revision(), root) if !repofs.ExistsOrDie(store.Default.BootsrtrapDir) { return nil, nil, fmt.Errorf("Bootstrap directory not found, please execute `repo bootstrap` command") } @@ -65,7 +68,7 @@ var ( } } - log.G().Debug("repository is ok") + log.G(ctx).Debug("repository is ok") return r, repofs, nil } @@ -95,8 +98,8 @@ func createApp(opts *createAppOptions) ([]byte, error) { Namespace: opts.namespace, Name: opts.name, Labels: map[string]string{ - "app.kubernetes.io/managed-by": store.Default.ManagedBy, - "app.kubernetes.io/name": opts.name, + store.Default.LabelKeyAppManagedBy: store.Default.LabelValueManagedBy, + "app.kubernetes.io/name": opts.name, }, Finalizers: []string{ "resources-finalizer.argocd.argoproj.io", @@ -141,6 +144,20 @@ func createApp(opts *createAppOptions) ([]byte, error) { return yaml.Marshal(app) } +func waitAppSynced(ctx context.Context, f kube.Factory, timeout time.Duration, appName, namespace, revision string, waitForCreation bool) error { + return f.Wait(ctx, &kube.WaitOptions{ + Interval: store.Default.WaitInterval, + Timeout: timeout, + Resources: []kube.Resource{ + { + Name: appName, + Namespace: namespace, + WaitFunc: argocd.GetAppSyncWaitFunc(revision, waitForCreation), + }, + }, + }) +} + type createAppSetOptions struct { name string namespace string @@ -220,8 +237,8 @@ func createAppSet(o *createAppSetOptions) ([]byte, error) { if o.appLabels == nil { // default labels appSet.Spec.Template.ApplicationSetTemplateMeta.Labels = map[string]string{ - "app.kubernetes.io/managed-by": store.Default.ManagedBy, - "app.kubernetes.io/name": o.appName, + store.Default.LabelKeyAppManagedBy: store.Default.LabelValueManagedBy, + "app.kubernetes.io/name": o.appName, } } diff --git a/cmd/commands/project.go b/cmd/commands/project.go index 2ea9d1f1..d3433ae8 100644 --- a/cmd/commands/project.go +++ b/cmd/commands/project.go @@ -108,11 +108,12 @@ func NewProjectCreateCommand(cloneOpts *git.CloneOptions) *cobra.Command { `), PreRun: func(_ *cobra.Command, _ []string) { cloneOpts.Parse() }, RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() if len(args) < 1 { - log.G().Fatal("must enter project name") + log.G(ctx).Fatal("must enter project name") } - return RunProjectCreate(cmd.Context(), &ProjectCreateOptions{ + return RunProjectCreate(ctx, &ProjectCreateOptions{ CloneOpts: cloneOpts, ProjectName: args[0], DestKubeContext: kubeContext, @@ -152,7 +153,7 @@ func RunProjectCreate(ctx context.Context, opts *ProjectCreateOptions) error { return fmt.Errorf("project '%s' already exists", opts.ProjectName) } - log.G().Debug("repository is ok") + log.G(ctx).Debug("repository is ok") destServer := store.Default.DestServer if opts.DestKubeContext != "" { @@ -176,14 +177,14 @@ func RunProjectCreate(ctx context.Context, opts *ProjectCreateOptions) error { } if opts.DryRun { - log.G().Printf("%s", util.JoinManifests(projectYAML, appsetYAML)) + log.G(ctx).Printf("%s", util.JoinManifests(projectYAML, appsetYAML)) return nil } bulkWrites := []fsutils.BulkWriteRequest{} if opts.DestKubeContext != "" { - log.G().Infof("adding cluster: %s", opts.DestKubeContext) + log.G(ctx).Infof("adding cluster: %s", opts.DestKubeContext) if err = opts.AddCmd.Execute(ctx, opts.DestKubeContext); err != nil { return fmt.Errorf("failed to add new cluster credentials: %w", err) } @@ -213,12 +214,12 @@ func RunProjectCreate(ctx context.Context, opts *ProjectCreateOptions) error { return err } - log.G().Infof("pushing new project manifest to repo") - if err = r.Persist(ctx, &git.PushOptions{CommitMsg: fmt.Sprintf("Added project '%s'", opts.ProjectName)}); err != nil { + log.G(ctx).Infof("pushing new project manifest to repo") + if _, err = r.Persist(ctx, &git.PushOptions{CommitMsg: fmt.Sprintf("Added project '%s'", opts.ProjectName)}); err != nil { return err } - log.G().Infof("project created: '%s'", opts.ProjectName) + log.G(ctx).Infof("project created: '%s'", opts.ProjectName) return nil } @@ -280,8 +281,8 @@ func generateProjectManifests(o *GenerateProjectOptions) (projectYAML, appSetYAM prune: true, preserveResourcesOnDeletion: false, appLabels: map[string]string{ - "app.kubernetes.io/managed-by": store.Default.ManagedBy, - "app.kubernetes.io/name": "{{ appName }}", + store.Default.LabelKeyAppManagedBy: store.Default.LabelValueManagedBy, + "app.kubernetes.io/name": "{{ appName }}", }, generators: []appset.ApplicationSetGenerator{ { @@ -403,11 +404,12 @@ func NewProjectDeleteCommand(cloneOpts *git.CloneOptions) *cobra.Command { `), PreRun: func(_ *cobra.Command, _ []string) { cloneOpts.Parse() }, RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() if len(args) < 1 { - log.G().Fatal("must enter project name") + log.G(ctx).Fatal("must enter project name") } - return RunProjectDelete(cmd.Context(), &ProjectDeleteOptions{ + return RunProjectDelete(ctx, &ProjectDeleteOptions{ CloneOpts: cloneOpts, ProjectName: args[0], }) @@ -440,8 +442,8 @@ func RunProjectDelete(ctx context.Context, opts *ProjectDeleteOptions) error { return fmt.Errorf("failed to delete project '%s': %w", opts.ProjectName, err) } - log.G().Info("committing changes to gitops repo...") - if err = r.Persist(ctx, &git.PushOptions{CommitMsg: fmt.Sprintf("Deleted project '%s'", opts.ProjectName)}); err != nil { + log.G(ctx).Info("committing changes to gitops repo...") + if _, err = r.Persist(ctx, &git.PushOptions{CommitMsg: fmt.Sprintf("Deleted project '%s'", opts.ProjectName)}); err != nil { return fmt.Errorf("failed to push to repo: %w", err) } diff --git a/cmd/commands/project_test.go b/cmd/commands/project_test.go index 9b5106a1..1836b51c 100644 --- a/cmd/commands/project_test.go +++ b/cmd/commands/project_test.go @@ -87,7 +87,9 @@ func TestRunProjectCreate(t *testing.T) { prepareRepo: func() (git.Repository, fs.FS, error) { memfs := memfs.New() mockedRepo := &gitmocks.Repository{} - mockedRepo.On("Persist", mock.AnythingOfType("*context.emptyCtx"), &git.PushOptions{CommitMsg: "Added project 'project'"}).Return(fmt.Errorf("failed to persist")) + mockedRepo.On("Persist", mock.AnythingOfType("*context.emptyCtx"), &git.PushOptions{ + CommitMsg: "Added project 'project'", + }).Return("", fmt.Errorf("failed to persist")) return mockedRepo, fs.Create(memfs), nil }, getInstallationNamespace: func(_ fs.FS) (string, error) { @@ -100,7 +102,9 @@ func TestRunProjectCreate(t *testing.T) { prepareRepo: func() (git.Repository, fs.FS, error) { memfs := memfs.New() mockedRepo := &gitmocks.Repository{} - mockedRepo.On("Persist", mock.AnythingOfType("*context.emptyCtx"), &git.PushOptions{CommitMsg: "Added project 'project'"}).Return(nil) + mockedRepo.On("Persist", mock.AnythingOfType("*context.emptyCtx"), &git.PushOptions{ + CommitMsg: "Added project 'project'", + }).Return("revision", nil) return mockedRepo, fs.Create(memfs), nil }, getInstallationNamespace: func(_ fs.FS) (string, error) { @@ -512,7 +516,7 @@ func TestRunProjectDelete(t *testing.T) { mockRepo := &gitmocks.Repository{} mockRepo.On("Persist", mock.AnythingOfType("*context.emptyCtx"), &git.PushOptions{ CommitMsg: "Deleted project 'project'", - }).Return(fmt.Errorf("some error")) + }).Return("revision", fmt.Errorf("some error")) return mockRepo, fs.Create(memfs), nil }, }, @@ -526,7 +530,7 @@ func TestRunProjectDelete(t *testing.T) { mockRepo := &gitmocks.Repository{} mockRepo.On("Persist", mock.AnythingOfType("*context.emptyCtx"), &git.PushOptions{ CommitMsg: "Deleted project 'project'", - }).Return(fmt.Errorf("some error")) + }).Return("", fmt.Errorf("some error")) return mockRepo, fs.Create(memfs), nil }, }, @@ -539,7 +543,7 @@ func TestRunProjectDelete(t *testing.T) { mockRepo := &gitmocks.Repository{} mockRepo.On("Persist", mock.AnythingOfType("*context.emptyCtx"), &git.PushOptions{ CommitMsg: "Deleted project 'project'", - }).Return(nil) + }).Return("revision", nil) return mockRepo, fs.Create(memfs), nil }, assertFn: func(t *testing.T, repo git.Repository, repofs fs.FS) { @@ -557,7 +561,7 @@ func TestRunProjectDelete(t *testing.T) { mockRepo := &gitmocks.Repository{} mockRepo.On("Persist", mock.AnythingOfType("*context.emptyCtx"), &git.PushOptions{ CommitMsg: "Deleted project 'project'", - }).Return(nil) + }).Return("revision", nil) return mockRepo, fs.Create(memfs), nil }, assertFn: func(t *testing.T, repo git.Repository, repofs fs.FS) { @@ -575,7 +579,7 @@ func TestRunProjectDelete(t *testing.T) { mockRepo := &gitmocks.Repository{} mockRepo.On("Persist", mock.AnythingOfType("*context.emptyCtx"), &git.PushOptions{ CommitMsg: "Deleted project 'project'", - }).Return(nil) + }).Return("revision", nil) return mockRepo, fs.Create(memfs), nil }, assertFn: func(t *testing.T, repo git.Repository, repofs fs.FS) { @@ -597,7 +601,7 @@ func TestRunProjectDelete(t *testing.T) { mockRepo := &gitmocks.Repository{} mockRepo.On("Persist", mock.AnythingOfType("*context.emptyCtx"), &git.PushOptions{ CommitMsg: "Deleted project 'project'", - }).Return(nil) + }).Return("revision", nil) return mockRepo, fs.Create(memfs), nil }, assertFn: func(t *testing.T, repo git.Repository, repofs fs.FS) { diff --git a/cmd/commands/repo.go b/cmd/commands/repo.go index a74a487a..a286de2e 100644 --- a/cmd/commands/repo.go +++ b/cmd/commands/repo.go @@ -20,11 +20,14 @@ import ( "github.com/argoproj-labs/argocd-autopilot/pkg/util" appset "github.com/argoproj-labs/applicationset/api/v1alpha1" + argocdcommon "github.com/argoproj/argo-cd/v2/common" argocdsettings "github.com/argoproj/argo-cd/v2/util/settings" "github.com/ghodss/yaml" "github.com/go-git/go-billy/v5/memfs" + billyUtils "github.com/go-git/go-billy/v5/util" "github.com/spf13/cobra" v1 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" kusttypes "sigs.k8s.io/kustomize/api/types" ) @@ -55,6 +58,14 @@ type ( CloneOptions *git.CloneOptions } + RepoUninstallOptions struct { + Namespace string + KubeContext string + Timeout time.Duration + CloneOptions *git.CloneOptions + KubeFactory kube.Factory + } + bootstrapManifests struct { bootstrapApp []byte rootApp []byte @@ -79,6 +90,7 @@ func NewRepoCommand() *cobra.Command { } cmd.AddCommand(NewRepoBootstrapCommand()) + cmd.AddCommand(NewRepoUninstallCommand()) return cmd } @@ -112,8 +124,8 @@ func NewRepoBootstrapCommand() *cobra.Command { repo bootstrap --repo https://github.com/example/repo - # Install argo-cd on the current kubernetes context in the argocd namespace - # and persists the bootstrap manifests to a specific folder in the gitops repository +# Install argo-cd on the current kubernetes context in the argocd namespace +# and persists the bootstrap manifests to a specific folder in the gitops repository repo bootstrap --repo https://github.com/example/repo/path/to/installation_root `), @@ -159,7 +171,7 @@ func RunRepoBootstrap(ctx context.Context, opts *RepoBootstrapOptions) error { return err } - log.G().WithFields(log.Fields{ + log.G(ctx).WithFields(log.Fields{ "repo-url": opts.CloneOptions.URL(), "revision": opts.CloneOptions.Revision(), "namespace": opts.Namespace, @@ -189,7 +201,7 @@ func RunRepoBootstrap(ctx context.Context, opts *RepoBootstrapOptions) error { return nil } - log.G().Infof("cloning repo: %s", opts.CloneOptions.URL()) + log.G(ctx).Infof("cloning repo: %s", opts.CloneOptions.URL()) // clone GitOps repo r, repofs, err := getRepo(ctx, opts.CloneOptions) @@ -197,16 +209,16 @@ func RunRepoBootstrap(ctx context.Context, opts *RepoBootstrapOptions) error { return err } - log.G().Infof("using revision: \"%s\", installation path: \"%s\"", opts.CloneOptions.Revision(), opts.CloneOptions.Path()) + log.G(ctx).Infof("using revision: \"%s\", installation path: \"%s\"", opts.CloneOptions.Revision(), opts.CloneOptions.Path()) if err = validateRepo(repofs); err != nil { return err } - log.G().Debug("repository is ok") + log.G(ctx).Debug("repository is ok") // apply built manifest to k8s cluster - log.G().Infof("using context: \"%s\", namespace: \"%s\"", opts.KubeContext, opts.Namespace) - log.G().Infof("applying bootstrap manifests to cluster...") + log.G(ctx).Infof("using context: \"%s\", namespace: \"%s\"", opts.KubeContext, opts.Namespace) + log.G(ctx).Infof("applying bootstrap manifests to cluster...") if err = opts.KubeFactory.Apply(ctx, opts.Namespace, util.JoinManifests(manifests.namespace, manifests.applyManifests, manifests.repoCreds)); err != nil { return fmt.Errorf("failed to apply bootstrap manifests to cluster: %w", err) } @@ -227,18 +239,18 @@ func RunRepoBootstrap(ctx context.Context, opts *RepoBootstrapOptions) error { stop() // push results to repo - log.G().Infof("pushing bootstrap manifests to repo") + log.G(ctx).Infof("pushing bootstrap manifests to repo") commitMsg := "Autopilot Bootstrap" if opts.CloneOptions.Path() != "" { commitMsg = "Autopilot Bootstrap at " + opts.CloneOptions.Path() } - if err = r.Persist(ctx, &git.PushOptions{CommitMsg: commitMsg}); err != nil { + if _, err = r.Persist(ctx, &git.PushOptions{CommitMsg: commitMsg}); err != nil { return err } // apply "Argo-CD" Application that references "bootstrap/argo-cd" - log.G().Infof("applying argo-cd bootstrap application") + log.G(ctx).Infof("applying argo-cd bootstrap application") if err = opts.KubeFactory.Apply(ctx, opts.Namespace, manifests.bootstrapApp); err != nil { return err } @@ -248,7 +260,7 @@ func RunRepoBootstrap(ctx context.Context, opts *RepoBootstrapOptions) error { return err } - log.G().Infof("running argocd login to initialize argocd config") + log.G(ctx).Infof("running argocd login to initialize argocd config") err = argocdLogin(&argocd.LoginOptions{ Namespace: opts.Namespace, Username: "admin", @@ -257,6 +269,7 @@ func RunRepoBootstrap(ctx context.Context, opts *RepoBootstrapOptions) error { if err != nil { return err } + if !opts.HidePassword { log.G(ctx).Printf("") log.G(ctx).Infof("argocd initialized. password: %s", passwd) @@ -266,6 +279,114 @@ func RunRepoBootstrap(ctx context.Context, opts *RepoBootstrapOptions) error { return nil } +func NewRepoUninstallCommand() *cobra.Command { + var ( + cloneOpts *git.CloneOptions + f kube.Factory + ) + + cmd := &cobra.Command{ + Use: "uninstall", + Short: "Uninstalls an installation", + Example: util.Doc(` +# To run this command you need to create a personal access token for your git provider +# and provide it using: + + export GIT_TOKEN= + +# or with the flag: + + --git-token + +# Uninstall argo-cd from the current kubernetes context in the argocd namespace +# and delete all manifests rom the root of gitops repository + + repo uninstall --repo https://github.com/example/repo + +# Uninstall argo-cd from the current kubernetes context in the argocd namespace +# and delete all manifests from a specific folder in the gitops repository + + repo uninstall --repo https://github.com/example/repo/path/to/installation_root +`), + PreRun: func(_ *cobra.Command, _ []string) { cloneOpts.Parse() }, + RunE: func(cmd *cobra.Command, args []string) error { + return RunRepoUninstall(cmd.Context(), &RepoUninstallOptions{ + Namespace: cmd.Flag("namespace").Value.String(), + Timeout: util.MustParseDuration(cmd.Flag("request-timeout").Value.String()), + KubeContext: cmd.Flag("context").Value.String(), + CloneOptions: cloneOpts, + KubeFactory: f, + }) + }, + } + + cloneOpts = git.AddFlags(cmd, &git.AddFlagsOptions{ + FS: memfs.New(), + }) + f = kube.AddFlags(cmd.Flags()) + + return cmd +} + +func RunRepoUninstall(ctx context.Context, opts *RepoUninstallOptions) error { + var err error + + if opts, err = setUninstallOptsDefaults(*opts); err != nil { + return err + } + + log.G(ctx).WithFields(log.Fields{ + "repo-url": opts.CloneOptions.URL(), + "revision": opts.CloneOptions.Revision(), + "namespace": opts.Namespace, + "kube-context": opts.KubeContext, + }).Debug("starting with options: ") + + log.G(ctx).Infof("cloning repo: %s", opts.CloneOptions.URL()) + r, repofs, err := getRepo(ctx, opts.CloneOptions) + if err != nil { + return err + } + + log.G(ctx).Debug("deleting files from repo") + if err = deleteGitOpsFiles(repofs); err != nil { + return err + } + + log.G(ctx).Info("pushing changes to remote") + revision, err := r.Persist(ctx, &git.PushOptions{CommitMsg: "Autopilot Uninstall"}) + if err != nil { + return err + } + + stop := util.WithSpinner(ctx, fmt.Sprintf("waiting for '%s' to be finish syncing", store.Default.BootsrtrapAppName)) + if err = waitAppSynced(ctx, opts.KubeFactory, opts.Timeout, store.Default.BootsrtrapAppName, opts.Namespace, revision, false); err != nil { + se, ok := err.(*kerrors.StatusError) + if !ok || se.ErrStatus.Reason != metav1.StatusReasonNotFound { + stop() + return err + } + } + + stop() + log.G(ctx).Info("Deleting cluster resources") + if err = deleteClusterResources(ctx, opts.KubeFactory, opts.Timeout); err != nil { + return err + } + + log.G(ctx).Debug("Deleting leftovers from repo") + if err = billyUtils.RemoveAll(repofs, store.Default.BootsrtrapDir); err != nil { + return err + } + + log.G(ctx).Info("pushing final commit to remote") + if _, err := r.Persist(ctx, &git.PushOptions{CommitMsg: "Autopilot Uninstall, deleted leftovers"}); err != nil { + return err + } + + return nil +} + func setBootstrapOptsDefaults(opts RepoBootstrapOptions) (*RepoBootstrapOptions, error) { var err error @@ -333,6 +454,9 @@ func getRepoCredsSecret(token, namespace string) ([]byte, error) { ObjectMeta: metav1.ObjectMeta{ Name: store.Default.RepoCredsSecretName, Namespace: namespace, + Labels: map[string]string{ + store.Default.LabelKeyAppManagedBy: store.Default.LabelValueManagedBy, + }, }, Data: map[string][]byte{ "git_username": []byte(store.Default.GitUsername), @@ -555,13 +679,13 @@ func createCreds(repoUrl string) ([]byte, error) { URL: host, UsernameSecret: &v1.SecretKeySelector{ LocalObjectReference: v1.LocalObjectReference{ - Name: "autopilot-secret", + Name: store.Default.RepoCredsSecretName, }, Key: "git_username", }, PasswordSecret: &v1.SecretKeySelector{ LocalObjectReference: v1.LocalObjectReference{ - Name: "autopilot-secret", + Name: store.Default.RepoCredsSecretName, }, Key: "git_token", }, @@ -570,3 +694,67 @@ func createCreds(repoUrl string) ([]byte, error) { return yaml.Marshal(creds) } + +func setUninstallOptsDefaults(opts RepoUninstallOptions) (*RepoUninstallOptions, error) { + var err error + + if opts.Namespace == "" { + opts.Namespace = store.Default.ArgoCDNamespace + } + + if opts.KubeContext == "" { + if opts.KubeContext, err = currentKubeContext(); err != nil { + return nil, err + } + } + + return &opts, nil +} + +func deleteGitOpsFiles(repofs fs.FS) error { + if err := billyUtils.RemoveAll(repofs, store.Default.AppsDir); err != nil { + return fmt.Errorf("failed deleting '%s' folder: %w", store.Default.AppsDir, err) + } + + if err := billyUtils.RemoveAll(repofs, store.Default.BootsrtrapDir); err != nil { + return fmt.Errorf("failed deleting '%s' folder: %w", store.Default.BootsrtrapDir, err) + } + + if err := billyUtils.RemoveAll(repofs, store.Default.ProjectsDir); err != nil { + return fmt.Errorf("failed deleting '%s' folder: %w", store.Default.ProjectsDir, err) + } + + if err := billyUtils.WriteFile(repofs, repofs.Join(store.Default.BootsrtrapDir, store.Default.DummyName), []byte{}, 0666); err != nil { + return fmt.Errorf("failed creating '%s' file in '%s' folder: %w", store.Default.DummyName, store.Default.ProjectsDir, err) + } + + return nil +} + +func deleteClusterResources(ctx context.Context, f kube.Factory, timeout time.Duration) error { + if err := f.Delete(ctx, &kube.DeleteOptions{ + LabelSelector: store.Default.LabelKeyAppManagedBy + "=" + store.Default.LabelValueManagedBy, + ResourceTypes: []string{"applications", "secrets"}, + Timeout: timeout, + }); err != nil { + return fmt.Errorf("failed deleting argocd-autopilot resources: %w", err) + } + + if err := f.Delete(ctx, &kube.DeleteOptions{ + LabelSelector: argocdcommon.LabelKeyAppInstance + "=" + store.Default.ArgoCDName, + ResourceTypes: []string{ + "all", + "configmaps", + "secrets", + "serviceaccounts", + "networkpolicies", + "rolebindings", + "roles", + }, + Timeout: timeout, + }); err != nil { + return fmt.Errorf("failed deleting Argo-CD resources: %w", err) + } + + return nil +} diff --git a/cmd/commands/repo_test.go b/cmd/commands/repo_test.go index 177bdb35..4be2615b 100644 --- a/cmd/commands/repo_test.go +++ b/cmd/commands/repo_test.go @@ -2,6 +2,7 @@ package commands import ( "context" + "errors" "fmt" "path/filepath" "testing" @@ -10,12 +11,15 @@ import ( "github.com/argoproj-labs/argocd-autopilot/pkg/fs" "github.com/argoproj-labs/argocd-autopilot/pkg/git" gitmocks "github.com/argoproj-labs/argocd-autopilot/pkg/git/mocks" + "github.com/argoproj-labs/argocd-autopilot/pkg/kube" kubemocks "github.com/argoproj-labs/argocd-autopilot/pkg/kube/mocks" "github.com/argoproj-labs/argocd-autopilot/pkg/store" + argocdcommon "github.com/argoproj/argo-cd/v2/common" argocdv1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" "github.com/ghodss/yaml" "github.com/go-git/go-billy/v5/memfs" + billyUtils "github.com/go-git/go-billy/v5/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" v1 "k8s.io/api/core/v1" @@ -237,8 +241,8 @@ func TestRunRepoBootstrap(t *testing.T) { exitCalled := false tests := map[string]struct { opts *RepoBootstrapOptions - preFn func(r *gitmocks.Repository, repofs fs.FS, f *kubemocks.Factory) - assertFn func(t *testing.T, r *gitmocks.Repository, repofs fs.FS, f *kubemocks.Factory, ret error) + beforeFn func(*gitmocks.Repository, *kubemocks.Factory) + assertFn func(*testing.T, fs.FS, error) }{ "DryRun": { opts: &RepoBootstrapOptions{ @@ -251,7 +255,8 @@ func TestRunRepoBootstrap(t *testing.T) { Auth: git.Auth{Password: "test"}, }, }, - assertFn: func(t *testing.T, _ *gitmocks.Repository, _ fs.FS, _ *kubemocks.Factory, ret error) { + beforeFn: func(*gitmocks.Repository, *kubemocks.Factory) {}, + assertFn: func(t *testing.T, _ fs.FS, ret error) { assert.NoError(t, ret) assert.True(t, exitCalled) }, @@ -266,7 +271,7 @@ func TestRunRepoBootstrap(t *testing.T) { Auth: git.Auth{Password: "test"}, }, }, - preFn: func(r *gitmocks.Repository, _ fs.FS, f *kubemocks.Factory) { + beforeFn: func(r *gitmocks.Repository, f *kubemocks.Factory) { mockCS := fake.NewSimpleClientset(&v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "argocd-initial-admin-secret", @@ -276,21 +281,14 @@ func TestRunRepoBootstrap(t *testing.T) { "password": []byte("foo"), }, }) + r.On("Persist", mock.Anything, mock.Anything).Return("revision", nil) f.On("Apply", mock.Anything, mock.Anything, mock.Anything).Return(nil) f.On("Wait", mock.Anything, mock.Anything).Return(nil) f.On("KubernetesClientSetOrDie").Return(mockCS) - - r.On("Persist", mock.Anything, mock.Anything).Return(nil) - }, - assertFn: func(t *testing.T, r *gitmocks.Repository, repofs fs.FS, f *kubemocks.Factory, ret error) { + assertFn: func(t *testing.T, repofs fs.FS, ret error) { assert.NoError(t, ret) assert.False(t, exitCalled) - r.AssertCalled(t, "Persist", mock.Anything, mock.Anything) - f.AssertCalled(t, "Apply", mock.Anything, "bar", mock.Anything) - f.AssertCalled(t, "Wait", mock.Anything, mock.Anything) - f.AssertCalled(t, "KubernetesClientSetOrDie") - f.AssertNumberOfCalls(t, "Apply", 2) // bootstrap dir assert.True(t, repofs.ExistsOrDie(repofs.Join( @@ -330,7 +328,7 @@ func TestRunRepoBootstrap(t *testing.T) { Auth: git.Auth{Password: "test"}, }, }, - preFn: func(r *gitmocks.Repository, _ fs.FS, f *kubemocks.Factory) { + beforeFn: func(r *gitmocks.Repository, f *kubemocks.Factory) { mockCS := fake.NewSimpleClientset(&v1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "argocd-initial-admin-secret", @@ -340,19 +338,14 @@ func TestRunRepoBootstrap(t *testing.T) { "password": []byte("foo"), }, }) + r.On("Persist", mock.Anything, &git.PushOptions{CommitMsg: "Autopilot Bootstrap"}).Return("revision", nil) f.On("Apply", mock.Anything, mock.Anything, mock.Anything).Return(nil) f.On("Wait", mock.Anything, mock.Anything).Return(nil) f.On("KubernetesClientSetOrDie").Return(mockCS) - r.On("Persist", mock.Anything, mock.Anything).Return(nil) }, - assertFn: func(t *testing.T, r *gitmocks.Repository, repofs fs.FS, f *kubemocks.Factory, ret error) { + assertFn: func(t *testing.T, repofs fs.FS, ret error) { assert.NoError(t, ret) assert.False(t, exitCalled) - r.AssertCalled(t, "Persist", mock.Anything, mock.Anything) - f.AssertCalled(t, "Apply", mock.Anything, "bar", mock.Anything) - f.AssertCalled(t, "Wait", mock.Anything, mock.Anything) - f.AssertCalled(t, "KubernetesClientSetOrDie") - f.AssertNumberOfCalls(t, "Apply", 2) // bootstrap dir assert.True(t, repofs.ExistsOrDie(repofs.Join( @@ -384,39 +377,357 @@ func TestRunRepoBootstrap(t *testing.T) { }, } - orgExit := exit - orgClone := getRepo - orgRunKustomizeBuild := runKustomizeBuild - orgArgoLogin := argocdLogin + origExit, origGetRepo, origRunKustomizeBuild, origArgoLogin := exit, getRepo, runKustomizeBuild, argocdLogin + defer func() { + exit = origExit + getRepo = origGetRepo + runKustomizeBuild = origRunKustomizeBuild + argocdLogin = origArgoLogin + }() + exit = func(_ int) { exitCalled = true } + runKustomizeBuild = func(k *kusttypes.Kustomization) ([]byte, error) { return []byte("test"), nil } + argocdLogin = func(opts *argocd.LoginOptions) error { return nil } for tname, tt := range tests { t.Run(tname, func(t *testing.T) { + r := &gitmocks.Repository{} + repofs := fs.Create(memfs.New()) + f := &kubemocks.Factory{} exitCalled = false - mockRepo := &gitmocks.Repository{} - mockFactory := &kubemocks.Factory{} + + tt.beforeFn(r, f) + tt.opts.KubeFactory = f + getRepo = func(_ context.Context, _ *git.CloneOptions) (git.Repository, fs.FS, error) { + return r, repofs, nil + } + + err := RunRepoBootstrap(context.Background(), tt.opts) + tt.assertFn(t, repofs, err) + r.AssertExpectations(t) + f.AssertExpectations(t) + }) + } +} + +func Test_setUninstallOptsDefaults(t *testing.T) { + tests := map[string]struct { + opts RepoUninstallOptions + want *RepoUninstallOptions + wantErr string + currentKubeContext func() (string, error) + }{ + "Should not change anything, if all options are set": { + opts: RepoUninstallOptions{ + Namespace: "namespace", + KubeContext: "context", + }, + want: &RepoUninstallOptions{ + Namespace: "namespace", + KubeContext: "context", + }, + }, + "Should set default argocd namespace, if it is not set": { + opts: RepoUninstallOptions{ + KubeContext: "context", + }, + want: &RepoUninstallOptions{ + Namespace: store.Default.ArgoCDNamespace, + KubeContext: "context", + }, + }, + "Should get current kube context, if it is not set": { + opts: RepoUninstallOptions{ + Namespace: "namespace", + }, + want: &RepoUninstallOptions{ + Namespace: "namespace", + KubeContext: "currentContext", + }, + currentKubeContext: func() (string, error) { + return "currentContext", nil + }, + }, + "Should fail, if getting current context fails": { + opts: RepoUninstallOptions{}, + wantErr: "some error", + currentKubeContext: func() (string, error) { + return "", errors.New("some error") + }, + }, + } + origCurrentKubeContext := currentKubeContext + defer func() { currentKubeContext = origCurrentKubeContext }() + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + if tt.currentKubeContext != nil { + currentKubeContext = tt.currentKubeContext + } + + got, err := setUninstallOptsDefaults(tt.opts) + if err != nil { + if tt.wantErr != "" { + assert.EqualError(t, err, tt.wantErr) + } else { + t.Errorf("setUninstallOptsDefaults() error = %v", err) + } + + return + } + + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_deleteGitOpsFiles(t *testing.T) { + tests := map[string]struct { + wantErr string + beforeFn func() fs.FS + assertFn func(*testing.T, fs.FS, error) + }{ + "Should remove apps|project folders, and keep only bootstrap/DUMMY file": { + beforeFn: func() fs.FS { + repofs := memfs.New() + _ = billyUtils.WriteFile(repofs, repofs.Join(store.Default.AppsDir, "some_file"), []byte{}, 0666) + _ = billyUtils.WriteFile(repofs, repofs.Join(store.Default.BootsrtrapDir, "some_file"), []byte{}, 0666) + _ = billyUtils.WriteFile(repofs, repofs.Join(store.Default.ProjectsDir, "some_file"), []byte{}, 0666) + return fs.Create(repofs) + }, + assertFn: func(t *testing.T, repofs fs.FS, err error) { + assert.Nil(t, err) + assert.False(t, repofs.ExistsOrDie(store.Default.AppsDir)) + assert.True(t, repofs.ExistsOrDie(repofs.Join(store.Default.BootsrtrapDir, store.Default.DummyName))) + assert.False(t, repofs.ExistsOrDie(store.Default.ProjectsDir)) + fi, _ := repofs.ReadDir(store.Default.BootsrtrapDir) + assert.Len(t, fi, 1) + }, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + fs := tt.beforeFn() + err := deleteGitOpsFiles(fs) + tt.assertFn(t, fs, err) + }) + } +} + +func Test_deleteClusterResources(t *testing.T) { + tests := map[string]struct { + beforeFn func() kube.Factory + assertFn func(*testing.T, kube.Factory, error) + }{ + "Should delete all resources": { + beforeFn: func() kube.Factory { + mf := &kubemocks.Factory{} + mf.On("Delete", mock.Anything, &kube.DeleteOptions{ + LabelSelector: store.Default.LabelKeyAppManagedBy + "=" + store.Default.LabelValueManagedBy, + ResourceTypes: []string{"applications", "secrets"}, + }).Return(nil) + mf.On("Delete", mock.Anything, &kube.DeleteOptions{ + LabelSelector: argocdcommon.LabelKeyAppInstance + "=" + store.Default.ArgoCDName, + ResourceTypes: []string{ + "all", + "configmaps", + "secrets", + "serviceaccounts", + "networkpolicies", + "rolebindings", + "roles", + }, + }).Return(nil) + return mf + }, + assertFn: func(t *testing.T, f kube.Factory, err error) { + assert.Nil(t, err) + f.(*kubemocks.Factory).AssertExpectations(t) + }, + }, + "Should fail if failed to delete argocd-autopilot resources": { + beforeFn: func() kube.Factory { + mf := &kubemocks.Factory{} + mf.On("Delete", mock.Anything, &kube.DeleteOptions{ + LabelSelector: store.Default.LabelKeyAppManagedBy + "=" + store.Default.LabelValueManagedBy, + ResourceTypes: []string{"applications", "secrets"}, + }).Return(errors.New("some error")) + return mf + }, + assertFn: func(t *testing.T, f kube.Factory, err error) { + assert.EqualError(t, err, "failed deleting argocd-autopilot resources: some error") + f.(*kubemocks.Factory).AssertExpectations(t) + }, + }, + "Should fail if failed to delete Argo-CD resources": { + beforeFn: func() kube.Factory { + mf := &kubemocks.Factory{} + mf.On("Delete", mock.Anything, &kube.DeleteOptions{ + LabelSelector: store.Default.LabelKeyAppManagedBy + "=" + store.Default.LabelValueManagedBy, + ResourceTypes: []string{"applications", "secrets"}, + }).Return(nil) + mf.On("Delete", mock.Anything, &kube.DeleteOptions{ + LabelSelector: argocdcommon.LabelKeyAppInstance + "=" + store.Default.ArgoCDName, + ResourceTypes: []string{ + "all", + "configmaps", + "secrets", + "serviceaccounts", + "networkpolicies", + "rolebindings", + "roles", + }, + }).Return(errors.New("some error")) + return mf + }, + assertFn: func(t *testing.T, f kube.Factory, err error) { + assert.EqualError(t, err, "failed deleting Argo-CD resources: some error") + f.(*kubemocks.Factory).AssertExpectations(t) + }, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + f := tt.beforeFn() + err := deleteClusterResources(context.Background(), f, 0) + tt.assertFn(t, f, err) + }) + } +} + +func TestRunRepoUninstall(t *testing.T) { + tests := map[string]struct { + currentKubeContextErr error + getRepoErr error + wantErr string + beforeFn func(*gitmocks.Repository, *kubemocks.Factory) + }{ + "Should fail if getCurrentKubeContext fails": { + currentKubeContextErr: errors.New("some error"), + wantErr: "some error", + }, + "Should fail if getRepo fails": { + getRepoErr: errors.New("some error"), + wantErr: "some error", + }, + "Should fail if Persist fails": { + wantErr: "some error", + beforeFn: func(r *gitmocks.Repository, _ *kubemocks.Factory) { + r.On("Persist", mock.Anything, &git.PushOptions{CommitMsg: "Autopilot Uninstall"}).Return("", errors.New("some error")) + }, + }, + "Should fail if Wait fails": { + wantErr: "some error", + beforeFn: func(r *gitmocks.Repository, f *kubemocks.Factory) { + r.On("Persist", mock.Anything, &git.PushOptions{CommitMsg: "Autopilot Uninstall"}).Return("revision", nil) + f.On("Wait", mock.Anything, mock.Anything).Return(errors.New("some error")) + }, + }, + "Should fail if deleteClusterResources fails": { + wantErr: "failed deleting argocd-autopilot resources: some error", + beforeFn: func(r *gitmocks.Repository, f *kubemocks.Factory) { + r.On("Persist", mock.Anything, &git.PushOptions{CommitMsg: "Autopilot Uninstall"}).Return("revision", nil) + f.On("Wait", mock.Anything, mock.Anything).Return(nil) + f.On("Delete", mock.Anything, &kube.DeleteOptions{ + LabelSelector: store.Default.LabelKeyAppManagedBy + "=" + store.Default.LabelValueManagedBy, + ResourceTypes: []string{"applications", "secrets"}, + }).Return(errors.New("some error")) + }, + }, + "Should fail if 2nd Persist fails": { + wantErr: "some error", + beforeFn: func(r *gitmocks.Repository, f *kubemocks.Factory) { + r.On("Persist", mock.Anything, &git.PushOptions{CommitMsg: "Autopilot Uninstall"}).Return("revision", nil) + r.On("Persist", mock.Anything, &git.PushOptions{CommitMsg: "Autopilot Uninstall, deleted leftovers"}).Return("", errors.New("some error")) + f.On("Wait", mock.Anything, mock.Anything).Return(nil) + f.On("Delete", mock.Anything, &kube.DeleteOptions{ + LabelSelector: store.Default.LabelKeyAppManagedBy + "=" + store.Default.LabelValueManagedBy, + ResourceTypes: []string{"applications", "secrets"}, + }).Return(nil) + f.On("Delete", mock.Anything, &kube.DeleteOptions{ + LabelSelector: argocdcommon.LabelKeyAppInstance + "=" + store.Default.ArgoCDName, + ResourceTypes: []string{ + "all", + "configmaps", + "secrets", + "serviceaccounts", + "networkpolicies", + "rolebindings", + "roles", + }, + }).Return(nil) + }, + }, + "Should succeed if no errors": { + beforeFn: func(r *gitmocks.Repository, f *kubemocks.Factory) { + r.On("Persist", mock.Anything, &git.PushOptions{CommitMsg: "Autopilot Uninstall"}).Return("revision", nil) + r.On("Persist", mock.Anything, &git.PushOptions{CommitMsg: "Autopilot Uninstall, deleted leftovers"}).Return("", nil) + f.On("Wait", mock.Anything, mock.Anything).Return(nil) + f.On("Delete", mock.Anything, &kube.DeleteOptions{ + LabelSelector: store.Default.LabelKeyAppManagedBy + "=" + store.Default.LabelValueManagedBy, + ResourceTypes: []string{"applications", "secrets"}, + }).Return(nil) + f.On("Delete", mock.Anything, &kube.DeleteOptions{ + LabelSelector: argocdcommon.LabelKeyAppInstance + "=" + store.Default.ArgoCDName, + ResourceTypes: []string{ + "all", + "configmaps", + "secrets", + "serviceaccounts", + "networkpolicies", + "rolebindings", + "roles", + }, + }).Return(nil) + }, + }, + } + + origGetRepo, origCurrentKubeContext := getRepo, currentKubeContext + defer func() { getRepo, currentKubeContext = origGetRepo, origCurrentKubeContext }() + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + r := &gitmocks.Repository{} repofs := fs.Create(memfs.New()) + f := &kubemocks.Factory{} - if tt.preFn != nil { - tt.preFn(mockRepo, repofs, mockFactory) + if tt.beforeFn != nil { + tt.beforeFn(r, f) + } + + getRepo = func(_ context.Context, _ *git.CloneOptions) (git.Repository, fs.FS, error) { + if tt.getRepoErr != nil { + return nil, nil, tt.getRepoErr + } + + return r, repofs, nil } + currentKubeContext = func() (string, error) { + if tt.currentKubeContextErr != nil { + return "", tt.currentKubeContextErr + } - tt.opts.KubeFactory = mockFactory + return "context", nil + } - exit = func(_ int) { exitCalled = true } - getRepo = func(ctx context.Context, cloneOpts *git.CloneOptions) (git.Repository, fs.FS, error) { - return mockRepo, repofs, nil + opts := &RepoUninstallOptions{ + CloneOptions: &git.CloneOptions{ + Repo: "https://github.com/owner/name", + }, + KubeFactory: f, } - runKustomizeBuild = func(k *kusttypes.Kustomization) ([]byte, error) { return []byte("test"), nil } - argocdLogin = func(opts *argocd.LoginOptions) error { return nil } + opts.CloneOptions.Parse() + err := RunRepoUninstall(context.Background(), opts) + if err != nil { + if tt.wantErr != "" { + assert.EqualError(t, err, tt.wantErr) + } else { + t.Errorf("RunRepoUninstall() error = %v", err) + } - defer func() { - exit = orgExit - getRepo = orgClone - runKustomizeBuild = orgRunKustomizeBuild - argocdLogin = orgArgoLogin - }() + return + } - tt.assertFn(t, mockRepo, repofs, mockFactory, RunRepoBootstrap(context.Background(), tt.opts)) + r.AssertExpectations(t) + f.AssertExpectations(t) }) } } diff --git a/docs/Getting-Started.md b/docs/Getting-Started.md index 3e860edc..ab3edf79 100644 --- a/docs/Getting-Started.md +++ b/docs/Getting-Started.md @@ -77,3 +77,9 @@ After the application is created, and after Argo CD has finished its sync cycle, And the "hello-world" application will also be deployed to the cluster: ![Step 3](assets/getting_started_3.png) + +## Uninstall everything when done +The following command will clear your entire GitOps Repository of related files, and your k8s cluster from any resources autopilot resources +``` +argocd-autopilot repo uninstall +``` diff --git a/docs/commands/argocd-autopilot_repo.md b/docs/commands/argocd-autopilot_repo.md index 735d8247..49a5e660 100644 --- a/docs/commands/argocd-autopilot_repo.md +++ b/docs/commands/argocd-autopilot_repo.md @@ -17,4 +17,5 @@ argocd-autopilot repo [flags] * [argocd-autopilot](argocd-autopilot.md) - argocd-autopilot is used for installing and managing argo-cd installations and argo-cd applications using gitops * [argocd-autopilot repo bootstrap](argocd-autopilot_repo_bootstrap.md) - Bootstrap a new installation +* [argocd-autopilot repo uninstall](argocd-autopilot_repo_uninstall.md) - Uninstalls an installation diff --git a/docs/commands/argocd-autopilot_repo_bootstrap.md b/docs/commands/argocd-autopilot_repo_bootstrap.md index 2e9eb61e..b8eed603 100644 --- a/docs/commands/argocd-autopilot_repo_bootstrap.md +++ b/docs/commands/argocd-autopilot_repo_bootstrap.md @@ -24,8 +24,8 @@ argocd-autopilot repo bootstrap [flags] argocd-autopilot repo bootstrap --repo https://github.com/example/repo - # Install argo-cd on the current kubernetes context in the argocd namespace - # and persists the bootstrap manifests to a specific folder in the gitops repository +# Install argo-cd on the current kubernetes context in the argocd namespace +# and persists the bootstrap manifests to a specific folder in the gitops repository argocd-autopilot repo bootstrap --repo https://github.com/example/repo/path/to/installation_root diff --git a/docs/commands/argocd-autopilot_repo_uninstall.md b/docs/commands/argocd-autopilot_repo_uninstall.md new file mode 100644 index 00000000..41d2b3f9 --- /dev/null +++ b/docs/commands/argocd-autopilot_repo_uninstall.md @@ -0,0 +1,61 @@ +## argocd-autopilot repo uninstall + +Uninstalls an installation + +``` +argocd-autopilot repo uninstall [flags] +``` + +### Examples + +``` + +# To run this command you need to create a personal access token for your git provider +# and provide it using: + + export GIT_TOKEN= + +# or with the flag: + + --git-token + +# Uninstall argo-cd from the current kubernetes context in the argocd namespace +# and delete all manifests rom the root of gitops repository + + argocd-autopilot repo uninstall --repo https://github.com/example/repo + +# Uninstall argo-cd from the current kubernetes context in the argocd namespace +# and delete all manifests from a specific folder in the gitops repository + + argocd-autopilot repo uninstall --repo https://github.com/example/repo/path/to/installation_root + +``` + +### Options + +``` + --as string Username to impersonate for the operation + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --cache-dir string Default cache directory (default "/home/user/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + -t, --git-token string Your git provider api token [GIT_TOKEN] + -h, --help help for uninstall + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + -n, --namespace string If present, the namespace scope for this CLI request + --repo string Repository URL [GIT_REPO] + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [argocd-autopilot repo](argocd-autopilot_repo.md) - Manage gitops repositories + diff --git a/go.mod b/go.mod index 47a3997b..d90a847a 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/argoproj-labs/applicationset v0.1.0 github.com/argoproj/argo-cd v1.8.7 github.com/argoproj/argo-cd/v2 v2.0.3 + github.com/argoproj/gitops-engine v0.3.3 github.com/briandowns/spinner v1.13.0 github.com/ghodss/yaml v1.0.0 github.com/go-git/go-billy/v5 v5.3.1 diff --git a/go.sum b/go.sum index 20c5a62e..c2469288 100644 --- a/go.sum +++ b/go.sum @@ -116,8 +116,9 @@ github.com/argoproj/argo-cd/v2 v2.0.3 h1:9KK9u5wegvBhUQIrK+dfsGiJglPNlzFGoUFM/5i github.com/argoproj/argo-cd/v2 v2.0.3/go.mod h1:eg4iTfTUICd6o6ZpbPElWGcHSHWOsgXIQM13+HzhIYE= github.com/argoproj/gitops-engine v0.2.1/go.mod h1:OxXp8YaT73rw9gEBnGBWg55af80nkV/uIjWCbJu1Nw0= github.com/argoproj/gitops-engine v0.2.2/go.mod h1:OxXp8YaT73rw9gEBnGBWg55af80nkV/uIjWCbJu1Nw0= -github.com/argoproj/gitops-engine v0.3.2 h1:m5bjOk/bWwMsFBGFpurdK31/hC5UuLMQn0hAd51TlEk= github.com/argoproj/gitops-engine v0.3.2/go.mod h1:IBHhAkqlC+3r/wBWUitWSidQhPzlLoSTWp2htq3dyQk= +github.com/argoproj/gitops-engine v0.3.3 h1:zRNwKRj3h+EBpciy/+Eyo4vW2GTG3UG4HXAdWn0mQRI= +github.com/argoproj/gitops-engine v0.3.3/go.mod h1:IBHhAkqlC+3r/wBWUitWSidQhPzlLoSTWp2htq3dyQk= github.com/argoproj/pkg v0.2.0 h1:ETgC600kr8WcAi3MEVY5sA1H7H/u1/IysYOobwsZ8No= github.com/argoproj/pkg v0.2.0/go.mod h1:F4TZgInLUEjzsWFB/BTJBsewoEy0ucnKSq6vmQiD/yc= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= diff --git a/manifests/kustomization.yaml b/manifests/kustomization.yaml index 796607d6..69771e54 100644 --- a/manifests/kustomization.yaml +++ b/manifests/kustomization.yaml @@ -11,31 +11,32 @@ configMapGenerator: literals: - "timeout.reconciliation=20s" -# currently in use since we are on 2.0.4 patches: -- target: - group: rbac.authorization.k8s.io - version: v1 - kind: ClusterRoleBinding - patch: |- - - op: replace - path: /subjects/0/namespace - value: default -- patch: |- - apiVersion: apps/v1 - kind: StatefulSet - metadata: - name: argocd-application-controller - spec: - template: - spec: - containers: - - name: argocd-application-controller - command: - - argocd-application-controller - - --status-processors - - "20" - - --operation-processors - - "10" - - --app-resync - - "20" + # reset the crbs to `subject.namespace: default`, so that argo-cd will later change them to the actual ns + - target: + group: rbac.authorization.k8s.io + version: v1 + kind: ClusterRoleBinding + patch: |- + - op: replace + path: /subjects/0/namespace + value: default + # currently in use since we are on 2.0.4 + - patch: |- + apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: argocd-application-controller + spec: + template: + spec: + containers: + - name: argocd-application-controller + command: + - argocd-application-controller + - --status-processors + - "20" + - --operation-processors + - "10" + - --app-resync + - "20" diff --git a/pkg/argocd/argocd.go b/pkg/argocd/argocd.go index 5b640e38..e082f88a 100644 --- a/pkg/argocd/argocd.go +++ b/pkg/argocd/argocd.go @@ -12,9 +12,10 @@ import ( "github.com/argoproj/argo-cd/v2/cmd/argocd/commands" "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" - argocdcd "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned" + argocdcs "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned" + "github.com/argoproj/gitops-engine/pkg/health" "github.com/spf13/cobra" - "k8s.io/apimachinery/pkg/api/errors" + kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -54,29 +55,40 @@ func AddClusterAddFlags(cmd *cobra.Command) (AddClusterCmd, error) { return &addClusterImpl{root, args}, nil } -func CheckAppSynced(ctx context.Context, f kube.Factory, ns, name string) (bool, error) { - rc, err := f.ToRESTConfig() - if err != nil { - return false, err - } - - c, err := argocdcd.NewForConfig(rc) - if err != nil { - return false, err - } +// GetAppSyncWaitFunc returns a WaitFunc that will return true when the Application +// is in Sync + Healthy state, and at the specific revision (if supplied. If revision is "", no revision check is made) +func GetAppSyncWaitFunc(revision string, waitForCreation bool) kube.WaitFunc { + return func(ctx context.Context, f kube.Factory, ns, name string) (bool, error) { + rc, err := f.ToRESTConfig() + if err != nil { + return false, err + } - app, err := c.ArgoprojV1alpha1().Applications(ns).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - se, ok := err.(*errors.StatusError) - if !ok || se.ErrStatus.Reason != metav1.StatusReasonNotFound { + c, err := argocdcs.NewForConfig(rc) + if err != nil { return false, err } - return false, nil - } + app, err := c.ArgoprojV1alpha1().Applications(ns).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + se, ok := err.(*kerrors.StatusError) + if !waitForCreation || !ok || se.ErrStatus.Reason != metav1.StatusReasonNotFound { + return false, err + } + + return false, nil + } - log.G().Debugf("Application found, Sync Status = %s", app.Status.Sync.Status) - return app.Status.Sync.Status == v1alpha1.SyncStatusCodeSynced, nil + synced := app.Status.Sync.Status == v1alpha1.SyncStatusCodeSynced + healthy := app.Status.Health.Status == health.HealthStatusHealthy + onRevision := true + if revision != "" { + onRevision = revision == app.Status.Sync.Revision + } + + log.G(ctx).Debugf("Application found, Sync Status: %s, Health Status: %s, Revision: %s", app.Status.Sync.Status, app.Status.Health.Status, app.Status.Sync.Revision) + return synced && healthy && onRevision, nil + } } func (a *addClusterImpl) Execute(ctx context.Context, clusterName string) error { diff --git a/pkg/git/mocks/repository.go b/pkg/git/mocks/repository.go index 29a621fa..9a97fb0e 100644 --- a/pkg/git/mocks/repository.go +++ b/pkg/git/mocks/repository.go @@ -15,15 +15,22 @@ type Repository struct { } // Persist provides a mock function with given fields: ctx, opts -func (_m *Repository) Persist(ctx context.Context, opts *git.PushOptions) error { +func (_m *Repository) Persist(ctx context.Context, opts *git.PushOptions) (string, error) { ret := _m.Called(ctx, opts) - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *git.PushOptions) error); ok { + var r0 string + if rf, ok := ret.Get(0).(func(context.Context, *git.PushOptions) string); ok { r0 = rf(ctx, opts) } else { - r0 = ret.Error(0) + r0 = ret.Get(0).(string) } - return r0 + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *git.PushOptions) error); ok { + r1 = rf(ctx, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } diff --git a/pkg/git/repository.go b/pkg/git/repository.go index 2caf8267..5cb84f9b 100644 --- a/pkg/git/repository.go +++ b/pkg/git/repository.go @@ -33,7 +33,7 @@ type ( // Repository represents a git repository Repository interface { // Persist runs add, commit and push to the repository default remote - Persist(ctx context.Context, opts *PushOptions) error + Persist(ctx context.Context, opts *PushOptions) (string, error) } AddFlagsOptions struct { @@ -192,9 +192,9 @@ func (o *CloneOptions) Path() string { return o.path } -func (r *repo) Persist(ctx context.Context, opts *PushOptions) error { +func (r *repo) Persist(ctx context.Context, opts *PushOptions) (string, error) { if opts == nil { - return ErrNilOpts + return "", ErrNilOpts } addPattern := "." @@ -205,18 +205,19 @@ func (r *repo) Persist(ctx context.Context, opts *PushOptions) error { w, err := worktree(r) if err != nil { - return err + return "", err } if err := w.AddGlob(addPattern); err != nil { - return err + return "", err } - if _, err = w.Commit(opts.CommitMsg, &gg.CommitOptions{All: true}); err != nil { - return err + h, err := w.Commit(opts.CommitMsg, &gg.CommitOptions{All: true}) + if err != nil { + return "", err } - return r.PushContext(ctx, &gg.PushOptions{ + return h.String(), r.PushContext(ctx, &gg.PushOptions{ Auth: getAuth(r.auth), Progress: os.Stderr, }) diff --git a/pkg/git/repository_test.go b/pkg/git/repository_test.go index 333258b3..168cb67a 100644 --- a/pkg/git/repository_test.go +++ b/pkg/git/repository_test.go @@ -514,15 +514,18 @@ func TestGetRepo(t *testing.T) { func Test_repo_Persist(t *testing.T) { tests := map[string]struct { - opts *PushOptions - wantErr bool - retErr error - assertFn func(t *testing.T, r *mocks.Repository, w *mocks.Worktree) + opts *PushOptions + retRevision string + retErr error + assertFn func(t *testing.T, r *mocks.Repository, w *mocks.Worktree, revision string, err error) }{ "NilOpts": { - opts: nil, - wantErr: true, - assertFn: func(t *testing.T, r *mocks.Repository, _ *mocks.Worktree) { + opts: nil, + assertFn: func(t *testing.T, r *mocks.Repository, wt *mocks.Worktree, revision string, err error) { + assert.ErrorIs(t, err, ErrNilOpts) + assert.Equal(t, "", revision) + wt.AssertNotCalled(t, "AddGlob") + wt.AssertNotCalled(t, "Commit") r.AssertNotCalled(t, "PushContext") }, }, @@ -531,13 +534,14 @@ func Test_repo_Persist(t *testing.T) { AddGlobPattern: "", CommitMsg: "hello", }, - wantErr: false, - assertFn: func(t *testing.T, r *mocks.Repository, w *mocks.Worktree) { - r.AssertCalled(t, "PushContext", mock.Anything, mock.Anything) - assert.True(t, reflect.DeepEqual(r.Calls[0].Arguments[1], &gg.PushOptions{ + retRevision: "0dee45f70b37aeb59e6d2efb29855f97df9bccb2", + assertFn: func(t *testing.T, r *mocks.Repository, w *mocks.Worktree, revision string, err error) { + assert.Equal(t, "0dee45f70b37aeb59e6d2efb29855f97df9bccb2", revision) + assert.Nil(t, err) + r.AssertCalled(t, "PushContext", mock.Anything, &gg.PushOptions{ Auth: nil, Progress: os.Stderr, - })) + }) w.AssertCalled(t, "AddGlob", ".") w.AssertCalled(t, "Commit", "hello", mock.Anything) }, @@ -547,13 +551,14 @@ func Test_repo_Persist(t *testing.T) { AddGlobPattern: "test", CommitMsg: "hello", }, - wantErr: false, - assertFn: func(t *testing.T, r *mocks.Repository, w *mocks.Worktree) { - r.AssertCalled(t, "PushContext", mock.Anything, mock.Anything) - assert.True(t, reflect.DeepEqual(r.Calls[0].Arguments[1], &gg.PushOptions{ + retRevision: "0dee45f70b37aeb59e6d2efb29855f97df9bccb2", + assertFn: func(t *testing.T, r *mocks.Repository, w *mocks.Worktree, revision string, err error) { + assert.Equal(t, "0dee45f70b37aeb59e6d2efb29855f97df9bccb2", revision) + assert.Nil(t, err) + r.AssertCalled(t, "PushContext", mock.Anything, &gg.PushOptions{ Auth: nil, Progress: os.Stderr, - })) + }) w.AssertCalled(t, "AddGlob", "test") w.AssertCalled(t, "Commit", "hello", mock.Anything) }, @@ -570,18 +575,15 @@ func Test_repo_Persist(t *testing.T) { mockWt := &mocks.Worktree{} mockWt.On("AddGlob", mock.Anything).Return(tt.retErr) - mockWt.On("Commit", mock.Anything, mock.Anything).Return(nil, tt.retErr) + mockWt.On("Commit", mock.Anything, mock.Anything).Return(plumbing.NewHash(tt.retRevision), tt.retErr) r := &repo{Repository: mockRepo} worktree = func(r gogit.Repository) (gogit.Worktree, error) { return mockWt, tt.retErr } - if err := r.Persist(context.Background(), tt.opts); (err != nil) != tt.wantErr { - t.Errorf("repo.Persist() error = %v, wantErr %v", err, tt.wantErr) - } - - tt.assertFn(t, mockRepo, mockWt) + revision, err := r.Persist(context.Background(), tt.opts) + tt.assertFn(t, mockRepo, mockWt, revision, err) }) } } diff --git a/pkg/kube/kube.go b/pkg/kube/kube.go index 2a21165d..cea80e03 100644 --- a/pkg/kube/kube.go +++ b/pkg/kube/kube.go @@ -3,6 +3,7 @@ package kube import ( "context" "os" + "strings" "time" "github.com/argoproj-labs/argocd-autopilot/pkg/log" @@ -18,6 +19,7 @@ import ( restclient "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "k8s.io/kubectl/pkg/cmd/apply" + del "k8s.io/kubectl/pkg/cmd/delete" cmdutil "k8s.io/kubectl/pkg/cmd/util" ) @@ -43,49 +45,62 @@ func WaitDeploymentReady(ctx context.Context, f Factory, ns, name string) (bool, return d.Status.ReadyReplicas >= *d.Spec.Replicas, nil } -type Factory interface { - // KubernetesClientSet returns a new kubernetes clientset or error - KubernetesClientSet() (kubernetes.Interface, error) +type ( + Factory interface { + // KubernetesClientSet returns a new kubernetes clientset or error + KubernetesClientSet() (kubernetes.Interface, error) - // KubernetesClientSetOrDie calls KubernetesClientSet() and panics if it returns an error - KubernetesClientSetOrDie() kubernetes.Interface + // KubernetesClientSetOrDie calls KubernetesClientSet() and panics if it returns an error + KubernetesClientSetOrDie() kubernetes.Interface - // ToRESTConfig returns a rest Config object or error - ToRESTConfig() (*restclient.Config, error) + // ToRESTConfig returns a rest Config object or error + ToRESTConfig() (*restclient.Config, error) - // Apply applies the provided manifests on the specified namespace - Apply(ctx context.Context, namespace string, manifests []byte) error + // Apply applies the provided manifests on the specified namespace + Apply(ctx context.Context, namespace string, manifests []byte) error - // Wait waits for all of the provided `Resources` to be ready by calling - // the `WaitFunc` of each resource until all of them returns `true` - Wait(context.Context, *WaitOptions) error -} + // Delete delets the resources by their type(s) and labelSelector + Delete(context.Context, *DeleteOptions) error -type Resource struct { - Name string - Namespace string + // Wait waits for all of the provided `Resources` to be ready by calling + // the `WaitFunc` of each resource until all of them returns `true` + Wait(context.Context, *WaitOptions) error + } - // WaitFunc will be called to check if the resources is ready. Should return (true, nil) - // if the resources is ready, (false, nil) if the resource is not ready yet, or (false, err) - // if some error occured (in that case the `Wait` will fail with that error). WaitFunc func(ctx context.Context, f Factory, ns, name string) (bool, error) -} -type WaitOptions struct { - // Inverval the duration between each iteration of calling all of the resources' `WaitFunc`s. - Interval time.Duration + Resource struct { + Name string + Namespace string - // Timeout the max time to wait for all of the resources to be ready. If not all of the - // resourecs are ready at time this will cause `Wait` to return an error. - Timeout time.Duration + // WaitFunc will be called to check if the resources is ready. Should return (true, nil) + // if the resources is ready, (false, nil) if the resource is not ready yet, or (false, err) + // if some error occured (in that case the `Wait` will fail with that error). + WaitFunc WaitFunc + } - // Resources the list of resources to wait for. - Resources []Resource -} + DeleteOptions struct { + LabelSelector string + ResourceTypes []string + Timeout time.Duration + } -type factory struct { - f cmdutil.Factory -} + WaitOptions struct { + // Inverval the duration between each iteration of calling all of the resources' `WaitFunc`s. + Interval time.Duration + + // Timeout the max time to wait for all of the resources to be ready. If not all of the + // resourecs are ready at time this will cause `Wait` to return an error. + Timeout time.Duration + + // Resources the list of resources to wait for. + Resources []Resource + } + + factory struct { + f cmdutil.Factory + } +) func AddFlags(flags *pflag.FlagSet) Factory { confFlags := genericclioptions.NewConfigFlags(true) @@ -156,7 +171,7 @@ func (f *factory) Apply(ctx context.Context, namespace string, manifests []byte) defer func() { os.Stdin = stdin }() cmd := &cobra.Command{ - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { o.DeleteFlags.FileNameFlags.Filenames = &[]string{"-"} o.Overwrite = true @@ -201,6 +216,41 @@ func (f *factory) Apply(ctx context.Context, namespace string, manifests []byte) return cmd.ExecuteContext(ctx) } +func (f *factory) Delete(ctx context.Context, opts *DeleteOptions) error { + timeout := defaultPollTimeout + if opts.Timeout > 0 { + timeout = opts.Timeout + } + + o := &del.DeleteOptions{ + IOStreams: DefaultIOStreams(), + CascadingStrategy: metav1.DeletePropagationForeground, + DeleteAllNamespaces: true, + LabelSelector: opts.LabelSelector, + Timeout: timeout, + WaitForDeletion: true, + } + + cmd := &cobra.Command{ + RunE: func(cmd *cobra.Command, _ []string) error { + args := strings.Join(opts.ResourceTypes, ",") + if err := o.Complete(f.f, []string{args}, cmd); err != nil { + return err + } + + return o.RunDelete(f.f) + }, + SilenceErrors: true, + SilenceUsage: true, + } + + cmdutil.AddDryRunFlag(cmd) + + cmd.SetArgs([]string{}) + + return cmd.ExecuteContext(ctx) +} + func (f *factory) Wait(ctx context.Context, opts *WaitOptions) error { itr := 0 resources := map[*Resource]bool{} diff --git a/pkg/kube/mocks/kube.go b/pkg/kube/mocks/kube.go index 5d5d293d..040f63ae 100644 --- a/pkg/kube/mocks/kube.go +++ b/pkg/kube/mocks/kube.go @@ -32,6 +32,20 @@ func (_m *Factory) Apply(ctx context.Context, namespace string, manifests []byte return r0 } +// Delete provides a mock function with given fields: _a0, _a1 +func (_m *Factory) Delete(_a0 context.Context, _a1 *kube.DeleteOptions) error { + ret := _m.Called(_a0, _a1) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *kube.DeleteOptions) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // KubernetesClientSet provides a mock function with given fields: func (_m *Factory) KubernetesClientSet() (kubernetes.Interface, error) { ret := _m.Called() diff --git a/pkg/store/store.go b/pkg/store/store.go index 1cf54c96..5e7d4775 100644 --- a/pkg/store/store.go +++ b/pkg/store/store.go @@ -34,42 +34,45 @@ type Store struct { } var Default = struct { - BootsrtrapDir string AppsDir string - OverlaysDir string - BaseDir string - ClusterResourcesDir string ArgoCDName string ArgoCDNamespace string + BaseDir string BootsrtrapAppName string + BootsrtrapDir string + ClusterContextName string + ClusterResourcesDir string + DestServer string DummyName string + DestServerAnnotation string + GitUsername string + LabelKeyAppManagedBy string + LabelValueManagedBy string + OverlaysDir string ProjectsDir string - ManagedBy string RootAppName string RepoCredsSecretName string - GitUsername string WaitInterval time.Duration - DestServer string - DestServerAnnotation string - ClusterContextName string }{ AppsDir: "apps", - BootsrtrapDir: "bootstrap", - OverlaysDir: "overlays", - BaseDir: "base", - ClusterResourcesDir: "cluster-resources", ArgoCDName: "argo-cd", ArgoCDNamespace: "argocd", + BaseDir: "base", BootsrtrapAppName: "autopilot-bootstrap", + BootsrtrapDir: "bootstrap", + ClusterContextName: "in-cluster", + ClusterResourcesDir: "cluster-resources", + DestServer: "https://kubernetes.default.svc", + DestServerAnnotation: "argocd-autopilot.argoproj-labs.io/default-dest-server", + DummyName: "DUMMY", + GitUsername: "username", + LabelKeyAppManagedBy: "app.kubernetes.io/managed-by", + LabelValueManagedBy: "argocd-autopilot", + OverlaysDir: "overlays", ProjectsDir: "projects", - ManagedBy: "argo-autopilot", RootAppName: "root", RepoCredsSecretName: "autopilot-secret", - GitUsername: "username", WaitInterval: time.Second * 3, - DestServer: "https://kubernetes.default.svc", - DestServerAnnotation: "argocd-autopilot.argoproj-labs.io/default-dest-server", - ClusterContextName: "in-cluster", } // Get returns the global store