0
0
forked from toolshed/abra

Compare commits

..

16 Commits

Author SHA1 Message Date
8cbf118f66 feat: introduce local recipes 2025-11-04 09:18:29 +01:00
cc8703310c chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-11-04 07:35:14 +00:00
fcd5bd863d chore: make i18n 2025-11-04 08:35:04 +01:00
e6af2da9dd refactor: named note, merge if clause 2025-11-04 08:34:49 +01:00
4b688825e0 feat: create docker context when server folder does exist 2025-11-03 17:29:04 +01:00
b0cf2a1f8e chore: make i18n 2025-11-02 10:44:34 +00:00
6b7020d457 test: env version to .config.env 2025-11-02 10:44:34 +00:00
efdac610bd fix: skip local server on it's own 2025-11-02 10:44:34 +00:00
cd6021f116 fix: expose new version
See toolshed/abra#713
2025-11-02 10:44:34 +00:00
ee8de8ef5c chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-10-31 20:38:18 +00:00
e5a653c002 chore: make i18n 2025-10-31 21:38:01 +01:00
2cca04de90 fix(move): does not error when secret already exists on new server
See toolshed/abra#709
2025-10-31 21:37:55 +01:00
f2f79e2df8 chore: update translation files
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-10-31 20:33:36 +00:00
dd83741a9f chore: i18n 2025-10-31 21:31:49 +01:00
dc2cd85d91 feat!: abra app env pull
`abra app env` -> `abra app env list`.

See toolshed/abra#497
2025-10-31 21:31:43 +01:00
96e59cf196 test: adjust to match new reality [ci skip] 2025-10-31 14:35:57 +01:00
23 changed files with 1161 additions and 347 deletions

View File

@ -152,7 +152,6 @@ checkout as-is. Recipe commit hashes are also supported as values for
log.Fatal(err) log.Fatal(err)
} }
appPkg.ExposeAllEnv(stackName, compose, app.Env)
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name) appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
appPkg.SetChaosLabel(compose, stackName, internal.Chaos) appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
if internal.Chaos { if internal.Chaos {
@ -171,6 +170,9 @@ checkout as-is. Recipe commit hashes are also supported as values for
} }
appPkg.SetVersionLabel(compose, stackName, versionLabel) appPkg.SetVersionLabel(compose, stackName, versionLabel)
newRecipeWithDeployVersion := fmt.Sprintf("%s:%s", app.Recipe.Name, toDeployVersion)
appPkg.ExposeAllEnv(stackName, compose, app.Env, newRecipeWithDeployVersion)
envVars, err := appPkg.CheckEnv(app) envVars, err := appPkg.CheckEnv(app)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)

View File

