forked from toolshed/abra
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			deploy-rel
			...
			deploy-uns
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 124a91c67f | 
| @ -46,10 +46,7 @@ ${FOO:<default>} syntax). "check" does not confirm or deny this for you.`, | ||||
| 		} | ||||
|  | ||||
| 		table. | ||||
| 			Headers( | ||||
| 				fmt.Sprintf("%s .env.sample", app.Recipe.Name), | ||||
| 				fmt.Sprintf("%s.env", app.Name), | ||||
| 			). | ||||
| 			Headers("RECIPE ENV SAMPLE", "APP ENV"). | ||||
| 			StyleFunc(func(row, col int) lipgloss.Style { | ||||
| 				switch { | ||||
| 				case col == 1: | ||||
| @ -74,9 +71,7 @@ ${FOO:<default>} syntax). "check" does not confirm or deny this for you.`, | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if err := formatter.PrintTable(table); err != nil { | ||||
| 			log.Fatal(err) | ||||
| 		} | ||||
| 		fmt.Println(table) | ||||
| 	}, | ||||
| } | ||||
|  | ||||
|  | ||||
| @ -10,7 +10,6 @@ import ( | ||||
| 	"coopcloud.tech/abra/pkg/config" | ||||
| 	"coopcloud.tech/abra/pkg/envfile" | ||||
| 	"coopcloud.tech/abra/pkg/secret" | ||||
| 	"coopcloud.tech/tagcmp" | ||||
|  | ||||
| 	appPkg "coopcloud.tech/abra/pkg/app" | ||||
| 	"coopcloud.tech/abra/pkg/client" | ||||
| @ -46,16 +45,15 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`, | ||||
| 	ValidArgsFunction: func( | ||||
| 		cmd *cobra.Command, | ||||
| 		args []string, | ||||
| 		toComplete string, | ||||
| 	) ([]string, cobra.ShellCompDirective) { | ||||
| 		toComplete string) ([]string, cobra.ShellCompDirective) { | ||||
| 		switch l := len(args); l { | ||||
| 		case 0: | ||||
| 			return autocomplete.AppNameComplete() | ||||
| 		case 1: | ||||
| 			app, err := appPkg.Get(args[0]) | ||||
| 			if err != nil { | ||||
| 				errMsg := fmt.Sprintf("autocomplete failed: %s", err) | ||||
| 				return []string{errMsg}, cobra.ShellCompDirectiveError | ||||
| 				log.Debugf("autocomplete failed: %s", err) | ||||
| 				return nil, cobra.ShellCompDirectiveDefault | ||||
| 			} | ||||
| 			return autocomplete.RecipeVersionComplete(app.Recipe.Name) | ||||
| 		default: | ||||
| @ -112,19 +110,19 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`, | ||||
| 		// is because we need to deal with GetComposeFiles under the hood and these | ||||
| 		// files change from version to version which therefore affects which | ||||
| 		// secrets might be generated | ||||
| 		toDeployVersion := deployMeta.Version | ||||
| 		version := deployMeta.Version | ||||
| 		if specificVersion != "" { | ||||
| 			toDeployVersion = specificVersion | ||||
| 			log.Debugf("choosing %s as version to deploy", toDeployVersion) | ||||
| 			version = specificVersion | ||||
| 			log.Debugf("choosing %s as version to deploy", version) | ||||
|  | ||||
| 			var err error | ||||
| 			isChaosCommit, err = app.Recipe.EnsureVersion(toDeployVersion) | ||||
| 			isChaosCommit, err = app.Recipe.EnsureVersion(version) | ||||
| 			if err != nil { | ||||
| 				log.Fatal(err) | ||||
| 			} | ||||
|  | ||||
| 			if isChaosCommit { | ||||
| 				log.Debugf("assuming '%s' is a chaos commit", toDeployVersion) | ||||
| 				log.Debugf("assuming '%s' is a chaos commit", version) | ||||
| 				internal.Chaos = true | ||||
| 			} | ||||
| 		} | ||||
| @ -140,6 +138,14 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`, | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if deployMeta.IsDeployed { | ||||
| 			if internal.Force || internal.Chaos { | ||||
| 				warnMessages = append(warnMessages, fmt.Sprintf("%s is already deployed", app.Name)) | ||||
| 			} else { | ||||
| 				log.Fatalf("%s is already deployed", app.Name) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if !internal.Chaos && specificVersion == "" { | ||||
| 			versions, err := app.Recipe.Tags() | ||||
| 			if err != nil { | ||||
| @ -147,9 +153,9 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`, | ||||
| 			} | ||||
|  | ||||
| 			if len(versions) > 0 && !internal.Chaos { | ||||
| 				toDeployVersion = versions[len(versions)-1] | ||||
| 				log.Debugf("choosing %s as version to deploy", toDeployVersion) | ||||
| 				if _, err := app.Recipe.EnsureVersion(toDeployVersion); err != nil { | ||||
| 				version = versions[len(versions)-1] | ||||
| 				log.Debugf("choosing %s as version to deploy", version) | ||||
| 				if _, err := app.Recipe.EnsureVersion(version); err != nil { | ||||
| 					log.Fatal(err) | ||||
| 				} | ||||
| 			} else { | ||||
| @ -157,55 +163,31 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`, | ||||
| 				if err != nil { | ||||
| 					log.Fatal(err) | ||||
| 				} | ||||
| 				toDeployVersion = formatter.SmallSHA(head.String()) | ||||
| 				version = formatter.SmallSHA(head.String()) | ||||
| 				warnMessages = append(warnMessages, fmt.Sprintf("no versions detected, using latest commit")) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		toDeployChaosVersion := config.CHAOS_DEFAULT | ||||
| 		chaosVersion := config.CHAOS_DEFAULT | ||||
| 		if internal.Chaos { | ||||
| 			warnMessages = append(warnMessages, "chaos mode engaged") | ||||
|  | ||||
| 			if isChaosCommit { | ||||
| 				toDeployChaosVersion = specificVersion | ||||
| 				chaosVersion = specificVersion | ||||
| 				versionLabelLocal, err := app.Recipe.GetVersionLabelLocal() | ||||
| 				if err != nil { | ||||
| 					log.Fatal(err) | ||||
| 				} | ||||
| 				toDeployVersion = versionLabelLocal | ||||
| 				version = versionLabelLocal | ||||
| 			} else { | ||||
| 				var err error | ||||
| 				toDeployChaosVersion, err = app.Recipe.ChaosVersion() | ||||
| 				chaosVersion, err = app.Recipe.ChaosVersion() | ||||
| 				if err != nil { | ||||
| 					log.Fatal(err) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if deployMeta.IsDeployed { | ||||
| 			if !internal.Force { | ||||
| 				appStatus, err := app.Status() | ||||
| 				if err != nil { | ||||
| 					log.Fatal(err) | ||||
| 				} | ||||
| 				if appStatus.Chaos && !internal.Chaos { | ||||
| 					log.Fatalf("%s is deployed from a chaos version. Are you sure the local changes are in the current version (%s)?", app.Name, toDeployVersion) | ||||
| 				} | ||||
|  | ||||
| 				if toDeployVersion != "" && appStatus.Version != "" { | ||||
| 					localVersion, err := tagcmp.Parse(toDeployVersion) | ||||
| 					if err != nil { | ||||
| 						log.Fatal(err) | ||||
| 					} | ||||
| 					remoteVersion, err := tagcmp.Parse(appStatus.Version) | ||||
| 					if err != nil { | ||||
| 						log.Fatal(err) | ||||
| 					} | ||||
|  | ||||
| 					if localVersion.IsLessThan(remoteVersion) { | ||||
| 						log.Fatalf("%s is deployed at %s. Are you sure you want to downgrade to %s?", app.Name, appStatus.Version, toDeployVersion) | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		abraShEnv, err := envfile.ReadAbraShEnvVars(app.Recipe.AbraShPath) | ||||
| 		if err != nil { | ||||
| 			log.Fatal(err) | ||||
| @ -234,7 +216,7 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`, | ||||
| 		appPkg.ExposeAllEnv(stackName, compose, app.Env) | ||||
| 		appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name) | ||||
| 		appPkg.SetChaosLabel(compose, stackName, internal.Chaos) | ||||
| 		appPkg.SetChaosVersionLabel(compose, stackName, toDeployChaosVersion) | ||||
| 		appPkg.SetChaosVersionLabel(compose, stackName, chaosVersion) | ||||
| 		appPkg.SetUpdateLabel(compose, stackName, app.Env) | ||||
|  | ||||
| 		envVars, err := appPkg.CheckEnv(app) | ||||
| @ -257,24 +239,13 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`, | ||||
| 					log.Fatal(err) | ||||
| 				} | ||||
| 			} else { | ||||
| 				log.Debug("skipping domain checks as no DOMAIN=... configured for app") | ||||
| 				warnMessages = append(warnMessages, "skipping domain checks as no DOMAIN=... configured for app") | ||||
| 			} | ||||
| 		} else { | ||||
| 			log.Debug("skipping domain checks as requested") | ||||
| 			warnMessages = append(warnMessages, "skipping domain checks as requested") | ||||
| 		} | ||||
|  | ||||
| 		deployedVersion := config.NO_VERSION_DEFAULT | ||||
| 		if deployMeta.IsDeployed { | ||||
| 			deployedVersion = deployMeta.Version | ||||
| 		} | ||||
|  | ||||
| 		if err := internal.DeployOverview( | ||||
| 			app, | ||||
| 			warnMessages, | ||||
| 			deployedVersion, | ||||
| 			deployMeta.ChaosVersion, | ||||
| 			toDeployVersion, | ||||
| 			toDeployChaosVersion); err != nil { | ||||
| 		if err := internal.DeployOverview(app, warnMessages, version, chaosVersion); err != nil { | ||||
| 			log.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| @ -296,9 +267,9 @@ Please note, "upgrade"/"rollback" do not support chaos operations.`, | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		app.Recipe.Version = toDeployVersion | ||||
| 		if toDeployChaosVersion != config.CHAOS_DEFAULT { | ||||
| 			app.Recipe.Version = toDeployChaosVersion | ||||
| 		app.Recipe.Version = version | ||||
| 		if chaosVersion != config.CHAOS_DEFAULT { | ||||
| 			app.Recipe.Version = chaosVersion | ||||
| 		} | ||||
| 		log.Debugf("choosing %s as version to save to env file", app.Recipe.Version) | ||||
| 		if err := app.WriteRecipeVersion(app.Recipe.Version, false); err != nil { | ||||
|  | ||||
| @ -173,7 +173,7 @@ Use "--status/-S" flag to query all servers for the live deployment status.`, | ||||
| 							stats.LatestCount++ | ||||
| 						} | ||||
| 					} else { | ||||
| 						newUpdates = internal.SortVersionsDesc(newUpdates) | ||||
| 						newUpdates = internal.ReverseStringList(newUpdates) | ||||
| 						appStats.Upgrade = strings.Join(newUpdates, "\n") | ||||
| 						stats.UpgradeCount++ | ||||
| 					} | ||||
| @ -208,7 +208,7 @@ Use "--status/-S" flag to query all servers for the live deployment status.`, | ||||
|  | ||||
| 			serverStat := allStats[app.Server] | ||||
|  | ||||
| 			headers := []string{"RECIPE", "DOMAIN", "SERVER"} | ||||
| 			headers := []string{"RECIPE", "DOMAIN"} | ||||
| 			if status { | ||||
| 				headers = append(headers, []string{ | ||||
| 					"STATUS", | ||||
| @ -228,7 +228,7 @@ Use "--status/-S" flag to query all servers for the live deployment status.`, | ||||
|  | ||||
| 			var rows [][]string | ||||
| 			for _, appStat := range serverStat.Apps { | ||||
| 				row := []string{appStat.Recipe, appStat.Domain, appStat.Server} | ||||
| 				row := []string{appStat.Recipe, appStat.Domain} | ||||
| 				if status { | ||||
| 					chaosStatus := appStat.Chaos | ||||
| 					if chaosStatus != "unknown" { | ||||
| @ -256,8 +256,20 @@ Use "--status/-S" flag to query all servers for the live deployment status.`, | ||||
| 			table.Rows(rows...) | ||||
|  | ||||
| 			if len(rows) > 0 { | ||||
| 				if err := formatter.PrintTable(table); err != nil { | ||||
| 					log.Fatal(err) | ||||
| 				fmt.Println(table) | ||||
|  | ||||
| 				if status { | ||||
| 					fmt.Println(fmt.Sprintf( | ||||
| 						"SERVER: %s | TOTAL APPS: %v | VERSIONED: %v | UNVERSIONED: %v | LATEST : %v | UPGRADE: %v", | ||||
| 						app.Server, | ||||
| 						serverStat.AppCount, | ||||
| 						serverStat.VersionCount, | ||||
| 						serverStat.UnversionedCount, | ||||
| 						serverStat.LatestCount, | ||||
| 						serverStat.UpgradeCount, | ||||
| 					)) | ||||
| 				} else { | ||||
| 					log.Infof("SERVER: %s TOTAL APPS: %v", app.Server, serverStat.AppCount) | ||||
| 				} | ||||
|  | ||||
| 				if len(allStats) > 1 && len(rows) > 0 { | ||||
| @ -267,6 +279,12 @@ Use "--status/-S" flag to query all servers for the live deployment status.`, | ||||
|  | ||||
| 			alreadySeen[app.Server] = true | ||||
| 		} | ||||
|  | ||||
| 		if len(allStats) > 1 { | ||||
| 			totalServers := formatter.BoldStyle.Render("TOTAL SERVERS") | ||||
| 			totalApps := formatter.BoldStyle.Render("TOTAL APPS") | ||||
| 			log.Infof("%s: %v | %s: %v ", totalServers, totalServersCount, totalApps, totalAppsCount) | ||||
| 		} | ||||
| 	}, | ||||
| } | ||||
|  | ||||
|  | ||||
| @ -2,7 +2,6 @@ package app | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"os" | ||||
| 	"slices" | ||||
| @ -38,8 +37,8 @@ var AppLogsCommand = &cobra.Command{ | ||||
| 		case 1: | ||||
| 			app, err := appPkg.Get(args[0]) | ||||
| 			if err != nil { | ||||
| 				errMsg := fmt.Sprintf("autocomplete failed: %s", err) | ||||
| 				return []string{errMsg}, cobra.ShellCompDirectiveError | ||||
| 				log.Debugf("autocomplete failed: %s", err) | ||||
| 				return nil, cobra.ShellCompDirectiveDefault | ||||
| 			} | ||||
| 			return autocomplete.ServiceNameComplete(app.Name) | ||||
| 		default: | ||||
|  | ||||
							
								
								
									
										105
									
								
								cli/app/new.go
									
									
									
									
									
								
							
							
						
						
									
										105
									
								
								cli/app/new.go
									
									
									
									
									
								
							| @ -64,56 +64,56 @@ var AppNewCommand = &cobra.Command{ | ||||
| 	Run: func(cmd *cobra.Command, args []string) { | ||||
| 		recipe := internal.ValidateRecipe(args, cmd.Name()) | ||||
|  | ||||
| 		if len(args) == 2 && internal.Chaos { | ||||
| 			log.Fatal("cannot use [version] and --chaos together") | ||||
| 		} | ||||
|  | ||||
| 		var recipeVersion string | ||||
| 		if len(args) == 2 { | ||||
| 			recipeVersion = args[1] | ||||
| 		} | ||||
|  | ||||
| 		chaosVersion := config.CHAOS_DEFAULT | ||||
| 		if internal.Chaos { | ||||
| 			recipeVersion = chaosVersion | ||||
|  | ||||
| 		if !internal.Chaos { | ||||
| 			if err := recipe.EnsureIsClean(); err != nil { | ||||
| 				log.Fatal(err) | ||||
| 			} | ||||
| 			if !internal.Offline { | ||||
| 				if err := recipe.EnsureUpToDate(); err != nil { | ||||
| 					log.Fatal(err) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if !internal.Chaos { | ||||
| 			if err := recipe.EnsureIsClean(); err != nil { | ||||
| 				log.Fatal(err) | ||||
| 			if len(args) == 2 { | ||||
| 				recipeVersion = args[1] | ||||
| 			} | ||||
|  | ||||
| 			if recipeVersion == "" { | ||||
| 				recipeVersions, err := recipe.GetRecipeVersions() | ||||
| 				if err != nil { | ||||
| 					log.Fatal(err) | ||||
| 				} | ||||
|  | ||||
| 				if len(recipeVersions) > 0 { | ||||
| 					latest := recipeVersions[len(recipeVersions)-1] | ||||
| 					for tag := range latest { | ||||
| 						recipeVersion = tag | ||||
| 					} | ||||
|  | ||||
| 					if _, err := recipe.EnsureVersion(recipeVersion); err != nil { | ||||
| 						log.Fatal(err) | ||||
| 					} | ||||
| 				} else { | ||||
| 					if err := recipe.EnsureLatest(); err != nil { | ||||
| 						log.Fatal(err) | ||||
| 					} | ||||
| 				} | ||||
| 			} else { | ||||
| 				if _, err := recipe.EnsureVersion(recipeVersion); err != nil { | ||||
| 					log.Fatal(err) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		var recipeVersions recipePkg.RecipeVersions | ||||
| 		if recipeVersion == "" { | ||||
| 		if internal.Chaos && recipeVersion == "" { | ||||
| 			var err error | ||||
| 			recipeVersions, err = recipe.GetRecipeVersions() | ||||
| 			recipeVersion, err = recipe.ChaosVersion() | ||||
| 			if err != nil { | ||||
| 				log.Fatal(err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if len(recipeVersions) > 0 { | ||||
| 			latest := recipeVersions[len(recipeVersions)-1] | ||||
| 			for tag := range latest { | ||||
| 				recipeVersion = tag | ||||
| 			} | ||||
|  | ||||
| 			if _, err := recipe.EnsureVersion(recipeVersion); err != nil { | ||||
| 				log.Fatal(err) | ||||
| 			} | ||||
| 		} else { | ||||
| 			if err := recipe.EnsureLatest(); err != nil { | ||||
| 				log.Fatal(err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if err := ensureServerFlag(); err != nil { | ||||
| 			log.Fatal(err) | ||||
| 		} | ||||
| @ -187,20 +187,37 @@ var AppNewCommand = &cobra.Command{ | ||||
| 			newAppServer = "local" | ||||
| 		} | ||||
|  | ||||
| 		log.Infof("%s created successfully (version: %s, chaos: %s)", appDomain, recipeVersion, chaosVersion) | ||||
| 		table, err := formatter.CreateTable() | ||||
| 		if err != nil { | ||||
| 			log.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		headers := []string{"SERVER", "DOMAIN", "RECIPE", "VERSION"} | ||||
| 		table.Headers(headers...) | ||||
|  | ||||
| 		table.Row(newAppServer, appDomain, recipe.Name, recipeVersion) | ||||
|  | ||||
| 		log.Infof("new app '%s' created 🌞", recipe.Name) | ||||
|  | ||||
| 		fmt.Println("") | ||||
| 		fmt.Println(table) | ||||
| 		fmt.Println("") | ||||
|  | ||||
| 		fmt.Println("Configure this app:") | ||||
| 		fmt.Println(fmt.Sprintf("\n    abra app config %s", appDomain)) | ||||
|  | ||||
| 		fmt.Println("") | ||||
| 		fmt.Println("Deploy this app:") | ||||
| 		fmt.Println(fmt.Sprintf("\n    abra app deploy %s", appDomain)) | ||||
|  | ||||
| 		if len(appSecrets) > 0 { | ||||
| 			rows := [][]string{} | ||||
| 			for k, v := range appSecrets { | ||||
| 				rows = append(rows, []string{k, v}) | ||||
| 			} | ||||
|  | ||||
| 			overview := formatter.CreateOverview("SECRETS OVERVIEW", rows) | ||||
|  | ||||
| 			fmt.Println(overview) | ||||
| 			fmt.Println("") | ||||
| 			fmt.Println("Generated secrets:") | ||||
| 			fmt.Println("") | ||||
| 			fmt.Println(secretsTable) | ||||
|  | ||||
| 			log.Warnf( | ||||
| 				"secrets are %s shown again, please save them %s", | ||||
| 				"generated secrets %s shown again, please take note of them %s", | ||||
| 				formatter.BoldStyle.Render("NOT"), | ||||
| 				formatter.BoldStyle.Render("NOW"), | ||||
| 			) | ||||
|  | ||||
| @ -128,35 +128,24 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao | ||||
|  | ||||
| 		allContainerStats[containerStats["service"]] = containerStats | ||||
|  | ||||
| 		// NOTE(d1): don't clobber these variables for --machine output | ||||
| 		dVersion := deployedVersion | ||||
| 		cVersion := chaosVersion | ||||
|  | ||||
| 		if containerStats["service"] != "app" { | ||||
| 			// NOTE(d1): don't repeat info which only relevant for the "app" service | ||||
| 			dVersion = "" | ||||
| 			cVersion = "" | ||||
| 		} | ||||
|  | ||||
| 		row := []string{ | ||||
| 			containerStats["service"], | ||||
| 			containerStats["image"], | ||||
| 			dVersion, | ||||
| 			cVersion, | ||||
| 			containerStats["created"], | ||||
| 			containerStats["status"], | ||||
| 			containerStats["state"], | ||||
| 			containerStats["ports"], | ||||
| 		} | ||||
|  | ||||
| 		rows = append(rows, row) | ||||
| 	} | ||||
|  | ||||
| 	if internal.MachineReadable { | ||||
| 		rendered, err := json.Marshal(allContainerStats) | ||||
| 		jsonstring, err := json.Marshal(allContainerStats) | ||||
| 		if err != nil { | ||||
| 			log.Fatal("unable to convert to JSON: %s", err) | ||||
| 		} | ||||
|  | ||||
| 		fmt.Println(string(rendered)) | ||||
|  | ||||
| 		fmt.Println(string(jsonstring)) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| @ -168,18 +157,19 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao | ||||
| 	headers := []string{ | ||||
| 		"SERVICE", | ||||
| 		"IMAGE", | ||||
| 		"VERSION", | ||||
| 		"CHAOS", | ||||
| 		"CREATED", | ||||
| 		"STATUS", | ||||
| 		"STATE", | ||||
| 		"PORTS", | ||||
| 	} | ||||
|  | ||||
| 	table. | ||||
| 		Headers(headers...). | ||||
| 		Rows(rows...) | ||||
|  | ||||
| 	if err := formatter.PrintTable(table); err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
| 	fmt.Println(table) | ||||
|  | ||||
| 	log.Infof("VERSION: %s CHAOS: %s", deployedVersion, chaosVersion) | ||||
| } | ||||
|  | ||||
| func init() { | ||||
|  | ||||
| @ -46,8 +46,8 @@ beforehand.`, | ||||
| 		case 1: | ||||
| 			app, err := appPkg.Get(args[0]) | ||||
| 			if err != nil { | ||||
| 				errMsg := fmt.Sprintf("autocomplete failed: %s", err) | ||||
| 				return []string{errMsg}, cobra.ShellCompDirectiveError | ||||
| 				log.Debugf("autocomplete failed: %s", err) | ||||
| 				return nil, cobra.ShellCompDirectiveDefault | ||||
| 			} | ||||
| 			return autocomplete.RecipeVersionComplete(app.Recipe.Name) | ||||
| 		default: | ||||
| @ -167,7 +167,7 @@ beforehand.`, | ||||
|  | ||||
| 				prompt := &survey.Select{ | ||||
| 					Message: msg, | ||||
| 					Options: internal.SortVersionsDesc(availableDowngrades), | ||||
| 					Options: internal.ReverseStringList(availableDowngrades), | ||||
| 				} | ||||
|  | ||||
| 				if err := survey.AskOne(prompt, &chosenDowngrade); err != nil { | ||||
|  | ||||
| @ -34,8 +34,8 @@ var AppSecretGenerateCommand = &cobra.Command{ | ||||
| 		case 1: | ||||
| 			app, err := appPkg.Get(args[0]) | ||||
| 			if err != nil { | ||||
| 				errMsg := fmt.Sprintf("autocomplete failed: %s", err) | ||||
| 				return []string{errMsg}, cobra.ShellCompDirectiveError | ||||
| 				log.Debugf("autocomplete failed: %s", err) | ||||
| 				return nil, cobra.ShellCompDirectiveDefault | ||||
| 			} | ||||
| 			return autocomplete.SecretComplete(app.Recipe.Name) | ||||
| 		default: | ||||
| @ -127,9 +127,7 @@ var AppSecretGenerateCommand = &cobra.Command{ | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if err := formatter.PrintTable(table); err != nil { | ||||
| 			log.Fatal(err) | ||||
| 		} | ||||
| 		fmt.Println(table) | ||||
|  | ||||
| 		log.Warnf( | ||||
| 			"generated secrets %s shown again, please take note of them %s", | ||||
| @ -159,8 +157,8 @@ environment. Typically, you can let Abra generate them for you on app creation | ||||
| 		case 1: | ||||
| 			app, err := appPkg.Get(args[0]) | ||||
| 			if err != nil { | ||||
| 				errMsg := fmt.Sprintf("autocomplete failed: %s", err) | ||||
| 				return []string{errMsg}, cobra.ShellCompDirectiveError | ||||
| 				log.Debugf("autocomplete failed: %s", err) | ||||
| 				return nil, cobra.ShellCompDirectiveDefault | ||||
| 			} | ||||
| 			return autocomplete.SecretComplete(app.Recipe.Name) | ||||
| 		default: | ||||
| @ -245,8 +243,8 @@ var AppSecretRmCommand = &cobra.Command{ | ||||
| 			if !rmAllSecrets { | ||||
| 				app, err := appPkg.Get(args[0]) | ||||
| 				if err != nil { | ||||
| 					errMsg := fmt.Sprintf("autocomplete failed: %s", err) | ||||
| 					return []string{errMsg}, cobra.ShellCompDirectiveError | ||||
| 					log.Debugf("autocomplete failed: %s", err) | ||||
| 					return nil, cobra.ShellCompDirectiveDefault | ||||
| 				} | ||||
| 				return autocomplete.SecretComplete(app.Recipe.Name) | ||||
| 			} | ||||
| @ -396,10 +394,7 @@ var AppSecretLsCommand = &cobra.Command{ | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			if err := formatter.PrintTable(table); err != nil { | ||||
| 				log.Fatal(err) | ||||
| 			} | ||||
|  | ||||
| 			fmt.Println(table) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
|  | ||||
| @ -63,7 +63,7 @@ var AppServicesCommand = &cobra.Command{ | ||||
| 			log.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		headers := []string{"SERVICE (SHORT)", "SERVICE (LONG)"} | ||||
| 		headers := []string{"SERVICE (SHORT)", "SERVICE (LONG)", "IMAGE"} | ||||
| 		table.Headers(headers...) | ||||
|  | ||||
| 		var rows [][]string | ||||
| @ -80,6 +80,7 @@ var AppServicesCommand = &cobra.Command{ | ||||
| 			row := []string{ | ||||
| 				serviceShortName, | ||||
| 				serviceLongName, | ||||
| 				formatter.RemoveSha(container.Image), | ||||
| 			} | ||||
|  | ||||
| 			rows = append(rows, row) | ||||
| @ -88,9 +89,7 @@ var AppServicesCommand = &cobra.Command{ | ||||
| 		table.Rows(rows...) | ||||
|  | ||||
| 		if len(rows) > 0 { | ||||
| 			if err := formatter.PrintTable(table); err != nil { | ||||
| 				log.Fatal(err) | ||||
| 			} | ||||
| 			fmt.Println(table) | ||||
| 		} | ||||
| 	}, | ||||
| } | ||||
|  | ||||
| @ -21,7 +21,7 @@ var AppUndeployCommand = &cobra.Command{ | ||||
| 	Use:     "undeploy <app> [flags]", | ||||
| 	Aliases: []string{"un"}, | ||||
| 	Short:   "Undeploy an app", | ||||
| 	Long: `This does not destroy any application data. | ||||
| 	Long: `This does not destroy any of the application data. | ||||
|  | ||||
| However, you should remain vigilant, as your swarm installation will consider | ||||
| any previously attached volumes as eligible for pruning once undeployed. | ||||
| @ -59,10 +59,7 @@ Passing "--prune/-p" does not remove those volumes.`, | ||||
| 			chaosVersion = deployMeta.ChaosVersion | ||||
| 		} | ||||
|  | ||||
| 		if err := internal.UndeployOverview( | ||||
| 			app, | ||||
| 			deployMeta.Version, | ||||
| 			chaosVersion); err != nil { | ||||
| 		if err := internal.DeployOverview(app, []string{}, deployMeta.Version, chaosVersion); err != nil { | ||||
| 			log.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| @ -79,11 +76,6 @@ Passing "--prune/-p" does not remove those volumes.`, | ||||
| 				log.Fatal(err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		log.Debugf("choosing %s as version to save to env file", deployMeta.Version) | ||||
| 		if err := app.WriteRecipeVersion(deployMeta.Version, false); err != nil { | ||||
| 			log.Fatalf("writing undeployed recipe version in env file: %s", err) | ||||
| 		} | ||||
| 	}, | ||||
| } | ||||
|  | ||||
|  | ||||
| @ -40,8 +40,8 @@ beforehand.`, | ||||
| 		case 1: | ||||
| 			app, err := appPkg.Get(args[0]) | ||||
| 			if err != nil { | ||||
| 				errMsg := fmt.Sprintf("autocomplete failed: %s", err) | ||||
| 				return []string{errMsg}, cobra.ShellCompDirectiveError | ||||
| 				log.Debugf("autocomplete failed: %s", err) | ||||
| 				return nil, cobra.ShellCompDirectiveDefault | ||||
| 			} | ||||
| 			return autocomplete.RecipeVersionComplete(app.Recipe.Name) | ||||
| 		default: | ||||
| @ -159,7 +159,7 @@ beforehand.`, | ||||
|  | ||||
| 				prompt := &survey.Select{ | ||||
| 					Message: msg, | ||||
| 					Options: internal.SortVersionsDesc(availableUpgrades), | ||||
| 					Options: internal.ReverseStringList(availableUpgrades), | ||||
| 				} | ||||
|  | ||||
| 				if err := survey.AskOne(prompt, &chosenUpgrade); err != nil { | ||||
| @ -250,6 +250,7 @@ beforehand.`, | ||||
| 		} | ||||
|  | ||||
| 		if showReleaseNotes { | ||||
| 			fmt.Println() | ||||
| 			fmt.Print(releaseNotes) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| @ -2,6 +2,7 @@ package app | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
|  | ||||
| 	"coopcloud.tech/abra/cli/internal" | ||||
| 	"coopcloud.tech/abra/pkg/autocomplete" | ||||
| @ -42,7 +43,7 @@ var AppVolumeListCommand = &cobra.Command{ | ||||
| 			log.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		headers := []string{"NAME", "ON SERVER"} | ||||
| 		headers := []string{"name", "created", "mounted"} | ||||
|  | ||||
| 		table, err := formatter.CreateTable() | ||||
| 		if err != nil { | ||||
| @ -53,16 +54,14 @@ var AppVolumeListCommand = &cobra.Command{ | ||||
|  | ||||
| 		var rows [][]string | ||||
| 		for _, volume := range volumes { | ||||
| 			row := []string{volume.Name, volume.Mountpoint} | ||||
| 			row := []string{volume.Name, volume.CreatedAt, volume.Mountpoint} | ||||
| 			rows = append(rows, row) | ||||
| 		} | ||||
|  | ||||
| 		table.Rows(rows...) | ||||
|  | ||||
| 		if len(rows) > 0 { | ||||
| 			if err := formatter.PrintTable(table); err != nil { | ||||
| 				log.Fatal(err) | ||||
| 			} | ||||
| 			fmt.Println(table) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
|  | ||||
| @ -3,14 +3,10 @@ package internal | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"sort" | ||||
| 	"strings" | ||||
|  | ||||
| 	appPkg "coopcloud.tech/abra/pkg/app" | ||||
| 	"coopcloud.tech/abra/pkg/config" | ||||
| 	"coopcloud.tech/abra/pkg/formatter" | ||||
| 	"coopcloud.tech/abra/pkg/log" | ||||
| 	"coopcloud.tech/tagcmp" | ||||
| 	"github.com/AlecAivazis/survey/v2" | ||||
| 	"github.com/charmbracelet/lipgloss" | ||||
| 	dockerClient "github.com/docker/docker/client" | ||||
| @ -24,8 +20,7 @@ var borderStyle = lipgloss.NewStyle(). | ||||
|  | ||||
| var headerStyle = lipgloss.NewStyle(). | ||||
| 	Underline(true). | ||||
| 	Bold(true). | ||||
| 	PaddingBottom(1) | ||||
| 	Bold(true) | ||||
|  | ||||
| var leftStyle = lipgloss.NewStyle(). | ||||
| 	Bold(true) | ||||
| @ -42,13 +37,13 @@ func NewVersionOverview( | ||||
| 	app appPkg.App, | ||||
| 	warnMessages []string, | ||||
| 	kind, | ||||
| 	deployedVersion, | ||||
| 	deployedChaosVersion, | ||||
| 	toDeployVersion, | ||||
| 	currentVersion, | ||||
| 	chaosVersion, | ||||
| 	newVersion, | ||||
| 	releaseNotes string) error { | ||||
| 	deployConfig := "compose.yml" | ||||
| 	if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { | ||||
| 		deployConfig = composeFiles | ||||
| 		deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n") | ||||
| 	} | ||||
|  | ||||
| 	server := app.Server | ||||
| @ -56,35 +51,32 @@ func NewVersionOverview( | ||||
| 		server = "local" | ||||
| 	} | ||||
|  | ||||
| 	domain := app.Domain | ||||
| 	if domain == "" { | ||||
| 		domain = config.NO_DOMAIN_DEFAULT | ||||
| 	} | ||||
|  | ||||
| 	rows := [][]string{ | ||||
| 		[]string{"APP", domain}, | ||||
| 		[]string{"RECIPE", app.Recipe.Name}, | ||||
| 		[]string{"SERVER", server}, | ||||
| 		[]string{"DEPLOYED", deployedVersion}, | ||||
| 		[]string{"CURRENT CHAOS ", deployedChaosVersion}, | ||||
| 		[]string{fmt.Sprintf("TO %s", strings.ToUpper(kind)), toDeployVersion}, | ||||
| 		[]string{"CONFIG", deployConfig}, | ||||
| 	} | ||||
|  | ||||
| 	overview := formatter.CreateOverview( | ||||
| 		fmt.Sprintf("%s OVERVIEW", strings.ToUpper(kind)), | ||||
| 		rows, | ||||
| 	body := strings.Builder{} | ||||
| 	body.WriteString( | ||||
| 		borderStyle.Render( | ||||
| 			lipgloss.JoinVertical( | ||||
| 				lipgloss.Center, | ||||
| 				headerStyle.Render(fmt.Sprintf("%s OVERVIEW", strings.ToUpper(kind))), | ||||
| 				lipgloss.JoinVertical( | ||||
| 					lipgloss.Left, | ||||
| 					horizontal(leftStyle.Render("SERVER"), "   ", rightStyle.Render(server)), | ||||
| 					horizontal(leftStyle.Render("DOMAIN"), "   ", rightStyle.Render(app.Domain)), | ||||
| 					horizontal(leftStyle.Render("RECIPE"), "   ", rightStyle.Render(app.Recipe.Name)), | ||||
| 					horizontal(leftStyle.Render("CONFIG"), "   ", rightStyle.Render(deployConfig)), | ||||
| 					horizontal(leftStyle.Render("VERSION"), "  ", rightStyle.Render(currentVersion)), | ||||
| 					horizontal(leftStyle.Render("CHAOS"), "    ", rightStyle.Render(chaosVersion)), | ||||
| 					horizontal(leftStyle.Render("DEPLOY"), "   ", rightStyle.Padding(0).Render(newVersion)), | ||||
| 				), | ||||
| 			), | ||||
| 		), | ||||
| 	) | ||||
| 	fmt.Println(body.String()) | ||||
|  | ||||
| 	fmt.Println(overview) | ||||
|  | ||||
| 	if releaseNotes != "" && toDeployVersion != "" { | ||||
| 	if releaseNotes != "" && newVersion != "" { | ||||
| 		fmt.Println() | ||||
| 		fmt.Print(releaseNotes) | ||||
| 	} else { | ||||
| 		warnMessages = append( | ||||
| 			warnMessages, | ||||
| 			fmt.Sprintf("no release notes available for %s", toDeployVersion), | ||||
| 		) | ||||
| 		warnMessages = append(warnMessages, fmt.Sprintf("no release notes available for %s", newVersion)) | ||||
| 	} | ||||
|  | ||||
| 	for _, msg := range warnMessages { | ||||
| @ -109,16 +101,10 @@ func NewVersionOverview( | ||||
| } | ||||
|  | ||||
| // DeployOverview shows a deployment overview | ||||
| func DeployOverview( | ||||
| 	app appPkg.App, | ||||
| 	warnMessages []string, | ||||
| 	deployedVersion string, | ||||
| 	deployedChaosVersion string, | ||||
| 	toDeployVersion, | ||||
| 	toDeployChaosVersion string) error { | ||||
| func DeployOverview(app appPkg.App, warnMessages []string, version, chaosVersion string) error { | ||||
| 	deployConfig := "compose.yml" | ||||
| 	if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { | ||||
| 		deployConfig = composeFiles | ||||
| 		deployConfig = strings.Join(strings.Split(composeFiles, ":"), "\n") | ||||
| 	} | ||||
|  | ||||
| 	server := app.Server | ||||
| @ -126,25 +112,25 @@ func DeployOverview( | ||||
| 		server = "local" | ||||
| 	} | ||||
|  | ||||
| 	domain := app.Domain | ||||
| 	if domain == "" { | ||||
| 		domain = config.NO_DOMAIN_DEFAULT | ||||
| 	} | ||||
|  | ||||
| 	rows := [][]string{ | ||||
| 		[]string{"APP", domain}, | ||||
| 		[]string{"RECIPE", app.Recipe.Name}, | ||||
| 		[]string{"SERVER", server}, | ||||
| 		[]string{"DEPLOYED", deployedVersion}, | ||||
| 		[]string{"CURRENT CHAOS ", deployedChaosVersion}, | ||||
| 		[]string{"TO DEPLOY", toDeployVersion}, | ||||
| 		[]string{"NEW CHAOS", toDeployChaosVersion}, | ||||
| 		[]string{"CONFIG", deployConfig}, | ||||
| 	} | ||||
|  | ||||
| 	overview := formatter.CreateOverview("DEPLOY OVERVIEW", rows) | ||||
|  | ||||
| 	fmt.Println(overview) | ||||
| 	body := strings.Builder{} | ||||
| 	body.WriteString( | ||||
| 		borderStyle.Render( | ||||
| 			lipgloss.JoinVertical( | ||||
| 				lipgloss.Center, | ||||
| 				headerStyle.Render("DEPLOY OVERVIEW"), | ||||
| 				lipgloss.JoinVertical( | ||||
| 					lipgloss.Left, | ||||
| 					horizontal(leftStyle.Render("SERVER"), "   ", rightStyle.Render(server)), | ||||
| 					horizontal(leftStyle.Render("DOMAIN"), "   ", rightStyle.Render(app.Domain)), | ||||
| 					horizontal(leftStyle.Render("RECIPE"), "   ", rightStyle.Render(app.Recipe.Name)), | ||||
| 					horizontal(leftStyle.Render("CONFIG"), "   ", rightStyle.Render(deployConfig)), | ||||
| 					horizontal(leftStyle.Render("VERSION"), "  ", rightStyle.Render(version)), | ||||
| 					horizontal(leftStyle.Render("CHAOS"), "    ", rightStyle.Padding(0).Render(chaosVersion)), | ||||
| 				), | ||||
| 			), | ||||
| 		), | ||||
| 	) | ||||
| 	fmt.Println(body.String()) | ||||
|  | ||||
| 	for _, msg := range warnMessages { | ||||
| 		log.Warn(msg) | ||||
| @ -167,56 +153,6 @@ func DeployOverview( | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // UndeployOverview shows an undeployment overview | ||||
| func UndeployOverview( | ||||
| 	app appPkg.App, | ||||
| 	version, | ||||
| 	chaosVersion string) error { | ||||
| 	deployConfig := "compose.yml" | ||||
| 	if composeFiles, ok := app.Env["COMPOSE_FILE"]; ok { | ||||
| 		deployConfig = composeFiles | ||||
| 	} | ||||
|  | ||||
| 	server := app.Server | ||||
| 	if app.Server == "default" { | ||||
| 		server = "local" | ||||
| 	} | ||||
|  | ||||
| 	domain := app.Domain | ||||
| 	if domain == "" { | ||||
| 		domain = config.NO_DOMAIN_DEFAULT | ||||
| 	} | ||||
|  | ||||
| 	rows := [][]string{ | ||||
| 		[]string{"APP", domain}, | ||||
| 		[]string{"RECIPE", app.Recipe.Name}, | ||||
| 		[]string{"SERVER", server}, | ||||
| 		[]string{"DEPLOYED", version}, | ||||
| 		[]string{"CHAOS", chaosVersion}, | ||||
| 		[]string{"CONFIG", deployConfig}, | ||||
| 	} | ||||
|  | ||||
| 	overview := formatter.CreateOverview("UNDEPLOY OVERVIEW", rows) | ||||
|  | ||||
| 	fmt.Println(overview) | ||||
|  | ||||
| 	if NoInput { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	response := false | ||||
| 	prompt := &survey.Confirm{Message: "proceed?"} | ||||
| 	if err := survey.AskOne(prompt, &response); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if !response { | ||||
| 		log.Fatal("undeploy cancelled") | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // PostCmds parses a string of commands and executes them inside of the respective services | ||||
| // the commands string must have the following format: | ||||
| // "<service> <command> <arguments>|<service> <command> <arguments>|... " | ||||
| @ -274,22 +210,3 @@ func PostCmds(cl *dockerClient.Client, app appPkg.App, commands string) error { | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // SortVersionsDesc sorts versions in descending order. | ||||
| func SortVersionsDesc(versions []string) []string { | ||||
| 	var tags []tagcmp.Tag | ||||
|  | ||||
| 	for _, v := range versions { | ||||
| 		parsed, _ := tagcmp.Parse(v) // skips unsupported tags | ||||
| 		tags = append(tags, parsed) | ||||
| 	} | ||||
|  | ||||
| 	sort.Sort(tagcmp.ByTagDesc(tags)) | ||||
|  | ||||
| 	var desc []string | ||||
| 	for _, t := range tags { | ||||
| 		desc = append(desc, t.String()) | ||||
| 	} | ||||
|  | ||||
| 	return desc | ||||
| } | ||||
|  | ||||
							
								
								
									
										10
									
								
								cli/internal/list.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								cli/internal/list.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| package internal | ||||
|  | ||||
| // ReverseStringList reverses a list of a strings. Roll on Go generics. | ||||
| func ReverseStringList(strings []string) []string { | ||||
| 	for i, j := 0, len(strings)-1; i < j; i, j = i+1, j-1 { | ||||
| 		strings[i], strings[j] = strings[j], strings[i] | ||||
| 	} | ||||
|  | ||||
| 	return strings | ||||
| } | ||||
| @ -10,9 +10,10 @@ import ( | ||||
| ) | ||||
|  | ||||
| var RecipeFetchCommand = &cobra.Command{ | ||||
| 	Use:     "fetch [recipe | --all] [flags]", | ||||
| 	Use:     "fetch [recipe] [flags]", | ||||
| 	Aliases: []string{"f"}, | ||||
| 	Short:   "Clone recipe(s) locally", | ||||
| 	Short:   "Fetch recipe(s)", | ||||
| 	Long:    "Retrieves all recipes if no [recipe] argument is passed.", | ||||
| 	Args:    cobra.RangeArgs(0, 1), | ||||
| 	ValidArgsFunction: func( | ||||
| 		cmd *cobra.Command, | ||||
| @ -26,14 +27,6 @@ var RecipeFetchCommand = &cobra.Command{ | ||||
| 			recipeName = args[0] | ||||
| 		} | ||||
|  | ||||
| 		if recipeName == "" && !fetchAllRecipes { | ||||
| 			log.Fatal("missing [recipe] or --all/-a") | ||||
| 		} | ||||
|  | ||||
| 		if recipeName != "" && fetchAllRecipes { | ||||
| 			log.Fatal("cannot use [recipe] and --all/-a together") | ||||
| 		} | ||||
|  | ||||
| 		if recipeName != "" { | ||||
| 			r := internal.ValidateRecipe(args, cmd.Name()) | ||||
| 			if err := r.Ensure(false, false); err != nil { | ||||
| @ -57,17 +50,3 @@ var RecipeFetchCommand = &cobra.Command{ | ||||
| 		} | ||||
| 	}, | ||||
| } | ||||
|  | ||||
| var ( | ||||
| 	fetchAllRecipes bool | ||||
| ) | ||||
|  | ||||
| func init() { | ||||
| 	RecipeFetchCommand.Flags().BoolVarP( | ||||
| 		&fetchAllRecipes, | ||||
| 		"all", | ||||
| 		"a", | ||||
| 		false, | ||||
| 		"fetch all recipes", | ||||
| 	) | ||||
| } | ||||
|  | ||||
| @ -1,6 +1,8 @@ | ||||
| package recipe | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
|  | ||||
| 	"coopcloud.tech/abra/cli/internal" | ||||
| 	"coopcloud.tech/abra/pkg/autocomplete" | ||||
| 	"coopcloud.tech/abra/pkg/formatter" | ||||
| @ -102,9 +104,7 @@ var RecipeLintCommand = &cobra.Command{ | ||||
| 		} | ||||
|  | ||||
| 		if len(rows) > 0 { | ||||
| 			if err := formatter.PrintTable(table); err != nil { | ||||
| 				log.Fatal(err) | ||||
| 			} | ||||
| 			fmt.Println(table) | ||||
|  | ||||
| 			for _, warnMsg := range warnMessages { | ||||
| 				log.Warn(warnMsg) | ||||
|  | ||||
| @ -79,9 +79,8 @@ var RecipeListCommand = &cobra.Command{ | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			if err := formatter.PrintTable(table); err != nil { | ||||
| 				log.Fatal(err) | ||||
| 			} | ||||
| 			fmt.Println(table) | ||||
| 			log.Infof("total recipes: %v", len(rows)) | ||||
| 		} | ||||
| 	}, | ||||
| } | ||||
|  | ||||
| @ -252,14 +252,7 @@ func getTagCreateOptions(tag string) (git.CreateTagOptions, error) { | ||||
| // addReleaseNotes checks if the release/next release note exists and moves the | ||||
| // file to release/<tag>. | ||||
| func addReleaseNotes(recipe recipe.Recipe, tag string) error { | ||||
| 	releaseDir := path.Join(recipe.Dir, "release") | ||||
| 	if _, err := os.Stat(releaseDir); errors.Is(err, os.ErrNotExist) { | ||||
| 		if err := os.Mkdir(releaseDir, 0755); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	tagReleaseNotePath := path.Join(releaseDir, tag) | ||||
| 	tagReleaseNotePath := path.Join(recipe.Dir, "release", tag) | ||||
| 	if _, err := os.Stat(tagReleaseNotePath); err == nil { | ||||
| 		// Release note for current tag already exist exists. | ||||
| 		return nil | ||||
| @ -267,7 +260,7 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	nextReleaseNotePath := path.Join(releaseDir, "next") | ||||
| 	nextReleaseNotePath := path.Join(recipe.Dir, "release", "next") | ||||
| 	if _, err := os.Stat(nextReleaseNotePath); err == nil { | ||||
| 		// release/next note exists. Move it to release/<tag> | ||||
| 		if internal.Dry { | ||||
|  | ||||
| @ -55,32 +55,15 @@ var RecipeVersionCommand = &cobra.Command{ | ||||
| 				log.Fatal(err) | ||||
| 			} | ||||
|  | ||||
| 			table.Headers("SERVICE", "IMAGE", "TAG", "VERSION") | ||||
| 			table.Headers("SERVICE", "NAME", "TAG") | ||||
|  | ||||
| 			for version, meta := range recipeMeta.Versions[i] { | ||||
| 				var allRows [][]string | ||||
| 				var rows [][]string | ||||
|  | ||||
| 				for service, serviceMeta := range meta { | ||||
| 					recipeVersion := version | ||||
| 					if service != "app" { | ||||
| 						recipeVersion = "" | ||||
| 					} | ||||
|  | ||||
| 					rows = append(rows, []string{ | ||||
| 						service, | ||||
| 						serviceMeta.Image, | ||||
| 						serviceMeta.Tag, | ||||
| 						recipeVersion, | ||||
| 					}) | ||||
|  | ||||
| 					allRows = append(allRows, []string{ | ||||
| 						version, | ||||
| 						service, | ||||
| 						serviceMeta.Image, | ||||
| 						serviceMeta.Tag, | ||||
| 						recipeVersion, | ||||
| 					}) | ||||
| 					rows = append(rows, []string{service, serviceMeta.Image, serviceMeta.Tag}) | ||||
| 					allRows = append(allRows, []string{version, service, serviceMeta.Image, serviceMeta.Tag}) | ||||
| 				} | ||||
|  | ||||
| 				sort.Slice(rows, sortServiceByName(rows)) | ||||
| @ -88,9 +71,9 @@ var RecipeVersionCommand = &cobra.Command{ | ||||
| 				table.Rows(rows...) | ||||
|  | ||||
| 				if !internal.MachineReadable { | ||||
| 					if err := formatter.PrintTable(table); err != nil { | ||||
| 						log.Fatal(err) | ||||
| 					} | ||||
| 					fmt.Println(table) | ||||
| 					log.Infof("VERSION: %s", version) | ||||
| 					fmt.Println() | ||||
| 					continue | ||||
| 				} | ||||
|  | ||||
| @ -117,7 +100,11 @@ var RecipeVersionCommand = &cobra.Command{ | ||||
|  | ||||
| func sortServiceByName(versions [][]string) func(i, j int) bool { | ||||
| 	return func(i, j int) bool { | ||||
| 		return versions[i][0] < versions[j][0] | ||||
| 		// NOTE(d1): corresponds to the `tableCol` definition below | ||||
| 		if versions[i][1] == "app" { | ||||
| 			return true | ||||
| 		} | ||||
| 		return versions[i][1] < versions[j][1] | ||||
| 	} | ||||
| } | ||||
|  | ||||
|  | ||||
| @ -88,6 +88,10 @@ developer machine. The domain is then set to "default".`, | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if _, err := dns.EnsureIPv4(name); err != nil { | ||||
| 			log.Warn(err) | ||||
| 		} | ||||
|  | ||||
| 		_, err := createServerDir(name) | ||||
| 		if err != nil { | ||||
| 			log.Fatal(err) | ||||
| @ -96,28 +100,21 @@ developer machine. The domain is then set to "default".`, | ||||
| 		created, err := newContext(name) | ||||
| 		if err != nil { | ||||
| 			cleanUp(name) | ||||
| 			log.Fatalf("unable to create local context: %s", err) | ||||
| 			log.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		log.Debugf("attempting to create client for %s", name) | ||||
|  | ||||
| 		if _, err := client.New(name, timeout); err != nil { | ||||
| 			cleanUp(name) | ||||
| 			log.Debugf("ssh %s error: %s", name, sshPkg.Fatal(name, err)) | ||||
| 			log.Fatalf("can't ssh to %s, make sure \"ssh %s\" works", name, name) | ||||
| 			log.Fatal(sshPkg.Fatal(name, err)) | ||||
| 		} | ||||
|  | ||||
| 		if created { | ||||
| 			log.Infof("%s successfully added", name) | ||||
|  | ||||
| 			if _, err := dns.EnsureIPv4(name); err != nil { | ||||
| 				log.Warnf("unable to resolve IPv4 for %s", name) | ||||
| 			} | ||||
|  | ||||
| 			return | ||||
| 		} else { | ||||
| 			log.Warnf("%s already exists", name) | ||||
| 		} | ||||
|  | ||||
| 		log.Warnf("%s already exists", name) | ||||
| 	}, | ||||
| } | ||||
|  | ||||
|  | ||||
| @ -86,9 +86,7 @@ var ServerListCommand = &cobra.Command{ | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if err := formatter.PrintTable(table); err != nil { | ||||
| 			log.Fatal(err) | ||||
| 		} | ||||
| 		fmt.Println(table) | ||||
| 	}, | ||||
| } | ||||
|  | ||||
|  | ||||
| @ -89,8 +89,6 @@ type App struct { | ||||
| 	Env    envfile.AppEnv | ||||
| 	Server string | ||||
| 	Path   string | ||||
|  | ||||
| 	status *Status | ||||
| } | ||||
|  | ||||
| // Type aliases to make code hints easier to understand | ||||
| @ -134,19 +132,6 @@ func StackName(appName string) string { | ||||
| 	return stackName | ||||
| } | ||||
|  | ||||
| func (a App) Status() (Status, error) { | ||||
| 	if a.status != nil { | ||||
| 		return *a.status, nil | ||||
| 	} | ||||
| 	appStatuses, err := GetAppStatuses([]App{a}, true) | ||||
| 	if err != nil { | ||||
| 		return Status{}, err | ||||
| 	} | ||||
| 	appStatus := appStatuses[a.StackName()] | ||||
| 	a.status = &appStatus | ||||
| 	return appStatus, nil | ||||
| } | ||||
|  | ||||
| // Filters retrieves app filters for querying the container runtime. By default | ||||
| // it filters on all services in the app. It is also possible to pass an | ||||
| // otional list of service names, which get filtered instead. | ||||
| @ -412,17 +397,9 @@ func SanitiseAppName(name string) string { | ||||
| 	return strings.ReplaceAll(name, ".", "_") | ||||
| } | ||||
|  | ||||
| type Status struct { | ||||
| 	Status       string | ||||
| 	Version      string | ||||
| 	Chaos        bool | ||||
| 	ChaosVersion string | ||||
| 	AutoUpdate   bool | ||||
| } | ||||
|  | ||||
| // GetAppStatuses queries servers to check the deployment status of given apps. | ||||
| func GetAppStatuses(apps []App, MachineReadable bool) (map[string]Status, error) { | ||||
| 	statuses := make(map[string]Status) | ||||
| func GetAppStatuses(apps []App, MachineReadable bool) (map[string]map[string]string, error) { | ||||
| 	statuses := make(map[string]map[string]string) | ||||
|  | ||||
| 	servers := make(map[string]struct{}) | ||||
| 	for _, app := range apps { | ||||
| @ -458,32 +435,36 @@ func GetAppStatuses(apps []App, MachineReadable bool) (map[string]Status, error) | ||||
| 		} | ||||
|  | ||||
| 		for _, service := range status.Services { | ||||
| 			result := Status{} | ||||
| 			result := make(map[string]string) | ||||
| 			name := service.Spec.Labels[convert.LabelNamespace] | ||||
|  | ||||
| 			if _, ok := statuses[name]; !ok { | ||||
| 				result.Status = "deployed" | ||||
| 				result["status"] = "deployed" | ||||
| 			} | ||||
|  | ||||
| 			labelKey := fmt.Sprintf("coop-cloud.%s.chaos", name) | ||||
| 			chaos, ok := service.Spec.Labels[labelKey] | ||||
| 			if ok { | ||||
| 				result.Chaos = chaos == "true" | ||||
| 				result["chaos"] = chaos | ||||
| 			} | ||||
|  | ||||
| 			labelKey = fmt.Sprintf("coop-cloud.%s.chaos-version", name) | ||||
| 			if chaosVersion, ok := service.Spec.Labels[labelKey]; ok { | ||||
| 				result.ChaosVersion = chaosVersion | ||||
| 				result["chaosVersion"] = chaosVersion | ||||
| 			} | ||||
|  | ||||
| 			labelKey = fmt.Sprintf("coop-cloud.%s.autoupdate", name) | ||||
| 			if autoUpdate, ok := service.Spec.Labels[labelKey]; ok { | ||||
| 				result.AutoUpdate = autoUpdate == "true" | ||||
| 				result["autoUpdate"] = autoUpdate | ||||
| 			} else { | ||||
| 				result["autoUpdate"] = "false" | ||||
| 			} | ||||
|  | ||||
| 			labelKey = fmt.Sprintf("coop-cloud.%s.version", name) | ||||
| 			if version, ok := service.Spec.Labels[labelKey]; ok { | ||||
| 				result.Version = version | ||||
| 				result["version"] = version | ||||
| 			} else { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			statuses[name] = result | ||||
| @ -634,7 +615,7 @@ func (a App) WriteRecipeVersion(version string, dryRun bool) error { | ||||
| 	} | ||||
|  | ||||
| 	if !skipped { | ||||
| 		log.Debugf("version %s saved to %s.env", version, a.Domain) | ||||
| 		log.Infof("version %s saved to %s.env", version, a.Domain) | ||||
| 	} else { | ||||
| 		log.Debugf("skipping version %s write as already exists in %s.env", version, a.Domain) | ||||
| 	} | ||||
|  | ||||
| @ -1,26 +1,21 @@ | ||||
| package autocomplete | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"sort" | ||||
|  | ||||
| 	"coopcloud.tech/abra/pkg/app" | ||||
| 	appPkg "coopcloud.tech/abra/pkg/app" | ||||
| 	"coopcloud.tech/abra/pkg/log" | ||||
| 	"coopcloud.tech/abra/pkg/recipe" | ||||
| 	"github.com/spf13/cobra" | ||||
| ) | ||||
|  | ||||
| // AppNameComplete copletes app names. | ||||
| func AppNameComplete() ([]string, cobra.ShellCompDirective) { | ||||
| 	appFiles, err := app.LoadAppFiles("") | ||||
| 	appNames, err := app.GetAppNames() | ||||
| 	if err != nil { | ||||
| 		err := fmt.Sprintf("autocomplete failed: %s", err) | ||||
| 		return []string{err}, cobra.ShellCompDirectiveError | ||||
| 	} | ||||
|  | ||||
| 	var appNames []string | ||||
| 	for appName := range appFiles { | ||||
| 		appNames = append(appNames, appName) | ||||
| 		log.Debugf("autocomplete failed: %s", err) | ||||
| 		return nil, cobra.ShellCompDirectiveError | ||||
| 	} | ||||
|  | ||||
| 	return appNames, cobra.ShellCompDirectiveDefault | ||||
| @ -29,8 +24,8 @@ func AppNameComplete() ([]string, cobra.ShellCompDirective) { | ||||
| func ServiceNameComplete(appName string) ([]string, cobra.ShellCompDirective) { | ||||
| 	serviceNames, err := app.GetAppServiceNames(appName) | ||||
| 	if err != nil { | ||||
| 		err := fmt.Sprintf("autocomplete failed: %s", err) | ||||
| 		return []string{err}, cobra.ShellCompDirectiveError | ||||
| 		log.Debugf("autocomplete failed: %s", err) | ||||
| 		return nil, cobra.ShellCompDirectiveError | ||||
| 	} | ||||
|  | ||||
| 	return serviceNames, cobra.ShellCompDirectiveDefault | ||||
| @ -40,8 +35,8 @@ func ServiceNameComplete(appName string) ([]string, cobra.ShellCompDirective) { | ||||
| func RecipeNameComplete() ([]string, cobra.ShellCompDirective) { | ||||
| 	catl, err := recipe.ReadRecipeCatalogue(false) | ||||
| 	if err != nil { | ||||
| 		err := fmt.Sprintf("autocomplete failed: %s", err) | ||||
| 		return []string{err}, cobra.ShellCompDirectiveError | ||||
| 		log.Debugf("autocomplete failed: %s", err) | ||||
| 		return nil, cobra.ShellCompDirectiveError | ||||
| 	} | ||||
|  | ||||
| 	var recipeNames []string | ||||
| @ -56,8 +51,8 @@ func RecipeNameComplete() ([]string, cobra.ShellCompDirective) { | ||||
| func RecipeVersionComplete(recipeName string) ([]string, cobra.ShellCompDirective) { | ||||
| 	catl, err := recipe.ReadRecipeCatalogue(false) | ||||
| 	if err != nil { | ||||
| 		err := fmt.Sprintf("autocomplete failed: %s", err) | ||||
| 		return []string{err}, cobra.ShellCompDirectiveError | ||||
| 		log.Debugf("autocomplete failed: %s", err) | ||||
| 		return nil, cobra.ShellCompDirectiveError | ||||
| 	} | ||||
|  | ||||
| 	var recipeVersions []string | ||||
| @ -74,8 +69,8 @@ func RecipeVersionComplete(recipeName string) ([]string, cobra.ShellCompDirectiv | ||||
| func ServerNameComplete() ([]string, cobra.ShellCompDirective) { | ||||
| 	files, err := app.LoadAppFiles("") | ||||
| 	if err != nil { | ||||
| 		err := fmt.Sprintf("autocomplete failed: %s", err) | ||||
| 		return []string{err}, cobra.ShellCompDirectiveError | ||||
| 		log.Debugf("autocomplete failed: %s", err) | ||||
| 		return nil, cobra.ShellCompDirectiveError | ||||
| 	} | ||||
|  | ||||
| 	var serverNames []string | ||||
| @ -90,14 +85,14 @@ func ServerNameComplete() ([]string, cobra.ShellCompDirective) { | ||||
| func CommandNameComplete(appName string) ([]string, cobra.ShellCompDirective) { | ||||
| 	app, err := app.Get(appName) | ||||
| 	if err != nil { | ||||
| 		err := fmt.Sprintf("autocomplete failed: %s", err) | ||||
| 		return []string{err}, cobra.ShellCompDirectiveError | ||||
| 		log.Debugf("autocomplete failed: %s", err) | ||||
| 		return nil, cobra.ShellCompDirectiveError | ||||
| 	} | ||||
|  | ||||
| 	cmdNames, err := appPkg.ReadAbraShCmdNames(app.Recipe.AbraShPath) | ||||
| 	if err != nil { | ||||
| 		err := fmt.Sprintf("autocomplete failed: %s", err) | ||||
| 		return []string{err}, cobra.ShellCompDirectiveError | ||||
| 		log.Debugf("autocomplete failed: %s", err) | ||||
| 		return nil, cobra.ShellCompDirectiveError | ||||
| 	} | ||||
|  | ||||
| 	sort.Strings(cmdNames) | ||||
| @ -111,8 +106,8 @@ func SecretComplete(recipeName string) ([]string, cobra.ShellCompDirective) { | ||||
|  | ||||
| 	config, err := r.GetComposeConfig(nil) | ||||
| 	if err != nil { | ||||
| 		err := fmt.Sprintf("autocomplete failed: %s", err) | ||||
| 		return []string{err}, cobra.ShellCompDirectiveError | ||||
| 		log.Debugf("autocomplete failed: %s", err) | ||||
| 		return nil, cobra.ShellCompDirectiveError | ||||
| 	} | ||||
|  | ||||
| 	var secretNames []string | ||||
|  | ||||
| @ -107,13 +107,5 @@ var ( | ||||
| 	REPOS_BASE_URL           = "https://git.coopcloud.tech/coop-cloud" | ||||
| 	CATALOGUE_JSON_REPO_NAME = "recipes-catalogue-json" | ||||
| 	SSH_URL_TEMPLATE         = "ssh://git@git.coopcloud.tech:2222/coop-cloud/%s.git" | ||||
|  | ||||
| 	// NOTE(d1): please note, this was done purely out of laziness on our part | ||||
| 	// AFAICR. it's easy to punt the value into the label because that is what is | ||||
| 	// expects. it's not particularly useful in terms of UI/UX but hey, nobody | ||||
| 	// complained yet! | ||||
| 	CHAOS_DEFAULT = "false" | ||||
|  | ||||
| 	NO_DOMAIN_DEFAULT  = "N/A" | ||||
| 	NO_VERSION_DEFAULT = "N/A" | ||||
| 	CHAOS_DEFAULT            = "false" | ||||
| ) | ||||
|  | ||||
| @ -9,11 +9,12 @@ import ( | ||||
| func EnsureIPv4(domainName string) (string, error) { | ||||
| 	ipv4, err := net.ResolveIPAddr("ip4", domainName) | ||||
| 	if err != nil { | ||||
| 		return "", fmt.Errorf("%s: unable to resolve IPv4 address: %s", domainName, err) | ||||
| 		return "", fmt.Errorf("unable to resolve ipv4 address for %s, %s", domainName, err) | ||||
| 	} | ||||
|  | ||||
| 	// NOTE(d1): e.g. when there is only an ipv6 record available | ||||
| 	if ipv4 == nil { | ||||
| 		return "", fmt.Errorf("%s: no IPv4 available", domainName) | ||||
| 		return "", fmt.Errorf("unable to resolve ipv4 address for %s", domainName) | ||||
| 	} | ||||
|  | ||||
| 	return ipv4.String(), nil | ||||
|  | ||||
| @ -43,122 +43,33 @@ func HumanDuration(timestamp int64) string { | ||||
|  | ||||
| // CreateTable prepares a table layout for output. | ||||
| func CreateTable() (*table.Table, error) { | ||||
| 	var ( | ||||
| 		renderer    = lipgloss.NewRenderer(os.Stdout) | ||||
| 		headerStyle = renderer.NewStyle().Bold(true).Align(lipgloss.Center) | ||||
| 		cellStyle   = renderer.NewStyle().Padding(0, 1) | ||||
| 		borderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("63")) | ||||
| 	) | ||||
|  | ||||
| 	table := table.New(). | ||||
| 		Border(lipgloss.ThickBorder()). | ||||
| 		BorderStyle(borderStyle). | ||||
| 		StyleFunc(func(row, col int) lipgloss.Style { | ||||
| 			var style lipgloss.Style | ||||
| 		BorderStyle( | ||||
| 			lipgloss.NewStyle(). | ||||
| 				Foreground(lipgloss.Color("63")), | ||||
| 		) | ||||
|  | ||||
| 			switch { | ||||
| 			case row == table.HeaderRow: | ||||
| 				return headerStyle | ||||
| 			default: | ||||
| 				style = cellStyle | ||||
| 			} | ||||
|  | ||||
| 			return style | ||||
| 		}) | ||||
|  | ||||
| 	return table, nil | ||||
| } | ||||
|  | ||||
| func PrintTable(t *table.Table) error { | ||||
| 	if isAbraCI, ok := os.LookupEnv("ABRA_CI"); ok && isAbraCI == "1" { | ||||
| 		// NOTE(d1): no width limits for CI testing since we test against outputs | ||||
| 		log.Debug("detected ABRA_CI=1") | ||||
| 		fmt.Println(t) | ||||
| 		return nil | ||||
| 		return table, nil | ||||
| 	} | ||||
|  | ||||
| 	tWidth, _ := lipgloss.Size(t.String()) | ||||
|  | ||||
| 	width, _, err := term.GetSize(0) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if tWidth > width { | ||||
| 		t.Width(width - 10) | ||||
| 	if width-10 < 79 { | ||||
| 		// NOTE(d1): maintain standard minimum width | ||||
| 		table.Width(79) | ||||
| 	} else { | ||||
| 		// NOTE(d1): tests show that this produces stable border drawing | ||||
| 		table.Width(width - 10) | ||||
| 	} | ||||
|  | ||||
| 	fmt.Println(t) | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // horizontal is a JoinHorizontal helper function. | ||||
| func horizontal(left, mid, right string) string { | ||||
| 	return lipgloss.JoinHorizontal(lipgloss.Right, left, mid, right) | ||||
| } | ||||
|  | ||||
| func CreateOverview(header string, rows [][]string) string { | ||||
| 	var borderStyle = lipgloss.NewStyle(). | ||||
| 		BorderStyle(lipgloss.ThickBorder()). | ||||
| 		Padding(0, 1, 0, 1). | ||||
| 		MaxWidth(79). | ||||
| 		BorderForeground(lipgloss.Color("63")) | ||||
|  | ||||
| 	var headerStyle = lipgloss.NewStyle(). | ||||
| 		Underline(true). | ||||
| 		Bold(true). | ||||
| 		PaddingBottom(1) | ||||
|  | ||||
| 	var leftStyle = lipgloss.NewStyle(). | ||||
| 		Bold(true) | ||||
|  | ||||
| 	var rightStyle = lipgloss.NewStyle() | ||||
|  | ||||
| 	var longest int | ||||
| 	for _, row := range rows { | ||||
| 		if len(row[0]) > longest { | ||||
| 			longest = len(row[0]) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	var renderedRows []string | ||||
| 	for _, row := range rows { | ||||
| 		if len(row) > 2 { | ||||
| 			panic("CreateOverview: only accepts rows of len == 2") | ||||
| 		} | ||||
|  | ||||
| 		lenOffset := 4 | ||||
| 		if len(row[0]) < longest { | ||||
| 			lenOffset += longest - len(row[0]) | ||||
| 		} | ||||
|  | ||||
| 		offset := "" | ||||
| 		for range lenOffset { | ||||
| 			offset = offset + " " | ||||
| 		} | ||||
|  | ||||
| 		renderedRows = append( | ||||
| 			renderedRows, | ||||
| 			horizontal(leftStyle.Render(row[0]), offset, rightStyle.Render(row[1])), | ||||
| 		) | ||||
| 	} | ||||
|  | ||||
| 	body := strings.Builder{} | ||||
| 	body.WriteString( | ||||
| 		borderStyle.Render( | ||||
| 			lipgloss.JoinVertical( | ||||
| 				lipgloss.Center, | ||||
| 				headerStyle.Render(header), | ||||
| 				lipgloss.JoinVertical( | ||||
| 					lipgloss.Left, | ||||
| 					renderedRows..., | ||||
| 				), | ||||
| 			), | ||||
| 		), | ||||
| 	) | ||||
|  | ||||
| 	return body.String() | ||||
| 	return table, nil | ||||
| } | ||||
|  | ||||
| // ToJSON converts a lipgloss.Table to JSON representation. It's not a robust | ||||
|  | ||||
| @ -137,13 +137,6 @@ var LintRules = map[string][]LintRule{ | ||||
| 			HowToResolve: "name a servce 'app'", | ||||
| 			Function:     LintAppService, | ||||
| 		}, | ||||
| 		{ | ||||
| 			Ref:          "R015", | ||||
| 			Level:        "error", | ||||
| 			Description:  "deploy labels stanza present", | ||||
| 			HowToResolve: "include \"deploy: labels: ...\" stanza", | ||||
| 			Function:     LintDeployLabelsPresent, | ||||
| 		}, | ||||
| 		{ | ||||
| 			Ref:           "R010", | ||||
| 			Level:         "error", | ||||
| @ -276,21 +269,6 @@ func LintTraefikEnabled(recipe recipe.Recipe) (bool, error) { | ||||
| 	return false, nil | ||||
| } | ||||
|  | ||||
| func LintDeployLabelsPresent(recipe recipe.Recipe) (bool, error) { | ||||
| 	config, err := recipe.GetComposeConfig(nil) | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
|  | ||||
| 	for _, service := range config.Services { | ||||
| 		if service.Name == "app" && service.Deploy.Labels != nil { | ||||
| 			return true, nil | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return false, nil | ||||
| } | ||||
|  | ||||
| func LintHealthchecks(recipe recipe.Recipe) (bool, error) { | ||||
| 	config, err := recipe.GetComposeConfig(nil) | ||||
| 	if err != nil { | ||||
|  | ||||
| @ -6,7 +6,6 @@ import ( | ||||
| 	"path" | ||||
|  | ||||
| 	"coopcloud.tech/abra/pkg/envfile" | ||||
| 	"coopcloud.tech/abra/pkg/formatter" | ||||
| ) | ||||
|  | ||||
| func (r Recipe) SampleEnv() (map[string]string, error) { | ||||
| @ -30,10 +29,7 @@ func (r Recipe) GetReleaseNotes(version string) (string, error) { | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
|  | ||||
| 		title := formatter.BoldStyle.Render(fmt.Sprintf("%s release notes:", version)) | ||||
| 		withTitle := fmt.Sprintf("%s\n%s\n", title, releaseNotes) | ||||
|  | ||||
| 		withTitle := fmt.Sprintf("%s release notes:\n%s", version, string(releaseNotes)) | ||||
| 		return withTitle, nil | ||||
| 	} | ||||
|  | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| package recipe | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| @ -67,6 +68,9 @@ func (r Recipe) EnsureExists() error { | ||||
|  | ||||
| // EnsureVersion checks whether a specific version exists for a recipe. | ||||
| func (r Recipe) EnsureVersion(version string) (bool, error) { | ||||
| 	if strings.Contains(version, "+ unstaged changes") { | ||||
| 		return true, errors.New("A chaos version is configured. \nMaybe your coworker deployed a local version of the recipe?\nOr did you forget to add --chaos?") | ||||
| 	} | ||||
| 	isChaosCommit := false | ||||
|  | ||||
| 	if err := gitPkg.EnsureGitRepo(r.Dir); err != nil { | ||||
| @ -247,7 +251,7 @@ func (r Recipe) ChaosVersion() (string, error) { | ||||
| 	} | ||||
|  | ||||
| 	if !isClean { | ||||
| 		version = fmt.Sprintf("%s+U", version) | ||||
| 		version = fmt.Sprintf("%s + unstaged changes", version) | ||||
| 	} | ||||
|  | ||||
| 	return version, nil | ||||
|  | ||||
| @ -65,7 +65,7 @@ teardown(){ | ||||
|   run $ABRA app deploy "$TEST_APP_DOMAIN" \ | ||||
|     --chaos --no-input --no-converge-checks | ||||
|   assert_success | ||||
|   assert_output --partial 'NEW CHAOS' | ||||
|   assert_output --partial 'chaos' | ||||
| } | ||||
|  | ||||
| # bats test_tags=slow | ||||
| @ -111,6 +111,8 @@ teardown(){ | ||||
|     --no-input --no-converge-checks --offline | ||||
|   assert_success | ||||
|   assert_output --partial "$latestCommit" | ||||
|   assert_output --partial 'using latest commit' | ||||
|   refute_output --partial 'chaos' | ||||
| } | ||||
|  | ||||
| # bats test_tags=slow | ||||
| @ -128,7 +130,7 @@ teardown(){ | ||||
|     --no-input --no-converge-checks --chaos | ||||
|   assert_success | ||||
|   assert_output --partial "${wantHash:0:8}" | ||||
|   assert_output --partial 'NEW CHAOS' | ||||
|   assert_output --partial 'chaos' | ||||
| } | ||||
|  | ||||
| # bats test_tags=slow | ||||
| @ -170,10 +172,12 @@ teardown(){ | ||||
|   run $ABRA app deploy "$TEST_APP_DOMAIN" \ | ||||
|     --no-input --no-converge-checks --force | ||||
|   assert_success | ||||
|   assert_output --partial 'already deployed' | ||||
|  | ||||
|   run $ABRA app deploy "$TEST_APP_DOMAIN" \ | ||||
|     --no-input --no-converge-checks --chaos | ||||
|   assert_success | ||||
|   assert_output --partial 'already deployed' | ||||
| } | ||||
|  | ||||
| # bats test_tags=slow | ||||
| @ -224,6 +228,7 @@ teardown(){ | ||||
|  | ||||
|   run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks | ||||
|   assert_success | ||||
|   assert_output --partial 'no DOMAIN=... configured for app' | ||||
|  | ||||
|   run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input | ||||
|   assert_success | ||||
| @ -257,6 +262,7 @@ teardown(){ | ||||
|   run $ABRA app deploy "$TEST_APP_DOMAIN" \ | ||||
|     --no-input --no-converge-checks --no-domain-checks | ||||
|   assert_success | ||||
|   assert_output --partial 'skipping domain checks as requested' | ||||
| } | ||||
|  | ||||
| @test "error if specific version does not exist" { | ||||
|  | ||||
| @ -25,6 +25,17 @@ teardown(){ | ||||
|   _reset_app | ||||
| } | ||||
|  | ||||
| # bats test_tags=slow | ||||
| @test "fails to deploy + unstaged changes version" { | ||||
|   run sed -i 's/TYPE=abra-test-recipe.*/TYPE=git.coopcloud.tech\/coop-cloud\/abra-test-recipe:111111 + unstaged changes/g' \ | ||||
|     "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" | ||||
|   assert_success | ||||
|  | ||||
|   run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input | ||||
|   assert_failure | ||||
|   assert_output --partial 'A chaos version is configured. \nMaybe your coworker deployed a local version of the recipe?\nOr did you forget to add --chaos?' | ||||
| } | ||||
|  | ||||
| # bats test_tags=slow | ||||
| @test "deploy version written to env" { | ||||
|   run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input --no-converge-checks | ||||
|  | ||||
| @ -60,6 +60,11 @@ teardown(){ | ||||
|   assert_output --partial "$TEST_SERVER" | ||||
|   assert_output --partial "foo.com" | ||||
|  | ||||
|   run $ABRA app ls --server foo.com | ||||
|   assert_success | ||||
|   refute_output --partial "SERVER: $TEST_SERVER" | ||||
|   assert_output --partial "SERVER: foo.com" | ||||
|  | ||||
|   run rm -rf "$ABRA_DIR/servers/foo.com" | ||||
|   assert_success | ||||
|   assert_not_exists "$ABRA_DIR/servers/foo.com" | ||||
| @ -89,6 +94,33 @@ teardown(){ | ||||
|   assert_output --partial "foo-recipe" | ||||
| } | ||||
|  | ||||
| @test "server stats are correct" { | ||||
|   run $ABRA app ls | ||||
|   assert_success | ||||
|   assert_output --partial "SERVER: $TEST_SERVER" | ||||
|   assert_output --partial "TOTAL APPS: 1" | ||||
|  | ||||
|   run mkdir -p "$ABRA_DIR/servers/foo.com" | ||||
|   assert_success | ||||
|   assert_exists "$ABRA_DIR/servers/foo.com" | ||||
|  | ||||
|   run cp \ | ||||
|     "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" \ | ||||
|     "$ABRA_DIR/servers/foo.com/app.foo.com.env" | ||||
|   assert_exists "$ABRA_DIR/servers/foo.com/app.foo.com.env" | ||||
|  | ||||
|   run $ABRA app ls | ||||
|   assert_success | ||||
|   assert_output --partial "$TEST_SERVER" | ||||
|   assert_output --partial "foo.com" | ||||
|   assert_output --partial "TOTAL SERVERS: 2" | ||||
|   assert_output --partial "TOTAL APPS: 2" | ||||
|  | ||||
|   run rm -rf "$ABRA_DIR/servers/foo.com" | ||||
|   assert_success | ||||
|   assert_not_exists "$ABRA_DIR/servers/foo.com" | ||||
| } | ||||
|  | ||||
| @test "output is machine readable" { | ||||
|   run $ABRA app ls --machine | ||||
|  | ||||
|  | ||||
| @ -173,6 +173,8 @@ teardown(){ | ||||
|     --domain "$TEST_APP_DOMAIN" \ | ||||
|     --secrets | ||||
|   assert_success | ||||
|   assert_output --partial 'generated secrets' | ||||
|   assert_output --partial 'test_pass_one' | ||||
|   assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" | ||||
|  | ||||
|   run $ABRA app secret ls "$TEST_APP_DOMAIN" | ||||
|  | ||||
| @ -45,4 +45,5 @@ teardown(){ | ||||
|  | ||||
|   sanitisedDomainName="${TEST_APP_DOMAIN//./_}" | ||||
|   assert_output --partial "$sanitisedDomainName_app" | ||||
|   assert_output --partial "nginx" | ||||
| } | ||||
|  | ||||
| @ -107,10 +107,10 @@ teardown(){ | ||||
|  | ||||
| # bats test_tags=slow | ||||
| @test "undeploy chaos deployment" { | ||||
|   run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --chaos | ||||
|   run $ABRA app deploy "$TEST_APP_DOMAIN" \ | ||||
|     --no-input --no-converge-checks --chaos | ||||
|  | ||||
|   run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input | ||||
|   assert_success | ||||
|   _undeploy_app | ||||
|    | ||||
|   # NOTE(d1): ensure chaos undeploy | ||||
|   assert_output --partial ${_get_current_hash:0:8} | ||||
|  | ||||
| @ -1,60 +0,0 @@ | ||||
| #!/usr/bin/env bash | ||||
|  | ||||
| setup_file(){ | ||||
|   load "$PWD/tests/integration/helpers/common" | ||||
|   _common_setup | ||||
|   _add_server | ||||
|   _new_app | ||||
| } | ||||
|  | ||||
| teardown_file(){ | ||||
|   _rm_app | ||||
|   _rm_server | ||||
|   _reset_recipe | ||||
| } | ||||
|  | ||||
| setup(){ | ||||
|   load "$PWD/tests/integration/helpers/common" | ||||
|   _common_setup | ||||
|   _ensure_catalogue | ||||
| } | ||||
|  | ||||
| teardown(){ | ||||
|   _reset_recipe | ||||
|   _undeploy_app | ||||
|   _reset_app | ||||
| } | ||||
|  | ||||
| # bats test_tags=slow | ||||
| @test "undeploy version written to env" { | ||||
|   run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" \ | ||||
|     --no-input --no-converge-checks | ||||
|   assert_success | ||||
|  | ||||
|   run grep -q "TYPE=$TEST_RECIPE:0.1.0+1.20.0" \ | ||||
|     "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" | ||||
|   assert_success | ||||
|  | ||||
|   run sed -i "s/TYPE=$TEST_RECIPE:.*/TYPE=$TEST_RECIPE/g" \ | ||||
|     "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" | ||||
|   assert_success | ||||
|  | ||||
|   run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input | ||||
|   assert_success | ||||
|  | ||||
|   run grep -q "TYPE=$TEST_RECIPE:0.1.0+1.20.0" \ | ||||
|     "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" | ||||
|   assert_success | ||||
| } | ||||
|  | ||||
| # bats test_tags=slow | ||||
| @test "chaos commit written to env" { | ||||
|   run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --chaos | ||||
|  | ||||
|   run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input | ||||
|   assert_success | ||||
|  | ||||
|   run grep -q "TYPE=$TEST_RECIPE:${_get_current_hash:0:8}" \ | ||||
|     "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" | ||||
|   assert_success | ||||
| } | ||||
| @ -15,14 +15,12 @@ teardown_file(){ | ||||
| setup(){ | ||||
|   load "$PWD/tests/integration/helpers/common" | ||||
|   _common_setup | ||||
|   _ensure_catalogue | ||||
| } | ||||
|  | ||||
| teardown(){ | ||||
|   _reset_recipe | ||||
|   _reset_app | ||||
|   _undeploy_app | ||||
|   _reset_app # NOTE(d1): _undeploy_app writes version | ||||
| } | ||||
|  | ||||
| @test "validate app argument" { | ||||
|  | ||||
| @ -11,7 +11,7 @@ setup() { | ||||
|   assert_success | ||||
|   assert_not_exists "$ABRA_DIR/recipes/matrix-synapse" | ||||
|  | ||||
|   run $ABRA recipe fetch --all | ||||
|   run $ABRA recipe fetch | ||||
|   assert_success | ||||
|   assert_exists "$ABRA_DIR/recipes/matrix-synapse" | ||||
| } | ||||
| @ -25,13 +25,3 @@ setup() { | ||||
|   assert_success | ||||
|   assert_exists "$ABRA_DIR/recipes/matrix-synapse" | ||||
| } | ||||
|  | ||||
| @test "error if missing args/flags" { | ||||
|   run $ABRA recipe fetch | ||||
|   assert_failure | ||||
| } | ||||
|  | ||||
| @test "error if single recipe and --all" { | ||||
|   run $ABRA recipe fetch matrix-synapse --all | ||||
|   assert_failure | ||||
| } | ||||
|  | ||||
| @ -5,6 +5,13 @@ setup() { | ||||
|   _common_setup | ||||
| } | ||||
|  | ||||
| @test "recipe list" { | ||||
|   run $ABRA recipe list | ||||
|   assert_success | ||||
|   NUM_RECIPES=$(jq length "$ABRA_DIR/catalogue/recipes.json") | ||||
|   assert_output --partial "total recipes: $NUM_RECIPES" | ||||
| } | ||||
|  | ||||
| @test "recipe list with pattern" { | ||||
|   run $ABRA recipe list --pattern cloud | ||||
|   assert_success | ||||
|  | ||||
| @ -48,9 +48,11 @@ teardown(){ | ||||
|     --server "$TEST_SERVER" \ | ||||
|     --domain "foobar.$TEST_SERVER" | ||||
|   assert_success | ||||
|   assert_output --partial "new app 'foobar' created" | ||||
|  | ||||
|   run $ABRA app deploy "foobar.$TEST_SERVER" --no-input | ||||
|   assert_success | ||||
|   assert_output --partial 'using latest commit' | ||||
| } | ||||
|  | ||||
| @test "create new recipe with git credentials" { | ||||
|  | ||||
| @ -32,3 +32,13 @@ setup() { | ||||
|   assert_success | ||||
|   assert_output "$latestVersion" | ||||
| } | ||||
|  | ||||
| @test "app is first service listed" { | ||||
|   run bash -c '$ABRA recipe versions gitea --machine | jq -r ".[0].service" | uniq' | ||||
|   assert_success | ||||
|   assert_output 'app' | ||||
|  | ||||
|   run bash -c '$ABRA recipe versions gitea --machine | jq -r ".[1].service" | uniq' | ||||
|   assert_success | ||||
|   assert_output 'db' | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user