From 3c3d8dc0e769e6200f70fe7362a4024300069fb2 Mon Sep 17 00:00:00 2001 From: decentral1se Date: Wed, 8 Sep 2021 12:55:33 +0200 Subject: [PATCH] WIP: add first run at app rollback command See https://git.coopcloud.tech/coop-cloud/organising/issues/146. --- cli/app/new.go | 21 +------------- cli/app/rollback.go | 57 ++++++++++++++++++++++++++++++++++++-- cli/app/version.go | 18 ++---------- cli/internal/validate.go | 3 -- pkg/app/app.go | 48 ++++++++++++++++++++++++++++++++ pkg/catalogue/catalogue.go | 20 +++++++++++++ pkg/client/stack/remove.go | 2 +- pkg/client/stack/stack.go | 7 +++-- 8 files changed, 132 insertions(+), 44 deletions(-) diff --git a/cli/app/new.go b/cli/app/new.go index 4c442b0e..0f99a42c 100644 --- a/cli/app/new.go +++ b/cli/app/new.go @@ -92,25 +92,6 @@ var appNewCommand = &cli.Command{ }, } -// getRecipeMeta retrieves the recipe metadata from the recipe catalogue. -func getRecipeMeta(recipeName string) (catalogue.RecipeMeta, error) { - catl, err := catalogue.ReadRecipeCatalogue() - if err != nil { - return catalogue.RecipeMeta{}, err - } - - recipeMeta, ok := catl[recipeName] - if !ok { - err := fmt.Errorf("recipe '%s' does not exist?", recipeName) - return catalogue.RecipeMeta{}, err - } - if err := recipePkg.EnsureExists(recipeName); err != nil { - return catalogue.RecipeMeta{}, err - } - - return recipeMeta, nil -} - // ensureDomainFlag checks if the domain flag was used. if not, asks the user for it/ func ensureDomainFlag() error { if domain == "" { @@ -190,7 +171,7 @@ func action(c *cli.Context) error { logrus.Fatal(err) } - recipeMeta, err := getRecipeMeta(recipe.Name) + recipeMeta, err := catalogue.GetRecipeMeta(recipe.Name) if err != nil { logrus.Fatal(err) } diff --git a/cli/app/rollback.go b/cli/app/rollback.go index 61bb04bc..ec7edab7 100644 --- a/cli/app/rollback.go +++ b/cli/app/rollback.go @@ -3,19 +3,27 @@ package app import ( "fmt" + "context" + "coopcloud.tech/abra/pkg/config" + + "coopcloud.tech/abra/cli/internal" + appPkg "coopcloud.tech/abra/pkg/app" + "coopcloud.tech/abra/pkg/catalogue" + "coopcloud.tech/abra/pkg/client" + "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" ) var appRollbackCommand = &cli.Command{ Name: "rollback", Usage: "Roll an app back to a previous version", - Aliases: []string{"b"}, + Aliases: []string{"r"}, ArgsUsage: "[]", BashComplete: func(c *cli.Context) { appNames, err := config.GetAppNames() if err != nil { - return + logrus.Warn(err) } if c.NArg() > 0 { return @@ -24,4 +32,49 @@ var appRollbackCommand = &cli.Command{ fmt.Println(a) } }, + Action: func(c *cli.Context) error { + app := internal.ValidateApp(c) + + ctx := context.Background() + cl, err := client.New(app.Server) + if err != nil { + logrus.Fatal(err) + } + + recipeMeta, err := catalogue.GetRecipeMeta(app.Type) + if err != nil { + logrus.Fatal(err) + } + if len(recipeMeta.Versions) == 0 { + logrus.Fatalf("no catalogue versions available for '%s'", app.Type) + } + + deployedVersions, isDeployed, err := appPkg.DeployedVersions(ctx, cl, app) + if err != nil { + logrus.Fatal(err) + } + if !isDeployed { + logrus.Fatalf("'%s' is not deployed?", app.Name) + } + if _, exists := deployedVersions["app"]; !exists { + logrus.Fatalf("no versioned 'app' service for '%s', cannot determine version", app.Name) + } + + version := c.Args().Get(1) + if version == "" { + // TODO: + // using deployedVersions["app"], get index+1 version from catalogue + // otherwise bail out saying there is nothing to rollback to + } else { + // TODO + // ensure this version is listed in the catalogue + // ensure this version is "older" (lower down in the list) + } + + // TODO + // display table of existing state and expected state and prompt + // run the deployment with this target version! + + return nil + }, } diff --git a/cli/app/version.go b/cli/app/version.go index 6c7f4a72..e5bd916e 100644 --- a/cli/app/version.go +++ b/cli/app/version.go @@ -7,6 +7,7 @@ import ( abraFormatter "coopcloud.tech/abra/cli/formatter" "coopcloud.tech/abra/cli/internal" + appPkg "coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/client/stack" "coopcloud.tech/abra/pkg/config" "github.com/docker/distribution/reference" @@ -27,19 +28,6 @@ func getImagePath(image string) (string, error) { return path, nil } -// parseVersionLabel parses a $STACK_NAME_$SERVICE_NAME service label -func parseServiceName(label string) string { - idx := strings.LastIndex(label, "_") - return label[idx+1:] -} - -// parseVersionLabel parses a $VERSION-$DIGEST service label -func parseVersionLabel(label string) (string, string) { - // versions may look like v4.2-abcd or v4.2-alpine-abcd - idx := strings.LastIndex(label, "-") - return label[:idx], label[idx+1:] -} - var appVersionCommand = &cli.Command{ Name: "version", Aliases: []string{"v"}, @@ -72,7 +60,7 @@ var appVersionCommand = &cli.Command{ for range compose.Services { status := <-ch if len(status.Services) > 0 { - serviceName := parseServiceName(status.Services[0].Spec.Name) + serviceName := appPkg.ParseServiceName(status.Services[0].Spec.Name) statuses[serviceName] = status } } @@ -85,7 +73,7 @@ var appVersionCommand = &cli.Command{ if status, ok := statuses[service.Name]; ok { statusService := status.Services[0] label := fmt.Sprintf("coop-cloud.%s.%s.version", app.StackName(), service.Name) - version, digest := parseVersionLabel(statusService.Spec.Labels[label]) + version, digest := appPkg.ParseVersionLabel(statusService.Spec.Labels[label]) image, err := getImagePath(statusService.Spec.Labels["com.docker.stack.image"]) if err != nil { logrus.Fatal(err) diff --git a/cli/internal/validate.go b/cli/internal/validate.go index a4fd5c48..d89f8e32 100644 --- a/cli/internal/validate.go +++ b/cli/internal/validate.go @@ -2,7 +2,6 @@ package internal import ( "errors" - "os" "coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/config" @@ -22,7 +21,6 @@ func ValidateRecipe(c *cli.Context) recipe.Recipe { recipe, err := recipe.Get(recipeName) if err != nil { logrus.Fatal(err) - os.Exit(1) } return recipe @@ -39,7 +37,6 @@ func ValidateApp(c *cli.Context) config.App { app, err := app.Get(appName) if err != nil { logrus.Fatal(err) - os.Exit(1) } return app diff --git a/pkg/app/app.go b/pkg/app/app.go index 08de1b02..c995d16a 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -1,7 +1,13 @@ package app import ( + "context" + "fmt" + "strings" + + "coopcloud.tech/abra/pkg/client/stack" "coopcloud.tech/abra/pkg/config" + apiclient "github.com/docker/docker/client" ) // Get retrieves an app @@ -18,3 +24,45 @@ func Get(appName string) (config.App, error) { return app, nil } + +// deployedServiceSpec represents a deployed service of an app. +type deployedServiceSpec struct { + Name string + Version string +} + +// VersionSpec represents a deployed app and associated metadata. +type VersionSpec map[string]deployedServiceSpec + +// DeployedVersions lists metadata (e.g. versions) for deployed +func DeployedVersions(ctx context.Context, cl *apiclient.Client, app config.App) (VersionSpec, bool, error) { + services, err := stack.GetStackServices(ctx, cl, app.StackName()) + if err != nil { + return VersionSpec{}, false, err + } + + appSpec := make(VersionSpec) + for _, service := range services { + serviceName := ParseServiceName(service.Spec.Name) + label := fmt.Sprintf("coop-cloud.%s.%s.version", app.StackName(), serviceName) + if deployLabel, ok := service.Spec.Labels[label]; ok { + version, _ := ParseVersionLabel(deployLabel) + appSpec[serviceName] = deployedServiceSpec{Name: serviceName, Version: version} + } + } + + return appSpec, len(services) > 0, nil +} + +// ParseVersionLabel parses a $VERSION-$DIGEST app service label. +func ParseVersionLabel(label string) (string, string) { + // versions may look like v4.2-abcd or v4.2-alpine-abcd + idx := strings.LastIndex(label, "-") + return label[:idx], label[idx+1:] +} + +// ParseVersionName parses a $STACK_NAME_$SERVICE_NAME service label. +func ParseServiceName(label string) string { + idx := strings.LastIndex(label, "_") + return label[idx+1:] +} diff --git a/pkg/catalogue/catalogue.go b/pkg/catalogue/catalogue.go index 36b991ad..585dd5f5 100644 --- a/pkg/catalogue/catalogue.go +++ b/pkg/catalogue/catalogue.go @@ -13,6 +13,7 @@ import ( "time" "coopcloud.tech/abra/pkg/config" + "coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/web" ) @@ -212,3 +213,22 @@ func VersionsOfService(recipe, serviceName string) ([]string, error) { return versions, nil } + +// GetRecipeMeta retrieves the recipe metadata from the recipe catalogue. +func GetRecipeMeta(recipeName string) (RecipeMeta, error) { + catl, err := ReadRecipeCatalogue() + if err != nil { + return RecipeMeta{}, err + } + + recipeMeta, ok := catl[recipeName] + if !ok { + err := fmt.Errorf("recipe '%s' does not exist?", recipeName) + return RecipeMeta{}, err + } + if err := recipe.EnsureExists(recipeName); err != nil { + return RecipeMeta{}, err + } + + return recipeMeta, nil +} diff --git a/pkg/client/stack/remove.go b/pkg/client/stack/remove.go index ba863256..b15e0565 100644 --- a/pkg/client/stack/remove.go +++ b/pkg/client/stack/remove.go @@ -18,7 +18,7 @@ import ( func RunRemove(ctx context.Context, client *apiclient.Client, opts Remove) error { var errs []string for _, namespace := range opts.Namespaces { - services, err := getStackServices(ctx, client, namespace) + services, err := GetStackServices(ctx, client, namespace) if err != nil { return err } diff --git a/pkg/client/stack/stack.go b/pkg/client/stack/stack.go index dc8f2ac0..5eac34c5 100644 --- a/pkg/client/stack/stack.go +++ b/pkg/client/stack/stack.go @@ -47,7 +47,7 @@ func getAllStacksFilter() filters.Args { return filter } -func getStackServices(ctx context.Context, dockerclient client.APIClient, namespace string) ([]swarm.Service, error) { +func GetStackServices(ctx context.Context, dockerclient client.APIClient, namespace string) ([]swarm.Service, error) { return dockerclient.ServiceList(ctx, types.ServiceListOptions{Filters: getStackServiceFilter(namespace)}) } @@ -94,7 +94,7 @@ func GetAllDeployedServices(contextName string) StackStatus { // pruneServices removes services that are no longer referenced in the source func pruneServices(ctx context.Context, cl *dockerclient.Client, namespace convert.Namespace, services map[string]struct{}) { - oldServices, err := getStackServices(ctx, cl, namespace.Name()) + oldServices, err := GetStackServices(ctx, cl, namespace.Name()) if err != nil { logrus.Infof("Failed to list services: %s\n", err) } @@ -174,6 +174,7 @@ func deployCompose(ctx context.Context, cl *dockerclient.Client, opts Deploy, co if err != nil { return err } + return deployServices(ctx, cl, services, namespace, opts.SendRegistryAuth, opts.ResolveImage) } @@ -290,7 +291,7 @@ func deployServices( namespace convert.Namespace, sendAuth bool, resolveImage string) error { - existingServices, err := getStackServices(ctx, cl, namespace.Name()) + existingServices, err := GetStackServices(ctx, cl, namespace.Name()) if err != nil { return err }