@ -1,28 +1,50 @@
package app package app
import ( import (
"context"
"fmt" "fmt"
"os"
"path"
"path/filepath"
"regexp"
"sort" "sort"
"strings" "strings"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/app"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
containerPkg "coopcloud.tech/abra/pkg/container"
contextPkg "coopcloud.tech/abra/pkg/context"
"coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n" "coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/docker/api/types/filters"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// translators: `abra app env` aliases. use a comma separated list of aliases with // translators: `abra app env` aliases. use a comma separated list of aliases
// no spaces in between // with no spaces in between
var appEnvAliases = i18n.G("e") var appEnvAliases = i18n.G("e")
var AppEnvCommand = &cobra.Command{ // translators: `abra app env list` aliases. use a comma separated list of
// translators: `app env` command // aliases with no spaces in between
Use: i18n.G("env <domain> [flags]"), var appEnvListAliases = i18n.G("l,ls")
Aliases: strings.Split(appEnvAliases, ","),
// translators: Short description for `app env` command // translators: `abra app env pull` aliases. use a comma separated list of
Short: i18n.G("Show app .env values"), // aliases with no spaces in between
Example: i18n.G(" abra app env 1312.net"), var appEnvPullAliases = i18n.G("pl,p")
var AppEnvListCommand = &cobra.Command{
// translators: `app env list` command
Use: i18n.G("list <domain> [flags]"),
Aliases: strings.Split(appEnvListAliases, ","),
// translators: Short description for `app env list` command
Short: i18n.G("List all app environment values"),
Example: i18n.G(" abra app env list 1312.net"),
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
@ -49,3 +71,274 @@ var AppEnvCommand = &cobra.Command{
fmt.Println(overview) fmt.Println(overview)
}, },
} }
var AppEnvPullCommand = &cobra.Command{
// translators: `app pull` command
Use: i18n.G("pull <domain> [flags]"),
Aliases: strings.Split(appEnvPullAliases, ","),
// translators: Short description for `app env pull` command
Short: i18n.G("Pull app environment values from a deployed app"),
Long: i18n.G(`Pull app environment values from a deploymed app.
A convenient command for when you've lost your app environment file or want to
synchronize your local app environment values with what is deployed live.`),
Example: i18n.G(` # pull existing .env file and overwrite local values
abra app env pull 1312.net --force
# pull lost app .env file
abra app env pull my.gitea.net --server 1312.net`),
Args: cobra.MaximumNArgs(2),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.AppNameComplete()
},
Run: func(cmd *cobra.Command, args []string) {
appName := args[0]
appEnvPath := path.Join(config.ABRA_DIR, "servers", server, fmt.Sprintf("%s.env", appName))
if _, err := os.Stat(appEnvPath); !os.IsNotExist(err) {
log.Fatal(i18n.G("%s already exists?", appEnvPath))
}
if server == "" {
log.Fatal(i18n.G("unable to determine server of app %s, please pass --server/-s", appName))
}
serverDir := filepath.Join(config.SERVERS_DIR, server)
if _, err := os.Stat(serverDir); os.IsNotExist(err) {
log.Fatal(i18n.G("unknown server %s, run \"abra server add %s\"?", server, server))
}
store := contextPkg.NewDefaultDockerContextStore()
contexts, err := store.Store.List()
if err != nil {
log.Fatal(i18n.G("unable to look up server context for %s: %s", server, err))
}
var contextCreated bool
if server == "default" {
contextCreated = true
}
for _, context := range contexts {
if context.Name == server {
contextCreated = true
}
}
if !contextCreated {
log.Fatal(i18n.G("%s missing context, run \"abra server add %s\"?", server, server))
}
cl, err := client.New(server)
if err != nil {
log.Fatal(err)
}
deployMeta, err := stack.IsDeployed(context.Background(), cl, appPkg.StackName(appName))
if err != nil {
log.Fatal(err)
}
if !deployMeta.IsDeployed {
log.Fatal(i18n.G("%s is not deployed?", appName))
}
filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(appName), "app"))
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, internal.NoInput)
if err != nil {
log.Fatal(i18n.G("unable to retrieve container for %s: %s", appName, err))
}
inspectResult, err := cl.ContainerInspect(context.Background(), targetContainer.ID)
if err != nil {
log.Fatal(i18n.G("unable to inspect container for %s: %s", appName, err))
}
deploymentEnv := make(map[string]string)
for _, envVar := range inspectResult.Config.Env {
split := strings.SplitN(envVar, "=", 2)
if len(split) != 2 {
log.Debug(i18n.G("no value attached to %s", envVar))
continue
}
key, val := split[0], split[1]
deploymentEnv[key] = val
}
log.Debug(i18n.G("pulled env values from %s deployment: %s", appName, deploymentEnv))
var (
recipeEnvVar string
recipeKey string
)
if r, ok := deploymentEnv["TYPE"]; ok {
recipeKey = "TYPE"
recipeEnvVar = r
}
if r, ok := deploymentEnv["RECIPE"]; ok {
recipeKey = "RECIPE"
recipeEnvVar = r
}
if recipeEnvVar == "" {
log.Fatal(i18n.G("unable to determine recipe type from %s, env: %v", appName, inspectResult.Config.Env))
}
var recipeName = recipeEnvVar
if strings.Contains(recipeEnvVar, ":") {
split := strings.Split(recipeEnvVar, ":")
recipeName = split[0]
}
recipe := internal.ValidateRecipe(
[]string{recipeName},
cmd.Name(),
)
version := deployMeta.Version
if deployMeta.IsChaos {
version = deployMeta.ChaosVersion
}
if _, err := recipe.EnsureVersion(version); err != nil {
log.Fatal(err)
}
mergedEnv, err := recipe.SampleEnv()
if err != nil {
log.Fatal(err)
}
log.Debug(i18n.G("retrieved env values from .env.sample of %s: %s", recipe.Name, mergedEnv))
for k, v := range deploymentEnv {
mergedEnv[k] = v
}
if !strings.Contains(recipeEnvVar, ":") {
mergedEnv[recipeKey] = fmt.Sprintf("%s:%s", mergedEnv[recipeKey], version)
}
log.Debug(i18n.G("final merged env values for %s are: %s", appName, mergedEnv))
envSample, err := os.ReadFile(recipe.SampleEnvPath)
if err != nil {
log.Fatal(err)
}
err = os.WriteFile(appEnvPath, envSample, 0o664)
if err != nil {
log.Fatal(i18n.G("unable to write new env %s: %s", appEnvPath, err))
}
read, err := os.ReadFile(appEnvPath)
if err != nil {
log.Fatal(i18n.G("unable to read new env %s: %s", appEnvPath, err))
}
sampleEnv, err := recipe.SampleEnv()
if err != nil {
log.Fatal(err)
}
var composeFileUpdated bool
newContents := string(read)
for key, val := range mergedEnv {
if sampleEnv[key] == val {
continue
}
if key == "COMPOSE_FILE" {
composeFileUpdated = true
continue
}
if m, _ := regexp.MatchString(fmt.Sprintf(`#%s=`, key), newContents); m {
log.Debug(i18n.G("uncommenting %s", key))
re := regexp.MustCompile(fmt.Sprintf(`#%s=`, key))
newContents = re.ReplaceAllString(newContents, fmt.Sprintf("%s=", key))
}
if m, _ := regexp.MatchString(fmt.Sprintf(`# %s=`, key), newContents); m {
log.Debug(i18n.G("uncommenting %s", key))
re := regexp.MustCompile(fmt.Sprintf(`# %s=`, key))
newContents = re.ReplaceAllString(newContents, fmt.Sprintf("%s=", key))
}
if m, _ := regexp.MatchString(fmt.Sprintf(`%s=".*"`, key), newContents); m {
log.Debug(i18n.G(`inserting %s="%s" (double quotes)`, key, val))
re := regexp.MustCompile(fmt.Sprintf(`%s=".*"`, key))
newContents = re.ReplaceAllString(newContents, fmt.Sprintf(`%s="%s"`, key, val))
continue
}
if m, _ := regexp.MatchString(fmt.Sprintf(`%s='.*'`, key), newContents); m {
log.Debug(i18n.G(`inserting %s='%s' (single quotes)`, key, val))
re := regexp.MustCompile(fmt.Sprintf(`%s='.*'`, key))
newContents = re.ReplaceAllString(newContents, fmt.Sprintf(`%s='%s'`, key, val))
continue
}
if m, _ := regexp.MatchString(fmt.Sprintf("%s=.*", key), newContents); m {
log.Debug(i18n.G("inserting %s=%s (no quotes)", key, val))
re := regexp.MustCompile(fmt.Sprintf("%s=.*", key))
newContents = re.ReplaceAllString(newContents, fmt.Sprintf("%s=%s", key, val))
}
}
err = os.WriteFile(appEnvPath, []byte(newContents), 0)
if err != nil {
log.Fatal(i18n.G("unable to write new env %s: %s", appEnvPath, err))
}
log.Info(i18n.G("%s successfully created", appEnvPath))
if composeFileUpdated {
log.Warn(i18n.G("manual update required: COMPOSE_FILE=\"%s\"", mergedEnv["COMPOSE_FILE"]))
}
},
}
var AppEnvCommand = &cobra.Command{
// translators: `app env` command group
Use: i18n.G("env [cmd] [args] [flags]"),
Aliases: strings.Split(appEnvAliases, ","),
// translators: Short description for `app env` command group
Short: i18n.G("Manage app environment values"),
}
var (
server string
)
func init() {
AppEnvPullCommand.Flags().BoolVarP(
&internal.Force,
i18n.G("force"),
i18n.G("f"),
false,
i18n.G("perform action without further prompt"),
)
AppEnvPullCommand.Flags().StringVarP(
&server,
i18n.G("server"),
i18n.G("s"),
"",
i18n.G("server associated with deployed app"),
)
AppEnvPullCommand.RegisterFlagCompletionFunc(
i18n.G("server"),
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return autocomplete.ServerNameComplete()
},
)
}

