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

View File

@ -6,8 +6,6 @@ import (
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/urfave/cli"
)
@ -38,26 +36,10 @@ ${FOO:<default>} syntax). "check" does not confirm or deny this for you.`,
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := recipe.EnsureExists(app.Recipe); err != nil {
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
if !internal.Chaos {
if err := recipePkg.EnsureIsClean(app.Recipe); err != nil {
log.Fatal(err)
}
if !internal.Offline {
if err := recipePkg.EnsureUpToDate(app.Recipe); err != nil {
log.Fatal(err)
}
}
if err := recipePkg.EnsureLatest(app.Recipe); err != nil {
log.Fatal(err)
}
}
tableCol := []string{"recipe env sample", "app env"}
table := formatter.CreateTable(tableCol)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -53,7 +53,7 @@ var appPsCommand = cli.Command{
statuses, err := appPkg.GetAppStatuses([]appPkg.App{app}, true)
if statusMeta, ok := statuses[app.StackName()]; ok {
if _, exists := statusMeta["chaos"]; !exists {
if err := recipe.EnsureVersion(app.Recipe, deployedVersion); err != nil {
if err := app.Recipe.EnsureVersion(deployedVersion); err != nil {
log.Fatal(err)
}
}
@ -67,7 +67,8 @@ var appPsCommand = cli.Command{
// showPSOutput renders ps output.
func showPSOutput(app appPkg.App, cl *dockerClient.Client) {
composeFiles, err := recipe.GetComposeFiles(app.Recipe, app.Env)
r := recipe.Get(app.Name)
composeFiles, err := r.GetComposeFiles(app.Env)
if err != nil {
log.Fatal(err)
return

View File

@ -7,7 +7,6 @@ import (
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/urfave/cli"
)
@ -33,26 +32,10 @@ var appRestoreCommand = cli.Command{
Action: func(c *cli.Context) error {
app := internal.ValidateApp(c)
if err := recipe.EnsureExists(app.Recipe); err != nil {
if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
if !internal.Chaos {
if err := recipe.EnsureIsClean(app.Recipe); err != nil {
log.Fatal(err)
}
if !internal.Offline {
if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
log.Fatal(err)
}
}
if err := recipe.EnsureLatest(app.Recipe); err != nil {
log.Fatal(err)
}
}
cl, err := client.New(app.Server)
if err != nil {
log.Fatal(err)

View File

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

View File

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

View File

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

View File

@ -78,7 +78,7 @@ var appVersionCommand = cli.Command{
log.Fatalf("failed to determine version of deployed %s", app.Name)
}
recipeMeta, err := recipe.GetRecipeMeta(app.Recipe, internal.Offline)
recipeMeta, err := recipe.GetRecipeMeta(app.Recipe.Name, internal.Offline)
if err != nil {
log.Fatal(err)
}

View File

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

View File

@ -2,13 +2,10 @@ package internal
import (
"fmt"
"io/ioutil"
"os"
"path"
"strings"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"github.com/AlecAivazis/survey/v2"
@ -30,7 +27,7 @@ func NewVersionOverview(app appPkg.App, currentVersion, newVersion, releaseNotes
server = "local"
}
table.Append([]string{server, app.Recipe, deployConfig, app.Domain, currentVersion, newVersion})
table.Append([]string{server, app.Recipe.Name, deployConfig, app.Domain, currentVersion, newVersion})
table.Render()
if releaseNotes != "" && newVersion != "" {
@ -60,34 +57,13 @@ func NewVersionOverview(app appPkg.App, currentVersion, newVersion, releaseNotes
return nil
}
// GetReleaseNotes prints release notes for a recipe version
func GetReleaseNotes(recipeName, version string) (string, error) {
if version == "" {
return "", nil
}
fpath := path.Join(config.RECIPES_DIR, recipeName, "release", version)
if _, err := os.Stat(fpath); !os.IsNotExist(err) {
releaseNotes, err := ioutil.ReadFile(fpath)
if err != nil {
return "", err
}
withTitle := fmt.Sprintf("%s release notes:\n%s", version, string(releaseNotes))
return withTitle, nil
}
return "", nil
}
// PostCmds parses a string of commands and executes them inside of the respective services
// the commands string must have the following format:
// "<service> <command> <arguments>|<service> <command> <arguments>|... "
func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error {
abraSh := path.Join(config.RECIPES_DIR, app.Recipe, "abra.sh")
if _, err := os.Stat(abraSh); err != nil {
if _, err := os.Stat(app.Recipe.AbraShPath); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf(fmt.Sprintf("%s does not exist for %s?", abraSh, app.Name))
return fmt.Errorf(fmt.Sprintf("%s does not exist for %s?", app.Recipe.AbraShPath, app.Name))
}
return err
}
@ -105,7 +81,7 @@ func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error {
}
log.Infof("running post-command '%s %s' in container %s", cmdName, parsedCmdArgs, targetServiceName)
if err := EnsureCommand(abraSh, app.Recipe, cmdName); err != nil {
if err := EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil {
return err
}
@ -128,7 +104,7 @@ func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error {
log.Debugf("running command %s %s within the context of %s_%s", cmdName, parsedCmdArgs, app.StackName(), targetServiceName)
Tty = true
if err := RunCmdRemote(cl, app, abraSh, targetServiceName, cmdName, parsedCmdArgs); err != nil {
if err := RunCmdRemote(cl, app, app.Recipe.AbraShPath, targetServiceName, cmdName, parsedCmdArgs); err != nil {
return err
}
}
@ -150,7 +126,7 @@ func DeployOverview(app appPkg.App, version, message string) error {
server = "local"
}
table.Append([]string{server, app.Recipe, deployConfig, app.Domain, version})
table.Append([]string{server, app.Recipe.Name, deployConfig, app.Domain, version})
table.Render()
if NoInput {

View File

@ -88,7 +88,11 @@ func SetBumpType(bumpType string) {
func GetMainAppImage(recipe recipe.Recipe) (string, error) {
var path string
for _, service := range recipe.Config.Services {
config, err := recipe.GetComposeConfig(nil)
if err != nil {
return "", err
}
for _, service := range config.Services {
if service.Name == "app" {
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {

View File

@ -57,7 +57,12 @@ func ValidateRecipe(c *cli.Context) recipe.Recipe {
ShowSubcommandHelpAndError(c, errors.New("no recipe name provided"))
}
chosenRecipe, err := recipe.Get(recipeName, Offline)
chosenRecipe := recipe.Get(recipeName)
err := chosenRecipe.EnsureExists()
if err != nil {
log.Fatal(err)
}
_, err = chosenRecipe.GetComposeConfig(nil)
if err != nil {
if c.Command.Name == "generate" {
if strings.Contains(err.Error(), "missing a compose") {

View File

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

View File

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

View File

@ -8,7 +8,6 @@ import (
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/urfave/cli"
)
@ -29,26 +28,10 @@ var recipeLintCommand = cli.Command{
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
if err := recipePkg.EnsureExists(recipe.Name); err != nil {
if err := recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
if !internal.Chaos {
if err := recipePkg.EnsureIsClean(recipe.Name); err != nil {
log.Fatal(err)
}
if !internal.Offline {
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
log.Fatal(err)
}
}
if err := recipePkg.EnsureLatest(recipe.Name); err != nil {
log.Fatal(err)
}
}
tableCol := []string{"ref", "rule", "severity", "satisfied", "skipped", "resolve"}
table := formatter.CreateTable(tableCol)

View File

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

View File

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

View File

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

View File

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

View File

@ -12,7 +12,6 @@ import (
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/log"
@ -73,19 +72,7 @@ You may invoke this command in "wizard" mode and be prompted for input:
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipe(c)
if err := recipePkg.EnsureIsClean(recipe.Name); err != nil {
log.Fatal(err)
}
if err := recipePkg.EnsureExists(recipe.Name); err != nil {
log.Fatal(err)
}
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
log.Fatal(err)
}
if err := recipePkg.EnsureLatest(recipe.Name); err != nil {
if err := recipe.Ensure(internal.Chaos, internal.Offline); err != nil {
log.Fatal(err)
}
@ -106,9 +93,8 @@ You may invoke this command in "wizard" mode and be prompted for input:
// check for versions file and load pinned versions
versionsPresent := false
recipeDir := path.Join(config.RECIPES_DIR, recipe.Name)
versionsPath := path.Join(recipeDir, "versions")
var servicePins = make(map[string]imgPin)
versionsPath := path.Join(recipe.Dir, "versions")
servicePins := make(map[string]imgPin)
if _, err := os.Stat(versionsPath); err == nil {
log.Debugf("found versions file for %s", recipe.Name)
file, err := os.Open(versionsPath)
@ -141,7 +127,12 @@ You may invoke this command in "wizard" mode and be prompted for input:
log.Debugf("did not find versions file for %s", recipe.Name)
}
for _, service := range recipe.Config.Services {
config, err := recipe.GetComposeConfig(nil)
if err != nil {
log.Fatal(err)
}
for _, service := range config.Services {
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
log.Fatal(err)
@ -339,13 +330,13 @@ You may invoke this command in "wizard" mode and be prompted for input:
}
}
isClean, err := gitPkg.IsClean(recipeDir)
isClean, err := gitPkg.IsClean(recipe.Dir)
if err != nil {
log.Fatal(err)
}
if !isClean {
log.Infof("%s currently has these unstaged changes 👇", recipe.Name)
if err := gitPkg.DiffUnstaged(recipeDir); err != nil {
if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil {
log.Fatal(err)
}
}

View File

@ -7,9 +7,9 @@ import (
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/olekukonko/tablewriter"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
@ -42,16 +42,16 @@ var recipeVersionCommand = cli.Command{
catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
recipeMeta, ok := catl[recipe.Name]
if !ok {
log.Fatalf("%s is not published on the catalogue?", recipe.Name)
logrus.Fatalf("%s is not published on the catalogue?", recipe.Name)
}
if len(recipeMeta.Versions) == 0 {
log.Fatalf("%s has no catalogue published versions?", recipe.Name)
logrus.Fatalf("%s has no catalogue published versions?", recipe.Name)
}
tableCols := []string{"version", "service", "image", "tag"}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +1,12 @@
package compose
package recipe
import (
"fmt"
"io/ioutil"
"path"
"os"
"path/filepath"
"strings"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/stack"
@ -17,9 +15,105 @@ import (
composetypes "github.com/docker/cli/cli/compose/types"
)
// UpdateTag updates an image tag in-place on file system local compose files.
func UpdateTag(pattern, image, tag, recipeName string) (bool, error) {
// GetComposeFiles gets the list of compose files for an app (or recipe if you
// don't already have an app) which should be merged into a composetypes.Config
// while respecting the COMPOSE_FILE env var.
func (r Recipe) GetComposeFiles(appEnv map[string]string) ([]string, error) {
composeFileEnvVar, ok := appEnv["COMPOSE_FILE"]
if !ok {
if err := ensurePathExists(r.ComposePath); err != nil {
return []string{}, err
}
log.Debugf("no COMPOSE_FILE detected, loading default: %s", r.ComposePath)
return []string{r.ComposePath}, nil
}
if !strings.Contains(composeFileEnvVar, ":") {
path := fmt.Sprintf("%s/%s", r.Dir, composeFileEnvVar)
if err := ensurePathExists(path); err != nil {
return []string{}, err
}
log.Debugf("COMPOSE_FILE detected, loading %s", path)
return []string{path}, nil
}
var composeFiles []string
numComposeFiles := strings.Count(composeFileEnvVar, ":") + 1
envVars := strings.SplitN(composeFileEnvVar, ":", numComposeFiles)
if len(envVars) != numComposeFiles {
return composeFiles, fmt.Errorf("COMPOSE_FILE (=\"%s\") parsing failed?", composeFileEnvVar)
}
for _, file := range envVars {
path := fmt.Sprintf("%s/%s", r.Dir, file)
if err := ensurePathExists(path); err != nil {
return composeFiles, err
}
composeFiles = append(composeFiles, path)
}
log.Debugf("COMPOSE_FILE detected (%s), loading %s", composeFileEnvVar, strings.Join(envVars, ", "))
log.Debugf("retrieved %s configs for %s", strings.Join(composeFiles, ", "), r.Name)
return composeFiles, nil
}
func (r Recipe) GetComposeConfig(env map[string]string) (*composetypes.Config, error) {
pattern := fmt.Sprintf("%s/compose**yml", r.Dir)
composeFiles, err := filepath.Glob(pattern)
if err != nil {
return nil, err
}
if len(composeFiles) == 0 {
return nil, fmt.Errorf("%s is missing a compose.yml or compose.*.yml file?", r.Name)
}
if env == nil {
env, err = r.SampleEnv()
if err != nil {
return nil, err
}
}
opts := stack.Deploy{Composefiles: composeFiles}
config, err := loader.LoadComposefile(opts, env)
if err != nil {
return nil, err
}
return config, nil
}
// GetVersionLabelLocal retrieves the version label on the local recipe config
func (r Recipe) GetVersionLabelLocal() (string, error) {
var label string
config, err := r.GetComposeConfig(nil)
if err != nil {
return "", err
}
for _, service := range config.Services {
for label, value := range service.Deploy.Labels {
if strings.HasPrefix(label, "coop-cloud") && strings.Contains(label, "version") {
return value, nil
}
}
}
if label == "" {
return label, fmt.Errorf("%s has no version label? try running \"abra recipe sync %s\" first?", r.Name, r.Name)
}
return label, nil
}
// UpdateTag updates an image tag in-place on file system local compose files.
func (r Recipe) UpdateTag(image, tag string) (bool, error) {
fullPattern := fmt.Sprintf("%s/compose**yml", r.Dir)
image = formatter.StripTagMeta(image)
composeFiles, err := filepath.Glob(fullPattern)
if err != nil {
return false, err
}
@ -29,8 +123,7 @@ func UpdateTag(pattern, image, tag, recipeName string) (bool, error) {
for _, composeFile := range composeFiles {
opts := stack.Deploy{Composefiles: []string{composeFile}}
envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample")
sampleEnv, err := envfile.ReadEnv(envSamplePath)
sampleEnv, err := r.SampleEnv()
if err != nil {
return false, err
}
@ -75,7 +168,7 @@ func UpdateTag(pattern, image, tag, recipeName string) (bool, error) {
log.Debugf("updating %s to %s in %s", old, new, compose.Filename)
if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0764); err != nil {
if err := os.WriteFile(compose.Filename, []byte(replacedBytes), 0o764); err != nil {
return false, err
}
}
@ -86,8 +179,9 @@ func UpdateTag(pattern, image, tag, recipeName string) (bool, error) {
}
// UpdateLabel updates a label in-place on file system local compose files.
func UpdateLabel(pattern, serviceName, label, recipeName string) error {
composeFiles, err := filepath.Glob(pattern)
func (r Recipe) UpdateLabel(pattern, serviceName, label string) error {
fullPattern := fmt.Sprintf("%s/%s", r.Dir, pattern)
composeFiles, err := filepath.Glob(fullPattern)
if err != nil {
return err
}
@ -97,8 +191,7 @@ func UpdateLabel(pattern, serviceName, label, recipeName string) error {
for _, composeFile := range composeFiles {
opts := stack.Deploy{Composefiles: []string{composeFile}}
envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample")
sampleEnv, err := envfile.ReadEnv(envSamplePath)
sampleEnv, err := r.SampleEnv()
if err != nil {
return err
}
@ -141,7 +234,7 @@ func UpdateLabel(pattern, serviceName, label, recipeName string) error {
log.Debugf("updating %s to %s in %s", old, label, compose.Filename)
if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0764); err != nil {
if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0o764); err != nil {
return err
}

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"
"os"
"path"
"path/filepath"
"slices"
"sort"
"strconv"
"strings"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/compose"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/limit"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/stack"
loader "coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/abra/pkg/web"
"coopcloud.tech/tagcmp"
"github.com/distribution/reference"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
)
// RecipeCatalogueURL is the only current recipe catalogue available.
@ -131,307 +122,29 @@ type Features struct {
SSO string `json:"sso"`
}
// Recipe represents a recipe.
func Get(name string) Recipe {
dir := path.Join(config.RECIPES_DIR, name)
return Recipe{
Name: name,
Dir: dir,
SSHURL: fmt.Sprintf(config.SSH_URL_TEMPLATE, name),
ComposePath: path.Join(dir, "compose.yml"),
ReadmePath: path.Join(dir, "README.md"),
SampleEnvPath: path.Join(dir, ".env.sample"),
AbraShPath: path.Join(dir, "abra.sh"),
}
}
type Recipe struct {
Name string
Config *composetypes.Config
Meta RecipeMeta
}
Dir string
SSHURL string
// Push pushes the latest changes to a SSH URL remote. You need to have your
// local SSH configuration for git.coopcloud.tech working for this to work
func (r Recipe) Push(dryRun bool) error {
repo, err := git.PlainOpen(r.Dir())
if err != nil {
return err
}
if err := gitPkg.CreateRemote(repo, "origin-ssh", r.Meta.SSHURL, dryRun); err != nil {
return err
}
if err := gitPkg.Push(r.Dir(), "origin-ssh", true, dryRun); err != nil {
return err
}
return nil
}
// Dir retrieves the recipe repository path
func (r Recipe) Dir() string {
return path.Join(config.RECIPES_DIR, r.Name)
}
// UpdateLabel updates a recipe label
func (r Recipe) UpdateLabel(pattern, serviceName, label string) error {
fullPattern := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, r.Name, pattern)
if err := compose.UpdateLabel(fullPattern, serviceName, label, r.Name); err != nil {
return err
}
return nil
}
// UpdateTag updates a recipe tag
func (r Recipe) UpdateTag(image, tag string) (bool, error) {
pattern := fmt.Sprintf("%s/%s/compose**yml", config.RECIPES_DIR, r.Name)
image = formatter.StripTagMeta(image)
ok, err := compose.UpdateTag(pattern, image, tag, r.Name)
if err != nil {
return false, err
}
return ok, nil
}
// Tags list the recipe tags
func (r Recipe) Tags() ([]string, error) {
var tags []string
repo, err := git.PlainOpen(r.Dir())
if err != nil {
return tags, err
}
gitTags, err := repo.Tags()
if err != nil {
return tags, err
}
if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) {
tags = append(tags, strings.TrimPrefix(string(ref.Name()), "refs/tags/"))
return nil
}); err != nil {
return tags, err
}
log.Debugf("detected %s as tags for recipe %s", strings.Join(tags, ", "), r.Name)
return tags, nil
}
// Get retrieves a recipe.
func Get(recipeName string, offline bool) (Recipe, error) {
if err := EnsureExists(recipeName); err != nil {
return Recipe{}, err
}
pattern := fmt.Sprintf("%s/%s/compose**yml", config.RECIPES_DIR, recipeName)
composeFiles, err := filepath.Glob(pattern)
if err != nil {
return Recipe{}, err
}
if len(composeFiles) == 0 {
return Recipe{}, fmt.Errorf("%s is missing a compose.yml or compose.*.yml file?", recipeName)
}
envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample")
sampleEnv, err := envfile.ReadEnv(envSamplePath)
if err != nil {
return Recipe{}, err
}
opts := stack.Deploy{Composefiles: composeFiles}
config, err := loader.LoadComposefile(opts, sampleEnv)
if err != nil {
return Recipe{}, err
}
meta, err := GetRecipeMeta(recipeName, offline)
if err != nil {
switch err.(type) {
case RecipeMissingFromCatalogue:
meta = RecipeMeta{}
default:
return Recipe{}, err
}
}
return Recipe{
Name: recipeName,
Config: config,
Meta: meta,
}, nil
}
func (r Recipe) SampleEnv() (map[string]string, error) {
envSamplePath := path.Join(config.RECIPES_DIR, r.Name, ".env.sample")
sampleEnv, err := envfile.ReadEnv(envSamplePath)
if err != nil {
return sampleEnv, fmt.Errorf("unable to discover .env.sample for %s", r.Name)
}
return sampleEnv, nil
}
// Ensure makes sure the recipe exists, is up to date and has the latest version checked out.
func Ensure(recipeName string) error {
if err := EnsureExists(recipeName); err != nil {
return err
}
if err := EnsureUpToDate(recipeName); err != nil {
return err
}
if err := EnsureLatest(recipeName); err != nil {
return err
}
return nil
}
// EnsureExists ensures that a recipe is locally cloned
func EnsureExists(recipeName string) error {
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
if _, err := os.Stat(recipeDir); os.IsNotExist(err) {
log.Debugf("%s does not exist, attemmpting to clone", recipeDir)
url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, recipeName)
if err := gitPkg.Clone(recipeDir, url); err != nil {
return err
}
}
if err := gitPkg.EnsureGitRepo(recipeDir); err != nil {
return err
}
return nil
}
// EnsureVersion checks whether a specific version exists for a recipe.
func EnsureVersion(recipeName, version string) error {
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
if err := gitPkg.EnsureGitRepo(recipeDir); err != nil {
return err
}
repo, err := git.PlainOpen(recipeDir)
if err != nil {
return err
}
tags, err := repo.Tags()
if err != nil {
return nil
}
var parsedTags []string
var tagRef plumbing.ReferenceName
if err := tags.ForEach(func(ref *plumbing.Reference) (err error) {
parsedTags = append(parsedTags, ref.Name().Short())
if ref.Name().Short() == version {
tagRef = ref.Name()
}
return nil
}); err != nil {
return err
}
joinedTags := strings.Join(parsedTags, ", ")
if joinedTags != "" {
log.Debugf("read %s as tags for recipe %s", joinedTags, recipeName)
}
if tagRef.String() == "" {
return fmt.Errorf("the local copy of %s doesn't seem to have version %s available?", recipeName, version)
}
worktree, err := repo.Worktree()
if err != nil {
return err
}
opts := &git.CheckoutOptions{
Branch: tagRef,
Create: false,
Force: true,
}
if err := worktree.Checkout(opts); err != nil {
return err
}
log.Debugf("successfully checked %s out to %s in %s", recipeName, tagRef.Short(), recipeDir)
return nil
}
// EnsureIsClean makes sure that the recipe repository has no unstaged changes.
func EnsureIsClean(recipeName string) error {
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
isClean, err := gitPkg.IsClean(recipeDir)
if err != nil {
return fmt.Errorf("unable to check git clean status in %s: %s", recipeDir, err)
}
if !isClean {
msg := "%s (%s) has locally unstaged changes? please commit/remove your changes before proceeding"
return fmt.Errorf(msg, recipeName, recipeDir)
}
return nil
}
// EnsureLatest makes sure the latest commit is checked out for a local recipe repository
func EnsureLatest(recipeName string) error {
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
if err := gitPkg.EnsureGitRepo(recipeDir); err != nil {
return err
}
repo, err := git.PlainOpen(recipeDir)
if err != nil {
return err
}
worktree, err := repo.Worktree()
if err != nil {
return err
}
branch, err := gitPkg.GetDefaultBranch(repo, recipeDir)
if err != nil {
return err
}
checkOutOpts := &git.CheckoutOptions{
Create: false,
Force: true,
Branch: plumbing.ReferenceName(branch),
}
if err := worktree.Checkout(checkOutOpts); err != nil {
log.Debugf("failed to check out %s in %s", branch, recipeDir)
return err
}
return nil
}
// ChaosVersion constructs a chaos mode recipe version.
func ChaosVersion(recipeName string) (string, error) {
var version string
head, err := gitPkg.GetRecipeHead(recipeName)
if err != nil {
return version, err
}
version = formatter.SmallSHA(head.String())
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
isClean, err := gitPkg.IsClean(recipeDir)
if err != nil {
return version, err
}
if !isClean {
version = fmt.Sprintf("%s + unstaged changes", version)
}
return version, nil
ComposePath string
ReadmePath string
SampleEnvPath string
AbraShPath string
}
// GetRecipesLocal retrieves all local recipe directories
@ -446,41 +159,20 @@ func GetRecipesLocal() ([]string, error) {
return recipes, nil
}
// GetVersionLabelLocal retrieves the version label on the local recipe config
func GetVersionLabelLocal(recipe Recipe) (string, error) {
var label string
for _, service := range recipe.Config.Services {
for label, value := range service.Deploy.Labels {
if strings.HasPrefix(label, "coop-cloud") && strings.Contains(label, "version") {
return value, nil
}
}
}
if label == "" {
return label, fmt.Errorf("%s has no version label? try running \"abra recipe sync %s\" first?", recipe.Name, recipe.Name)
}
return label, nil
}
func GetRecipeFeaturesAndCategory(recipeName string) (Features, string, error) {
func GetRecipeFeaturesAndCategory(r Recipe) (Features, string, error) {
feat := Features{}
var category string
readmePath := path.Join(config.RECIPES_DIR, recipeName, "README.md")
log.Debugf("attempting to open %s for recipe metadata parsing", r.ReadmePath)
log.Debugf("attempting to open %s for recipe metadata parsing", readmePath)
readmeFS, err := ioutil.ReadFile(readmePath)
readmeFS, err := ioutil.ReadFile(r.ReadmePath)
if err != nil {
return feat, category, err
}
readmeMetadata, err := GetStringInBetween( // Find text between delimiters
recipeName,
r.Name,
string(readmeFS),
"<!-- metadata -->", "<!-- endmetadata -->",
)
@ -531,7 +223,7 @@ func GetRecipeFeaturesAndCategory(recipeName string) (Features, string, error) {
if strings.Contains(val, "**Image**") {
imageMetadata, err := GetImageMetadata(strings.TrimSpace(
strings.TrimPrefix(val, "* **Image**:"),
), recipeName)
), r.Name)
if err != nil {
continue
}
@ -597,59 +289,6 @@ func GetStringInBetween(recipeName, str, start, end string) (result string, err
return str[s : s+e], nil
}
// EnsureUpToDate ensures that the local repo is synced to the remote
func EnsureUpToDate(recipeName string) error {
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
repo, err := git.PlainOpen(recipeDir)
if err != nil {
return fmt.Errorf("unable to open %s: %s", recipeDir, err)
}
remotes, err := repo.Remotes()
if err != nil {
return fmt.Errorf("unable to read remotes in %s: %s", recipeDir, err)
}
if len(remotes) == 0 {
log.Debugf("cannot ensure %s is up-to-date, no git remotes configured", recipeName)
return nil
}
worktree, err := repo.Worktree()
if err != nil {
return fmt.Errorf("unable to open git work tree in %s: %s", recipeDir, err)
}
branch, err := gitPkg.CheckoutDefaultBranch(repo, recipeDir)
if err != nil {
return fmt.Errorf("unable to check out default branch in %s: %s", recipeDir, err)
}
fetchOpts := &git.FetchOptions{Tags: git.AllTags}
if err := repo.Fetch(fetchOpts); err != nil {
if !strings.Contains(err.Error(), "already up-to-date") {
return fmt.Errorf("unable to fetch tags in %s: %s", recipeDir, err)
}
}
opts := &git.PullOptions{
Force: true,
ReferenceName: branch,
SingleBranch: true,
}
if err := worktree.Pull(opts); err != nil {
if !strings.Contains(err.Error(), "already up-to-date") {
return fmt.Errorf("unable to git pull in %s: %s", recipeDir, err)
}
}
log.Debugf("fetched latest git changes for %s", recipeName)
return nil
}
// ReadRecipeCatalogue reads the recipe catalogue.
func ReadRecipeCatalogue(offline bool) (RecipeCatalogue, error) {
recipes := make(RecipeCatalogue)
@ -864,96 +503,6 @@ func ReadReposMetadata() (RepoCatalogue, error) {
return reposMeta, nil
}
// GetRecipeVersions retrieves all recipe versions.
func GetRecipeVersions(recipeName string, offline bool) (RecipeVersions, error) {
versions := RecipeVersions{}
recipeDir := path.Join(config.RECIPES_DIR, recipeName)
log.Debugf("attempting to open git repository in %s", recipeDir)
repo, err := git.PlainOpen(recipeDir)
if err != nil {
return versions, err
}
worktree, err := repo.Worktree()
if err != nil {
return versions, err
}
gitTags, err := repo.Tags()
if err != nil {
return versions, err
}
if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) {
tag := strings.TrimPrefix(string(ref.Name()), "refs/tags/")
log.Debugf("processing %s for %s", tag, recipeName)
checkOutOpts := &git.CheckoutOptions{
Create: false,
Force: true,
Branch: plumbing.ReferenceName(ref.Name()),
}
if err := worktree.Checkout(checkOutOpts); err != nil {
log.Debugf("failed to check out %s in %s", tag, recipeDir)
return err
}
log.Debugf("successfully checked out %s in %s", ref.Name(), recipeDir)
recipe, err := Get(recipeName, offline)
if err != nil {
return err
}
versionMeta := make(map[string]ServiceMeta)
for _, service := range recipe.Config.Services {
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
return err
}
path := reference.Path(img)
path = formatter.StripTagMeta(path)
var tag string
switch img.(type) {
case reference.NamedTagged:
tag = img.(reference.NamedTagged).Tag()
case reference.Named:
log.Warnf("%s service is missing image tag?", path)
continue
}
versionMeta[service.Name] = ServiceMeta{
Image: path,
Tag: tag,
}
}
versions = append(versions, map[string]map[string]ServiceMeta{tag: versionMeta})
return nil
}); err != nil {
return versions, err
}
_, err = gitPkg.CheckoutDefaultBranch(repo, recipeDir)
if err != nil {
return versions, err
}
sortRecipeVersions(versions)
log.Debugf("collected %s for %s", versions, recipeName)
return versions, nil
}
// sortRecipeVersions sorts the recipe semver versions
func sortRecipeVersions(versions RecipeVersions) {
sort.Slice(versions, func(i, j int) bool {
@ -1034,8 +583,7 @@ func UpdateRepositories(repos RepoCatalogue, recipeName string) error {
return
}
recipeDir := path.Join(config.RECIPES_DIR, rm.Name)
if err := gitPkg.Clone(recipeDir, rm.CloneURL); err != nil {
if err := gitPkg.Clone(Get(rm.Name).Dir, rm.CloneURL); err != nil {
log.Fatal(err)
}
@ -1063,50 +611,3 @@ func ensurePathExists(path string) error {
}
return nil
}
// GetComposeFiles gets the list of compose files for an app (or recipe if you
// don't already have an app) which should be merged into a composetypes.Config
// while respecting the COMPOSE_FILE env var.
func GetComposeFiles(recipe string, appEnv map[string]string) ([]string, error) {
var composeFiles []string
composeFileEnvVar, ok := appEnv["COMPOSE_FILE"]
if !ok {
path := fmt.Sprintf("%s/%s/compose.yml", config.RECIPES_DIR, recipe)
if err := ensurePathExists(path); err != nil {
return composeFiles, err
}
log.Debugf("no COMPOSE_FILE detected, loading default: %s", path)
composeFiles = append(composeFiles, path)
return composeFiles, nil
}
if !strings.Contains(composeFileEnvVar, ":") {
path := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, recipe, composeFileEnvVar)
if err := ensurePathExists(path); err != nil {
return composeFiles, err
}
log.Debugf("COMPOSE_FILE detected, loading %s", path)
composeFiles = append(composeFiles, path)
return composeFiles, nil
}
numComposeFiles := strings.Count(composeFileEnvVar, ":") + 1
envVars := strings.SplitN(composeFileEnvVar, ":", numComposeFiles)
if len(envVars) != numComposeFiles {
return composeFiles, fmt.Errorf("COMPOSE_FILE (=\"%s\") parsing failed?", composeFileEnvVar)
}
for _, file := range envVars {
path := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, recipe, file)
if err := ensurePathExists(path); err != nil {
return composeFiles, err
}
composeFiles = append(composeFiles, path)
}
log.Debugf("COMPOSE_FILE detected (%s), loading %s", composeFileEnvVar, strings.Join(envVars, ", "))
log.Debugf("retrieved %s configs for %s", strings.Join(composeFiles, ", "), recipe)
return composeFiles, nil
}

View File

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

View File

@ -246,7 +246,8 @@ type secretStatuses []secretStatus
func PollSecretsStatus(cl *dockerClient.Client, app appPkg.App) (secretStatuses, error) {
var secStats secretStatuses
composeFiles, err := recipe.GetComposeFiles(app.Recipe, app.Env)
r := recipe.Get(app.Name)
composeFiles, err := r.GetComposeFiles(app.Env)
if err != nil {
return secStats, err
}

View File

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