forked from coop-cloud/abra
PoC auto updater
This commit is contained in:
parent
c47aa49373
commit
667264b5bb
|
@ -3,6 +3,7 @@
|
|||
.envrc
|
||||
.vscode/
|
||||
abra
|
||||
kadabra
|
||||
dist/
|
||||
tests/integration/.abra/catalogue
|
||||
vendor/
|
||||
|
|
3
Makefile
3
Makefile
|
@ -1,4 +1,5 @@
|
|||
ABRA := ./cmd/abra
|
||||
KADABRA := ./cmd/kadabra
|
||||
COMMIT := $(shell git rev-list -1 HEAD)
|
||||
GOPATH := $(shell go env GOPATH)
|
||||
LDFLAGS := "-X 'main.Commit=$(COMMIT)'"
|
||||
|
@ -18,9 +19,11 @@ build-dev:
|
|||
|
||||
build:
|
||||
@go build -ldflags=$(DIST_LDFLAGS) $(ABRA)
|
||||
@go build -ldflags=$(DIST_LDFLAGS) $(KADABRA)
|
||||
|
||||
clean:
|
||||
@rm '$(GOPATH)/bin/abra'
|
||||
@rm '$(GOPATH)/bin/kadabra'
|
||||
|
||||
format:
|
||||
@gofmt -s -w .
|
||||
|
|
|
@ -0,0 +1,277 @@
|
|||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/abra/pkg/upstream/convert"
|
||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||
"coopcloud.tech/tagcmp"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
dockerclient "github.com/docker/docker/client"
|
||||
|
||||
// "github.com/docker/cli/cli/command/stack/swarm"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// Upgrade a specific app
|
||||
var UpgradeApp = cli.Command{
|
||||
Name: "appupgrade",
|
||||
Aliases: []string{"a"},
|
||||
Usage: "Upgrade an app",
|
||||
ArgsUsage: "<stack_name> <recipe>",
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
internal.ForceFlag,
|
||||
internal.DontWaitConvergeFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
Description: `Upgrade an app`,
|
||||
Action: func(c *cli.Context) error {
|
||||
stackName := c.Args().Get(0)
|
||||
recipeName := c.Args().Get(1)
|
||||
cl, err := client.New("default")
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
upgrade(cl, stackName, recipeName)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Upgrade all appS
|
||||
var UpgradeAll = cli.Command{
|
||||
Name: "upgrade",
|
||||
Aliases: []string{"u"},
|
||||
Usage: "Upgrade all apps",
|
||||
Flags: []cli.Flag{
|
||||
internal.DebugFlag,
|
||||
internal.ForceFlag,
|
||||
internal.DontWaitConvergeFlag,
|
||||
},
|
||||
Before: internal.SubCommandBefore,
|
||||
Description: `Upgrade all deployed apps`,
|
||||
Action: func(c *cli.Context) error {
|
||||
|
||||
cl, err := client.New("default")
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
// stacks := swarm.GetStacks(cl)
|
||||
stacks, err := stack.GetStacks(cl)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
for _, stackInfo := range stacks {
|
||||
stackName := stackInfo.Name
|
||||
recipeName := getRecipe(cl, stackName)
|
||||
// TODO: read chaos from docker label
|
||||
if recipeName != "" {
|
||||
logrus.Debugf("RecipeName: %s", recipeName)
|
||||
upgrade(cl, stackName, recipeName)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Read recipe from docker label
|
||||
func getRecipe(cl *dockerclient.Client, stackName string) string {
|
||||
filter := filters.NewArgs()
|
||||
filter.Add("label", fmt.Sprintf("%s=%s", convert.LabelNamespace, stackName))
|
||||
|
||||
services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: filter})
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
for _, service := range services {
|
||||
labelKey := fmt.Sprintf("coop-cloud.%s.recipe", stackName)
|
||||
if recipeName, ok := service.Spec.Labels[labelKey]; ok {
|
||||
return recipeName
|
||||
}
|
||||
}
|
||||
logrus.Debugf("no recipe name found for %s", stackName)
|
||||
return ""
|
||||
}
|
||||
|
||||
// Read Env variables from docker services
|
||||
func getEnv(cl *dockerclient.Client, stackName string) config.AppEnv {
|
||||
envMap := make(map[string]string)
|
||||
filter := filters.NewArgs()
|
||||
filter.Add("label", fmt.Sprintf("%s=%s", convert.LabelNamespace, stackName))
|
||||
|
||||
services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: filter})
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
for _, service := range services {
|
||||
envList := service.Spec.TaskTemplate.ContainerSpec.Env
|
||||
for _, envString := range envList {
|
||||
splitString := strings.SplitN(envString, "=", 2)
|
||||
if len(splitString) != 2 {
|
||||
logrus.Debugf("can't separate key from value: %s (this variable is probably unset)", envString)
|
||||
continue
|
||||
}
|
||||
k := splitString[0]
|
||||
v := splitString[1]
|
||||
logrus.Debugf("Env Key: %s Value: %s", k, v)
|
||||
envMap[k] = v
|
||||
}
|
||||
}
|
||||
return envMap
|
||||
}
|
||||
|
||||
func upgrade(cl *dockerclient.Client, stackName string, recipeName string) {
|
||||
logrus.Debugf("Upgrade StackName: %s \n Recipe: %s", stackName, recipeName)
|
||||
app := config.App{
|
||||
Name: stackName,
|
||||
Recipe: recipeName,
|
||||
Server: "localhost",
|
||||
Env: getEnv(cl, stackName),
|
||||
}
|
||||
// Workaround, is there a better way?
|
||||
app.Env["STACK_NAME"] = stackName
|
||||
// TODO: read COMPOSE_FILE from docker label
|
||||
// TODO: evaluate ENABLE_AUTO_UPDATE env var
|
||||
|
||||
logrus.Debugf("Retrieve deployed version whether %s is already deployed", stackName)
|
||||
isDeployed, deployedVersion, err := stack.IsDeployed(context.Background(), cl, stackName)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if !isDeployed {
|
||||
logrus.Fatalf("%s is not deployed?", stackName)
|
||||
}
|
||||
|
||||
catl, err := recipe.ReadRecipeCatalogue()
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
versions, err := recipe.GetRecipeCatalogueVersions(recipeName, catl)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if len(versions) == 0 {
|
||||
logrus.Fatalf("no published releases for %s in the recipe catalogue?", recipeName)
|
||||
}
|
||||
|
||||
var availableUpgrades []string
|
||||
if deployedVersion == "unknown" {
|
||||
availableUpgrades = versions
|
||||
logrus.Warnf("failed to determine version of deployed %s", stackName)
|
||||
}
|
||||
|
||||
if deployedVersion != "unknown" {
|
||||
for _, version := range versions {
|
||||
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
parsedVersion, err := tagcmp.Parse(version)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
versionDelta, err := parsedDeployedVersion.UpgradeDelta(parsedVersion)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
// Only update Patch/Minor updates
|
||||
if 0 < versionDelta.UpgradeType() && versionDelta.UpgradeType() < 4 {
|
||||
availableUpgrades = append(availableUpgrades, version)
|
||||
}
|
||||
}
|
||||
|
||||
if len(availableUpgrades) == 0 {
|
||||
logrus.Fatalf("no available upgrades, you're on latest (%s) ✌️", deployedVersion)
|
||||
}
|
||||
}
|
||||
|
||||
availableUpgrades = internal.ReverseStringList(availableUpgrades)
|
||||
|
||||
var chosenUpgrade string
|
||||
if len(availableUpgrades) > 0 {
|
||||
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
|
||||
logrus.Debugf("choosing %s as version to upgrade to", chosenUpgrade)
|
||||
}
|
||||
|
||||
if err := recipe.EnsureExists(recipeName); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
if err := recipe.EnsureVersion(recipeName, chosenUpgrade); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
abraShPath := fmt.Sprintf("%s/%s/%s", config.RECIPES_DIR, recipeName, "abra.sh")
|
||||
abraShEnv, err := config.ReadAbraShEnvVars(abraShPath)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
for k, v := range abraShEnv {
|
||||
logrus.Debugf("read v:%s k: %s", v, k)
|
||||
app.Env[k] = v
|
||||
}
|
||||
|
||||
composeFiles, err := config.GetAppComposeFiles(recipeName, app.Env)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
deployOpts := stack.Deploy{
|
||||
Composefiles: composeFiles,
|
||||
Namespace: stackName,
|
||||
Prune: false,
|
||||
ResolveImage: stack.ResolveImageAlways,
|
||||
}
|
||||
compose, err := config.GetAppComposeConfig(stackName, deployOpts, app.Env)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if err := stack.RunDeploy(cl, deployOpts, compose, stackName, true); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func newAbraApp(version, commit string) *cli.App {
|
||||
app := &cli.App{
|
||||
Name: "kadabra",
|
||||
Usage: `The Co-op Cloud update daemon
|
||||
____ ____ _ _
|
||||
/ ___|___ ___ _ __ / ___| | ___ _ _ __| |
|
||||
| | / _ \ _____ / _ \| '_ \ | | | |/ _ \| | | |/ _' |
|
||||
| |__| (_) |_____| (_) | |_) | | |___| | (_) | |_| | (_| |
|
||||
\____\___/ \___/| .__/ \____|_|\___/ \__,_|\__,_|
|
||||
|_|
|
||||
`,
|
||||
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
|
||||
Commands: []cli.Command{
|
||||
UpgradeApp,
|
||||
UpgradeAll,
|
||||
},
|
||||
}
|
||||
|
||||
app.Before = func(c *cli.Context) error {
|
||||
logrus.Debugf("abra version %s, commit %s", version, commit)
|
||||
return nil
|
||||
}
|
||||
return app
|
||||
}
|
||||
|
||||
// RunApp runs CLI abra app.
|
||||
func RunApp(version, commit string) {
|
||||
app := newAbraApp(version, commit)
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ import (
|
|||
abraClient "coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/upstream/convert"
|
||||
"github.com/docker/cli/cli/command/service/progress"
|
||||
"github.com/docker/cli/cli/command/stack/formatter"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
|
@ -507,3 +508,37 @@ If a service is failing to even start, try smoke out the error with:
|
|||
`, appName, timeout, appName, appName, appName))
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: Copypasta from https://github.com/docker/cli/blob/master/cli/command/stack/swarm/list.go because I could't import "github.com/docker/cli/cli/command/stack/swarm"
|
||||
// GetStacks lists the swarm stacks.
|
||||
func GetStacks(cl *dockerclient.Client) ([]*formatter.Stack, error) {
|
||||
services, err := cl.ServiceList(
|
||||
context.Background(),
|
||||
types.ServiceListOptions{Filters: getAllStacksFilter()})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[string]*formatter.Stack)
|
||||
for _, service := range services {
|
||||
labels := service.Spec.Labels
|
||||
name, ok := labels[convert.LabelNamespace]
|
||||
if !ok {
|
||||
return nil, errors.Errorf("cannot get label %s for service %s",
|
||||
convert.LabelNamespace, service.ID)
|
||||
}
|
||||
ztack, ok := m[name]
|
||||
if !ok {
|
||||
m[name] = &formatter.Stack{
|
||||
Name: name,
|
||||
Services: 1,
|
||||
}
|
||||
} else {
|
||||
ztack.Services++
|
||||
}
|
||||
}
|
||||
var stacks []*formatter.Stack
|
||||
for _, stack := range m {
|
||||
stacks = append(stacks, stack)
|
||||
}
|
||||
return stacks, nil
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue