From c616907b715e89d11bd38b51e1b0a441f33db2e6 Mon Sep 17 00:00:00 2001
From: decentral1se <lukewm@riseup.net>
Date: Tue, 5 Oct 2021 10:28:09 +0200
Subject: [PATCH] feat: teach recipe sync to understand new versions

Closes https://git.coopcloud.tech/coop-cloud/organising/issues/177.
---
 cli/recipe/sync.go   | 82 +++++++++++++++++++++++++++++---------------
 pkg/recipe/recipe.go | 27 +++++++++++++++
 2 files changed, 81 insertions(+), 28 deletions(-)

diff --git a/cli/recipe/sync.go b/cli/recipe/sync.go
index 1e23e3ef1..05cdc3af6 100644
--- a/cli/recipe/sync.go
+++ b/cli/recipe/sync.go
@@ -4,62 +4,88 @@ import (
 	"fmt"
 
 	"coopcloud.tech/abra/cli/internal"
-	"coopcloud.tech/abra/pkg/client"
-	"github.com/docker/distribution/reference"
+	"coopcloud.tech/abra/pkg/catalogue"
+	"github.com/AlecAivazis/survey/v2"
 	"github.com/sirupsen/logrus"
 	"github.com/urfave/cli/v2"
 )
 
 var recipeSyncCommand = &cli.Command{
 	Name:    "sync",
-	Usage:   "Generate new recipe labels",
+	Usage:   "Ensure recipe version labels are up-to-date",
 	Aliases: []string{"s"},
 	Description: `
-This command will generate labels for each service which correspond to the
-following format:
+This command will generate labels for the main recipe service (i.e. the service
+named "app", by convention) which corresponds to the following format:
 
-    coop-cloud.${STACK_NAME}.${SERVICE_NAME}.version=${IMAGE_TAG}-${IMAGE_DIGEST}
+    coop-cloud.${STACK_NAME}.version=${RECIPE_TAG}
 
-The <recipe> configuration will be updated on the local file system. These
-labels are consumed by abra in other command invocations and used to determine
-the versioning metadata of up-and-running containers are.
+The ${RECIPE_TAG} is determined by the recipe maintainer and is retrieved by
+this command by asking for the list of git tags on the local git repository.
+The <recipe> configuration will be updated on the local file system.
 `,
 	ArgsUsage: "<recipe>",
+	BashComplete: func(c *cli.Context) {
+		catl, err := catalogue.ReadRecipeCatalogue()
+		if err != nil {
+			logrus.Warn(err)
+		}
+		if c.NArg() > 0 {
+			return
+		}
+		for name := range catl {
+			fmt.Println(name)
+		}
+	},
 	Action: func(c *cli.Context) error {
 		recipe := internal.ValidateRecipe(c)
 
+		mainService := "app"
+
+		var services []string
 		hasAppService := false
 		for _, service := range recipe.Config.Services {
+			services = append(services, service.Name)
 			if service.Name == "app" {
 				hasAppService = true
+				logrus.Debugf("detected app service in '%s'", recipe.Name)
 			}
 		}
 
 		if !hasAppService {
-			logrus.Fatal(fmt.Sprintf("no 'app' service defined in '%s'", recipe.Name))
+			logrus.Warnf("no 'app' service defined in '%s'", recipe.Name)
+			var chosenService string
+			prompt := &survey.Select{
+				Message: fmt.Sprintf("what is the main service name for '%s'?", recipe.Name),
+				Options: services,
+			}
+			if err := survey.AskOne(prompt, &chosenService); err != nil {
+				logrus.Fatal(err)
+			}
+			mainService = chosenService
 		}
 
-		for _, service := range recipe.Config.Services {
-			img, err := reference.ParseNormalizedNamed(service.Image)
-			if err != nil {
-				logrus.Fatal(err)
-			}
-			logrus.Debugf("detected image '%s' for service '%s'", img, service.Name)
+		logrus.Debugf("selecting '%s' as the service to sync version labels", mainService)
 
-			digest, err := client.GetTagDigest(img)
-			if err != nil {
-				logrus.Fatal(err)
-			}
-			logrus.Debugf("retrieved digest '%s' for '%s'", digest, img)
-
-			tag := img.(reference.NamedTagged).Tag()
-			label := fmt.Sprintf("coop-cloud.${STACK_NAME}.%s.version=%s-%s", service.Name, tag, digest)
-			if err := recipe.UpdateLabel(service.Name, label); err != nil {
-				logrus.Fatal(err)
-			}
-			logrus.Debugf("added label '%s' to service '%s'", label, service.Name)
+		tags, err := recipe.Tags()
+		if err != nil {
+			logrus.Fatal(err)
 		}
 
+		if len(tags) == 0 {
+			logrus.Fatalf("no tags detected for '%s'", recipe.Name)
+		}
+
+		latestTag := tags[len(tags)-1]
+		logrus.Infof("choosing '%s' as latest tag for recipe '%s'", latestTag, recipe.Name)
+
+		label := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", latestTag)
+		if err := recipe.UpdateLabel(mainService, label); err != nil {
+			logrus.Fatal(err)
+		}
+
+		logrus.Infof("added label '%s' to service '%s'", label, mainService)
+
 		return nil
 	},
 }
diff --git a/pkg/recipe/recipe.go b/pkg/recipe/recipe.go
index 2b4025eb3..bee90cf13 100644
--- a/pkg/recipe/recipe.go
+++ b/pkg/recipe/recipe.go
@@ -41,6 +41,33 @@ func (r Recipe) UpdateTag(image, tag string) error {
 	return nil
 }
 
+// Tags list the recipe tags
+func (r Recipe) Tags() ([]string, error) {
+	var tags []string
+
+	recipeDir := path.Join(config.ABRA_DIR, "apps", r.Name)
+	repo, err := git.PlainOpen(recipeDir)
+	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
+	}
+
+	logrus.Debugf("detected '%s' as tags for recipe '%s'", strings.Join(tags, ", "), r.Name)
+
+	return tags, nil
+}
+
 // Get retrieves a recipe.
 func Get(recipeName string) (Recipe, error) {
 	if err := EnsureExists(recipeName); err != nil {