diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 40a9edd3..70c3f0e2 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -30,7 +30,7 @@ jobs: - run: go test -v -race ./pkg/test/e2e/... -tags="e2e" - run: | cd $HOME/test - plural destroy --force --commit="" + plural destroy --force --all --commit="" env: PLURAL_DESTROY_CONFIRM: true PLURAL_DESTROY_AFFIRM_UNINSTALL_APPS: true diff --git a/cmd/plural/deploy.go b/cmd/plural/deploy.go index 016213af..35392de0 100644 --- a/cmd/plural/deploy.go +++ b/cmd/plural/deploy.go @@ -117,7 +117,6 @@ func (p *Plural) build(c *cli.Context) error { } func (p *Plural) doBuild(installation *api.Installation, force bool) error { - p.InitPluralClient() repoName := installation.Repository.Name fmt.Printf("Building workspace for %s\n", repoName) @@ -360,6 +359,7 @@ func (p *Plural) destroy(c *cli.Context) error { repoName := c.Args().Get(0) repoRoot, err := git.Root() force := c.Bool("force") + all := c.Bool("all") if err != nil { return err } @@ -367,6 +367,8 @@ func (p *Plural) destroy(c *cli.Context) error { infix := "this workspace" if repoName != "" { infix = repoName + } else if !all { + return fmt.Errorf("you must either specify an individual application or `--all` to destroy the entire workspace") } if !force && !confirm(fmt.Sprintf("Are you sure you want to destroy %s?", infix), "PLURAL_DESTROY_CONFIRM") { diff --git a/cmd/plural/init.go b/cmd/plural/init.go index 96a929fb..d9d76150 100644 --- a/cmd/plural/init.go +++ b/cmd/plural/init.go @@ -51,7 +51,7 @@ func (p *Plural) handleInit(c *cli.Context) error { } prov, err := runPreflights() - if err != nil { + if err != nil && !c.Bool("ignore-preflights") { return err } diff --git a/cmd/plural/plural.go b/cmd/plural/plural.go index a70dfd49..3539b330 100644 --- a/cmd/plural/plural.go +++ b/cmd/plural/plural.go @@ -235,6 +235,10 @@ func (p *Plural) getCommands() []cli.Command { Name: "force", Usage: "use force push when pushing to git", }, + cli.BoolFlag{ + Name: "all", + Usage: "tear down the entire cluster gracefully in one go", + }, }, Action: tracked(latestVersion(owned(upstreamSynced(p.destroy))), "cli.destroy"), }, @@ -250,6 +254,10 @@ func (p *Plural) getCommands() []cli.Command { Name: "service-account", Usage: "email for the service account you'd like to use for this workspace", }, + cli.BoolFlag{ + Name: "ignore-preflights", + Usage: "whether to ignore preflight check failures prior to init", + }, }, Action: tracked(latestVersion(p.handleInit), "cli.init"), }, diff --git a/pkg/provider/gcp.go b/pkg/provider/gcp.go index 80541b69..f063a84f 100644 --- a/pkg/provider/gcp.go +++ b/pkg/provider/gcp.go @@ -364,16 +364,15 @@ func (gcp *GCPProvider) validateEnabled() error { return errEnabled } - wrapped := func(name string) string { - return fmt.Sprintf("projects/%s/services/%s", proj.ProjectId, name) - } + services := algorithms.Map([]string{ + "serviceusage.googleapis.com", + "cloudresourcemanager.googleapis.com", + "container.googleapis.com", + }, func(name string) string { return fmt.Sprintf("projects/%s/services/%s", proj.ProjectId, name) }) + parent := fmt.Sprintf("projects/%s", proj.ProjectId) req := &serviceusagepb.BatchGetServicesRequest{ - Parent: fmt.Sprintf("projects/%s", proj.ProjectId), - Names: []string{ - wrapped("serviceusage.googleapis.com"), - wrapped("cloudresourcemanager.googleapis.com"), - wrapped("container.googleapis.com"), - }, + Parent: parent, + Names: services, } resp, err := c.BatchGetServices(ctx, req) if err != nil { @@ -381,15 +380,35 @@ func (gcp *GCPProvider) validateEnabled() error { return errEnabled } - for _, svc := range resp.Services { - if svc.State != serviceusagepb.State_ENABLED { - utils.LogError().Printf("the service state %v != %v", svc.State, serviceusagepb.State_ENABLED) + missing := algorithms.Filter(resp.Services, func(svc *serviceusagepb.Service) bool { + return svc.State != serviceusagepb.State_ENABLED + }) + + if len(missing) > 0 { + services := algorithms.Map(missing, func(svc *serviceusagepb.Service) string { return svc.Name }) + enableReq := &serviceusagepb.BatchEnableServicesRequest{ + Parent: parent, + ServiceIds: services, + } + utils.LogError().Printf("Attempting to enable services %v", services) + if err := tryToEnableServices(ctx, c, enableReq); err != nil { return errEnabled } } + return nil } +func tryToEnableServices(ctx context.Context, client *serviceusage.Client, req *serviceusagepb.BatchEnableServicesRequest) (err error) { + op, err := client.BatchEnableServices(ctx, req) + if err != nil { + return + } + + _, err = op.Wait(ctx) + return +} + func (gcp *GCPProvider) Permissions() (permissions.Checker, error) { proj, err := gcp.getProject() if err != nil { diff --git a/pkg/scaffold/terraform.go b/pkg/scaffold/terraform.go index a24a4a4b..74480f98 100644 --- a/pkg/scaffold/terraform.go +++ b/pkg/scaffold/terraform.go @@ -13,6 +13,7 @@ import ( "golang.org/x/mod/semver" "github.com/pluralsh/plural/pkg/api" + "github.com/pluralsh/plural/pkg/config" "github.com/pluralsh/plural/pkg/template" "github.com/pluralsh/plural/pkg/utils" "github.com/pluralsh/plural/pkg/utils/pathing" @@ -122,6 +123,7 @@ func (scaffold *Scaffold) handleTerraform(wk *wkspace.Workspace) error { "Namespace": wk.Config.Namespace(repo.Name), "Region": wk.Provider.Region(), "Context": wk.Provider.Context(), + "Config": config.Read(), "Applications": apps, } if err := tmpl.Execute(&buf, values); err != nil { diff --git a/pkg/server/setup.go b/pkg/server/setup.go index 2a3dab6a..23c2c1f7 100644 --- a/pkg/server/setup.go +++ b/pkg/server/setup.go @@ -14,6 +14,8 @@ import ( "github.com/pluralsh/plural/pkg/provider" "github.com/pluralsh/plural/pkg/utils" "github.com/pluralsh/plural/pkg/wkspace" + + "github.com/pluralsh/polly/algorithms" ) func toConfig(setup *SetupRequest) *config.Config { @@ -151,6 +153,11 @@ func setupCli(c *gin.Context) error { if err != nil { return err } + + if err := runPreflights(prov); err != nil { + return err + } + missing, err := prov.Permissions() if err != nil { return err @@ -161,3 +168,20 @@ func setupCli(c *gin.Context) error { c.JSON(http.StatusOK, gin.H{"success": true, "missing": missing}) return nil } + +func runPreflights(prov provider.Provider) error { + // run only relevant preflights + preflights := []*provider.Preflight{} + if prov.Name() == provider.GCP { + preflights = algorithms.Filter(prov.Preflights(), func(pre *provider.Preflight) bool { + return pre.Name == "Enabled Services" + }) + } + + for _, pre := range preflights { + if err := pre.Validate(); err != nil { + return err + } + } + return nil +}