forked from toolshed/abra
		
	Compare commits
	
		
			64 Commits
		
	
	
		
			0.1.5-alph
			...
			0.2.0-alph
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 75fb9a2774 | |||
| 0d500b636d | |||
| 5dd97cace0 | |||
| ae32b1eed2 | |||
| 113bdf9e86 | |||
| d4d4da19b7 | |||
| 454ee696d6 | |||
| ca16c002ba | |||
| 91cc8b00b3 | |||
| d0828c4d8d | |||
| b69aed3bcf | |||
| 875255fd8c | |||
| 2dca602c0b | |||
| 1dca8a1067 | |||
| 37022bf0c8 | |||
| eb5b35d47f | |||
| ece1130797 | |||
| c266316f7e | |||
| d804276cf2 | |||
| 4235e06943 | |||
| a9af0b3627 | |||
| a0b4886eba | |||
| 84489495dc | |||
| a8683dc38a | |||
| e2128ea5b6 | |||
| ca3c5fef0f | |||
| 4a01e411be | |||
| 777d49ac1d | |||
| deb7d21158 | |||
| 6db1fdcfba | |||
| 44dc0edf7b | |||
| 36ff50312c | |||
| ff4b978876 | |||
| b68547b2c2 | |||
| 0140f96ca1 | |||
| 1cb45113db | |||
| c764243f3a | |||
| dde8afcd43 | |||
| 98ffc210e1 | |||
| 7c0d883135 | |||
| e78ced41fb | |||
| e9113500d8 | |||
| 7368cabc49 | |||
| f75e264811 | |||
| 8bfd76fd04 | |||
| 1cb5e3509d | |||
| 3cd2399cca | |||
| 11c4651a3b | |||
| 49f90674f2 | |||
| 74a70edb03 | |||
| 6fc5c31347 | |||
| c616907b71 | |||
| a58cea3e0a | |||
| 700f89425a | |||
| 8cc0a350e6 | |||
| 46e67fa420 | |||
| cacbb5a0f1 | |||
| e7046a15aa | |||
| c1fd97c427 | |||
| 2f218bd99f | |||
| 48290aa316 | |||
| db5cbfa992 | |||
| 4c11e813e8 | |||
| 6ae75e013a | 
| @ -14,12 +14,12 @@ builds: | ||||
|     dir: cmd/abra | ||||
|     goos: | ||||
|       - linux | ||||
|       - darwin | ||||
|     ldflags: | ||||
|       - "-X 'main.Commit={{ .Commit }}'" | ||||
|       - "-X 'main.Version={{ .Version }}'" | ||||
| archives: | ||||
|   - replacements: | ||||
|       linux: Linux | ||||
|       386: i386 | ||||
|       amd64: x86_64 | ||||
|     format: binary | ||||
| @ -28,8 +28,13 @@ checksum: | ||||
| snapshot: | ||||
|   name_template: "{{ incpatch .Version }}-next" | ||||
| changelog: | ||||
|   sort: asc | ||||
|   sort: desc | ||||
|   filters: | ||||
|     exclude: | ||||
|       - "^WIP:" | ||||
|       - "^chore:" | ||||
|       - "^docs:" | ||||
|       - "^refactor:" | ||||
|       - "^style:" | ||||
|       - "^test:" | ||||
|       - "^tests:" | ||||
|  | ||||
| @ -19,6 +19,7 @@ to scaling apps up and spinning them down. | ||||
| 		appNewCommand, | ||||
| 		appConfigCommand, | ||||
| 		appDeployCommand, | ||||
| 		appUpgradeCommand, | ||||
| 		appUndeployCommand, | ||||
| 		appBackupCommand, | ||||
| 		appRestoreCommand, | ||||
|  | ||||
| @ -2,11 +2,16 @@ package app | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
|  | ||||
| 	abraFormatter "coopcloud.tech/abra/cli/formatter" | ||||
| 	"coopcloud.tech/abra/cli/internal" | ||||
| 	"coopcloud.tech/abra/pkg/catalogue" | ||||
| 	"coopcloud.tech/abra/pkg/client" | ||||
| 	stack "coopcloud.tech/abra/pkg/client/stack" | ||||
| 	"coopcloud.tech/abra/pkg/config" | ||||
| 	"coopcloud.tech/abra/pkg/recipe" | ||||
| 	"github.com/AlecAivazis/survey/v2" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/urfave/cli/v2" | ||||
| ) | ||||
| @ -15,14 +20,63 @@ var appDeployCommand = &cli.Command{ | ||||
| 	Name:    "deploy", | ||||
| 	Aliases: []string{"d"}, | ||||
| 	Usage:   "Deploy an app", | ||||
| 	Flags: []cli.Flag{ | ||||
| 		internal.ForceFlag, | ||||
| 	}, | ||||
| 	Description: ` | ||||
| This command deploys a new instance of an app. It does not support changing the | ||||
| version of an existing deployed app, for this you need to look at the "abra app | ||||
| upgrade <app>" command. | ||||
|  | ||||
| You may pass "--force" to re-deploy the same version again. This can be useful | ||||
| if the container runtime has gotten into a weird state or your doing some live | ||||
| hacking. | ||||
| `, | ||||
| 	Action: func(c *cli.Context) error { | ||||
| 		app := internal.ValidateApp(c) | ||||
| 		stackName := app.StackName() | ||||
|  | ||||
| 		cl, err := client.New(app.Server) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		logrus.Debugf("checking whether '%s' is already deployed", stackName) | ||||
|  | ||||
| 		isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		if isDeployed { | ||||
| 			if internal.Force { | ||||
| 				logrus.Infof("continuing with deployment due to '--force/-f' being set") | ||||
| 			} else { | ||||
| 				logrus.Fatalf("'%s' is already deployed", stackName) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		version := deployedVersion | ||||
| 		if version == "" { | ||||
| 			versions, err := catalogue.GetRecipeCatalogueVersions(app.Type) | ||||
| 			if err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 			if len(versions) > 0 { | ||||
| 				version = versions[len(versions)-1] | ||||
| 				logrus.Infof("choosing '%s' as version to deploy", version) | ||||
| 				if err := recipe.EnsureVersion(app.Type, version); err != nil { | ||||
| 					logrus.Fatal(err) | ||||
| 				} | ||||
| 			} else { | ||||
| 				version = "latest commit" | ||||
| 				logrus.Warning("no versions detected, using latest commit") | ||||
| 				if err := recipe.EnsureLatest(app.Type); err != nil { | ||||
| 					logrus.Fatal(err) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		abraShPath := fmt.Sprintf("%s/%s/%s", config.APPS_DIR, app.Type, "abra.sh") | ||||
| 		abraShEnv, err := config.ReadAbraShEnvVars(abraShPath) | ||||
| 		if err != nil { | ||||
| @ -31,7 +85,6 @@ var appDeployCommand = &cli.Command{ | ||||
| 		for k, v := range abraShEnv { | ||||
| 			app.Env[k] = v | ||||
| 		} | ||||
| 		app.Env["STACK_NAME"] = app.StackName() | ||||
|  | ||||
| 		composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env) | ||||
| 		if err != nil { | ||||
| @ -39,7 +92,7 @@ var appDeployCommand = &cli.Command{ | ||||
| 		} | ||||
| 		deployOpts := stack.Deploy{ | ||||
| 			Composefiles: composeFiles, | ||||
| 			Namespace:    app.StackName(), | ||||
| 			Namespace:    stackName, | ||||
| 			Prune:        false, | ||||
| 			ResolveImage: stack.ResolveImageAlways, | ||||
| 		} | ||||
| @ -48,6 +101,10 @@ var appDeployCommand = &cli.Command{ | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		if err := DeployOverview(app, version); err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		if err := stack.RunDeploy(cl, deployOpts, compose); err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
| @ -67,3 +124,61 @@ var appDeployCommand = &cli.Command{ | ||||
| 		} | ||||
| 	}, | ||||
| } | ||||
|  | ||||
| // DeployOverview shows a deployment overview | ||||
| func DeployOverview(app config.App, version string) error { | ||||
| 	tableCol := []string{"server", "compose", "domain", "stack", "version"} | ||||
| 	table := abraFormatter.CreateTable(tableCol) | ||||
|  | ||||
| 	deployConfig := "compose.yml" | ||||
| 	if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { | ||||
| 		deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n") | ||||
| 	} | ||||
|  | ||||
| 	table.Append([]string{app.Server, deployConfig, app.Domain, app.StackName(), version}) | ||||
| 	table.Render() | ||||
|  | ||||
| 	response := false | ||||
| 	prompt := &survey.Confirm{ | ||||
| 		Message: "continue with deployment?", | ||||
| 	} | ||||
|  | ||||
| 	if err := survey.AskOne(prompt, &response); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if !response { | ||||
| 		logrus.Fatal("exiting as requested") | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // NewVersionOverview shows an upgrade or downgrade overview | ||||
| func NewVersionOverview(app config.App, currentVersion, newVersion string) error { | ||||
| 	tableCol := []string{"server", "compose", "domain", "stack", "current version", "to be deployed"} | ||||
| 	table := abraFormatter.CreateTable(tableCol) | ||||
|  | ||||
| 	deployConfig := "compose.yml" | ||||
| 	if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { | ||||
| 		deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n") | ||||
| 	} | ||||
|  | ||||
| 	table.Append([]string{app.Server, deployConfig, app.Domain, app.StackName(), currentVersion, newVersion}) | ||||
| 	table.Render() | ||||
|  | ||||
| 	response := false | ||||
| 	prompt := &survey.Confirm{ | ||||
| 		Message: "continue with deployment?", | ||||
| 	} | ||||
|  | ||||
| 	if err := survey.AskOne(prompt, &response); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if !response { | ||||
| 		logrus.Fatal("exiting as requested") | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| @ -1,10 +1,14 @@ | ||||
| package app | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"sort" | ||||
| 	"strings" | ||||
|  | ||||
| 	abraFormatter "coopcloud.tech/abra/cli/formatter" | ||||
| 	"coopcloud.tech/abra/pkg/catalogue" | ||||
| 	"coopcloud.tech/abra/pkg/config" | ||||
| 	"coopcloud.tech/tagcmp" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/urfave/cli/v2" | ||||
| ) | ||||
| @ -65,10 +69,10 @@ can take some time. | ||||
| 		} | ||||
| 		sort.Sort(config.ByServerAndType(apps)) | ||||
|  | ||||
| 		statuses := map[string]string{} | ||||
| 		statuses := make(map[string]map[string]string) | ||||
| 		tableCol := []string{"Server", "Type", "Domain"} | ||||
| 		if status { | ||||
| 			tableCol = append(tableCol, "Status") | ||||
| 			tableCol = append(tableCol, "Status", "Version", "Updates") | ||||
| 			statuses, err = config.GetAppStatuses(appFiles) | ||||
| 			if err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| @ -78,22 +82,90 @@ can take some time. | ||||
| 		table := abraFormatter.CreateTable(tableCol) | ||||
| 		table.SetAutoMergeCellsByColumnIndex([]int{0}) | ||||
|  | ||||
| 		var ( | ||||
| 			versionedAppsCount   int | ||||
| 			unversionedAppsCount int | ||||
| 			onLatestCount        int | ||||
| 			canUpgradeCount      int | ||||
| 		) | ||||
|  | ||||
| 		for _, app := range apps { | ||||
| 			var tableRow []string | ||||
| 			if app.Type == appType || appType == "" { | ||||
| 				// If type flag is set, check for it, if not, Type == "" | ||||
| 				tableRow = []string{app.Server, app.Type, app.Domain} | ||||
| 				if status { | ||||
| 					if status, ok := statuses[app.StackName()]; ok { | ||||
| 						tableRow = append(tableRow, status) | ||||
| 					stackName := app.StackName() | ||||
| 					status := "unknown" | ||||
| 					version := "unknown" | ||||
| 					if statusMeta, ok := statuses[stackName]; ok { | ||||
| 						if currentVersion, exists := statusMeta["version"]; exists { | ||||
| 							version = currentVersion | ||||
| 						} | ||||
| 						if statusMeta["status"] != "" { | ||||
| 							status = statusMeta["status"] | ||||
| 						} | ||||
| 						tableRow = append(tableRow, status, version) | ||||
| 						versionedAppsCount++ | ||||
| 					} else { | ||||
| 						tableRow = append(tableRow, status, version) | ||||
| 						unversionedAppsCount++ | ||||
| 					} | ||||
|  | ||||
| 					var newUpdates []string | ||||
| 					if version != "unknown" { | ||||
| 						updates, err := catalogue.GetRecipeCatalogueVersions(app.Type) | ||||
| 						if err != nil { | ||||
| 							logrus.Fatal(err) | ||||
| 						} | ||||
|  | ||||
| 						parsedVersion, err := tagcmp.Parse(version) | ||||
| 						if err != nil { | ||||
| 							logrus.Fatal(err) | ||||
| 						} | ||||
|  | ||||
| 						for _, update := range updates { | ||||
| 							parsedUpdate, err := tagcmp.Parse(update) | ||||
| 							if err != nil { | ||||
| 								logrus.Fatal(err) | ||||
| 							} | ||||
|  | ||||
| 							if update != version && parsedUpdate.IsGreaterThan(parsedVersion) { | ||||
| 								newUpdates = append(newUpdates, update) | ||||
| 							} | ||||
| 						} | ||||
| 					} | ||||
|  | ||||
| 					if len(newUpdates) == 0 { | ||||
| 						if version == "unknown" { | ||||
| 							tableRow = append(tableRow, "unknown") | ||||
| 						} else { | ||||
| 							tableRow = append(tableRow, "on latest") | ||||
| 							onLatestCount++ | ||||
| 						} | ||||
| 					} else { | ||||
| 						// FIXME: jeezus golang why do you not have a list reverse function | ||||
| 						for i, j := 0, len(newUpdates)-1; i < j; i, j = i+1, j-1 { | ||||
| 							newUpdates[i], newUpdates[j] = newUpdates[j], newUpdates[i] | ||||
| 						} | ||||
| 						tableRow = append(tableRow, strings.Join(newUpdates, "\n")) | ||||
| 						canUpgradeCount++ | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 			table.Append(tableRow) | ||||
| 		} | ||||
|  | ||||
| 		stats := fmt.Sprintf( | ||||
| 			"Total apps: %v | Versioned: %v | Unversioned: %v | On latest: %v | Can upgrade: %v", | ||||
| 			len(apps), | ||||
| 			versionedAppsCount, | ||||
| 			unversionedAppsCount, | ||||
| 			onLatestCount, | ||||
| 			canUpgradeCount, | ||||
| 		) | ||||
|  | ||||
| 		table.SetCaption(true, stats) | ||||
| 		table.Render() | ||||
|  | ||||
| 		return nil | ||||
|  | ||||
| @ -3,6 +3,7 @@ package app | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	abraFormatter "coopcloud.tech/abra/cli/formatter" | ||||
| 	"coopcloud.tech/abra/cli/internal" | ||||
| @ -15,11 +16,50 @@ import ( | ||||
| 	"github.com/urfave/cli/v2" | ||||
| ) | ||||
|  | ||||
| var watch bool | ||||
| var watchFlag = &cli.BoolFlag{ | ||||
| 	Name:        "watch", | ||||
| 	Aliases:     []string{"w"}, | ||||
| 	Value:       false, | ||||
| 	Usage:       "Watch status by polling repeatedly", | ||||
| 	Destination: &watch, | ||||
| } | ||||
|  | ||||
| var appPsCommand = &cli.Command{ | ||||
| 	Name:    "ps", | ||||
| 	Usage:   "Check app status", | ||||
| 	Aliases: []string{"p"}, | ||||
| 	Flags: []cli.Flag{ | ||||
| 		watchFlag, | ||||
| 	}, | ||||
| 	Action: func(c *cli.Context) error { | ||||
| 		if !watch { | ||||
| 			showPSOutput(c) | ||||
| 			return nil | ||||
| 		} | ||||
|  | ||||
| 		// TODO: how do we make this update in-place in an x-platform way? | ||||
| 		for { | ||||
| 			showPSOutput(c) | ||||
| 			time.Sleep(2 * time.Second) | ||||
| 		} | ||||
| 	}, | ||||
| 	BashComplete: func(c *cli.Context) { | ||||
| 		appNames, err := config.GetAppNames() | ||||
| 		if err != nil { | ||||
| 			logrus.Warn(err) | ||||
| 		} | ||||
| 		if c.NArg() > 0 { | ||||
| 			return | ||||
| 		} | ||||
| 		for _, a := range appNames { | ||||
| 			fmt.Println(a) | ||||
| 		} | ||||
| 	}, | ||||
| } | ||||
|  | ||||
| // showPSOutput renders ps output. | ||||
| func showPSOutput(c *cli.Context) { | ||||
| 	app := internal.ValidateApp(c) | ||||
|  | ||||
| 	cl, err := client.New(app.Server) | ||||
| @ -35,35 +75,25 @@ var appPsCommand = &cli.Command{ | ||||
| 		logrus.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 		tableCol := []string{"id", "image", "command", "created", "status", "ports", "names"} | ||||
| 	tableCol := []string{"image", "created", "status", "ports", "names"} | ||||
| 	table := abraFormatter.CreateTable(tableCol) | ||||
|  | ||||
| 	for _, container := range containers { | ||||
| 		var containerNames []string | ||||
| 		for _, containerName := range container.Names { | ||||
| 			trimmed := strings.TrimPrefix(containerName, "/") | ||||
| 			containerNames = append(containerNames, trimmed) | ||||
| 		} | ||||
|  | ||||
| 		tableRow := []string{ | ||||
| 				abraFormatter.ShortenID(container.ID), | ||||
| 			abraFormatter.RemoveSha(container.Image), | ||||
| 				abraFormatter.Truncate(container.Command), | ||||
| 			abraFormatter.HumanDuration(container.Created), | ||||
| 			container.Status, | ||||
| 			formatter.DisplayablePorts(container.Ports), | ||||
| 				strings.Join(container.Names, ", "), | ||||
| 			strings.Join(containerNames, "\n"), | ||||
| 		} | ||||
| 		table.Append(tableRow) | ||||
| 	} | ||||
|  | ||||
| 	table.Render() | ||||
| 		return nil | ||||
| 	}, | ||||
| 	BashComplete: func(c *cli.Context) { | ||||
| 		appNames, err := config.GetAppNames() | ||||
| 		if err != nil { | ||||
| 			logrus.Warn(err) | ||||
| 		} | ||||
| 		if c.NArg() > 0 { | ||||
| 			return | ||||
| 		} | ||||
| 		for _, a := range appNames { | ||||
| 			fmt.Println(a) | ||||
| 		} | ||||
| 	}, | ||||
| } | ||||
|  | ||||
| @ -63,7 +63,7 @@ var appRemoveCommand = &cli.Command{ | ||||
| 			if err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 			if statuses[app.Name] == "deployed" { | ||||
| 			if statuses[app.Name]["status"] == "deployed" { | ||||
| 				logrus.Fatalf("'%s' is still deployed. Run \"abra app %s undeploy\" or pass --force", app.Name, app.Name) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| @ -3,14 +3,15 @@ package app | ||||
| import ( | ||||
| 	"fmt" | ||||
|  | ||||
| 	"context" | ||||
|  | ||||
| 	"coopcloud.tech/abra/pkg/catalogue" | ||||
| 	stack "coopcloud.tech/abra/pkg/client/stack" | ||||
| 	"coopcloud.tech/abra/pkg/config" | ||||
| 	"coopcloud.tech/abra/pkg/recipe" | ||||
| 	"coopcloud.tech/tagcmp" | ||||
|  | ||||
| 	"coopcloud.tech/abra/cli/internal" | ||||
| 	appPkg "coopcloud.tech/abra/pkg/app" | ||||
| 	"coopcloud.tech/abra/pkg/catalogue" | ||||
| 	"coopcloud.tech/abra/pkg/client" | ||||
| 	"github.com/AlecAivazis/survey/v2" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/urfave/cli/v2" | ||||
| ) | ||||
| @ -18,8 +19,21 @@ import ( | ||||
| var appRollbackCommand = &cli.Command{ | ||||
| 	Name:      "rollback", | ||||
| 	Usage:     "Roll an app back to a previous version", | ||||
| 	Aliases:   []string{"r"}, | ||||
| 	ArgsUsage: "[<version>]", | ||||
| 	Aliases:   []string{"r", "downgrade"}, | ||||
| 	ArgsUsage: "<app>", | ||||
| 	Flags: []cli.Flag{ | ||||
| 		internal.ForceFlag, | ||||
| 	}, | ||||
| 	Description: ` | ||||
| This command rolls an app back to a previous version if one exists. | ||||
|  | ||||
| You may pass "--force/-f" to downgrade to the same version again. This can be | ||||
| useful if the container runtime has gotten into a weird state or your doing | ||||
| some live hacking. | ||||
|  | ||||
| This action could be destructive, please ensure you have a copy of your app | ||||
| data beforehand - see "abra app backup <app>" for more. | ||||
| `, | ||||
| 	BashComplete: func(c *cli.Context) { | ||||
| 		appNames, err := config.GetAppNames() | ||||
| 		if err != nil { | ||||
| @ -34,48 +48,110 @@ var appRollbackCommand = &cli.Command{ | ||||
| 	}, | ||||
| 	Action: func(c *cli.Context) error { | ||||
| 		app := internal.ValidateApp(c) | ||||
| 		stackName := app.StackName() | ||||
|  | ||||
| 		ctx := context.Background() | ||||
| 		cl, err := client.New(app.Server) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		recipeMeta, err := catalogue.GetRecipeMeta(app.Type) | ||||
| 		logrus.Debugf("checking whether '%s' is already deployed", stackName) | ||||
|  | ||||
| 		isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName) | ||||
| 		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 deployedVersion == "" { | ||||
| 			logrus.Fatalf("failed to determine version of deployed '%s'", app.Name) | ||||
| 		} | ||||
|  | ||||
| 		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) | ||||
|  | ||||
| 		versions, err := catalogue.GetRecipeCatalogueVersions(app.Type) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		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) | ||||
| 		var availableDowngrades []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) | ||||
| 			} | ||||
| 			if parsedVersion != parsedDeployedVersion && parsedVersion.IsLessThan(parsedDeployedVersion) { | ||||
| 				availableDowngrades = append(availableDowngrades, version) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// TODO | ||||
| 		// display table of existing state and expected state and prompt | ||||
| 		// run the deployment with this target version! | ||||
| 		if len(availableDowngrades) == 0 { | ||||
| 			logrus.Fatal("no available downgrades, you're on latest") | ||||
| 		} | ||||
|  | ||||
| 		logrus.Fatal("command not implemented yet, coming soon TM") | ||||
| 		// FIXME: jeezus golang why do you not have a list reverse function | ||||
| 		for i, j := 0, len(availableDowngrades)-1; i < j; i, j = i+1, j-1 { | ||||
| 			availableDowngrades[i], availableDowngrades[j] = availableDowngrades[j], availableDowngrades[i] | ||||
| 		} | ||||
|  | ||||
| 		var chosenDowngrade string | ||||
| 		if !internal.Force { | ||||
| 			prompt := &survey.Select{ | ||||
| 				Message: fmt.Sprintf("Please select a downgrade (current version: '%s'):", deployedVersion), | ||||
| 				Options: availableDowngrades, | ||||
| 			} | ||||
| 			if err := survey.AskOne(prompt, &chosenDowngrade); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if internal.Force { | ||||
| 			chosenDowngrade = availableDowngrades[0] | ||||
| 			logrus.Debugf("choosing '%s' as version to downgrade to", chosenDowngrade) | ||||
| 		} | ||||
|  | ||||
| 		if err := recipe.EnsureVersion(app.Type, chosenDowngrade); err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		abraShPath := fmt.Sprintf("%s/%s/%s", config.APPS_DIR, app.Type, "abra.sh") | ||||
| 		abraShEnv, err := config.ReadAbraShEnvVars(abraShPath) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
| 		for k, v := range abraShEnv { | ||||
| 			app.Env[k] = v | ||||
| 		} | ||||
|  | ||||
| 		composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
| 		deployOpts := stack.Deploy{ | ||||
| 			Composefiles: composeFiles, | ||||
| 			Namespace:    stackName, | ||||
| 			Prune:        false, | ||||
| 			ResolveImage: stack.ResolveImageAlways, | ||||
| 		} | ||||
| 		compose, err := config.GetAppComposeConfig(app.Name, deployOpts, app.Env) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		if !internal.Force { | ||||
| 			if err := NewVersionOverview(app, deployedVersion, chosenDowngrade); err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if err := stack.RunDeploy(cl, deployOpts, compose); err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		return nil | ||||
| 	}, | ||||
|  | ||||
							
								
								
									
										157
									
								
								cli/app/upgrade.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								cli/app/upgrade.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,157 @@ | ||||
| package app | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
|  | ||||
| 	"coopcloud.tech/abra/cli/internal" | ||||
| 	"coopcloud.tech/abra/pkg/catalogue" | ||||
| 	"coopcloud.tech/abra/pkg/client" | ||||
| 	stack "coopcloud.tech/abra/pkg/client/stack" | ||||
| 	"coopcloud.tech/abra/pkg/config" | ||||
| 	"coopcloud.tech/abra/pkg/recipe" | ||||
| 	"coopcloud.tech/tagcmp" | ||||
| 	"github.com/AlecAivazis/survey/v2" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/urfave/cli/v2" | ||||
| ) | ||||
|  | ||||
| var appUpgradeCommand = &cli.Command{ | ||||
| 	Name:      "upgrade", | ||||
| 	Aliases:   []string{"u"}, | ||||
| 	Usage:     "Upgrade an app", | ||||
| 	ArgsUsage: "<app>", | ||||
| 	Flags: []cli.Flag{ | ||||
| 		internal.ForceFlag, | ||||
| 	}, | ||||
| 	Description: ` | ||||
| This command supports upgrading an app. You can use it to choose and roll out a | ||||
| new upgrade to an existing app. | ||||
|  | ||||
| This command specifically supports changing the version of running apps, as | ||||
| opposed to "abra app deploy <app>" which will not change the version of a | ||||
| deployed app. | ||||
|  | ||||
| You may pass "--force/-f" to upgrade to the same version again. This can be | ||||
| useful if the container runtime has gotten into a weird state or your doing | ||||
| some live hacking. | ||||
|  | ||||
| This action could be destructive, please ensure you have a copy of your app | ||||
| data beforehand - see "abra app backup <app>" for more. | ||||
| `, | ||||
| 	Action: func(c *cli.Context) error { | ||||
| 		app := internal.ValidateApp(c) | ||||
| 		stackName := app.StackName() | ||||
|  | ||||
| 		cl, err := client.New(app.Server) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		logrus.Debugf("checking whether '%s' is already deployed", stackName) | ||||
|  | ||||
| 		isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		if deployedVersion == "" { | ||||
| 			logrus.Fatalf("failed to determine version of deployed '%s'", app.Name) | ||||
| 		} | ||||
|  | ||||
| 		if !isDeployed { | ||||
| 			logrus.Fatalf("'%s' is not deployed?", app.Name) | ||||
| 		} | ||||
|  | ||||
| 		versions, err := catalogue.GetRecipeCatalogueVersions(app.Type) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		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) | ||||
| 			} | ||||
| 			if parsedVersion.IsGreaterThan(parsedDeployedVersion) { | ||||
| 				availableUpgrades = append(availableUpgrades, version) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if len(availableUpgrades) == 0 { | ||||
| 			logrus.Fatal("no available upgrades, you're on latest") | ||||
| 		} | ||||
|  | ||||
| 		var chosenUpgrade string | ||||
| 		if !internal.Force { | ||||
| 			prompt := &survey.Select{ | ||||
| 				Message: fmt.Sprintf("Please select an upgrade (current version: '%s'):", deployedVersion), | ||||
| 				Options: availableUpgrades, | ||||
| 			} | ||||
| 			if err := survey.AskOne(prompt, &chosenUpgrade); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if internal.Force { | ||||
| 			chosenUpgrade = availableUpgrades[len(availableUpgrades)-1] | ||||
| 			logrus.Debugf("choosing '%s' as version to upgrade to", chosenUpgrade) | ||||
| 		} | ||||
|  | ||||
| 		if err := recipe.EnsureVersion(app.Type, chosenUpgrade); err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		abraShPath := fmt.Sprintf("%s/%s/%s", config.APPS_DIR, app.Type, "abra.sh") | ||||
| 		abraShEnv, err := config.ReadAbraShEnvVars(abraShPath) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
| 		for k, v := range abraShEnv { | ||||
| 			app.Env[k] = v | ||||
| 		} | ||||
|  | ||||
| 		composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
| 		deployOpts := stack.Deploy{ | ||||
| 			Composefiles: composeFiles, | ||||
| 			Namespace:    stackName, | ||||
| 			Prune:        false, | ||||
| 			ResolveImage: stack.ResolveImageAlways, | ||||
| 		} | ||||
| 		compose, err := config.GetAppComposeConfig(app.Name, deployOpts, app.Env) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		if !internal.Force { | ||||
| 			if err := NewVersionOverview(app, deployedVersion, chosenUpgrade); err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if err := stack.RunDeploy(cl, deployOpts, compose); err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		return nil | ||||
| 	}, | ||||
| 	BashComplete: func(c *cli.Context) { | ||||
| 		appNames, err := config.GetAppNames() | ||||
| 		if err != nil { | ||||
| 			logrus.Warn(err) | ||||
| 		} | ||||
| 		if c.NArg() > 0 { | ||||
| 			return | ||||
| 		} | ||||
| 		for _, a := range appNames { | ||||
| 			fmt.Println(a) | ||||
| 		} | ||||
| 	}, | ||||
| } | ||||
| @ -2,12 +2,12 @@ package app | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"sort" | ||||
| 	"strings" | ||||
|  | ||||
| 	abraFormatter "coopcloud.tech/abra/cli/formatter" | ||||
| 	"coopcloud.tech/abra/cli/internal" | ||||
| 	appPkg "coopcloud.tech/abra/pkg/app" | ||||
| 	"coopcloud.tech/abra/pkg/catalogue" | ||||
| 	"coopcloud.tech/abra/pkg/client" | ||||
| 	"coopcloud.tech/abra/pkg/client/stack" | ||||
| 	"coopcloud.tech/abra/pkg/config" | ||||
| 	"github.com/docker/distribution/reference" | ||||
| @ -32,65 +32,61 @@ func getImagePath(image string) (string, error) { | ||||
| var appVersionCommand = &cli.Command{ | ||||
| 	Name:    "version", | ||||
| 	Aliases: []string{"v"}, | ||||
| 	Usage:   "Show version of all services in app", | ||||
| 	Usage:   "Show app versions", | ||||
| 	Description: ` | ||||
| This command shows all information about versioning related to a deployed app. | ||||
| This includes the individual image names, tags and digests. But also the Co-op | ||||
| Cloud recipe version. | ||||
| `, | ||||
| 	Action: func(c *cli.Context) error { | ||||
| 		app := internal.ValidateApp(c) | ||||
| 		stackName := app.StackName() | ||||
|  | ||||
| 		composeFiles, err := config.GetAppComposeFiles(app.Type, app.Env) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
| 		opts := stack.Deploy{Composefiles: composeFiles} | ||||
| 		compose, err := config.GetAppComposeConfig(app.Type, opts, app.Env) | ||||
| 		cl, err := client.New(app.Server) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		ch := make(chan stack.StackStatus, len(compose.Services)) | ||||
| 		for _, service := range compose.Services { | ||||
| 			label := fmt.Sprintf("coop-cloud.%s.%s.version", app.StackName(), service.Name) | ||||
| 			go func(s string, l string) { | ||||
| 				ch <- stack.GetDeployedServicesByLabel(s, l) | ||||
| 			}(app.Server, label) | ||||
| 		logrus.Debugf("checking whether '%s' is already deployed", stackName) | ||||
|  | ||||
| 		isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		tableCol := []string{"name", "image", "version", "digest"} | ||||
| 		if deployedVersion == "" { | ||||
| 			logrus.Fatalf("failed to determine version of deployed '%s'", app.Name) | ||||
| 		} | ||||
|  | ||||
| 		if !isDeployed { | ||||
| 			logrus.Fatalf("'%s' is not deployed?", app.Name) | ||||
| 		} | ||||
|  | ||||
| 		recipeMeta, err := catalogue.GetRecipeMeta(app.Type) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		versionsMeta := make(map[string]catalogue.ServiceMeta) | ||||
| 		for _, recipeVersion := range recipeMeta.Versions { | ||||
| 			if currentVersion, exists := recipeVersion[deployedVersion]; exists { | ||||
| 				versionsMeta = currentVersion | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if len(versionsMeta) == 0 { | ||||
| 			logrus.Fatalf("PANIC: could not retrieve deployed version ('%s') from recipe catalogue?", deployedVersion) | ||||
| 		} | ||||
|  | ||||
| 		tableCol := []string{"name", "image", "version", "tag", "digest"} | ||||
| 		table := abraFormatter.CreateTable(tableCol) | ||||
|  | ||||
| 		statuses := make(map[string]stack.StackStatus) | ||||
| 		for range compose.Services { | ||||
| 			status := <-ch | ||||
| 			if len(status.Services) > 0 { | ||||
| 				serviceName := appPkg.ParseServiceName(status.Services[0].Spec.Name) | ||||
| 				statuses[serviceName] = status | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		sort.SliceStable(compose.Services, func(i, j int) bool { | ||||
| 			return compose.Services[i].Name < compose.Services[j].Name | ||||
| 		}) | ||||
|  | ||||
| 		for _, service := range compose.Services { | ||||
| 			if status, ok := statuses[service.Name]; ok { | ||||
| 				statusService := status.Services[0] | ||||
| 				label := fmt.Sprintf("coop-cloud.%s.%s.version", app.StackName(), service.Name) | ||||
| 				version, digest := appPkg.ParseVersionLabel(statusService.Spec.Labels[label]) | ||||
| 				image, err := getImagePath(statusService.Spec.Labels["com.docker.stack.image"]) | ||||
| 				if err != nil { | ||||
| 					logrus.Fatal(err) | ||||
| 				} | ||||
| 				table.Append([]string{service.Name, image, version, digest}) | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			image, err := getImagePath(service.Image) | ||||
| 			if err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 			table.Append([]string{service.Name, image, "?", "?"}) | ||||
| 		for serviceName, versionMeta := range versionsMeta { | ||||
| 			table.Append([]string{serviceName, versionMeta.Image, deployedVersion, versionMeta.Tag, versionMeta.Digest}) | ||||
| 		} | ||||
|  | ||||
| 		table.Render() | ||||
|  | ||||
| 		return nil | ||||
| 	}, | ||||
| 	BashComplete: func(c *cli.Context) { | ||||
|  | ||||
| @ -96,7 +96,7 @@ var appVolumeRemoveCommand = &cli.Command{ | ||||
|  | ||||
| var appVolumeCommand = &cli.Command{ | ||||
| 	Name:      "volume", | ||||
| 	Aliases:   []string{"v"}, | ||||
| 	Aliases:   []string{"vl"}, | ||||
| 	Usage:     "Manage app volumes", | ||||
| 	ArgsUsage: "<command>", | ||||
| 	Subcommands: []*cli.Command{ | ||||
|  | ||||
| @ -2,7 +2,9 @@ package catalogue | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"path" | ||||
|  | ||||
| 	"coopcloud.tech/abra/cli/formatter" | ||||
| @ -49,7 +51,18 @@ var catalogueGenerateCommand = &cli.Command{ | ||||
| 	Aliases:   []string{"g"}, | ||||
| 	Usage:     "Generate a new copy of the catalogue", | ||||
| 	ArgsUsage: "[<recipe>]", | ||||
| 	BashComplete: func(c *cli.Context) {}, | ||||
| 	BashComplete: func(c *cli.Context) { | ||||
| 		catl, err := catalogue.ReadRecipeCatalogue() | ||||
| 		if err != nil { | ||||
| 			logrus.Warn(err) | ||||
| 		} | ||||
| 		if c.NArg() > 0 { | ||||
| 			return | ||||
| 		} | ||||
| 		for name := range catl { | ||||
| 			fmt.Println(name) | ||||
| 		} | ||||
| 	}, | ||||
| 	Action: func(c *cli.Context) error { | ||||
| 		recipeName := c.Args().First() | ||||
|  | ||||
| @ -60,18 +73,18 @@ var catalogueGenerateCommand = &cli.Command{ | ||||
|  | ||||
| 		logrus.Debugf("ensuring '%v' recipe(s) are locally present and up-to-date", len(repos)) | ||||
|  | ||||
| 		bar := formatter.CreateProgressbar(len(repos), "retrieving recipes...") | ||||
| 		retrieveBar := formatter.CreateProgressbar(len(repos), "retrieving recipes...") | ||||
| 		ch := make(chan string, len(repos)) | ||||
| 		for _, repoMeta := range repos { | ||||
| 			go func(rm catalogue.RepoMeta) { | ||||
| 				if recipeName != "" && recipeName != rm.Name { | ||||
| 					ch <- rm.Name | ||||
| 					bar.Add(1) | ||||
| 					retrieveBar.Add(1) | ||||
| 					return | ||||
| 				} | ||||
| 				if _, exists := CatalogueSkipList[rm.Name]; exists { | ||||
| 					ch <- rm.Name | ||||
| 					bar.Add(1) | ||||
| 					retrieveBar.Add(1) | ||||
| 					return | ||||
| 				} | ||||
|  | ||||
| @ -86,7 +99,7 @@ var catalogueGenerateCommand = &cli.Command{ | ||||
| 				} | ||||
|  | ||||
| 				ch <- rm.Name | ||||
| 				bar.Add(1) | ||||
| 				retrieveBar.Add(1) | ||||
| 			}(repoMeta) | ||||
| 		} | ||||
|  | ||||
| @ -95,14 +108,23 @@ var catalogueGenerateCommand = &cli.Command{ | ||||
| 		} | ||||
|  | ||||
| 		catl := make(catalogue.RecipeCatalogue) | ||||
| 		catlBar := formatter.CreateProgressbar(len(repos), "generating catalogue...") | ||||
| 		for _, recipeMeta := range repos { | ||||
| 			if recipeName != "" && recipeName != recipeMeta.Name { | ||||
| 				catlBar.Add(1) | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			if _, exists := CatalogueSkipList[recipeMeta.Name]; exists { | ||||
| 				catlBar.Add(1) | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			versions, err := catalogue.GetRecipeVersions(recipeMeta.Name) | ||||
| 			if err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
|  | ||||
| 			catl[recipeMeta.Name] = catalogue.RecipeMeta{ | ||||
| 				Name:          recipeMeta.Name, | ||||
| 				Repository:    recipeMeta.CloneURL, | ||||
| @ -110,10 +132,11 @@ var catalogueGenerateCommand = &cli.Command{ | ||||
| 				DefaultBranch: recipeMeta.DefaultBranch, | ||||
| 				Description:   recipeMeta.Description, | ||||
| 				Website:       recipeMeta.Website, | ||||
| 				// Versions:      ..., // FIXME: once the new versions work goes down | ||||
| 				Versions:      versions, | ||||
| 				// Category:      ..., // FIXME: once we sort out the machine-readable catalogue interface | ||||
| 				// Features:      ..., // FIXME: once we figure out the machine-readable catalogue interface | ||||
| 			} | ||||
| 			catlBar.Add(1) | ||||
| 		} | ||||
|  | ||||
| 		recipesJSON, err := json.MarshalIndent(catl, "", "    ") | ||||
| @ -121,11 +144,29 @@ var catalogueGenerateCommand = &cli.Command{ | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		if _, err := os.Stat(config.APPS_JSON); err != nil && os.IsNotExist(err) { | ||||
| 			if err := ioutil.WriteFile(config.APPS_JSON, recipesJSON, 0644); err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 		} else { | ||||
| 			if recipeName != "" { | ||||
| 				catlFS, err := catalogue.ReadRecipeCatalogue() | ||||
| 				if err != nil { | ||||
| 					logrus.Fatal(err) | ||||
| 				} | ||||
| 				catlFS[recipeName] = catl[recipeName] | ||||
|  | ||||
| 		logrus.Debugf("generated new recipe catalogue in '%s'", config.APPS_JSON) | ||||
| 				updatedRecipesJSON, err := json.MarshalIndent(catlFS, "", "    ") | ||||
| 				if err != nil { | ||||
| 					logrus.Fatal(err) | ||||
| 				} | ||||
| 				if err := ioutil.WriteFile(config.APPS_JSON, updatedRecipesJSON, 0644); err != nil { | ||||
| 					logrus.Fatal(err) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		logrus.Infof("generated new recipe catalogue in '%s'", config.APPS_JSON) | ||||
|  | ||||
| 		return nil | ||||
| 	}, | ||||
|  | ||||
							
								
								
									
										25
									
								
								cli/cli.go
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								cli/cli.go
									
									
									
									
									
								
							| @ -4,11 +4,13 @@ package cli | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"path" | ||||
|  | ||||
| 	"coopcloud.tech/abra/cli/app" | ||||
| 	"coopcloud.tech/abra/cli/catalogue" | ||||
| 	"coopcloud.tech/abra/cli/recipe" | ||||
| 	"coopcloud.tech/abra/cli/server" | ||||
| 	"coopcloud.tech/abra/pkg/config" | ||||
| 	logrusStack "github.com/Gurpartap/logrus-stack" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/urfave/cli/v2" | ||||
| @ -81,10 +83,29 @@ func RunApp(version, commit string) { | ||||
| 			logrus.SetOutput(os.Stderr) | ||||
| 			logrus.AddHook(logrusStack.StandardHook()) | ||||
| 		} | ||||
| 		return nil | ||||
|  | ||||
| 		paths := []string{ | ||||
| 			config.ABRA_DIR, | ||||
| 			path.Join(config.ABRA_DIR, "servers"), | ||||
| 			path.Join(config.ABRA_DIR, "apps"), | ||||
| 			path.Join(config.ABRA_DIR, "vendor"), | ||||
| 		} | ||||
|  | ||||
| 	logrus.Debugf("Flying abra version '%s', commit '%s', enjoy the ride", version, commit) | ||||
| 		for _, path := range paths { | ||||
| 			if err := os.Mkdir(path, 0755); err != nil { | ||||
| 				if !os.IsExist(err) { | ||||
| 					logrus.Fatal(err) | ||||
| 				} | ||||
| 				logrus.Debugf("'%s' already created, moving on...", path) | ||||
| 				continue | ||||
| 			} | ||||
| 			logrus.Debugf("'%s' is missing, creating...", path) | ||||
| 		} | ||||
|  | ||||
| 		logrus.Debugf("abra version '%s', commit '%s'", version, commit) | ||||
|  | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	if err := app.Run(os.Args); err != nil { | ||||
| 		logrus.Fatal(err) | ||||
|  | ||||
| @ -41,6 +41,10 @@ func ValidateApp(c *cli.Context) config.App { | ||||
| 		logrus.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	if err := recipe.EnsureExists(app.Type); err != nil { | ||||
| 		logrus.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	logrus.Debugf("validated '%s' as app argument", appName) | ||||
|  | ||||
| 	return app | ||||
|  | ||||
| @ -4,6 +4,30 @@ import ( | ||||
| 	"github.com/urfave/cli/v2" | ||||
| ) | ||||
|  | ||||
| var Major bool | ||||
| var MajorFlag = &cli.BoolFlag{ | ||||
| 	Name:        "major", | ||||
| 	Value:       false, | ||||
| 	Aliases:     []string{"ma", "x"}, | ||||
| 	Destination: &Major, | ||||
| } | ||||
|  | ||||
| var Minor bool | ||||
| var MinorFlag = &cli.BoolFlag{ | ||||
| 	Name:        "minor", | ||||
| 	Value:       false, | ||||
| 	Aliases:     []string{"mi", "y"}, | ||||
| 	Destination: &Minor, | ||||
| } | ||||
|  | ||||
| var Patch bool | ||||
| var PatchFlag = &cli.BoolFlag{ | ||||
| 	Name:        "patch", | ||||
| 	Value:       false, | ||||
| 	Aliases:     []string{"p", "z"}, | ||||
| 	Destination: &Patch, | ||||
| } | ||||
|  | ||||
| // RecipeCommand defines all recipe related sub-commands. | ||||
| var RecipeCommand = &cli.Command{ | ||||
| 	Name:      "recipe", | ||||
|  | ||||
| @ -9,11 +9,11 @@ import ( | ||||
| 	"coopcloud.tech/abra/cli/internal" | ||||
| 	"coopcloud.tech/abra/pkg/config" | ||||
| 	"coopcloud.tech/abra/pkg/recipe" | ||||
| 	recipePkg "coopcloud.tech/abra/pkg/recipe" | ||||
| 	"coopcloud.tech/tagcmp" | ||||
| 	"github.com/AlecAivazis/survey/v2" | ||||
| 	"github.com/go-git/go-git/v5" | ||||
| 	"github.com/go-git/go-git/v5/plumbing" | ||||
| 	"github.com/go-git/go-git/v5/plumbing/object" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/urfave/cli/v2" | ||||
| ) | ||||
| @ -33,34 +33,10 @@ var DryFlag = &cli.BoolFlag{ | ||||
| 	Destination: &Dry, | ||||
| } | ||||
|  | ||||
| var Major bool | ||||
| var MajorFlag = &cli.BoolFlag{ | ||||
| 	Name:        "major", | ||||
| 	Value:       false, | ||||
| 	Aliases:     []string{"ma", "x"}, | ||||
| 	Destination: &Major, | ||||
| } | ||||
|  | ||||
| var Minor bool | ||||
| var MinorFlag = &cli.BoolFlag{ | ||||
| 	Name:        "minor", | ||||
| 	Value:       false, | ||||
| 	Aliases:     []string{"mi", "y"}, | ||||
| 	Destination: &Minor, | ||||
| } | ||||
|  | ||||
| var Patch bool | ||||
| var PatchFlag = &cli.BoolFlag{ | ||||
| 	Name:        "patch", | ||||
| 	Value:       false, | ||||
| 	Aliases:     []string{"p", "z"}, | ||||
| 	Destination: &Patch, | ||||
| } | ||||
|  | ||||
| var CommitMessage string | ||||
| var CommitMessageFlag = &cli.StringFlag{ | ||||
| 	Name:        "commit-message", | ||||
| 	Usage:       "commit message", | ||||
| 	Usage:       "commit message. Implies --commit", | ||||
| 	Aliases:     []string{"cm"}, | ||||
| 	Destination: &CommitMessage, | ||||
| } | ||||
| @ -68,11 +44,20 @@ var CommitMessageFlag = &cli.StringFlag{ | ||||
| var Commit bool | ||||
| var CommitFlag = &cli.BoolFlag{ | ||||
| 	Name:        "commit", | ||||
| 	Usage:       "add compose.yml to staging area and commit changes", | ||||
| 	Value:       false, | ||||
| 	Aliases:     []string{"c"}, | ||||
| 	Destination: &Commit, | ||||
| } | ||||
|  | ||||
| var TagMessage string | ||||
| var TagMessageFlag = &cli.StringFlag{ | ||||
| 	Name:        "tag-comment", | ||||
| 	Usage:       "tag comment. If not given, user will be asked for it", | ||||
| 	Aliases:     []string{"t", "tm"}, | ||||
| 	Destination: &TagMessage, | ||||
| } | ||||
|  | ||||
| var recipeReleaseCommand = &cli.Command{ | ||||
| 	Name:      "release", | ||||
| 	Usage:     "tag a recipe", | ||||
| @ -107,6 +92,7 @@ or a rollback of an app. | ||||
| 		PushFlag, | ||||
| 		CommitFlag, | ||||
| 		CommitMessageFlag, | ||||
| 		TagMessageFlag, | ||||
| 	}, | ||||
| 	Action: func(c *cli.Context) error { | ||||
| 		recipe := internal.ValidateRecipe(c) | ||||
| @ -115,17 +101,31 @@ or a rollback of an app. | ||||
| 		imagesTmp := getImageVersions(recipe) | ||||
| 		mainApp := getMainApp(recipe) | ||||
| 		mainAppVersion := imagesTmp[mainApp] | ||||
|  | ||||
| 		if err := recipePkg.EnsureExists(recipe.Name); err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		if mainAppVersion == "" { | ||||
| 			logrus.Fatal("main app version is empty?") | ||||
| 		} | ||||
|  | ||||
| 		if tagstring != "" { | ||||
| 			_, err := tagcmp.Parse(tagstring) | ||||
| 			if err != nil { | ||||
| 			if _, err := tagcmp.Parse(tagstring); err != nil { | ||||
| 				logrus.Fatal("invalid tag specified") | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if TagMessage == "" { | ||||
| 			prompt := &survey.Input{ | ||||
| 				Message: "tag message", | ||||
| 			} | ||||
| 			survey.AskOne(prompt, &TagMessage) | ||||
| 		} | ||||
|  | ||||
| 		var createTagOptions git.CreateTagOptions | ||||
| 		createTagOptions.Message = TagMessage | ||||
|  | ||||
| 		if Commit || (CommitMessage != "") { | ||||
| 			commitRepo, err := git.PlainOpen(directory) | ||||
| 			if err != nil { | ||||
| @ -142,6 +142,12 @@ or a rollback of an app. | ||||
| 				} | ||||
| 				survey.AskOne(prompt, &CommitMessage) | ||||
| 			} | ||||
| 			err = commitWorktree.AddGlob("compose.**yml") | ||||
| 			if err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 			logrus.Debug("staged compose.**yml for commit") | ||||
|  | ||||
| 			_, err = commitWorktree.Commit(CommitMessage, &git.CommitOptions{}) | ||||
| 			if err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| @ -189,9 +195,7 @@ or a rollback of an app. | ||||
| 				return nil | ||||
| 			} | ||||
|  | ||||
| 			repo.CreateTag(tagstring, head.Hash(), nil) /* &git.CreateTagOptions{ | ||||
| 				Message: tag, | ||||
| 			})*/ | ||||
| 			repo.CreateTag(tagstring, head.Hash(), &createTagOptions) | ||||
| 			logrus.Info(fmt.Sprintf("created tag %s at %s", tagstring, head.Hash())) | ||||
| 			if Push { | ||||
| 				if err := repo.Push(&git.PushOptions{}); err != nil { | ||||
| @ -204,27 +208,33 @@ or a rollback of an app. | ||||
| 		} | ||||
|  | ||||
| 		// get the latest tag with its hash, name etc | ||||
| 		var lastGitTag *object.Tag | ||||
| 		var lastGitTag tagcmp.Tag | ||||
| 		iter, err := repo.Tags() | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
| 		if err := iter.ForEach(func(ref *plumbing.Reference) error { | ||||
| 			obj, err := repo.TagObject(ref.Hash()) | ||||
| 			if err == nil { | ||||
| 				lastGitTag = obj | ||||
| 				return nil | ||||
| 			} | ||||
| 			if err != nil { | ||||
| 				return err | ||||
|  | ||||
| 			} | ||||
| 			tagcmpTag, err := tagcmp.Parse(obj.Name) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			if (lastGitTag == tagcmp.Tag{}) { | ||||
| 				lastGitTag = tagcmpTag | ||||
| 			} else if tagcmpTag.IsGreaterThan(lastGitTag) { | ||||
| 				lastGitTag = tagcmpTag | ||||
| 			} | ||||
| 			return nil | ||||
| 		}); err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
| 		newTag, err := tagcmp.Parse(lastGitTag.Name) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		fmt.Println(lastGitTag) | ||||
|  | ||||
| 		newTag := lastGitTag | ||||
| 		var newTagString string | ||||
| 		if bumpType > 0 { | ||||
| 			if Patch { | ||||
| @ -238,28 +248,28 @@ or a rollback of an app. | ||||
| 				if err != nil { | ||||
| 					logrus.Fatal(err) | ||||
| 				} | ||||
| 				newTag.Patch = "0" | ||||
| 				newTag.Minor = strconv.Itoa(now + 1) | ||||
| 			} else if Major { | ||||
| 				now, err := strconv.Atoi(newTag.Major) | ||||
| 				if err != nil { | ||||
| 					logrus.Fatal(err) | ||||
| 				} | ||||
| 				newTag.Patch = "0" | ||||
| 				newTag.Minor = "0" | ||||
| 				newTag.Major = strconv.Itoa(now + 1) | ||||
| 			} | ||||
| 			newTagString = newTag.String() | ||||
| 		} else { | ||||
| 			logrus.Fatal("we don't support automatic tag generation yet - specify a version or use one of: --major --minor --patch") | ||||
| 		} | ||||
|  | ||||
| 		newTagString = fmt.Sprintf("%s+%s", newTagString, mainAppVersion) | ||||
| 		newTag.Metadata = mainAppVersion | ||||
| 		newTagString = newTag.String() | ||||
| 		if Dry { | ||||
| 			logrus.Info(fmt.Sprintf("dry run only: NOT creating tag %s at %s", newTagString, head.Hash())) | ||||
| 			return nil | ||||
| 		} | ||||
|  | ||||
| 		repo.CreateTag(newTagString, head.Hash(), nil) /* &git.CreateTagOptions{ | ||||
| 			Message: tag, | ||||
| 		})*/ | ||||
| 		repo.CreateTag(newTagString, head.Hash(), &createTagOptions) | ||||
| 		logrus.Info(fmt.Sprintf("created tag %s at %s", newTagString, head.Hash())) | ||||
| 		if Push { | ||||
| 			if err := repo.Push(&git.PushOptions{}); err != nil { | ||||
|  | ||||
| @ -1,64 +1,87 @@ | ||||
| package recipe | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
|  | ||||
| 	"coopcloud.tech/abra/cli/internal" | ||||
| 	"coopcloud.tech/abra/pkg/client" | ||||
| 	"github.com/docker/distribution/reference" | ||||
| 	"coopcloud.tech/abra/pkg/catalogue" | ||||
| 	"github.com/AlecAivazis/survey/v2" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/urfave/cli/v2" | ||||
| ) | ||||
|  | ||||
| var recipeSyncCommand = &cli.Command{ | ||||
| 	Name:      "sync", | ||||
| 	Usage:   "Generate new recipe labels", | ||||
| 	Usage:     "Ensure recipe version labels are up-to-date", | ||||
| 	Aliases:   []string{"s"}, | ||||
| 	ArgsUsage: "<recipe> <version>", | ||||
| 	Description: ` | ||||
| This command will generate labels for each service which correspond to the | ||||
| This command will generate labels for the main recipe service (i.e. by | ||||
| convention, typically the service named "app") which corresponds to the | ||||
| following format: | ||||
|  | ||||
|     coop-cloud.${STACK_NAME}.${SERVICE_NAME}.version=${IMAGE_TAG}-${IMAGE_DIGEST} | ||||
|     coop-cloud.${STACK_NAME}.version=<version> | ||||
|  | ||||
| The <recipe> configuration will be updated on the local file system. These | ||||
| labels are consumed by abra in other command invocations and used to determine | ||||
| the versioning metadata of up-and-running containers are. | ||||
| The <version> is determined by the recipe maintainer and is specified on the | ||||
| command-line. The <recipe> configuration will be updated on the local file | ||||
| system. | ||||
| `, | ||||
| 	ArgsUsage: "<recipe>", | ||||
| 	BashComplete: func(c *cli.Context) { | ||||
| 		catl, err := catalogue.ReadRecipeCatalogue() | ||||
| 		if err != nil { | ||||
| 			logrus.Warn(err) | ||||
| 		} | ||||
| 		if c.NArg() > 0 { | ||||
| 			return | ||||
| 		} | ||||
| 		for name := range catl { | ||||
| 			fmt.Println(name) | ||||
| 		} | ||||
| 	}, | ||||
| 	Action: func(c *cli.Context) error { | ||||
| 		if c.Args().Len() != 2 { | ||||
| 			internal.ShowSubcommandHelpAndError(c, errors.New("missing <recipe>/<version> arguments?")) | ||||
| 		} | ||||
|  | ||||
| 		recipe := internal.ValidateRecipe(c) | ||||
|  | ||||
| 		// TODO: validate with tagcmp when new commits come in | ||||
| 		// See https://git.coopcloud.tech/coop-cloud/abra/pulls/109 | ||||
| 		nextTag := c.Args().Get(1) | ||||
|  | ||||
| 		mainService := "app" | ||||
| 		var services []string | ||||
| 		hasAppService := false | ||||
| 		for _, service := range recipe.Config.Services { | ||||
| 			services = append(services, service.Name) | ||||
| 			if service.Name == "app" { | ||||
| 				hasAppService = true | ||||
| 				logrus.Debugf("detected app service in '%s'", recipe.Name) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if !hasAppService { | ||||
| 			logrus.Fatal(fmt.Sprintf("no 'app' service defined in '%s'", recipe.Name)) | ||||
| 			logrus.Warnf("no 'app' service defined in '%s'", recipe.Name) | ||||
| 			var chosenService string | ||||
| 			prompt := &survey.Select{ | ||||
| 				Message: fmt.Sprintf("what is the main service name for '%s'?", recipe.Name), | ||||
| 				Options: services, | ||||
| 			} | ||||
|  | ||||
| 		for _, service := range recipe.Config.Services { | ||||
| 			img, err := reference.ParseNormalizedNamed(service.Image) | ||||
| 			if err != nil { | ||||
| 			if err := survey.AskOne(prompt, &chosenService); err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 			logrus.Debugf("detected image '%s' for service '%s'", img, service.Name) | ||||
| 			mainService = chosenService | ||||
| 		} | ||||
|  | ||||
| 			digest, err := client.GetTagDigest(img) | ||||
| 			if err != nil { | ||||
| 		logrus.Debugf("selecting '%s' as the service to sync version labels", mainService) | ||||
|  | ||||
| 		label := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", nextTag) | ||||
| 		if err := recipe.UpdateLabel(mainService, label); err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
| 			logrus.Debugf("retrieved digest '%s' for '%s'", digest, img) | ||||
|  | ||||
| 			tag := img.(reference.NamedTagged).Tag() | ||||
| 			label := fmt.Sprintf("coop-cloud.${STACK_NAME}.%s.version=%s-%s", service.Name, tag, digest) | ||||
| 			if err := recipe.UpdateLabel(service.Name, label); err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 			logrus.Debugf("added label '%s' to service '%s'", label, service.Name) | ||||
| 		} | ||||
| 		logrus.Infof("synced label '%s' to service '%s'", label, mainService) | ||||
|  | ||||
| 		return nil | ||||
| 	}, | ||||
|  | ||||
| @ -27,14 +27,24 @@ update the relevant compose file tags on the local file system. | ||||
| Some image tags cannot be parsed because they do not follow some sort of | ||||
| semver-like convention. In this case, all possible tags will be listed and it | ||||
| is up to the end-user to decide. | ||||
|  | ||||
| This is step 1 of upgrading a recipe. Step 2 is running "abra recipe sync | ||||
| <recipe>". | ||||
| `, | ||||
| 	ArgsUsage: "<recipe>", | ||||
| 	Flags: []cli.Flag{ | ||||
| 		PatchFlag, | ||||
| 		MinorFlag, | ||||
| 		MajorFlag, | ||||
| 	}, | ||||
| 	Action: func(c *cli.Context) error { | ||||
| 		recipe := internal.ValidateRecipe(c) | ||||
|  | ||||
| 		bumpType := btoi(Major)*4 + btoi(Minor)*2 + btoi(Patch) | ||||
| 		if bumpType != 0 { | ||||
| 			// a bitwise check if the number is a power of 2 | ||||
| 			if (bumpType & (bumpType - 1)) != 0 { | ||||
| 				logrus.Fatal("you can only use one of: --major, --minor, --patch.") | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		for _, service := range recipe.Config.Services { | ||||
| 			catlVersions, err := catalogue.VersionsOfService(recipe.Name, service.Name) | ||||
| 			if err != nil { | ||||
| @ -107,7 +117,24 @@ This is step 1 of upgrading a recipe. Step 2 is running "abra recipe sync | ||||
| 			} | ||||
|  | ||||
| 			logrus.Debugf("detected compatible upgradable tags '%s' for '%s'", compatibleStrings, service.Name) | ||||
|  | ||||
| 			var upgradeTag string | ||||
| 			if bumpType != 0 { | ||||
| 				for _, upTag := range compatible { | ||||
| 					upElement, err := tag.UpgradeDelta(upTag) | ||||
| 					if err != nil { | ||||
| 						return err | ||||
| 					} | ||||
| 					delta := upElement.UpgradeType() | ||||
| 					if delta <= bumpType { | ||||
| 						upgradeTag = upTag.String() | ||||
| 						break | ||||
| 					} | ||||
| 				} | ||||
| 				if upgradeTag == "" { | ||||
| 					logrus.Warnf("not upgrading from '%s' to '%s' for '%s', because the upgrade type is more serious than what user wants.", tag.String(), compatible[0].String(), image) | ||||
| 					continue | ||||
| 				} | ||||
| 			} else { | ||||
| 				msg := fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag) | ||||
| 				if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) { | ||||
| 					tag := img.(reference.NamedTagged).Tag() | ||||
| @ -119,7 +146,6 @@ This is step 1 of upgrading a recipe. Step 2 is running "abra recipe sync | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 			var upgradeTag string | ||||
| 				prompt := &survey.Select{ | ||||
| 					Message: msg, | ||||
| 					Options: compatibleStrings, | ||||
| @ -127,11 +153,12 @@ This is step 1 of upgrading a recipe. Step 2 is running "abra recipe sync | ||||
| 				if err := survey.AskOne(prompt, &upgradeTag); err != nil { | ||||
| 					logrus.Fatal(err) | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if err := recipe.UpdateTag(image, upgradeTag); err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 			logrus.Debugf("tag updated from '%s' to '%s' for '%s'", image, upgradeTag, recipe.Name) | ||||
| 			logrus.Infof("tag upgraded from '%s' to '%s' for '%s'", tag.String(), upgradeTag, image) | ||||
| 		} | ||||
|  | ||||
| 		return nil | ||||
|  | ||||
| @ -3,14 +3,12 @@ package server | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"os" | ||||
| 	"os/user" | ||||
| 	"path" | ||||
| 	"strings" | ||||
|  | ||||
| 	"coopcloud.tech/abra/cli/internal" | ||||
| 	"coopcloud.tech/abra/pkg/client" | ||||
| 	"coopcloud.tech/abra/pkg/config" | ||||
| 	"coopcloud.tech/abra/pkg/server" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/urfave/cli/v2" | ||||
| ) | ||||
| @ -30,10 +28,14 @@ var serverAddCommand = &cli.Command{ | ||||
| 	Description: ` | ||||
| This command adds a new server that abra will communicate with, to deploy apps. | ||||
|  | ||||
| The <domain> argument must be a publicy accessible domain name which points to | ||||
| your server. You should have SSH access to this server, Abra will assume port | ||||
| 22 and will use your current system username to make an initial connection. You | ||||
| can use the <user> and <port> arguments to adjust this. | ||||
| If "--local" is passed, then Abra assumes that the current local server is | ||||
| intended as the target server. | ||||
|  | ||||
| Otherwise, you may specify a remote server. The <domain> argument must be a | ||||
| publicy accessible domain name which points to your server. You should have SSH | ||||
| access to this server, Abra will assume port 22 and will use your current | ||||
| system username to make an initial connection. You can use the <user> and | ||||
| <port> arguments to adjust this. | ||||
|  | ||||
| For example: | ||||
|  | ||||
| @ -44,9 +46,6 @@ Abra will construct the following SSH connection string then: | ||||
|     ssh://globemodem@varia.zone:12345 | ||||
|  | ||||
| All communication between Abra and the server will use this SSH connection. | ||||
|  | ||||
| NOTE: If you specify --local, none of the above applies 🙃 | ||||
|  | ||||
| `, | ||||
| 	Aliases: []string{"a"}, | ||||
| 	Flags: []cli.Flag{ | ||||
| @ -67,7 +66,10 @@ NOTE: If you specify --local, none of the above applies 🙃 | ||||
| 		domainName := "default" | ||||
|  | ||||
| 		if local { | ||||
| 			os.Mkdir(path.Join(config.ABRA_DIR, "servers", domainName), 0755) | ||||
| 			if err := server.CreateServerDir(domainName); err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 			logrus.Info("local server has been added") | ||||
| 			return nil | ||||
| 		} | ||||
|  | ||||
| @ -124,9 +126,12 @@ NOTE: If you specify --local, none of the above applies 🙃 | ||||
| 		} | ||||
|  | ||||
| 		logrus.Debugf("remote connection to '%s' is definitely up", domainName) | ||||
| 		logrus.Infof("server at '%s' has been added", domainName) | ||||
|  | ||||
| 		os.Mkdir(path.Join(config.ABRA_DIR, "servers", domainName), 0755) | ||||
| 		if err := server.CreateServerDir(domainName); err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		logrus.Infof("server at '%s' has been added", domainName) | ||||
|  | ||||
| 		return nil | ||||
| 	}, | ||||
|  | ||||
| @ -35,6 +35,9 @@ later for more advanced use cases. | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		// https://www.privacy-handbuch.de/handbuch_93d.htm | ||||
| 		freifunkDNS := "5.1.66.255:53" | ||||
|  | ||||
| 		resolver := &net.Resolver{ | ||||
| 			PreferGo: false, | ||||
| 			Dial: func(ctx context.Context, network, address string) (net.Conn, error) { | ||||
| @ -42,10 +45,10 @@ later for more advanced use cases. | ||||
| 					Timeout: time.Millisecond * time.Duration(10000), | ||||
| 				} | ||||
| 				// comrade librehosters DNS resolver https://snopyta.org/service/dns/ | ||||
| 				return d.DialContext(ctx, "udp", "95.216.24.230:53") | ||||
| 				return d.DialContext(ctx, "udp", freifunkDNS) | ||||
| 			}, | ||||
| 		} | ||||
| 		logrus.Debugf("created DNS resolver via 95.216.24.230") | ||||
| 		logrus.Debugf("created DNS resolver via '%s'", freifunkDNS) | ||||
|  | ||||
| 		ctx := context.Background() | ||||
| 		ips, err := resolver.LookupIPAddr(ctx, domainName) | ||||
|  | ||||
| @ -45,7 +45,11 @@ var serverListCommand = &cli.Command{ | ||||
| 				} | ||||
| 			} | ||||
| 			if len(row) == 0 { | ||||
| 				row = []string{serverName, "UNKNOWN"} | ||||
| 				if serverName == "default" { | ||||
| 					row = []string{serverName, "local"} | ||||
| 				} else { | ||||
| 					row = []string{serverName, "unknown"} | ||||
| 				} | ||||
| 			} | ||||
| 			table.Append(row) | ||||
| 		} | ||||
|  | ||||
							
								
								
									
										63
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										63
									
								
								go.mod
									
									
									
									
									
								
							| @ -1,9 +1,9 @@ | ||||
| module coopcloud.tech/abra | ||||
|  | ||||
| go 1.17 | ||||
| go 1.16 | ||||
|  | ||||
| require ( | ||||
| 	coopcloud.tech/tagcmp v0.0.0-20210906102006-2a8edd82d75d | ||||
| 	coopcloud.tech/tagcmp v0.0.0-20211011140827-4f27c74467eb | ||||
| 	github.com/AlecAivazis/survey/v2 v2.3.1 | ||||
| 	github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731170023-c37c0920d1a4 | ||||
| 	github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4 | ||||
| @ -25,75 +25,16 @@ require ( | ||||
| ) | ||||
|  | ||||
| require ( | ||||
| 	github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect | ||||
| 	github.com/Microsoft/go-winio v0.4.17 // indirect | ||||
| 	github.com/Microsoft/hcsshim v0.8.21 // indirect | ||||
| 	github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 // indirect | ||||
| 	github.com/acomagu/bufpipe v1.0.3 // indirect | ||||
| 	github.com/beorn7/perks v1.0.1 // indirect | ||||
| 	github.com/cespare/xxhash/v2 v2.1.1 // indirect | ||||
| 	github.com/containerd/cgroups v1.0.1 // indirect | ||||
| 	github.com/containerd/containerd v1.5.5 // indirect | ||||
| 	github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect | ||||
| 	github.com/docker/docker-credential-helpers v0.6.4 // indirect | ||||
| 	github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect | ||||
| 	github.com/docker/go-connections v0.4.0 // indirect | ||||
| 	github.com/docker/go-metrics v0.0.1 // indirect | ||||
| 	github.com/emirpasic/gods v1.12.0 // indirect | ||||
| 	github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect | ||||
| 	github.com/fvbommel/sortorder v1.0.2 // indirect | ||||
| 	github.com/go-git/gcfg v1.5.0 // indirect | ||||
| 	github.com/go-git/go-billy/v5 v5.3.1 // indirect | ||||
| 	github.com/gogo/protobuf v1.3.2 // indirect | ||||
| 	github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect | ||||
| 	github.com/golang/protobuf v1.5.0 // indirect | ||||
| 	github.com/google/go-cmp v0.5.5 // indirect | ||||
| 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect | ||||
| 	github.com/gorilla/mux v1.8.0 // indirect | ||||
| 	github.com/imdario/mergo v0.3.12 // indirect | ||||
| 	github.com/inconshreveable/mousetrap v1.0.0 // indirect | ||||
| 	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect | ||||
| 	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect | ||||
| 	github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect | ||||
| 	github.com/mattn/go-colorable v0.1.2 // indirect | ||||
| 	github.com/mattn/go-isatty v0.0.14 // indirect | ||||
| 	github.com/mattn/go-runewidth v0.0.13 // indirect | ||||
| 	github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect | ||||
| 	github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect | ||||
| 	github.com/miekg/pkcs11 v1.0.3 // indirect | ||||
| 	github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect | ||||
| 	github.com/mitchellh/go-homedir v1.1.0 // indirect | ||||
| 	github.com/mitchellh/mapstructure v1.1.2 // indirect | ||||
| 	github.com/moby/sys/mount v0.2.0 // indirect | ||||
| 	github.com/moby/sys/mountinfo v0.4.1 // indirect | ||||
| 	github.com/morikuni/aec v1.0.0 // indirect | ||||
| 	github.com/opencontainers/go-digest v1.0.0 // indirect | ||||
| 	github.com/opencontainers/image-spec v1.0.1 // indirect | ||||
| 	github.com/opencontainers/runc v1.0.2 // indirect | ||||
| 	github.com/prometheus/client_golang v1.11.0 // indirect | ||||
| 	github.com/prometheus/client_model v0.2.0 // indirect | ||||
| 	github.com/prometheus/common v0.26.0 // indirect | ||||
| 	github.com/prometheus/procfs v0.6.0 // indirect | ||||
| 	github.com/rivo/uniseg v0.2.0 // indirect | ||||
| 	github.com/russross/blackfriday/v2 v2.0.1 // indirect | ||||
| 	github.com/sergi/go-diff v1.1.0 // indirect | ||||
| 	github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect | ||||
| 	github.com/spf13/cobra v1.0.0 // indirect | ||||
| 	github.com/spf13/pflag v1.0.5 // indirect | ||||
| 	github.com/theupdateframework/notary v0.7.0 // indirect | ||||
| 	github.com/xanzy/ssh-agent v0.3.0 // indirect | ||||
| 	github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect | ||||
| 	github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect | ||||
| 	github.com/xeipuuv/gojsonschema v1.2.0 // indirect | ||||
| 	go.opencensus.io v0.22.3 // indirect | ||||
| 	golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect | ||||
| 	golang.org/x/net v0.0.0-20210326060303-6b1517762897 // indirect | ||||
| 	golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 // indirect | ||||
| 	golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect | ||||
| 	golang.org/x/text v0.3.4 // indirect | ||||
| 	google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a // indirect | ||||
| 	google.golang.org/grpc v1.33.2 // indirect | ||||
| 	google.golang.org/protobuf v1.26.0 // indirect | ||||
| 	gopkg.in/warnings.v0 v0.1.2 // indirect | ||||
| 	gopkg.in/yaml.v2 v2.4.0 // indirect | ||||
| ) | ||||
|  | ||||
							
								
								
									
										4
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.sum
									
									
									
									
									
								
							| @ -21,8 +21,8 @@ cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIA | ||||
| cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= | ||||
| cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= | ||||
| cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= | ||||
| coopcloud.tech/tagcmp v0.0.0-20210906102006-2a8edd82d75d h1:5jeUIiToqQ7vTlLeycdGp4Ezurd6/RTNl5K38usHtoo= | ||||
| coopcloud.tech/tagcmp v0.0.0-20210906102006-2a8edd82d75d/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ= | ||||
| coopcloud.tech/tagcmp v0.0.0-20211011140827-4f27c74467eb h1:Jf+Dnna2kXcNQvcA5JMp6d2Uyvg2pIVJfip9+X5FrH0= | ||||
| coopcloud.tech/tagcmp v0.0.0-20211011140827-4f27c74467eb/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ= | ||||
| dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= | ||||
| github.com/AlecAivazis/survey/v2 v2.3.1 h1:lzkuHA60pER7L4eYL8qQJor4bUWlJe4V0gqAT19tdOA= | ||||
| github.com/AlecAivazis/survey/v2 v2.3.1/go.mod h1:TH2kPCDU3Kqq7pLbnCWwZXDBjnhZtmsCle5EiYDJ2fg= | ||||
|  | ||||
| @ -9,12 +9,17 @@ import ( | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"path" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"coopcloud.tech/abra/pkg/client" | ||||
| 	"coopcloud.tech/abra/pkg/config" | ||||
| 	"coopcloud.tech/abra/pkg/recipe" | ||||
| 	"coopcloud.tech/abra/pkg/web" | ||||
| 	"github.com/docker/distribution/reference" | ||||
| 	"github.com/go-git/go-git/v5" | ||||
| 	"github.com/go-git/go-git/v5/plumbing" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| ) | ||||
|  | ||||
| @ -49,13 +54,16 @@ type tag = string | ||||
| // service represents a service within a recipe. | ||||
| type service = string | ||||
|  | ||||
| // serviceMeta represents meta info associated with a service. | ||||
| type serviceMeta struct { | ||||
| // ServiceMeta represents meta info associated with a service. | ||||
| type ServiceMeta struct { | ||||
| 	Digest string `json:"digest"` | ||||
| 	Image  string `json:"image"` | ||||
| 	Tag    string `json:"tag"` | ||||
| } | ||||
|  | ||||
| // RecipeVersions are the versions associated with a recipe. | ||||
| type RecipeVersions []map[tag]map[service]ServiceMeta | ||||
|  | ||||
| // RecipeMeta represents metadata for a recipe in the abra catalogue. | ||||
| type RecipeMeta struct { | ||||
| 	Category      string         `json:"category"` | ||||
| @ -65,7 +73,7 @@ type RecipeMeta struct { | ||||
| 	Icon          string         `json:"icon"` | ||||
| 	Name          string         `json:"name"` | ||||
| 	Repository    string         `json:"repository"` | ||||
| 	Versions      []map[tag]map[service]serviceMeta `json:"versions"` | ||||
| 	Versions      RecipeVersions `json:"versions"` | ||||
| 	Website       string         `json:"website"` | ||||
| } | ||||
|  | ||||
| @ -365,3 +373,128 @@ func ReadReposMetadata() (RepoCatalogue, error) { | ||||
|  | ||||
| 	return reposMeta, nil | ||||
| } | ||||
|  | ||||
| // GetRecipeVersions retrieves all recipe versions. | ||||
| func GetRecipeVersions(recipeName string) (RecipeVersions, error) { | ||||
| 	versions := RecipeVersions{} | ||||
|  | ||||
| 	recipeDir := path.Join(config.ABRA_DIR, "apps", recipeName) | ||||
|  | ||||
| 	logrus.Debugf("attempting to open git repository in '%s'", recipeDir) | ||||
|  | ||||
| 	repo, err := git.PlainOpen(recipeDir) | ||||
| 	if err != nil { | ||||
| 		return versions, err | ||||
| 	} | ||||
|  | ||||
| 	worktree, err := repo.Worktree() | ||||
| 	if err != nil { | ||||
| 		logrus.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	gitTags, err := repo.Tags() | ||||
| 	if err != nil { | ||||
| 		return versions, err | ||||
| 	} | ||||
|  | ||||
| 	if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) { | ||||
| 		tag := strings.TrimPrefix(string(ref.Name()), "refs/tags/") | ||||
|  | ||||
| 		logrus.Debugf("processing '%s' for '%s'", tag, recipeName) | ||||
|  | ||||
| 		checkOutOpts := &git.CheckoutOptions{ | ||||
| 			Create: false, | ||||
| 			Force:  true, | ||||
| 			Branch: plumbing.ReferenceName(ref.Name()), | ||||
| 		} | ||||
| 		if err := worktree.Checkout(checkOutOpts); err != nil { | ||||
| 			logrus.Debugf("failed to check out '%s' in '%s'", tag, recipeDir) | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		logrus.Debugf("successfully checked out '%s' in '%s'", ref.Name(), recipeDir) | ||||
|  | ||||
| 		recipe, err := recipe.Get(recipeName) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		versionMeta := make(map[string]ServiceMeta) | ||||
| 		for _, service := range recipe.Config.Services { | ||||
|  | ||||
| 			img, err := reference.ParseNormalizedNamed(service.Image) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			path := reference.Path(img) | ||||
| 			if strings.Contains(path, "library") { | ||||
| 				path = strings.Split(path, "/")[1] | ||||
| 			} | ||||
|  | ||||
| 			digest, err := client.GetTagDigest(img) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			versionMeta[service.Name] = ServiceMeta{ | ||||
| 				Digest: digest, | ||||
| 				Image:  path, | ||||
| 				Tag:    img.(reference.NamedTagged).Tag(), | ||||
| 			} | ||||
|  | ||||
| 			logrus.Debugf("collecting digest: '%s', image: '%s', tag: '%s'", digest, path, tag) | ||||
| 		} | ||||
|  | ||||
| 		versions = append(versions, map[string]map[string]ServiceMeta{tag: versionMeta}) | ||||
|  | ||||
| 		return nil | ||||
| 	}); err != nil { | ||||
| 		return versions, err | ||||
| 	} | ||||
|  | ||||
| 	branch := "master" | ||||
| 	if _, err := repo.Branch("master"); err != nil { | ||||
| 		if _, err := repo.Branch("main"); err != nil { | ||||
| 			logrus.Debugf("failed to select branch in '%s'", recipeDir) | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
| 		branch = "main" | ||||
| 	} | ||||
|  | ||||
| 	refName := fmt.Sprintf("refs/heads/%s", branch) | ||||
| 	checkOutOpts := &git.CheckoutOptions{ | ||||
| 		Create: false, | ||||
| 		Force:  true, | ||||
| 		Branch: plumbing.ReferenceName(refName), | ||||
| 	} | ||||
| 	if err := worktree.Checkout(checkOutOpts); err != nil { | ||||
| 		logrus.Debugf("failed to check out '%s' in '%s'", branch, recipeDir) | ||||
| 		logrus.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	logrus.Debugf("switched back to '%s' in '%s'", branch, recipeDir) | ||||
| 	logrus.Debugf("collected '%s' for '%s'", versions, recipeName) | ||||
|  | ||||
| 	return versions, nil | ||||
| } | ||||
|  | ||||
| // GetRecipeCatalogueVersions list the recipe versions listed in the recipe catalogue. | ||||
| func GetRecipeCatalogueVersions(recipeName string) ([]string, error) { | ||||
| 	var versions []string | ||||
|  | ||||
| 	catl, err := ReadRecipeCatalogue() | ||||
| 	if err != nil { | ||||
| 		return versions, err | ||||
| 	} | ||||
|  | ||||
| 	if recipeMeta, exists := catl[recipeName]; exists { | ||||
| 		for _, versionMeta := range recipeMeta.Versions { | ||||
| 			for tag := range versionMeta { | ||||
| 				versions = append(versions, tag) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return versions, nil | ||||
| } | ||||
|  | ||||
| @ -2,6 +2,7 @@ package stack | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
|  | ||||
| 	abraClient "coopcloud.tech/abra/pkg/client" | ||||
| @ -92,6 +93,50 @@ func GetAllDeployedServices(contextName string) StackStatus { | ||||
| 	return StackStatus{services, nil} | ||||
| } | ||||
|  | ||||
| // GetDeployedServicesByName filters services by name | ||||
| func GetDeployedServicesByName(ctx context.Context, cl *dockerclient.Client, stackName, serviceName string) StackStatus { | ||||
| 	filters := filters.NewArgs() | ||||
| 	filters.Add("name", fmt.Sprintf("%s_%s", stackName, serviceName)) | ||||
|  | ||||
| 	services, err := cl.ServiceList(ctx, types.ServiceListOptions{Filters: filters}) | ||||
| 	if err != nil { | ||||
| 		return StackStatus{[]swarm.Service{}, err} | ||||
| 	} | ||||
|  | ||||
| 	return StackStatus{services, nil} | ||||
| } | ||||
|  | ||||
| // IsDeployed chekcks whether an appp is deployed or not. | ||||
| func IsDeployed(ctx context.Context, cl *dockerclient.Client, stackName string) (bool, string, error) { | ||||
| 	version := "" | ||||
| 	isDeployed := false | ||||
|  | ||||
| 	filter := filters.NewArgs() | ||||
| 	filter.Add("label", fmt.Sprintf("%s=%s", convert.LabelNamespace, stackName)) | ||||
|  | ||||
| 	services, err := cl.ServiceList(ctx, types.ServiceListOptions{Filters: filter}) | ||||
| 	if err != nil { | ||||
| 		return false, version, err | ||||
| 	} | ||||
|  | ||||
| 	if len(services) > 0 { | ||||
| 		for _, service := range services { | ||||
| 			labelKey := fmt.Sprintf("coop-cloud.%s.version", stackName) | ||||
| 			if deployedVersion, ok := service.Spec.Labels[labelKey]; ok { | ||||
| 				version = deployedVersion | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		logrus.Debugf("'%s' has been detected as deployed with version '%s'", stackName, version) | ||||
|  | ||||
| 		return true, version, nil | ||||
| 	} | ||||
|  | ||||
| 	logrus.Debugf("'%s' has been detected as not deployed", stackName) | ||||
| 	return isDeployed, version, nil | ||||
| } | ||||
|  | ||||
| // 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{}) { | ||||
| 	oldServices, err := GetStackServices(ctx, cl, namespace.Name()) | ||||
|  | ||||
| @ -3,18 +3,20 @@ package compose | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"path" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
|  | ||||
| 	"coopcloud.tech/abra/pkg/client/stack" | ||||
| 	loader "coopcloud.tech/abra/pkg/client/stack" | ||||
| 	"coopcloud.tech/abra/pkg/config" | ||||
| 	composetypes "github.com/docker/cli/cli/compose/types" | ||||
| 	"github.com/docker/distribution/reference" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| ) | ||||
|  | ||||
| // UpdateTag updates an image tag in-place on file system local compose files. | ||||
| func UpdateTag(pattern, image, tag string) error { | ||||
| func UpdateTag(pattern, image, tag, recipeName string) error { | ||||
| 	composeFiles, err := filepath.Glob(pattern) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| @ -24,8 +26,14 @@ func UpdateTag(pattern, image, tag string) error { | ||||
|  | ||||
| 	for _, composeFile := range composeFiles { | ||||
| 		opts := stack.Deploy{Composefiles: []string{composeFile}} | ||||
| 		emptyEnv := make(map[string]string) | ||||
| 		compose, err := loader.LoadComposefile(opts, emptyEnv) | ||||
|  | ||||
| 		envSamplePath := path.Join(config.ABRA_DIR, "apps", recipeName, ".env.sample") | ||||
| 		sampleEnv, err := config.ReadEnv(envSamplePath) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		compose, err := loader.LoadComposefile(opts, sampleEnv) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| @ -74,7 +82,7 @@ func UpdateTag(pattern, image, tag string) error { | ||||
| } | ||||
|  | ||||
| // UpdateLabel updates a label in-place on file system local compose files. | ||||
| func UpdateLabel(pattern, serviceName, label string) error { | ||||
| func UpdateLabel(pattern, serviceName, label, recipeName string) error { | ||||
| 	composeFiles, err := filepath.Glob(pattern) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| @ -84,8 +92,14 @@ func UpdateLabel(pattern, serviceName, label string) error { | ||||
|  | ||||
| 	for _, composeFile := range composeFiles { | ||||
| 		opts := stack.Deploy{Composefiles: []string{composeFile}} | ||||
| 		emptyEnv := make(map[string]string) | ||||
| 		compose, err := loader.LoadComposefile(opts, emptyEnv) | ||||
|  | ||||
| 		envSamplePath := path.Join(config.ABRA_DIR, "apps", recipeName, ".env.sample") | ||||
| 		sampleEnv, err := config.ReadEnv(envSamplePath) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		compose, err := loader.LoadComposefile(opts, sampleEnv) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| @ -110,7 +124,7 @@ func UpdateLabel(pattern, serviceName, label string) error { | ||||
| 					return err | ||||
| 				} | ||||
|  | ||||
| 				old := fmt.Sprintf("coop-cloud.${STACK_NAME}.%s.version=%s", service.Name, value) | ||||
| 				old := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", value) | ||||
| 				replacedBytes := strings.Replace(string(bytes), old, label, -1) | ||||
|  | ||||
| 				logrus.Debugf("updating '%s' to '%s' in '%s'", old, label, compose.Filename) | ||||
|  | ||||
| @ -6,7 +6,6 @@ import ( | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"path" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
|  | ||||
| 	"coopcloud.tech/abra/cli/formatter" | ||||
| @ -46,7 +45,12 @@ type App struct { | ||||
|  | ||||
| // StackName gets what the docker safe stack name is for the app | ||||
| func (a App) StackName() string { | ||||
| 	return SanitiseAppName(a.Name) | ||||
| 	if _, exists := a.Env["STACK_NAME"]; exists { | ||||
| 		return a.Env["STACK_NAME"] | ||||
| 	} | ||||
| 	stackName := SanitiseAppName(a.Name) | ||||
| 	a.Env["STACK_NAME"] = stackName | ||||
| 	return stackName | ||||
| } | ||||
|  | ||||
| // SORTING TYPES | ||||
| @ -139,7 +143,7 @@ func LoadAppFiles(servers ...string) (AppFiles, error) { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	logrus.Debugf("collecting metadata from '%v' servers: '%s'", len(servers), servers) | ||||
| 	logrus.Debugf("collecting metadata from '%v' servers: '%s'", len(servers), strings.Join(servers, ", ")) | ||||
|  | ||||
| 	for _, server := range servers { | ||||
| 		serverDir := path.Join(ABRA_SERVER_FOLDER, server) | ||||
| @ -276,8 +280,8 @@ func SanitiseAppName(name string) string { | ||||
| } | ||||
|  | ||||
| // GetAppStatuses queries servers to check the deployment status of given apps | ||||
| func GetAppStatuses(appFiles AppFiles) (map[string]string, error) { | ||||
| 	statuses := map[string]string{} | ||||
| func GetAppStatuses(appFiles AppFiles) (map[string]map[string]string, error) { | ||||
| 	statuses := make(map[string]map[string]string) | ||||
|  | ||||
| 	var unique []string | ||||
| 	servers := make(map[string]struct{}) | ||||
| @ -300,10 +304,24 @@ func GetAppStatuses(appFiles AppFiles) (map[string]string, error) { | ||||
| 	for range servers { | ||||
| 		status := <-ch | ||||
| 		for _, service := range status.Services { | ||||
| 			result := make(map[string]string) | ||||
| 			name := service.Spec.Labels[convert.LabelNamespace] | ||||
|  | ||||
| 			if _, ok := statuses[name]; !ok { | ||||
| 				statuses[name] = "deployed" | ||||
| 				result["status"] = "deployed" | ||||
| 			} | ||||
|  | ||||
| 			labelKey := fmt.Sprintf("coop-cloud.%s.version", name) | ||||
| 			if version, ok := service.Spec.Labels[labelKey]; ok { | ||||
| 				result["version"] = version | ||||
| 			} else { | ||||
| 				//FIXME: we only need to check containers with the version label not | ||||
| 				//       every single container and then skip when we see no label perf gains | ||||
| 				//       to be had here | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			statuses[name] = result | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @ -315,20 +333,18 @@ func GetAppStatuses(appFiles AppFiles) (map[string]string, error) { | ||||
| // GetAppComposeFiles gets the list of compose files for an app which should be | ||||
| // merged into a composetypes.Config while respecting the COMPOSE_FILE env var. | ||||
| func GetAppComposeFiles(recipe string, appEnv AppEnv) ([]string, error) { | ||||
| 	var composeFiles []string | ||||
|  | ||||
| 	if _, ok := appEnv["COMPOSE_FILE"]; !ok { | ||||
| 		logrus.Debug("no COMPOSE_FILE detected, loading all compose files") | ||||
| 		pattern := fmt.Sprintf("%s/%s/compose**yml", APPS_DIR, recipe) | ||||
| 		composeFiles, err := filepath.Glob(pattern) | ||||
| 		if err != nil { | ||||
| 			return composeFiles, err | ||||
| 		} | ||||
| 		logrus.Debug("no COMPOSE_FILE detected, loading compose.yml") | ||||
| 		path := fmt.Sprintf("%s/%s/compose.yml", APPS_DIR, recipe) | ||||
| 		composeFiles = append(composeFiles, path) | ||||
| 		return composeFiles, nil | ||||
| 	} | ||||
|  | ||||
| 	var composeFiles []string | ||||
| 	composeFileEnvVar := appEnv["COMPOSE_FILE"] | ||||
| 	envVars := strings.Split(composeFileEnvVar, ":") | ||||
| 	logrus.Debugf("COMPOSE_FILE detected ('%s'), loading '%s'", composeFileEnvVar, envVars) | ||||
| 	logrus.Debugf("COMPOSE_FILE detected ('%s'), loading '%s'", composeFileEnvVar, strings.Join(envVars, ", ")) | ||||
| 	for _, file := range strings.Split(composeFileEnvVar, ":") { | ||||
| 		path := fmt.Sprintf("%s/%s/%s", APPS_DIR, recipe, file) | ||||
| 		composeFiles = append(composeFiles, path) | ||||
|  | ||||
| @ -62,7 +62,6 @@ func EnsureUpToDate(dir string) error { | ||||
| 	checkOutOpts := &git.CheckoutOptions{ | ||||
| 		Create: false, | ||||
| 		Force:  true, | ||||
| 		Keep:   false, | ||||
| 		Branch: plumbing.ReferenceName(refName), | ||||
| 	} | ||||
| 	if err := worktree.Checkout(checkOutOpts); err != nil { | ||||
|  | ||||
| @ -26,7 +26,7 @@ type Recipe struct { | ||||
| // UpdateLabel updates a recipe label | ||||
| func (r Recipe) UpdateLabel(serviceName, label string) error { | ||||
| 	pattern := fmt.Sprintf("%s/%s/compose**yml", config.APPS_DIR, r.Name) | ||||
| 	if err := compose.UpdateLabel(pattern, serviceName, label); err != nil { | ||||
| 	if err := compose.UpdateLabel(pattern, serviceName, label, r.Name); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return nil | ||||
| @ -35,12 +35,39 @@ func (r Recipe) UpdateLabel(serviceName, label string) error { | ||||
| // UpdateTag updates a recipe tag | ||||
| func (r Recipe) UpdateTag(image, tag string) error { | ||||
| 	pattern := fmt.Sprintf("%s/%s/compose**yml", config.APPS_DIR, r.Name) | ||||
| 	if err := compose.UpdateTag(pattern, image, tag); err != nil { | ||||
| 	if err := compose.UpdateTag(pattern, image, tag, r.Name); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Tags list the recipe tags | ||||
| func (r Recipe) Tags() ([]string, error) { | ||||
| 	var tags []string | ||||
|  | ||||
| 	recipeDir := path.Join(config.ABRA_DIR, "apps", r.Name) | ||||
| 	repo, err := git.PlainOpen(recipeDir) | ||||
| 	if err != nil { | ||||
| 		return tags, err | ||||
| 	} | ||||
|  | ||||
| 	gitTags, err := repo.Tags() | ||||
| 	if err != nil { | ||||
| 		return tags, err | ||||
| 	} | ||||
|  | ||||
| 	if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) { | ||||
| 		tags = append(tags, strings.TrimPrefix(string(ref.Name()), "refs/tags/")) | ||||
| 		return nil | ||||
| 	}); err != nil { | ||||
| 		return tags, err | ||||
| 	} | ||||
|  | ||||
| 	logrus.Debugf("detected '%s' as tags for recipe '%s'", strings.Join(tags, ", "), r.Name) | ||||
|  | ||||
| 	return tags, nil | ||||
| } | ||||
|  | ||||
| // Get retrieves a recipe. | ||||
| func Get(recipeName string) (Recipe, error) { | ||||
| 	if err := EnsureExists(recipeName); err != nil { | ||||
| @ -92,10 +119,10 @@ func EnsureVersion(recipeName, version string) error { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	logrus.Debugf("read '%s' as tags for recipe '%s'", tags, recipeName) | ||||
|  | ||||
| 	var parsedTags []string | ||||
| 	var tagRef plumbing.ReferenceName | ||||
| 	if err := tags.ForEach(func(ref *plumbing.Reference) (err error) { | ||||
| 		parsedTags = append(parsedTags, ref.Name().Short()) | ||||
| 		if ref.Name().Short() == version { | ||||
| 			tagRef = ref.Name() | ||||
| 		} | ||||
| @ -104,6 +131,8 @@ func EnsureVersion(recipeName, version string) error { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	logrus.Debugf("read '%s' as tags for recipe '%s'", strings.Join(parsedTags, ", "), recipeName) | ||||
|  | ||||
| 	if tagRef.String() == "" { | ||||
| 		return fmt.Errorf("%s is not available?", version) | ||||
| 	} | ||||
| @ -113,12 +142,56 @@ func EnsureVersion(recipeName, version string) error { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	opts := &git.CheckoutOptions{Branch: tagRef, Keep: true} | ||||
| 	opts := &git.CheckoutOptions{ | ||||
| 		Branch: tagRef, | ||||
| 		Create: false, | ||||
| 		Force:  true, | ||||
| 	} | ||||
| 	if err := worktree.Checkout(opts); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	logrus.Debugf("successfully checked '%s' out to '%s' in '%s'", recipeName, tagRef, recipeDir) | ||||
| 	logrus.Debugf("successfully checked '%s' out to '%s' in '%s'", recipeName, tagRef.Short(), recipeDir) | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // EnsureLatest makes sure the latest commit is checkout on for a local recipe repository. | ||||
| func EnsureLatest(recipeName string) error { | ||||
| 	recipeDir := path.Join(config.ABRA_DIR, "apps", recipeName) | ||||
|  | ||||
| 	logrus.Debugf("attempting to open git repository in '%s'", recipeDir) | ||||
|  | ||||
| 	repo, err := git.PlainOpen(recipeDir) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	worktree, err := repo.Worktree() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	branch := "master" | ||||
| 	if _, err := repo.Branch("master"); err != nil { | ||||
| 		if _, err := repo.Branch("main"); err != nil { | ||||
| 			logrus.Debugf("failed to select branch in '%s'", path.Join(config.APPS_DIR, recipeName)) | ||||
| 			return err | ||||
| 		} | ||||
| 		branch = "main" | ||||
| 	} | ||||
|  | ||||
| 	refName := fmt.Sprintf("refs/heads/%s", branch) | ||||
| 	checkOutOpts := &git.CheckoutOptions{ | ||||
| 		Create: false, | ||||
| 		Force:  true, | ||||
| 		Branch: plumbing.ReferenceName(refName), | ||||
| 	} | ||||
|  | ||||
| 	if err := worktree.Checkout(checkOutOpts); err != nil { | ||||
| 		logrus.Debugf("failed to check out '%s' in '%s'", branch, recipeDir) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
							
								
								
									
										24
									
								
								pkg/server/server.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								pkg/server/server.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"os" | ||||
| 	"path" | ||||
|  | ||||
| 	"coopcloud.tech/abra/pkg/config" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| ) | ||||
|  | ||||
| // CreateServerDir creates a server directory under ~/.abra. | ||||
| func CreateServerDir(serverName string) error { | ||||
| 	serverPath := path.Join(config.ABRA_DIR, "servers", serverName) | ||||
|  | ||||
| 	if err := os.Mkdir(serverPath, 0755); err != nil { | ||||
| 		if !os.IsExist(err) { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		logrus.Infof("'%s' already exists, moving on...", serverPath) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
| @ -1,6 +1,6 @@ | ||||
| #!/usr/bin/env bash | ||||
|  | ||||
| ABRA_VERSION="0.1.5-alpha" | ||||
| ABRA_VERSION="0.2.0-alpha" | ||||
| ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$ABRA_VERSION" | ||||
|  | ||||
| function show_banner { | ||||
| @ -35,13 +35,11 @@ function install_abra_release { | ||||
|   fi | ||||
|  | ||||
|   # FIXME: support different architectures | ||||
|   release_url=$(curl -s "$ABRA_RELEASE_URL" | | ||||
|     python3 -c "import sys, json; \ | ||||
|                 payload = json.load(sys.stdin); \ | ||||
|                 url = [a['browser_download_url'] for a in payload['assets'] if 'x86_64' in a['name']][0]; \ | ||||
|                 print(url)") | ||||
|   PLATFORM=$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m) | ||||
|   sed_command='s/.*"assets":\[\{[^}]*"name":"abra.*_'"$PLATFORM"'"[^}]*"browser_download_url":"([^"]*)".*\].*/\1/p' | ||||
|   release_url=$(curl -s $ABRA_RELEASE_URL | sed -En $sed_command) | ||||
|  | ||||
|   echo "downloading $ABRA_VERSION x86_64 binary release for abra..." | ||||
|   echo "downloading $ABRA_VERSION $PLATFORM binary release for abra..." | ||||
|   curl --progress-bar "$release_url" --output "$HOME/.local/bin/abra" | ||||
|   chmod +x "$HOME/.local/bin/abra" | ||||
|  | ||||
|  | ||||
| @ -3,5 +3,5 @@ STACK := abra_installer_script | ||||
| default: deploy | ||||
|  | ||||
| deploy: | ||||
| 	@docker stack rm $(STACK) && \ | ||||
| 		docker stack deploy -c compose.yml $(STACK) | ||||
| 	@DOCKER_CONTEXT=swarm.autonomic.zone docker stack rm $(STACK) && \ | ||||
| 		DOCKER_CONTEXT=swarm.autonomic.zone docker stack deploy -c compose.yml $(STACK) | ||||
|  | ||||
							
								
								
									
										0
									
								
								tests/resources/test_folder/cruft.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/resources/test_folder/cruft.txt
									
									
									
									
									
										Normal file
									
								
							
		Reference in New Issue
	
	Block a user