From d7a870b887d7470ed8110184ffffe86973db154b Mon Sep 17 00:00:00 2001 From: p4u1 Date: Mon, 8 Jul 2024 15:13:13 +0200 Subject: [PATCH] feat: remote recipes --- cli/app/cmd.go | 10 +++----- cli/app/ps.go | 7 +++--- cli/recipe/release.go | 3 +-- cli/updater/updater.go | 3 +-- pkg/app/app_test.go | 6 ++--- pkg/envfile/envfile_test.go | 9 +++---- pkg/lint/recipe.go | 16 ++++-------- pkg/recipe/git.go | 27 ++++++++------------ pkg/recipe/recipe.go | 29 ++++++++++++++++++++-- pkg/recipe/recipe_test.go | 49 +++++++++++++++++++++++++++++++++++++ pkg/secret/secret.go | 4 +-- 11 files changed, 107 insertions(+), 56 deletions(-) diff --git a/cli/app/cmd.go b/cli/app/cmd.go index 9a09d6ad..58ec5dc3 100644 --- a/cli/app/cmd.go +++ b/cli/app/cmd.go @@ -14,7 +14,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" ) @@ -209,24 +208,23 @@ var appCmdListCommand = cli.Command{ Before: internal.SubCommandBefore, Action: func(c *cli.Context) error { app := internal.ValidateApp(c) - r := recipe.Get(app.Recipe.Name) - if err := r.EnsureExists(); err != nil { + if err := app.Recipe.EnsureExists(); err != nil { log.Fatal(err) } if !internal.Chaos { - if err := r.EnsureIsClean(); err != nil { + if err := app.Recipe.EnsureIsClean(); err != nil { log.Fatal(err) } if !internal.Offline { - if err := r.EnsureUpToDate(); err != nil { + if err := app.Recipe.EnsureUpToDate(); err != nil { log.Fatal(err) } } - if err := r.EnsureLatest(); err != nil { + if err := app.Recipe.EnsureLatest(); err != nil { log.Fatal(err) } } diff --git a/cli/app/ps.go b/cli/app/ps.go index 537c4018..008d59ce 100644 --- a/cli/app/ps.go +++ b/cli/app/ps.go @@ -11,7 +11,6 @@ import ( "coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/formatter" "coopcloud.tech/abra/pkg/log" - "coopcloud.tech/abra/pkg/recipe" abraService "coopcloud.tech/abra/pkg/service" stack "coopcloud.tech/abra/pkg/upstream/stack" dockerFormatter "github.com/docker/cli/cli/command/formatter" @@ -35,6 +34,9 @@ var appPsCommand = cli.Command{ BashComplete: autocomplete.AppNameComplete, Action: func(c *cli.Context) error { app := internal.ValidateApp(c) + if err := app.Recipe.Ensure(false, false); err != nil { + log.Fatal(err) + } cl, err := client.New(app.Server) if err != nil { @@ -74,8 +76,7 @@ var appPsCommand = cli.Command{ // showPSOutput renders ps output. func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chaosVersion string) { - r := recipe.Get(app.Recipe.Name) - composeFiles, err := r.GetComposeFiles(app.Env) + composeFiles, err := app.Recipe.GetComposeFiles(app.Env) if err != nil { log.Fatal(err) return diff --git a/cli/recipe/release.go b/cli/recipe/release.go index 2bb17d70..ad867040 100644 --- a/cli/recipe/release.go +++ b/cli/recipe/release.go @@ -10,7 +10,6 @@ import ( "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/autocomplete" - "coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/formatter" gitPkg "coopcloud.tech/abra/pkg/git" "coopcloud.tech/abra/pkg/log" @@ -391,7 +390,7 @@ func pushRelease(recipe recipe.Recipe, tagString string) error { if err := recipe.Push(internal.Dry); err != nil { return err } - url := fmt.Sprintf("%s/%s/src/tag/%s", config.REPOS_BASE_URL, recipe.Name, tagString) + url := fmt.Sprintf("%s/src/tag/%s", recipe.GitURL, tagString) log.Infof("new release published: %s", url) } else { log.Info("no -p/--publish passed, not publishing") diff --git a/cli/updater/updater.go b/cli/updater/updater.go index fd2d321e..ca2b8ff2 100644 --- a/cli/updater/updater.go +++ b/cli/updater/updater.go @@ -433,8 +433,7 @@ func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error { } // upgrade performs all necessary steps to upgrade an app. -func upgrade(cl *dockerclient.Client, stackName, recipeName, - upgradeVersion string) error { +func upgrade(cl *dockerclient.Client, stackName, recipeName, upgradeVersion string) error { env, err := getEnv(cl, stackName) if err != nil { return err diff --git a/pkg/app/app_test.go b/pkg/app/app_test.go index a10cc753..3eb2d7dd 100644 --- a/pkg/app/app_test.go +++ b/pkg/app/app_test.go @@ -85,8 +85,7 @@ func TestGetComposeFiles(t *testing.T) { } for _, test := range tests { - r2 := recipe.Get(r.Name) - composeFiles, err := r2.GetComposeFiles(test.appEnv) + composeFiles, err := r.GetComposeFiles(test.appEnv) if err != nil { t.Fatal(err) } @@ -107,8 +106,7 @@ func TestGetComposeFilesError(t *testing.T) { } for _, test := range tests { - r2 := recipe.Get(r.Name) - _, err := r2.GetComposeFiles(test.appEnv) + _, err := r.GetComposeFiles(test.appEnv) if err == nil { t.Fatalf("should have failed: %v", test.appEnv) } diff --git a/pkg/envfile/envfile_test.go b/pkg/envfile/envfile_test.go index 3c15555b..ffab3ee4 100644 --- a/pkg/envfile/envfile_test.go +++ b/pkg/envfile/envfile_test.go @@ -113,8 +113,7 @@ func TestCheckEnv(t *testing.T) { t.Fatal(err) } - r2 := recipe.Get(r.Name) - envSample, err := r2.SampleEnv() + envSample, err := r.SampleEnv() if err != nil { t.Fatal(err) } @@ -147,8 +146,7 @@ func TestCheckEnvError(t *testing.T) { t.Fatal(err) } - r2 := recipe.Get(r.Name) - envSample, err := r2.SampleEnv() + envSample, err := r.SampleEnv() if err != nil { t.Fatal(err) } @@ -183,8 +181,7 @@ func TestEnvVarCommentsRemoved(t *testing.T) { t.Fatal(err) } - r2 := recipe.Get(r.Name) - envSample, err := r2.SampleEnv() + envSample, err := r.SampleEnv() if err != nil { t.Fatal(err) } diff --git a/pkg/lint/recipe.go b/pkg/lint/recipe.go index 7c7c6ac9..51ee392f 100644 --- a/pkg/lint/recipe.go +++ b/pkg/lint/recipe.go @@ -6,7 +6,6 @@ import ( "os" "path" - "coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/log" "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe" @@ -214,8 +213,7 @@ func LintComposeVersion(recipe recipe.Recipe) (bool, error) { } func LintEnvConfigPresent(r recipe.Recipe) (bool, error) { - r2 := recipe.Get(r.Name) - if _, err := os.Stat(r2.SampleEnvPath); !os.IsNotExist(err) { + if _, err := os.Stat(r.SampleEnvPath); !os.IsNotExist(err) { return true, nil } @@ -241,10 +239,9 @@ func LintAppService(recipe recipe.Recipe) (bool, error) { // the recipe. This typically means that no domain is required to deploy and // therefore no matching traefik deploy label will be present. func LintTraefikEnabledSkipCondition(r recipe.Recipe) (bool, error) { - r2 := recipe.Get(r.Name) - sampleEnv, err := r2.SampleEnv() + sampleEnv, err := r.SampleEnv() if err != nil { - return false, fmt.Errorf("Unable to discover .env.sample for %s", r2.Name) + return false, fmt.Errorf("Unable to discover .env.sample for %s", r.Name) } if _, ok := sampleEnv["DOMAIN"]; !ok { @@ -390,8 +387,7 @@ func LintHasPublishedVersion(recipe recipe.Recipe) (bool, error) { } func LintMetadataFilledIn(r recipe.Recipe) (bool, error) { - r2 := recipe.Get(r.Name) - features, category, err := recipe.GetRecipeFeaturesAndCategory(r2) + features, category, err := recipe.GetRecipeFeaturesAndCategory(r) if err != nil { return false, err } @@ -431,9 +427,7 @@ func LintAbraShVendors(recipe recipe.Recipe) (bool, error) { } func LintHasRecipeRepo(recipe recipe.Recipe) (bool, error) { - url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, recipe.Name) - - res, err := http.Get(url) + res, err := http.Get(recipe.GitURL) if err != nil { return false, err } diff --git a/pkg/recipe/git.go b/pkg/recipe/git.go index 95cb87c9..851e3229 100644 --- a/pkg/recipe/git.go +++ b/pkg/recipe/git.go @@ -39,17 +39,14 @@ func (r Recipe) Ensure(chaos bool, offline bool) error { // 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 { + if _, err := os.Stat(r.Dir); os.IsNotExist(err) { + log.Debugf("%s does not exist, attemmpting to clone", r.Dir) + if err := gitPkg.Clone(r.Dir, r.GitURL); err != nil { return err } } - if err := gitPkg.EnsureGitRepo(recipeDir); err != nil { + if err := gitPkg.EnsureGitRepo(r.Dir); err != nil { return err } @@ -60,13 +57,11 @@ func (r Recipe) EnsureExists() error { func (r Recipe) EnsureVersion(version string) (bool, error) { isChaosCommit := false - recipeDir := path.Join(config.RECIPES_DIR, r.Name) - - if err := gitPkg.EnsureGitRepo(recipeDir); err != nil { + if err := gitPkg.EnsureGitRepo(r.Dir); err != nil { return isChaosCommit, err } - repo, err := git.PlainOpen(recipeDir) + repo, err := git.PlainOpen(r.Dir) if err != nil { return isChaosCommit, err } @@ -117,23 +112,21 @@ func (r Recipe) EnsureVersion(version string) (bool, error) { return isChaosCommit, nil } - log.Debugf("successfully checked %s out to %s in %s", r.Name, tagRef.Short(), recipeDir) + log.Debugf("successfully checked %s out to %s in %s", r.Name, tagRef.Short(), r.Dir) return isChaosCommit, 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) + isClean, err := gitPkg.IsClean(r.Dir) if err != nil { - return fmt.Errorf("unable to check git clean status in %s: %s", recipeDir, err) + return fmt.Errorf("unable to check git clean status in %s: %s", r.Dir, 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 fmt.Errorf(msg, r.Name, r.Dir) } return nil diff --git a/pkg/recipe/recipe.go b/pkg/recipe/recipe.go index 5a33dabc..a68a9c53 100644 --- a/pkg/recipe/recipe.go +++ b/pkg/recipe/recipe.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "io/ioutil" + "net/url" "os" "path" "slices" @@ -123,11 +124,28 @@ type Features struct { } func Get(name string) Recipe { - dir := path.Join(config.RECIPES_DIR, name) + gitURL := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, name) + sshURL := fmt.Sprintf(config.SSH_URL_TEMPLATE, name) + if strings.Contains(name, "/") { + u, err := url.Parse(name) + if err != nil { + log.Fatalf("invalid recipe: %s", err) + } + u.Scheme = "https" + gitURL = u.String() + ".git" + + u.Scheme = "ssh" + u.User = url.User("git") + sshURL = u.String() + ".git" + } + + dir := path.Join(config.RECIPES_DIR, escapeRecipeName(name)) + return Recipe{ Name: name, Dir: dir, - SSHURL: fmt.Sprintf(config.SSH_URL_TEMPLATE, name), + GitURL: gitURL, + SSHURL: sshURL, ComposePath: path.Join(dir, "compose.yml"), ReadmePath: path.Join(dir, "README.md"), @@ -139,6 +157,7 @@ func Get(name string) Recipe { type Recipe struct { Name string Dir string + GitURL string SSHURL string ComposePath string @@ -147,6 +166,12 @@ type Recipe struct { AbraShPath string } +func escapeRecipeName(recipeName string) string { + recipeName = strings.ReplaceAll(recipeName, "/", "_") + recipeName = strings.ReplaceAll(recipeName, ".", "_") + return recipeName +} + // GetRecipesLocal retrieves all local recipe directories func GetRecipesLocal() ([]string, error) { var recipes []string diff --git a/pkg/recipe/recipe_test.go b/pkg/recipe/recipe_test.go index f73d613b..7837cde3 100644 --- a/pkg/recipe/recipe_test.go +++ b/pkg/recipe/recipe_test.go @@ -1,11 +1,60 @@ package recipe import ( + "path" "testing" + "coopcloud.tech/abra/pkg/config" + + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" ) +func TestGet(t *testing.T) { + cfg := config.LoadAbraConfig() + testcases := []struct { + name string + recipe Recipe + }{ + { + name: "foo", + recipe: Recipe{ + Name: "foo", + Dir: path.Join(cfg.GetAbraDir(), "/recipes/foo"), + GitURL: "https://git.coopcloud.tech/coop-cloud/foo.git", + SSHURL: "ssh://git@git.coopcloud.tech:2222/coop-cloud/foo.git", + ComposePath: path.Join(cfg.GetAbraDir(), "recipes/foo/compose.yml"), + ReadmePath: path.Join(cfg.GetAbraDir(), "recipes/foo/README.md"), + SampleEnvPath: path.Join(cfg.GetAbraDir(), "recipes/foo/.env.sample"), + AbraShPath: path.Join(cfg.GetAbraDir(), "recipes/foo/abra.sh"), + }, + }, + { + name: "mygit.org/myorg/cool-recipe", + recipe: Recipe{ + Name: "mygit.org/myorg/cool-recipe", + Dir: path.Join(cfg.GetAbraDir(), "/recipes/mygit_org_myorg_cool-recipe"), + GitURL: "https://mygit.org/myorg/cool-recipe.git", + SSHURL: "ssh://git@mygit.org/myorg/cool-recipe.git", + ComposePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/compose.yml"), + ReadmePath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/README.md"), + SampleEnvPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/.env.sample"), + AbraShPath: path.Join(cfg.GetAbraDir(), "recipes/mygit_org_myorg_cool-recipe/abra.sh"), + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + t.Setenv("ABRA_DIR", "") + recipe := Get(tc.name) + if diff := cmp.Diff(tc.recipe, recipe); diff != "" { + t.Errorf("Recipe mismatch (-want +got):\n%s", diff) + } + }) + } +} + func TestGetVersionLabelLocalDoesNotUseTimeoutLabel(t *testing.T) { offline := true diff --git a/pkg/secret/secret.go b/pkg/secret/secret.go index 63e3efdb..1f17169c 100644 --- a/pkg/secret/secret.go +++ b/pkg/secret/secret.go @@ -16,7 +16,6 @@ import ( "coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/envfile" "coopcloud.tech/abra/pkg/log" - "coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/upstream/stack" loader "coopcloud.tech/abra/pkg/upstream/stack" "github.com/decentral1se/passgen" @@ -246,8 +245,7 @@ type secretStatuses []secretStatus func PollSecretsStatus(cl *dockerClient.Client, app appPkg.App) (secretStatuses, error) { var secStats secretStatuses - r := recipe.Get(app.Recipe.Name) - composeFiles, err := r.GetComposeFiles(app.Env) + composeFiles, err := app.Recipe.GetComposeFiles(app.Env) if err != nil { return secStats, err }