refactor(recipe): create a recipe struct that gets used everywhere #430
All checks were successful
continuous-integration/drone/push Build is passing

Reviewed-on: coop-cloud/abra#430
This commit is contained in:
p4u1 2024-07-08 12:18:58 +00:00
commit 9cd1fe658b
38 changed files with 882 additions and 1047 deletions

View File

@ -7,7 +7,6 @@ import (
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -47,26 +46,10 @@ var appBackupListCommand = cli.Command{
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) 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) 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) cl, err := client.New(app.Server)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -110,22 +93,22 @@ var appBackupDownloadCommand = cli.Command{
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
if err := recipe.EnsureExists(app.Recipe); err != nil { if err := app.Recipe.EnsureExists(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
if !internal.Chaos { if !internal.Chaos {
if err := recipe.EnsureIsClean(app.Recipe); err != nil { if err := app.Recipe.EnsureIsClean(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
if !internal.Offline { if !internal.Offline {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil { if err := app.Recipe.EnsureUpToDate(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
if err := recipe.EnsureLatest(app.Recipe); err != nil { if err := app.Recipe.EnsureLatest(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
@ -180,22 +163,22 @@ var appBackupCreateCommand = cli.Command{
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
if err := recipe.EnsureExists(app.Recipe); err != nil { if err := app.Recipe.EnsureExists(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
if !internal.Chaos { if !internal.Chaos {
if err := recipe.EnsureIsClean(app.Recipe); err != nil { if err := app.Recipe.EnsureIsClean(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
if !internal.Offline { if !internal.Offline {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil { if err := app.Recipe.EnsureUpToDate(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
if err := recipe.EnsureLatest(app.Recipe); err != nil { if err := app.Recipe.EnsureLatest(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
@ -238,22 +221,22 @@ var appBackupSnapshotsCommand = cli.Command{
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
if err := recipe.EnsureExists(app.Recipe); err != nil { if err := app.Recipe.EnsureExists(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
if !internal.Chaos { if !internal.Chaos {
if err := recipe.EnsureIsClean(app.Recipe); err != nil { if err := app.Recipe.EnsureIsClean(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
if !internal.Offline { if !internal.Offline {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil { if err := app.Recipe.EnsureUpToDate(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
if err := recipe.EnsureLatest(app.Recipe); err != nil { if err := app.Recipe.EnsureLatest(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }

View File

@ -6,8 +6,6 @@ import (
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -38,26 +36,10 @@ ${FOO:<default>} syntax). "check" does not confirm or deny this for you.`,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) 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) 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"} tableCol := []string{"recipe env sample", "app env"}
table := formatter.CreateTable(tableCol) table := formatter.CreateTable(tableCol)

View File

@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"path"
"sort" "sort"
"strings" "strings"
@ -14,10 +13,8 @@ import (
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -61,36 +58,19 @@ Example:
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) 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) 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 != "" { if internal.LocalCmd && internal.RemoteUser != "" {
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use --local & --user together")) internal.ShowSubcommandHelpAndError(c, errors.New("cannot use --local & --user together"))
} }
hasCmdArgs, parsedCmdArgs := parseCmdArgs(c.Args(), internal.LocalCmd) hasCmdArgs, parsedCmdArgs := parseCmdArgs(c.Args(), internal.LocalCmd)
abraSh := path.Join(config.RECIPES_DIR, app.Recipe, "abra.sh") if _, err := os.Stat(app.Recipe.AbraShPath); err != nil {
if _, err := os.Stat(abraSh); err != nil {
if os.IsNotExist(err) { 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) log.Fatal(err)
} }
@ -101,7 +81,7 @@ Example:
} }
cmdName := c.Args().Get(1) 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) log.Fatal(err)
} }
@ -115,10 +95,10 @@ Example:
var sourceAndExec string var sourceAndExec string
if hasCmdArgs { if hasCmdArgs {
log.Debugf("parsed following command arguments: %s", parsedCmdArgs) 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 { } else {
log.Debug("did not detect any command arguments") 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" shell := "/bin/bash"
@ -139,7 +119,7 @@ Example:
targetServiceName := c.Args().Get(1) targetServiceName := c.Args().Get(1)
cmdName := c.Args().Get(2) 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) log.Fatal(err)
} }
@ -172,7 +152,7 @@ Example:
log.Fatal(err) 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) log.Fatal(err)
} }
} }
@ -228,23 +208,24 @@ var appCmdListCommand = cli.Command{
Before: internal.SubCommandBefore, Before: internal.SubCommandBefore,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) 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) log.Fatal(err)
} }
if !internal.Chaos { if !internal.Chaos {
if err := recipePkg.EnsureIsClean(app.Recipe); err != nil { if err := r.EnsureIsClean(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
if !internal.Offline { if !internal.Offline {
if err := recipePkg.EnsureUpToDate(app.Recipe); err != nil { if err := r.EnsureUpToDate(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
if err := recipePkg.EnsureLatest(app.Recipe); err != nil { if err := r.EnsureLatest(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
@ -263,8 +244,7 @@ var appCmdListCommand = cli.Command{
} }
func getShCmdNames(app appPkg.App) ([]string, error) { func getShCmdNames(app appPkg.App) ([]string, error) {
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh") cmdNames, err := appPkg.ReadAbraShCmdNames(app.Recipe.AbraShPath)
cmdNames, err := appPkg.ReadAbraShCmdNames(abraShPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -2,7 +2,6 @@ package app
import ( import (
"context" "context"
"fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
@ -11,7 +10,6 @@ import (
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/dns" "coopcloud.tech/abra/pkg/dns"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/git" "coopcloud.tech/abra/pkg/git"
@ -58,32 +56,11 @@ recipes.
log.Fatal("cannot use <version> and --chaos together") log.Fatal("cannot use <version> 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) log.Fatal(err)
} }
if !internal.Chaos { if err := lint.LintForErrors(app.Recipe); err != nil {
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 {
log.Fatal(err) log.Fatal(err)
} }
@ -107,7 +84,7 @@ recipes.
if specificVersion != "" { if specificVersion != "" {
version = specificVersion version = specificVersion
log.Debugf("choosing %s as version to deploy", version) 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) log.Fatal(err)
} }
} }
@ -136,14 +113,14 @@ recipes.
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl) versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe.Name, catl)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
if len(versions) == 0 && !internal.Chaos { if len(versions) == 0 && !internal.Chaos {
log.Warn("no published versions in catalogue, trying local recipe repository") 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 { if err != nil {
log.Warn(err) log.Warn(err)
} }
@ -157,11 +134,11 @@ recipes.
if len(versions) > 0 && !internal.Chaos { if len(versions) > 0 && !internal.Chaos {
version = versions[len(versions)-1] version = versions[len(versions)-1]
log.Debugf("choosing %s as version to deploy", version) 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) log.Fatal(err)
} }
} else { } else {
head, err := git.GetRecipeHead(app.Recipe) head, err := git.GetRecipeHead(app.Recipe.Name)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -173,14 +150,13 @@ recipes.
if internal.Chaos { if internal.Chaos {
log.Warnf("chaos mode engaged") log.Warnf("chaos mode engaged")
var err error var err error
version, err = recipe.ChaosVersion(app.Recipe) version, err = app.Recipe.ChaosVersion()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh") abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath)
abraShEnv, err := envfile.ReadAbraShEnvVars(abraShPath)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -188,7 +164,7 @@ recipes.
app.Env[k] = v app.Env[k] = v
} }
composeFiles, err := recipe.GetComposeFiles(app.Recipe, app.Env) composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -206,7 +182,7 @@ recipes.
} }
appPkg.ExposeAllEnv(stackName, compose, app.Env) 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.SetChaosLabel(compose, stackName, internal.Chaos)
appPkg.SetChaosVersionLabel(compose, stackName, version) appPkg.SetChaosVersionLabel(compose, stackName, version)
appPkg.SetUpdateLabel(compose, stackName, app.Env) appPkg.SetUpdateLabel(compose, stackName, app.Env)

View File

@ -16,28 +16,34 @@ import (
"github.com/urfave/cli" "github.com/urfave/cli"
) )
var status bool var (
var statusFlag = &cli.BoolFlag{ status bool
Name: "status, S", statusFlag = &cli.BoolFlag{
Usage: "Show app deployment status", Name: "status, S",
Destination: &status, Usage: "Show app deployment status",
} Destination: &status,
}
)
var recipeFilter string var (
var recipeFlag = &cli.StringFlag{ recipeFilter string
Name: "recipe, r", recipeFlag = &cli.StringFlag{
Value: "", Name: "recipe, r",
Usage: "Show apps of a specific recipe", Value: "",
Destination: &recipeFilter, Usage: "Show apps of a specific recipe",
} Destination: &recipeFilter,
}
)
var listAppServer string var (
var listAppServerFlag = &cli.StringFlag{ listAppServer string
Name: "server, s", listAppServerFlag = &cli.StringFlag{
Value: "", Name: "server, s",
Usage: "Show apps of a specific server", Value: "",
Destination: &listAppServer, Usage: "Show apps of a specific server",
} Destination: &listAppServer,
}
)
type appStatus struct { type appStatus struct {
Server string `json:"server"` 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 != "" { if recipeFilter != "" {
// only count server if matches filter // only count server if matches filter
totalServersCount++ totalServersCount++
@ -177,7 +183,7 @@ can take some time.
var newUpdates []string var newUpdates []string
if version != "unknown" { if version != "unknown" {
updates, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl) updates, err := recipe.GetRecipeCatalogueVersions(app.Recipe.Name, catl)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -214,7 +220,7 @@ can take some time.
} }
appStats.Server = app.Server appStats.Server = app.Server
appStats.Recipe = app.Recipe appStats.Recipe = app.Recipe.Name
appStats.AppName = app.Name appStats.AppName = app.Name
appStats.Domain = app.Domain appStats.Domain = app.Domain

View File

@ -13,7 +13,6 @@ import (
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
containerTypes "github.com/docker/docker/api/types/container" containerTypes "github.com/docker/docker/api/types/container"
@ -39,7 +38,7 @@ var appLogsCommand = cli.Command{
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
stackName := app.StackName() stackName := app.StackName()
if err := recipe.EnsureExists(app.Recipe); err != nil { if err := app.Recipe.EnsureExists(); err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@ -2,7 +2,6 @@ package app
import ( import (
"fmt" "fmt"
"path"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
@ -69,18 +68,18 @@ var appNewCommand = cli.Command{
recipe := internal.ValidateRecipe(c) recipe := internal.ValidateRecipe(c)
if !internal.Chaos { if !internal.Chaos {
if err := recipePkg.EnsureIsClean(recipe.Name); err != nil { if err := recipe.EnsureIsClean(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
if !internal.Offline { if !internal.Offline {
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil { if err := recipe.EnsureUpToDate(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
if c.Args().Get(1) == "" { if c.Args().Get(1) == "" {
var version string var version string
recipeVersions, err := recipePkg.GetRecipeVersions(recipe.Name, internal.Offline) recipeVersions, err := recipe.GetRecipeVersions(internal.Offline)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -93,16 +92,16 @@ var appNewCommand = cli.Command{
version = tag version = tag
} }
if err := recipePkg.EnsureVersion(recipe.Name, version); err != nil { if err := recipe.EnsureVersion(version); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} else { } else {
if err := recipePkg.EnsureLatest(recipe.Name); err != nil { if err := recipe.EnsureLatest(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
} else { } 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) log.Fatal(err)
} }
} }
@ -120,7 +119,7 @@ var appNewCommand = cli.Command{
log.Debugf("%s sanitised as %s for new app", internal.Domain, sanitisedAppName) log.Debugf("%s sanitised as %s for new app", internal.Domain, sanitisedAppName)
if err := appPkg.TemplateAppEnvSample( if err := appPkg.TemplateAppEnvSample(
recipe.Name, recipe,
internal.Domain, internal.Domain,
internal.NewAppServer, internal.NewAppServer,
internal.Domain, internal.Domain,
@ -136,13 +135,12 @@ var appNewCommand = cli.Command{
log.Fatal(err) log.Fatal(err)
} }
composeFiles, err := recipePkg.GetComposeFiles(recipe.Name, sampleEnv) composeFiles, err := recipe.GetComposeFiles(sampleEnv)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
envSamplePath := path.Join(config.RECIPES_DIR, recipe.Name, ".env.sample") secretsConfig, err := secret.ReadSecretsConfig(recipe.SampleEnvPath, composeFiles, appPkg.StackName(internal.Domain))
secretsConfig, err := secret.ReadSecretsConfig(envSamplePath, composeFiles, appPkg.StackName(internal.Domain))
if err != nil { if err != nil {
return err return err
} }

View File

@ -53,7 +53,7 @@ var appPsCommand = cli.Command{
statuses, err := appPkg.GetAppStatuses([]appPkg.App{app}, true) statuses, err := appPkg.GetAppStatuses([]appPkg.App{app}, true)
if statusMeta, ok := statuses[app.StackName()]; ok { if statusMeta, ok := statuses[app.StackName()]; ok {
if _, exists := statusMeta["chaos"]; !exists { 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) log.Fatal(err)
} }
} }
@ -67,7 +67,8 @@ var appPsCommand = cli.Command{
// showPSOutput renders ps output. // showPSOutput renders ps output.
func showPSOutput(app appPkg.App, cl *dockerClient.Client) { 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 { if err != nil {
log.Fatal(err) log.Fatal(err)
return return

View File

@ -7,7 +7,6 @@ import (
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -33,26 +32,10 @@ var appRestoreCommand = cli.Command{
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) 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) 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) cl, err := client.New(app.Server)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)

View File

@ -6,7 +6,6 @@ import (
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile" "coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
@ -58,32 +57,11 @@ recipes.
log.Fatal("cannot use <version> and --chaos together") log.Fatal("cannot use <version> 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) log.Fatal(err)
} }
if !internal.Chaos { if err := lint.LintForErrors(app.Recipe); err != nil {
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 {
log.Fatal(err) log.Fatal(err)
} }
@ -108,14 +86,14 @@ recipes.
log.Fatal(err) log.Fatal(err)
} }
versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl) versions, err := recipe.GetRecipeCatalogueVersions(app.Recipe.Name, catl)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
if len(versions) == 0 && !internal.Chaos { if len(versions) == 0 && !internal.Chaos {
log.Warn("no published versions in catalogue, trying local recipe repository") 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 { if err != nil {
log.Warn(err) log.Warn(err)
} }
@ -185,7 +163,7 @@ recipes.
} }
if !internal.Chaos { if !internal.Chaos {
if err := recipe.EnsureVersion(app.Recipe, chosenDowngrade); err != nil { if err := app.Recipe.EnsureVersion(chosenDowngrade); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
@ -193,14 +171,13 @@ recipes.
if internal.Chaos { if internal.Chaos {
log.Warn("chaos mode engaged") log.Warn("chaos mode engaged")
var err error var err error
chosenDowngrade, err = recipe.ChaosVersion(app.Recipe) chosenDowngrade, err = app.Recipe.ChaosVersion()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh") abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath)
abraShEnv, err := envfile.ReadAbraShEnvVars(abraShPath)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -208,7 +185,7 @@ recipes.
app.Env[k] = v app.Env[k] = v
} }
composeFiles, err := recipe.GetComposeFiles(app.Recipe, app.Env) composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -224,7 +201,7 @@ recipes.
log.Fatal(err) log.Fatal(err)
} }
appPkg.ExposeAllEnv(stackName, compose, app.Env) 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.SetChaosLabel(compose, stackName, internal.Chaos)
appPkg.SetChaosVersionLabel(compose, stackName, chosenDowngrade) appPkg.SetChaosVersionLabel(compose, stackName, chosenDowngrade)
appPkg.SetUpdateLabel(compose, stackName, app.Env) appPkg.SetUpdateLabel(compose, stackName, app.Env)

View File

@ -14,7 +14,6 @@ import (
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/secret" "coopcloud.tech/abra/pkg/secret"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
dockerClient "github.com/docker/docker/client" dockerClient "github.com/docker/docker/client"
@ -57,26 +56,10 @@ var appSecretGenerateCommand = cli.Command{
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) 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) 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 { if len(c.Args()) == 1 && !allSecrets {
err := errors.New("missing arguments <secret>/<version> or '--all'") err := errors.New("missing arguments <secret>/<version> or '--all'")
internal.ShowSubcommandHelpAndError(c, err) internal.ShowSubcommandHelpAndError(c, err)
@ -87,7 +70,7 @@ var appSecretGenerateCommand = cli.Command{
internal.ShowSubcommandHelpAndError(c, err) internal.ShowSubcommandHelpAndError(c, err)
} }
composeFiles, err := recipe.GetComposeFiles(app.Recipe, app.Env) composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -264,27 +247,27 @@ Example:
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
if err := recipe.EnsureExists(app.Recipe); err != nil { if err := app.Recipe.EnsureExists(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
if !internal.Chaos { if !internal.Chaos {
if err := recipe.EnsureIsClean(app.Recipe); err != nil { if err := app.Recipe.EnsureIsClean(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
if !internal.Offline { if !internal.Offline {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil { if err := app.Recipe.EnsureUpToDate(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
if err := recipe.EnsureLatest(app.Recipe); err != nil { if err := app.Recipe.EnsureLatest(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
composeFiles, err := recipe.GetComposeFiles(app.Recipe, app.Env) composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -372,22 +355,22 @@ var appSecretLsCommand = cli.Command{
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
if err := recipe.EnsureExists(app.Recipe); err != nil { if err := app.Recipe.EnsureExists(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
if !internal.Chaos { if !internal.Chaos {
if err := recipe.EnsureIsClean(app.Recipe); err != nil { if err := app.Recipe.EnsureIsClean(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
if !internal.Offline { if !internal.Offline {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil { if err := app.Recipe.EnsureUpToDate(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
if err := recipe.EnsureLatest(app.Recipe); err != nil { if err := app.Recipe.EnsureLatest(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }

View File

@ -8,11 +8,9 @@ import (
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile" "coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
@ -64,28 +62,11 @@ recipes.
log.Fatal("cannot use <version> and --chaos together") log.Fatal("cannot use <version> and --chaos together")
} }
if !internal.Chaos { if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
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 {
log.Fatal(err) log.Fatal(err)
} }
if err := lint.LintForErrors(recipe); err != nil { if err := lint.LintForErrors(app.Recipe); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -110,14 +91,14 @@ recipes.
log.Fatal(err) log.Fatal(err)
} }
versions, err := recipePkg.GetRecipeCatalogueVersions(app.Recipe, catl) versions, err := recipePkg.GetRecipeCatalogueVersions(app.Recipe.Name, catl)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
if len(versions) == 0 && !internal.Chaos { if len(versions) == 0 && !internal.Chaos {
log.Warn("no published versions in catalogue, trying local recipe repository") 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 { if err != nil {
log.Warn(err) log.Warn(err)
} }
@ -207,7 +188,7 @@ recipes.
log.Fatal(err) log.Fatal(err)
} }
if parsedVersion.IsGreaterThan(parsedDeployedVersion) && parsedVersion.IsLessThan(parsedChosenUpgrade) { if parsedVersion.IsGreaterThan(parsedDeployedVersion) && parsedVersion.IsLessThan(parsedChosenUpgrade) {
note, err := internal.GetReleaseNotes(app.Recipe, version) note, err := app.Recipe.GetReleaseNotes(version)
if err != nil { if err != nil {
return err return err
} }
@ -219,7 +200,7 @@ recipes.
} }
if !internal.Chaos { if !internal.Chaos {
if err := recipePkg.EnsureVersion(app.Recipe, chosenUpgrade); err != nil { if err := app.Recipe.EnsureVersion(chosenUpgrade); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
@ -227,14 +208,13 @@ recipes.
if internal.Chaos { if internal.Chaos {
log.Warn("chaos mode engaged") log.Warn("chaos mode engaged")
var err error var err error
chosenUpgrade, err = recipePkg.ChaosVersion(app.Recipe) chosenUpgrade, err = app.Recipe.ChaosVersion()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, app.Recipe, "abra.sh") abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath)
abraShEnv, err := envfile.ReadAbraShEnvVars(abraShPath)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -242,7 +222,7 @@ recipes.
app.Env[k] = v app.Env[k] = v
} }
composeFiles, err := recipePkg.GetComposeFiles(app.Recipe, app.Env) composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -258,7 +238,7 @@ recipes.
log.Fatal(err) log.Fatal(err)
} }
appPkg.ExposeAllEnv(stackName, compose, app.Env) 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.SetChaosLabel(compose, stackName, internal.Chaos)
appPkg.SetChaosVersionLabel(compose, stackName, chosenUpgrade) appPkg.SetChaosVersionLabel(compose, stackName, chosenUpgrade)
appPkg.SetUpdateLabel(compose, stackName, app.Env) appPkg.SetUpdateLabel(compose, stackName, app.Env)

View File

@ -78,7 +78,7 @@ var appVersionCommand = cli.Command{
log.Fatalf("failed to determine version of deployed %s", app.Name) 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 { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@ -57,6 +57,7 @@ keys configured on your account.
BashComplete: autocomplete.RecipeNameComplete, BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
recipeName := c.Args().First() recipeName := c.Args().First()
r := recipe.Get(recipeName)
if recipeName != "" { if recipeName != "" {
internal.ValidateRecipe(c) internal.ValidateRecipe(c)
@ -98,12 +99,12 @@ keys configured on your account.
continue continue
} }
versions, err := recipe.GetRecipeVersions(recipeMeta.Name, internal.Offline) versions, err := r.GetRecipeVersions(internal.Offline)
if err != nil { if err != nil {
log.Warn(err) log.Warn(err)
} }
features, category, err := recipe.GetRecipeFeaturesAndCategory(recipeMeta.Name) features, category, err := recipe.GetRecipeFeaturesAndCategory(r)
if err != nil { if err != nil {
log.Warn(err) log.Warn(err)
} }

View File

@ -2,13 +2,10 @@ package internal
import ( import (
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path"
"strings" "strings"
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
@ -30,7 +27,7 @@ func NewVersionOverview(app appPkg.App, currentVersion, newVersion, releaseNotes
server = "local" 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() table.Render()
if releaseNotes != "" && newVersion != "" { if releaseNotes != "" && newVersion != "" {
@ -60,34 +57,13 @@ func NewVersionOverview(app appPkg.App, currentVersion, newVersion, releaseNotes
return nil 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 // PostCmds parses a string of commands and executes them inside of the respective services
// the commands string must have the following format: // the commands string must have the following format:
// "<service> <command> <arguments>|<service> <command> <arguments>|... " // "<service> <command> <arguments>|<service> <command> <arguments>|... "
func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error { 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(app.Recipe.AbraShPath); err != nil {
if _, err := os.Stat(abraSh); err != nil {
if os.IsNotExist(err) { 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 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) 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 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) log.Debugf("running command %s %s within the context of %s_%s", cmdName, parsedCmdArgs, app.StackName(), targetServiceName)
Tty = true 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 return err
} }
} }
@ -150,7 +126,7 @@ func DeployOverview(app appPkg.App, version, message string) error {
server = "local" 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() table.Render()
if NoInput { if NoInput {

View File

@ -88,7 +88,11 @@ func SetBumpType(bumpType string) {
func GetMainAppImage(recipe recipe.Recipe) (string, error) { func GetMainAppImage(recipe recipe.Recipe) (string, error) {
var path string 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" { if service.Name == "app" {
img, err := reference.ParseNormalizedNamed(service.Image) img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil { if err != nil {

View File

@ -57,7 +57,12 @@ func ValidateRecipe(c *cli.Context) recipe.Recipe {
ShowSubcommandHelpAndError(c, errors.New("no recipe name provided")) 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 err != nil {
if c.Command.Name == "generate" { if c.Command.Name == "generate" {
if strings.Contains(err.Error(), "missing a compose") { if strings.Contains(err.Error(), "missing a compose") {

View File

@ -1,13 +1,11 @@
package recipe package recipe
import ( import (
"path"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
gitPkg "coopcloud.tech/abra/pkg/git" gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -25,13 +23,13 @@ var recipeDiffCommand = cli.Command{
BashComplete: autocomplete.RecipeNameComplete, BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
recipeName := c.Args().First() recipeName := c.Args().First()
r := recipe.Get(recipeName)
if recipeName != "" { if recipeName != "" {
internal.ValidateRecipe(c) internal.ValidateRecipe(c)
} }
recipeDir := path.Join(config.RECIPES_DIR, recipeName) if err := gitPkg.DiffUnstaged(r.Dir); err != nil {
if err := gitPkg.DiffUnstaged(recipeDir); err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@ -24,9 +24,10 @@ var recipeFetchCommand = cli.Command{
BashComplete: autocomplete.RecipeNameComplete, BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
recipeName := c.Args().First() recipeName := c.Args().First()
r := recipe.Get(recipeName)
if recipeName != "" { if recipeName != "" {
internal.ValidateRecipe(c) internal.ValidateRecipe(c)
if err := recipe.Ensure(recipeName); err != nil { if err := r.Ensure(false, false); err != nil {
log.Fatal(err) log.Fatal(err)
} }
return nil return nil
@ -39,7 +40,8 @@ var recipeFetchCommand = cli.Command{
catlBar := formatter.CreateProgressbar(len(catalogue), "fetching latest recipes...") catlBar := formatter.CreateProgressbar(len(catalogue), "fetching latest recipes...")
for recipeName := range catalogue { 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) log.Error(err)
} }
catlBar.Add(1) catlBar.Add(1)

View File

@ -8,7 +8,6 @@ import (
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -29,26 +28,10 @@ var recipeLintCommand = cli.Command{
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c) 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) 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"} tableCol := []string{"ref", "rule", "severity", "satisfied", "skipped", "resolve"}
table := formatter.CreateTable(tableCol) table := formatter.CreateTable(tableCol)

View File

@ -12,6 +12,7 @@ import (
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/git" "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -55,22 +56,22 @@ recipe and domain in the sample environment config).
`, `,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
recipeName := c.Args().First() recipeName := c.Args().First()
r := recipe.Get(recipeName)
if recipeName == "" { if recipeName == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no recipe name provided")) internal.ShowSubcommandHelpAndError(c, errors.New("no recipe name provided"))
} }
directory := path.Join(config.RECIPES_DIR, recipeName) if _, err := os.Stat(r.Dir); !os.IsNotExist(err) {
if _, err := os.Stat(directory); !os.IsNotExist(err) { log.Fatalf("%s recipe directory already exists?", r.Dir)
log.Fatalf("%s recipe directory already exists?", directory)
} }
url := fmt.Sprintf("%s/example.git", config.REPOS_BASE_URL) 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) log.Fatal(err)
} }
gitRepo := path.Join(config.RECIPES_DIR, recipeName, ".git") gitRepo := path.Join(r.Dir, ".git")
if err := os.RemoveAll(gitRepo); err != nil { if err := os.RemoveAll(gitRepo); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -78,11 +79,7 @@ recipe and domain in the sample environment config).
meta := newRecipeMeta(recipeName) meta := newRecipeMeta(recipeName)
toParse := []string{ for _, path := range []string{r.ReadmePath, r.SampleEnvPath} {
path.Join(config.RECIPES_DIR, recipeName, "README.md"),
path.Join(config.RECIPES_DIR, recipeName, ".env.sample"),
}
for _, path := range toParse {
tpl, err := template.ParseFiles(path) tpl, err := template.ParseFiles(path)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -93,14 +90,13 @@ recipe and domain in the sample environment config).
log.Fatal(err) 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) log.Fatal(err)
} }
} }
newGitRepo := path.Join(config.RECIPES_DIR, recipeName) if err := git.Init(r.Dir, true, internal.GitName, internal.GitEmail); err != nil {
if err := git.Init(newGitRepo, true, internal.GitName, internal.GitEmail); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -119,7 +115,7 @@ See "abra recipe -h" for additional recipe maintainer commands.
Happy Hacking! Happy Hacking!
`, recipeName, path.Join(config.RECIPES_DIR, recipeName), recipeName)) `, recipeName, path.Join(r.Dir), recipeName))
return nil return nil
}, },

View File

@ -15,11 +15,11 @@ import (
gitPkg "coopcloud.tech/abra/pkg/git" gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/distribution/reference" "github.com/distribution/reference"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus"
"github.com/urfave/cli" "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 { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
if !isClean { if !isClean {
log.Infof("%s currently has these unstaged changes 👇", recipe.Name) 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) log.Fatal(err)
} }
} }
if len(tags) > 0 { 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 { if err := createReleaseFromPreviousTag(tagString, mainAppVersion, recipe, tags); err != nil {
log.Fatal(err) 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) 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 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(cleanUpErr)
} }
log.Fatal(err) log.Fatal(err)
@ -144,8 +144,12 @@ your SSH keys configured on your account.
func getImageVersions(recipe recipe.Recipe) (map[string]string, error) { func getImageVersions(recipe recipe.Recipe) (map[string]string, error) {
services := make(map[string]string) services := make(map[string]string)
config, err := recipe.GetComposeConfig(nil)
if err != nil {
return nil, err
}
missingTag := false missingTag := false
for _, service := range recipe.Config.Services { for _, service := range config.Services {
if service.Image == "" { if service.Image == "" {
continue continue
} }
@ -184,8 +188,7 @@ func getImageVersions(recipe recipe.Recipe) (map[string]string, error) {
func createReleaseFromTag(recipe recipe.Recipe, tagString, mainAppVersion string) error { func createReleaseFromTag(recipe recipe.Recipe, tagString, mainAppVersion string) error {
var err error var err error
directory := path.Join(config.RECIPES_DIR, recipe.Name) repo, err := git.PlainOpen(recipe.Dir)
repo, err := git.PlainOpen(directory)
if err != nil { if err != nil {
return err 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 // addReleaseNotes checks if the release/next release note exists and moves the
// file to release/<tag>. // file to release/<tag>.
func addReleaseNotes(recipe recipe.Recipe, tag string) error { func addReleaseNotes(recipe recipe.Recipe, tag string) error {
repoPath := path.Join(config.RECIPES_DIR, recipe.Name) tagReleaseNotePath := path.Join(recipe.Dir, "release", tag)
tagReleaseNotePath := path.Join(repoPath, "release", tag)
if _, err := os.Stat(tagReleaseNotePath); err == nil { if _, err := os.Stat(tagReleaseNotePath); err == nil {
// Release note for current tag already exist exists. // Release note for current tag already exist exists.
return nil return nil
@ -255,7 +257,7 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
return err return err
} }
nextReleaseNotePath := path.Join(repoPath, "release", "next") nextReleaseNotePath := path.Join(recipe.Dir, "release", "next")
if _, err := os.Stat(nextReleaseNotePath); err == nil { if _, err := os.Stat(nextReleaseNotePath); err == nil {
// release/next note exists. Move it to release/<tag> // release/next note exists. Move it to release/<tag>
if internal.Dry { if internal.Dry {
@ -278,11 +280,11 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
if err != nil { if err != nil {
return err 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 { if err != nil {
return err 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 { if err != nil {
return err return err
} }
@ -311,7 +313,7 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
if err != nil { if err != nil {
return err 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 { if err != nil {
return err return err
} }
@ -325,20 +327,19 @@ func commitRelease(recipe recipe.Recipe, tag string) error {
return nil return nil
} }
isClean, err := gitPkg.IsClean(recipe.Dir()) isClean, err := gitPkg.IsClean(recipe.Dir)
if err != nil { if err != nil {
return err return err
} }
if isClean { if isClean {
if !internal.Dry { 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) msg := fmt.Sprintf("chore: publish %s release", tag)
repoPath := path.Join(config.RECIPES_DIR, recipe.Name) if err := gitPkg.Commit(recipe.Dir, msg, internal.Dry); err != nil {
if err := gitPkg.Commit(repoPath, msg, internal.Dry); err != nil {
return err 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 { func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recipe.Recipe, tags []string) error {
directory := path.Join(config.RECIPES_DIR, recipe.Name) repo, err := git.PlainOpen(recipe.Dir)
repo, err := git.PlainOpen(directory)
if err != nil { if err != nil {
return err return err
} }
@ -506,9 +506,8 @@ func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recip
} }
// cleanUpTag removes a freshly created tag // cleanUpTag removes a freshly created tag
func cleanUpTag(tag, recipeName string) error { func cleanUpTag(recipe recipe.Recipe, tag string) error {
directory := path.Join(config.RECIPES_DIR, recipeName) repo, err := git.PlainOpen(recipe.Dir)
repo, err := git.PlainOpen(directory)
if err != nil { if err != nil {
return err return err
} }
@ -525,7 +524,7 @@ func cleanUpTag(tag, recipeName string) error {
} }
func getLabelVersion(recipe recipe.Recipe, prompt bool) (string, error) { func getLabelVersion(recipe recipe.Recipe, prompt bool) (string, error) {
initTag, err := recipePkg.GetVersionLabelLocal(recipe) initTag, err := recipe.GetVersionLabelLocal()
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -1,12 +1,10 @@
package recipe package recipe
import ( import (
"path"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -25,13 +23,13 @@ var recipeResetCommand = cli.Command{
BashComplete: autocomplete.RecipeNameComplete, BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
recipeName := c.Args().First() recipeName := c.Args().First()
r := recipe.Get(recipeName)
if recipeName != "" { if recipeName != "" {
internal.ValidateRecipe(c) internal.ValidateRecipe(c)
} }
repoPath := path.Join(config.RECIPES_DIR, recipeName) repo, err := git.PlainOpen(r.Dir)
repo, err := git.PlainOpen(repoPath)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@ -2,12 +2,10 @@ package recipe
import ( import (
"fmt" "fmt"
"path"
"strconv" "strconv"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
gitPkg "coopcloud.tech/abra/pkg/git" gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
@ -107,8 +105,7 @@ likely to change.
} }
if nextTag == "" { if nextTag == "" {
recipeDir := path.Join(config.RECIPES_DIR, recipe.Name) repo, err := git.PlainOpen(recipe.Dir)
repo, err := git.PlainOpen(recipeDir)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -199,13 +196,13 @@ likely to change.
log.Infof("dry run: not syncing label %s for recipe %s", nextTag, recipe.Name) 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 { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
if !isClean { if !isClean {
log.Infof("%s currently has these unstaged changes 👇", recipe.Name) 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) log.Fatal(err)
} }
} }

View File

@ -12,7 +12,6 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git" gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log" "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 { Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c) recipe := internal.ValidateRecipe(c)
if err := recipePkg.EnsureIsClean(recipe.Name); err != nil { if err := recipe.Ensure(internal.Chaos, internal.Offline); 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 {
log.Fatal(err) 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 // check for versions file and load pinned versions
versionsPresent := false versionsPresent := false
recipeDir := path.Join(config.RECIPES_DIR, recipe.Name) versionsPath := path.Join(recipe.Dir, "versions")
versionsPath := path.Join(recipeDir, "versions") servicePins := make(map[string]imgPin)
var servicePins = make(map[string]imgPin)
if _, err := os.Stat(versionsPath); err == nil { if _, err := os.Stat(versionsPath); err == nil {
log.Debugf("found versions file for %s", recipe.Name) log.Debugf("found versions file for %s", recipe.Name)
file, err := os.Open(versionsPath) 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) 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) img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil { if err != nil {
log.Fatal(err) 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 { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
if !isClean { if !isClean {
log.Infof("%s currently has these unstaged changes 👇", recipe.Name) 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) log.Fatal(err)
} }
} }

View File

@ -7,9 +7,9 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
recipePkg "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter"
"github.com/sirupsen/logrus"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -42,16 +42,16 @@ var recipeVersionCommand = cli.Command{
catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline) catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline)
if err != nil { if err != nil {
log.Fatal(err) logrus.Fatal(err)
} }
recipeMeta, ok := catl[recipe.Name] recipeMeta, ok := catl[recipe.Name]
if !ok { 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 { 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"} tableCols := []string{"version", "service", "image", "tag"}

View File

@ -10,7 +10,6 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile" "coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/lint" "coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/recipe" "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 // processRecipeRepoVersion clones, pulls, checks out the version and lints the
// recipe repository. // recipe repository.
func processRecipeRepoVersion(recipeName, version string) error { func processRecipeRepoVersion(r recipe.Recipe, version string) error {
if err := recipe.EnsureExists(recipeName); err != nil { if err := r.EnsureExists(); err != nil {
return err return err
} }
if err := recipe.EnsureUpToDate(recipeName); err != nil { if err := r.EnsureUpToDate(); err != nil {
return err return err
} }
if err := recipe.EnsureVersion(recipeName, version); err != nil { if err := r.EnsureVersion(version); err != nil {
return err return err
} }
if r, err := recipe.Get(recipeName, internal.Offline); err != nil { if err := lint.LintForErrors(r); err != nil {
return err
} else if err := lint.LintForErrors(r); err != nil {
return err return err
} }
@ -341,9 +338,8 @@ func processRecipeRepoVersion(recipeName, version string) error {
} }
// mergeAbraShEnv merges abra.sh env vars into the app env vars. // mergeAbraShEnv merges abra.sh env vars into the app env vars.
func mergeAbraShEnv(recipeName string, env envfile.AppEnv) error { func mergeAbraShEnv(recipe recipe.Recipe, env envfile.AppEnv) error {
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, recipeName, "abra.sh") abraShEnv, err := envfile.ReadAbraShEnvVars(recipe.AbraShPath)
abraShEnv, err := envfile.ReadAbraShEnvVars(abraShPath)
if err != nil { if err != nil {
return err return err
} }
@ -357,7 +353,7 @@ func mergeAbraShEnv(recipeName string, env envfile.AppEnv) error {
} }
// createDeployConfig merges and enriches the compose config for the deployment. // 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 env["STACK_NAME"] = stackName
deployOpts := stack.Deploy{ deployOpts := stack.Deploy{
@ -367,7 +363,7 @@ func createDeployConfig(recipeName string, stackName string, env envfile.AppEnv)
Detach: false, Detach: false,
} }
composeFiles, err := recipe.GetComposeFiles(recipeName, env) composeFiles, err := r.GetComposeFiles(env)
if err != nil { if err != nil {
return nil, deployOpts, err 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 // after the upgrade the deployment won't be in chaos state anymore
appPkg.SetChaosLabel(compose, stackName, false) appPkg.SetChaosLabel(compose, stackName, false)
appPkg.SetRecipeLabel(compose, stackName, recipeName) appPkg.SetRecipeLabel(compose, stackName, r.Name)
appPkg.SetUpdateLabel(compose, stackName, env) appPkg.SetUpdateLabel(compose, stackName, env)
return compose, deployOpts, nil return compose, deployOpts, nil
@ -440,20 +436,22 @@ func upgrade(cl *dockerclient.Client, stackName, recipeName,
app := appPkg.App{ app := appPkg.App{
Name: stackName, Name: stackName,
Recipe: recipeName, Recipe: recipe.Get(recipeName),
Server: SERVER, Server: SERVER,
Env: env, Env: env,
} }
if err = processRecipeRepoVersion(recipeName, upgradeVersion); err != nil { r := recipe.Get(recipeName)
if err = processRecipeRepoVersion(r, upgradeVersion); err != nil {
return err return err
} }
if err = mergeAbraShEnv(recipeName, app.Env); err != nil { if err = mergeAbraShEnv(app.Recipe, app.Env); err != nil {
return err return err
} }
compose, deployOpts, err := createDeployConfig(recipeName, stackName, app.Env) compose, deployOpts, err := createDeployConfig(r, stackName, app.Env)
if err != nil { if err != nil {
return err return err
} }

View File

@ -70,7 +70,7 @@ func GetApps(appFiles AppFiles, recipeFilter string) ([]App, error) {
} }
if recipeFilter != "" { if recipeFilter != "" {
if app.Recipe == recipeFilter { if app.Recipe.Name == recipeFilter {
apps = append(apps, app) apps = append(apps, app)
} }
} else { } else {
@ -84,7 +84,7 @@ func GetApps(appFiles AppFiles, recipeFilter string) ([]App, error) {
// App reprents an app with its env file read into memory // App reprents an app with its env file read into memory
type App struct { type App struct {
Name AppName Name AppName
Recipe string Recipe recipe.Recipe
Domain string Domain string
Env envfile.AppEnv Env envfile.AppEnv
Server string Server string
@ -161,13 +161,13 @@ func (a App) Filters(appendServiceNames, exactMatch bool, services ...string) (f
return filters, nil return filters, nil
} }
composeFiles, err := recipe.GetComposeFiles(a.Recipe, a.Env) composeFiles, err := a.Recipe.GetComposeFiles(a.Env)
if err != nil { if err != nil {
return filters, err return filters, err
} }
opts := stack.Deploy{Composefiles: composeFiles} 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 { if err != nil {
return filters, err 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) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByServerAndRecipe) Less(i, j int) bool { func (a ByServerAndRecipe) Less(i, j int) bool {
if a[i].Server == a[j].Server { 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) 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) Len() int { return len(a) }
func (a ByRecipe) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByRecipe) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByRecipe) Less(i, j int) bool { 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 // 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) { func NewApp(env envfile.AppEnv, name string, appFile AppFile) (App, error) {
domain := env["DOMAIN"] domain := env["DOMAIN"]
recipe, exists := env["RECIPE"] recipeName, exists := env["RECIPE"]
if !exists { if !exists {
recipe, exists = env["TYPE"] recipeName, exists = env["TYPE"]
if !exists { if !exists {
return App{}, fmt.Errorf("%s is missing the TYPE env var?", name) 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{ return App{
Name: name, Name: name,
Domain: domain, Domain: domain,
Recipe: recipe, Recipe: recipe.Get(recipeName),
Env: env, Env: env,
Server: appFile.Server, Server: appFile.Server,
Path: appFile.Path, Path: appFile.Path,
@ -317,13 +317,13 @@ func GetAppServiceNames(appName string) ([]string, error) {
return serviceNames, err return serviceNames, err
} }
composeFiles, err := recipe.GetComposeFiles(app.Recipe, app.Env) composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
if err != nil { if err != nil {
return serviceNames, err return serviceNames, err
} }
opts := stack.Deploy{Composefiles: composeFiles} 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 { if err != nil {
return serviceNames, err return serviceNames, err
} }
@ -358,9 +358,8 @@ func GetAppNames() ([]string, error) {
// TemplateAppEnvSample copies the example env file for the app into the users // TemplateAppEnvSample copies the example env file for the app into the users
// env files. // env files.
func TemplateAppEnvSample(recipeName, appName, server, domain string) error { func TemplateAppEnvSample(r recipe.Recipe, appName, server, domain string) error {
envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample") envSample, err := os.ReadFile(r.SampleEnvPath)
envSample, err := os.ReadFile(envSamplePath)
if err != nil { if err != nil {
return err return err
} }
@ -380,14 +379,14 @@ func TemplateAppEnvSample(recipeName, appName, server, domain string) error {
return err 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) err = os.WriteFile(appEnvPath, []byte(newContents), 0)
if err != nil { if err != nil {
return err return err
} }
log.Debugf("copied & templated %s to %s", envSamplePath, appEnvPath) log.Debugf("copied & templated %s to %s", r.SampleEnvPath, appEnvPath)
return nil return nil
} }
@ -511,15 +510,7 @@ func ExposeAllEnv(stackName string, compose *composetypes.Config, appEnv envfile
func CheckEnv(app App) ([]envfile.EnvVar, error) { func CheckEnv(app App) ([]envfile.EnvVar, error) {
var envVars []envfile.EnvVar var envVars []envfile.EnvVar
envSamplePath := path.Join(config.RECIPES_DIR, app.Recipe, ".env.sample") envSample, err := app.Recipe.SampleEnv()
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)
if err != nil { if err != nil {
return envVars, err return envVars, err
} }

View File

@ -47,8 +47,8 @@ func TestGetApp(t *testing.T) {
} }
func TestGetComposeFiles(t *testing.T) { func TestGetComposeFiles(t *testing.T) {
offline := true r := recipe.Get("abra-test-recipe")
r, err := recipe.Get("abra-test-recipe", offline) err := r.EnsureExists()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -60,32 +60,33 @@ func TestGetComposeFiles(t *testing.T) {
{ {
map[string]string{}, map[string]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"}, map[string]string{"COMPOSE_FILE": "compose.yml"},
[]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.extra_secret.yml"}, map[string]string{"COMPOSE_FILE": "compose.extra_secret.yml"},
[]string{ []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"}, map[string]string{"COMPOSE_FILE": "compose.yml:compose.extra_secret.yml"},
[]string{ []string{
fmt.Sprintf("%s/%s/compose.yml", config.RECIPES_DIR, r.Name), fmt.Sprintf("%s/compose.yml", r.Dir),
fmt.Sprintf("%s/%s/compose.extra_secret.yml", config.RECIPES_DIR, r.Name), fmt.Sprintf("%s/compose.extra_secret.yml", r.Dir),
}, },
}, },
} }
for _, test := range tests { 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -94,8 +95,8 @@ func TestGetComposeFiles(t *testing.T) {
} }
func TestGetComposeFilesError(t *testing.T) { func TestGetComposeFilesError(t *testing.T) {
offline := true r := recipe.Get("abra-test-recipe")
r, err := recipe.Get("abra-test-recipe", offline) err := r.EnsureExists()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -106,7 +107,8 @@ func TestGetComposeFilesError(t *testing.T) {
} }
for _, test := range tests { for _, test := range tests {
_, err := recipe.GetComposeFiles(r.Name, test.appEnv) r2 := recipe.Get(r.Name)
_, err := r2.GetComposeFiles(test.appEnv)
if err == nil { if err == nil {
t.Fatalf("should have failed: %v", test.appEnv) t.Fatalf("should have failed: %v", test.appEnv)
} }

View File

@ -1,8 +1,6 @@
package envfile_test package envfile_test
import ( import (
"fmt"
"path"
"reflect" "reflect"
"slices" "slices"
"strings" "strings"
@ -56,14 +54,13 @@ func TestReadEnv(t *testing.T) {
} }
func TestReadAbraShEnvVars(t *testing.T) { func TestReadAbraShEnvVars(t *testing.T) {
offline := true r := recipe.Get("abra-test-recipe")
r, err := recipe.Get("abra-test-recipe", offline) err := r.EnsureExists()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, r.Name, "abra.sh") abraShEnv, err := envfile.ReadAbraShEnvVars(r.AbraShPath)
abraShEnv, err := envfile.ReadAbraShEnvVars(abraShPath)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -86,14 +83,13 @@ func TestReadAbraShEnvVars(t *testing.T) {
} }
func TestReadAbraShCmdNames(t *testing.T) { func TestReadAbraShCmdNames(t *testing.T) {
offline := true r := recipe.Get("abra-test-recipe")
r, err := recipe.Get("abra-test-recipe", offline) err := r.EnsureExists()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, r.Name, "abra.sh") cmdNames, err := appPkg.ReadAbraShCmdNames(r.AbraShPath)
cmdNames, err := appPkg.ReadAbraShCmdNames(abraShPath)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -105,27 +101,27 @@ func TestReadAbraShCmdNames(t *testing.T) {
expectedCmdNames := []string{"test_cmd", "test_cmd_args"} expectedCmdNames := []string{"test_cmd", "test_cmd_args"}
for _, cmdName := range expectedCmdNames { for _, cmdName := range expectedCmdNames {
if !slices.Contains(cmdNames, cmdName) { 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) { func TestCheckEnv(t *testing.T) {
offline := true r := recipe.Get("abra-test-recipe")
r, err := recipe.Get("abra-test-recipe", offline) err := r.EnsureExists()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
envSamplePath := path.Join(config.RECIPES_DIR, r.Name, ".env.sample") r2 := recipe.Get(r.Name)
envSample, err := envfile.ReadEnv(envSamplePath) envSample, err := r2.SampleEnv()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
app := appPkg.App{ app := appPkg.App{
Name: "test-app", Name: "test-app",
Recipe: r.Name, Recipe: recipe.Get(r.Name),
Domain: "example.com", Domain: "example.com",
Env: envSample, Env: envSample,
Path: "example.com.env", Path: "example.com.env",
@ -145,14 +141,14 @@ func TestCheckEnv(t *testing.T) {
} }
func TestCheckEnvError(t *testing.T) { func TestCheckEnvError(t *testing.T) {
offline := true r := recipe.Get("abra-test-recipe")
r, err := recipe.Get("abra-test-recipe", offline) err := r.EnsureExists()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
envSamplePath := path.Join(config.RECIPES_DIR, r.Name, ".env.sample") r2 := recipe.Get(r.Name)
envSample, err := envfile.ReadEnv(envSamplePath) envSample, err := r2.SampleEnv()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -161,7 +157,7 @@ func TestCheckEnvError(t *testing.T) {
app := appPkg.App{ app := appPkg.App{
Name: "test-app", Name: "test-app",
Recipe: r.Name, Recipe: recipe.Get(r.Name),
Domain: "example.com", Domain: "example.com",
Env: envSample, Env: envSample,
Path: "example.com.env", Path: "example.com.env",
@ -181,14 +177,14 @@ func TestCheckEnvError(t *testing.T) {
} }
func TestEnvVarCommentsRemoved(t *testing.T) { func TestEnvVarCommentsRemoved(t *testing.T) {
offline := true r := recipe.Get("abra-test-recipe")
r, err := recipe.Get("abra-test-recipe", offline) err := r.EnsureExists()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
envSamplePath := path.Join(config.RECIPES_DIR, r.Name, ".env.sample") r2 := recipe.Get(r.Name)
envSample, err := envfile.ReadEnv(envSamplePath) envSample, err := r2.SampleEnv()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -213,14 +209,13 @@ func TestEnvVarCommentsRemoved(t *testing.T) {
} }
func TestEnvVarModifiersIncluded(t *testing.T) { func TestEnvVarModifiersIncluded(t *testing.T) {
offline := true r := recipe.Get("abra-test-recipe")
r, err := recipe.Get("abra-test-recipe", offline) err := r.EnsureExists()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
envSamplePath := path.Join(config.RECIPES_DIR, r.Name, ".env.sample") envSample, modifiers, err := envfile.ReadEnvWithModifiers(r.SampleEnvPath)
envSample, modifiers, err := envfile.ReadEnvWithModifiers(envSamplePath)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -7,7 +7,6 @@ import (
"path" "path"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
recipePkg "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) { 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
} }
return true, nil return true, nil
} }
func LintEnvConfigPresent(recipe recipe.Recipe) (bool, error) { func LintEnvConfigPresent(r recipe.Recipe) (bool, error) {
envSample := fmt.Sprintf("%s/%s/.env.sample", config.RECIPES_DIR, recipe.Name) r2 := recipe.Get(r.Name)
if _, err := os.Stat(envSample); !os.IsNotExist(err) { if _, err := os.Stat(r2.SampleEnvPath); !os.IsNotExist(err) {
return true, nil return true, nil
} }
@ -220,7 +223,11 @@ func LintEnvConfigPresent(recipe recipe.Recipe) (bool, error) {
} }
func LintAppService(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" { if service.Name == "app" {
return true, nil 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 // 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 // the recipe. This typically means that no domain is required to deploy and
// therefore no matching traefik deploy label will be present. // therefore no matching traefik deploy label will be present.
func LintTraefikEnabledSkipCondition(recipe recipe.Recipe) (bool, error) { func LintTraefikEnabledSkipCondition(r recipe.Recipe) (bool, error) {
envSamplePath := path.Join(config.RECIPES_DIR, recipe.Name, ".env.sample") r2 := recipe.Get(r.Name)
sampleEnv, err := envfile.ReadEnv(envSamplePath) sampleEnv, err := r2.SampleEnv()
if err != nil { 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 { if _, ok := sampleEnv["DOMAIN"]; !ok {
@ -248,7 +255,11 @@ func LintTraefikEnabledSkipCondition(recipe recipe.Recipe) (bool, error) {
} }
func LintTraefikEnabled(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 { for label := range service.Deploy.Labels {
if label == "traefik.enable" { if label == "traefik.enable" {
if service.Deploy.Labels[label] == "true" { if service.Deploy.Labels[label] == "true" {
@ -262,7 +273,11 @@ func LintTraefikEnabled(recipe recipe.Recipe) (bool, error) {
} }
func LintHealthchecks(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 { if service.HealthCheck == nil {
return false, nil return false, nil
} }
@ -272,7 +287,11 @@ func LintHealthchecks(recipe recipe.Recipe) (bool, error) {
} }
func LintAllImagesTagged(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) img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil { if err != nil {
return false, err return false, err
@ -286,7 +305,11 @@ func LintAllImagesTagged(recipe recipe.Recipe) (bool, error) {
} }
func LintNoUnstableTags(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) img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil { if err != nil {
return false, err return false, err
@ -309,7 +332,11 @@ func LintNoUnstableTags(recipe recipe.Recipe) (bool, error) {
} }
func LintSemverLikeTags(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) img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil { if err != nil {
return false, err return false, err
@ -332,7 +359,11 @@ func LintSemverLikeTags(recipe recipe.Recipe) (bool, error) {
} }
func LintImagePresent(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 == "" { if service.Image == "" {
return false, nil return false, nil
} }
@ -359,7 +390,8 @@ func LintHasPublishedVersion(recipe recipe.Recipe) (bool, error) {
} }
func LintMetadataFilledIn(r 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 { if err != nil {
return false, err return false, err
} }
@ -380,9 +412,13 @@ func LintMetadataFilledIn(r recipe.Recipe) (bool, error) {
} }
func LintAbraShVendors(recipe 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 { 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 _, err := os.Stat(abraSh); err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return false, err return false, err
@ -410,7 +446,11 @@ func LintHasRecipeRepo(recipe recipe.Recipe) (bool, error) {
} }
func LintSecretLengths(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 { if len(name) > 12 {
return false, fmt.Errorf("secret %s is longer than 12 characters", name) 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) { func LintValidTags(recipe recipe.Recipe) (bool, error) {
recipeDir := path.Join(config.RECIPES_DIR, recipe.Name) repo, err := git.PlainOpen(recipe.Dir)
repo, err := git.PlainOpen(recipeDir)
if err != nil { 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() iter, err := repo.Tags()

View File

@ -1,14 +1,12 @@
package compose package recipe
import ( import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"path" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/abra/pkg/upstream/stack"
@ -17,9 +15,105 @@ import (
composetypes "github.com/docker/cli/cli/compose/types" composetypes "github.com/docker/cli/cli/compose/types"
) )
// UpdateTag updates an image tag in-place on file system local compose files. // GetComposeFiles gets the list of compose files for an app (or recipe if you
func UpdateTag(pattern, image, tag, recipeName string) (bool, error) { // 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) 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 { if err != nil {
return false, err return false, err
} }
@ -29,8 +123,7 @@ func UpdateTag(pattern, image, tag, recipeName string) (bool, error) {
for _, composeFile := range composeFiles { for _, composeFile := range composeFiles {
opts := stack.Deploy{Composefiles: []string{composeFile}} opts := stack.Deploy{Composefiles: []string{composeFile}}
envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample") sampleEnv, err := r.SampleEnv()
sampleEnv, err := envfile.ReadEnv(envSamplePath)
if err != nil { if err != nil {
return false, err 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) 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 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. // UpdateLabel updates a label in-place on file system local compose files.
func UpdateLabel(pattern, serviceName, label, recipeName string) error { func (r Recipe) UpdateLabel(pattern, serviceName, label string) error {
composeFiles, err := filepath.Glob(pattern) fullPattern := fmt.Sprintf("%s/%s", r.Dir, pattern)
composeFiles, err := filepath.Glob(fullPattern)
if err != nil { if err != nil {
return err return err
} }
@ -97,8 +191,7 @@ func UpdateLabel(pattern, serviceName, label, recipeName string) error {
for _, composeFile := range composeFiles { for _, composeFile := range composeFiles {
opts := stack.Deploy{Composefiles: []string{composeFile}} opts := stack.Deploy{Composefiles: []string{composeFile}}
envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample") sampleEnv, err := r.SampleEnv()
sampleEnv, err := envfile.ReadEnv(envSamplePath)
if err != nil { if err != nil {
return err 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) 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 return err
} }

37
pkg/recipe/files.go Normal file
View File

@ -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
}

379
pkg/recipe/git.go Normal file
View File

@ -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
}

View File

@ -6,28 +6,19 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path" "path"
"path/filepath"
"slices" "slices"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"coopcloud.tech/abra/pkg/catalogue" "coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/compose"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git" gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/limit" "coopcloud.tech/abra/pkg/limit"
"coopcloud.tech/abra/pkg/log" "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/abra/pkg/web"
"coopcloud.tech/tagcmp" "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. // RecipeCatalogueURL is the only current recipe catalogue available.
@ -131,307 +122,29 @@ type Features struct {
SSO string `json:"sso"` 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 { type Recipe struct {
Name string Name string
Config *composetypes.Config Dir string
Meta RecipeMeta SSHURL string
}
// Push pushes the latest changes to a SSH URL remote. You need to have your ComposePath string
// local SSH configuration for git.coopcloud.tech working for this to work ReadmePath string
func (r Recipe) Push(dryRun bool) error { SampleEnvPath string
repo, err := git.PlainOpen(r.Dir()) AbraShPath string
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
} }
// GetRecipesLocal retrieves all local recipe directories // GetRecipesLocal retrieves all local recipe directories
@ -446,41 +159,20 @@ func GetRecipesLocal() ([]string, error) {
return recipes, nil return recipes, nil
} }
// GetVersionLabelLocal retrieves the version label on the local recipe config func GetRecipeFeaturesAndCategory(r Recipe) (Features, string, error) {
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) {
feat := Features{} feat := Features{}
var category string 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(r.ReadmePath)
readmeFS, err := ioutil.ReadFile(readmePath)
if err != nil { if err != nil {
return feat, category, err return feat, category, err
} }
readmeMetadata, err := GetStringInBetween( // Find text between delimiters readmeMetadata, err := GetStringInBetween( // Find text between delimiters
recipeName, r.Name,
string(readmeFS), string(readmeFS),
"<!-- metadata -->", "<!-- endmetadata -->", "<!-- metadata -->", "<!-- endmetadata -->",
) )
@ -531,7 +223,7 @@ func GetRecipeFeaturesAndCategory(recipeName string) (Features, string, error) {
if strings.Contains(val, "**Image**") { if strings.Contains(val, "**Image**") {
imageMetadata, err := GetImageMetadata(strings.TrimSpace( imageMetadata, err := GetImageMetadata(strings.TrimSpace(
strings.TrimPrefix(val, "* **Image**:"), strings.TrimPrefix(val, "* **Image**:"),
), recipeName) ), r.Name)
if err != nil { if err != nil {
continue continue
} }
@ -597,59 +289,6 @@ func GetStringInBetween(recipeName, str, start, end string) (result string, err
return str[s : s+e], nil 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. // ReadRecipeCatalogue reads the recipe catalogue.
func ReadRecipeCatalogue(offline bool) (RecipeCatalogue, error) { func ReadRecipeCatalogue(offline bool) (RecipeCatalogue, error) {
recipes := make(RecipeCatalogue) recipes := make(RecipeCatalogue)
@ -864,96 +503,6 @@ func ReadReposMetadata() (RepoCatalogue, error) {
return reposMeta, nil 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 // sortRecipeVersions sorts the recipe semver versions
func sortRecipeVersions(versions RecipeVersions) { func sortRecipeVersions(versions RecipeVersions) {
sort.Slice(versions, func(i, j int) bool { sort.Slice(versions, func(i, j int) bool {
@ -1034,8 +583,7 @@ func UpdateRepositories(repos RepoCatalogue, recipeName string) error {
return return
} }
recipeDir := path.Join(config.RECIPES_DIR, rm.Name) if err := gitPkg.Clone(Get(rm.Name).Dir, rm.CloneURL); err != nil {
if err := gitPkg.Clone(recipeDir, rm.CloneURL); err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -1063,50 +611,3 @@ func ensurePathExists(path string) error {
} }
return nil 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
}

View File

@ -14,13 +14,14 @@ func TestGetVersionLabelLocalDoesNotUseTimeoutLabel(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
r, err := Get("traefik", offline) r := Get("traefik")
err = r.EnsureExists()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
for i := 1; i < 1000; i++ { for i := 1; i < 1000; i++ {
label, err := GetVersionLabelLocal(r) label, err := r.GetVersionLabelLocal()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -246,7 +246,8 @@ type secretStatuses []secretStatus
func PollSecretsStatus(cl *dockerClient.Client, app appPkg.App) (secretStatuses, error) { func PollSecretsStatus(cl *dockerClient.Client, app appPkg.App) (secretStatuses, error) {
var secStats secretStatuses 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 { if err != nil {
return secStats, err return secStats, err
} }

View File

@ -7,6 +7,7 @@ import (
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/envfile" "coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
) )
var ( var (
@ -32,7 +33,7 @@ var ExpectedAppEnv = envfile.AppEnv{
var ExpectedApp = appPkg.App{ var ExpectedApp = appPkg.App{
Name: AppName, Name: AppName,
Recipe: ExpectedAppEnv["RECIPE"], Recipe: recipe.Get(ExpectedAppEnv["RECIPE"]),
Domain: ExpectedAppEnv["DOMAIN"], Domain: ExpectedAppEnv["DOMAIN"],
Env: ExpectedAppEnv, Env: ExpectedAppEnv,
Path: ExpectedAppFile.Path, Path: ExpectedAppFile.Path,