View File

@ -128,6 +128,10 @@ Use "--dry-run/-r" to see which secrets and volumes will be moved.`),
secretName := strings.Join(sname[:len(sname)-1], "_") secretName := strings.Join(sname[:len(sname)-1], "_")
data := resources.Secrets[secretName] data := resources.Secrets[secretName]
if err := client.StoreSecret(newServerClient, s.Spec.Name, data); err != nil { if err := client.StoreSecret(newServerClient, s.Spec.Name, data); err != nil {
if strings.Contains(err.Error(), "already exists") {
log.Info(i18n.G("skipping secret (because it already exists) on %s: %s", s.Spec.Name, newServer))
continue
}
log.Fatal(i18n.G("failed to store secret on %s: %s", err, newServer)) log.Fatal(i18n.G("failed to store secret on %s: %s", err, newServer))
} }
log.Info(i18n.G("created secret on %s: %s", s.Spec.Name, newServer)) log.Info(i18n.G("created secret on %s: %s", s.Spec.Name, newServer))

View File

@ -2,6 +2,7 @@ package app
import ( import (
"errors" "errors"
"fmt"
"strings" "strings"
appPkg "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app"
@ -177,7 +178,9 @@ beforehand. See "abra app backup" for more.`),
log.Fatal(err) log.Fatal(err)
} }
appPkg.ExposeAllEnv(stackName, compose, app.Env) newRecipeWithDowngradeVersion := fmt.Sprintf("%s:%s", app.Recipe.Name, chosenDowngrade)
appPkg.ExposeAllEnv(stackName, compose, app.Env, newRecipeWithDowngradeVersion)
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name) appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
appPkg.SetChaosLabel(compose, stackName, internal.Chaos) appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
if internal.Chaos { if internal.Chaos {

View File

@ -28,6 +28,7 @@ var AppUndeployCommand = &cobra.Command{
Use: i18n.G("undeploy <domain> [flags]"), Use: i18n.G("undeploy <domain> [flags]"),
// translators: Short description for `app undeploy` command // translators: Short description for `app undeploy` command
Aliases: strings.Split(appUndeployAliases, ","), Aliases: strings.Split(appUndeployAliases, ","),
Short: i18n.G("Undeploy a deployed app"),
Long: i18n.G(`This does not destroy any application data. Long: i18n.G(`This does not destroy any application data.
However, you should remain vigilant, as your swarm installation will consider However, you should remain vigilant, as your swarm installation will consider

View File

@ -190,7 +190,9 @@ beforehand. See "abra app backup" for more.`),
log.Fatal(err) log.Fatal(err)
} }
appPkg.ExposeAllEnv(stackName, compose, app.Env) newRecipeWithUpgradeVersion := fmt.Sprintf("%s:%s", app.Recipe.Name, chosenUpgrade)
appPkg.ExposeAllEnv(stackName, compose, app.Env, newRecipeWithUpgradeVersion)
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name) appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
appPkg.SetChaosLabel(compose, stackName, internal.Chaos) appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
if internal.Chaos { if internal.Chaos {

View File

@ -42,8 +42,7 @@ local file system.`),
ValidArgsFunction: func( ValidArgsFunction: func(
cmd *cobra.Command, cmd *cobra.Command,
args []string, args []string,
toComplete string, toComplete string) ([]string, cobra.ShellCompDirective) {
) ([]string, cobra.ShellCompDirective) {
switch l := len(args); l { switch l := len(args); l {
case 0: case 0:
return autocomplete.RecipeNameComplete() return autocomplete.RecipeNameComplete()
@ -132,16 +131,10 @@ likely to change.
log.Fatal(err) log.Fatal(err)
} }
latestRelease := "0.0.0+0.0.0" latestRelease := tags[len(tags)-1]
if len(tags) > 0 {
latestRelease = tags[len(tags)-1]
}
changesTable.Headers(i18n.G("SERVICE"), latestRelease, i18n.G("PROPOSED CHANGES")) changesTable.Headers(i18n.G("SERVICE"), latestRelease, i18n.G("PROPOSED CHANGES"))
latestRecipeVersion := latestRelease latestRecipeVersion := versions[len(versions)-1]
if len(versions) > 0 {
latestRecipeVersion = versions[len(versions)-1]
}
allRecipeVersions := catl[recipe.Name].Versions allRecipeVersions := catl[recipe.Name].Versions
for _, recipeVersion := range allRecipeVersions { for _, recipeVersion := range allRecipeVersions {
if serviceVersions, ok := recipeVersion[latestRecipeVersion]; ok { if serviceVersions, ok := recipeVersion[latestRecipeVersion]; ok {

View File

@ -283,6 +283,11 @@ Config:
app.AppBackupSnapshotsCommand, app.AppBackupSnapshotsCommand,
) )
app.AppEnvCommand.AddCommand(
app.AppEnvListCommand,
app.AppEnvPullCommand,
)
app.AppCommand.AddCommand( app.AppCommand.AddCommand(
app.AppBackupCommand, app.AppBackupCommand,
app.AppCheckCommand, app.AppCheckCommand,

View File

@ -502,7 +502,12 @@ func GetAppComposeConfig(recipe string, opts stack.Deploy, appEnv envfile.AppEnv
} }
// ExposeAllEnv exposes all env variables to the app container // ExposeAllEnv exposes all env variables to the app container
func ExposeAllEnv(stackName string, compose *composetypes.Config, appEnv envfile.AppEnv) { func ExposeAllEnv(
stackName string,
compose *composetypes.Config,
appEnv envfile.AppEnv,
toDeployVersion string,
) {
for _, service := range compose.Services { for _, service := range compose.Services {
if service.Name == "app" { if service.Name == "app" {
log.Debug(i18n.G("adding env vars to %s service config", stackName)) log.Debug(i18n.G("adding env vars to %s service config", stackName))
@ -510,6 +515,11 @@ func ExposeAllEnv(stackName string, compose *composetypes.Config, appEnv envfile
_, exists := service.Environment[k] _, exists := service.Environment[k]
if !exists { if !exists {
value := v value := v
if k == "TYPE" || k == "RECIPE" {
// NOTE(d1): don't use the wrong version from the app env
// since we are deploying a new version
value = toDeployVersion
}
service.Environment[k] = &value service.Environment[k] = &value
log.Debug(i18n.G("%s: %s: %s", stackName, k, value)) log.Debug(i18n.G("%s: %s: %s", stackName, k, value))
} }
@ -624,6 +634,10 @@ func (a App) WipeRecipeVersion() error {
// WriteRecipeVersion writes the recipe version to the app .env file. // WriteRecipeVersion writes the recipe version to the app .env file.
func (a App) WriteRecipeVersion(version string, dryRun bool) error { func (a App) WriteRecipeVersion(version string, dryRun bool) error {
if a.Recipe.Local {
return nil
}
file, err := os.Open(a.Path) file, err := os.Open(a.Path)
if err != nil { if err != nil {
return err return err

View File

@ -44,11 +44,20 @@ func New(serverName string, opts ...Opt) (*client.Client, error) {
ctx, err := GetContext(serverName) ctx, err := GetContext(serverName)
if err != nil { if err != nil {
serverDir := path.Join(config.SERVERS_DIR, serverName) serverDir := path.Join(config.SERVERS_DIR, serverName)
if _, err := os.Stat(serverDir); err == nil { if _, err := os.Stat(serverDir); err != nil {
return nil, errors.New(i18n.G("server missing context, run \"abra server add %s\"?", serverName)) return nil, errors.New(i18n.G("server missing, run \"abra server add %s\"?", serverName))
} }
return nil, errors.New(i18n.G("unknown server, run \"abra server add %s\"?", serverName)) // NOTE(p4u1): when the docker context does not exist but the server folder
// is there, let's create a new docker context.
if err = CreateContext(serverName); err != nil {
return nil, errors.New(i18n.G("server missing context, context creation failed: %s", err))
}
ctx, err = GetContext(serverName)
if err != nil {
return nil, errors.New(i18n.G("server missing context, run \"abra server add %s\"?", serverName))
}
} }
ctxEndpoint, err := contextPkg.GetContextEndpoint(ctx) ctxEndpoint, err := contextPkg.GetContextEndpoint(ctx)

View File

@ -282,11 +282,7 @@ func GatherImagesForDeploy(cl *dockerClient.Client, app appPkg.App, compose *com
} }
imageBaseName := reference.Path(imageParsed) imageBaseName := reference.Path(imageParsed)
namedTag, ok := imageParsed.(reference.NamedTagged) imageTag := imageParsed.(reference.NamedTagged).Tag()
if !ok {
continue
}
imageTag := namedTag.Tag()
existingImageVersion, ok := newImages[imageBaseName] existingImageVersion, ok := newImages[imageBaseName]
if !ok { if !ok {

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -486,6 +486,10 @@ func LintSecretLengths(recipe recipe.Recipe) (bool, error) {
} }
func LintValidTags(recipe recipe.Recipe) (bool, error) { func LintValidTags(recipe recipe.Recipe) (bool, error) {
if recipe.Local {
return true, nil
}
repo, err := git.PlainOpen(recipe.Dir) repo, err := git.PlainOpen(recipe.Dir)
if err != nil { if err != nil {
return false, errors.New(i18n.G("unable to open %s: %s", recipe.Dir, err)) return false, errors.New(i18n.G("unable to open %s: %s", recipe.Dir, err))

View File

@ -28,6 +28,10 @@ type EnsureContext struct {
// Ensure makes sure the recipe exists, is up to date and has the specific // Ensure makes sure the recipe exists, is up to date and has the specific
// version checked out. // version checked out.
func (r Recipe) Ensure(ctx EnsureContext) error { func (r Recipe) Ensure(ctx EnsureContext) error {
if r.Local {
return nil
}
if err := r.EnsureExists(); err != nil { if err := r.EnsureExists(); err != nil {
return err return err
} }
@ -68,6 +72,10 @@ func (r Recipe) Ensure(ctx EnsureContext) error {
// EnsureExists ensures that the recipe is locally cloned // EnsureExists ensures that the recipe is locally cloned
func (r Recipe) EnsureExists() error { func (r Recipe) EnsureExists() error {
if r.Local {
return nil
}
if _, err := os.Stat(r.Dir); os.IsNotExist(err) { if _, err := os.Stat(r.Dir); os.IsNotExist(err) {
if err := gitPkg.Clone(r.Dir, r.GitURL); err != nil { if err := gitPkg.Clone(r.Dir, r.GitURL); err != nil {
return err return err
@ -83,6 +91,10 @@ func (r Recipe) EnsureExists() error {
// IsChaosCommit determines if a version sttring is a chaos commit or not. // IsChaosCommit determines if a version sttring is a chaos commit or not.
func (r Recipe) IsChaosCommit(version string) (bool, error) { func (r Recipe) IsChaosCommit(version string) (bool, error) {
if r.Local {
return false, nil
}
isChaosCommit := false isChaosCommit := false
if err := gitPkg.EnsureGitRepo(r.Dir); err != nil { if err := gitPkg.EnsureGitRepo(r.Dir); err != nil {
@ -118,6 +130,10 @@ func (r Recipe) IsChaosCommit(version string) (bool, error) {
// EnsureVersion checks whether a specific version exists for a recipe. // EnsureVersion checks whether a specific version exists for a recipe.
func (r Recipe) EnsureVersion(version string) (bool, error) { func (r Recipe) EnsureVersion(version string) (bool, error) {
if r.Local {
return false, nil
}
isChaosCommit := false isChaosCommit := false
if err := gitPkg.EnsureGitRepo(r.Dir); err != nil { if err := gitPkg.EnsureGitRepo(r.Dir); err != nil {
@ -182,6 +198,10 @@ func (r Recipe) EnsureVersion(version string) (bool, error) {
// EnsureIsClean makes sure that the recipe repository has no unstaged changes. // EnsureIsClean makes sure that the recipe repository has no unstaged changes.
func (r Recipe) EnsureIsClean() error { func (r Recipe) EnsureIsClean() error {
if r.Local {
return nil
}
isClean, err := gitPkg.IsClean(r.Dir) isClean, err := gitPkg.IsClean(r.Dir)
if err != nil { if err != nil {
return errors.New(i18n.G("unable to check git clean status in %s: %s", r.Dir, err)) return errors.New(i18n.G("unable to check git clean status in %s: %s", r.Dir, err))
@ -196,6 +216,10 @@ func (r Recipe) EnsureIsClean() error {
// EnsureLatest makes sure the latest commit is checked out for the local recipe repository // EnsureLatest makes sure the latest commit is checked out for the local recipe repository
func (r Recipe) EnsureLatest() error { func (r Recipe) EnsureLatest() error {
if r.Local {
return nil
}
if err := gitPkg.EnsureGitRepo(r.Dir); err != nil { if err := gitPkg.EnsureGitRepo(r.Dir); err != nil {
return err return err
} }
@ -231,6 +255,10 @@ func (r Recipe) EnsureLatest() error {
// EnsureUpToDate ensures that the local repo is synced to the remote // EnsureUpToDate ensures that the local repo is synced to the remote
func (r Recipe) EnsureUpToDate() error { func (r Recipe) EnsureUpToDate() error {
if r.Local {
return nil
}
repo, err := git.PlainOpen(r.Dir) repo, err := git.PlainOpen(r.Dir)
if err != nil { if err != nil {
return errors.New(i18n.G("unable to open %s: %s", r.Dir, err)) return errors.New(i18n.G("unable to open %s: %s", r.Dir, err))
@ -282,6 +310,10 @@ func (r Recipe) EnsureUpToDate() error {
// IsDirty checks whether a recipe is dirty or not. // IsDirty checks whether a recipe is dirty or not.
func (r *Recipe) IsDirty() (bool, error) { func (r *Recipe) IsDirty() (bool, error) {
if r.Local {
return false, nil
}
isClean, err := gitPkg.IsClean(r.Dir) isClean, err := gitPkg.IsClean(r.Dir)
if err != nil { if err != nil {
return false, err return false, err
@ -292,6 +324,10 @@ func (r *Recipe) IsDirty() (bool, error) {
// ChaosVersion constructs a chaos mode recipe version. // ChaosVersion constructs a chaos mode recipe version.
func (r *Recipe) ChaosVersion() (string, error) { func (r *Recipe) ChaosVersion() (string, error) {
if r.Local {
return "", nil
}
var version string var version string
head, err := r.Head() head, err := r.Head()
@ -315,6 +351,10 @@ func (r *Recipe) ChaosVersion() (string, error) {
// Push pushes the latest changes to a SSH URL remote. You need to have your // 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 // local SSH configuration for git.coopcloud.tech working for this to work
func (r Recipe) Push(dryRun bool) error { func (r Recipe) Push(dryRun bool) error {
if r.Local {
return nil
}
repo, err := git.PlainOpen(r.Dir) repo, err := git.PlainOpen(r.Dir)
if err != nil { if err != nil {
return err return err
@ -333,6 +373,10 @@ func (r Recipe) Push(dryRun bool) error {
// Tags list the recipe tags // Tags list the recipe tags
func (r Recipe) Tags() ([]string, error) { func (r Recipe) Tags() ([]string, error) {
if r.Local {
return nil, nil
}
var tags []string var tags []string
repo, err := git.PlainOpen(r.Dir) repo, err := git.PlainOpen(r.Dir)
@ -371,6 +415,10 @@ func (r Recipe) Tags() ([]string, error) {
// GetRecipeVersions retrieves all recipe versions. // GetRecipeVersions retrieves all recipe versions.
func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) { func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
if r.Local {
return nil, nil, nil
}
var warnMsg []string var warnMsg []string
versions := RecipeVersions{} versions := RecipeVersions{}

View File

@ -124,6 +124,14 @@ type Features struct {
func Get(name string) Recipe { func Get(name string) Recipe {
version := "" version := ""
versionRaw := "" versionRaw := ""
gitURL := ""
sshURL := ""
dir := ""
local := false
if strings.HasPrefix(name, "./") {
dir = path.Join(config.ABRA_DIR, name)
local = true
} else {
if strings.Contains(name, ":") { if strings.Contains(name, ":") {
split := strings.Split(name, ":") split := strings.Split(name, ":")
if len(split) > 2 { if len(split) > 2 {
@ -139,8 +147,8 @@ func Get(name string) Recipe {
} }
} }
gitURL := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, name) gitURL = fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, name)
sshURL := fmt.Sprintf(config.RECIPES_SSH_URL_TEMPLATE, name) sshURL = fmt.Sprintf(config.RECIPES_SSH_URL_TEMPLATE, name)
if strings.Contains(name, "/") { if strings.Contains(name, "/") {
u, err := url.Parse(name) u, err := url.Parse(name)
if err != nil { if err != nil {
@ -154,7 +162,8 @@ func Get(name string) Recipe {
sshURL = u.String() + ".git" sshURL = u.String() + ".git"
} }
dir := path.Join(config.RECIPES_DIR, escapeRecipeName(name)) dir = path.Join(config.RECIPES_DIR, escapeRecipeName(name))
}
r := Recipe{ r := Recipe{
Name: name, Name: name,
@ -163,6 +172,7 @@ func Get(name string) Recipe {
Dir: dir, Dir: dir,
GitURL: gitURL, GitURL: gitURL,
SSHURL: sshURL, SSHURL: sshURL,
Local: local,
ComposePath: path.Join(dir, "compose.yml"), ComposePath: path.Join(dir, "compose.yml"),
ReadmePath: path.Join(dir, "README.md"), ReadmePath: path.Join(dir, "README.md"),
@ -187,6 +197,7 @@ type Recipe struct {
Dir string Dir string
GitURL string GitURL string
SSHURL string SSHURL string
Local bool
ComposePath string ComposePath string
ReadmePath string ReadmePath string

View File

@ -127,3 +127,14 @@ teardown(){
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success assert_success
} }
# bats test_tags=slow
@test "new env version written to container env" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input
assert_success
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
assert_success
assert_output --partial "$TEST_RECIIPE:0.1.0+1.20.0"
}

View File

@ -28,17 +28,17 @@ teardown(){
} }
@test "validate app argument" { @test "validate app argument" {
run $ABRA app env run $ABRA app env list
assert_failure assert_failure
run $ABRA app env DOESNTEXIST run $ABRA app env list DOESNTEXIST
assert_failure assert_failure
} }
@test "show env version" { @test "show env version" {
latestRelease=$(_latest_release) latestRelease=$(_latest_release)
run $ABRA app env "$TEST_APP_DOMAIN" run $ABRA app env list "$TEST_APP_DOMAIN"
assert_success assert_success
assert_output --partial "$latestRelease" assert_output --partial "$latestRelease"
} }
@ -48,7 +48,7 @@ teardown(){
assert_success assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo" assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run $ABRA app env "$TEST_APP_DOMAIN" run $ABRA app env list "$TEST_APP_DOMAIN"
assert_success assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo" assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
@ -57,3 +57,44 @@ teardown(){
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo" run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo" assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
} }
@test "app env pull explodes when no deployed app" {
run $ABRA app env pull "$TEST_APP_DOMAIN" -s "$TEST_SERVER"
assert_failure
}
# bats test_tags=slow
@test "app env pull recreates app env when missing" {
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input
assert_success
run rm -rf "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
assert_not_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
run $ABRA app env pull "$TEST_APP_DOMAIN" -s "$TEST_SERVER"
assert_success
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
}
# bats test_tags=slow
@test "app env pull recreates correct version" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input
assert_success
run grep -q "TYPE=$TEST_RECIPE:0.1.0+1.20.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run rm -rf "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
assert_not_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
run $ABRA app env pull "$TEST_APP_DOMAIN" -s "$TEST_SERVER"
assert_success
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
run grep -q "TYPE=$TEST_RECIPE:0.1.0+1.20.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}

View File

@ -33,10 +33,29 @@ teardown(){
assert_success assert_success
run $ABRA app rollback "$TEST_APP_DOMAIN" "0.1.0+1.20.0" \ run $ABRA app rollback "$TEST_APP_DOMAIN" "0.1.0+1.20.0" \
--no-input --no-converge-checks --debug --no-input --no-converge-checks
assert_success assert_success
run grep -q "TYPE=abra-test-recipe:0.1.0+1.20.0" \ run grep -q "TYPE=abra-test-recipe:0.1.0+1.20.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success assert_success
} }
# bats test_tags=slow
@test "new env version written to container env" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" --no-input
assert_success
run grep -q "TYPE=abra-test-recipe:0.2.0+1.21.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app rollback "$TEST_APP_DOMAIN" "0.1.0+1.20.0" \
--no-input
assert_success
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
assert_success
assert_output --partial "$TEST_RECIIPE:0.1.0+1.20.0"
}

View File

@ -256,7 +256,7 @@ teardown(){
} }
# bats test_tags=slow # bats test_tags=slow
@test "commit deploy upgrade is possible" { @test "specific version upgrade after chaos deploy" {
tagHash=$(_get_tag_hash "0.1.0+1.20.0") tagHash=$(_get_tag_hash "0.1.0+1.20.0")
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" checkout "$tagHash" run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" checkout "$tagHash"
assert_success assert_success
@ -269,17 +269,26 @@ teardown(){
assert_success assert_success
assert_output --regexp "CURRENT DEPLOYMENT.*${tagHash:0:8}" assert_output --regexp "CURRENT DEPLOYMENT.*${tagHash:0:8}"
assert_output --regexp "ENV VERSION.*${tagHash:0:8}" assert_output --regexp "ENV VERSION.*${tagHash:0:8}"
assert_output --regexp "NEW DEPLOYMENT.*0\.1\.1\+1\.20\.2"
} }
@test "chaos commit upgrade is possible" { # bats test_tags=slow
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input --no-converge-checks @test "upgrade to latest after chaos deploy" {
assert_success latestRelease=$(_latest_release)
assert_output --partial '0.1.0+1.20.0'
tagHash=$(_get_tag_hash "0.2.0+1.21.0") tagHash=$(_get_tag_hash "0.1.0+1.20.0")
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" checkout "$tagHash"
run $ABRA app upgrade "$TEST_APP_DOMAIN" "$tagHash" --no-input --no-converge-checks
assert_success assert_success
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks --chaos
assert_success
assert_output --partial "${tagHash:0:8}"
run $ABRA app upgrade "$TEST_APP_DOMAIN" --no-input --no-converge-checks
assert_success
assert_output --regexp "CURRENT DEPLOYMENT.*${tagHash:0:8}"
assert_output --regexp "ENV VERSION.*${tagHash:0:8}"
assert_output --partial "${latestRelease}"
} }
# bats test_tags=slow # bats test_tags=slow

View File

@ -40,3 +40,21 @@ teardown(){
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success assert_success
} }
# bats test_tags=slow
@test "new env version written to container env" {
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input
assert_success
run grep -q "TYPE=abra-test-recipe:0.1.0+1.20.0" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
run $ABRA app upgrade "$TEST_APP_DOMAIN" "0.2.0+1.21.0" --no-input
assert_success
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
assert_success
assert_output --partial "$TEST_RECIIPE:0.2.0+1.21.0"
}

View File

@ -101,6 +101,9 @@ teardown() {
assert_success assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo" assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run $ABRA recipe sync "$TEST_RECIPE" --no-input --patch
assert_success
run $ABRA recipe release "$TEST_RECIPE" --no-input --patch run $ABRA recipe release "$TEST_RECIPE" --no-input --patch
assert_success assert_success
assert_output --partial 'no -p/--publish passed, not publishing' assert_output --partial 'no -p/--publish passed, not publishing'
@ -119,6 +122,9 @@ teardown() {
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" commit -m "added some release notes" run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" commit -m "added some release notes"
assert_success assert_success
run $ABRA recipe sync "$TEST_RECIPE" --no-input --patch
assert_success
run $ABRA recipe release "$TEST_RECIPE" --no-input --minor run $ABRA recipe release "$TEST_RECIPE" --no-input --minor
assert_success assert_success
assert_output --partial 'no -p/--publish passed, not publishing' assert_output --partial 'no -p/--publish passed, not publishing'