From 7d8e2d9dd153c162814bc0fc908259e3111f582c Mon Sep 17 00:00:00 2001 From: decentral1se Date: Wed, 3 Nov 2021 09:10:13 +0100 Subject: [PATCH] WIP: make "abra app new" callable by code Part of https://git.coopcloud.tech/coop-cloud/organising/issues/212. --- cli/app/deploy.go | 197 ++++++++++++++++++++------------------- cli/app/new.go | 186 +----------------------------------- cli/internal/app.go | 189 +++++++++++++++++++++++++++++++++++++ cli/internal/validate.go | 7 +- cli/server/add.go | 11 ++- 5 files changed, 308 insertions(+), 282 deletions(-) create mode 100644 cli/internal/app.go diff --git a/cli/app/deploy.go b/cli/app/deploy.go index 6f111417..abf913eb 100644 --- a/cli/app/deploy.go +++ b/cli/app/deploy.go @@ -36,103 +36,7 @@ Chas mode ("--chaos") will deploy your local checkout of a recipe as-is, including unstaged changes and can be useful for live hacking and testing new recipes. `, - Action: func(c *cli.Context) error { - app := internal.ValidateApp(c) - stackName := app.StackName() - - cl, err := client.New(app.Server) - if err != nil { - logrus.Fatal(err) - } - - logrus.Debugf("checking whether '%s' is already deployed", stackName) - - isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName) - if err != nil { - logrus.Fatal(err) - } - - if isDeployed { - if internal.Force { - logrus.Warnf("'%s' already deployed but continuing (--force)", stackName) - } else if internal.Chaos { - logrus.Warnf("'%s' already deployed but continuing (--chaos)", stackName) - } else { - logrus.Fatalf("'%s' is already deployed", stackName) - } - } - - version := deployedVersion - if version == "" && !internal.Chaos { - versions, err := catalogue.GetRecipeCatalogueVersions(app.Type) - if err != nil { - logrus.Fatal(err) - } - if len(versions) > 0 { - version = versions[len(versions)-1] - logrus.Debugf("choosing '%s' as version to deploy", version) - if err := recipe.EnsureVersion(app.Type, version); err != nil { - logrus.Fatal(err) - } - } else { - version = "latest commit" - logrus.Warn("no versions detected, using latest commit") - if err := recipe.EnsureLatest(app.Type); err != nil { - logrus.Fatal(err) - } - } - } - - if version == "" && !internal.Chaos { - logrus.Debugf("choosing '%s' as version to deploy", version) - if err := recipe.EnsureVersion(app.Type, version); err != nil { - logrus.Fatal(err) - } - } - - if internal.Chaos { - logrus.Warnf("chaos mode engaged") - var err error - version, err = recipe.ChaosVersion(app.Type) - if err != nil { - logrus.Fatal(err) - } - } - - abraShPath := fmt.Sprintf("%s/%s/%s", config.APPS_DIR, app.Type, "abra.sh") - abraShEnv, err := config.ReadAbraShEnvVars(abraShPath) - if err != nil { - logrus.Fatal(err) - } - for k, v := range abraShEnv { - app.Env[k] = v - } - - composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env) - if err != nil { - logrus.Fatal(err) - } - deployOpts := stack.Deploy{ - Composefiles: composeFiles, - Namespace: stackName, - Prune: false, - ResolveImage: stack.ResolveImageAlways, - } - compose, err := config.GetAppComposeConfig(app.Name, deployOpts, app.Env) - if err != nil { - logrus.Fatal(err) - } - - if err := DeployOverview(app, version, "continue with deployment?"); err != nil { - logrus.Fatal(err) - } - - if err := stack.RunDeploy(cl, deployOpts, compose); err != nil { - logrus.Fatal(err) - } - - return nil - }, + Action: DeployAction, BashComplete: func(c *cli.Context) { appNames, err := config.GetAppNames() if err != nil { @@ -147,6 +51,105 @@ recipes. }, } +// DeployAction is the main command-line action for this package +func DeployAction(c *cli.Context) error { + app := internal.ValidateApp(c) + stackName := app.StackName() + + cl, err := client.New(app.Server) + if err != nil { + logrus.Fatal(err) + } + + logrus.Debugf("checking whether '%s' is already deployed", stackName) + + isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName) + if err != nil { + logrus.Fatal(err) + } + + if isDeployed { + if internal.Force { + logrus.Warnf("'%s' already deployed but continuing (--force)", stackName) + } else if internal.Chaos { + logrus.Warnf("'%s' already deployed but continuing (--chaos)", stackName) + } else { + logrus.Fatalf("'%s' is already deployed", stackName) + } + } + + version := deployedVersion + if version == "" && !internal.Chaos { + versions, err := catalogue.GetRecipeCatalogueVersions(app.Type) + if err != nil { + logrus.Fatal(err) + } + if len(versions) > 0 { + version = versions[len(versions)-1] + logrus.Debugf("choosing '%s' as version to deploy", version) + if err := recipe.EnsureVersion(app.Type, version); err != nil { + logrus.Fatal(err) + } + } else { + version = "latest commit" + logrus.Warn("no versions detected, using latest commit") + if err := recipe.EnsureLatest(app.Type); err != nil { + logrus.Fatal(err) + } + } + } + + if version == "" && !internal.Chaos { + logrus.Debugf("choosing '%s' as version to deploy", version) + if err := recipe.EnsureVersion(app.Type, version); err != nil { + logrus.Fatal(err) + } + } + + if internal.Chaos { + logrus.Warnf("chaos mode engaged") + var err error + version, err = recipe.ChaosVersion(app.Type) + if err != nil { + logrus.Fatal(err) + } + } + + abraShPath := fmt.Sprintf("%s/%s/%s", config.APPS_DIR, app.Type, "abra.sh") + abraShEnv, err := config.ReadAbraShEnvVars(abraShPath) + if err != nil { + logrus.Fatal(err) + } + for k, v := range abraShEnv { + app.Env[k] = v + } + + composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env) + if err != nil { + logrus.Fatal(err) + } + deployOpts := stack.Deploy{ + Composefiles: composeFiles, + Namespace: stackName, + Prune: false, + ResolveImage: stack.ResolveImageAlways, + } + compose, err := config.GetAppComposeConfig(app.Name, deployOpts, app.Env) + if err != nil { + logrus.Fatal(err) + } + + if err := DeployOverview(app, version, "continue with deployment?"); err != nil { + logrus.Fatal(err) + } + + if err := stack.RunDeploy(cl, deployOpts, compose); err != nil { + logrus.Fatal(err) + } + + return nil +} + // DeployOverview shows a deployment overview func DeployOverview(app config.App, version, message string) error { if internal.NoInput { diff --git a/cli/app/new.go b/cli/app/new.go index 0461df7c..7a6b9eae 100644 --- a/cli/app/new.go +++ b/cli/app/new.go @@ -2,48 +2,13 @@ package app import ( "fmt" - "path" - abraFormatter "coopcloud.tech/abra/cli/formatter" "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/catalogue" - "coopcloud.tech/abra/pkg/config" - "coopcloud.tech/abra/pkg/recipe" - "coopcloud.tech/abra/pkg/secret" - "github.com/AlecAivazis/survey/v2" "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" ) -type secrets map[string]string - -var domain string -var domainFlag = &cli.StringFlag{ - Name: "domain", - Aliases: []string{"d"}, - Value: "", - Usage: "Choose a domain name", - Destination: &domain, -} - -var newAppServer string -var newAppServerFlag = &cli.StringFlag{ - Name: "server", - Aliases: []string{"s"}, - Value: "", - Usage: "Show apps of a specific server", - Destination: &newAppServer, -} - -var newAppName string -var newAppNameFlag = &cli.StringFlag{ - Name: "app-name", - Aliases: []string{"a"}, - Value: "", - Usage: "Choose an app name", - Destination: &newAppName, -} - var appNewDescription = ` This command takes a recipe and uses it to create a new app. This new app configuration is stored in your ~/.abra directory under the appropriate server. @@ -70,14 +35,14 @@ var appNewCommand = &cli.Command{ Aliases: []string{"n"}, Description: appNewDescription, Flags: []cli.Flag{ - newAppServerFlag, - domainFlag, - newAppNameFlag, + internal.NewAppServerFlag, + internal.DomainFlag, + internal.NewAppNameFlag, internal.PassFlag, internal.SecretsFlag, }, ArgsUsage: "", - Action: action, + Action: internal.NewAction, BashComplete: func(c *cli.Context) { catl, err := catalogue.ReadRecipeCatalogue() if err != nil { @@ -91,146 +56,3 @@ var appNewCommand = &cli.Command{ } }, } - -// ensureDomainFlag checks if the domain flag was used. if not, asks the user for it/ -func ensureDomainFlag(recipe recipe.Recipe, server string) error { - if domain == "" { - prompt := &survey.Input{ - Message: "Specify app domain", - Default: fmt.Sprintf("%s.%s", recipe.Name, server), - } - if err := survey.AskOne(prompt, &domain); err != nil { - return err - } - } - return nil -} - -// ensureServerFlag checks if the server flag was used. if not, asks the user for it. -func ensureServerFlag() error { - servers, err := config.GetServers() - if err != nil { - return err - } - - if newAppServer == "" { - prompt := &survey.Select{ - Message: "Select app server:", - Options: servers, - } - if err := survey.AskOne(prompt, &newAppServer); err != nil { - return err - } - } - - return nil -} - -// ensureServerFlag checks if the AppName flag was used. if not, asks the user for it. -func ensureAppNameFlag() error { - if newAppName == "" { - prompt := &survey.Input{ - Message: "Specify app name:", - Default: config.SanitiseAppName(domain), - } - if err := survey.AskOne(prompt, &newAppName); err != nil { - return err - } - } - return nil -} - -// createSecrets creates all secrets for a new app. -func createSecrets(sanitisedAppName string) (secrets, error) { - appEnvPath := path.Join(config.ABRA_DIR, "servers", newAppServer, fmt.Sprintf("%s.env", sanitisedAppName)) - appEnv, err := config.ReadEnv(appEnvPath) - if err != nil { - return nil, err - } - - secretEnvVars := secret.ReadSecretEnvVars(appEnv) - secrets, err := secret.GenerateSecrets(secretEnvVars, sanitisedAppName, newAppServer) - if err != nil { - return nil, err - } - - if internal.Pass { - for secretName := range secrets { - secretValue := secrets[secretName] - if err := secret.PassInsertSecret(secretValue, secretName, sanitisedAppName, newAppServer); err != nil { - return nil, err - } - } - } - return secrets, nil -} - -// action is the main command-line action for this package -func action(c *cli.Context) error { - recipe := internal.ValidateRecipeWithPrompt(c) - - if err := config.EnsureAbraDirExists(); err != nil { - logrus.Fatal(err) - } - - if err := ensureServerFlag(); err != nil { - logrus.Fatal(err) - } - - if err := ensureDomainFlag(recipe, newAppServer); err != nil { - logrus.Fatal(err) - } - - if err := ensureAppNameFlag(); err != nil { - logrus.Fatal(err) - } - - sanitisedAppName := config.SanitiseAppName(newAppName) - if len(sanitisedAppName) > 45 { - logrus.Fatalf("'%s' cannot be longer than 45 characters", sanitisedAppName) - } - logrus.Debugf("'%s' sanitised as '%s' for new app", newAppName, sanitisedAppName) - - if err := config.TemplateAppEnvSample(recipe.Name, newAppName, newAppServer, domain, recipe.Name); err != nil { - logrus.Fatal(err) - } - - if internal.Secrets { - secrets, err := createSecrets(sanitisedAppName) - if err != nil { - logrus.Fatal(err) - } - - secretCols := []string{"Name", "Value"} - secretTable := abraFormatter.CreateTable(secretCols) - for secret := range secrets { - secretTable.Append([]string{secret, secrets[secret]}) - } - - if len(secrets) > 0 { - defer secretTable.Render() - } - } - - if newAppServer == "default" { - newAppServer = "local" - } - - tableCol := []string{"Name", "Domain", "Type", "Server"} - table := abraFormatter.CreateTable(tableCol) - table.Append([]string{sanitisedAppName, domain, recipe.Name, newAppServer}) - - fmt.Println("") - fmt.Println(fmt.Sprintf("New '%s' created! Here is your new app overview:", recipe.Name)) - fmt.Println("") - table.Render() - fmt.Println("") - fmt.Println("You can configure this app by running the following:") - fmt.Println(fmt.Sprintf("\n abra app config %s", sanitisedAppName)) - fmt.Println("") - fmt.Println("You can deploy this app by running the following:") - fmt.Println(fmt.Sprintf("\n abra app deploy %s", sanitisedAppName)) - fmt.Println("") - - return nil -} diff --git a/cli/internal/app.go b/cli/internal/app.go new file mode 100644 index 00000000..07a09622 --- /dev/null +++ b/cli/internal/app.go @@ -0,0 +1,189 @@ +package internal + +import ( + "fmt" + "path" + + abraFormatter "coopcloud.tech/abra/cli/formatter" + "coopcloud.tech/abra/pkg/config" + "coopcloud.tech/abra/pkg/recipe" + "coopcloud.tech/abra/pkg/secret" + "github.com/AlecAivazis/survey/v2" + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" +) + +type AppSecrets map[string]string + +var Domain string +var DomainFlag = &cli.StringFlag{ + Name: "domain", + Aliases: []string{"d"}, + Value: "", + Usage: "Choose a domain name", + Destination: &Domain, +} + +var NewAppServer string +var NewAppServerFlag = &cli.StringFlag{ + Name: "server", + Aliases: []string{"s"}, + Value: "", + Usage: "Show apps of a specific server", + Destination: &NewAppServer, +} + +var NewAppName string +var NewAppNameFlag = &cli.StringFlag{ + Name: "app-name", + Aliases: []string{"a"}, + Value: "", + Usage: "Choose an app name", + Destination: &NewAppName, +} + +// RecipeName is used for configuring NewAction programmatically +var RecipeName string + +// createSecrets creates all secrets for a new app. +func createSecrets(sanitisedAppName string) (AppSecrets, error) { + appEnvPath := path.Join(config.ABRA_DIR, "servers", NewAppServer, fmt.Sprintf("%s.env", sanitisedAppName)) + appEnv, err := config.ReadEnv(appEnvPath) + if err != nil { + return nil, err + } + + secretEnvVars := secret.ReadSecretEnvVars(appEnv) + secrets, err := secret.GenerateSecrets(secretEnvVars, sanitisedAppName, NewAppServer) + if err != nil { + return nil, err + } + + if Pass { + for secretName := range secrets { + secretValue := secrets[secretName] + if err := secret.PassInsertSecret(secretValue, secretName, sanitisedAppName, NewAppServer); err != nil { + return nil, err + } + } + } + return secrets, nil +} + +// ensureDomainFlag checks if the domain flag was used. if not, asks the user for it/ +func ensureDomainFlag(recipe recipe.Recipe, server string) error { + if Domain == "" { + prompt := &survey.Input{ + Message: "Specify app domain", + Default: fmt.Sprintf("%s.%s", recipe.Name, server), + } + if err := survey.AskOne(prompt, &Domain); err != nil { + return err + } + } + return nil +} + +// ensureServerFlag checks if the server flag was used. if not, asks the user for it. +func ensureServerFlag() error { + servers, err := config.GetServers() + if err != nil { + return err + } + + if NewAppServer == "" { + prompt := &survey.Select{ + Message: "Select app server:", + Options: servers, + } + if err := survey.AskOne(prompt, &NewAppServer); err != nil { + return err + } + } + + return nil +} + +// ensureServerFlag checks if the AppName flag was used. if not, asks the user for it. +func ensureAppNameFlag() error { + if NewAppName == "" { + prompt := &survey.Input{ + Message: "Specify app name:", + Default: config.SanitiseAppName(Domain), + } + if err := survey.AskOne(prompt, &NewAppName); err != nil { + return err + } + } + return nil +} + +// NewAction is the new app creation logic +func NewAction(c *cli.Context) error { + recipe := ValidateRecipeWithPrompt(c) + + if err := config.EnsureAbraDirExists(); err != nil { + logrus.Fatal(err) + } + + if err := ensureServerFlag(); err != nil { + logrus.Fatal(err) + } + + if err := ensureDomainFlag(recipe, NewAppServer); err != nil { + logrus.Fatal(err) + } + + if err := ensureAppNameFlag(); err != nil { + logrus.Fatal(err) + } + + sanitisedAppName := config.SanitiseAppName(NewAppName) + if len(sanitisedAppName) > 45 { + logrus.Fatalf("'%s' cannot be longer than 45 characters", sanitisedAppName) + } + logrus.Debugf("'%s' sanitised as '%s' for new app", NewAppName, sanitisedAppName) + + if err := config.TemplateAppEnvSample(recipe.Name, NewAppName, NewAppServer, Domain, recipe.Name); err != nil { + logrus.Fatal(err) + } + + if Secrets { + secrets, err := createSecrets(sanitisedAppName) + if err != nil { + logrus.Fatal(err) + } + + secretCols := []string{"Name", "Value"} + secretTable := abraFormatter.CreateTable(secretCols) + for secret := range secrets { + secretTable.Append([]string{secret, secrets[secret]}) + } + + if len(secrets) > 0 { + defer secretTable.Render() + } + } + + if NewAppServer == "default" { + NewAppServer = "local" + } + + tableCol := []string{"Name", "Domain", "Type", "Server"} + table := abraFormatter.CreateTable(tableCol) + table.Append([]string{sanitisedAppName, Domain, recipe.Name, NewAppServer}) + + fmt.Println("") + fmt.Println(fmt.Sprintf("New '%s' created! Here is your new app overview:", recipe.Name)) + fmt.Println("") + table.Render() + fmt.Println("") + fmt.Println("You can configure this app by running the following:") + fmt.Println(fmt.Sprintf("\n abra app config %s", sanitisedAppName)) + fmt.Println("") + fmt.Println("You can deploy this app by running the following:") + fmt.Println(fmt.Sprintf("\n abra app deploy %s", sanitisedAppName)) + fmt.Println("") + + return nil +} diff --git a/cli/internal/validate.go b/cli/internal/validate.go index b177e9d6..fb12c7b0 100644 --- a/cli/internal/validate.go +++ b/cli/internal/validate.go @@ -54,7 +54,12 @@ func ValidateRecipeWithPrompt(c *cli.Context) recipe.Recipe { } } - if recipeName == "" { + if recipeName == "" && RecipeName != "" { + recipeName = RecipeName + logrus.Debugf("programmatically setting recipe name to %s", recipeName) + } + + if recipeName == "" && RecipeName == "" { ShowSubcommandHelpAndError(c, errors.New("no recipe provided")) } diff --git a/cli/server/add.go b/cli/server/add.go index 6e2fd29f..19851980 100644 --- a/cli/server/add.go +++ b/cli/server/add.go @@ -330,8 +330,15 @@ func createServerDir(domainName string) error { } func deployTraefik(c *cli.Context, cl *dockerClient.Client, domainName string) error { - // TODO: implement - logrus.Warn("NOT IMPLEMENTED - COMING SOON") + internal.NoInput = true + internal.RecipeName = "traefik" + internal.NewAppServer = domainName + internal.NewAppName = config.SanitiseAppName(domainName) + if err := internal.NewAction(c); err != nil { + logrus.Fatal(err) + } + + // TODO: run app deploy with all args passed through return nil }