Compare commits

...

14 Commits

Author SHA1 Message Date
Moritz 72c807e31a refactor: replace some functions with general getLabel function 2023-02-01 12:27:35 +01:00
Moritz 8e16d2f350 evaluate if autoupdates are enabled 2023-02-01 12:23:32 +01:00
Moritz b62b46a1c6 Refactor upgrade function: extract logical parts 2023-02-01 12:23:32 +01:00
Moritz fe62b677e0 change .gitignore and add kadabras main.go 2023-02-01 12:23:32 +01:00
Moritz d02a049dde more verbose update info 2023-02-01 12:23:32 +01:00
Moritz df42585490 pull recipe and lint it 2023-02-01 12:23:32 +01:00
Moritz ddead5dac6 updater: read chaos deployment from docker label 2023-02-01 12:23:27 +01:00
Moritz 1931a9b74d PoC auto updater 2023-02-01 12:22:11 +01:00
Moritz 76717531bd resolve PR: include the service info in the log message 2023-01-31 16:15:11 +01:00
Moritz 6774893412 add env ENABLE_AUTO_UPDATE as label to enable/disable the auto update process 2023-01-31 16:12:02 +01:00
moritz ebb86391af add a label to signal that a deploy is a chaos deploy (!265)
Resolves coop-cloud/organising#390 by adding the following label `coop-cloud.${STACK_NAME}.chaos=true` (according to the version label).
This is required for the auto updater coop-cloud/organising#236 (comment)

Co-authored-by: Moritz <moritz.m@local-it.org>
Reviewed-on: coop-cloud/abra#265
2023-01-31 15:06:35 +00:00
moritz 50db39424c add a label to signal that a deploy is connected with a recipe (!264)
Resolves coop-cloud/organising#391 by adding the following label `coop-cloud.${STACK_NAME}.recipe=${RECIPE}` (according to the version label).
This is required for the auto updater coop-cloud/organising#236 (comment)

Co-authored-by: Moritz <moritz.m@local-it.org>
Reviewed-on: coop-cloud/abra#264
2023-01-31 14:35:43 +00:00
moritz ca1ea32c46 Expose all env vars to `app` container. (!263)
Resolves coop-cloud/organising#393 and is required for the auto updater coop-cloud/organising#236 (comment)

Co-authored-by: Moritz <moritz.m@local-it.org>
Reviewed-on: coop-cloud/abra#263
2023-01-31 14:13:43 +00:00
Moritz 32851d4d99 fix: always fetch all repository tags 2023-01-31 11:52:15 +01:00
10 changed files with 468 additions and 2 deletions

1
.gitignore vendored
View File

@ -3,6 +3,7 @@
.envrc
.vscode/
abra
/kadabra
dist/
tests/integration/.abra/catalogue
vendor/

View File

@ -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 .

View File

@ -178,6 +178,10 @@ recipes.
if err != nil {
logrus.Fatal(err)
}
config.ExposeAllEnv(stackName, compose, app.Env)
config.SetRecipeLabel(compose, stackName, app.Recipe)
config.SetChaosLabel(compose, stackName, internal.Chaos)
config.SetUpdateLabel(compose, stackName, app.Env)
if !internal.Force {
if err := internal.NewVersionOverview(app, deployedVersion, chosenDowngrade, ""); err != nil {

View File

@ -189,6 +189,10 @@ recipes.
if err != nil {
logrus.Fatal(err)
}
config.ExposeAllEnv(stackName, compose, app.Env)
config.SetRecipeLabel(compose, stackName, app.Recipe)
config.SetChaosLabel(compose, stackName, internal.Chaos)
config.SetUpdateLabel(compose, stackName, app.Env)
if err := internal.NewVersionOverview(app, deployedVersion, chosenUpgrade, releaseNotes); err != nil {
logrus.Fatal(err)

View File

@ -134,6 +134,10 @@ func DeployAction(c *cli.Context) error {
if err != nil {
logrus.Fatal(err)
}
config.ExposeAllEnv(app.StackName(), compose, app.Env)
config.SetRecipeLabel(compose, app.StackName(), app.Recipe)
config.SetChaosLabel(compose, app.StackName(), Chaos)
config.SetUpdateLabel(compose, app.StackName(), app.Env)
if err := DeployOverview(app, version, "continue with deployment?"); err != nil {
logrus.Fatal(err)

329
cli/updater/updater.go Normal file
View File

@ -0,0 +1,329 @@
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"
)
// 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,
},
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)
}
// 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) {
upgrade(cl, stackName, recipeName)
} else {
logrus.Infof("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("Env Key: %s Value: %s", k, v)
envMap[k] = v
}
}
return envMap
}
func getAvailableUpgrades(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)
}
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.Fatalf("failed to determine deployed version of %s", stackName)
}
if deployedVersion != "unknown" {
logrus.Infof("%s is deployed on %s with version %s", recipeName, stackName, deployedVersion)
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)
}
}
} else {
logrus.Warnf("Could not determine the deployed version for %s", stackName)
}
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) {
logrus.Debugf("Upgrade StackName: %s \n Recipe: %s", stackName, recipeName)
app := config.App{
Name: stackName,
Recipe: recipeName,
Server: "localhost",
Env: getEnv(cl, stackName),
}
availableUpgrades := getAvailableUpgrades(cl, stackName, recipeName)
if len(availableUpgrades) == 0 {
logrus.Infof("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("choosing %s as version to upgrade to", chosenUpgrade)
}
processRecipeRepoVersion(recipeName, chosenUpgrade)
mergeAbraShEnv(recipeName, app.Env)
compose, deployOpts := createDeployConfig(recipeName, stackName, app.Env)
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{
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)
}
}

