diff --git a/cli/app/backup.go b/cli/app/backup.go index 9b2a4be7..9b9832e0 100644 --- a/cli/app/backup.go +++ b/cli/app/backup.go @@ -41,6 +41,7 @@ var appBackupCommand = cli.Command{ ArgsUsage: " []", Flags: []cli.Flag{ internal.DebugFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, BashComplete: autocomplete.AppNameComplete, @@ -71,8 +72,8 @@ This file is a compressed archive which contains all backup paths. To see paths, This single file can be used to restore your app. See "abra app restore" for more. `, Action: func(c *cli.Context) error { - app := internal.ValidateApp(c) - conf := runtime.New() + conf := runtime.New(runtime.WithOffline(internal.Offline)) + app := internal.ValidateApp(c, conf) cl, err := client.New(app.Server) if err != nil { diff --git a/cli/app/check.go b/cli/app/check.go index 5cf90ab0..72c07a65 100644 --- a/cli/app/check.go +++ b/cli/app/check.go @@ -8,6 +8,7 @@ import ( "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/config" + "coopcloud.tech/abra/pkg/runtime" "github.com/sirupsen/logrus" "github.com/urfave/cli" ) @@ -19,10 +20,12 @@ var appCheckCommand = cli.Command{ ArgsUsage: "", Flags: []cli.Flag{ internal.DebugFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, Action: func(c *cli.Context) error { - app := internal.ValidateApp(c) + conf := runtime.New(runtime.WithOffline(internal.Offline)) + app := internal.ValidateApp(c, conf) envSamplePath := path.Join(config.RECIPES_DIR, app.Recipe, ".env.sample") if _, err := os.Stat(envSamplePath); err != nil { diff --git a/cli/app/cmd.go b/cli/app/cmd.go index a3bdf22e..928fb3d0 100644 --- a/cli/app/cmd.go +++ b/cli/app/cmd.go @@ -12,6 +12,7 @@ import ( "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/config" + "coopcloud.tech/abra/pkg/runtime" "github.com/sirupsen/logrus" "github.com/urfave/cli" ) @@ -38,11 +39,13 @@ Example: internal.LocalCmdFlag, internal.RemoteUserFlag, internal.TtyFlag, + internal.OfflineFlag, }, BashComplete: autocomplete.AppNameComplete, Before: internal.SubCommandBefore, Action: func(c *cli.Context) error { - app := internal.ValidateApp(c) + conf := runtime.New(runtime.WithOffline(internal.Offline)) + app := internal.ValidateApp(c, conf) cl, err := client.New(app.Server) if err != nil { diff --git a/cli/app/config.go b/cli/app/config.go index ec1ff17e..6aebb786 100644 --- a/cli/app/config.go +++ b/cli/app/config.go @@ -20,6 +20,7 @@ var appConfigCommand = cli.Command{ ArgsUsage: "", Flags: []cli.Flag{ internal.DebugFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, Action: func(c *cli.Context) error { diff --git a/cli/app/cp.go b/cli/app/cp.go index d9ec9218..910fbc05 100644 --- a/cli/app/cp.go +++ b/cli/app/cp.go @@ -12,6 +12,7 @@ import ( "coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/container" "coopcloud.tech/abra/pkg/formatter" + "coopcloud.tech/abra/pkg/runtime" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" dockerClient "github.com/docker/docker/client" @@ -27,6 +28,7 @@ var appCpCommand = cli.Command{ Flags: []cli.Flag{ internal.DebugFlag, internal.NoInputFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, Usage: "Copy files to/from a running app service", @@ -42,7 +44,8 @@ And if you want to copy that file back to your current working directory locally abra app cp app:/myfile.txt . `, Action: func(c *cli.Context) error { - app := internal.ValidateApp(c) + conf := runtime.New(runtime.WithOffline(internal.Offline)) + app := internal.ValidateApp(c, conf) cl, err := client.New(app.Server) if err != nil { diff --git a/cli/app/deploy.go b/cli/app/deploy.go index e8bbde75..da50050f 100644 --- a/cli/app/deploy.go +++ b/cli/app/deploy.go @@ -10,6 +10,7 @@ import ( "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/autocomplete" + "coopcloud.tech/abra/pkg/runtime" "coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/config" @@ -18,7 +19,6 @@ import ( "coopcloud.tech/abra/pkg/git" "coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/recipe" - "coopcloud.tech/abra/pkg/runtime" "coopcloud.tech/abra/pkg/upstream/stack" "github.com/AlecAivazis/survey/v2" dockerClient "github.com/docker/docker/client" @@ -38,6 +38,7 @@ var appDeployCommand = cli.Command{ internal.ChaosFlag, internal.NoDomainChecksFlag, internal.DontWaitConvergeFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, Description: ` @@ -53,9 +54,9 @@ recipes. `, BashComplete: autocomplete.AppNameComplete, Action: func(c *cli.Context) error { - app := internal.ValidateApp(c) + conf := runtime.New(runtime.WithOffline(internal.Offline)) + app := internal.ValidateApp(c, conf) stackName := app.StackName() - conf := runtime.New() cl, err := client.New(app.Server) if err != nil { @@ -94,7 +95,7 @@ recipes. version := deployedVersion if version == "unknown" && !internal.Chaos { - catl, err := recipe.ReadRecipeCatalogue() + catl, err := recipe.ReadRecipeCatalogue(conf) if err != nil { logrus.Fatal(err) } diff --git a/cli/app/errors.go b/cli/app/errors.go index 4918e87a..4d6d21cb 100644 --- a/cli/app/errors.go +++ b/cli/app/errors.go @@ -51,12 +51,13 @@ the logs. Flags: []cli.Flag{ internal.DebugFlag, internal.WatchFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, BashComplete: autocomplete.AppNameComplete, Action: func(c *cli.Context) error { - app := internal.ValidateApp(c) - conf := runtime.New() + conf := runtime.New(runtime.WithOffline(internal.Offline)) + app := internal.ValidateApp(c, conf) cl, err := client.New(app.Server) if err != nil { diff --git a/cli/app/list.go b/cli/app/list.go index 55a82331..b3e01606 100644 --- a/cli/app/list.go +++ b/cli/app/list.go @@ -11,6 +11,7 @@ import ( "coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/recipe" + "coopcloud.tech/abra/pkg/runtime" "coopcloud.tech/tagcmp" "github.com/sirupsen/logrus" "github.com/urfave/cli" @@ -79,9 +80,12 @@ can take some time. statusFlag, listAppServerFlag, recipeFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, Action: func(c *cli.Context) error { + conf := runtime.New(runtime.WithOffline(internal.Offline)) + appFiles, err := config.LoadAppFiles(listAppServer) if err != nil { logrus.Fatal(err) @@ -109,7 +113,7 @@ can take some time. logrus.Fatal(err) } - catl, err = recipe.ReadRecipeCatalogue() + catl, err = recipe.ReadRecipeCatalogue(conf) if err != nil { logrus.Fatal(err) } diff --git a/cli/app/logs.go b/cli/app/logs.go index 3b9117ed..008be8f7 100644 --- a/cli/app/logs.go +++ b/cli/app/logs.go @@ -79,11 +79,13 @@ var appLogsCommand = cli.Command{ internal.StdErrOnlyFlag, internal.SinceLogsFlag, internal.DebugFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, BashComplete: autocomplete.AppNameComplete, Action: func(c *cli.Context) error { - app := internal.ValidateApp(c, runtime.WithEnsureRecipeExists(false)) + conf := runtime.New(runtime.WithOffline(internal.Offline)) + app := internal.ValidateApp(c, conf) cl, err := client.New(app.Server) if err != nil { diff --git a/cli/app/new.go b/cli/app/new.go index 8cb4285f..40927d1f 100644 --- a/cli/app/new.go +++ b/cli/app/new.go @@ -1,8 +1,23 @@ 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" ) @@ -38,9 +53,199 @@ var appNewCommand = cli.Command{ internal.DomainFlag, internal.PassFlag, internal.SecretsFlag, + internal.OfflineFlag, + }, + Before: internal.SubCommandBefore, + ArgsUsage: "[]", + Action: func(c *cli.Context) error { + conf := runtime.New(runtime.WithOffline(internal.Offline)) + 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 }, - Before: internal.SubCommandBefore, - ArgsUsage: "[]", - Action: internal.NewAction, 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 +} diff --git a/cli/app/ps.go b/cli/app/ps.go index b593a64f..9f0665c2 100644 --- a/cli/app/ps.go +++ b/cli/app/ps.go @@ -10,6 +10,7 @@ import ( "coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/formatter" + "coopcloud.tech/abra/pkg/runtime" "coopcloud.tech/abra/pkg/service" stack "coopcloud.tech/abra/pkg/upstream/stack" "github.com/buger/goterm" @@ -29,11 +30,13 @@ var appPsCommand = cli.Command{ Flags: []cli.Flag{ internal.WatchFlag, internal.DebugFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, BashComplete: autocomplete.AppNameComplete, Action: func(c *cli.Context) error { - app := internal.ValidateApp(c) + conf := runtime.New(runtime.WithOffline(internal.Offline)) + app := internal.ValidateApp(c, conf) cl, err := client.New(app.Server) if err != nil { diff --git a/cli/app/remove.go b/cli/app/remove.go index 186b8e43..40b5802a 100644 --- a/cli/app/remove.go +++ b/cli/app/remove.go @@ -8,6 +8,7 @@ import ( "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/client" + "coopcloud.tech/abra/pkg/runtime" stack "coopcloud.tech/abra/pkg/upstream/stack" "github.com/AlecAivazis/survey/v2" "github.com/docker/docker/api/types" @@ -43,11 +44,13 @@ flag. internal.ForceFlag, internal.DebugFlag, internal.NoInputFlag, + internal.OfflineFlag, }, BashComplete: autocomplete.AppNameComplete, Before: internal.SubCommandBefore, Action: func(c *cli.Context) error { - app := internal.ValidateApp(c) + conf := runtime.New(runtime.WithOffline(internal.Offline)) + app := internal.ValidateApp(c, conf) if !internal.Force && !internal.NoInput { response := false diff --git a/cli/app/restart.go b/cli/app/restart.go index 695424c4..0dede1fa 100644 --- a/cli/app/restart.go +++ b/cli/app/restart.go @@ -8,6 +8,7 @@ import ( "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/client" + "coopcloud.tech/abra/pkg/runtime" upstream "coopcloud.tech/abra/pkg/upstream/service" stack "coopcloud.tech/abra/pkg/upstream/stack" "github.com/sirupsen/logrus" @@ -21,12 +22,14 @@ var appRestartCommand = cli.Command{ ArgsUsage: "", Flags: []cli.Flag{ internal.DebugFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, Description: `This command restarts a service within a deployed app.`, BashComplete: autocomplete.AppNameComplete, Action: func(c *cli.Context) error { - app := internal.ValidateApp(c) + conf := runtime.New(runtime.WithOffline(internal.Offline)) + app := internal.ValidateApp(c, conf) serviceNameShort := c.Args().Get(1) if serviceNameShort == "" { diff --git a/cli/app/restore.go b/cli/app/restore.go index d5c2f538..6303ad28 100644 --- a/cli/app/restore.go +++ b/cli/app/restore.go @@ -35,6 +35,7 @@ var appRestoreCommand = cli.Command{ ArgsUsage: " ", Flags: []cli.Flag{ internal.DebugFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, BashComplete: autocomplete.AppNameComplete, @@ -55,8 +56,8 @@ Example: abra app restore example.com app ~/.abra/backups/example_com_app_609341138.tar.gz `, Action: func(c *cli.Context) error { - app := internal.ValidateApp(c) - conf := runtime.New() + conf := runtime.New(runtime.WithOffline(internal.Offline)) + app := internal.ValidateApp(c, conf) cl, err := client.New(app.Server) if err != nil { diff --git a/cli/app/rollback.go b/cli/app/rollback.go index 4d537310..c03f4701 100644 --- a/cli/app/rollback.go +++ b/cli/app/rollback.go @@ -31,6 +31,7 @@ var appRollbackCommand = cli.Command{ internal.ChaosFlag, internal.NoDomainChecksFlag, internal.DontWaitConvergeFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, Description: ` @@ -48,9 +49,9 @@ recipes. `, BashComplete: autocomplete.AppNameComplete, Action: func(c *cli.Context) error { - app := internal.ValidateApp(c) + conf := runtime.New(runtime.WithOffline(internal.Offline)) + app := internal.ValidateApp(c, conf) stackName := app.StackName() - conf := runtime.New() if !internal.Chaos { if err := recipe.EnsureUpToDate(app.Recipe, conf); err != nil { @@ -83,7 +84,7 @@ recipes. logrus.Fatalf("%s is not deployed?", app.Name) } - catl, err := recipe.ReadRecipeCatalogue() + catl, err := recipe.ReadRecipeCatalogue(conf) if err != nil { logrus.Fatal(err) } diff --git a/cli/app/run.go b/cli/app/run.go index 4ae68c1b..901edc1e 100644 --- a/cli/app/run.go +++ b/cli/app/run.go @@ -9,6 +9,7 @@ import ( "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/client" containerPkg "coopcloud.tech/abra/pkg/container" + "coopcloud.tech/abra/pkg/runtime" "coopcloud.tech/abra/pkg/upstream/container" "github.com/docker/cli/cli/command" "github.com/docker/docker/api/types" @@ -37,13 +38,15 @@ var appRunCommand = cli.Command{ internal.DebugFlag, noTTYFlag, userFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, ArgsUsage: " ...", Usage: "Run a command in a service container", BashComplete: autocomplete.AppNameComplete, Action: func(c *cli.Context) error { - app := internal.ValidateApp(c) + conf := runtime.New(runtime.WithOffline(internal.Offline)) + app := internal.ValidateApp(c, conf) if len(c.Args()) < 2 { internal.ShowSubcommandHelpAndError(c, errors.New("no provided?")) diff --git a/cli/app/secret.go b/cli/app/secret.go index 9e9fe2b8..eefbdc90 100644 --- a/cli/app/secret.go +++ b/cli/app/secret.go @@ -12,6 +12,7 @@ import ( "coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/formatter" + "coopcloud.tech/abra/pkg/runtime" "coopcloud.tech/abra/pkg/secret" "github.com/docker/docker/api/types" dockerClient "github.com/docker/docker/client" @@ -42,11 +43,13 @@ var appSecretGenerateCommand = cli.Command{ internal.DebugFlag, allSecretsFlag, internal.PassFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, BashComplete: autocomplete.AppNameComplete, Action: func(c *cli.Context) error { - app := internal.ValidateApp(c) + conf := runtime.New(runtime.WithOffline(internal.Offline)) + app := internal.ValidateApp(c, conf) cl, err := client.New(app.Server) if err != nil { @@ -121,6 +124,7 @@ var appSecretInsertCommand = cli.Command{ Flags: []cli.Flag{ internal.DebugFlag, internal.PassFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, ArgsUsage: " ", @@ -138,7 +142,8 @@ Example: `, Action: func(c *cli.Context) error { - app := internal.ValidateApp(c) + conf := runtime.New(runtime.WithOffline(internal.Offline)) + app := internal.ValidateApp(c, conf) cl, err := client.New(app.Server) if err != nil { @@ -198,6 +203,7 @@ var appSecretRmCommand = cli.Command{ internal.NoInputFlag, rmAllSecretsFlag, internal.PassRemoveFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, ArgsUsage: " []", @@ -210,7 +216,8 @@ Example: abra app secret remove myapp db_pass `, Action: func(c *cli.Context) error { - app := internal.ValidateApp(c) + conf := runtime.New(runtime.WithOffline(internal.Offline)) + app := internal.ValidateApp(c, conf) secrets := secret.ReadSecretEnvVars(app.Env) if c.Args().Get(1) != "" && rmAllSecrets { @@ -288,11 +295,13 @@ var appSecretLsCommand = cli.Command{ Aliases: []string{"ls"}, Flags: []cli.Flag{ internal.DebugFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, Usage: "List all secrets", Action: func(c *cli.Context) error { - app := internal.ValidateApp(c) + conf := runtime.New(runtime.WithOffline(internal.Offline)) + app := internal.ValidateApp(c, conf) secrets := secret.ReadSecretEnvVars(app.Env) tableCol := []string{"Name", "Version", "Generated Name", "Created On Server"} diff --git a/cli/app/services.go b/cli/app/services.go index 60a7245e..765c939f 100644 --- a/cli/app/services.go +++ b/cli/app/services.go @@ -9,6 +9,7 @@ import ( "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/formatter" + "coopcloud.tech/abra/pkg/runtime" "coopcloud.tech/abra/pkg/service" stack "coopcloud.tech/abra/pkg/upstream/stack" "github.com/docker/docker/api/types" @@ -23,11 +24,13 @@ var appServicesCommand = cli.Command{ ArgsUsage: "", Flags: []cli.Flag{ internal.DebugFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, BashComplete: autocomplete.AppNameComplete, Action: func(c *cli.Context) error { - app := internal.ValidateApp(c) + conf := runtime.New(runtime.WithOffline(internal.Offline)) + app := internal.ValidateApp(c, conf) cl, err := client.New(app.Server) if err != nil { diff --git a/cli/app/undeploy.go b/cli/app/undeploy.go index 0bcc1afd..6800868f 100644 --- a/cli/app/undeploy.go +++ b/cli/app/undeploy.go @@ -10,6 +10,7 @@ import ( "coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/formatter" + "coopcloud.tech/abra/pkg/runtime" stack "coopcloud.tech/abra/pkg/upstream/stack" "github.com/docker/docker/api/types/filters" dockerClient "github.com/docker/docker/client" @@ -86,6 +87,7 @@ var appUndeployCommand = cli.Command{ internal.DebugFlag, internal.NoInputFlag, pruneFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, Usage: "Undeploy an app", @@ -99,8 +101,10 @@ any previously attached volumes as eligible for pruning once undeployed. Passing "-p/--prune" does not remove those volumes. `, Action: func(c *cli.Context) error { - app := internal.ValidateApp(c) + conf := runtime.New(runtime.WithOffline(internal.Offline)) + app := internal.ValidateApp(c, conf) stackName := app.StackName() + cl, err := client.New(app.Server) if err != nil { logrus.Fatal(err) diff --git a/cli/app/upgrade.go b/cli/app/upgrade.go index 9e0ef6be..225e2d7f 100644 --- a/cli/app/upgrade.go +++ b/cli/app/upgrade.go @@ -30,6 +30,7 @@ var appUpgradeCommand = cli.Command{ internal.ChaosFlag, internal.NoDomainChecksFlag, internal.DontWaitConvergeFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, Description: ` @@ -51,9 +52,9 @@ including unstaged changes and can be useful for live hacking and testing new recipes. `, Action: func(c *cli.Context) error { - app := internal.ValidateApp(c) + conf := runtime.New(runtime.WithOffline(internal.Offline)) + app := internal.ValidateApp(c, conf) stackName := app.StackName() - conf := runtime.New() cl, err := client.New(app.Server) if err != nil { @@ -86,7 +87,7 @@ recipes. logrus.Fatalf("%s is not deployed?", app.Name) } - catl, err := recipe.ReadRecipeCatalogue() + catl, err := recipe.ReadRecipeCatalogue(conf) if err != nil { logrus.Fatal(err) } diff --git a/cli/app/version.go b/cli/app/version.go index 92b3dbc3..0777d4aa 100644 --- a/cli/app/version.go +++ b/cli/app/version.go @@ -38,6 +38,7 @@ var appVersionCommand = cli.Command{ Flags: []cli.Flag{ internal.DebugFlag, internal.NoInputFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, Usage: "Show app versions", @@ -47,9 +48,9 @@ the individual image names, tags and digests. But also the Co-op Cloud recipe version. `, Action: func(c *cli.Context) error { - app := internal.ValidateApp(c) + conf := runtime.New(runtime.WithOffline(internal.Offline)) + app := internal.ValidateApp(c, conf) stackName := app.StackName() - conf := runtime.New() cl, err := client.New(app.Server) if err != nil { diff --git a/cli/app/volume.go b/cli/app/volume.go index 573cd90b..2008c1e9 100644 --- a/cli/app/volume.go +++ b/cli/app/volume.go @@ -7,6 +7,7 @@ import ( "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/formatter" + "coopcloud.tech/abra/pkg/runtime" "github.com/AlecAivazis/survey/v2" "github.com/sirupsen/logrus" "github.com/urfave/cli" @@ -19,12 +20,14 @@ var appVolumeListCommand = cli.Command{ Flags: []cli.Flag{ internal.DebugFlag, internal.NoInputFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, Usage: "List volumes associated with an app", BashComplete: autocomplete.AppNameComplete, Action: func(c *cli.Context) error { - app := internal.ValidateApp(c) + conf := runtime.New(runtime.WithOffline(internal.Offline)) + app := internal.ValidateApp(c, conf) cl, err := client.New(app.Server) if err != nil { @@ -80,10 +83,12 @@ Passing "--force/-f" will select all volumes for removal. Be careful. internal.DebugFlag, internal.NoInputFlag, internal.ForceFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, Action: func(c *cli.Context) error { - app := internal.ValidateApp(c) + conf := runtime.New(runtime.WithOffline(internal.Offline)) + app := internal.ValidateApp(c, conf) cl, err := client.New(app.Server) if err != nil { diff --git a/cli/catalogue/catalogue.go b/cli/catalogue/catalogue.go index 5e255d48..a2260acf 100644 --- a/cli/catalogue/catalogue.go +++ b/cli/catalogue/catalogue.go @@ -29,6 +29,7 @@ var catalogueGenerateCommand = cli.Command{ internal.PublishFlag, internal.DryFlag, internal.SkipUpdatesFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, Description: ` @@ -54,18 +55,18 @@ keys configured on your account. `, ArgsUsage: "[]", Action: func(c *cli.Context) error { + conf := runtime.New(runtime.WithOffline(internal.Offline)) recipeName := c.Args().First() - conf := runtime.New() if recipeName != "" { - internal.ValidateRecipe(c) + internal.ValidateRecipe(c, conf) } - if err := catalogue.EnsureUpToDate(); err != nil { + if err := catalogue.EnsureUpToDate(conf); err != nil { logrus.Fatal(err) } - repos, err := recipe.ReadReposMetadata() + repos, err := recipe.ReadReposMetadata(conf) if err != nil { logrus.Fatal(err) } @@ -136,7 +137,7 @@ keys configured on your account. logrus.Fatal(err) } } else { - catlFS, err := recipe.ReadRecipeCatalogue() + catlFS, err := recipe.ReadRecipeCatalogue(conf) if err != nil { logrus.Fatal(err) } diff --git a/cli/internal/cli.go b/cli/internal/cli.go index d31228ce..6c715c43 100644 --- a/cli/internal/cli.go +++ b/cli/internal/cli.go @@ -253,6 +253,16 @@ var DebugFlag = &cli.BoolFlag{ Usage: "Show DEBUG messages", } +// Offline stores the variable from OfflineFlag. +var Offline bool + +// DebugFlag turns on/off offline mode. +var OfflineFlag = &cli.BoolFlag{ + Name: "offline, o", + Destination: &Offline, + Usage: "Prefer offline & filesystem access when possible", +} + // MachineReadable stores the variable from MachineReadableFlag var MachineReadable bool @@ -270,7 +280,7 @@ var RC bool var RCFlag = &cli.BoolFlag{ Name: "rc, r", Destination: &RC, - Usage: "Insatll the latest release candidate", + Usage: "Install the latest release candidate", } var Major bool diff --git a/cli/internal/new.go b/cli/internal/new.go deleted file mode 100644 index 86547d4c..00000000 --- a/cli/internal/new.go +++ /dev/null @@ -1,199 +0,0 @@ -package internal - -import ( - "fmt" - "path" - - "coopcloud.tech/abra/pkg/app" - "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" -) - -// AppSecrets represents all app secrest -type AppSecrets map[string]string - -// RecipeName is used for configuring recipe name programmatically -var RecipeName 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", NewAppServer, fmt.Sprintf("%s.env", Domain)) - appEnv, err := config.ReadEnv(appEnvPath) - if err != nil { - return nil, err - } - - secretEnvVars := secret.ReadSecretEnvVars(appEnv) - secrets, err := secret.GenerateSecrets(cl, secretEnvVars, sanitisedAppName, NewAppServer) - if err != nil { - return nil, err - } - - if Pass { - for secretName := range secrets { - secretValue := secrets[secretName] - if err := secret.PassInsertSecret(secretValue, secretName, Domain, 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 == "" && !NoInput { - 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 - } - } - - if 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 !Secrets && !NoInput { - prompt := &survey.Confirm{ - Message: "Generate app secrets?", - } - if err := survey.AskOne(prompt, &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 NewAppServer == "" && !NoInput { - prompt := &survey.Select{ - Message: "Select app server:", - Options: servers, - } - if err := survey.AskOne(prompt, &NewAppServer); err != nil { - return err - } - } - - if NewAppServer == "" { - return fmt.Errorf("no server provided") - } - - return nil -} - -// NewAction is the new app creation logic -func NewAction(c *cli.Context) error { - recipe := ValidateRecipeWithPrompt(c) - conf := runtime.New() - - if err := recipePkg.EnsureUpToDate(recipe.Name, conf); err != nil { - logrus.Fatal(err) - } - - if err := ensureServerFlag(); err != nil { - logrus.Fatal(err) - } - - if err := ensureDomainFlag(recipe, NewAppServer); err != nil { - logrus.Fatal(err) - } - - sanitisedAppName := config.SanitiseAppName(Domain) - logrus.Debugf("%s sanitised as %s for new app", Domain, sanitisedAppName) - - if err := config.TemplateAppEnvSample(recipe.Name, Domain, NewAppServer, Domain); err != nil { - logrus.Fatal(err) - } - - if err := promptForSecrets(Domain); err != nil { - logrus.Fatal(err) - } - - cl, err := client.New(NewAppServer) - if err != nil { - logrus.Fatal(err) - } - - var secrets AppSecrets - var secretTable *jsontable.JSONTable - if 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 NewAppServer == "default" { - NewAppServer = "local" - } - - tableCol := []string{"server", "recipe", "domain"} - table := formatter.CreateTable(tableCol) - table.Append([]string{NewAppServer, recipe.Name, 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", Domain)) - fmt.Println("") - fmt.Println("You can deploy this app by running the following:") - fmt.Println(fmt.Sprintf("\n abra app deploy %s", 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 -} diff --git a/cli/internal/validate.go b/cli/internal/validate.go index 7a66444d..e04948a1 100644 --- a/cli/internal/validate.go +++ b/cli/internal/validate.go @@ -15,13 +15,9 @@ import ( "github.com/urfave/cli" ) -// AppName is used for configuring app name programmatically -var AppName string - // ValidateRecipe ensures the recipe arg is valid. -func ValidateRecipe(c *cli.Context, opts ...runtime.Option) recipe.Recipe { +func ValidateRecipe(c *cli.Context, conf *runtime.Config) recipe.Recipe { recipeName := c.Args().First() - conf := runtime.New(opts...) if recipeName == "" { ShowSubcommandHelpAndError(c, errors.New("no recipe name provided")) @@ -53,14 +49,13 @@ func ValidateRecipe(c *cli.Context, opts ...runtime.Option) recipe.Recipe { // ValidateRecipeWithPrompt ensures a recipe argument is present before // validating, asking for input if required. -func ValidateRecipeWithPrompt(c *cli.Context, opts ...runtime.Option) recipe.Recipe { +func ValidateRecipeWithPrompt(c *cli.Context, conf *runtime.Config) recipe.Recipe { recipeName := c.Args().First() - conf := runtime.New(opts...) if recipeName == "" && !NoInput { var recipes []string - catl, err := recipe.ReadRecipeCatalogue() + catl, err := recipe.ReadRecipeCatalogue(conf) if err != nil { logrus.Fatal(err) } @@ -94,11 +89,6 @@ func ValidateRecipeWithPrompt(c *cli.Context, opts ...runtime.Option) recipe.Rec } } - if RecipeName != "" { - recipeName = RecipeName - logrus.Debugf("programmatically setting recipe name to %s", recipeName) - } - if recipeName == "" { ShowSubcommandHelpAndError(c, errors.New("no recipe name provided")) } @@ -118,14 +108,8 @@ func ValidateRecipeWithPrompt(c *cli.Context, opts ...runtime.Option) recipe.Rec } // ValidateApp ensures the app name arg is valid. -func ValidateApp(c *cli.Context, opts ...runtime.Option) config.App { +func ValidateApp(c *cli.Context, conf *runtime.Config) config.App { appName := c.Args().First() - conf := runtime.New(opts...) - - if AppName != "" { - appName = AppName - logrus.Debugf("programmatically setting app name to %s", appName) - } if appName == "" { ShowSubcommandHelpAndError(c, errors.New("no app provided")) diff --git a/cli/recipe/fetch.go b/cli/recipe/fetch.go index 3968f418..17d6b59c 100644 --- a/cli/recipe/fetch.go +++ b/cli/recipe/fetch.go @@ -17,19 +17,20 @@ var recipeFetchCommand = cli.Command{ Description: "Fetchs all recipes without arguments.", Flags: []cli.Flag{ internal.DebugFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, BashComplete: autocomplete.RecipeNameComplete, Action: func(c *cli.Context) error { + conf := runtime.New(runtime.WithOffline(internal.Offline)) recipeName := c.Args().First() - conf := runtime.New() if recipeName != "" { - internal.ValidateRecipe(c) + internal.ValidateRecipe(c, conf) return nil // ValidateRecipe ensures latest checkout } - repos, err := recipe.ReadReposMetadata() + repos, err := recipe.ReadReposMetadata(conf) if err != nil { logrus.Fatal(err) } diff --git a/cli/recipe/lint.go b/cli/recipe/lint.go index e5e440d3..623872e6 100644 --- a/cli/recipe/lint.go +++ b/cli/recipe/lint.go @@ -21,12 +21,13 @@ var recipeLintCommand = cli.Command{ Flags: []cli.Flag{ internal.DebugFlag, internal.OnlyErrorFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, BashComplete: autocomplete.RecipeNameComplete, Action: func(c *cli.Context) error { - recipe := internal.ValidateRecipe(c) - conf := runtime.New() + conf := runtime.New(runtime.WithOffline(internal.Offline)) + recipe := internal.ValidateRecipe(c, conf) if err := recipePkg.EnsureUpToDate(recipe.Name, conf); err != nil { logrus.Fatal(err) diff --git a/cli/recipe/list.go b/cli/recipe/list.go index 8e7476e6..846a5d11 100644 --- a/cli/recipe/list.go +++ b/cli/recipe/list.go @@ -9,6 +9,7 @@ import ( "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/recipe" + "coopcloud.tech/abra/pkg/runtime" "github.com/sirupsen/logrus" "github.com/urfave/cli" ) @@ -29,10 +30,13 @@ var recipeListCommand = cli.Command{ internal.DebugFlag, internal.MachineReadableFlag, patternFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, Action: func(c *cli.Context) error { - catl, err := recipe.ReadRecipeCatalogue() + conf := runtime.New(runtime.WithOffline(internal.Offline)) + + catl, err := recipe.ReadRecipeCatalogue(conf) if err != nil { logrus.Fatal(err.Error()) } diff --git a/cli/recipe/new.go b/cli/recipe/new.go index 1f651985..87e84d87 100644 --- a/cli/recipe/new.go +++ b/cli/recipe/new.go @@ -36,6 +36,7 @@ var recipeNewCommand = cli.Command{ Flags: []cli.Flag{ internal.DebugFlag, internal.NoInputFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, Usage: "Create a new recipe", diff --git a/cli/recipe/release.go b/cli/recipe/release.go index 78a6b291..01c109f1 100644 --- a/cli/recipe/release.go +++ b/cli/recipe/release.go @@ -13,6 +13,7 @@ import ( gitPkg "coopcloud.tech/abra/pkg/git" "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe" + "coopcloud.tech/abra/pkg/runtime" "coopcloud.tech/tagcmp" "github.com/AlecAivazis/survey/v2" "github.com/docker/distribution/reference" @@ -54,11 +55,13 @@ your SSH keys configured on your account. internal.MinorFlag, internal.PatchFlag, internal.PublishFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, BashComplete: autocomplete.RecipeNameComplete, Action: func(c *cli.Context) error { - recipe := internal.ValidateRecipeWithPrompt(c) + conf := runtime.New(runtime.WithOffline(internal.Offline)) + recipe := internal.ValidateRecipeWithPrompt(c, conf) imagesTmp, err := getImageVersions(recipe) if err != nil { diff --git a/cli/recipe/sync.go b/cli/recipe/sync.go index f95ba90b..b6576237 100644 --- a/cli/recipe/sync.go +++ b/cli/recipe/sync.go @@ -8,6 +8,7 @@ import ( "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/config" + "coopcloud.tech/abra/pkg/runtime" "coopcloud.tech/tagcmp" "github.com/AlecAivazis/survey/v2" "github.com/go-git/go-git/v5" @@ -28,6 +29,7 @@ var recipeSyncCommand = cli.Command{ internal.MajorFlag, internal.MinorFlag, internal.PatchFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, Description: ` @@ -41,7 +43,8 @@ auto-generate it for you. The configuration will be updated on the local file system. `, Action: func(c *cli.Context) error { - recipe := internal.ValidateRecipeWithPrompt(c) + conf := runtime.New(runtime.WithOffline(internal.Offline)) + recipe := internal.ValidateRecipeWithPrompt(c, conf) mainApp, err := internal.GetMainAppImage(recipe) if err != nil { diff --git a/cli/recipe/upgrade.go b/cli/recipe/upgrade.go index a243858e..b152568f 100644 --- a/cli/recipe/upgrade.go +++ b/cli/recipe/upgrade.go @@ -68,11 +68,12 @@ You may invoke this command in "wizard" mode and be prompted for input: internal.MajorFlag, internal.MachineReadableFlag, internal.AllTagsFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, Action: func(c *cli.Context) error { - recipe := internal.ValidateRecipeWithPrompt(c) - conf := runtime.New() + conf := runtime.New(runtime.WithOffline(internal.Offline)) + recipe := internal.ValidateRecipeWithPrompt(c, conf) if err := recipePkg.EnsureUpToDate(recipe.Name, conf); err != nil { logrus.Fatal(err) @@ -184,7 +185,7 @@ You may invoke this command in "wizard" mode and be prompted for input: continue // skip on to the next tag and don't update any compose files } - catlVersions, err := recipePkg.VersionsOfService(recipe.Name, service.Name) + catlVersions, err := recipePkg.VersionsOfService(recipe.Name, service.Name, conf) if err != nil { logrus.Fatal(err) } diff --git a/cli/recipe/version.go b/cli/recipe/version.go index 57c4ce9d..b42afb94 100644 --- a/cli/recipe/version.go +++ b/cli/recipe/version.go @@ -5,6 +5,7 @@ import ( "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/formatter" recipePkg "coopcloud.tech/abra/pkg/recipe" + "coopcloud.tech/abra/pkg/runtime" "github.com/sirupsen/logrus" "github.com/urfave/cli" ) @@ -16,13 +17,15 @@ var recipeVersionCommand = cli.Command{ ArgsUsage: "", Flags: []cli.Flag{ internal.DebugFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, BashComplete: autocomplete.RecipeNameComplete, Action: func(c *cli.Context) error { - recipe := internal.ValidateRecipe(c) + conf := runtime.New(runtime.WithOffline(internal.Offline)) + recipe := internal.ValidateRecipe(c, conf) - catalogue, err := recipePkg.ReadRecipeCatalogue() + catalogue, err := recipePkg.ReadRecipeCatalogue(conf) if err != nil { logrus.Fatal(err) } diff --git a/cli/record/list.go b/cli/record/list.go index cdd1b99d..054caccb 100644 --- a/cli/record/list.go +++ b/cli/record/list.go @@ -22,6 +22,7 @@ var RecordListCommand = cli.Command{ Flags: []cli.Flag{ internal.DebugFlag, internal.DNSProviderFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, Description: ` diff --git a/cli/record/new.go b/cli/record/new.go index 15758375..cebf310b 100644 --- a/cli/record/new.go +++ b/cli/record/new.go @@ -30,6 +30,7 @@ var RecordNewCommand = cli.Command{ internal.DNSValueFlag, internal.DNSTTLFlag, internal.DNSPriorityFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, Description: ` diff --git a/cli/record/remove.go b/cli/record/remove.go index 04873657..4208e03a 100644 --- a/cli/record/remove.go +++ b/cli/record/remove.go @@ -27,6 +27,7 @@ var RecordRemoveCommand = cli.Command{ internal.DNSProviderFlag, internal.DNSTypeFlag, internal.DNSNameFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, Description: ` diff --git a/cli/server/add.go b/cli/server/add.go index c4bf6f13..098bcdae 100644 --- a/cli/server/add.go +++ b/cli/server/add.go @@ -118,6 +118,7 @@ developer machine. internal.DebugFlag, internal.NoInputFlag, localFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, ArgsUsage: "", diff --git a/cli/server/list.go b/cli/server/list.go index 838cc9d3..3da5a816 100644 --- a/cli/server/list.go +++ b/cli/server/list.go @@ -28,6 +28,7 @@ var serverListCommand = cli.Command{ problemsFilterFlag, internal.DebugFlag, internal.MachineReadableFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, Action: func(c *cli.Context) error { diff --git a/cli/server/new.go b/cli/server/new.go index 04a79473..98e28151 100644 --- a/cli/server/new.go +++ b/cli/server/new.go @@ -222,6 +222,7 @@ API tokens are read from the environment if specified, e.g. internal.DebugFlag, internal.NoInputFlag, internal.ServerProviderFlag, + internal.OfflineFlag, // Capsul internal.CapsulInstanceURLFlag, diff --git a/cli/server/prune.go b/cli/server/prune.go index bbedbb3d..a64734d8 100644 --- a/cli/server/prune.go +++ b/cli/server/prune.go @@ -43,6 +43,7 @@ also be removed. This can result in unwanted data loss if not used carefully. allFilterFlag, volumesFilterFlag, internal.DebugFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, BashComplete: autocomplete.ServerNameComplete, diff --git a/cli/server/remove.go b/cli/server/remove.go index c7d15a89..b6f303f1 100644 --- a/cli/server/remove.go +++ b/cli/server/remove.go @@ -120,6 +120,7 @@ like tears in rain. internal.NoInputFlag, rmServerFlag, internal.ServerProviderFlag, + internal.OfflineFlag, // Hetzner internal.HetznerCloudNameFlag, diff --git a/cli/updater/updater.go b/cli/updater/updater.go index 789e0f81..3ed7d787 100644 --- a/cli/updater/updater.go +++ b/cli/updater/updater.go @@ -49,6 +49,7 @@ var Notify = cli.Command{ Flags: []cli.Flag{ internal.DebugFlag, majorFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, Description: ` @@ -57,6 +58,8 @@ catalogue. If a new patch/minor version is available, a notification is printed. To include major versions use the --major flag. `, Action: func(c *cli.Context) error { + conf := runtime.New(runtime.WithOffline(internal.Offline)) + cl, err := client.New("default") if err != nil { logrus.Fatal(err) @@ -75,7 +78,7 @@ printed. To include major versions use the --major flag. } if recipeName != "" { - _, err = getLatestUpgrade(cl, stackName, recipeName) + _, err = getLatestUpgrade(cl, stackName, recipeName, conf) if err != nil { logrus.Fatal(err) } @@ -97,6 +100,7 @@ var UpgradeApp = cli.Command{ internal.ChaosFlag, majorFlag, allFlag, + internal.OfflineFlag, }, Before: internal.SubCommandBefore, Description: ` @@ -109,13 +113,13 @@ break things. Only apps that are not deployed with "--chaos" are upgraded, to update chaos deployments use the "--chaos" flag. Use it with care. `, Action: func(c *cli.Context) error { + conf := runtime.New(runtime.WithOffline(internal.Offline)) + cl, err := client.New("default") if err != nil { logrus.Fatal(err) } - conf := runtime.New() - if !updateAll { stackName := c.Args().Get(0) recipeName := c.Args().Get(1) @@ -223,13 +227,14 @@ func getEnv(cl *dockerclient.Client, stackName string) (config.AppEnv, error) { // getLatestUpgrade returns the latest available version for an app respecting // the "--major" flag if it is newer than the currently deployed version. -func getLatestUpgrade(cl *dockerclient.Client, stackName string, recipeName string) (string, error) { +func getLatestUpgrade(cl *dockerclient.Client, stackName string, + recipeName string, conf *runtime.Config) (string, error) { deployedVersion, err := getDeployedVersion(cl, stackName, recipeName) if err != nil { return "", err } - availableUpgrades, err := getAvailableUpgrades(cl, stackName, recipeName, deployedVersion) + availableUpgrades, err := getAvailableUpgrades(cl, stackName, recipeName, deployedVersion, conf) if err != nil { return "", err } @@ -272,8 +277,8 @@ func getDeployedVersion(cl *dockerclient.Client, stackName string, recipeName st // than the deployed version. It only includes major upgrades if the "--major" // flag is set. func getAvailableUpgrades(cl *dockerclient.Client, stackName string, recipeName string, - deployedVersion string) ([]string, error) { - catl, err := recipe.ReadRecipeCatalogue() + deployedVersion string, conf *runtime.Config) ([]string, error) { + catl, err := recipe.ReadRecipeCatalogue(conf) if err != nil { return nil, err } @@ -389,7 +394,7 @@ func createDeployConfig(recipeName string, stackName string, env config.AppEnv) // tryUpgrade performs the upgrade if all the requirements are fulfilled. func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string, conf *runtime.Config) error { if recipeName == "" { - logrus.Debugf("Don't update %s due to missing recipe name", stackName) + logrus.Debugf("don't update %s due to missing recipe name", stackName) return nil } @@ -399,7 +404,7 @@ func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string, conf *run } if chaos && !internal.Chaos { - logrus.Debugf("Don't update %s due to chaos deployment.", stackName) + logrus.Debugf("don't update %s due to chaos deployment", stackName) return nil } @@ -409,17 +414,17 @@ func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string, conf *run } if !updatesEnabled { - logrus.Debugf("Don't update %s due to disabled auto updates or missing ENABLE_AUTO_UPDATE env.", stackName) + logrus.Debugf("don't update %s due to disabled auto updates or missing ENABLE_AUTO_UPDATE env", stackName) return nil } - upgradeVersion, err := getLatestUpgrade(cl, stackName, recipeName) + upgradeVersion, err := getLatestUpgrade(cl, stackName, recipeName, conf) if err != nil { return err } if upgradeVersion == "" { - logrus.Debugf("Don't update %s due to no new version.", stackName) + logrus.Debugf("don't update %s due to no new version", stackName) return nil } @@ -429,7 +434,8 @@ func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string, conf *run } // upgrade performs all necessary steps to upgrade an app. -func upgrade(cl *dockerclient.Client, stackName, recipeName, upgradeVersion string, conf *runtime.Config) error { +func upgrade(cl *dockerclient.Client, stackName, recipeName, + upgradeVersion string, conf *runtime.Config) error { env, err := getEnv(cl, stackName) if err != nil { return err @@ -455,7 +461,7 @@ func upgrade(cl *dockerclient.Client, stackName, recipeName, upgradeVersion stri return err } - logrus.Infof("Upgrade %s (%s) to version %s", stackName, recipeName, upgradeVersion) + logrus.Infof("upgrade %s (%s) to version %s", stackName, recipeName, upgradeVersion) err = stack.RunDeploy(cl, deployOpts, compose, stackName, true) diff --git a/pkg/autocomplete/autocomplete.go b/pkg/autocomplete/autocomplete.go index 40cf0ae2..e1eb560b 100644 --- a/pkg/autocomplete/autocomplete.go +++ b/pkg/autocomplete/autocomplete.go @@ -5,6 +5,7 @@ import ( "coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/recipe" + "coopcloud.tech/abra/pkg/runtime" "github.com/sirupsen/logrus" "github.com/urfave/cli" ) @@ -27,7 +28,12 @@ func AppNameComplete(c *cli.Context) { // RecipeNameComplete completes recipe names. func RecipeNameComplete(c *cli.Context) { - catl, err := recipe.ReadRecipeCatalogue() + // defaults since we can't take arguments here... this means auto-completion + // of recipe names always access the network if e.g. the catalogue needs + // cloning / updating + conf := runtime.New() + + catl, err := recipe.ReadRecipeCatalogue(conf) if err != nil { logrus.Warn(err) } diff --git a/pkg/catalogue/catalogue.go b/pkg/catalogue/catalogue.go index 3cf6b2c0..88cf13cb 100644 --- a/pkg/catalogue/catalogue.go +++ b/pkg/catalogue/catalogue.go @@ -8,6 +8,7 @@ import ( "coopcloud.tech/abra/pkg/config" gitPkg "coopcloud.tech/abra/pkg/git" + "coopcloud.tech/abra/pkg/runtime" "github.com/go-git/go-git/v5" "github.com/sirupsen/logrus" ) @@ -52,9 +53,13 @@ var CatalogueSkipList = map[string]bool{ } // EnsureCatalogue ensures that the catalogue is cloned locally & present. -func EnsureCatalogue() error { +func EnsureCatalogue(conf *runtime.Config) error { catalogueDir := path.Join(config.ABRA_DIR, "catalogue") if _, err := os.Stat(catalogueDir); err != nil && os.IsNotExist(err) { + if conf.Offline { + return fmt.Errorf("no local copy of the catalogue available, network access required") + } + url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, config.CATALOGUE_JSON_REPO_NAME) if err := gitPkg.Clone(catalogueDir, url); err != nil { return err @@ -68,7 +73,7 @@ func EnsureCatalogue() error { // EnsureUpToDate ensures that the local catalogue has no unstaged changes as // is up to date. This is useful to run before doing catalogue generation. -func EnsureUpToDate() error { +func EnsureUpToDate(conf *runtime.Config) error { isClean, err := gitPkg.IsClean(config.CATALOGUE_DIR) if err != nil { return err @@ -79,6 +84,11 @@ func EnsureUpToDate() error { return fmt.Errorf(msg, config.CATALOGUE_DIR) } + if conf.Offline { + logrus.Debug("attempting to use local catalogue without access network (\"--offline\")") + return nil + } + repo, err := git.PlainOpen(config.CATALOGUE_DIR) if err != nil { return err diff --git a/pkg/lint/recipe.go b/pkg/lint/recipe.go index 3c6d6c5e..d77ef870 100644 --- a/pkg/lint/recipe.go +++ b/pkg/lint/recipe.go @@ -9,6 +9,7 @@ import ( "coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe" + "coopcloud.tech/abra/pkg/runtime" "coopcloud.tech/tagcmp" "github.com/docker/distribution/reference" "github.com/go-git/go-git/v5" @@ -333,7 +334,11 @@ func LintImagePresent(recipe recipe.Recipe) (bool, error) { } func LintHasPublishedVersion(recipe recipe.Recipe) (bool, error) { - catl, err := recipePkg.ReadRecipeCatalogue() + // defaults since we can't take arguments here... this means this lint rule + // always access the network if e.g. the catalogue needs cloning / updating + conf := runtime.New() + + catl, err := recipePkg.ReadRecipeCatalogue(conf) if err != nil { logrus.Fatal(err) } diff --git a/pkg/recipe/recipe.go b/pkg/recipe/recipe.go index ddb6a5ba..64ce637b 100644 --- a/pkg/recipe/recipe.go +++ b/pkg/recipe/recipe.go @@ -252,13 +252,13 @@ func Get(recipeName string, conf *runtime.Config) (Recipe, error) { // EnsureExists ensures that a recipe is locally cloned func EnsureExists(recipeName string, conf *runtime.Config) error { - if !conf.EnsureRecipeExists { - return nil - } - recipeDir := path.Join(config.RECIPES_DIR, recipeName) if _, err := os.Stat(recipeDir); os.IsNotExist(err) { + if conf.Offline { + return fmt.Errorf("no local copy of %s available, network access required", recipeName) + } + logrus.Debugf("%s does not exist, attemmpting to clone", recipeDir) url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, recipeName) if err := gitPkg.Clone(recipeDir, url); err != nil { @@ -339,10 +339,6 @@ func EnsureVersion(recipeName, version string) error { // EnsureLatest makes sure the latest commit is checked out for a local recipe repository func EnsureLatest(recipeName string, conf *runtime.Config) error { - if !conf.EnsureRecipeLatest { - return nil - } - recipeDir := path.Join(config.RECIPES_DIR, recipeName) isClean, err := gitPkg.IsClean(recipeDir) @@ -588,10 +584,6 @@ func GetStringInBetween(recipeName, str, start, end string) (result string, err // EnsureUpToDate ensures that the local repo is synced to the remote func EnsureUpToDate(recipeName string, conf *runtime.Config) error { - if !conf.EnsureRecipeLatest { - return nil - } - recipeDir := path.Join(config.RECIPES_DIR, recipeName) isClean, err := gitPkg.IsClean(recipeDir) @@ -604,6 +596,11 @@ func EnsureUpToDate(recipeName string, conf *runtime.Config) error { return fmt.Errorf(msg, recipeName, recipeDir) } + if conf.Offline { + logrus.Debug("attempting to use local recipe without access network (\"--offline\")") + return nil + } + repo, err := git.PlainOpen(recipeDir) if err != nil { return fmt.Errorf("unable to open %s: %s", recipeDir, err) @@ -659,14 +656,14 @@ func EnsureUpToDate(recipeName string, conf *runtime.Config) error { } // ReadRecipeCatalogue reads the recipe catalogue. -func ReadRecipeCatalogue() (RecipeCatalogue, error) { +func ReadRecipeCatalogue(conf *runtime.Config) (RecipeCatalogue, error) { recipes := make(RecipeCatalogue) - if err := catalogue.EnsureCatalogue(); err != nil { + if err := catalogue.EnsureCatalogue(conf); err != nil { return nil, err } - if err := catalogue.EnsureUpToDate(); err != nil { + if err := catalogue.EnsureUpToDate(conf); err != nil { return nil, err } @@ -694,10 +691,10 @@ func readRecipeCatalogueFS(target interface{}) error { } // VersionsOfService lists the version of a service. -func VersionsOfService(recipe, serviceName string) ([]string, error) { +func VersionsOfService(recipe, serviceName string, conf *runtime.Config) ([]string, error) { var versions []string - catalogue, err := ReadRecipeCatalogue() + catalogue, err := ReadRecipeCatalogue(conf) if err != nil { return nil, err } @@ -724,7 +721,7 @@ func VersionsOfService(recipe, serviceName string) ([]string, error) { // GetRecipeMeta retrieves the recipe metadata from the recipe catalogue. func GetRecipeMeta(recipeName string, conf *runtime.Config) (RecipeMeta, error) { - catl, err := ReadRecipeCatalogue() + catl, err := ReadRecipeCatalogue(conf) if err != nil { return RecipeMeta{}, err } @@ -821,7 +818,11 @@ type InternalTracker struct { type RepoCatalogue map[string]RepoMeta // ReadReposMetadata retrieves coop-cloud/... repo metadata from Gitea. -func ReadReposMetadata() (RepoCatalogue, error) { +func ReadReposMetadata(conf *runtime.Config) (RepoCatalogue, error) { + if conf.Offline { + return nil, fmt.Errorf("network access required to query recipes metadata") + } + reposMeta := make(RepoCatalogue) pageIdx := 1 @@ -1002,6 +1003,10 @@ func GetRecipeCatalogueVersions(recipeName string, catl RecipeCatalogue) ([]stri // UpdateRepositories clones and updates all recipe repositories locally. func UpdateRepositories(repos RepoCatalogue, recipeName string, conf *runtime.Config) error { + if conf.Offline { + return fmt.Errorf("network access required to update recipes") + } + var barLength int if recipeName != "" { barLength = 1 diff --git a/pkg/runtime/config.go b/pkg/runtime/config.go new file mode 100644 index 00000000..d4518203 --- /dev/null +++ b/pkg/runtime/config.go @@ -0,0 +1,28 @@ +package runtime + +import "github.com/sirupsen/logrus" + +type Config struct { + Offline bool +} + +type Option func(c *Config) + +func New(opts ...Option) *Config { + conf := &Config{Offline: false} + + for _, optFunc := range opts { + optFunc(conf) + } + + return conf +} + +func WithOffline(offline bool) Option { + return func(c *Config) { + if offline { + logrus.Debugf("runtime config: attempting to run in offline mode") + } + c.Offline = offline + } +} diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go deleted file mode 100644 index 2ee968f1..00000000 --- a/pkg/runtime/runtime.go +++ /dev/null @@ -1,62 +0,0 @@ -package runtime - -import "github.com/sirupsen/logrus" - -// Config is an internal configuration modifier. It can be instantiated on a -// command call and can be changed on the fly to help make decisions further -// down in the internals, e.g. whether or not to clone the recipe locally or -// not. -type Config struct { - EnsureRecipeExists bool // ensure that the recipe is cloned locally - EnsureRecipeLatest bool // ensure the local recipe has latest changes -} - -// Option modifies a Config. The convention for passing an Option to a function -// is so far, only used in internal/validate.go. A Config is then constructed -// and passed down further into the code. This may change in the future but -// this is at least the abstraction so far. -type Option func(c *Config) - -// New instantiates a Config. -func New(opts ...Option) *Config { - conf := &Config{ - EnsureRecipeExists: true, - EnsureRecipeLatest: true, - } - - for _, optFunc := range opts { - optFunc(conf) - } - - return conf -} - -// WithEnsureRecipeExists determines whether or not we should be cloning the -// local recipe or not. This can be useful for being more adaptable to offline -// scenarios. -func WithEnsureRecipeExists(ensureRecipeExists bool) Option { - return func(c *Config) { - if ensureRecipeExists { - logrus.Debugf("runtime config: EnsureRecipeExists = %v, ensuring recipes are cloned", ensureRecipeExists) - } else { - logrus.Debugf("runtime config: EnsureRecipeExists = %v, not cloning recipes", ensureRecipeExists) - } - - c.EnsureRecipeExists = ensureRecipeExists - } -} - -// WithEnsureRecipeLatest determines whether we should update the local recipes -// remotely via Git. This can be useful when e.g. ensuring we have the latest -// changes before making new ones. -func WithEnsureRecipeLatest(ensureRecipeLatest bool) Option { - return func(c *Config) { - if ensureRecipeLatest { - logrus.Debugf("runtime config: EnsureRecipeLatest = %v, ensuring recipes have latest changes", ensureRecipeLatest) - } else { - logrus.Debugf("runtime config: EnsureRecipeLatest = %v, leaving recipes alone", ensureRecipeLatest) - } - - c.EnsureRecipeLatest = ensureRecipeLatest - } -}