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: " ", 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) } }