All checks were successful
continuous-integration/drone/push Build is passing
This implements proper modifier support in the env file using this new fork of the godotenv library. The modifier implementation is quite basic for but can be improved later if needed. See this commit for the actual implementation. Because we are now using proper modifer parsing, it does not affect the parsing of value, so this is possible again: ``` MY_VAR="#foo" ``` Closes coop-cloud/organising#535
261 lines
6.9 KiB
Go
261 lines
6.9 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>]",
|
|
BashComplete: autocomplete.RecipeNameComplete,
|
|
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 err := recipePkg.EnsureLatest(recipe.Name); 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, recipe.Name)
|
|
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.SecretValue, sanitisedAppName string) (AppSecrets, error) {
|
|
// NOTE(d1): trim to match app.StackName() implementation
|
|
if len(sanitisedAppName) > 45 {
|
|
logrus.Debugf("trimming %s to %s to avoid runtime limits", sanitisedAppName, sanitisedAppName[:45])
|
|
sanitisedAppName = sanitisedAppName[:45]
|
|
}
|
|
|
|
secrets, err := secret.GenerateSecrets(cl, secretsConfig, sanitisedAppName, 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.SecretValue) 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
|
|
}
|