diff --git a/cli/app/backup.go b/cli/app/backup.go index c7067941..5c3e1029 100644 --- a/cli/app/backup.go +++ b/cli/app/backup.go @@ -7,7 +7,6 @@ import ( "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/log" - "coopcloud.tech/abra/pkg/recipe" "github.com/urfave/cli" ) @@ -47,26 +46,10 @@ var appBackupListCommand = cli.Command{ Action: func(c *cli.Context) error { app := internal.ValidateApp(c) - if err := recipe.EnsureExists(app.Recipe); err != nil { + if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { log.Fatal(err) } - if !internal.Chaos { - if err := recipe.EnsureIsClean(app.Recipe); err != nil { - log.Fatal(err) - } - - if !internal.Offline { - if err := recipe.EnsureUpToDate(app.Recipe); err != nil { - log.Fatal(err) - } - } - - if err := recipe.EnsureLatest(app.Recipe); err != nil { - log.Fatal(err) - } - } - cl, err := client.New(app.Server) if err != nil { log.Fatal(err) @@ -110,22 +93,22 @@ var appBackupDownloadCommand = cli.Command{ Action: func(c *cli.Context) error { app := internal.ValidateApp(c) - if err := recipe.EnsureExists(app.Recipe); err != nil { + if err := app.Recipe.EnsureExists(); err != nil { log.Fatal(err) } if !internal.Chaos { - if err := recipe.EnsureIsClean(app.Recipe); err != nil { + if err := app.Recipe.EnsureIsClean(); err != nil { log.Fatal(err) } if !internal.Offline { - if err := recipe.EnsureUpToDate(app.Recipe); err != nil { + if err := app.Recipe.EnsureUpToDate(); err != nil { log.Fatal(err) } } - if err := recipe.EnsureLatest(app.Recipe); err != nil { + if err := app.Recipe.EnsureLatest(); err != nil { log.Fatal(err) } } @@ -180,22 +163,22 @@ var appBackupCreateCommand = cli.Command{ Action: func(c *cli.Context) error { app := internal.ValidateApp(c) - if err := recipe.EnsureExists(app.Recipe); err != nil { + if err := app.Recipe.EnsureExists(); err != nil { log.Fatal(err) } if !internal.Chaos { - if err := recipe.EnsureIsClean(app.Recipe); err != nil { + if err := app.Recipe.EnsureIsClean(); err != nil { log.Fatal(err) } if !internal.Offline { - if err := recipe.EnsureUpToDate(app.Recipe); err != nil { + if err := app.Recipe.EnsureUpToDate(); err != nil { log.Fatal(err) } } - if err := recipe.EnsureLatest(app.Recipe); err != nil { + if err := app.Recipe.EnsureLatest(); err != nil { log.Fatal(err) } } @@ -238,22 +221,22 @@ var appBackupSnapshotsCommand = cli.Command{ Action: func(c *cli.Context) error { app := internal.ValidateApp(c) - if err := recipe.EnsureExists(app.Recipe); err != nil { + if err := app.Recipe.EnsureExists(); err != nil { log.Fatal(err) } if !internal.Chaos { - if err := recipe.EnsureIsClean(app.Recipe); err != nil { + if err := app.Recipe.EnsureIsClean(); err != nil { log.Fatal(err) } if !internal.Offline { - if err := recipe.EnsureUpToDate(app.Recipe); err != nil { + if err := app.Recipe.EnsureUpToDate(); err != nil { log.Fatal(err) } } - if err := recipe.EnsureLatest(app.Recipe); err != nil { + if err := app.Recipe.EnsureLatest(); err != nil { log.Fatal(err) } } diff --git a/cli/app/check.go b/cli/app/check.go index 53f68901..b5eca951 100644 --- a/cli/app/check.go +++ b/cli/app/check.go @@ -6,8 +6,6 @@ import ( "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/log" - "coopcloud.tech/abra/pkg/recipe" - recipePkg "coopcloud.tech/abra/pkg/recipe" "github.com/urfave/cli" ) @@ -38,26 +36,10 @@ ${FOO:} syntax). "check" does not confirm or deny this for you.`, Action: func(c *cli.Context) error { app := internal.ValidateApp(c) - if err := recipe.EnsureExists(app.Recipe); err != nil { + if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { log.Fatal(err) } - if !internal.Chaos { - if err := recipePkg.EnsureIsClean(app.Recipe); err != nil { - log.Fatal(err) - } - - if !internal.Offline { - if err := recipePkg.EnsureUpToDate(app.Recipe); err != nil { - log.Fatal(err) - } - } - - if err := recipePkg.EnsureLatest(app.Recipe); err != nil { - log.Fatal(err) - } - } - tableCol := []string{"recipe env sample", "app env"} table := formatter.CreateTable(tableCol) diff --git a/cli/app/cmd.go b/cli/app/cmd.go index c8215d28..52dc03eb 100644 --- a/cli/app/cmd.go +++ b/cli/app/cmd.go @@ -5,7 +5,6 @@ import ( "fmt" "os" "os/exec" - "path" "sort" "strings" @@ -14,10 +13,8 @@ import ( 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/log" "coopcloud.tech/abra/pkg/recipe" - recipePkg "coopcloud.tech/abra/pkg/recipe" "github.com/urfave/cli" ) @@ -61,36 +58,19 @@ Example: Action: func(c *cli.Context) error { app := internal.ValidateApp(c) - if err := recipe.EnsureExists(app.Recipe); err != nil { + if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { log.Fatal(err) } - if !internal.Chaos { - if err := recipePkg.EnsureIsClean(app.Recipe); err != nil { - log.Fatal(err) - } - - if !internal.Offline { - if err := recipePkg.EnsureUpToDate(app.Recipe); err != nil { - log.Fatal(err) - } - } - - if err := recipePkg.EnsureLatest(app.Recipe); err != nil { - log.Fatal(err) - } - } - if internal.LocalCmd && internal.RemoteUser != "" { internal.ShowSubcommandHelpAndError(c, errors.New("cannot use --local & --user together")) } hasCmdArgs, parsedCmdArgs := parseCmdArgs(c.Args(), internal.LocalCmd) - abraSh := path.Join(config.RECIPES_DIR, app.Recipe, "abra.sh") - if _, err := os.Stat(abraSh); err != nil { + if _, err := os.Stat(app.Recipe.AbraShPath); err != nil { if os.IsNotExist(err) { - log.Fatalf("%s does not exist for %s?", abraSh, app.Name) + log.Fatalf("%s does not exist for %s?", app.Recipe.AbraShPath, app.Name) } log.Fatal(err) } @@ -101,7 +81,7 @@ Example: } cmdName := c.Args().Get(1) - if err := internal.EnsureCommand(abraSh, app.Recipe, cmdName); err != nil { + if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil { log.Fatal(err) } @@ -115,10 +95,10 @@ Example: var sourceAndExec string if hasCmdArgs { log.Debugf("parsed following command arguments: %s", parsedCmdArgs) - sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; STACK_NAME=%s; %s . %s; %s %s", app.Name, app.StackName(), exportEnv, abraSh, cmdName, parsedCmdArgs) + sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; STACK_NAME=%s; %s . %s; %s %s", app.Name, app.StackName(), exportEnv, app.Recipe.AbraShPath, cmdName, parsedCmdArgs) } else { log.Debug("did not detect any command arguments") - sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; STACK_NAME=%s; %s . %s; %s", app.Name, app.StackName(), exportEnv, abraSh, cmdName) + sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; STACK_NAME=%s; %s . %s; %s", app.Name, app.StackName(), exportEnv, app.Recipe.AbraShPath, cmdName) } shell := "/bin/bash" @@ -139,7 +119,7 @@ Example: targetServiceName := c.Args().Get(1) cmdName := c.Args().Get(2) - if err := internal.EnsureCommand(abraSh, app.Recipe, cmdName); err != nil { + if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil { log.Fatal(err) } @@ -172,7 +152,7 @@ Example: log.Fatal(err) } - if err := internal.RunCmdRemote(cl, app, abraSh, targetServiceName, cmdName, parsedCmdArgs); err != nil { + if err := internal.RunCmdRemote(cl, app, app.Recipe.AbraShPath, targetServiceName, cmdName, parsedCmdArgs); err != nil { log.Fatal(err) } } @@ -228,23 +208,24 @@ var appCmdListCommand = cli.Command{ Before: internal.SubCommandBefore, Action: func(c *cli.Context) error { app := internal.ValidateApp(c) + r := recipe.Get(app.Name) - if err := recipe.EnsureExists(app.Recipe); err != nil { + if err := r.EnsureExists(); err != nil { log.Fatal(err) } if !internal.Chaos { - if err := recipePkg.EnsureIsClean(app.Recipe); err != nil { + if err := r.EnsureIsClean(); err != nil { log.Fatal(err) } if !internal.Offline { - if err := recipePkg.EnsureUpToDate(app.Recipe); err != nil { + if err := r.EnsureUpToDate(); err != nil { log.Fatal(err) } } - if err := recipePkg.EnsureLatest(app.Recipe); err != nil { + if err := r.EnsureLatest(); err != nil { log.Fatal(err) } } @@ -263,8 +244,7 @@ var appCmdListCommand = cli.Command{ } func getShCmdNames(app appPkg.App) ([]string, error) { - abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh") - cmdNames, err := appPkg.ReadAbraShCmdNames(abraShPath) + cmdNames, err := appPkg.ReadAbraShCmdNames(app.Recipe.AbraShPath) if err != nil { return nil, err } diff --git a/cli/app/deploy.go b/cli/app/deploy.go index e7c3d5fd..cdbca1af 100644 --- a/cli/app/deploy.go +++ b/cli/app/deploy.go @@ -2,7 +2,6 @@ package app import ( "context" - "fmt" "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/autocomplete" @@ -11,7 +10,6 @@ import ( appPkg "coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/client" - "coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/dns" "coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/git" @@ -58,32 +56,11 @@ recipes. log.Fatal("cannot use and --chaos together") } - if err := recipe.EnsureExists(app.Recipe); err != nil { + if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { log.Fatal(err) } - if !internal.Chaos { - if err := recipe.EnsureIsClean(app.Recipe); err != nil { - log.Fatal(err) - } - - if !internal.Offline { - if err := recipe.EnsureUpToDate(app.Recipe); err != nil { - log.Fatal(err) - } - } - - if err := recipe.EnsureLatest(app.Recipe); err != nil { - log.Fatal(err) - } - } - - r, err := recipe.Get(app.Recipe, internal.Offline) - if err != nil { - log.Fatal(err) - } - - if err := lint.LintForErrors(r); err != nil { + if err := lint.LintForErrors(app.Recipe); err != nil { log.Fatal(err) } @@ -107,7 +84,7 @@ recipes. if specificVersion != "" { version = specificVersion log.Debugf("choosing %s as version to deploy", version) - if err := recipe.EnsureVersion(app.Recipe, version); err != nil { + if err := app.Recipe.EnsureVersion(version); err != nil { log.Fatal(err) } } @@ -136,14 +113,14 @@ recipes. if err != nil { log.Fatal(err) } - versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl) + versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe.Name, catl) if err != nil { log.Fatal(err) } if len(versions) == 0 && !internal.Chaos { log.Warn("no published versions in catalogue, trying local recipe repository") - recipeVersions, err := recipe.GetRecipeVersions(app.Recipe, internal.Offline) + recipeVersions, err := app.Recipe.GetRecipeVersions(internal.Offline) if err != nil { log.Warn(err) } @@ -157,11 +134,11 @@ recipes. if len(versions) > 0 && !internal.Chaos { version = versions[len(versions)-1] log.Debugf("choosing %s as version to deploy", version) - if err := recipe.EnsureVersion(app.Recipe, version); err != nil { + if err := app.Recipe.EnsureVersion(version); err != nil { log.Fatal(err) } } else { - head, err := git.GetRecipeHead(app.Recipe) + head, err := git.GetRecipeHead(app.Recipe.Name) if err != nil { log.Fatal(err) } @@ -173,14 +150,13 @@ recipes. if internal.Chaos { log.Warnf("chaos mode engaged") var err error - version, err = recipe.ChaosVersion(app.Recipe) + version, err = app.Recipe.ChaosVersion() if err != nil { log.Fatal(err) } } - abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh") - abraShEnv, err := envfile.ReadAbraShEnvVars(abraShPath) + abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath) if err != nil { log.Fatal(err) } @@ -188,7 +164,7 @@ recipes. app.Env[k] = v } - composeFiles, err := recipe.GetComposeFiles(app.Recipe, app.Env) + composeFiles, err := app.Recipe.GetComposeFiles(app.Env) if err != nil { log.Fatal(err) } @@ -206,7 +182,7 @@ recipes. } appPkg.ExposeAllEnv(stackName, compose, app.Env) - appPkg.SetRecipeLabel(compose, stackName, app.Recipe) + appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name) appPkg.SetChaosLabel(compose, stackName, internal.Chaos) appPkg.SetChaosVersionLabel(compose, stackName, version) appPkg.SetUpdateLabel(compose, stackName, app.Env) diff --git a/cli/app/list.go b/cli/app/list.go index 422461e3..0ddea59f 100644 --- a/cli/app/list.go +++ b/cli/app/list.go @@ -16,28 +16,34 @@ import ( "github.com/urfave/cli" ) -var status bool -var statusFlag = &cli.BoolFlag{ - Name: "status, S", - Usage: "Show app deployment status", - Destination: &status, -} +var ( + status bool + statusFlag = &cli.BoolFlag{ + Name: "status, S", + Usage: "Show app deployment status", + Destination: &status, + } +) -var recipeFilter string -var recipeFlag = &cli.StringFlag{ - Name: "recipe, r", - Value: "", - Usage: "Show apps of a specific recipe", - Destination: &recipeFilter, -} +var ( + recipeFilter string + recipeFlag = &cli.StringFlag{ + Name: "recipe, r", + Value: "", + Usage: "Show apps of a specific recipe", + Destination: &recipeFilter, + } +) -var listAppServer string -var listAppServerFlag = &cli.StringFlag{ - Name: "server, s", - Value: "", - Usage: "Show apps of a specific server", - Destination: &listAppServer, -} +var ( + listAppServer string + listAppServerFlag = &cli.StringFlag{ + Name: "server, s", + Value: "", + Usage: "Show apps of a specific server", + Destination: &listAppServer, + } +) type appStatus struct { Server string `json:"server"` @@ -130,7 +136,7 @@ can take some time. } } - if app.Recipe == recipeFilter || recipeFilter == "" { + if app.Recipe.Name == recipeFilter || recipeFilter == "" { if recipeFilter != "" { // only count server if matches filter totalServersCount++ @@ -177,7 +183,7 @@ can take some time. var newUpdates []string if version != "unknown" { - updates, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl) + updates, err := recipe.GetRecipeCatalogueVersions(app.Recipe.Name, catl) if err != nil { log.Fatal(err) } @@ -214,7 +220,7 @@ can take some time. } appStats.Server = app.Server - appStats.Recipe = app.Recipe + appStats.Recipe = app.Recipe.Name appStats.AppName = app.Name appStats.Domain = app.Domain diff --git a/cli/app/logs.go b/cli/app/logs.go index c98d62b5..c0682033 100644 --- a/cli/app/logs.go +++ b/cli/app/logs.go @@ -13,7 +13,6 @@ import ( "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/log" - "coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/upstream/stack" "github.com/docker/docker/api/types" containerTypes "github.com/docker/docker/api/types/container" @@ -39,7 +38,7 @@ var appLogsCommand = cli.Command{ app := internal.ValidateApp(c) stackName := app.StackName() - if err := recipe.EnsureExists(app.Recipe); err != nil { + if err := app.Recipe.EnsureExists(); err != nil { log.Fatal(err) } diff --git a/cli/app/new.go b/cli/app/new.go index 5011af63..c9e8e9bf 100644 --- a/cli/app/new.go +++ b/cli/app/new.go @@ -2,7 +2,6 @@ package app import ( "fmt" - "path" "coopcloud.tech/abra/cli/internal" appPkg "coopcloud.tech/abra/pkg/app" @@ -69,18 +68,18 @@ var appNewCommand = cli.Command{ recipe := internal.ValidateRecipe(c) if !internal.Chaos { - if err := recipePkg.EnsureIsClean(recipe.Name); err != nil { + if err := recipe.EnsureIsClean(); err != nil { log.Fatal(err) } if !internal.Offline { - if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil { + if err := recipe.EnsureUpToDate(); err != nil { log.Fatal(err) } } if c.Args().Get(1) == "" { var version string - recipeVersions, err := recipePkg.GetRecipeVersions(recipe.Name, internal.Offline) + recipeVersions, err := recipe.GetRecipeVersions(internal.Offline) if err != nil { log.Fatal(err) } @@ -93,16 +92,16 @@ var appNewCommand = cli.Command{ version = tag } - if err := recipePkg.EnsureVersion(recipe.Name, version); err != nil { + if err := recipe.EnsureVersion(version); err != nil { log.Fatal(err) } } else { - if err := recipePkg.EnsureLatest(recipe.Name); err != nil { + if err := recipe.EnsureLatest(); err != nil { log.Fatal(err) } } } else { - if err := recipePkg.EnsureVersion(recipe.Name, c.Args().Get(1)); err != nil { + if err := recipe.EnsureVersion(c.Args().Get(1)); err != nil { log.Fatal(err) } } @@ -120,7 +119,7 @@ var appNewCommand = cli.Command{ log.Debugf("%s sanitised as %s for new app", internal.Domain, sanitisedAppName) if err := appPkg.TemplateAppEnvSample( - recipe.Name, + recipe, internal.Domain, internal.NewAppServer, internal.Domain, @@ -136,13 +135,12 @@ var appNewCommand = cli.Command{ log.Fatal(err) } - composeFiles, err := recipePkg.GetComposeFiles(recipe.Name, sampleEnv) + composeFiles, err := recipe.GetComposeFiles(sampleEnv) if err != nil { log.Fatal(err) } - envSamplePath := path.Join(config.RECIPES_DIR, recipe.Name, ".env.sample") - secretsConfig, err := secret.ReadSecretsConfig(envSamplePath, composeFiles, appPkg.StackName(internal.Domain)) + secretsConfig, err := secret.ReadSecretsConfig(recipe.SampleEnvPath, composeFiles, appPkg.StackName(internal.Domain)) if err != nil { return err } diff --git a/cli/app/ps.go b/cli/app/ps.go index 162c6f1c..5ed15f6b 100644 --- a/cli/app/ps.go +++ b/cli/app/ps.go @@ -53,7 +53,7 @@ var appPsCommand = cli.Command{ statuses, err := appPkg.GetAppStatuses([]appPkg.App{app}, true) if statusMeta, ok := statuses[app.StackName()]; ok { if _, exists := statusMeta["chaos"]; !exists { - if err := recipe.EnsureVersion(app.Recipe, deployedVersion); err != nil { + if err := app.Recipe.EnsureVersion(deployedVersion); err != nil { log.Fatal(err) } } @@ -67,7 +67,8 @@ var appPsCommand = cli.Command{ // showPSOutput renders ps output. func showPSOutput(app appPkg.App, cl *dockerClient.Client) { - composeFiles, err := recipe.GetComposeFiles(app.Recipe, app.Env) + r := recipe.Get(app.Name) + composeFiles, err := r.GetComposeFiles(app.Env) if err != nil { log.Fatal(err) return diff --git a/cli/app/restore.go b/cli/app/restore.go index f95b497d..b31cc097 100644 --- a/cli/app/restore.go +++ b/cli/app/restore.go @@ -7,7 +7,6 @@ import ( "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/log" - "coopcloud.tech/abra/pkg/recipe" "github.com/urfave/cli" ) @@ -33,26 +32,10 @@ var appRestoreCommand = cli.Command{ Action: func(c *cli.Context) error { app := internal.ValidateApp(c) - if err := recipe.EnsureExists(app.Recipe); err != nil { + if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { log.Fatal(err) } - if !internal.Chaos { - if err := recipe.EnsureIsClean(app.Recipe); err != nil { - log.Fatal(err) - } - - if !internal.Offline { - if err := recipe.EnsureUpToDate(app.Recipe); err != nil { - log.Fatal(err) - } - } - - if err := recipe.EnsureLatest(app.Recipe); err != nil { - log.Fatal(err) - } - } - cl, err := client.New(app.Server) if err != nil { log.Fatal(err) diff --git a/cli/app/rollback.go b/cli/app/rollback.go index 6dbf449c..b552d2b1 100644 --- a/cli/app/rollback.go +++ b/cli/app/rollback.go @@ -6,7 +6,6 @@ import ( appPkg "coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/autocomplete" - "coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/envfile" "coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/recipe" @@ -58,32 +57,11 @@ recipes. log.Fatal("cannot use and --chaos together") } - if err := recipe.EnsureExists(app.Recipe); err != nil { + if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { log.Fatal(err) } - if !internal.Chaos { - if err := recipe.EnsureIsClean(app.Recipe); err != nil { - log.Fatal(err) - } - - if !internal.Offline { - if err := recipe.EnsureUpToDate(app.Recipe); err != nil { - log.Fatal(err) - } - } - - if err := recipe.EnsureLatest(app.Recipe); err != nil { - log.Fatal(err) - } - } - - r, err := recipe.Get(app.Recipe, internal.Offline) - if err != nil { - log.Fatal(err) - } - - if err := lint.LintForErrors(r); err != nil { + if err := lint.LintForErrors(app.Recipe); err != nil { log.Fatal(err) } @@ -108,14 +86,14 @@ recipes. log.Fatal(err) } - versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl) + versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe.Name, catl) if err != nil { log.Fatal(err) } if len(versions) == 0 && !internal.Chaos { log.Warn("no published versions in catalogue, trying local recipe repository") - recipeVersions, err := recipe.GetRecipeVersions(app.Recipe, internal.Offline) + recipeVersions, err := app.Recipe.GetRecipeVersions(internal.Offline) if err != nil { log.Warn(err) } @@ -185,7 +163,7 @@ recipes. } if !internal.Chaos { - if err := recipe.EnsureVersion(app.Recipe, chosenDowngrade); err != nil { + if err := app.Recipe.EnsureVersion(chosenDowngrade); err != nil { log.Fatal(err) } } @@ -193,14 +171,13 @@ recipes. if internal.Chaos { log.Warn("chaos mode engaged") var err error - chosenDowngrade, err = recipe.ChaosVersion(app.Recipe) + chosenDowngrade, err = app.Recipe.ChaosVersion() if err != nil { log.Fatal(err) } } - abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh") - abraShEnv, err := envfile.ReadAbraShEnvVars(abraShPath) + abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath) if err != nil { log.Fatal(err) } @@ -208,7 +185,7 @@ recipes. app.Env[k] = v } - composeFiles, err := recipe.GetComposeFiles(app.Recipe, app.Env) + composeFiles, err := app.Recipe.GetComposeFiles(app.Env) if err != nil { log.Fatal(err) } @@ -224,7 +201,7 @@ recipes. log.Fatal(err) } appPkg.ExposeAllEnv(stackName, compose, app.Env) - appPkg.SetRecipeLabel(compose, stackName, app.Recipe) + appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name) appPkg.SetChaosLabel(compose, stackName, internal.Chaos) appPkg.SetChaosVersionLabel(compose, stackName, chosenDowngrade) appPkg.SetUpdateLabel(compose, stackName, app.Env) diff --git a/cli/app/secret.go b/cli/app/secret.go index 672496f1..28554979 100644 --- a/cli/app/secret.go +++ b/cli/app/secret.go @@ -14,7 +14,6 @@ import ( "coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/log" - "coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/secret" "github.com/docker/docker/api/types" dockerClient "github.com/docker/docker/client" @@ -57,26 +56,10 @@ var appSecretGenerateCommand = cli.Command{ Action: func(c *cli.Context) error { app := internal.ValidateApp(c) - if err := recipe.EnsureExists(app.Recipe); err != nil { + if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { log.Fatal(err) } - if !internal.Chaos { - if err := recipe.EnsureIsClean(app.Recipe); err != nil { - log.Fatal(err) - } - - if !internal.Offline { - if err := recipe.EnsureUpToDate(app.Recipe); err != nil { - log.Fatal(err) - } - } - - if err := recipe.EnsureLatest(app.Recipe); err != nil { - log.Fatal(err) - } - } - if len(c.Args()) == 1 && !allSecrets { err := errors.New("missing arguments / or '--all'") internal.ShowSubcommandHelpAndError(c, err) @@ -87,7 +70,7 @@ var appSecretGenerateCommand = cli.Command{ internal.ShowSubcommandHelpAndError(c, err) } - composeFiles, err := recipe.GetComposeFiles(app.Recipe, app.Env) + composeFiles, err := app.Recipe.GetComposeFiles(app.Env) if err != nil { log.Fatal(err) } @@ -264,27 +247,27 @@ Example: Action: func(c *cli.Context) error { app := internal.ValidateApp(c) - if err := recipe.EnsureExists(app.Recipe); err != nil { + if err := app.Recipe.EnsureExists(); err != nil { log.Fatal(err) } if !internal.Chaos { - if err := recipe.EnsureIsClean(app.Recipe); err != nil { + if err := app.Recipe.EnsureIsClean(); err != nil { log.Fatal(err) } if !internal.Offline { - if err := recipe.EnsureUpToDate(app.Recipe); err != nil { + if err := app.Recipe.EnsureUpToDate(); err != nil { log.Fatal(err) } } - if err := recipe.EnsureLatest(app.Recipe); err != nil { + if err := app.Recipe.EnsureLatest(); err != nil { log.Fatal(err) } } - composeFiles, err := recipe.GetComposeFiles(app.Recipe, app.Env) + composeFiles, err := app.Recipe.GetComposeFiles(app.Env) if err != nil { log.Fatal(err) } @@ -372,22 +355,22 @@ var appSecretLsCommand = cli.Command{ Action: func(c *cli.Context) error { app := internal.ValidateApp(c) - if err := recipe.EnsureExists(app.Recipe); err != nil { + if err := app.Recipe.EnsureExists(); err != nil { log.Fatal(err) } if !internal.Chaos { - if err := recipe.EnsureIsClean(app.Recipe); err != nil { + if err := app.Recipe.EnsureIsClean(); err != nil { log.Fatal(err) } if !internal.Offline { - if err := recipe.EnsureUpToDate(app.Recipe); err != nil { + if err := app.Recipe.EnsureUpToDate(); err != nil { log.Fatal(err) } } - if err := recipe.EnsureLatest(app.Recipe); err != nil { + if err := app.Recipe.EnsureLatest(); err != nil { log.Fatal(err) } } diff --git a/cli/app/upgrade.go b/cli/app/upgrade.go index 4f54a481..b663526b 100644 --- a/cli/app/upgrade.go +++ b/cli/app/upgrade.go @@ -8,11 +8,9 @@ import ( 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/envfile" "coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/log" - "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe" stack "coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/tagcmp" @@ -64,28 +62,11 @@ recipes. log.Fatal("cannot use and --chaos together") } - if !internal.Chaos { - if err := recipe.EnsureIsClean(app.Recipe); err != nil { - log.Fatal(err) - } - - if !internal.Offline { - if err := recipe.EnsureUpToDate(app.Recipe); err != nil { - log.Fatal(err) - } - } - - if err := recipe.EnsureLatest(app.Recipe); err != nil { - log.Fatal(err) - } - } - - recipe, err := recipePkg.Get(app.Recipe, internal.Offline) - if err != nil { + if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { log.Fatal(err) } - if err := lint.LintForErrors(recipe); err != nil { + if err := lint.LintForErrors(app.Recipe); err != nil { log.Fatal(err) } @@ -110,14 +91,14 @@ recipes. log.Fatal(err) } - versions, err := recipePkg.GetRecipeCatalogueVersions(app.Recipe, catl) + versions, err := recipePkg.GetRecipeCatalogueVersions(app.Recipe.Name, catl) if err != nil { log.Fatal(err) } if len(versions) == 0 && !internal.Chaos { log.Warn("no published versions in catalogue, trying local recipe repository") - recipeVersions, err := recipePkg.GetRecipeVersions(app.Recipe, internal.Offline) + recipeVersions, err := app.Recipe.GetRecipeVersions(internal.Offline) if err != nil { log.Warn(err) } @@ -207,7 +188,7 @@ recipes. log.Fatal(err) } if parsedVersion.IsGreaterThan(parsedDeployedVersion) && parsedVersion.IsLessThan(parsedChosenUpgrade) { - note, err := internal.GetReleaseNotes(app.Recipe, version) + note, err := app.Recipe.GetReleaseNotes(version) if err != nil { return err } @@ -219,7 +200,7 @@ recipes. } if !internal.Chaos { - if err := recipePkg.EnsureVersion(app.Recipe, chosenUpgrade); err != nil { + if err := app.Recipe.EnsureVersion(chosenUpgrade); err != nil { log.Fatal(err) } } @@ -227,14 +208,13 @@ recipes. if internal.Chaos { log.Warn("chaos mode engaged") var err error - chosenUpgrade, err = recipePkg.ChaosVersion(app.Recipe) + chosenUpgrade, err = app.Recipe.ChaosVersion() if err != nil { log.Fatal(err) } } - abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh") - abraShEnv, err := envfile.ReadAbraShEnvVars(abraShPath) + abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath) if err != nil { log.Fatal(err) } @@ -242,7 +222,7 @@ recipes. app.Env[k] = v } - composeFiles, err := recipePkg.GetComposeFiles(app.Recipe, app.Env) + composeFiles, err := app.Recipe.GetComposeFiles(app.Env) if err != nil { log.Fatal(err) } @@ -258,7 +238,7 @@ recipes. log.Fatal(err) } appPkg.ExposeAllEnv(stackName, compose, app.Env) - appPkg.SetRecipeLabel(compose, stackName, app.Recipe) + appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name) appPkg.SetChaosLabel(compose, stackName, internal.Chaos) appPkg.SetChaosVersionLabel(compose, stackName, chosenUpgrade) appPkg.SetUpdateLabel(compose, stackName, app.Env) diff --git a/cli/app/version.go b/cli/app/version.go index 4c5b4667..4ef6469f 100644 --- a/cli/app/version.go +++ b/cli/app/version.go @@ -78,7 +78,7 @@ var appVersionCommand = cli.Command{ log.Fatalf("failed to determine version of deployed %s", app.Name) } - recipeMeta, err := recipe.GetRecipeMeta(app.Recipe, internal.Offline) + recipeMeta, err := recipe.GetRecipeMeta(app.Recipe.Name, internal.Offline) if err != nil { log.Fatal(err) } diff --git a/cli/catalogue/catalogue.go b/cli/catalogue/catalogue.go index 52ee6923..e3a81e02 100644 --- a/cli/catalogue/catalogue.go +++ b/cli/catalogue/catalogue.go @@ -57,6 +57,7 @@ keys configured on your account. BashComplete: autocomplete.RecipeNameComplete, Action: func(c *cli.Context) error { recipeName := c.Args().First() + r := recipe.Get(recipeName) if recipeName != "" { internal.ValidateRecipe(c) @@ -98,12 +99,12 @@ keys configured on your account. continue } - versions, err := recipe.GetRecipeVersions(recipeMeta.Name, internal.Offline) + versions, err := r.GetRecipeVersions(internal.Offline) if err != nil { log.Warn(err) } - features, category, err := recipe.GetRecipeFeaturesAndCategory(recipeMeta.Name) + features, category, err := recipe.GetRecipeFeaturesAndCategory(r) if err != nil { log.Warn(err) } diff --git a/cli/internal/deploy.go b/cli/internal/deploy.go index 9262fd32..698aab05 100644 --- a/cli/internal/deploy.go +++ b/cli/internal/deploy.go @@ -2,13 +2,10 @@ package internal import ( "fmt" - "io/ioutil" "os" - "path" "strings" appPkg "coopcloud.tech/abra/pkg/app" - "coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/log" "github.com/AlecAivazis/survey/v2" @@ -30,7 +27,7 @@ func NewVersionOverview(app appPkg.App, currentVersion, newVersion, releaseNotes server = "local" } - table.Append([]string{server, app.Recipe, deployConfig, app.Domain, currentVersion, newVersion}) + table.Append([]string{server, app.Recipe.Name, deployConfig, app.Domain, currentVersion, newVersion}) table.Render() if releaseNotes != "" && newVersion != "" { @@ -60,34 +57,13 @@ func NewVersionOverview(app appPkg.App, currentVersion, newVersion, releaseNotes return nil } -// GetReleaseNotes prints release notes for a recipe version -func GetReleaseNotes(recipeName, version string) (string, error) { - if version == "" { - return "", nil - } - - fpath := path.Join(config.RECIPES_DIR, recipeName, "release", version) - - if _, err := os.Stat(fpath); !os.IsNotExist(err) { - releaseNotes, err := ioutil.ReadFile(fpath) - if err != nil { - return "", err - } - withTitle := fmt.Sprintf("%s release notes:\n%s", version, string(releaseNotes)) - return withTitle, nil - } - - return "", nil -} - // PostCmds parses a string of commands and executes them inside of the respective services // the commands string must have the following format: // " | |... " func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error { - abraSh := path.Join(config.RECIPES_DIR, app.Recipe, "abra.sh") - if _, err := os.Stat(abraSh); err != nil { + if _, err := os.Stat(app.Recipe.AbraShPath); err != nil { if os.IsNotExist(err) { - return fmt.Errorf(fmt.Sprintf("%s does not exist for %s?", abraSh, app.Name)) + return fmt.Errorf(fmt.Sprintf("%s does not exist for %s?", app.Recipe.AbraShPath, app.Name)) } return err } @@ -105,7 +81,7 @@ func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error { } log.Infof("running post-command '%s %s' in container %s", cmdName, parsedCmdArgs, targetServiceName) - if err := EnsureCommand(abraSh, app.Recipe, cmdName); err != nil { + if err := EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil { return err } @@ -128,7 +104,7 @@ func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error { log.Debugf("running command %s %s within the context of %s_%s", cmdName, parsedCmdArgs, app.StackName(), targetServiceName) Tty = true - if err := RunCmdRemote(cl, app, abraSh, targetServiceName, cmdName, parsedCmdArgs); err != nil { + if err := RunCmdRemote(cl, app, app.Recipe.AbraShPath, targetServiceName, cmdName, parsedCmdArgs); err != nil { return err } } @@ -150,7 +126,7 @@ func DeployOverview(app appPkg.App, version, message string) error { server = "local" } - table.Append([]string{server, app.Recipe, deployConfig, app.Domain, version}) + table.Append([]string{server, app.Recipe.Name, deployConfig, app.Domain, version}) table.Render() if NoInput { diff --git a/cli/internal/recipe.go b/cli/internal/recipe.go index 9c87cce2..2e209ae1 100644 --- a/cli/internal/recipe.go +++ b/cli/internal/recipe.go @@ -88,7 +88,11 @@ func SetBumpType(bumpType string) { func GetMainAppImage(recipe recipe.Recipe) (string, error) { var path string - for _, service := range recipe.Config.Services { + config, err := recipe.GetComposeConfig(nil) + if err != nil { + return "", err + } + for _, service := range config.Services { if service.Name == "app" { img, err := reference.ParseNormalizedNamed(service.Image) if err != nil { diff --git a/cli/internal/validate.go b/cli/internal/validate.go index 92a0ba3a..9f416e91 100644 --- a/cli/internal/validate.go +++ b/cli/internal/validate.go @@ -57,7 +57,12 @@ func ValidateRecipe(c *cli.Context) recipe.Recipe { ShowSubcommandHelpAndError(c, errors.New("no recipe name provided")) } - chosenRecipe, err := recipe.Get(recipeName, Offline) + chosenRecipe := recipe.Get(recipeName) + err := chosenRecipe.EnsureExists() + if err != nil { + log.Fatal(err) + } + _, err = chosenRecipe.GetComposeConfig(nil) if err != nil { if c.Command.Name == "generate" { if strings.Contains(err.Error(), "missing a compose") { diff --git a/cli/recipe/diff.go b/cli/recipe/diff.go index 67278746..fa687650 100644 --- a/cli/recipe/diff.go +++ b/cli/recipe/diff.go @@ -1,13 +1,11 @@ package recipe import ( - "path" - "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/autocomplete" - "coopcloud.tech/abra/pkg/config" gitPkg "coopcloud.tech/abra/pkg/git" "coopcloud.tech/abra/pkg/log" + "coopcloud.tech/abra/pkg/recipe" "github.com/urfave/cli" ) @@ -25,13 +23,13 @@ var recipeDiffCommand = cli.Command{ BashComplete: autocomplete.RecipeNameComplete, Action: func(c *cli.Context) error { recipeName := c.Args().First() + r := recipe.Get(recipeName) if recipeName != "" { internal.ValidateRecipe(c) } - recipeDir := path.Join(config.RECIPES_DIR, recipeName) - if err := gitPkg.DiffUnstaged(recipeDir); err != nil { + if err := gitPkg.DiffUnstaged(r.Dir); err != nil { log.Fatal(err) } diff --git a/cli/recipe/fetch.go b/cli/recipe/fetch.go index 4bb0cb28..102f6fb4 100644 --- a/cli/recipe/fetch.go +++ b/cli/recipe/fetch.go @@ -24,9 +24,10 @@ var recipeFetchCommand = cli.Command{ BashComplete: autocomplete.RecipeNameComplete, Action: func(c *cli.Context) error { recipeName := c.Args().First() + r := recipe.Get(recipeName) if recipeName != "" { internal.ValidateRecipe(c) - if err := recipe.Ensure(recipeName); err != nil { + if err := r.Ensure(false, false); err != nil { log.Fatal(err) } return nil @@ -39,7 +40,8 @@ var recipeFetchCommand = cli.Command{ catlBar := formatter.CreateProgressbar(len(catalogue), "fetching latest recipes...") for recipeName := range catalogue { - if err := recipe.Ensure(recipeName); err != nil { + r := recipe.Get(recipeName) + if err := r.Ensure(false, false); err != nil { log.Error(err) } catlBar.Add(1) diff --git a/cli/recipe/lint.go b/cli/recipe/lint.go index 41a78db5..4de8be2d 100644 --- a/cli/recipe/lint.go +++ b/cli/recipe/lint.go @@ -8,7 +8,6 @@ import ( "coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/log" - recipePkg "coopcloud.tech/abra/pkg/recipe" "github.com/urfave/cli" ) @@ -29,26 +28,10 @@ var recipeLintCommand = cli.Command{ Action: func(c *cli.Context) error { recipe := internal.ValidateRecipe(c) - if err := recipePkg.EnsureExists(recipe.Name); err != nil { + if err := recipe.Ensure(internal.Chaos, internal.Offline); err != nil { log.Fatal(err) } - if !internal.Chaos { - if err := recipePkg.EnsureIsClean(recipe.Name); err != nil { - log.Fatal(err) - } - - if !internal.Offline { - if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil { - log.Fatal(err) - } - } - - if err := recipePkg.EnsureLatest(recipe.Name); err != nil { - log.Fatal(err) - } - } - tableCol := []string{"ref", "rule", "severity", "satisfied", "skipped", "resolve"} table := formatter.CreateTable(tableCol) diff --git a/cli/recipe/new.go b/cli/recipe/new.go index 9d6346f2..f55335f5 100644 --- a/cli/recipe/new.go +++ b/cli/recipe/new.go @@ -12,6 +12,7 @@ import ( "coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/git" "coopcloud.tech/abra/pkg/log" + "coopcloud.tech/abra/pkg/recipe" "github.com/urfave/cli" ) @@ -55,22 +56,22 @@ recipe and domain in the sample environment config). `, Action: func(c *cli.Context) error { recipeName := c.Args().First() + r := recipe.Get(recipeName) if recipeName == "" { internal.ShowSubcommandHelpAndError(c, errors.New("no recipe name provided")) } - directory := path.Join(config.RECIPES_DIR, recipeName) - if _, err := os.Stat(directory); !os.IsNotExist(err) { - log.Fatalf("%s recipe directory already exists?", directory) + if _, err := os.Stat(r.Dir); !os.IsNotExist(err) { + log.Fatalf("%s recipe directory already exists?", r.Dir) } url := fmt.Sprintf("%s/example.git", config.REPOS_BASE_URL) - if err := git.Clone(directory, url); err != nil { + if err := git.Clone(r.Dir, url); err != nil { log.Fatal(err) } - gitRepo := path.Join(config.RECIPES_DIR, recipeName, ".git") + gitRepo := path.Join(r.Dir, ".git") if err := os.RemoveAll(gitRepo); err != nil { log.Fatal(err) } @@ -78,11 +79,7 @@ recipe and domain in the sample environment config). meta := newRecipeMeta(recipeName) - toParse := []string{ - path.Join(config.RECIPES_DIR, recipeName, "README.md"), - path.Join(config.RECIPES_DIR, recipeName, ".env.sample"), - } - for _, path := range toParse { + for _, path := range []string{r.ReadmePath, r.SampleEnvPath} { tpl, err := template.ParseFiles(path) if err != nil { log.Fatal(err) @@ -93,14 +90,13 @@ recipe and domain in the sample environment config). log.Fatal(err) } - if err := os.WriteFile(path, templated.Bytes(), 0644); err != nil { + if err := os.WriteFile(path, templated.Bytes(), 0o644); err != nil { log.Fatal(err) } } - newGitRepo := path.Join(config.RECIPES_DIR, recipeName) - if err := git.Init(newGitRepo, true, internal.GitName, internal.GitEmail); err != nil { + if err := git.Init(r.Dir, true, internal.GitName, internal.GitEmail); err != nil { log.Fatal(err) } @@ -119,7 +115,7 @@ See "abra recipe -h" for additional recipe maintainer commands. Happy Hacking! -`, recipeName, path.Join(config.RECIPES_DIR, recipeName), recipeName)) +`, recipeName, path.Join(r.Dir), recipeName)) return nil }, diff --git a/cli/recipe/release.go b/cli/recipe/release.go index ad403c33..e7270d51 100644 --- a/cli/recipe/release.go +++ b/cli/recipe/release.go @@ -15,11 +15,11 @@ import ( gitPkg "coopcloud.tech/abra/pkg/git" "coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/recipe" - recipePkg "coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/tagcmp" "github.com/AlecAivazis/survey/v2" "github.com/distribution/reference" "github.com/go-git/go-git/v5" + "github.com/sirupsen/logrus" "github.com/urfave/cli" ) @@ -108,20 +108,20 @@ your SSH keys configured on your account. } } - isClean, err := gitPkg.IsClean(recipe.Dir()) + isClean, err := gitPkg.IsClean(recipe.Dir) if err != nil { log.Fatal(err) } if !isClean { log.Infof("%s currently has these unstaged changes 👇", recipe.Name) - if err := gitPkg.DiffUnstaged(recipe.Dir()); err != nil { + if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil { log.Fatal(err) } } if len(tags) > 0 { - log.Warnf("previous git tags detected, assuming this is a new semver release") + logrus.Warnf("previous git tags detected, assuming this is a new semver release") if err := createReleaseFromPreviousTag(tagString, mainAppVersion, recipe, tags); err != nil { log.Fatal(err) } @@ -129,7 +129,7 @@ your SSH keys configured on your account. log.Warnf("no tag specified and no previous tag available for %s, assuming this is the initial release", recipe.Name) if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil { - if cleanUpErr := cleanUpTag(tagString, recipe.Name); err != nil { + if cleanUpErr := cleanUpTag(recipe, tagString); err != nil { log.Fatal(cleanUpErr) } log.Fatal(err) @@ -144,8 +144,12 @@ your SSH keys configured on your account. func getImageVersions(recipe recipe.Recipe) (map[string]string, error) { services := make(map[string]string) + config, err := recipe.GetComposeConfig(nil) + if err != nil { + return nil, err + } missingTag := false - for _, service := range recipe.Config.Services { + for _, service := range config.Services { if service.Image == "" { continue } @@ -184,8 +188,7 @@ func getImageVersions(recipe recipe.Recipe) (map[string]string, error) { func createReleaseFromTag(recipe recipe.Recipe, tagString, mainAppVersion string) error { var err error - directory := path.Join(config.RECIPES_DIR, recipe.Name) - repo, err := git.PlainOpen(directory) + repo, err := git.PlainOpen(recipe.Dir) if err != nil { return err } @@ -246,8 +249,7 @@ func getTagCreateOptions(tag string) (git.CreateTagOptions, error) { // addReleaseNotes checks if the release/next release note exists and moves the // file to release/. func addReleaseNotes(recipe recipe.Recipe, tag string) error { - repoPath := path.Join(config.RECIPES_DIR, recipe.Name) - tagReleaseNotePath := path.Join(repoPath, "release", tag) + tagReleaseNotePath := path.Join(recipe.Dir, "release", tag) if _, err := os.Stat(tagReleaseNotePath); err == nil { // Release note for current tag already exist exists. return nil @@ -255,7 +257,7 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error { return err } - nextReleaseNotePath := path.Join(repoPath, "release", "next") + nextReleaseNotePath := path.Join(recipe.Dir, "release", "next") if _, err := os.Stat(nextReleaseNotePath); err == nil { // release/next note exists. Move it to release/ if internal.Dry { @@ -278,11 +280,11 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error { if err != nil { return err } - err = gitPkg.Add(repoPath, path.Join("release", "next"), internal.Dry) + err = gitPkg.Add(recipe.Dir, path.Join("release", "next"), internal.Dry) if err != nil { return err } - err = gitPkg.Add(repoPath, path.Join("release", tag), internal.Dry) + err = gitPkg.Add(recipe.Dir, path.Join("release", tag), internal.Dry) if err != nil { return err } @@ -311,7 +313,7 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error { if err != nil { return err } - err = gitPkg.Add(repoPath, path.Join("release", tag), internal.Dry) + err = gitPkg.Add(recipe.Dir, path.Join("release", tag), internal.Dry) if err != nil { return err } @@ -325,20 +327,19 @@ func commitRelease(recipe recipe.Recipe, tag string) error { return nil } - isClean, err := gitPkg.IsClean(recipe.Dir()) + isClean, err := gitPkg.IsClean(recipe.Dir) if err != nil { return err } if isClean { if !internal.Dry { - return fmt.Errorf("no changes discovered in %s, nothing to publish?", recipe.Dir()) + return fmt.Errorf("no changes discovered in %s, nothing to publish?", recipe.Dir) } } msg := fmt.Sprintf("chore: publish %s release", tag) - repoPath := path.Join(config.RECIPES_DIR, recipe.Name) - if err := gitPkg.Commit(repoPath, msg, internal.Dry); err != nil { + if err := gitPkg.Commit(recipe.Dir, msg, internal.Dry); err != nil { return err } @@ -402,8 +403,7 @@ func pushRelease(recipe recipe.Recipe, tagString string) error { } func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recipe.Recipe, tags []string) error { - directory := path.Join(config.RECIPES_DIR, recipe.Name) - repo, err := git.PlainOpen(directory) + repo, err := git.PlainOpen(recipe.Dir) if err != nil { return err } @@ -506,9 +506,8 @@ func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recip } // cleanUpTag removes a freshly created tag -func cleanUpTag(tag, recipeName string) error { - directory := path.Join(config.RECIPES_DIR, recipeName) - repo, err := git.PlainOpen(directory) +func cleanUpTag(recipe recipe.Recipe, tag string) error { + repo, err := git.PlainOpen(recipe.Dir) if err != nil { return err } @@ -525,7 +524,7 @@ func cleanUpTag(tag, recipeName string) error { } func getLabelVersion(recipe recipe.Recipe, prompt bool) (string, error) { - initTag, err := recipePkg.GetVersionLabelLocal(recipe) + initTag, err := recipe.GetVersionLabelLocal() if err != nil { return "", err } diff --git a/cli/recipe/reset.go b/cli/recipe/reset.go index 3c403219..915f3382 100644 --- a/cli/recipe/reset.go +++ b/cli/recipe/reset.go @@ -1,12 +1,10 @@ package recipe import ( - "path" - "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/autocomplete" - "coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/log" + "coopcloud.tech/abra/pkg/recipe" "github.com/go-git/go-git/v5" "github.com/urfave/cli" ) @@ -25,13 +23,13 @@ var recipeResetCommand = cli.Command{ BashComplete: autocomplete.RecipeNameComplete, Action: func(c *cli.Context) error { recipeName := c.Args().First() + r := recipe.Get(recipeName) if recipeName != "" { internal.ValidateRecipe(c) } - repoPath := path.Join(config.RECIPES_DIR, recipeName) - repo, err := git.PlainOpen(repoPath) + repo, err := git.PlainOpen(r.Dir) if err != nil { log.Fatal(err) } diff --git a/cli/recipe/sync.go b/cli/recipe/sync.go index c49b8726..f7432380 100644 --- a/cli/recipe/sync.go +++ b/cli/recipe/sync.go @@ -2,12 +2,10 @@ package recipe import ( "fmt" - "path" "strconv" "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/autocomplete" - "coopcloud.tech/abra/pkg/config" gitPkg "coopcloud.tech/abra/pkg/git" "coopcloud.tech/abra/pkg/log" "coopcloud.tech/tagcmp" @@ -107,8 +105,7 @@ likely to change. } if nextTag == "" { - recipeDir := path.Join(config.RECIPES_DIR, recipe.Name) - repo, err := git.PlainOpen(recipeDir) + repo, err := git.PlainOpen(recipe.Dir) if err != nil { log.Fatal(err) } @@ -199,13 +196,13 @@ likely to change. log.Infof("dry run: not syncing label %s for recipe %s", nextTag, recipe.Name) } - isClean, err := gitPkg.IsClean(recipe.Dir()) + isClean, err := gitPkg.IsClean(recipe.Dir) if err != nil { log.Fatal(err) } if !isClean { log.Infof("%s currently has these unstaged changes 👇", recipe.Name) - if err := gitPkg.DiffUnstaged(recipe.Dir()); err != nil { + if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil { log.Fatal(err) } } diff --git a/cli/recipe/upgrade.go b/cli/recipe/upgrade.go index f6011b62..4defb186 100644 --- a/cli/recipe/upgrade.go +++ b/cli/recipe/upgrade.go @@ -12,7 +12,6 @@ import ( "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" gitPkg "coopcloud.tech/abra/pkg/git" "coopcloud.tech/abra/pkg/log" @@ -73,19 +72,7 @@ You may invoke this command in "wizard" mode and be prompted for input: Action: func(c *cli.Context) error { recipe := internal.ValidateRecipe(c) - if err := recipePkg.EnsureIsClean(recipe.Name); err != nil { - log.Fatal(err) - } - - if err := recipePkg.EnsureExists(recipe.Name); err != nil { - log.Fatal(err) - } - - if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil { - log.Fatal(err) - } - - if err := recipePkg.EnsureLatest(recipe.Name); err != nil { + if err := recipe.Ensure(internal.Chaos, internal.Offline); err != nil { log.Fatal(err) } @@ -106,9 +93,8 @@ You may invoke this command in "wizard" mode and be prompted for input: // check for versions file and load pinned versions versionsPresent := false - recipeDir := path.Join(config.RECIPES_DIR, recipe.Name) - versionsPath := path.Join(recipeDir, "versions") - var servicePins = make(map[string]imgPin) + versionsPath := path.Join(recipe.Dir, "versions") + servicePins := make(map[string]imgPin) if _, err := os.Stat(versionsPath); err == nil { log.Debugf("found versions file for %s", recipe.Name) file, err := os.Open(versionsPath) @@ -141,7 +127,12 @@ You may invoke this command in "wizard" mode and be prompted for input: log.Debugf("did not find versions file for %s", recipe.Name) } - for _, service := range recipe.Config.Services { + config, err := recipe.GetComposeConfig(nil) + if err != nil { + log.Fatal(err) + } + + for _, service := range config.Services { img, err := reference.ParseNormalizedNamed(service.Image) if err != nil { log.Fatal(err) @@ -339,13 +330,13 @@ You may invoke this command in "wizard" mode and be prompted for input: } } - isClean, err := gitPkg.IsClean(recipeDir) + isClean, err := gitPkg.IsClean(recipe.Dir) if err != nil { log.Fatal(err) } if !isClean { log.Infof("%s currently has these unstaged changes 👇", recipe.Name) - if err := gitPkg.DiffUnstaged(recipeDir); err != nil { + if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil { log.Fatal(err) } } diff --git a/cli/recipe/version.go b/cli/recipe/version.go index 86ecc917..edad6c26 100644 --- a/cli/recipe/version.go +++ b/cli/recipe/version.go @@ -7,9 +7,9 @@ import ( "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/formatter" - "coopcloud.tech/abra/pkg/log" recipePkg "coopcloud.tech/abra/pkg/recipe" "github.com/olekukonko/tablewriter" + "github.com/sirupsen/logrus" "github.com/urfave/cli" ) @@ -42,16 +42,16 @@ var recipeVersionCommand = cli.Command{ catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline) if err != nil { - log.Fatal(err) + logrus.Fatal(err) } recipeMeta, ok := catl[recipe.Name] if !ok { - log.Fatalf("%s is not published on the catalogue?", recipe.Name) + logrus.Fatalf("%s is not published on the catalogue?", recipe.Name) } if len(recipeMeta.Versions) == 0 { - log.Fatalf("%s has no catalogue published versions?", recipe.Name) + logrus.Fatalf("%s has no catalogue published versions?", recipe.Name) } tableCols := []string{"version", "service", "image", "tag"} diff --git a/cli/updater/updater.go b/cli/updater/updater.go index 7fb7a2bd..352ececc 100644 --- a/cli/updater/updater.go +++ b/cli/updater/updater.go @@ -10,7 +10,6 @@ import ( "coopcloud.tech/abra/cli/internal" appPkg "coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/client" - "coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/envfile" "coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/recipe" @@ -318,22 +317,20 @@ func getAvailableUpgrades(cl *dockerclient.Client, stackName string, recipeName // processRecipeRepoVersion clones, pulls, checks out the version and lints the // recipe repository. -func processRecipeRepoVersion(recipeName, version string) error { - if err := recipe.EnsureExists(recipeName); err != nil { +func processRecipeRepoVersion(r recipe.Recipe, version string) error { + if err := r.EnsureExists(); err != nil { return err } - if err := recipe.EnsureUpToDate(recipeName); err != nil { + if err := r.EnsureUpToDate(); err != nil { return err } - if err := recipe.EnsureVersion(recipeName, version); err != nil { + if err := r.EnsureVersion(version); err != nil { return err } - if r, err := recipe.Get(recipeName, internal.Offline); err != nil { - return err - } else if err := lint.LintForErrors(r); err != nil { + if err := lint.LintForErrors(r); err != nil { return err } @@ -341,9 +338,8 @@ func processRecipeRepoVersion(recipeName, version string) error { } // mergeAbraShEnv merges abra.sh env vars into the app env vars. -func mergeAbraShEnv(recipeName string, env envfile.AppEnv) error { - abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, recipeName, "abra.sh") - abraShEnv, err := envfile.ReadAbraShEnvVars(abraShPath) +func mergeAbraShEnv(recipe recipe.Recipe, env envfile.AppEnv) error { + abraShEnv, err := envfile.ReadAbraShEnvVars(recipe.AbraShPath) if err != nil { return err } @@ -357,7 +353,7 @@ func mergeAbraShEnv(recipeName string, env envfile.AppEnv) error { } // createDeployConfig merges and enriches the compose config for the deployment. -func createDeployConfig(recipeName string, stackName string, env envfile.AppEnv) (*composetypes.Config, stack.Deploy, error) { +func createDeployConfig(r recipe.Recipe, stackName string, env envfile.AppEnv) (*composetypes.Config, stack.Deploy, error) { env["STACK_NAME"] = stackName deployOpts := stack.Deploy{ @@ -367,7 +363,7 @@ func createDeployConfig(recipeName string, stackName string, env envfile.AppEnv) Detach: false, } - composeFiles, err := recipe.GetComposeFiles(recipeName, env) + composeFiles, err := r.GetComposeFiles(env) if err != nil { return nil, deployOpts, err } @@ -382,7 +378,7 @@ func createDeployConfig(recipeName string, stackName string, env envfile.AppEnv) // after the upgrade the deployment won't be in chaos state anymore appPkg.SetChaosLabel(compose, stackName, false) - appPkg.SetRecipeLabel(compose, stackName, recipeName) + appPkg.SetRecipeLabel(compose, stackName, r.Name) appPkg.SetUpdateLabel(compose, stackName, env) return compose, deployOpts, nil @@ -440,20 +436,22 @@ func upgrade(cl *dockerclient.Client, stackName, recipeName, app := appPkg.App{ Name: stackName, - Recipe: recipeName, + Recipe: recipe.Get(recipeName), Server: SERVER, Env: env, } - if err = processRecipeRepoVersion(recipeName, upgradeVersion); err != nil { + r := recipe.Get(recipeName) + + if err = processRecipeRepoVersion(r, upgradeVersion); err != nil { return err } - if err = mergeAbraShEnv(recipeName, app.Env); err != nil { + if err = mergeAbraShEnv(app.Recipe, app.Env); err != nil { return err } - compose, deployOpts, err := createDeployConfig(recipeName, stackName, app.Env) + compose, deployOpts, err := createDeployConfig(r, stackName, app.Env) if err != nil { return err } diff --git a/pkg/app/app.go b/pkg/app/app.go index 3c2ee619..41bb2259 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -70,7 +70,7 @@ func GetApps(appFiles AppFiles, recipeFilter string) ([]App, error) { } if recipeFilter != "" { - if app.Recipe == recipeFilter { + if app.Recipe.Name == recipeFilter { apps = append(apps, app) } } else { @@ -84,7 +84,7 @@ func GetApps(appFiles AppFiles, recipeFilter string) ([]App, error) { // App reprents an app with its env file read into memory type App struct { Name AppName - Recipe string + Recipe recipe.Recipe Domain string Env envfile.AppEnv Server string @@ -161,13 +161,13 @@ func (a App) Filters(appendServiceNames, exactMatch bool, services ...string) (f return filters, nil } - composeFiles, err := recipe.GetComposeFiles(a.Recipe, a.Env) + composeFiles, err := a.Recipe.GetComposeFiles(a.Env) if err != nil { return filters, err } opts := stack.Deploy{Composefiles: composeFiles} - compose, err := GetAppComposeConfig(a.Recipe, opts, a.Env) + compose, err := GetAppComposeConfig(a.Recipe.Name, opts, a.Env) if err != nil { return filters, err } @@ -206,7 +206,7 @@ func (a ByServerAndRecipe) Len() int { return len(a) } func (a ByServerAndRecipe) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByServerAndRecipe) Less(i, j int) bool { if a[i].Server == a[j].Server { - return strings.ToLower(a[i].Recipe) < strings.ToLower(a[j].Recipe) + return strings.ToLower(a[i].Recipe.Name) < strings.ToLower(a[j].Recipe.Name) } return strings.ToLower(a[i].Server) < strings.ToLower(a[j].Server) } @@ -217,7 +217,7 @@ type ByRecipe []App func (a ByRecipe) Len() int { return len(a) } func (a ByRecipe) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByRecipe) Less(i, j int) bool { - return strings.ToLower(a[i].Recipe) < strings.ToLower(a[j].Recipe) + return strings.ToLower(a[i].Recipe.Name) < strings.ToLower(a[j].Recipe.Name) } // ByName sort a slice of Apps @@ -249,9 +249,9 @@ func ReadAppEnvFile(appFile AppFile, name AppName) (App, error) { func NewApp(env envfile.AppEnv, name string, appFile AppFile) (App, error) { domain := env["DOMAIN"] - recipe, exists := env["RECIPE"] + recipeName, exists := env["RECIPE"] if !exists { - recipe, exists = env["TYPE"] + recipeName, exists = env["TYPE"] if !exists { return App{}, fmt.Errorf("%s is missing the TYPE env var?", name) } @@ -260,7 +260,7 @@ func NewApp(env envfile.AppEnv, name string, appFile AppFile) (App, error) { return App{ Name: name, Domain: domain, - Recipe: recipe, + Recipe: recipe.Get(recipeName), Env: env, Server: appFile.Server, Path: appFile.Path, @@ -317,13 +317,13 @@ func GetAppServiceNames(appName string) ([]string, error) { return serviceNames, err } - composeFiles, err := recipe.GetComposeFiles(app.Recipe, app.Env) + composeFiles, err := app.Recipe.GetComposeFiles(app.Env) if err != nil { return serviceNames, err } opts := stack.Deploy{Composefiles: composeFiles} - compose, err := GetAppComposeConfig(app.Recipe, opts, app.Env) + compose, err := GetAppComposeConfig(app.Recipe.Name, opts, app.Env) if err != nil { return serviceNames, err } @@ -358,9 +358,8 @@ func GetAppNames() ([]string, error) { // TemplateAppEnvSample copies the example env file for the app into the users // env files. -func TemplateAppEnvSample(recipeName, appName, server, domain string) error { - envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample") - envSample, err := os.ReadFile(envSamplePath) +func TemplateAppEnvSample(r recipe.Recipe, appName, server, domain string) error { + envSample, err := os.ReadFile(r.SampleEnvPath) if err != nil { return err } @@ -380,14 +379,14 @@ func TemplateAppEnvSample(recipeName, appName, server, domain string) error { return err } - newContents := strings.Replace(string(read), recipeName+".example.com", domain, -1) + newContents := strings.Replace(string(read), r.Name+".example.com", domain, -1) err = os.WriteFile(appEnvPath, []byte(newContents), 0) if err != nil { return err } - log.Debugf("copied & templated %s to %s", envSamplePath, appEnvPath) + log.Debugf("copied & templated %s to %s", r.SampleEnvPath, appEnvPath) return nil } @@ -511,15 +510,7 @@ func ExposeAllEnv(stackName string, compose *composetypes.Config, appEnv envfile func CheckEnv(app App) ([]envfile.EnvVar, error) { var envVars []envfile.EnvVar - envSamplePath := path.Join(config.RECIPES_DIR, app.Recipe, ".env.sample") - if _, err := os.Stat(envSamplePath); err != nil { - if os.IsNotExist(err) { - return envVars, fmt.Errorf("%s does not exist?", envSamplePath) - } - return envVars, err - } - - envSample, err := envfile.ReadEnv(envSamplePath) + envSample, err := app.Recipe.SampleEnv() if err != nil { return envVars, err } diff --git a/pkg/app/app_test.go b/pkg/app/app_test.go index 3f32e985..a10cc753 100644 --- a/pkg/app/app_test.go +++ b/pkg/app/app_test.go @@ -47,8 +47,8 @@ func TestGetApp(t *testing.T) { } func TestGetComposeFiles(t *testing.T) { - offline := true - r, err := recipe.Get("abra-test-recipe", offline) + r := recipe.Get("abra-test-recipe") + err := r.EnsureExists() if err != nil { t.Fatal(err) } @@ -60,32 +60,33 @@ func TestGetComposeFiles(t *testing.T) { { map[string]string{}, []string{ - fmt.Sprintf("%s/%s/compose.yml", config.RECIPES_DIR, r.Name), + fmt.Sprintf("%s/compose.yml", r.Dir), }, }, { map[string]string{"COMPOSE_FILE": "compose.yml"}, []string{ - fmt.Sprintf("%s/%s/compose.yml", config.RECIPES_DIR, r.Name), + fmt.Sprintf("%s/compose.yml", r.Dir), }, }, { map[string]string{"COMPOSE_FILE": "compose.extra_secret.yml"}, []string{ - fmt.Sprintf("%s/%s/compose.extra_secret.yml", config.RECIPES_DIR, r.Name), + fmt.Sprintf("%s/compose.extra_secret.yml", r.Dir), }, }, { map[string]string{"COMPOSE_FILE": "compose.yml:compose.extra_secret.yml"}, []string{ - fmt.Sprintf("%s/%s/compose.yml", config.RECIPES_DIR, r.Name), - fmt.Sprintf("%s/%s/compose.extra_secret.yml", config.RECIPES_DIR, r.Name), + fmt.Sprintf("%s/compose.yml", r.Dir), + fmt.Sprintf("%s/compose.extra_secret.yml", r.Dir), }, }, } for _, test := range tests { - composeFiles, err := recipe.GetComposeFiles(r.Name, test.appEnv) + r2 := recipe.Get(r.Name) + composeFiles, err := r2.GetComposeFiles(test.appEnv) if err != nil { t.Fatal(err) } @@ -94,8 +95,8 @@ func TestGetComposeFiles(t *testing.T) { } func TestGetComposeFilesError(t *testing.T) { - offline := true - r, err := recipe.Get("abra-test-recipe", offline) + r := recipe.Get("abra-test-recipe") + err := r.EnsureExists() if err != nil { t.Fatal(err) } @@ -106,7 +107,8 @@ func TestGetComposeFilesError(t *testing.T) { } for _, test := range tests { - _, err := recipe.GetComposeFiles(r.Name, test.appEnv) + r2 := recipe.Get(r.Name) + _, err := r2.GetComposeFiles(test.appEnv) if err == nil { t.Fatalf("should have failed: %v", test.appEnv) } diff --git a/pkg/envfile/envfile_test.go b/pkg/envfile/envfile_test.go index 2ecd12a3..3c15555b 100644 --- a/pkg/envfile/envfile_test.go +++ b/pkg/envfile/envfile_test.go @@ -1,8 +1,6 @@ package envfile_test import ( - "fmt" - "path" "reflect" "slices" "strings" @@ -56,14 +54,13 @@ func TestReadEnv(t *testing.T) { } func TestReadAbraShEnvVars(t *testing.T) { - offline := true - r, err := recipe.Get("abra-test-recipe", offline) + r := recipe.Get("abra-test-recipe") + err := r.EnsureExists() if err != nil { t.Fatal(err) } - abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, r.Name, "abra.sh") - abraShEnv, err := envfile.ReadAbraShEnvVars(abraShPath) + abraShEnv, err := envfile.ReadAbraShEnvVars(r.AbraShPath) if err != nil { t.Fatal(err) } @@ -86,14 +83,13 @@ func TestReadAbraShEnvVars(t *testing.T) { } func TestReadAbraShCmdNames(t *testing.T) { - offline := true - r, err := recipe.Get("abra-test-recipe", offline) + r := recipe.Get("abra-test-recipe") + err := r.EnsureExists() if err != nil { t.Fatal(err) } - abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, r.Name, "abra.sh") - cmdNames, err := appPkg.ReadAbraShCmdNames(abraShPath) + cmdNames, err := appPkg.ReadAbraShCmdNames(r.AbraShPath) if err != nil { t.Fatal(err) } @@ -105,27 +101,27 @@ func TestReadAbraShCmdNames(t *testing.T) { expectedCmdNames := []string{"test_cmd", "test_cmd_args"} for _, cmdName := range expectedCmdNames { if !slices.Contains(cmdNames, cmdName) { - t.Fatalf("%s should have been found in %s", cmdName, abraShPath) + t.Fatalf("%s should have been found in %s", cmdName, r.AbraShPath) } } } func TestCheckEnv(t *testing.T) { - offline := true - r, err := recipe.Get("abra-test-recipe", offline) + r := recipe.Get("abra-test-recipe") + err := r.EnsureExists() if err != nil { t.Fatal(err) } - envSamplePath := path.Join(config.RECIPES_DIR, r.Name, ".env.sample") - envSample, err := envfile.ReadEnv(envSamplePath) + r2 := recipe.Get(r.Name) + envSample, err := r2.SampleEnv() if err != nil { t.Fatal(err) } app := appPkg.App{ Name: "test-app", - Recipe: r.Name, + Recipe: recipe.Get(r.Name), Domain: "example.com", Env: envSample, Path: "example.com.env", @@ -145,14 +141,14 @@ func TestCheckEnv(t *testing.T) { } func TestCheckEnvError(t *testing.T) { - offline := true - r, err := recipe.Get("abra-test-recipe", offline) + r := recipe.Get("abra-test-recipe") + err := r.EnsureExists() if err != nil { t.Fatal(err) } - envSamplePath := path.Join(config.RECIPES_DIR, r.Name, ".env.sample") - envSample, err := envfile.ReadEnv(envSamplePath) + r2 := recipe.Get(r.Name) + envSample, err := r2.SampleEnv() if err != nil { t.Fatal(err) } @@ -161,7 +157,7 @@ func TestCheckEnvError(t *testing.T) { app := appPkg.App{ Name: "test-app", - Recipe: r.Name, + Recipe: recipe.Get(r.Name), Domain: "example.com", Env: envSample, Path: "example.com.env", @@ -181,14 +177,14 @@ func TestCheckEnvError(t *testing.T) { } func TestEnvVarCommentsRemoved(t *testing.T) { - offline := true - r, err := recipe.Get("abra-test-recipe", offline) + r := recipe.Get("abra-test-recipe") + err := r.EnsureExists() if err != nil { t.Fatal(err) } - envSamplePath := path.Join(config.RECIPES_DIR, r.Name, ".env.sample") - envSample, err := envfile.ReadEnv(envSamplePath) + r2 := recipe.Get(r.Name) + envSample, err := r2.SampleEnv() if err != nil { t.Fatal(err) } @@ -213,14 +209,13 @@ func TestEnvVarCommentsRemoved(t *testing.T) { } func TestEnvVarModifiersIncluded(t *testing.T) { - offline := true - r, err := recipe.Get("abra-test-recipe", offline) + r := recipe.Get("abra-test-recipe") + err := r.EnsureExists() if err != nil { t.Fatal(err) } - envSamplePath := path.Join(config.RECIPES_DIR, r.Name, ".env.sample") - envSample, modifiers, err := envfile.ReadEnvWithModifiers(envSamplePath) + envSample, modifiers, err := envfile.ReadEnvWithModifiers(r.SampleEnvPath) if err != nil { t.Fatal(err) } diff --git a/pkg/lint/recipe.go b/pkg/lint/recipe.go index 52f24522..7c7c6ac9 100644 --- a/pkg/lint/recipe.go +++ b/pkg/lint/recipe.go @@ -7,7 +7,6 @@ import ( "path" "coopcloud.tech/abra/pkg/config" - "coopcloud.tech/abra/pkg/envfile" "coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe" @@ -203,16 +202,20 @@ func LintForErrors(recipe recipe.Recipe) error { } func LintComposeVersion(recipe recipe.Recipe) (bool, error) { - if recipe.Config.Version == "3.8" { + config, err := recipe.GetComposeConfig(nil) + if err != nil { + return false, err + } + if config.Version == "3.8" { return true, nil } return true, nil } -func LintEnvConfigPresent(recipe recipe.Recipe) (bool, error) { - envSample := fmt.Sprintf("%s/%s/.env.sample", config.RECIPES_DIR, recipe.Name) - if _, err := os.Stat(envSample); !os.IsNotExist(err) { +func LintEnvConfigPresent(r recipe.Recipe) (bool, error) { + r2 := recipe.Get(r.Name) + if _, err := os.Stat(r2.SampleEnvPath); !os.IsNotExist(err) { return true, nil } @@ -220,7 +223,11 @@ func LintEnvConfigPresent(recipe recipe.Recipe) (bool, error) { } func LintAppService(recipe recipe.Recipe) (bool, error) { - for _, service := range recipe.Config.Services { + config, err := recipe.GetComposeConfig(nil) + if err != nil { + return false, err + } + for _, service := range config.Services { if service.Name == "app" { return true, nil } @@ -233,11 +240,11 @@ func LintAppService(recipe recipe.Recipe) (bool, error) { // confirms that there is no "DOMAIN=..." in the .env.sample configuration of // the recipe. This typically means that no domain is required to deploy and // therefore no matching traefik deploy label will be present. -func LintTraefikEnabledSkipCondition(recipe recipe.Recipe) (bool, error) { - envSamplePath := path.Join(config.RECIPES_DIR, recipe.Name, ".env.sample") - sampleEnv, err := envfile.ReadEnv(envSamplePath) +func LintTraefikEnabledSkipCondition(r recipe.Recipe) (bool, error) { + r2 := recipe.Get(r.Name) + sampleEnv, err := r2.SampleEnv() if err != nil { - return false, fmt.Errorf("Unable to discover .env.sample for %s", recipe.Name) + return false, fmt.Errorf("Unable to discover .env.sample for %s", r2.Name) } if _, ok := sampleEnv["DOMAIN"]; !ok { @@ -248,7 +255,11 @@ func LintTraefikEnabledSkipCondition(recipe recipe.Recipe) (bool, error) { } func LintTraefikEnabled(recipe recipe.Recipe) (bool, error) { - for _, service := range recipe.Config.Services { + config, err := recipe.GetComposeConfig(nil) + if err != nil { + return false, err + } + for _, service := range config.Services { for label := range service.Deploy.Labels { if label == "traefik.enable" { if service.Deploy.Labels[label] == "true" { @@ -262,7 +273,11 @@ func LintTraefikEnabled(recipe recipe.Recipe) (bool, error) { } func LintHealthchecks(recipe recipe.Recipe) (bool, error) { - for _, service := range recipe.Config.Services { + config, err := recipe.GetComposeConfig(nil) + if err != nil { + return false, err + } + for _, service := range config.Services { if service.HealthCheck == nil { return false, nil } @@ -272,7 +287,11 @@ func LintHealthchecks(recipe recipe.Recipe) (bool, error) { } func LintAllImagesTagged(recipe recipe.Recipe) (bool, error) { - for _, service := range recipe.Config.Services { + config, err := recipe.GetComposeConfig(nil) + if err != nil { + return false, err + } + for _, service := range config.Services { img, err := reference.ParseNormalizedNamed(service.Image) if err != nil { return false, err @@ -286,7 +305,11 @@ func LintAllImagesTagged(recipe recipe.Recipe) (bool, error) { } func LintNoUnstableTags(recipe recipe.Recipe) (bool, error) { - for _, service := range recipe.Config.Services { + config, err := recipe.GetComposeConfig(nil) + if err != nil { + return false, err + } + for _, service := range config.Services { img, err := reference.ParseNormalizedNamed(service.Image) if err != nil { return false, err @@ -309,7 +332,11 @@ func LintNoUnstableTags(recipe recipe.Recipe) (bool, error) { } func LintSemverLikeTags(recipe recipe.Recipe) (bool, error) { - for _, service := range recipe.Config.Services { + config, err := recipe.GetComposeConfig(nil) + if err != nil { + return false, err + } + for _, service := range config.Services { img, err := reference.ParseNormalizedNamed(service.Image) if err != nil { return false, err @@ -332,7 +359,11 @@ func LintSemverLikeTags(recipe recipe.Recipe) (bool, error) { } func LintImagePresent(recipe recipe.Recipe) (bool, error) { - for _, service := range recipe.Config.Services { + config, err := recipe.GetComposeConfig(nil) + if err != nil { + return false, err + } + for _, service := range config.Services { if service.Image == "" { return false, nil } @@ -359,7 +390,8 @@ func LintHasPublishedVersion(recipe recipe.Recipe) (bool, error) { } func LintMetadataFilledIn(r recipe.Recipe) (bool, error) { - features, category, err := recipe.GetRecipeFeaturesAndCategory(r.Name) + r2 := recipe.Get(r.Name) + features, category, err := recipe.GetRecipeFeaturesAndCategory(r2) if err != nil { return false, err } @@ -380,9 +412,13 @@ func LintMetadataFilledIn(r recipe.Recipe) (bool, error) { } func LintAbraShVendors(recipe recipe.Recipe) (bool, error) { - for _, service := range recipe.Config.Services { + config, err := recipe.GetComposeConfig(nil) + if err != nil { + return false, err + } + for _, service := range config.Services { if len(service.Configs) > 0 { - abraSh := path.Join(config.RECIPES_DIR, recipe.Name, "abra.sh") + abraSh := path.Join(recipe.Dir, "abra.sh") if _, err := os.Stat(abraSh); err != nil { if os.IsNotExist(err) { return false, err @@ -410,7 +446,11 @@ func LintHasRecipeRepo(recipe recipe.Recipe) (bool, error) { } func LintSecretLengths(recipe recipe.Recipe) (bool, error) { - for name := range recipe.Config.Secrets { + config, err := recipe.GetComposeConfig(nil) + if err != nil { + return false, err + } + for name := range config.Secrets { if len(name) > 12 { return false, fmt.Errorf("secret %s is longer than 12 characters", name) } @@ -420,11 +460,9 @@ func LintSecretLengths(recipe recipe.Recipe) (bool, error) { } func LintValidTags(recipe recipe.Recipe) (bool, error) { - recipeDir := path.Join(config.RECIPES_DIR, recipe.Name) - - repo, err := git.PlainOpen(recipeDir) + repo, err := git.PlainOpen(recipe.Dir) if err != nil { - return false, fmt.Errorf("unable to open %s: %s", recipeDir, err) + return false, fmt.Errorf("unable to open %s: %s", recipe.Dir, err) } iter, err := repo.Tags() diff --git a/pkg/compose/compose.go b/pkg/recipe/compose.go similarity index 51% rename from pkg/compose/compose.go rename to pkg/recipe/compose.go index db7a7746..736e5f70 100644 --- a/pkg/compose/compose.go +++ b/pkg/recipe/compose.go @@ -1,14 +1,12 @@ -package compose +package recipe import ( "fmt" "io/ioutil" - "path" + "os" "path/filepath" "strings" - "coopcloud.tech/abra/pkg/config" - "coopcloud.tech/abra/pkg/envfile" "coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/upstream/stack" @@ -17,9 +15,105 @@ import ( composetypes "github.com/docker/cli/cli/compose/types" ) -// UpdateTag updates an image tag in-place on file system local compose files. -func UpdateTag(pattern, image, tag, recipeName string) (bool, error) { +// GetComposeFiles gets the list of compose files for an app (or recipe if you +// don't already have an app) which should be merged into a composetypes.Config +// while respecting the COMPOSE_FILE env var. +func (r Recipe) GetComposeFiles(appEnv map[string]string) ([]string, error) { + composeFileEnvVar, ok := appEnv["COMPOSE_FILE"] + if !ok { + if err := ensurePathExists(r.ComposePath); err != nil { + return []string{}, err + } + log.Debugf("no COMPOSE_FILE detected, loading default: %s", r.ComposePath) + return []string{r.ComposePath}, nil + } + + if !strings.Contains(composeFileEnvVar, ":") { + path := fmt.Sprintf("%s/%s", r.Dir, composeFileEnvVar) + if err := ensurePathExists(path); err != nil { + return []string{}, err + } + log.Debugf("COMPOSE_FILE detected, loading %s", path) + return []string{path}, nil + } + + var composeFiles []string + + numComposeFiles := strings.Count(composeFileEnvVar, ":") + 1 + envVars := strings.SplitN(composeFileEnvVar, ":", numComposeFiles) + if len(envVars) != numComposeFiles { + return composeFiles, fmt.Errorf("COMPOSE_FILE (=\"%s\") parsing failed?", composeFileEnvVar) + } + + for _, file := range envVars { + path := fmt.Sprintf("%s/%s", r.Dir, file) + if err := ensurePathExists(path); err != nil { + return composeFiles, err + } + composeFiles = append(composeFiles, path) + } + + log.Debugf("COMPOSE_FILE detected (%s), loading %s", composeFileEnvVar, strings.Join(envVars, ", ")) + log.Debugf("retrieved %s configs for %s", strings.Join(composeFiles, ", "), r.Name) + + return composeFiles, nil +} + +func (r Recipe) GetComposeConfig(env map[string]string) (*composetypes.Config, error) { + pattern := fmt.Sprintf("%s/compose**yml", r.Dir) composeFiles, err := filepath.Glob(pattern) + if err != nil { + return nil, err + } + + if len(composeFiles) == 0 { + return nil, fmt.Errorf("%s is missing a compose.yml or compose.*.yml file?", r.Name) + } + + if env == nil { + env, err = r.SampleEnv() + if err != nil { + return nil, err + } + } + + opts := stack.Deploy{Composefiles: composeFiles} + config, err := loader.LoadComposefile(opts, env) + if err != nil { + return nil, err + } + return config, nil +} + +// GetVersionLabelLocal retrieves the version label on the local recipe config +func (r Recipe) GetVersionLabelLocal() (string, error) { + var label string + config, err := r.GetComposeConfig(nil) + if err != nil { + return "", err + } + + for _, service := range config.Services { + for label, value := range service.Deploy.Labels { + if strings.HasPrefix(label, "coop-cloud") && strings.Contains(label, "version") { + return value, nil + } + } + } + + if label == "" { + return label, fmt.Errorf("%s has no version label? try running \"abra recipe sync %s\" first?", r.Name, r.Name) + } + + return label, nil +} + +// UpdateTag updates an image tag in-place on file system local compose files. +func (r Recipe) UpdateTag(image, tag string) (bool, error) { + fullPattern := fmt.Sprintf("%s/compose**yml", r.Dir) + image = formatter.StripTagMeta(image) + + composeFiles, err := filepath.Glob(fullPattern) if err != nil { return false, err } @@ -29,8 +123,7 @@ func UpdateTag(pattern, image, tag, recipeName string) (bool, error) { for _, composeFile := range composeFiles { opts := stack.Deploy{Composefiles: []string{composeFile}} - envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample") - sampleEnv, err := envfile.ReadEnv(envSamplePath) + sampleEnv, err := r.SampleEnv() if err != nil { return false, err } @@ -75,7 +168,7 @@ func UpdateTag(pattern, image, tag, recipeName string) (bool, error) { log.Debugf("updating %s to %s in %s", old, new, compose.Filename) - if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0764); err != nil { + if err := os.WriteFile(compose.Filename, []byte(replacedBytes), 0o764); err != nil { return false, err } } @@ -86,8 +179,9 @@ func UpdateTag(pattern, image, tag, recipeName string) (bool, error) { } // UpdateLabel updates a label in-place on file system local compose files. -func UpdateLabel(pattern, serviceName, label, recipeName string) error { - composeFiles, err := filepath.Glob(pattern) +func (r Recipe) UpdateLabel(pattern, serviceName, label string) error { + fullPattern := fmt.Sprintf("%s/%s", r.Dir, pattern) + composeFiles, err := filepath.Glob(fullPattern) if err != nil { return err } @@ -97,8 +191,7 @@ func UpdateLabel(pattern, serviceName, label, recipeName string) error { for _, composeFile := range composeFiles { opts := stack.Deploy{Composefiles: []string{composeFile}} - envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample") - sampleEnv, err := envfile.ReadEnv(envSamplePath) + sampleEnv, err := r.SampleEnv() if err != nil { return err } @@ -141,7 +234,7 @@ func UpdateLabel(pattern, serviceName, label, recipeName string) error { log.Debugf("updating %s to %s in %s", old, label, compose.Filename) - if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0764); err != nil { + if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0o764); err != nil { return err } diff --git a/pkg/recipe/files.go b/pkg/recipe/files.go new file mode 100644 index 00000000..f092fff3 --- /dev/null +++ b/pkg/recipe/files.go @@ -0,0 +1,37 @@ +package recipe + +import ( + "fmt" + "os" + "path" + + "coopcloud.tech/abra/pkg/envfile" +) + +func (r Recipe) SampleEnv() (map[string]string, error) { + sampleEnv, err := envfile.ReadEnv(r.SampleEnvPath) + if err != nil { + return sampleEnv, fmt.Errorf("unable to discover .env.sample for %s", r.Name) + } + return sampleEnv, nil +} + +// GetReleaseNotes prints release notes for the recipe version +func (r Recipe) GetReleaseNotes(version string) (string, error) { + if version == "" { + return "", nil + } + + fpath := path.Join(r.Dir, "release", version) + + if _, err := os.Stat(fpath); !os.IsNotExist(err) { + releaseNotes, err := os.ReadFile(fpath) + if err != nil { + return "", err + } + withTitle := fmt.Sprintf("%s release notes:\n%s", version, string(releaseNotes)) + return withTitle, nil + } + + return "", nil +} diff --git a/pkg/recipe/git.go b/pkg/recipe/git.go new file mode 100644 index 00000000..0d6ddc63 --- /dev/null +++ b/pkg/recipe/git.go @@ -0,0 +1,379 @@ +package recipe + +import ( + "fmt" + "os" + "path" + "strings" + + "coopcloud.tech/abra/pkg/config" + "coopcloud.tech/abra/pkg/formatter" + gitPkg "coopcloud.tech/abra/pkg/git" + "coopcloud.tech/abra/pkg/log" + "github.com/distribution/reference" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" +) + +// Ensure makes sure the recipe exists, is up to date and has the latest version checked out. +func (r Recipe) Ensure(chaos bool, offline bool) error { + if err := r.EnsureExists(); err != nil { + return err + } + + if !chaos { + if err := r.EnsureIsClean(); err != nil { + return err + } + if !offline { + if err := r.EnsureUpToDate(); err != nil { + log.Fatal(err) + } + } + if err := r.EnsureLatest(); err != nil { + return err + } + } + return nil +} + +// EnsureExists ensures that the recipe is locally cloned +func (r Recipe) EnsureExists() error { + recipeDir := path.Join(config.RECIPES_DIR, r.Name) + + if _, err := os.Stat(recipeDir); os.IsNotExist(err) { + log.Debugf("%s does not exist, attemmpting to clone", recipeDir) + url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, r.Name) + if err := gitPkg.Clone(recipeDir, url); err != nil { + return err + } + } + + if err := gitPkg.EnsureGitRepo(recipeDir); err != nil { + return err + } + + return nil +} + +// EnsureVersion checks whether a specific version exists for a recipe. +func (r Recipe) EnsureVersion(version string) error { + recipeDir := path.Join(config.RECIPES_DIR, r.Name) + + if err := gitPkg.EnsureGitRepo(recipeDir); err != nil { + return err + } + + repo, err := git.PlainOpen(recipeDir) + if err != nil { + return err + } + + tags, err := repo.Tags() + if err != nil { + return nil + } + + var parsedTags []string + var tagRef plumbing.ReferenceName + if err := tags.ForEach(func(ref *plumbing.Reference) (err error) { + parsedTags = append(parsedTags, ref.Name().Short()) + if ref.Name().Short() == version { + tagRef = ref.Name() + } + return nil + }); err != nil { + return err + } + + joinedTags := strings.Join(parsedTags, ", ") + if joinedTags != "" { + log.Debugf("read %s as tags for recipe %s", joinedTags, r.Name) + } + + if tagRef.String() == "" { + return fmt.Errorf("the local copy of %s doesn't seem to have version %s available?", r.Name, version) + } + + worktree, err := repo.Worktree() + if err != nil { + return err + } + + opts := &git.CheckoutOptions{ + Branch: tagRef, + Create: false, + Force: true, + } + if err := worktree.Checkout(opts); err != nil { + return err + } + + log.Debugf("successfully checked %s out to %s in %s", r.Name, tagRef.Short(), recipeDir) + + return nil +} + +// EnsureIsClean makes sure that the recipe repository has no unstaged changes. +func (r Recipe) EnsureIsClean() error { + recipeDir := path.Join(config.RECIPES_DIR, r.Name) + + isClean, err := gitPkg.IsClean(recipeDir) + if err != nil { + return fmt.Errorf("unable to check git clean status in %s: %s", recipeDir, err) + } + + if !isClean { + msg := "%s (%s) has locally unstaged changes? please commit/remove your changes before proceeding" + return fmt.Errorf(msg, r.Name, recipeDir) + } + + return nil +} + +// EnsureLatest makes sure the latest commit is checked out for the local recipe repository +func (r Recipe) EnsureLatest() error { + recipeDir := path.Join(config.RECIPES_DIR, r.Name) + + if err := gitPkg.EnsureGitRepo(recipeDir); err != nil { + return err + } + + repo, err := git.PlainOpen(recipeDir) + if err != nil { + return err + } + + worktree, err := repo.Worktree() + if err != nil { + return err + } + + branch, err := gitPkg.GetDefaultBranch(repo, recipeDir) + if err != nil { + return err + } + + checkOutOpts := &git.CheckoutOptions{ + Create: false, + Force: true, + Branch: plumbing.ReferenceName(branch), + } + + if err := worktree.Checkout(checkOutOpts); err != nil { + log.Debugf("failed to check out %s in %s", branch, recipeDir) + return err + } + + return nil +} + +// EnsureUpToDate ensures that the local repo is synced to the remote +func (r Recipe) EnsureUpToDate() error { + recipeDir := path.Join(config.RECIPES_DIR, r.Name) + + repo, err := git.PlainOpen(recipeDir) + if err != nil { + return fmt.Errorf("unable to open %s: %s", recipeDir, err) + } + + remotes, err := repo.Remotes() + if err != nil { + return fmt.Errorf("unable to read remotes in %s: %s", recipeDir, err) + } + + if len(remotes) == 0 { + log.Debugf("cannot ensure %s is up-to-date, no git remotes configured", r.Name) + return nil + } + + worktree, err := repo.Worktree() + if err != nil { + return fmt.Errorf("unable to open git work tree in %s: %s", recipeDir, err) + } + + branch, err := gitPkg.CheckoutDefaultBranch(repo, recipeDir) + if err != nil { + return fmt.Errorf("unable to check out default branch in %s: %s", recipeDir, err) + } + + fetchOpts := &git.FetchOptions{Tags: git.AllTags} + if err := repo.Fetch(fetchOpts); err != nil { + if !strings.Contains(err.Error(), "already up-to-date") { + return fmt.Errorf("unable to fetch tags in %s: %s", recipeDir, err) + } + } + + opts := &git.PullOptions{ + Force: true, + ReferenceName: branch, + SingleBranch: true, + } + + if err := worktree.Pull(opts); err != nil { + if !strings.Contains(err.Error(), "already up-to-date") { + return fmt.Errorf("unable to git pull in %s: %s", recipeDir, err) + } + } + + log.Debugf("fetched latest git changes for %s", r.Name) + + return nil +} + +// ChaosVersion constructs a chaos mode recipe version. +func (r Recipe) ChaosVersion() (string, error) { + var version string + + head, err := gitPkg.GetRecipeHead(r.Name) + if err != nil { + return version, err + } + + version = formatter.SmallSHA(head.String()) + + recipeDir := path.Join(config.RECIPES_DIR, r.Name) + isClean, err := gitPkg.IsClean(recipeDir) + if err != nil { + return version, err + } + + if !isClean { + version = fmt.Sprintf("%s + unstaged changes", version) + } + + return version, nil +} + +// Push pushes the latest changes to a SSH URL remote. You need to have your +// local SSH configuration for git.coopcloud.tech working for this to work +func (r Recipe) Push(dryRun bool) error { + repo, err := git.PlainOpen(r.Dir) + if err != nil { + return err + } + + if err := gitPkg.CreateRemote(repo, "origin-ssh", r.SSHURL, dryRun); err != nil { + return err + } + + if err := gitPkg.Push(r.Dir, "origin-ssh", true, dryRun); err != nil { + return err + } + + return nil +} + +// Tags list the recipe tags +func (r Recipe) Tags() ([]string, error) { + var tags []string + + repo, err := git.PlainOpen(r.Dir) + if err != nil { + return tags, err + } + + gitTags, err := repo.Tags() + if err != nil { + return tags, err + } + + if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) { + tags = append(tags, strings.TrimPrefix(string(ref.Name()), "refs/tags/")) + return nil + }); err != nil { + return tags, err + } + + log.Debugf("detected %s as tags for recipe %s", strings.Join(tags, ", "), r.Name) + + return tags, nil +} + +// GetRecipeVersions retrieves all recipe versions. +func (r Recipe) GetRecipeVersions(offline bool) (RecipeVersions, error) { + versions := RecipeVersions{} + log.Debugf("attempting to open git repository in %s", r.Dir) + + repo, err := git.PlainOpen(r.Dir) + if err != nil { + return versions, err + } + + worktree, err := repo.Worktree() + if err != nil { + return versions, err + } + + gitTags, err := repo.Tags() + if err != nil { + return versions, err + } + + if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) { + tag := strings.TrimPrefix(string(ref.Name()), "refs/tags/") + + log.Debugf("processing %s for %s", tag, r.Name) + + checkOutOpts := &git.CheckoutOptions{ + Create: false, + Force: true, + Branch: plumbing.ReferenceName(ref.Name()), + } + if err := worktree.Checkout(checkOutOpts); err != nil { + log.Debugf("failed to check out %s in %s", tag, r.Dir) + return err + } + + log.Debugf("successfully checked out %s in %s", ref.Name(), r.Dir) + + config, err := r.GetComposeConfig(nil) + if err != nil { + return err + } + + versionMeta := make(map[string]ServiceMeta) + for _, service := range config.Services { + + img, err := reference.ParseNormalizedNamed(service.Image) + if err != nil { + return err + } + + path := reference.Path(img) + + path = formatter.StripTagMeta(path) + + var tag string + switch img.(type) { + case reference.NamedTagged: + tag = img.(reference.NamedTagged).Tag() + case reference.Named: + log.Warnf("%s service is missing image tag?", path) + continue + } + + versionMeta[service.Name] = ServiceMeta{ + Image: path, + Tag: tag, + } + } + + versions = append(versions, map[string]map[string]ServiceMeta{tag: versionMeta}) + + return nil + }); err != nil { + return versions, err + } + + _, err = gitPkg.CheckoutDefaultBranch(repo, r.Dir) + if err != nil { + return versions, err + } + + sortRecipeVersions(versions) + + log.Debugf("collected %s for %s", versions, r.Dir) + + return versions, nil +} diff --git a/pkg/recipe/recipe.go b/pkg/recipe/recipe.go index a112a256..5a33dabc 100644 --- a/pkg/recipe/recipe.go +++ b/pkg/recipe/recipe.go @@ -6,28 +6,19 @@ import ( "io/ioutil" "os" "path" - "path/filepath" "slices" "sort" "strconv" "strings" "coopcloud.tech/abra/pkg/catalogue" - "coopcloud.tech/abra/pkg/compose" "coopcloud.tech/abra/pkg/config" - "coopcloud.tech/abra/pkg/envfile" "coopcloud.tech/abra/pkg/formatter" gitPkg "coopcloud.tech/abra/pkg/git" "coopcloud.tech/abra/pkg/limit" "coopcloud.tech/abra/pkg/log" - "coopcloud.tech/abra/pkg/upstream/stack" - loader "coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/abra/pkg/web" "coopcloud.tech/tagcmp" - "github.com/distribution/reference" - composetypes "github.com/docker/cli/cli/compose/types" - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing" ) // RecipeCatalogueURL is the only current recipe catalogue available. @@ -131,307 +122,29 @@ type Features struct { SSO string `json:"sso"` } -// Recipe represents a recipe. +func Get(name string) Recipe { + dir := path.Join(config.RECIPES_DIR, name) + return Recipe{ + Name: name, + Dir: dir, + SSHURL: fmt.Sprintf(config.SSH_URL_TEMPLATE, name), + + ComposePath: path.Join(dir, "compose.yml"), + ReadmePath: path.Join(dir, "README.md"), + SampleEnvPath: path.Join(dir, ".env.sample"), + AbraShPath: path.Join(dir, "abra.sh"), + } +} + type Recipe struct { Name string - Config *composetypes.Config - Meta RecipeMeta -} + Dir string + SSHURL string -// Push pushes the latest changes to a SSH URL remote. You need to have your -// local SSH configuration for git.coopcloud.tech working for this to work -func (r Recipe) Push(dryRun bool) error { - repo, err := git.PlainOpen(r.Dir()) - if err != nil { - return err - } - - if err := gitPkg.CreateRemote(repo, "origin-ssh", r.Meta.SSHURL, dryRun); err != nil { - return err - } - - if err := gitPkg.Push(r.Dir(), "origin-ssh", true, dryRun); err != nil { - return err - } - - return nil -} - -// Dir retrieves the recipe repository path -func (r Recipe) Dir() string { - return path.Join(config.RECIPES_DIR, r.Name) -} - -// UpdateLabel updates a recipe label -func (r Recipe) UpdateLabel(pattern, serviceName, label string) error { - fullPattern := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, r.Name, pattern) - if err := compose.UpdateLabel(fullPattern, serviceName, label, r.Name); err != nil { - return err - } - return nil -} - -// UpdateTag updates a recipe tag -func (r Recipe) UpdateTag(image, tag string) (bool, error) { - pattern := fmt.Sprintf("%s/%s/compose**yml", config.RECIPES_DIR, r.Name) - - image = formatter.StripTagMeta(image) - - ok, err := compose.UpdateTag(pattern, image, tag, r.Name) - if err != nil { - return false, err - } - - return ok, nil -} - -// Tags list the recipe tags -func (r Recipe) Tags() ([]string, error) { - var tags []string - - repo, err := git.PlainOpen(r.Dir()) - if err != nil { - return tags, err - } - - gitTags, err := repo.Tags() - if err != nil { - return tags, err - } - - if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) { - tags = append(tags, strings.TrimPrefix(string(ref.Name()), "refs/tags/")) - return nil - }); err != nil { - return tags, err - } - - log.Debugf("detected %s as tags for recipe %s", strings.Join(tags, ", "), r.Name) - - return tags, nil -} - -// Get retrieves a recipe. -func Get(recipeName string, offline bool) (Recipe, error) { - if err := EnsureExists(recipeName); err != nil { - return Recipe{}, err - } - - pattern := fmt.Sprintf("%s/%s/compose**yml", config.RECIPES_DIR, recipeName) - composeFiles, err := filepath.Glob(pattern) - if err != nil { - return Recipe{}, err - } - - if len(composeFiles) == 0 { - return Recipe{}, fmt.Errorf("%s is missing a compose.yml or compose.*.yml file?", recipeName) - } - - envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample") - sampleEnv, err := envfile.ReadEnv(envSamplePath) - if err != nil { - return Recipe{}, err - } - - opts := stack.Deploy{Composefiles: composeFiles} - config, err := loader.LoadComposefile(opts, sampleEnv) - if err != nil { - return Recipe{}, err - } - - meta, err := GetRecipeMeta(recipeName, offline) - if err != nil { - switch err.(type) { - case RecipeMissingFromCatalogue: - meta = RecipeMeta{} - default: - return Recipe{}, err - } - } - - return Recipe{ - Name: recipeName, - Config: config, - Meta: meta, - }, nil -} - -func (r Recipe) SampleEnv() (map[string]string, error) { - envSamplePath := path.Join(config.RECIPES_DIR, r.Name, ".env.sample") - sampleEnv, err := envfile.ReadEnv(envSamplePath) - if err != nil { - return sampleEnv, fmt.Errorf("unable to discover .env.sample for %s", r.Name) - } - return sampleEnv, nil -} - -// Ensure makes sure the recipe exists, is up to date and has the latest version checked out. -func Ensure(recipeName string) error { - if err := EnsureExists(recipeName); err != nil { - return err - } - if err := EnsureUpToDate(recipeName); err != nil { - return err - } - if err := EnsureLatest(recipeName); err != nil { - return err - } - return nil -} - -// EnsureExists ensures that a recipe is locally cloned -func EnsureExists(recipeName string) error { - recipeDir := path.Join(config.RECIPES_DIR, recipeName) - - if _, err := os.Stat(recipeDir); os.IsNotExist(err) { - log.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 { - return err - } - } - - if err := gitPkg.EnsureGitRepo(recipeDir); err != nil { - return err - } - - return nil -} - -// EnsureVersion checks whether a specific version exists for a recipe. -func EnsureVersion(recipeName, version string) error { - recipeDir := path.Join(config.RECIPES_DIR, recipeName) - - if err := gitPkg.EnsureGitRepo(recipeDir); err != nil { - return err - } - - repo, err := git.PlainOpen(recipeDir) - if err != nil { - return err - } - - tags, err := repo.Tags() - if err != nil { - return nil - } - - var parsedTags []string - var tagRef plumbing.ReferenceName - if err := tags.ForEach(func(ref *plumbing.Reference) (err error) { - parsedTags = append(parsedTags, ref.Name().Short()) - if ref.Name().Short() == version { - tagRef = ref.Name() - } - return nil - }); err != nil { - return err - } - - joinedTags := strings.Join(parsedTags, ", ") - if joinedTags != "" { - log.Debugf("read %s as tags for recipe %s", joinedTags, recipeName) - } - - if tagRef.String() == "" { - return fmt.Errorf("the local copy of %s doesn't seem to have version %s available?", recipeName, version) - } - - worktree, err := repo.Worktree() - if err != nil { - return err - } - - opts := &git.CheckoutOptions{ - Branch: tagRef, - Create: false, - Force: true, - } - if err := worktree.Checkout(opts); err != nil { - return err - } - - log.Debugf("successfully checked %s out to %s in %s", recipeName, tagRef.Short(), recipeDir) - - return nil -} - -// EnsureIsClean makes sure that the recipe repository has no unstaged changes. -func EnsureIsClean(recipeName string) error { - recipeDir := path.Join(config.RECIPES_DIR, recipeName) - - isClean, err := gitPkg.IsClean(recipeDir) - if err != nil { - return fmt.Errorf("unable to check git clean status in %s: %s", recipeDir, err) - } - - if !isClean { - msg := "%s (%s) has locally unstaged changes? please commit/remove your changes before proceeding" - return fmt.Errorf(msg, recipeName, recipeDir) - } - - return nil -} - -// EnsureLatest makes sure the latest commit is checked out for a local recipe repository -func EnsureLatest(recipeName string) error { - recipeDir := path.Join(config.RECIPES_DIR, recipeName) - - if err := gitPkg.EnsureGitRepo(recipeDir); err != nil { - return err - } - - repo, err := git.PlainOpen(recipeDir) - if err != nil { - return err - } - - worktree, err := repo.Worktree() - if err != nil { - return err - } - - branch, err := gitPkg.GetDefaultBranch(repo, recipeDir) - if err != nil { - return err - } - - checkOutOpts := &git.CheckoutOptions{ - Create: false, - Force: true, - Branch: plumbing.ReferenceName(branch), - } - - if err := worktree.Checkout(checkOutOpts); err != nil { - log.Debugf("failed to check out %s in %s", branch, recipeDir) - return err - } - - return nil -} - -// ChaosVersion constructs a chaos mode recipe version. -func ChaosVersion(recipeName string) (string, error) { - var version string - - head, err := gitPkg.GetRecipeHead(recipeName) - if err != nil { - return version, err - } - - version = formatter.SmallSHA(head.String()) - - recipeDir := path.Join(config.RECIPES_DIR, recipeName) - isClean, err := gitPkg.IsClean(recipeDir) - if err != nil { - return version, err - } - - if !isClean { - version = fmt.Sprintf("%s + unstaged changes", version) - } - - return version, nil + ComposePath string + ReadmePath string + SampleEnvPath string + AbraShPath string } // GetRecipesLocal retrieves all local recipe directories @@ -446,41 +159,20 @@ func GetRecipesLocal() ([]string, error) { return recipes, nil } -// GetVersionLabelLocal retrieves the version label on the local recipe config -func GetVersionLabelLocal(recipe Recipe) (string, error) { - var label string - - for _, service := range recipe.Config.Services { - for label, value := range service.Deploy.Labels { - if strings.HasPrefix(label, "coop-cloud") && strings.Contains(label, "version") { - return value, nil - } - } - } - - if label == "" { - return label, fmt.Errorf("%s has no version label? try running \"abra recipe sync %s\" first?", recipe.Name, recipe.Name) - } - - return label, nil -} - -func GetRecipeFeaturesAndCategory(recipeName string) (Features, string, error) { +func GetRecipeFeaturesAndCategory(r Recipe) (Features, string, error) { feat := Features{} var category string - readmePath := path.Join(config.RECIPES_DIR, recipeName, "README.md") + log.Debugf("attempting to open %s for recipe metadata parsing", r.ReadmePath) - log.Debugf("attempting to open %s for recipe metadata parsing", readmePath) - - readmeFS, err := ioutil.ReadFile(readmePath) + readmeFS, err := ioutil.ReadFile(r.ReadmePath) if err != nil { return feat, category, err } readmeMetadata, err := GetStringInBetween( // Find text between delimiters - recipeName, + r.Name, string(readmeFS), "", "", ) @@ -531,7 +223,7 @@ func GetRecipeFeaturesAndCategory(recipeName string) (Features, string, error) { if strings.Contains(val, "**Image**") { imageMetadata, err := GetImageMetadata(strings.TrimSpace( strings.TrimPrefix(val, "* **Image**:"), - ), recipeName) + ), r.Name) if err != nil { continue } @@ -597,59 +289,6 @@ func GetStringInBetween(recipeName, str, start, end string) (result string, err return str[s : s+e], nil } -// EnsureUpToDate ensures that the local repo is synced to the remote -func EnsureUpToDate(recipeName string) error { - recipeDir := path.Join(config.RECIPES_DIR, recipeName) - - repo, err := git.PlainOpen(recipeDir) - if err != nil { - return fmt.Errorf("unable to open %s: %s", recipeDir, err) - } - - remotes, err := repo.Remotes() - if err != nil { - return fmt.Errorf("unable to read remotes in %s: %s", recipeDir, err) - } - - if len(remotes) == 0 { - log.Debugf("cannot ensure %s is up-to-date, no git remotes configured", recipeName) - return nil - } - - worktree, err := repo.Worktree() - if err != nil { - return fmt.Errorf("unable to open git work tree in %s: %s", recipeDir, err) - } - - branch, err := gitPkg.CheckoutDefaultBranch(repo, recipeDir) - if err != nil { - return fmt.Errorf("unable to check out default branch in %s: %s", recipeDir, err) - } - - fetchOpts := &git.FetchOptions{Tags: git.AllTags} - if err := repo.Fetch(fetchOpts); err != nil { - if !strings.Contains(err.Error(), "already up-to-date") { - return fmt.Errorf("unable to fetch tags in %s: %s", recipeDir, err) - } - } - - opts := &git.PullOptions{ - Force: true, - ReferenceName: branch, - SingleBranch: true, - } - - if err := worktree.Pull(opts); err != nil { - if !strings.Contains(err.Error(), "already up-to-date") { - return fmt.Errorf("unable to git pull in %s: %s", recipeDir, err) - } - } - - log.Debugf("fetched latest git changes for %s", recipeName) - - return nil -} - // ReadRecipeCatalogue reads the recipe catalogue. func ReadRecipeCatalogue(offline bool) (RecipeCatalogue, error) { recipes := make(RecipeCatalogue) @@ -864,96 +503,6 @@ func ReadReposMetadata() (RepoCatalogue, error) { return reposMeta, nil } -// GetRecipeVersions retrieves all recipe versions. -func GetRecipeVersions(recipeName string, offline bool) (RecipeVersions, error) { - versions := RecipeVersions{} - recipeDir := path.Join(config.RECIPES_DIR, recipeName) - - log.Debugf("attempting to open git repository in %s", recipeDir) - - repo, err := git.PlainOpen(recipeDir) - if err != nil { - return versions, err - } - - worktree, err := repo.Worktree() - if err != nil { - return versions, err - } - - gitTags, err := repo.Tags() - if err != nil { - return versions, err - } - - if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) { - tag := strings.TrimPrefix(string(ref.Name()), "refs/tags/") - - log.Debugf("processing %s for %s", tag, recipeName) - - checkOutOpts := &git.CheckoutOptions{ - Create: false, - Force: true, - Branch: plumbing.ReferenceName(ref.Name()), - } - if err := worktree.Checkout(checkOutOpts); err != nil { - log.Debugf("failed to check out %s in %s", tag, recipeDir) - return err - } - - log.Debugf("successfully checked out %s in %s", ref.Name(), recipeDir) - - recipe, err := Get(recipeName, offline) - if err != nil { - return err - } - - versionMeta := make(map[string]ServiceMeta) - for _, service := range recipe.Config.Services { - - img, err := reference.ParseNormalizedNamed(service.Image) - if err != nil { - return err - } - - path := reference.Path(img) - - path = formatter.StripTagMeta(path) - - var tag string - switch img.(type) { - case reference.NamedTagged: - tag = img.(reference.NamedTagged).Tag() - case reference.Named: - log.Warnf("%s service is missing image tag?", path) - continue - } - - versionMeta[service.Name] = ServiceMeta{ - Image: path, - Tag: tag, - } - } - - versions = append(versions, map[string]map[string]ServiceMeta{tag: versionMeta}) - - return nil - }); err != nil { - return versions, err - } - - _, err = gitPkg.CheckoutDefaultBranch(repo, recipeDir) - if err != nil { - return versions, err - } - - sortRecipeVersions(versions) - - log.Debugf("collected %s for %s", versions, recipeName) - - return versions, nil -} - // sortRecipeVersions sorts the recipe semver versions func sortRecipeVersions(versions RecipeVersions) { sort.Slice(versions, func(i, j int) bool { @@ -1034,8 +583,7 @@ func UpdateRepositories(repos RepoCatalogue, recipeName string) error { return } - recipeDir := path.Join(config.RECIPES_DIR, rm.Name) - if err := gitPkg.Clone(recipeDir, rm.CloneURL); err != nil { + if err := gitPkg.Clone(Get(rm.Name).Dir, rm.CloneURL); err != nil { log.Fatal(err) } @@ -1063,50 +611,3 @@ func ensurePathExists(path string) error { } return nil } - -// GetComposeFiles gets the list of compose files for an app (or recipe if you -// don't already have an app) which should be merged into a composetypes.Config -// while respecting the COMPOSE_FILE env var. -func GetComposeFiles(recipe string, appEnv map[string]string) ([]string, error) { - var composeFiles []string - - composeFileEnvVar, ok := appEnv["COMPOSE_FILE"] - if !ok { - path := fmt.Sprintf("%s/%s/compose.yml", config.RECIPES_DIR, recipe) - if err := ensurePathExists(path); err != nil { - return composeFiles, err - } - log.Debugf("no COMPOSE_FILE detected, loading default: %s", path) - composeFiles = append(composeFiles, path) - return composeFiles, nil - } - - if !strings.Contains(composeFileEnvVar, ":") { - path := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, recipe, composeFileEnvVar) - if err := ensurePathExists(path); err != nil { - return composeFiles, err - } - log.Debugf("COMPOSE_FILE detected, loading %s", path) - composeFiles = append(composeFiles, path) - return composeFiles, nil - } - - numComposeFiles := strings.Count(composeFileEnvVar, ":") + 1 - envVars := strings.SplitN(composeFileEnvVar, ":", numComposeFiles) - if len(envVars) != numComposeFiles { - return composeFiles, fmt.Errorf("COMPOSE_FILE (=\"%s\") parsing failed?", composeFileEnvVar) - } - - for _, file := range envVars { - path := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, recipe, file) - if err := ensurePathExists(path); err != nil { - return composeFiles, err - } - composeFiles = append(composeFiles, path) - } - - log.Debugf("COMPOSE_FILE detected (%s), loading %s", composeFileEnvVar, strings.Join(envVars, ", ")) - log.Debugf("retrieved %s configs for %s", strings.Join(composeFiles, ", "), recipe) - - return composeFiles, nil -} diff --git a/pkg/recipe/recipe_test.go b/pkg/recipe/recipe_test.go index 01b0f093..f73d613b 100644 --- a/pkg/recipe/recipe_test.go +++ b/pkg/recipe/recipe_test.go @@ -14,13 +14,14 @@ func TestGetVersionLabelLocalDoesNotUseTimeoutLabel(t *testing.T) { t.Fatal(err) } - r, err := Get("traefik", offline) + r := Get("traefik") + err = r.EnsureExists() if err != nil { t.Fatal(err) } for i := 1; i < 1000; i++ { - label, err := GetVersionLabelLocal(r) + label, err := r.GetVersionLabelLocal() if err != nil { t.Fatal(err) } diff --git a/pkg/secret/secret.go b/pkg/secret/secret.go index fd69508a..d320816b 100644 --- a/pkg/secret/secret.go +++ b/pkg/secret/secret.go @@ -246,7 +246,8 @@ type secretStatuses []secretStatus func PollSecretsStatus(cl *dockerClient.Client, app appPkg.App) (secretStatuses, error) { var secStats secretStatuses - composeFiles, err := recipe.GetComposeFiles(app.Recipe, app.Env) + r := recipe.Get(app.Name) + composeFiles, err := r.GetComposeFiles(app.Env) if err != nil { return secStats, err } diff --git a/pkg/test/test.go b/pkg/test/test.go index a9f2593a..1109a5dd 100644 --- a/pkg/test/test.go +++ b/pkg/test/test.go @@ -7,6 +7,7 @@ import ( appPkg "coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/envfile" "coopcloud.tech/abra/pkg/log" + "coopcloud.tech/abra/pkg/recipe" ) var ( @@ -32,7 +33,7 @@ var ExpectedAppEnv = envfile.AppEnv{ var ExpectedApp = appPkg.App{ Name: AppName, - Recipe: ExpectedAppEnv["RECIPE"], + Recipe: recipe.Get(ExpectedAppEnv["RECIPE"]), Domain: ExpectedAppEnv["DOMAIN"], Env: ExpectedAppEnv, Path: ExpectedAppFile.Path,