23
cmd/kadabra/main.go Normal file
View File

@ -0,0 +1,23 @@
// Package main provides the command-line entrypoint.
package main
import (
"coopcloud.tech/abra/cli/updater"
)
// Version is the current version of Abra
var Version string
// Commit is the current git commit of Abra
var Commit string
func main() {
if Version == "" {
Version = "dev"
}
if Commit == "" {
Commit = " "
}
updater.RunApp(Version, Commit)
}

View File

@ -5,6 +5,7 @@ import (
"io/ioutil"
"os"
"path"
"strconv"
"strings"
"github.com/schollz/progressbar/v3"
@ -447,3 +448,61 @@ func GetAppComposeConfig(recipe string, opts stack.Deploy, appEnv AppEnv) (*comp
return compose, nil
}
// ExposeAllEnv exposes all env variables to the app container
func ExposeAllEnv(stackName string, compose *composetypes.Config, appEnv AppEnv) {
for _, service := range compose.Services {
if service.Name == "app" {
logrus.Debugf("Add the following environment to the app service config of %s:", stackName)
for k, v := range appEnv {
_, exists := service.Environment[k]
if !exists {
value := v
service.Environment[k] = &value
logrus.Debugf("Add Key: %s Value: %s to %s", k, value, stackName)
}
}
}
}
}
// SetRecipeLabel adds the label 'coop-cloud.${STACK_NAME}.recipe=${RECIPE}' to the app container
// to signal which recipe is connected to the deployed app
func SetRecipeLabel(compose *composetypes.Config, stackName string, recipe string) {
for _, service := range compose.Services {
if service.Name == "app" {
logrus.Debugf("set recipe label 'coop-cloud.%s.recipe' to %s for %s", stackName, recipe, stackName)
labelKey := fmt.Sprintf("coop-cloud.%s.recipe", stackName)
service.Deploy.Labels[labelKey] = recipe
}
}
}
// SetChaosLabel adds the label 'coop-cloud.${STACK_NAME}.chaos=true/false' to the app container
// to signal if the app is deployed in chaos mode
func SetChaosLabel(compose *composetypes.Config, stackName string, chaos bool) {
for _, service := range compose.Services {
if service.Name == "app" {
logrus.Debugf("set label 'coop-cloud.%s.chaos' to %s for %s", stackName, chaos, stackName)
labelKey := fmt.Sprintf("coop-cloud.%s.chaos", stackName)
service.Deploy.Labels[labelKey] = strconv.FormatBool(chaos)
}
}
}
// SetUpdateLabel adds env ENABLE_AUTO_UPDATE as label to enable/disable the
// auto update process for this app. The default if this variable is not set is to disable
// the auto update process.
func SetUpdateLabel(compose *composetypes.Config, stackName string, appEnv AppEnv) {
for _, service := range compose.Services {
if service.Name == "app" {
enable_auto_update, exists := appEnv["ENABLE_AUTO_UPDATE"]
if !exists {
enable_auto_update = "false"
}
logrus.Debugf("set label 'coop-cloud.%s.autoupdate' to %s for %s", stackName, enable_auto_update, stackName)
labelKey := fmt.Sprintf("coop-cloud.%s.autoupdate", stackName)
service.Deploy.Labels[labelKey] = enable_auto_update
}
}
}

View File

@ -309,8 +309,7 @@ func EnsureVersion(recipeName, version string) error {
logrus.Debugf("read %s as tags for recipe %s", strings.Join(parsedTags, ", "), recipeName)
if tagRef.String() == "" {
logrus.Warnf("no published release discovered for %s", recipeName)
return nil
return fmt.Errorf("no published release discovered for %s", recipeName)
}
worktree, err := repo.Worktree()
@ -616,6 +615,11 @@ func EnsureUpToDate(recipeName string) error {
return err
}
fetchOpts := &git.FetchOptions{
Tags: git.AllTags,
}
repo.Fetch(fetchOpts)
opts := &git.PullOptions{
Force: true,
ReferenceName: branch,

View File

@ -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
}