312 lines
7.7 KiB
Go
312 lines
7.7 KiB
Go
package app
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"coopcloud.tech/abra/cli/internal"
|
|
appPkg "coopcloud.tech/abra/pkg/app"
|
|
"coopcloud.tech/abra/pkg/autocomplete"
|
|
"coopcloud.tech/abra/pkg/client"
|
|
"coopcloud.tech/abra/pkg/config"
|
|
"coopcloud.tech/abra/pkg/formatter"
|
|
"coopcloud.tech/abra/pkg/log"
|
|
recipePkg "coopcloud.tech/abra/pkg/recipe"
|
|
"coopcloud.tech/abra/pkg/secret"
|
|
"github.com/AlecAivazis/survey/v2"
|
|
"github.com/charmbracelet/lipgloss/table"
|
|
dockerClient "github.com/docker/docker/client"
|
|
"github.com/urfave/cli"
|
|
)
|
|
|
|
var appNewDescription = `
|
|
Creates a new app from a default recipe. This new app configuration is stored
|
|
in your $ABRA_DIR 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 := recipe.EnsureIsClean(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
if !internal.Offline {
|
|
if err := recipe.EnsureUpToDate(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
if c.Args().Get(1) == "" {
|
|
var version string
|
|
|
|
recipeVersions, err := recipe.GetRecipeVersions()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// NOTE(d1): determine whether recipe versions exist or not and check
|
|
// out the latest version or current HEAD
|
|
if len(recipeVersions) > 0 {
|
|
latest := recipeVersions[len(recipeVersions)-1]
|
|
for tag := range latest {
|
|
version = tag
|
|
}
|
|
|
|
if _, err := recipe.EnsureVersion(version); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
} else {
|
|
if err := recipe.EnsureLatest(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
} else {
|
|
if _, err := recipe.EnsureVersion(c.Args().Get(1)); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := ensureServerFlag(); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
if err := ensureDomainFlag(recipe, internal.NewAppServer); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
sanitisedAppName := appPkg.SanitiseAppName(internal.Domain)
|
|
log.Debugf("%s sanitised as %s for new app", internal.Domain, sanitisedAppName)
|
|
|
|
if err := appPkg.TemplateAppEnvSample(
|
|
recipe,
|
|
internal.Domain,
|
|
internal.NewAppServer,
|
|
internal.Domain,
|
|
); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
var secrets AppSecrets
|
|
var secretsTable *table.Table
|
|
if internal.Secrets {
|
|
sampleEnv, err := recipe.SampleEnv()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
composeFiles, err := recipe.GetComposeFiles(sampleEnv)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
secretsConfig, err := secret.ReadSecretsConfig(recipe.SampleEnvPath, composeFiles, appPkg.StackName(internal.Domain))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := promptForSecrets(recipe.Name, secretsConfig); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
cl, err := client.New(internal.NewAppServer)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
secrets, err = createSecrets(cl, secretsConfig, sanitisedAppName)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
secretsTable, err = formatter.CreateTable()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
headers := []string{"NAME", "VALUE"}
|
|
secretsTable.Headers(headers...)
|
|
|
|
for name, val := range secrets {
|
|
secretsTable.Row(name, val)
|
|
}
|
|
}
|
|
|
|
if internal.NewAppServer == "default" {
|
|
internal.NewAppServer = "local"
|
|
}
|
|
|
|
table, err := formatter.CreateTable()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
headers := []string{"SERVER", "RECIPE", "DOMAIN"}
|
|
table.Headers(headers...)
|
|
|
|
table.Row(internal.NewAppServer, recipe.Name, internal.Domain)
|
|
|
|
log.Infof("new app '%s' created 🌞", recipe.Name)
|
|
|
|
fmt.Println("")
|
|
fmt.Println(table)
|
|
fmt.Println("")
|
|
|
|
fmt.Println("Configure this app:")
|
|
fmt.Println(fmt.Sprintf("\n abra app config %s", internal.Domain))
|
|
|
|
fmt.Println("")
|
|
fmt.Println("Deploy this app:")
|
|
fmt.Println(fmt.Sprintf("\n abra app deploy %s", internal.Domain))
|
|
|
|
if len(secrets) > 0 {
|
|
fmt.Println("")
|
|
fmt.Println("Generated secrets:")
|
|
fmt.Println("")
|
|
fmt.Println(secretsTable)
|
|
|
|
log.Warnf(
|
|
"generated secrets %s shown again, please take note of them %s",
|
|
formatter.BoldStyle.Render("NOT"),
|
|
formatter.BoldStyle.Render("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 {
|
|
log.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 recipePkg.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 {
|
|
log.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
|
|
}
|