From 950f85e2b4597fd943dfab7e69b4660af6f5ae18 Mon Sep 17 00:00:00 2001
From: p4u1 <p4u1_f4u1@riseup.net>
Date: Sun, 7 Jul 2024 12:35:09 +0200
Subject: [PATCH] refactor(recipe): introduce new recipe struct and move some
 methods

---
 cli/app/backup.go      |  36 +++++----
 cli/app/check.go       |  10 +--
 cli/app/cmd.go         |  19 ++---
 cli/app/deploy.go      |  15 ++--
 cli/app/logs.go        |   3 +-
 cli/app/new.go         |  11 +--
 cli/app/ps.go          |   3 +-
 cli/app/restore.go     |   9 ++-
 cli/app/rollback.go    |  13 ++--
 cli/app/secret.go      |  27 ++++---
 cli/app/upgrade.go     |  11 +--
 cli/recipe/fetch.go    |   6 +-
 cli/recipe/lint.go     |   9 ++-
 cli/recipe/upgrade.go  |   9 ++-
 cli/updater/updater.go |  14 ++--
 pkg/recipe/recipe.go   | 169 ++++++++++++++++++++++-------------------
 16 files changed, 197 insertions(+), 167 deletions(-)

diff --git a/cli/app/backup.go b/cli/app/backup.go
index c7067941b..75d9eb6b7 100644
--- a/cli/app/backup.go
+++ b/cli/app/backup.go
@@ -47,22 +47,23 @@ var appBackupListCommand = cli.Command{
 	Action: func(c *cli.Context) error {
 		app := internal.ValidateApp(c)
 
-		if err := recipe.EnsureExists(app.Recipe); err != nil {
+		r := recipe.Get2(app.Recipe)
+		if err := r.EnsureExists(); err != nil {
 			log.Fatal(err)
 		}
 
 		if !internal.Chaos {
-			if err := recipe.EnsureIsClean(app.Recipe); err != nil {
+			if err := r.EnsureIsClean(); err != nil {
 				log.Fatal(err)
 			}
 
 			if !internal.Offline {
-				if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
+				if err := r.EnsureUpToDate(); err != nil {
 					log.Fatal(err)
 				}
 			}
 
-			if err := recipe.EnsureLatest(app.Recipe); err != nil {
+			if err := r.EnsureLatest(); err != nil {
 				log.Fatal(err)
 			}
 		}
@@ -109,23 +110,24 @@ var appBackupDownloadCommand = cli.Command{
 	BashComplete: autocomplete.AppNameComplete,
 	Action: func(c *cli.Context) error {
 		app := internal.ValidateApp(c)
+		r := recipe.Get2(app.Recipe)
 
-		if err := recipe.EnsureExists(app.Recipe); err != nil {
+		if err := r.EnsureExists(); err != nil {
 			log.Fatal(err)
 		}
 
 		if !internal.Chaos {
-			if err := recipe.EnsureIsClean(app.Recipe); err != nil {
+			if err := r.EnsureIsClean(); err != nil {
 				log.Fatal(err)
 			}
 
 			if !internal.Offline {
-				if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
+				if err := r.EnsureUpToDate(); err != nil {
 					log.Fatal(err)
 				}
 			}
 
-			if err := recipe.EnsureLatest(app.Recipe); err != nil {
+			if err := r.EnsureLatest(); err != nil {
 				log.Fatal(err)
 			}
 		}
@@ -179,23 +181,24 @@ var appBackupCreateCommand = cli.Command{
 	BashComplete: autocomplete.AppNameComplete,
 	Action: func(c *cli.Context) error {
 		app := internal.ValidateApp(c)
+		r := recipe.Get2(app.Recipe)
 
-		if err := recipe.EnsureExists(app.Recipe); err != nil {
+		if err := r.EnsureExists(); err != nil {
 			log.Fatal(err)
 		}
 
 		if !internal.Chaos {
-			if err := recipe.EnsureIsClean(app.Recipe); err != nil {
+			if err := r.EnsureIsClean(); err != nil {
 				log.Fatal(err)
 			}
 
 			if !internal.Offline {
-				if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
+				if err := r.EnsureUpToDate(); err != nil {
 					log.Fatal(err)
 				}
 			}
 
-			if err := recipe.EnsureLatest(app.Recipe); err != nil {
+			if err := r.EnsureLatest(); err != nil {
 				log.Fatal(err)
 			}
 		}
@@ -237,23 +240,24 @@ var appBackupSnapshotsCommand = cli.Command{
 	BashComplete: autocomplete.AppNameComplete,
 	Action: func(c *cli.Context) error {
 		app := internal.ValidateApp(c)
+		r := recipe.Get2(app.Recipe)
 
-		if err := recipe.EnsureExists(app.Recipe); err != nil {
+		if err := r.EnsureExists(); err != nil {
 			log.Fatal(err)
 		}
 
 		if !internal.Chaos {
-			if err := recipe.EnsureIsClean(app.Recipe); err != nil {
+			if err := r.EnsureIsClean(); err != nil {
 				log.Fatal(err)
 			}
 
 			if !internal.Offline {
-				if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
+				if err := r.EnsureUpToDate(); err != nil {
 					log.Fatal(err)
 				}
 			}
 
-			if err := recipe.EnsureLatest(app.Recipe); err != nil {
+			if err := r.EnsureLatest(); err != nil {
 				log.Fatal(err)
 			}
 		}
diff --git a/cli/app/check.go b/cli/app/check.go
index 53f689011..002a2978d 100644
--- a/cli/app/check.go
+++ b/cli/app/check.go
@@ -7,7 +7,6 @@ import (
 	"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"
 )
 
@@ -37,23 +36,24 @@ ${FOO:<default>} syntax). "check" does not confirm or deny this for you.`,
 	BashComplete: autocomplete.AppNameComplete,
 	Action: func(c *cli.Context) error {
 		app := internal.ValidateApp(c)
+		r := recipe.Get2(app.Recipe)
 
-		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)
 			}
 		}
diff --git a/cli/app/cmd.go b/cli/app/cmd.go
index c8215d28e..a92dc946b 100644
--- a/cli/app/cmd.go
+++ b/cli/app/cmd.go
@@ -17,7 +17,6 @@ import (
 	"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"
 )
 
@@ -60,23 +59,24 @@ Example:
 	},
 	Action: func(c *cli.Context) error {
 		app := internal.ValidateApp(c)
+		r := recipe.Get2(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)
 			}
 		}
@@ -228,23 +228,24 @@ var appCmdListCommand = cli.Command{
 	Before:       internal.SubCommandBefore,
 	Action: func(c *cli.Context) error {
 		app := internal.ValidateApp(c)
+		r := recipe.Get2(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)
 			}
 		}
diff --git a/cli/app/deploy.go b/cli/app/deploy.go
index e7c3d5fd6..277162760 100644
--- a/cli/app/deploy.go
+++ b/cli/app/deploy.go
@@ -52,28 +52,29 @@ recipes.
 	Action: func(c *cli.Context) error {
 		app := internal.ValidateApp(c)
 		stackName := app.StackName()
+		r2 := recipe.Get2(app.Recipe)
 
 		specificVersion := c.Args().Get(1)
 		if specificVersion != "" && internal.Chaos {
 			log.Fatal("cannot use <version> and --chaos together")
 		}
 
-		if err := recipe.EnsureExists(app.Recipe); err != nil {
+		if err := r2.EnsureExists(); err != nil {
 			log.Fatal(err)
 		}
 
 		if !internal.Chaos {
-			if err := recipe.EnsureIsClean(app.Recipe); err != nil {
+			if err := r2.EnsureIsClean(); err != nil {
 				log.Fatal(err)
 			}
 
 			if !internal.Offline {
-				if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
+				if err := r2.EnsureUpToDate(); err != nil {
 					log.Fatal(err)
 				}
 			}
 
-			if err := recipe.EnsureLatest(app.Recipe); err != nil {
+			if err := r2.EnsureLatest(); err != nil {
 				log.Fatal(err)
 			}
 		}
@@ -107,7 +108,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 := r2.EnsureVersion(version); err != nil {
 				log.Fatal(err)
 			}
 		}
@@ -157,7 +158,7 @@ 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 := r2.EnsureVersion(version); err != nil {
 					log.Fatal(err)
 				}
 			} else {
@@ -173,7 +174,7 @@ recipes.
 		if internal.Chaos {
 			log.Warnf("chaos mode engaged")
 			var err error
-			version, err = recipe.ChaosVersion(app.Recipe)
+			version, err = r2.ChaosVersion()
 			if err != nil {
 				log.Fatal(err)
 			}
diff --git a/cli/app/logs.go b/cli/app/logs.go
index c98d62b57..346e4ed3c 100644
--- a/cli/app/logs.go
+++ b/cli/app/logs.go
@@ -38,8 +38,9 @@ var appLogsCommand = cli.Command{
 	Action: func(c *cli.Context) error {
 		app := internal.ValidateApp(c)
 		stackName := app.StackName()
+		r := recipe.Get2(app.Recipe)
 
-		if err := recipe.EnsureExists(app.Recipe); err != nil {
+		if err := r.EnsureExists(); err != nil {
 			log.Fatal(err)
 		}
 
diff --git a/cli/app/new.go b/cli/app/new.go
index 5011af63d..63aff1de6 100644
--- a/cli/app/new.go
+++ b/cli/app/new.go
@@ -67,13 +67,14 @@ var appNewCommand = cli.Command{
 	},
 	Action: func(c *cli.Context) error {
 		recipe := internal.ValidateRecipe(c)
+		r := recipePkg.Get2(recipe.Name)
 
 		if !internal.Chaos {
-			if err := recipePkg.EnsureIsClean(recipe.Name); err != nil {
+			if err := r.EnsureIsClean(); err != nil {
 				log.Fatal(err)
 			}
 			if !internal.Offline {
-				if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
+				if err := r.EnsureUpToDate(); err != nil {
 					log.Fatal(err)
 				}
 			}
@@ -93,16 +94,16 @@ var appNewCommand = cli.Command{
 						version = tag
 					}
 
-					if err := recipePkg.EnsureVersion(recipe.Name, version); err != nil {
+					if err := r.EnsureVersion(version); err != nil {
 						log.Fatal(err)
 					}
 				} else {
-					if err := recipePkg.EnsureLatest(recipe.Name); err != nil {
+					if err := r.EnsureLatest(); err != nil {
 						log.Fatal(err)
 					}
 				}
 			} else {
-				if err := recipePkg.EnsureVersion(recipe.Name, c.Args().Get(1)); err != nil {
+				if err := r.EnsureVersion(c.Args().Get(1)); err != nil {
 					log.Fatal(err)
 				}
 			}
diff --git a/cli/app/ps.go b/cli/app/ps.go
index 162c6f1cc..0db7a7dc3 100644
--- a/cli/app/ps.go
+++ b/cli/app/ps.go
@@ -35,6 +35,7 @@ var appPsCommand = cli.Command{
 	BashComplete: autocomplete.AppNameComplete,
 	Action: func(c *cli.Context) error {
 		app := internal.ValidateApp(c)
+		r := recipe.Get2(app.Recipe)
 
 		cl, err := client.New(app.Server)
 		if err != nil {
@@ -53,7 +54,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 := r.EnsureVersion(deployedVersion); err != nil {
 					log.Fatal(err)
 				}
 			}
diff --git a/cli/app/restore.go b/cli/app/restore.go
index f95b497d8..b6605d29e 100644
--- a/cli/app/restore.go
+++ b/cli/app/restore.go
@@ -32,23 +32,24 @@ var appRestoreCommand = cli.Command{
 	BashComplete: autocomplete.AppNameComplete,
 	Action: func(c *cli.Context) error {
 		app := internal.ValidateApp(c)
+		r := recipe.Get2(app.Recipe)
 
-		if err := recipe.EnsureExists(app.Recipe); err != nil {
+		if err := r.EnsureExists(); err != nil {
 			log.Fatal(err)
 		}
 
 		if !internal.Chaos {
-			if err := recipe.EnsureIsClean(app.Recipe); err != nil {
+			if err := r.EnsureIsClean(); err != nil {
 				log.Fatal(err)
 			}
 
 			if !internal.Offline {
-				if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
+				if err := r.EnsureUpToDate(); err != nil {
 					log.Fatal(err)
 				}
 			}
 
-			if err := recipe.EnsureLatest(app.Recipe); err != nil {
+			if err := r.EnsureLatest(); err != nil {
 				log.Fatal(err)
 			}
 		}
diff --git a/cli/app/rollback.go b/cli/app/rollback.go
index 6dbf449c0..82eae9cca 100644
--- a/cli/app/rollback.go
+++ b/cli/app/rollback.go
@@ -52,28 +52,29 @@ recipes.
 	Action: func(c *cli.Context) error {
 		app := internal.ValidateApp(c)
 		stackName := app.StackName()
+		r2 := recipe.Get2(app.Recipe)
 
 		specificVersion := c.Args().Get(1)
 		if specificVersion != "" && internal.Chaos {
 			log.Fatal("cannot use <version> and --chaos together")
 		}
 
-		if err := recipe.EnsureExists(app.Recipe); err != nil {
+		if err := r2.EnsureExists(); err != nil {
 			log.Fatal(err)
 		}
 
 		if !internal.Chaos {
-			if err := recipe.EnsureIsClean(app.Recipe); err != nil {
+			if err := r2.EnsureIsClean(); err != nil {
 				log.Fatal(err)
 			}
 
 			if !internal.Offline {
-				if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
+				if err := r2.EnsureUpToDate(); err != nil {
 					log.Fatal(err)
 				}
 			}
 
-			if err := recipe.EnsureLatest(app.Recipe); err != nil {
+			if err := r2.EnsureLatest(); err != nil {
 				log.Fatal(err)
 			}
 		}
@@ -185,7 +186,7 @@ recipes.
 		}
 
 		if !internal.Chaos {
-			if err := recipe.EnsureVersion(app.Recipe, chosenDowngrade); err != nil {
+			if err := r2.EnsureVersion(chosenDowngrade); err != nil {
 				log.Fatal(err)
 			}
 		}
@@ -193,7 +194,7 @@ recipes.
 		if internal.Chaos {
 			log.Warn("chaos mode engaged")
 			var err error
-			chosenDowngrade, err = recipe.ChaosVersion(app.Recipe)
+			chosenDowngrade, err = r2.ChaosVersion()
 			if err != nil {
 				log.Fatal(err)
 			}
diff --git a/cli/app/secret.go b/cli/app/secret.go
index 672496f17..0f995fd0a 100644
--- a/cli/app/secret.go
+++ b/cli/app/secret.go
@@ -56,23 +56,24 @@ var appSecretGenerateCommand = cli.Command{
 	BashComplete: autocomplete.AppNameComplete,
 	Action: func(c *cli.Context) error {
 		app := internal.ValidateApp(c)
+		r := recipe.Get2(app.Recipe)
 
-		if err := recipe.EnsureExists(app.Recipe); err != nil {
+		if err := r.EnsureExists(); err != nil {
 			log.Fatal(err)
 		}
 
 		if !internal.Chaos {
-			if err := recipe.EnsureIsClean(app.Recipe); err != nil {
+			if err := r.EnsureIsClean(); err != nil {
 				log.Fatal(err)
 			}
 
 			if !internal.Offline {
-				if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
+				if err := r.EnsureUpToDate(); err != nil {
 					log.Fatal(err)
 				}
 			}
 
-			if err := recipe.EnsureLatest(app.Recipe); err != nil {
+			if err := r.EnsureLatest(); err != nil {
 				log.Fatal(err)
 			}
 		}
@@ -263,23 +264,24 @@ Example:
 `,
 	Action: func(c *cli.Context) error {
 		app := internal.ValidateApp(c)
+		r := recipe.Get2(app.Recipe)
 
-		if err := recipe.EnsureExists(app.Recipe); err != nil {
+		if err := r.EnsureExists(); err != nil {
 			log.Fatal(err)
 		}
 
 		if !internal.Chaos {
-			if err := recipe.EnsureIsClean(app.Recipe); err != nil {
+			if err := r.EnsureIsClean(); err != nil {
 				log.Fatal(err)
 			}
 
 			if !internal.Offline {
-				if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
+				if err := r.EnsureUpToDate(); err != nil {
 					log.Fatal(err)
 				}
 			}
 
-			if err := recipe.EnsureLatest(app.Recipe); err != nil {
+			if err := r.EnsureLatest(); err != nil {
 				log.Fatal(err)
 			}
 		}
@@ -371,23 +373,24 @@ var appSecretLsCommand = cli.Command{
 	BashComplete: autocomplete.AppNameComplete,
 	Action: func(c *cli.Context) error {
 		app := internal.ValidateApp(c)
+		r := recipe.Get2(app.Recipe)
 
-		if err := recipe.EnsureExists(app.Recipe); err != nil {
+		if err := r.EnsureExists(); err != nil {
 			log.Fatal(err)
 		}
 
 		if !internal.Chaos {
-			if err := recipe.EnsureIsClean(app.Recipe); err != nil {
+			if err := r.EnsureIsClean(); err != nil {
 				log.Fatal(err)
 			}
 
 			if !internal.Offline {
-				if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
+				if err := r.EnsureUpToDate(); err != nil {
 					log.Fatal(err)
 				}
 			}
 
-			if err := recipe.EnsureLatest(app.Recipe); err != nil {
+			if err := r.EnsureLatest(); err != nil {
 				log.Fatal(err)
 			}
 		}
diff --git a/cli/app/upgrade.go b/cli/app/upgrade.go
index 4f54a4818..f4d2bf855 100644
--- a/cli/app/upgrade.go
+++ b/cli/app/upgrade.go
@@ -58,6 +58,7 @@ recipes.
 	Action: func(c *cli.Context) error {
 		app := internal.ValidateApp(c)
 		stackName := app.StackName()
+		r := recipe.Get2(app.Name)
 
 		specificVersion := c.Args().Get(1)
 		if specificVersion != "" && internal.Chaos {
@@ -65,17 +66,17 @@ recipes.
 		}
 
 		if !internal.Chaos {
-			if err := recipe.EnsureIsClean(app.Recipe); err != nil {
+			if err := r.EnsureIsClean(); err != nil {
 				log.Fatal(err)
 			}
 
 			if !internal.Offline {
-				if err := recipe.EnsureUpToDate(app.Recipe); err != nil {
+				if err := r.EnsureUpToDate(); err != nil {
 					log.Fatal(err)
 				}
 			}
 
-			if err := recipe.EnsureLatest(app.Recipe); err != nil {
+			if err := r.EnsureLatest(); err != nil {
 				log.Fatal(err)
 			}
 		}
@@ -219,7 +220,7 @@ recipes.
 		}
 
 		if !internal.Chaos {
-			if err := recipePkg.EnsureVersion(app.Recipe, chosenUpgrade); err != nil {
+			if err := r.EnsureVersion(chosenUpgrade); err != nil {
 				log.Fatal(err)
 			}
 		}
@@ -227,7 +228,7 @@ recipes.
 		if internal.Chaos {
 			log.Warn("chaos mode engaged")
 			var err error
-			chosenUpgrade, err = recipePkg.ChaosVersion(app.Recipe)
+			chosenUpgrade, err = r.ChaosVersion()
 			if err != nil {
 				log.Fatal(err)
 			}
diff --git a/cli/recipe/fetch.go b/cli/recipe/fetch.go
index 4bb0cb288..877700c7a 100644
--- a/cli/recipe/fetch.go
+++ b/cli/recipe/fetch.go
@@ -24,9 +24,10 @@ var recipeFetchCommand = cli.Command{
 	BashComplete: autocomplete.RecipeNameComplete,
 	Action: func(c *cli.Context) error {
 		recipeName := c.Args().First()
+		r := recipe.Get2(recipeName)
 		if recipeName != "" {
 			internal.ValidateRecipe(c)
-			if err := recipe.Ensure(recipeName); err != nil {
+			if err := r.Ensure(); 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.Get2(recipeName)
+			if err := r.Ensure(); err != nil {
 				log.Error(err)
 			}
 			catlBar.Add(1)
diff --git a/cli/recipe/lint.go b/cli/recipe/lint.go
index 41a78db5c..ecddc12bf 100644
--- a/cli/recipe/lint.go
+++ b/cli/recipe/lint.go
@@ -28,23 +28,24 @@ var recipeLintCommand = cli.Command{
 	BashComplete: autocomplete.RecipeNameComplete,
 	Action: func(c *cli.Context) error {
 		recipe := internal.ValidateRecipe(c)
+		r := recipePkg.Get2(recipe.Name)
 
-		if err := recipePkg.EnsureExists(recipe.Name); err != nil {
+		if err := r.EnsureExists(); err != nil {
 			log.Fatal(err)
 		}
 
 		if !internal.Chaos {
-			if err := recipePkg.EnsureIsClean(recipe.Name); err != nil {
+			if err := r.EnsureIsClean(); err != nil {
 				log.Fatal(err)
 			}
 
 			if !internal.Offline {
-				if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
+				if err := r.EnsureUpToDate(); err != nil {
 					log.Fatal(err)
 				}
 			}
 
-			if err := recipePkg.EnsureLatest(recipe.Name); err != nil {
+			if err := r.EnsureLatest(); err != nil {
 				log.Fatal(err)
 			}
 		}
diff --git a/cli/recipe/upgrade.go b/cli/recipe/upgrade.go
index f6011b62a..f51d5a964 100644
--- a/cli/recipe/upgrade.go
+++ b/cli/recipe/upgrade.go
@@ -72,20 +72,21 @@ You may invoke this command in "wizard" mode and be prompted for input:
 	BashComplete: autocomplete.RecipeNameComplete,
 	Action: func(c *cli.Context) error {
 		recipe := internal.ValidateRecipe(c)
+		r := recipePkg.Get2(recipe.Name)
 
-		if err := recipePkg.EnsureIsClean(recipe.Name); err != nil {
+		if err := r.EnsureIsClean(); err != nil {
 			log.Fatal(err)
 		}
 
-		if err := recipePkg.EnsureExists(recipe.Name); err != nil {
+		if err := r.EnsureExists(); err != nil {
 			log.Fatal(err)
 		}
 
-		if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
+		if err := r.EnsureUpToDate(); err != nil {
 			log.Fatal(err)
 		}
 
-		if err := recipePkg.EnsureLatest(recipe.Name); err != nil {
+		if err := r.EnsureLatest(); err != nil {
 			log.Fatal(err)
 		}
 
diff --git a/cli/updater/updater.go b/cli/updater/updater.go
index 7fb7a2bd8..49d0e854f 100644
--- a/cli/updater/updater.go
+++ b/cli/updater/updater.go
@@ -318,20 +318,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.Recipe2, 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 {
+	if r, err := recipe.Get(r.Name, internal.Offline); err != nil {
 		return err
 	} else if err := lint.LintForErrors(r); err != nil {
 		return err
@@ -445,7 +445,9 @@ func upgrade(cl *dockerclient.Client, stackName, recipeName,
 		Env:    env,
 	}
 
-	if err = processRecipeRepoVersion(recipeName, upgradeVersion); err != nil {
+	r := recipe.Get2(recipeName)
+
+	if err = processRecipeRepoVersion(r, upgradeVersion); err != nil {
 		return err
 	}
 
diff --git a/pkg/recipe/recipe.go b/pkg/recipe/recipe.go
index a112a256e..bc3439842 100644
--- a/pkg/recipe/recipe.go
+++ b/pkg/recipe/recipe.go
@@ -213,21 +213,22 @@ func (r Recipe) Tags() ([]string, error) {
 
 // Get retrieves a recipe.
 func Get(recipeName string, offline bool) (Recipe, error) {
-	if err := EnsureExists(recipeName); err != nil {
+	r := Get2(recipeName)
+	if err := r.EnsureExists(); err != nil {
 		return Recipe{}, err
 	}
 
-	pattern := fmt.Sprintf("%s/%s/compose**yml", config.RECIPES_DIR, recipeName)
+	pattern := fmt.Sprintf("%s/%s/compose**yml", config.RECIPES_DIR, r.Name)
 	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)
+		return Recipe{}, fmt.Errorf("%s is missing a compose.yml or compose.*.yml file?", r.Name)
 	}
 
-	envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample")
+	envSamplePath := path.Join(config.RECIPES_DIR, r.Name, ".env.sample")
 	sampleEnv, err := envfile.ReadEnv(envSamplePath)
 	if err != nil {
 		return Recipe{}, err
@@ -239,7 +240,7 @@ func Get(recipeName string, offline bool) (Recipe, error) {
 		return Recipe{}, err
 	}
 
-	meta, err := GetRecipeMeta(recipeName, offline)
+	meta, err := GetRecipeMeta(r.Name, offline)
 	if err != nil {
 		switch err.(type) {
 		case RecipeMissingFromCatalogue:
@@ -265,27 +266,35 @@ func (r Recipe) SampleEnv() (map[string]string, error) {
 	return sampleEnv, nil
 }
 
+func Get2(name string) Recipe2 {
+	return Recipe2{Name: name}
+}
+
+type Recipe2 struct {
+	Name string
+}
+
 // 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 {
+func (r Recipe2) Ensure() error {
+	if err := r.EnsureExists(); err != nil {
 		return err
 	}
-	if err := EnsureUpToDate(recipeName); err != nil {
+	if err := r.EnsureUpToDate(); err != nil {
 		return err
 	}
-	if err := EnsureLatest(recipeName); err != nil {
+	if err := r.EnsureLatest(); 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)
+// EnsureExists ensures that the recipe is locally cloned
+func (r Recipe2) 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, recipeName)
+		url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, r.Name)
 		if err := gitPkg.Clone(recipeDir, url); err != nil {
 			return err
 		}
@@ -299,8 +308,8 @@ func EnsureExists(recipeName string) error {
 }
 
 // EnsureVersion checks whether a specific version exists for a recipe.
-func EnsureVersion(recipeName, version string) error {
-	recipeDir := path.Join(config.RECIPES_DIR, recipeName)
+func (r Recipe2) EnsureVersion(version string) error {
+	recipeDir := path.Join(config.RECIPES_DIR, r.Name)
 
 	if err := gitPkg.EnsureGitRepo(recipeDir); err != nil {
 		return err
@@ -330,11 +339,11 @@ func EnsureVersion(recipeName, version string) error {
 
 	joinedTags := strings.Join(parsedTags, ", ")
 	if joinedTags != "" {
-		log.Debugf("read %s as tags for recipe %s", joinedTags, recipeName)
+		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?", recipeName, version)
+		return fmt.Errorf("the local copy of %s doesn't seem to have version %s available?", r.Name, version)
 	}
 
 	worktree, err := repo.Worktree()
@@ -351,14 +360,14 @@ func EnsureVersion(recipeName, version string) error {
 		return err
 	}
 
-	log.Debugf("successfully checked %s out to %s in %s", recipeName, tagRef.Short(), recipeDir)
+	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 EnsureIsClean(recipeName string) error {
-	recipeDir := path.Join(config.RECIPES_DIR, recipeName)
+func (r Recipe2) EnsureIsClean() error {
+	recipeDir := path.Join(config.RECIPES_DIR, r.Name)
 
 	isClean, err := gitPkg.IsClean(recipeDir)
 	if err != nil {
@@ -367,15 +376,15 @@ func EnsureIsClean(recipeName string) error {
 
 	if !isClean {
 		msg := "%s (%s) has locally unstaged changes? please commit/remove your changes before proceeding"
-		return fmt.Errorf(msg, recipeName, recipeDir)
+		return fmt.Errorf(msg, r.Name, 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)
+// EnsureLatest makes sure the latest commit is checked out for the local recipe repository
+func (r Recipe2) EnsureLatest() error {
+	recipeDir := path.Join(config.RECIPES_DIR, r.Name)
 
 	if err := gitPkg.EnsureGitRepo(recipeDir); err != nil {
 		return err
@@ -410,18 +419,71 @@ func EnsureLatest(recipeName string) error {
 	return nil
 }
 
+// EnsureUpToDate ensures that the local repo is synced to the remote
+func (r Recipe2) 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 ChaosVersion(recipeName string) (string, error) {
+func (r Recipe2) ChaosVersion() (string, error) {
 	var version string
 
-	head, err := gitPkg.GetRecipeHead(recipeName)
+	head, err := gitPkg.GetRecipeHead(r.Name)
 	if err != nil {
 		return version, err
 	}
 
 	version = formatter.SmallSHA(head.String())
 
-	recipeDir := path.Join(config.RECIPES_DIR, recipeName)
+	recipeDir := path.Join(config.RECIPES_DIR, r.Name)
 	isClean, err := gitPkg.IsClean(recipeDir)
 	if err != nil {
 		return version, err
@@ -597,59 +659,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)