WIP: add first run at app rollback command

See coop-cloud/organising#146.
This commit is contained in:
decentral1se 2021-09-08 12:55:33 +02:00
parent 855e9ea26d
commit 3c3d8dc0e7
No known key found for this signature in database
GPG Key ID: 5E2EF5A63E3718CC
8 changed files with 132 additions and 44 deletions

View File

@ -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/ // ensureDomainFlag checks if the domain flag was used. if not, asks the user for it/
func ensureDomainFlag() error { func ensureDomainFlag() error {
if domain == "" { if domain == "" {
@ -190,7 +171,7 @@ func action(c *cli.Context) error {
logrus.Fatal(err) logrus.Fatal(err)
} }
recipeMeta, err := getRecipeMeta(recipe.Name) recipeMeta, err := catalogue.GetRecipeMeta(recipe.Name)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

View File

@ -3,19 +3,27 @@ package app
import ( import (
"fmt" "fmt"
"context"
"coopcloud.tech/abra/pkg/config" "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" "github.com/urfave/cli/v2"
) )
var appRollbackCommand = &cli.Command{ var appRollbackCommand = &cli.Command{
Name: "rollback", Name: "rollback",
Usage: "Roll an app back to a previous version", Usage: "Roll an app back to a previous version",
Aliases: []string{"b"}, Aliases: []string{"r"},
ArgsUsage: "[<version>]", ArgsUsage: "[<version>]",
BashComplete: func(c *cli.Context) { BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames() appNames, err := config.GetAppNames()
if err != nil { if err != nil {
return logrus.Warn(err)
} }
if c.NArg() > 0 { if c.NArg() > 0 {
return return
@ -24,4 +32,49 @@ var appRollbackCommand = &cli.Command{
fmt.Println(a) 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
},
} }

View File

@ -7,6 +7,7 @@ import (
abraFormatter "coopcloud.tech/abra/cli/formatter" abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/client/stack" "coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
@ -27,19 +28,6 @@ func getImagePath(image string) (string, error) {
return path, nil 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{ var appVersionCommand = &cli.Command{
Name: "version", Name: "version",
Aliases: []string{"v"}, Aliases: []string{"v"},
@ -72,7 +60,7 @@ var appVersionCommand = &cli.Command{
for range compose.Services { for range compose.Services {
status := <-ch status := <-ch
if len(status.Services) > 0 { if len(status.Services) > 0 {
serviceName := parseServiceName(status.Services[0].Spec.Name) serviceName := appPkg.ParseServiceName(status.Services[0].Spec.Name)
statuses[serviceName] = status statuses[serviceName] = status
} }
} }
@ -85,7 +73,7 @@ var appVersionCommand = &cli.Command{
if status, ok := statuses[service.Name]; ok { if status, ok := statuses[service.Name]; ok {
statusService := status.Services[0] statusService := status.Services[0]
label := fmt.Sprintf("coop-cloud.%s.%s.version", app.StackName(), service.Name) 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"]) image, err := getImagePath(statusService.Spec.Labels["com.docker.stack.image"])
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)

View File

@ -2,7 +2,6 @@ package internal
import ( import (
"errors" "errors"
"os"
"coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
@ -22,7 +21,6 @@ func ValidateRecipe(c *cli.Context) recipe.Recipe {
recipe, err := recipe.Get(recipeName) recipe, err := recipe.Get(recipeName)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
os.Exit(1)
} }
return recipe return recipe
@ -39,7 +37,6 @@ func ValidateApp(c *cli.Context) config.App {
app, err := app.Get(appName) app, err := app.Get(appName)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
os.Exit(1)
} }
return app return app

View File

@ -1,7 +1,13 @@
package app package app
import ( import (
"context"
"fmt"
"strings"
"coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
apiclient "github.com/docker/docker/client"
) )
// Get retrieves an app // Get retrieves an app
@ -18,3 +24,45 @@ func Get(appName string) (config.App, error) {
return app, nil 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:]
}

View File

@ -13,6 +13,7 @@ import (
"time" "time"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/web" "coopcloud.tech/abra/pkg/web"
) )
@ -212,3 +213,22 @@ func VersionsOfService(recipe, serviceName string) ([]string, error) {
return versions, nil 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
}

View File

@ -18,7 +18,7 @@ import (
func RunRemove(ctx context.Context, client *apiclient.Client, opts Remove) error { func RunRemove(ctx context.Context, client *apiclient.Client, opts Remove) error {
var errs []string var errs []string
for _, namespace := range opts.Namespaces { for _, namespace := range opts.Namespaces {
services, err := getStackServices(ctx, client, namespace) services, err := GetStackServices(ctx, client, namespace)
if err != nil { if err != nil {
return err return err
} }

View File

@ -47,7 +47,7 @@ func getAllStacksFilter() filters.Args {
return filter 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)}) 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 // 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{}) { 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 { if err != nil {
logrus.Infof("Failed to list services: %s\n", err) 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 { if err != nil {
return err return err
} }
return deployServices(ctx, cl, services, namespace, opts.SendRegistryAuth, opts.ResolveImage) return deployServices(ctx, cl, services, namespace, opts.SendRegistryAuth, opts.ResolveImage)
} }
@ -290,7 +291,7 @@ func deployServices(
namespace convert.Namespace, namespace convert.Namespace,
sendAuth bool, sendAuth bool,
resolveImage string) error { resolveImage string) error {
existingServices, err := getStackServices(ctx, cl, namespace.Name()) existingServices, err := GetStackServices(ctx, cl, namespace.Name())
if err != nil { if err != nil {
return err return err
} }