github.com/pluralsh/plural-cli@v0.9.5/cmd/plural/deploy.go (about)

     1  package plural
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	"strings"
     8  
     9  	"github.com/AlecAivazis/survey/v2"
    10  	"github.com/pluralsh/plural-cli/pkg/api"
    11  	"github.com/pluralsh/plural-cli/pkg/application"
    12  	"github.com/pluralsh/plural-cli/pkg/bootstrap"
    13  	"github.com/pluralsh/plural-cli/pkg/diff"
    14  	"github.com/pluralsh/plural-cli/pkg/executor"
    15  	"github.com/pluralsh/plural-cli/pkg/kubernetes"
    16  	"github.com/pluralsh/plural-cli/pkg/manifest"
    17  	"github.com/pluralsh/plural-cli/pkg/scaffold"
    18  	"github.com/pluralsh/plural-cli/pkg/utils"
    19  	"github.com/pluralsh/plural-cli/pkg/utils/errors"
    20  	"github.com/pluralsh/plural-cli/pkg/utils/git"
    21  	"github.com/pluralsh/plural-cli/pkg/utils/pathing"
    22  	"github.com/pluralsh/plural-cli/pkg/wkspace"
    23  	"github.com/pluralsh/polly/algorithms"
    24  	"github.com/pluralsh/polly/containers"
    25  	"github.com/urfave/cli"
    26  )
    27  
    28  const Bootstrap = "bootstrap"
    29  
    30  func (p *Plural) getSortedInstallations(repo string) ([]*api.Installation, error) {
    31  	p.InitPluralClient()
    32  	installations, err := p.GetInstallations()
    33  	if err != nil {
    34  		return installations, api.GetErrorResponse(err, "GetInstallations")
    35  	}
    36  
    37  	if len(installations) == 0 {
    38  		return installations, fmt.Errorf("no installations present, run `plural bundle install <repo> <bundle-name>` to install your first app")
    39  	}
    40  
    41  	sorted, err := wkspace.UntilRepo(p.Client, repo, installations)
    42  	if err != nil {
    43  		sorted = installations // we don't know all the dependencies yet
    44  	}
    45  
    46  	return sorted, nil
    47  }
    48  
    49  func (p *Plural) allSortedRepos() ([]string, error) {
    50  	p.InitPluralClient()
    51  	insts, err := p.GetInstallations()
    52  	if err != nil {
    53  		return nil, api.GetErrorResponse(err, "GetInstallations")
    54  	}
    55  
    56  	return wkspace.SortAndFilter(insts)
    57  }
    58  
    59  func getSortedNames(filter bool) ([]string, error) {
    60  	diffed, err := wkspace.DiffedRepos()
    61  	if err != nil {
    62  		return nil, err
    63  	}
    64  
    65  	sorted, err := wkspace.TopSortNames(diffed)
    66  	if err != nil {
    67  		return nil, err
    68  	}
    69  
    70  	if filter {
    71  		repos := containers.ToSet(diffed)
    72  		return algorithms.Filter(sorted, repos.Has), nil
    73  	}
    74  
    75  	return sorted, nil
    76  }
    77  
    78  func diffed(_ *cli.Context) error {
    79  	diffed, err := wkspace.DiffedRepos()
    80  	if err != nil {
    81  		return err
    82  	}
    83  
    84  	for _, d := range diffed {
    85  		fmt.Println(d)
    86  	}
    87  
    88  	return nil
    89  }
    90  
    91  func (p *Plural) build(c *cli.Context) error {
    92  	p.InitPluralClient()
    93  	force := c.Bool("force")
    94  	if err := CheckGitCrypt(c); err != nil {
    95  		return errors.ErrorWrap(errNoGit, "Failed to scan your repo for secrets to encrypt them")
    96  	}
    97  
    98  	if c.IsSet("only") {
    99  		installation, err := p.GetInstallation(c.String("only"))
   100  		if err != nil {
   101  			return api.GetErrorResponse(err, "GetInstallation")
   102  		} else if installation == nil {
   103  			return utils.HighlightError(fmt.Errorf("%s is not installed. Please install it with `plural bundle install`", c.String("only")))
   104  		}
   105  
   106  		return p.doBuild(installation, force)
   107  	}
   108  
   109  	installations, err := p.getSortedInstallations("")
   110  	if err != nil {
   111  		return err
   112  	}
   113  
   114  	for _, installation := range installations {
   115  		if err := p.doBuild(installation, force); err != nil {
   116  			return err
   117  		}
   118  	}
   119  	return nil
   120  }
   121  
   122  func (p *Plural) doBuild(installation *api.Installation, force bool) error {
   123  	repoName := installation.Repository.Name
   124  	fmt.Printf("Building workspace for %s\n", repoName)
   125  
   126  	if !wkspace.Configured(repoName) {
   127  		fmt.Printf("You have not locally configured %s but have it registered as an installation in our api, ", repoName)
   128  		fmt.Printf("either delete it with `plural apps uninstall %s` or install it locally via a bundle in `plural bundle list %s`\n", repoName, repoName)
   129  		return nil
   130  	}
   131  
   132  	workspace, err := wkspace.New(p.Client, installation)
   133  	if err != nil {
   134  		return err
   135  	}
   136  
   137  	vsn, ok := workspace.RequiredCliVsn()
   138  	if ok && !versionValid(vsn) {
   139  		return fmt.Errorf("Your cli version is not sufficient to complete this build, please update to at least %s", vsn)
   140  	}
   141  
   142  	if err := workspace.Prepare(); err != nil {
   143  		return err
   144  	}
   145  
   146  	build, err := scaffold.Scaffolds(workspace)
   147  	if err != nil {
   148  		return err
   149  	}
   150  
   151  	err = build.Execute(workspace, force)
   152  	if err == nil {
   153  		utils.Success("Finished building %s\n\n", repoName)
   154  	}
   155  
   156  	workspace.PrintLinks()
   157  
   158  	appReadme(repoName, false) // nolint:errcheck
   159  	return err
   160  }
   161  
   162  func (p *Plural) info(c *cli.Context) error {
   163  	p.InitPluralClient()
   164  	repo := c.Args().Get(0)
   165  	installation, err := p.GetInstallation(repo)
   166  	if err != nil {
   167  		return api.GetErrorResponse(err, "GetInstallation")
   168  	}
   169  	if installation == nil {
   170  		return fmt.Errorf("You have not installed %s", repo)
   171  	}
   172  
   173  	return scaffold.Notes(installation)
   174  }
   175  
   176  func (p *Plural) deploy(c *cli.Context) error {
   177  	p.InitPluralClient()
   178  	verbose := c.Bool("verbose")
   179  	repoRoot, err := git.Root()
   180  	if err != nil {
   181  		return err
   182  	}
   183  
   184  	project, err := manifest.FetchProject()
   185  	if err != nil {
   186  		return err
   187  	}
   188  
   189  	var sorted []string
   190  	switch {
   191  	case len(c.StringSlice("from")) > 0:
   192  		sorted, err = wkspace.AllDependencies(c.StringSlice("from"))
   193  	case c.Bool("all"):
   194  		sorted, err = p.allSortedRepos()
   195  	default:
   196  		sorted, err = getSortedNames(true)
   197  	}
   198  	if err != nil {
   199  		return err
   200  	}
   201  
   202  	fmt.Printf("Deploying applications [%s] in topological order\n\n", strings.Join(sorted, ", "))
   203  
   204  	ignoreConsole := c.Bool("ignore-console")
   205  	for _, repo := range sorted {
   206  		if ignoreConsole && (repo == "console" || repo == Bootstrap) {
   207  			continue
   208  		}
   209  
   210  		if repo == Bootstrap && project.ClusterAPI {
   211  			ready, err := bootstrap.CheckClusterReadiness(project.Cluster, Bootstrap)
   212  
   213  			// Stop if cluster exists, but it is not ready yet.
   214  			if err != nil && err.Error() == bootstrap.ClusterNotReadyError {
   215  				return err
   216  			}
   217  
   218  			// If cluster does not exist bootstrap needs to be done first.
   219  			if !ready {
   220  				err := bootstrap.BootstrapCluster(RunPlural)
   221  				if err != nil {
   222  					return err
   223  				}
   224  			}
   225  		}
   226  
   227  		execution, err := executor.GetExecution(pathing.SanitizeFilepath(filepath.Join(repoRoot, repo)), "deploy")
   228  		if err != nil {
   229  			return err
   230  		}
   231  
   232  		if err := execution.Execute("deploying", verbose); err != nil {
   233  			utils.Note("It looks like your deployment failed. This may be a transient issue and rerunning the `plural deploy` command may resolve it. Or, feel free to reach out to us on discord (https://discord.gg/bEBAMXV64s) or Intercom and we should be able to help you out\n")
   234  			return err
   235  		}
   236  
   237  		fmt.Printf("\n")
   238  
   239  		installation, err := p.GetInstallation(repo)
   240  		if err != nil {
   241  			return api.GetErrorResponse(err, "GetInstallation")
   242  		}
   243  		if installation == nil {
   244  			return fmt.Errorf("The %s was unistalled, run `plural bundle install %s <bundle-name>` ", repo, repo)
   245  		}
   246  
   247  		if err := p.Client.MarkSynced(repo); err != nil {
   248  			utils.Warn("failed to mark %s as synced, this is not a critical error but might drift state in our api, you can run `plural repos synced %s` to mark it manually", repo, repo)
   249  		}
   250  
   251  		if c.Bool("silence") {
   252  			continue
   253  		}
   254  
   255  		if man, err := fetchManifest(repo); err == nil && man.Wait {
   256  			if kubeConf, err := kubernetes.KubeConfig(); err == nil {
   257  				fmt.Printf("Waiting for %s to become ready...\n", repo)
   258  				if err := application.SilentWait(kubeConf, repo); err != nil {
   259  					return err
   260  				}
   261  				fmt.Println("")
   262  			}
   263  		}
   264  
   265  		if err := scaffold.Notes(installation); err != nil {
   266  			return err
   267  		}
   268  	}
   269  
   270  	utils.Highlight("\n==> Commit and push your changes to record your deployment\n\n")
   271  
   272  	if commit := commitMsg(c); commit != "" {
   273  		utils.Highlight("Pushing upstream...\n")
   274  		return git.Sync(repoRoot, commit, c.Bool("force"))
   275  	}
   276  
   277  	return nil
   278  }
   279  
   280  func commitMsg(c *cli.Context) string {
   281  	if commit := c.String("commit"); commit != "" {
   282  		return commit
   283  	}
   284  
   285  	if !c.Bool("silence") {
   286  		var commit string
   287  		if err := survey.AskOne(&survey.Input{Message: "Enter a commit message (empty to not commit right now)"}, &commit); err != nil {
   288  			return ""
   289  		}
   290  		return commit
   291  	}
   292  
   293  	return ""
   294  }
   295  
   296  func handleDiff(_ *cli.Context) error {
   297  	repoRoot, err := git.Root()
   298  	if err != nil {
   299  		return err
   300  	}
   301  
   302  	sorted, err := getSortedNames(true)
   303  	if err != nil {
   304  		return err
   305  	}
   306  
   307  	fmt.Printf("Diffing applications [%s] in topological order\n\n", strings.Join(sorted, ", "))
   308  
   309  	for _, repo := range sorted {
   310  		d, err := diff.GetDiff(pathing.SanitizeFilepath(filepath.Join(repoRoot, repo)), "diff")
   311  		if err != nil {
   312  			return err
   313  		}
   314  
   315  		if err := d.Execute(); err != nil {
   316  			return err
   317  		}
   318  
   319  		fmt.Printf("\n")
   320  	}
   321  	return nil
   322  }
   323  
   324  func (p *Plural) bounce(c *cli.Context) error {
   325  	p.InitPluralClient()
   326  	repoRoot, err := git.Root()
   327  	if err != nil {
   328  		return err
   329  	}
   330  	repoName := c.Args().Get(0)
   331  
   332  	if repoName != "" {
   333  		installation, err := p.GetInstallation(repoName)
   334  		if err != nil {
   335  			return api.GetErrorResponse(err, "GetInstallation")
   336  		}
   337  		return p.doBounce(repoRoot, installation)
   338  	}
   339  
   340  	installations, err := p.getSortedInstallations(repoName)
   341  	if err != nil {
   342  		return err
   343  	}
   344  
   345  	for _, installation := range installations {
   346  		if err := p.doBounce(repoRoot, installation); err != nil {
   347  			return err
   348  		}
   349  	}
   350  	return nil
   351  }
   352  
   353  func (p *Plural) doBounce(repoRoot string, installation *api.Installation) error {
   354  	p.InitPluralClient()
   355  	repoName := installation.Repository.Name
   356  	utils.Warn("bouncing deployments in %s\n", repoName)
   357  	workspace, err := wkspace.New(p.Client, installation)
   358  	if err != nil {
   359  		return err
   360  	}
   361  
   362  	if err := os.Chdir(pathing.SanitizeFilepath(filepath.Join(repoRoot, repoName))); err != nil {
   363  		return err
   364  	}
   365  	return workspace.Bounce()
   366  }
   367  
   368  func (p *Plural) destroy(c *cli.Context) error {
   369  	p.InitPluralClient()
   370  	repoName := c.Args().Get(0)
   371  	repoRoot, err := git.Root()
   372  	if err != nil {
   373  		return err
   374  	}
   375  	force := c.Bool("force")
   376  	all := c.Bool("all")
   377  
   378  	project, err := manifest.FetchProject()
   379  	if err != nil {
   380  		return err
   381  	}
   382  
   383  	infix := "this workspace"
   384  	if repoName != "" {
   385  		infix = repoName
   386  	} else if !all {
   387  		return fmt.Errorf("you must either specify an individual application or `--all` to destroy the entire workspace")
   388  	}
   389  
   390  	if !force && !confirm(fmt.Sprintf("Are you sure you want to destroy %s?", infix), "PLURAL_DESTROY_CONFIRM") {
   391  		return nil
   392  	}
   393  
   394  	delete := force || affirm("Do you want to uninstall your applications from the plural api as well?", "PLURAL_DESTROY_AFFIRM_UNINSTALL_APPS")
   395  
   396  	if repoName != "" {
   397  		installation, err := p.GetInstallation(repoName)
   398  		if err != nil {
   399  			return api.GetErrorResponse(err, "GetInstallation")
   400  		}
   401  
   402  		if installation == nil {
   403  			return fmt.Errorf("No installation for app %s to destroy, if the app is still in your repo, you can always run cd %s/terraform && terraform destroy", repoName, repoName)
   404  		}
   405  
   406  		return p.doDestroy(repoRoot, installation, delete, project.ClusterAPI)
   407  	}
   408  
   409  	installations, err := p.getSortedInstallations(repoName)
   410  	if err != nil {
   411  		return err
   412  	}
   413  
   414  	from := c.String("from")
   415  	started := from == ""
   416  	for i := len(installations) - 1; i >= 0; i-- {
   417  		installation := installations[i]
   418  		if installation.Repository.Name == from {
   419  			started = true
   420  		}
   421  
   422  		if !started {
   423  			continue
   424  		}
   425  
   426  		if err := p.doDestroy(repoRoot, installation, delete, project.ClusterAPI); err != nil {
   427  			return err
   428  		}
   429  	}
   430  
   431  	man, _ := manifest.FetchProject()
   432  	if err := p.DeleteEabCredential(man.Cluster, man.Provider); err != nil {
   433  		fmt.Printf("no eab key to delete %s\n", err)
   434  	}
   435  
   436  	if repoName == "" {
   437  		utils.Success("Finished destroying workspace\n")
   438  		utils.Note("if you want to recreate this workspace, be sure to rename the cluster to ensure a clean redeploy")
   439  		man, err := manifest.FetchProject()
   440  		if err != nil {
   441  			return err
   442  		}
   443  		if err := p.DestroyCluster(man.Network.Subdomain, man.Cluster, man.Provider); err != nil {
   444  			return api.GetErrorResponse(err, "DestroyCluster")
   445  		}
   446  	}
   447  
   448  	utils.Highlight("\n==> Commit and push your changes to record your workspace changes\n\n")
   449  
   450  	if commit := commitMsg(c); commit != "" {
   451  		utils.Highlight("Pushing upstream...\n")
   452  		return git.Sync(repoRoot, commit, force)
   453  	}
   454  
   455  	return nil
   456  }
   457  
   458  func (p *Plural) doDestroy(repoRoot string, installation *api.Installation, delete, clusterAPI bool) error {
   459  	p.InitPluralClient()
   460  	if err := os.Chdir(repoRoot); err != nil {
   461  		return err
   462  	}
   463  	repo := installation.Repository.Name
   464  	if ctx, err := manifest.FetchContext(); err == nil && ctx.Protected(repo) {
   465  		return fmt.Errorf("This app is protected, you cannot plural destroy without updating context.yaml")
   466  	}
   467  
   468  	utils.Error("\nDestroying application %s\n", repo)
   469  	workspace, err := wkspace.New(p.Client, installation)
   470  	if err != nil {
   471  		return err
   472  	}
   473  
   474  	if repo == Bootstrap && clusterAPI {
   475  		if err = bootstrap.DestroyCluster(workspace.Destroy, RunPlural); err != nil {
   476  			return err
   477  		}
   478  
   479  	} else {
   480  		if err := workspace.Destroy(); err != nil {
   481  			return err
   482  		}
   483  	}
   484  
   485  	if delete {
   486  		utils.Highlight("Uninstalling %s from the plural api as well...\n", repo)
   487  		return p.Client.DeleteInstallation(installation.Id)
   488  	}
   489  
   490  	return nil
   491  }
   492  
   493  func fetchManifest(repo string) (*manifest.Manifest, error) {
   494  	p, err := manifest.ManifestPath(repo)
   495  	if err != nil {
   496  		return nil, err
   497  	}
   498  
   499  	return manifest.Read(p)
   500  }