abra/cli/updater/updater.go

380 lines
10 KiB
Go

package updater
import (
"context"
"fmt"
"os"
"strconv"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/upstream/convert"
"coopcloud.tech/abra/pkg/upstream/stack"
"coopcloud.tech/tagcmp"
composetypes "github.com/docker/cli/cli/compose/types"
"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"
)
var majorUpdate bool
var majorFlag = &cli.BoolFlag{
Name: "major, m",
Usage: "Also check for major updates",
Destination: &majorUpdate,
}
// Check for available upgrades
var Notify = cli.Command{
Name: "notify",
Aliases: []string{"n"},
Usage: "Check for available upgrades",
Flags: []cli.Flag{
internal.DebugFlag,
majorFlag,
},
Before: internal.SubCommandBefore,
Description: `Check for available upgrades`,
Action: func(c *cli.Context) error {
cl, err := client.New("default")
if err != nil {
logrus.Fatal(err)
}
// can't import this lib:
// stacks := swarm.GetStacks(cl)
stacks, err := stack.GetStacks(cl)
if err != nil {
logrus.Fatal(err)
}
for _, stackInfo := range stacks {
stackName := stackInfo.Name
recipeName := getLabel(cl, stackName, "recipe")
if recipeName != "" {
getLatestUpgrade(cl, stackName, recipeName)
}
}
return nil
},
}
// 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,
majorFlag,
},
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)
}
upgradeVersion := getLatestUpgrade(cl, stackName, recipeName)
if upgradeVersion != "" {
upgrade(cl, stackName, recipeName, upgradeVersion)
}
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,
majorFlag,
},
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)
}
// can't import this lib:
// stacks := swarm.GetStacks(cl)
stacks, err := stack.GetStacks(cl)
if err != nil {
logrus.Fatal(err)
}
for _, stackInfo := range stacks {
stackName := stackInfo.Name
recipeName := getLabel(cl, stackName, "recipe")
chaos := getBoolLabel(cl, stackName, "chaos")
updatesEnabled := getBoolLabel(cl, stackName, "autoupdate")
if recipeName != "" && updatesEnabled && (!chaos || internal.Force) {
upgradeVersion := getLatestUpgrade(cl, stackName, recipeName)
if upgradeVersion != "" {
upgrade(cl, stackName, recipeName, upgradeVersion)
}
} else {
logrus.Debugf("Don't update %s due to missing recipe name, disabled updates or chaos deployment", stackName)
}
}
return nil
},
}
// Read docker label in the format coop-cloud.${STACK_NAME}.${LABEL}
func getLabel(cl *dockerclient.Client, stackName string, label 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.%s", stackName, label)
if labelValue, ok := service.Spec.Labels[labelKey]; ok {
return labelValue
}
}
logrus.Debugf("no %s label found for %s", label, stackName)
return ""
}
// Read boolean docker label
func getBoolLabel(cl *dockerclient.Client, stackName string, label string) bool {
lableValue := getLabel(cl, stackName, label)
if lableValue != "" {
value, err := strconv.ParseBool(lableValue)
if err != nil {
logrus.Fatal(err)
}
return value
}
return false
}
// 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("For %s read env %s with value: %s from docker service", stackName, k, v)
envMap[k] = v
}
}
return envMap
}
func getLatestUpgrade(cl *dockerclient.Client, stackName string, recipeName string) string {
deployedVersion := getDeployedVersion(cl, stackName, recipeName)
availableUpgrades := getAvailableUpgrades(cl, stackName, recipeName, deployedVersion)
if len(availableUpgrades) == 0 {
logrus.Debugf("no available upgrades for %s", stackName)
return ""
}
availableUpgrades = internal.ReverseStringList(availableUpgrades)
var chosenUpgrade string
if len(availableUpgrades) > 0 {
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
logrus.Infof("%s (%s) can be upgraded from version %s to %s", stackName, recipeName, deployedVersion, chosenUpgrade)
}
return chosenUpgrade
}
func getDeployedVersion(cl *dockerclient.Client, stackName string, recipeName string) string {
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)
}
if deployedVersion == "unknown" {
logrus.Fatalf("failed to determine deployed version of %s", stackName)
}
return deployedVersion
}
func getAvailableUpgrades(cl *dockerclient.Client, stackName string, recipeName string,
deployedVersion string) []string {
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
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)
}
if 0 < versionDelta.UpgradeType() && (versionDelta.UpgradeType() < 4 || majorUpdate) {
availableUpgrades = append(availableUpgrades, version)
}
}
return availableUpgrades
}
// clone, pull, checkout version and lint the recipe repository
func processRecipeRepoVersion(recipeName string, version string) {
if err := recipe.EnsureExists(recipeName); err != nil {
logrus.Fatal(err)
}
if err := recipe.EnsureUpToDate(recipeName); err != nil {
logrus.Fatal(err)
}
if err := recipe.EnsureVersion(recipeName, version); err != nil {
logrus.Fatal(err)
}
if r, err := recipe.Get(recipeName); err != nil {
logrus.Fatal(err)
} else if err := lint.LintForErrors(r); err != nil {
logrus.Fatal(err)
}
}
// merge abra.sh env's into app env's
func mergeAbraShEnv(recipeName string, env config.AppEnv) {
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)
env[k] = v
}
}
func createDeployConfig(recipeName string, stackName string, env config.AppEnv) (*composetypes.Config, stack.Deploy) {
// Workaround, is there a better way?
env["STACK_NAME"] = stackName
composeFiles, err := config.GetAppComposeFiles(recipeName, 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, env)
if err != nil {
logrus.Fatal(err)
}
config.ExposeAllEnv(stackName, compose, env)
// after the upgrade the deployment won't be in chaos state anymore
config.SetChaosLabel(compose, stackName, false)
config.SetRecipeLabel(compose, stackName, recipeName)
return compose, deployOpts
}
func upgrade(cl *dockerclient.Client, stackName string, recipeName string, upgradeVersion string) {
app := config.App{
Name: stackName,
Recipe: recipeName,
Server: "localhost",
Env: getEnv(cl, stackName),
}
processRecipeRepoVersion(recipeName, upgradeVersion)
mergeAbraShEnv(recipeName, app.Env)
compose, deployOpts := createDeployConfig(recipeName, stackName, app.Env)
logrus.Infof("Upgrade %s (%s) to version %s", stackName, recipeName, upgradeVersion)
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 autoupdater
____ ____ _ _
/ ___|___ ___ _ __ / ___| | ___ _ _ __| |
| | / _ \ _____ / _ \| '_ \ | | | |/ _ \| | | |/ _' |
| |__| (_) |_____| (_) | |_) | | |___| | (_) | |_| | (_| |
\____\___/ \___/| .__/ \____|_|\___/ \__,_|\__,_|
|_|
`,
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
Commands: []cli.Command{
Notify,
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)
}
}