forked from toolshed/abra
		
	
		
			
				
	
	
		
			256 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			256 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package app
 | 
						|
 | 
						|
import (
 | 
						|
	"fmt"
 | 
						|
	"path"
 | 
						|
 | 
						|
	"coopcloud.tech/abra/cli/internal"
 | 
						|
	"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/jsontable"
 | 
						|
	"coopcloud.tech/abra/pkg/recipe"
 | 
						|
	recipePkg "coopcloud.tech/abra/pkg/recipe"
 | 
						|
	"coopcloud.tech/abra/pkg/runtime"
 | 
						|
	"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,
 | 
						|
	},
 | 
						|
	Before:    internal.SubCommandBefore,
 | 
						|
	ArgsUsage: "[<recipe>]",
 | 
						|
	Action: func(c *cli.Context) error {
 | 
						|
		conf := runtime.New(
 | 
						|
			runtime.WithOffline(internal.Offline),
 | 
						|
			runtime.WithEnsureRecipeUpToDate(false),
 | 
						|
		)
 | 
						|
 | 
						|
		recipe := internal.ValidateRecipeWithPrompt(c, conf)
 | 
						|
 | 
						|
		if err := recipePkg.EnsureUpToDate(recipe.Name, conf); 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)
 | 
						|
		}
 | 
						|
 | 
						|
		if err := promptForSecrets(internal.Domain); err != nil {
 | 
						|
			logrus.Fatal(err)
 | 
						|
		}
 | 
						|
 | 
						|
		cl, err := client.New(internal.NewAppServer)
 | 
						|
		if err != nil {
 | 
						|
			logrus.Fatal(err)
 | 
						|
		}
 | 
						|
 | 
						|
		var secrets AppSecrets
 | 
						|
		var secretTable *jsontable.JSONTable
 | 
						|
		if internal.Secrets {
 | 
						|
			secrets, err := createSecrets(cl, sanitisedAppName)
 | 
						|
			if err != nil {
 | 
						|
				logrus.Fatal(err)
 | 
						|
			}
 | 
						|
 | 
						|
			secretCols := []string{"Name", "Value"}
 | 
						|
			secretTable = formatter.CreateTable(secretCols)
 | 
						|
			for secret := range secrets {
 | 
						|
				secretTable.Append([]string{secret, secrets[secret]})
 | 
						|
			}
 | 
						|
 | 
						|
		}
 | 
						|
 | 
						|
		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.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))
 | 
						|
		fmt.Println("")
 | 
						|
 | 
						|
		if len(secrets) > 0 {
 | 
						|
			fmt.Println("Here are your generated secrets:")
 | 
						|
			fmt.Println("")
 | 
						|
			secretTable.Render()
 | 
						|
			fmt.Println("")
 | 
						|
			logrus.Warn("generated secrets are not shown again, please take note of them *now*")
 | 
						|
		}
 | 
						|
 | 
						|
		return nil
 | 
						|
	},
 | 
						|
	BashComplete: autocomplete.RecipeNameComplete,
 | 
						|
}
 | 
						|
 | 
						|
// AppSecrets represents all app secrest
 | 
						|
type AppSecrets map[string]string
 | 
						|
 | 
						|
// createSecrets creates all secrets for a new app.
 | 
						|
func createSecrets(cl *dockerClient.Client, sanitisedAppName string) (AppSecrets, error) {
 | 
						|
	appEnvPath := path.Join(
 | 
						|
		config.ABRA_DIR,
 | 
						|
		"servers",
 | 
						|
		internal.NewAppServer,
 | 
						|
		fmt.Sprintf("%s.env", internal.Domain),
 | 
						|
	)
 | 
						|
 | 
						|
	appEnv, err := config.ReadEnv(appEnvPath)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	secretEnvVars := secret.ReadSecretEnvVars(appEnv)
 | 
						|
	secrets, err := secret.GenerateSecrets(cl, secretEnvVars, 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(appName string) error {
 | 
						|
	app, err := app.Get(appName)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	secretEnvVars := secret.ReadSecretEnvVars(app.Env)
 | 
						|
	if len(secretEnvVars) == 0 {
 | 
						|
		logrus.Debugf("%s has no secrets to generate, skipping...", app.Recipe)
 | 
						|
		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
 | 
						|
}
 |