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 }