275 lines
7.4 KiB
Go
275 lines
7.4 KiB
Go
package app
|
|
|
|
import (
|
|
"fmt"
|
|
"path"
|
|
|
|
"coopcloud.tech/abra/cli/internal"
|
|
"coopcloud.tech/abra/pkg/autocomplete"
|
|
"coopcloud.tech/abra/pkg/client"
|
|
"coopcloud.tech/abra/pkg/config"
|
|
"coopcloud.tech/abra/pkg/formatter"
|
|
"coopcloud.tech/abra/pkg/jsontable"
|
|
"coopcloud.tech/abra/pkg/recipe"
|
|
recipePkg "coopcloud.tech/abra/pkg/recipe"
|
|
"coopcloud.tech/abra/pkg/secret"
|
|
"github.com/AlecAivazis/survey/v2"
|
|
dockerClient "github.com/docker/docker/client"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/urfave/cli"
|
|
)
|
|
|
|
var appNewDescription = `
|
|
Take a recipe and uses it to create a new app. This new app configuration is
|
|
stored in your ~/.abra directory under the appropriate server.
|
|
|
|
This command does not deploy your app for you. You will need to run "abra app
|
|
deploy <domain>" to do so.
|
|
|
|
You can see what recipes are available (i.e. values for the <recipe> argument)
|
|
by running "abra recipe ls".
|
|
|
|
Passing the "--secrets/-S" flag will automatically generate secrets for your
|
|
app and store them encrypted at rest on the chosen target server. These
|
|
generated secrets are only visible at generation time, so please take care to
|
|
store them somewhere safe.
|
|
|
|
You can use the "--pass/-P" to store these generated passwords locally in a
|
|
pass store (see passwordstore.org for more). The pass command must be available
|
|
on your $PATH.
|
|
`
|
|
|
|
var appNewCommand = cli.Command{
|
|
Name: "new",
|
|
Aliases: []string{"n"},
|
|
Usage: "Create a new app",
|
|
Description: appNewDescription,
|
|
Flags: []cli.Flag{
|
|
internal.DebugFlag,
|
|
internal.NoInputFlag,
|
|
internal.NewAppServerFlag,
|
|
internal.DomainFlag,
|
|
internal.PassFlag,
|
|
internal.SecretsFlag,
|
|
internal.OfflineFlag,
|
|
internal.ChaosFlag,
|
|
},
|
|
Before: internal.SubCommandBefore,
|
|
ArgsUsage: "[<recipe>] [<version>]",
|
|
BashComplete: func(ctx *cli.Context) {
|
|
args := ctx.Args()
|
|
switch len(args) {
|
|
case 0:
|
|
autocomplete.RecipeNameComplete(ctx)
|
|
case 1:
|
|
autocomplete.RecipeVersionComplete(ctx.Args().Get(0))
|
|
}
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
recipe := internal.ValidateRecipe(c)
|
|
|
|
if !internal.Chaos {
|
|
if err := recipePkg.EnsureIsClean(recipe.Name); err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
if !internal.Offline {
|
|
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
}
|
|
if c.Args().Get(1) == "" {
|
|
if err := recipePkg.EnsureLatest(recipe.Name); err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
} else {
|
|
if err := recipePkg.EnsureVersion(recipe.Name, c.Args().Get(1)); err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := ensureServerFlag(); err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
|
|
if err := ensureDomainFlag(recipe, internal.NewAppServer); err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
|
|
sanitisedAppName := config.SanitiseAppName(internal.Domain)
|
|
logrus.Debugf("%s sanitised as %s for new app", internal.Domain, sanitisedAppName)
|
|
|
|
if err := config.TemplateAppEnvSample(
|
|
recipe.Name,
|
|
internal.Domain,
|
|
internal.NewAppServer,
|
|
internal.Domain,
|
|
); err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
|
|
var secrets AppSecrets
|
|
var secretTable *jsontable.JSONTable
|
|
if internal.Secrets {
|
|
sampleEnv, err := recipe.SampleEnv()
|
|
if err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
|
|
composeFiles, err := config.GetComposeFiles(recipe.Name, sampleEnv)
|
|
if err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
|
|
envSamplePath := path.Join(config.RECIPES_DIR, recipe.Name, ".env.sample")
|
|
secretsConfig, err := secret.ReadSecretsConfig(envSamplePath, composeFiles, config.StackName(internal.Domain))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := promptForSecrets(recipe.Name, secretsConfig); err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
|
|
cl, err := client.New(internal.NewAppServer)
|
|
if err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
|
|
secrets, err = createSecrets(cl, secretsConfig, sanitisedAppName)
|
|
if err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
|
|
secretCols := []string{"Name", "Value"}
|
|
secretTable = formatter.CreateTable(secretCols)
|
|
for name, val := range secrets {
|
|
secretTable.Append([]string{name, val})
|
|
}
|
|
}
|
|
|
|
if internal.NewAppServer == "default" {
|
|
internal.NewAppServer = "local"
|
|
}
|
|
|
|
tableCol := []string{"server", "recipe", "domain"}
|
|
table := formatter.CreateTable(tableCol)
|
|
table.Append([]string{internal.NewAppServer, recipe.Name, internal.Domain})
|
|
|
|
fmt.Println(fmt.Sprintf("A new %s app has been created! Here is an 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", internal.Domain))
|
|
fmt.Println("")
|
|
fmt.Println("You can deploy this app by running the following:")
|
|
fmt.Println(fmt.Sprintf("\n abra app deploy %s", internal.Domain))
|
|
|
|
if len(secrets) > 0 {
|
|
fmt.Println("")
|
|
fmt.Println("Here are your generated secrets:")
|
|
fmt.Println("")
|
|
secretTable.Render()
|
|
logrus.Warn("generated secrets are not shown again, please take note of them NOW")
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
// AppSecrets represents all app secrest
|
|
type AppSecrets map[string]string
|
|
|
|
// createSecrets creates all secrets for a new app.
|
|
func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.Secret, sanitisedAppName string) (AppSecrets, error) {
|
|
// NOTE(d1): trim to match app.StackName() implementation
|
|
if len(sanitisedAppName) > config.MAX_SANITISED_APP_NAME_LENGTH {
|
|
logrus.Debugf("trimming %s to %s to avoid runtime limits", sanitisedAppName, sanitisedAppName[:config.MAX_SANITISED_APP_NAME_LENGTH])
|
|
sanitisedAppName = sanitisedAppName[:config.MAX_SANITISED_APP_NAME_LENGTH]
|
|
}
|
|
|
|
secrets, err := secret.GenerateSecrets(cl, secretsConfig, internal.NewAppServer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if internal.Pass {
|
|
for secretName := range secrets {
|
|
secretValue := secrets[secretName]
|
|
if err := secret.PassInsertSecret(
|
|
secretValue,
|
|
secretName,
|
|
internal.Domain,
|
|
internal.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 internal.Domain == "" && !internal.NoInput {
|
|
prompt := &survey.Input{
|
|
Message: "Specify app domain",
|
|
Default: fmt.Sprintf("%s.%s", recipe.Name, server),
|
|
}
|
|
if err := survey.AskOne(prompt, &internal.Domain); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if internal.Domain == "" {
|
|
return fmt.Errorf("no domain provided")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// promptForSecrets asks if we should generate secrets for a new app.
|
|
func promptForSecrets(recipeName string, secretsConfig map[string]secret.Secret) error {
|
|
if len(secretsConfig) == 0 {
|
|
logrus.Debugf("%s has no secrets to generate, skipping...", recipeName)
|
|
return nil
|
|
}
|
|
|
|
if !internal.Secrets && !internal.NoInput {
|
|
prompt := &survey.Confirm{
|
|
Message: "Generate app secrets?",
|
|
}
|
|
if err := survey.AskOne(prompt, &internal.Secrets); 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 internal.NewAppServer == "" && !internal.NoInput {
|
|
prompt := &survey.Select{
|
|
Message: "Select app server:",
|
|
Options: servers,
|
|
}
|
|
if err := survey.AskOne(prompt, &internal.NewAppServer); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if internal.NewAppServer == "" {
|
|
return fmt.Errorf("no server provided")
|
|
}
|
|
|
|
return nil
|
|
}
|