forked from toolshed/abra
		
	Compare commits
	
		
			35 Commits
		
	
	
		
			0.4.0-alph
			...
			0.4.0-alph
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| c6db9ee355 | |||
| 7733637767 | |||
| 88f9796aaf | |||
| 6cdba0f9de | |||
| 199aa5f4e3 | |||
| 9b26c24a5f | |||
| ca75654769 | |||
| fc2d83d203 | |||
| 2f4f288a46 | |||
| e98f00d354 | |||
| b4c2773b87 | |||
| 3aec5d1d7e | |||
| e0fa1b6995 | |||
| b69ab0df65 | |||
| 69a7d37fb7 | |||
| 87649cbbd0 | |||
| 4b7ec6384c | |||
| b22b63c2ba | |||
| d9f3a11265 | |||
| d7cf11b876 | |||
| d7e1b2947a | |||
| 1b37d2d5f5 | |||
| 74dfb12fd6 | |||
| 49ccf2d204 | |||
| 76adc45431 | |||
| e38a0078f3 | |||
| 25b44dc54e | |||
| 0c2f6fb676 | |||
| 10e4a8b97f | |||
| eed2756784 | |||
| b61b8f0d2a | |||
| 763e7b5bff | |||
| d5ab9aedbf | |||
| 2ebb00c9d4 | |||
| 6d76b3646a | 
| @ -66,9 +66,7 @@ We maintain a fork of [godotenv](https://github.com/Autonomic-Cooperative/godote | ||||
| 1. multi-line env var support | ||||
| 2. inline comment parsing | ||||
|  | ||||
| You can upgrade the version here by running `go get github.com/Autonomic-Cooperative/godotenv@<commit>` where `<commit>` is the | ||||
| latest commit you want to pin to. We are aiming to migrate to YAML format for the environment configuration, so this should only | ||||
| be a temporary thing. | ||||
| You can upgrade the version here by running `go get github.com/Autonomic-Cooperative/godotenv@<commit>` where `<commit>` is the latest commit you want to pin to. At time of writing, `go get github.com/Autonomic-Cooperative/godotenv@b031ea1211e7fd297af4c7747ffb562ebe00cd33` is the command you want to run to maintain the above functionality. | ||||
|  | ||||
| #### `docker/client` | ||||
|  | ||||
|  | ||||
| @ -30,10 +30,10 @@ is failing to deploy or having issues, it could be a lot of things. | ||||
|  | ||||
| This command currently takes into account: | ||||
|  | ||||
| 		Is the service deployed? | ||||
|     Is the service deployed? | ||||
|     Is the service killed by an OOM error? | ||||
| 		Is the service reporting an error (like in "ps --no-trunc" output) | ||||
| 		Is the service healthcheck failing? what are the healthcheck logs? | ||||
|     Is the service reporting an error (like in "ps --no-trunc" output) | ||||
|     Is the service healthcheck failing? what are the healthcheck logs? | ||||
|  | ||||
| Got any more ideas? Please let us know: | ||||
|  | ||||
|  | ||||
| @ -39,13 +39,13 @@ var appRemoveCommand = &cli.Command{ | ||||
| 		if !internal.Force { | ||||
| 			response := false | ||||
| 			prompt := &survey.Confirm{ | ||||
| 				Message: fmt.Sprintf("about to delete %s, are you sure?", app.Name), | ||||
| 				Message: fmt.Sprintf("about to remove %s, are you sure?", app.Name), | ||||
| 			} | ||||
| 			if err := survey.AskOne(prompt, &response); err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 			if !response { | ||||
| 				logrus.Fatal("user aborted app removal") | ||||
| 				logrus.Fatal("aborting as requested") | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| @ -54,18 +54,16 @@ var appRemoveCommand = &cli.Command{ | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		if !internal.Force { | ||||
| 			isDeployed, _, err := stack.IsDeployed(c.Context, cl, app.StackName()) | ||||
| 			if err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 			if isDeployed { | ||||
| 				logrus.Fatalf("%s is still deployed. Run \"abra app undeploy %s \" or pass --force", app.Name, app.Name) | ||||
| 			} | ||||
| 		isDeployed, _, err := stack.IsDeployed(c.Context, cl, app.StackName()) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
| 		if isDeployed { | ||||
| 			logrus.Fatalf("%s is still deployed. Run \"abra app undeploy %s \" or pass --force", app.Name, app.Name) | ||||
| 		} | ||||
|  | ||||
| 		fs := filters.NewArgs() | ||||
| 		fs.Add("name", app.Name) | ||||
| 		fs.Add("name", app.StackName()) | ||||
| 		secretList, err := cl.SecretList(c.Context, types.SecretListOptions{Filters: fs}) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| @ -81,6 +79,7 @@ var appRemoveCommand = &cli.Command{ | ||||
|  | ||||
| 		if len(secrets) > 0 { | ||||
| 			var secretNamesToRemove []string | ||||
|  | ||||
| 			if !internal.Force { | ||||
| 				secretsPrompt := &survey.MultiSelect{ | ||||
| 					Message: "which secrets do you want to remove?", | ||||
| @ -142,7 +141,9 @@ var appRemoveCommand = &cli.Command{ | ||||
| 				logrus.Info("no volumes were removed") | ||||
| 			} | ||||
| 		} else { | ||||
| 			logrus.Info("no volumes to remove") | ||||
| 			if Volumes { | ||||
| 				logrus.Info("no volumes to remove") | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		err = os.Remove(app.Path) | ||||
|  | ||||
| @ -45,8 +45,10 @@ recipes. | ||||
| 		app := internal.ValidateApp(c) | ||||
| 		stackName := app.StackName() | ||||
|  | ||||
| 		if err := recipe.EnsureUpToDate(app.Type); err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		if !internal.Chaos { | ||||
| 			if err := recipe.EnsureUpToDate(app.Type); err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		r, err := recipe.Get(app.Type) | ||||
|  | ||||
| @ -20,18 +20,19 @@ import ( | ||||
| var allSecrets bool | ||||
| var allSecretsFlag = &cli.BoolFlag{ | ||||
| 	Name:        "all", | ||||
| 	Aliases:     []string{"A"}, | ||||
| 	Aliases:     []string{"a"}, | ||||
| 	Value:       false, | ||||
| 	Destination: &allSecrets, | ||||
| 	Usage:       "Generate all secrets", | ||||
| } | ||||
|  | ||||
| var appSecretGenerateCommand = &cli.Command{ | ||||
| 	Name:      "generate", | ||||
| 	Aliases:   []string{"g"}, | ||||
| 	Usage:     "Generate secrets", | ||||
| 	ArgsUsage: "<secret> <version>", | ||||
| 	Flags:     []cli.Flag{allSecretsFlag, internal.PassFlag}, | ||||
| 	Name:         "generate", | ||||
| 	Aliases:      []string{"g"}, | ||||
| 	Usage:        "Generate secrets", | ||||
| 	ArgsUsage:    "<secret> <version>", | ||||
| 	Flags:        []cli.Flag{allSecretsFlag, internal.PassFlag}, | ||||
| 	BashComplete: autocomplete.AppNameComplete, | ||||
| 	Action: func(c *cli.Context) error { | ||||
| 		app := internal.ValidateApp(c) | ||||
|  | ||||
| @ -95,11 +96,12 @@ var appSecretGenerateCommand = &cli.Command{ | ||||
| } | ||||
|  | ||||
| var appSecretInsertCommand = &cli.Command{ | ||||
| 	Name:      "insert", | ||||
| 	Aliases:   []string{"i"}, | ||||
| 	Usage:     "Insert secret", | ||||
| 	Flags:     []cli.Flag{internal.PassFlag}, | ||||
| 	ArgsUsage: "<app> <secret-name> <version> <data>", | ||||
| 	Name:         "insert", | ||||
| 	Aliases:      []string{"i"}, | ||||
| 	Usage:        "Insert secret", | ||||
| 	Flags:        []cli.Flag{internal.PassFlag}, | ||||
| 	ArgsUsage:    "<app> <secret-name> <version> <data>", | ||||
| 	BashComplete: autocomplete.AppNameComplete, | ||||
| 	Description: ` | ||||
| This command inserts a secret into an app environment. | ||||
|  | ||||
| @ -139,11 +141,12 @@ Example: | ||||
| } | ||||
|  | ||||
| var appSecretRmCommand = &cli.Command{ | ||||
| 	Name:      "remove", | ||||
| 	Usage:     "Remove a secret", | ||||
| 	Aliases:   []string{"rm"}, | ||||
| 	Flags:     []cli.Flag{allSecretsFlag, internal.PassFlag}, | ||||
| 	ArgsUsage: "<app> <secret-name>", | ||||
| 	Name:         "remove", | ||||
| 	Usage:        "Remove a secret", | ||||
| 	Aliases:      []string{"rm"}, | ||||
| 	Flags:        []cli.Flag{allSecretsFlag, internal.PassFlag}, | ||||
| 	ArgsUsage:    "<app> <secret-name>", | ||||
| 	BashComplete: autocomplete.AppNameComplete, | ||||
| 	Description: ` | ||||
| This command removes a secret from an app environment. | ||||
|  | ||||
| @ -183,20 +186,29 @@ Example: | ||||
| 				if err := cl.SecretRemove(c.Context, secretName); err != nil { | ||||
| 					logrus.Fatal(err) | ||||
| 				} | ||||
| 				logrus.Infof("deleted %s successfully from server", secretName) | ||||
|  | ||||
| 				if internal.Pass { | ||||
| 					if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil { | ||||
| 						logrus.Fatal(err) | ||||
| 					} | ||||
|  | ||||
| 					logrus.Infof("deleted %s successfully from local pass store", secretName) | ||||
| 				} | ||||
| 			} else { | ||||
| 				if parsed == secretToRm { | ||||
| 					if err := cl.SecretRemove(c.Context, secretName); err != nil { | ||||
| 						logrus.Fatal(err) | ||||
| 					} | ||||
|  | ||||
| 					logrus.Infof("deleted %s successfully from server", secretName) | ||||
|  | ||||
| 					if internal.Pass { | ||||
| 						if err := secret.PassRmSecret(parsed, app.StackName(), app.Server); err != nil { | ||||
| 							logrus.Fatal(err) | ||||
| 						} | ||||
|  | ||||
| 						logrus.Infof("deleted %s successfully from local pass store", secretName) | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| @ -48,8 +48,10 @@ recipes. | ||||
| 		app := internal.ValidateApp(c) | ||||
| 		stackName := app.StackName() | ||||
|  | ||||
| 		if err := recipe.EnsureUpToDate(app.Type); err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		if !internal.Chaos { | ||||
| 			if err := recipe.EnsureUpToDate(app.Type); err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		r, err := recipe.Get(app.Type) | ||||
|  | ||||
| @ -1,8 +1,6 @@ | ||||
| package app | ||||
|  | ||||
| import ( | ||||
| 	"strings" | ||||
|  | ||||
| 	"coopcloud.tech/abra/cli/internal" | ||||
| 	"coopcloud.tech/abra/pkg/autocomplete" | ||||
| 	"coopcloud.tech/abra/pkg/client" | ||||
| @ -22,9 +20,8 @@ func getImagePath(image string) (string, error) { | ||||
| 	} | ||||
|  | ||||
| 	path := reference.Path(img) | ||||
| 	if strings.Contains(path, "library") { | ||||
| 		path = strings.Split(path, "/")[1] | ||||
| 	} | ||||
|  | ||||
| 	path = recipe.StripTagMeta(path) | ||||
|  | ||||
| 	logrus.Debugf("parsed %s from %s", path, image) | ||||
|  | ||||
|  | ||||
| @ -33,6 +33,7 @@ var CatalogueSkipList = map[string]bool{ | ||||
| 	"auto-mirror":           true, | ||||
| 	"backup-bot":            true, | ||||
| 	"backup-bot-two":        true, | ||||
| 	"beta.coopcloud.tech":   true, | ||||
| 	"comrade-renovate-bot":  true, | ||||
| 	"coopcloud.tech":        true, | ||||
| 	"coturn":                true, | ||||
| @ -93,12 +94,6 @@ keys configured on your account. | ||||
| 			internal.ValidateRecipe(c) | ||||
| 		} | ||||
|  | ||||
| 		catalogueDir := path.Join(config.ABRA_DIR, "catalogue") | ||||
| 		url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, "recipes") | ||||
| 		if err := gitPkg.Clone(catalogueDir, url); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		repos, err := recipe.ReadReposMetadata() | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
|  | ||||
| @ -24,8 +24,10 @@ import ( | ||||
| func DeployAction(c *cli.Context) error { | ||||
| 	app := ValidateApp(c) | ||||
|  | ||||
| 	if err := recipe.EnsureUpToDate(app.Type); err != nil { | ||||
| 		logrus.Fatal(err) | ||||
| 	if !Chaos { | ||||
| 		if err := recipe.EnsureUpToDate(app.Type); err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	r, err := recipe.Get(app.Type) | ||||
|  | ||||
| @ -438,6 +438,15 @@ var RegistryPasswordFlag = &cli.StringFlag{ | ||||
| 	Destination: &RegistryUsername, | ||||
| } | ||||
|  | ||||
| var AllTags bool | ||||
| var AllTagsFlag = &cli.BoolFlag{ | ||||
| 	Name:        "all-tags", | ||||
| 	Aliases:     []string{"a"}, | ||||
| 	Value:       false, | ||||
| 	Usage:       "List all tags, not just upgrades", | ||||
| 	Destination: &AllTags, | ||||
| } | ||||
|  | ||||
| // SSHFailMsg is a hopefully helpful SSH failure message | ||||
| var SSHFailMsg = ` | ||||
| Woops, Abra is unable to connect to connect to %s. | ||||
|  | ||||
| @ -23,7 +23,7 @@ var RecipeName string | ||||
|  | ||||
| // createSecrets creates all secrets for a new app. | ||||
| func createSecrets(sanitisedAppName string) (AppSecrets, error) { | ||||
| 	appEnvPath := path.Join(config.ABRA_DIR, "servers", NewAppServer, fmt.Sprintf("%s.env", sanitisedAppName)) | ||||
| 	appEnvPath := path.Join(config.ABRA_DIR, "servers", NewAppServer, fmt.Sprintf("%s.env", NewAppName)) | ||||
| 	appEnv, err := config.ReadEnv(appEnvPath) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| @ -163,9 +163,9 @@ func NewAction(c *cli.Context) error { | ||||
| 		NewAppServer = "local" | ||||
| 	} | ||||
|  | ||||
| 	tableCol := []string{"Name", "Domain", "Type", "Server"} | ||||
| 	tableCol := []string{"server", "type", "domain", "app name"} | ||||
| 	table := formatter.CreateTable(tableCol) | ||||
| 	table.Append([]string{sanitisedAppName, Domain, recipe.Name, NewAppServer}) | ||||
| 	table.Append([]string{NewAppServer, recipe.Name, Domain, NewAppName}) | ||||
|  | ||||
| 	fmt.Println("") | ||||
| 	fmt.Println(fmt.Sprintf("A new %s app has been created! Here is an overview:", recipe.Name)) | ||||
| @ -173,10 +173,10 @@ func NewAction(c *cli.Context) error { | ||||
| 	table.Render() | ||||
| 	fmt.Println("") | ||||
| 	fmt.Println("You can configure this app by running the following:") | ||||
| 	fmt.Println(fmt.Sprintf("\n    abra app config %s", sanitisedAppName)) | ||||
| 	fmt.Println(fmt.Sprintf("\n    abra app config %s", NewAppName)) | ||||
| 	fmt.Println("") | ||||
| 	fmt.Println("You can deploy this app by running the following:") | ||||
| 	fmt.Println(fmt.Sprintf("\n    abra app deploy %s", sanitisedAppName)) | ||||
| 	fmt.Println(fmt.Sprintf("\n    abra app deploy %s", NewAppName)) | ||||
| 	fmt.Println("") | ||||
|  | ||||
| 	return nil | ||||
|  | ||||
| @ -2,9 +2,9 @@ package internal | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
|  | ||||
| 	"coopcloud.tech/abra/pkg/recipe" | ||||
| 	recipePkg "coopcloud.tech/abra/pkg/recipe" | ||||
| 	"github.com/AlecAivazis/survey/v2" | ||||
| 	"github.com/docker/distribution/reference" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| @ -94,9 +94,7 @@ func GetMainAppImage(recipe recipe.Recipe) (string, error) { | ||||
| 			} | ||||
|  | ||||
| 			path = reference.Path(img) | ||||
| 			if strings.Contains(path, "library") { | ||||
| 				path = strings.Split(path, "/")[1] | ||||
| 			} | ||||
| 			path = recipePkg.StripTagMeta(path) | ||||
|  | ||||
| 			return path, nil | ||||
| 		} | ||||
|  | ||||
| @ -2,14 +2,11 @@ package recipe | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"path" | ||||
| 	"sort" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
|  | ||||
| 	"coopcloud.tech/abra/pkg/config" | ||||
| 	"coopcloud.tech/abra/pkg/formatter" | ||||
| 	gitPkg "coopcloud.tech/abra/pkg/git" | ||||
| 	"coopcloud.tech/abra/pkg/recipe" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/urfave/cli/v2" | ||||
| @ -32,12 +29,6 @@ var recipeListCommand = &cli.Command{ | ||||
| 		patternFlag, | ||||
| 	}, | ||||
| 	Action: func(c *cli.Context) error { | ||||
| 		catalogueDir := path.Join(config.ABRA_DIR, "catalogue") | ||||
| 		url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, "recipes") | ||||
| 		if err := gitPkg.Clone(catalogueDir, url); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		catl, err := recipe.ReadRecipeCatalogue() | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err.Error()) | ||||
|  | ||||
| @ -127,6 +127,7 @@ your SSH keys configured on your account. | ||||
| func getImageVersions(recipe recipe.Recipe) (map[string]string, error) { | ||||
| 	var services = make(map[string]string) | ||||
|  | ||||
| 	missingTag := false | ||||
| 	for _, service := range recipe.Config.Services { | ||||
| 		if service.Image == "" { | ||||
| 			continue | ||||
| @ -138,21 +139,27 @@ func getImageVersions(recipe recipe.Recipe) (map[string]string, error) { | ||||
| 		} | ||||
|  | ||||
| 		path := reference.Path(img) | ||||
| 		if strings.Contains(path, "library") { | ||||
| 			path = strings.Split(path, "/")[1] | ||||
| 		} | ||||
|  | ||||
| 		path = recipePkg.StripTagMeta(path) | ||||
|  | ||||
| 		var tag string | ||||
| 		switch img.(type) { | ||||
| 		case reference.NamedTagged: | ||||
| 			tag = img.(reference.NamedTagged).Tag() | ||||
| 		case reference.Named: | ||||
| 			return services, fmt.Errorf("%s service is missing image tag?", path) | ||||
| 			if service.Name == "app" { | ||||
| 				missingTag = true | ||||
| 			} | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		services[path] = tag | ||||
| 	} | ||||
|  | ||||
| 	if missingTag { | ||||
| 		return services, fmt.Errorf("app service is missing image tag?") | ||||
| 	} | ||||
|  | ||||
| 	return services, nil | ||||
| } | ||||
|  | ||||
| @ -232,12 +239,10 @@ func commitRelease(recipe recipe.Recipe, tag string) error { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if internal.Publish { | ||||
| 		msg := fmt.Sprintf("chore: publish %s release", tag) | ||||
| 		repoPath := path.Join(config.RECIPES_DIR, recipe.Name) | ||||
| 		if err := gitPkg.Commit(repoPath, "compose.**yml", msg, internal.Dry); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	msg := fmt.Sprintf("chore: publish %s release", tag) | ||||
| 	repoPath := path.Join(config.RECIPES_DIR, recipe.Name) | ||||
| 	if err := gitPkg.Commit(repoPath, ".", msg, internal.Dry); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| @ -290,13 +295,10 @@ func pushRelease(recipe recipe.Recipe, tagString string) error { | ||||
| 		if err := recipe.Push(internal.Dry); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		if !internal.Dry { | ||||
| 			url := fmt.Sprintf("%s/%s/src/tag/%s", config.REPOS_BASE_URL, recipe.Name, tagString) | ||||
| 			logrus.Infof("new release published: %s", url) | ||||
| 		} else { | ||||
| 			logrus.Info("dry run: no changes published") | ||||
| 		} | ||||
| 		url := fmt.Sprintf("%s/%s/src/tag/%s", config.REPOS_BASE_URL, recipe.Name, tagString) | ||||
| 		logrus.Infof("new release published: %s", url) | ||||
| 	} else { | ||||
| 		logrus.Info("no -p/--publish passed, not publishing") | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
|  | ||||
| @ -53,6 +53,7 @@ You may invoke this command in "wizard" mode and be prompted for input: | ||||
| 		internal.PatchFlag, | ||||
| 		internal.MinorFlag, | ||||
| 		internal.MajorFlag, | ||||
| 		internal.AllTagsFlag, | ||||
| 	}, | ||||
| 	Action: func(c *cli.Context) error { | ||||
| 		recipe := internal.ValidateRecipeWithPrompt(c) | ||||
| @ -115,23 +116,26 @@ You may invoke this command in "wizard" mode and be prompted for input: | ||||
| 			} | ||||
| 			logrus.Debugf("retrieved %s from remote registry for %s", regVersions, image) | ||||
|  | ||||
| 			if strings.Contains(image, "library") { | ||||
| 				// ParseNormalizedNamed prepends 'library' to images like nginx:<tag>, | ||||
| 				// postgres:<tag>, i.e. images which do not have a username in the | ||||
| 				// first position of the string | ||||
| 				image = strings.Split(image, "/")[1] | ||||
| 			} | ||||
| 			semverLikeTag := true | ||||
| 			if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) { | ||||
| 				logrus.Debugf("%s not considered semver-like", img.(reference.NamedTagged).Tag()) | ||||
| 				semverLikeTag = false | ||||
| 			image = recipePkg.StripTagMeta(image) | ||||
|  | ||||
| 			switch img.(type) { | ||||
| 			case reference.NamedTagged: | ||||
| 				if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) { | ||||
| 					logrus.Debugf("%s not considered semver-like", img.(reference.NamedTagged).Tag()) | ||||
| 				} | ||||
| 			default: | ||||
| 				logrus.Warnf("unable to read tag for image %s, is it missing? skipping upgrade for %s", image, service.Name) | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			tag, err := tagcmp.Parse(img.(reference.NamedTagged).Tag()) | ||||
| 			if err != nil && semverLikeTag { | ||||
| 				logrus.Fatal(err) | ||||
| 			if err != nil { | ||||
| 				logrus.Warnf("unable to parse %s, error was: %s, skipping upgrade for %s", image, err.Error(), service.Name) | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			logrus.Debugf("parsed %s for %s", tag, service.Name) | ||||
|  | ||||
| 			var compatible []tagcmp.Tag | ||||
| 			for _, regVersion := range regVersions { | ||||
| 				other, err := tagcmp.Parse(regVersion.Name) | ||||
| @ -148,7 +152,7 @@ You may invoke this command in "wizard" mode and be prompted for input: | ||||
|  | ||||
| 			sort.Sort(tagcmp.ByTagDesc(compatible)) | ||||
|  | ||||
| 			if len(compatible) == 0 && semverLikeTag { | ||||
| 			if len(compatible) == 0 && !internal.AllTags { | ||||
| 				logrus.Info(fmt.Sprintf("no new versions available for %s, %s is the latest", image, tag)) | ||||
| 				continue // skip on to the next tag and don't update any compose files | ||||
| 			} | ||||
| @ -188,13 +192,13 @@ You may invoke this command in "wizard" mode and be prompted for input: | ||||
| 						} | ||||
| 					} | ||||
| 					if contains { | ||||
| 						logrus.Infof("Upgrading service %s from %s to %s (pinned tag: %s)", service.Name, tag.String(), upgradeTag, pinnedTagString) | ||||
| 						logrus.Infof("upgrading service %s from %s to %s (pinned tag: %s)", service.Name, tag.String(), upgradeTag, pinnedTagString) | ||||
| 					} else { | ||||
| 						logrus.Infof("service %s, image %s pinned to %s, no compatible upgrade found", service.Name, servicePins[service.Name].image, pinnedTagString) | ||||
| 						continue | ||||
| 					} | ||||
| 				} else { | ||||
| 					logrus.Fatalf("Service %s is at version %s, but pinned to %s, please correct your compose.yml file manually!", service.Name, tag.String(), pinnedTag.String()) | ||||
| 					logrus.Fatalf("service %s is at version %s, but pinned to %s, please correct your compose.yml file manually!", service.Name, tag.String(), pinnedTag.String()) | ||||
| 					continue | ||||
| 				} | ||||
| 			} else { | ||||
| @ -211,16 +215,18 @@ You may invoke this command in "wizard" mode and be prompted for input: | ||||
| 						} | ||||
| 					} | ||||
| 					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) | ||||
| 						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, image: %s, tag: %s)", service.Name, image, tag) | ||||
| 					if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) { | ||||
| 					if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) || internal.AllTags { | ||||
| 						tag := img.(reference.NamedTagged).Tag() | ||||
| 						logrus.Warning(fmt.Sprintf("unable to determine versioning semantics of %s, listing all tags", tag)) | ||||
| 						if !internal.AllTags { | ||||
| 							logrus.Warning(fmt.Sprintf("unable to determine versioning semantics of %s, listing all tags", tag)) | ||||
| 						} | ||||
| 						msg = fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag) | ||||
| 						compatibleStrings = []string{} | ||||
| 						compatibleStrings = []string{"skip"} | ||||
| 						for _, regVersion := range regVersions { | ||||
| 							compatibleStrings = append(compatibleStrings, regVersion.Name) | ||||
| 						} | ||||
| @ -238,10 +244,13 @@ You may invoke this command in "wizard" mode and be prompted for input: | ||||
| 				} | ||||
| 			} | ||||
| 			if upgradeTag != "skip" { | ||||
| 				if err := recipe.UpdateTag(image, upgradeTag); err != nil { | ||||
| 				ok, err := recipe.UpdateTag(image, upgradeTag) | ||||
| 				if err != nil { | ||||
| 					logrus.Fatal(err) | ||||
| 				} | ||||
| 				logrus.Infof("tag upgraded from %s to %s for %s", tag.String(), upgradeTag, image) | ||||
| 				if ok { | ||||
| 					logrus.Infof("tag upgraded from %s to %s for %s", tag.String(), upgradeTag, image) | ||||
| 				} | ||||
| 			} else { | ||||
| 				logrus.Warnf("not upgrading %s, skipping as requested", image) | ||||
| 			} | ||||
|  | ||||
| @ -1,14 +1,9 @@ | ||||
| package recipe | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"path" | ||||
|  | ||||
| 	"coopcloud.tech/abra/cli/internal" | ||||
| 	"coopcloud.tech/abra/pkg/autocomplete" | ||||
| 	"coopcloud.tech/abra/pkg/config" | ||||
| 	"coopcloud.tech/abra/pkg/formatter" | ||||
| 	gitPkg "coopcloud.tech/abra/pkg/git" | ||||
| 	recipePkg "coopcloud.tech/abra/pkg/recipe" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/urfave/cli/v2" | ||||
| @ -23,12 +18,6 @@ var recipeVersionCommand = &cli.Command{ | ||||
| 	Action: func(c *cli.Context) error { | ||||
| 		recipe := internal.ValidateRecipe(c) | ||||
|  | ||||
| 		catalogueDir := path.Join(config.ABRA_DIR, "catalogue") | ||||
| 		url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, "recipes") | ||||
| 		if err := gitPkg.Clone(catalogueDir, url); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		catalogue, err := recipePkg.ReadRecipeCatalogue() | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
|  | ||||
							
								
								
									
										4
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.mod
									
									
									
									
									
								
							| @ -5,7 +5,7 @@ go 1.16 | ||||
| require ( | ||||
| 	coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52 | ||||
| 	github.com/AlecAivazis/survey/v2 v2.3.2 | ||||
| 	github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731170023-c37c0920d1a4 | ||||
| 	github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731094149-b031ea1211e7 | ||||
| 	github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4 | ||||
| 	github.com/docker/cli v20.10.12+incompatible | ||||
| 	github.com/docker/distribution v2.7.1+incompatible | ||||
| @ -21,7 +21,7 @@ require ( | ||||
| 	github.com/schultz-is/passgen v1.0.1 | ||||
| 	github.com/sirupsen/logrus v1.8.1 | ||||
| 	github.com/urfave/cli/v2 v2.3.0 | ||||
| 	gotest.tools/v3 v3.0.3 | ||||
| 	gotest.tools/v3 v3.1.0 | ||||
| ) | ||||
|  | ||||
| require ( | ||||
|  | ||||
							
								
								
									
										8
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								go.sum
									
									
									
									
									
								
							| @ -28,8 +28,8 @@ coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52/go.mod h1:ESVm0wQKcbcFi | ||||
| dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= | ||||
| github.com/AlecAivazis/survey/v2 v2.3.2 h1:TqTB+aDDCLYhf9/bD2TwSO8u8jDSmMUd2SUVO4gCnU8= | ||||
| github.com/AlecAivazis/survey/v2 v2.3.2/go.mod h1:TH2kPCDU3Kqq7pLbnCWwZXDBjnhZtmsCle5EiYDJ2fg= | ||||
| github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731170023-c37c0920d1a4 h1:aYUdiI42a4fWfPoUr25XlaJrFEICv24+o/gWhqYS/jk= | ||||
| github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731170023-c37c0920d1a4/go.mod h1:oZRCMMRS318l07ei4DTqbZoOawfJlJ4yyo8juk2v4Rk= | ||||
| github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731094149-b031ea1211e7 h1:asQtdXYbxEYWcwAQqJTVYC/RltB4eqoWKvqWg/LFPOg= | ||||
| github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731094149-b031ea1211e7/go.mod h1:oZRCMMRS318l07ei4DTqbZoOawfJlJ4yyo8juk2v4Rk= | ||||
| github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= | ||||
| github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= | ||||
| github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= | ||||
| @ -1045,6 +1045,7 @@ golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapK | ||||
| golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= | ||||
| golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= | ||||
| golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= | ||||
| golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= | ||||
| golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| @ -1160,8 +1161,9 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C | ||||
| gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= | ||||
| gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= | ||||
| gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= | ||||
| gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= | ||||
| gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= | ||||
| gotest.tools/v3 v3.1.0 h1:rVV8Tcg/8jHUkPUorwjaMTtemIMVXfIPKiOqnhEhakk= | ||||
| gotest.tools/v3 v3.1.0/go.mod h1:fHy7eyTmJFO5bQbUsEGQ1v4m2J3Jz9eWL54TP2/ZuYQ= | ||||
| honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | ||||
| honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | ||||
| honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | ||||
|  | ||||
| @ -16,10 +16,10 @@ import ( | ||||
| ) | ||||
|  | ||||
| // UpdateTag updates an image tag in-place on file system local compose files. | ||||
| func UpdateTag(pattern, image, tag, recipeName string) error { | ||||
| func UpdateTag(pattern, image, tag, recipeName string) (bool, error) { | ||||
| 	composeFiles, err := filepath.Glob(pattern) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 		return false, err | ||||
| 	} | ||||
|  | ||||
| 	logrus.Debugf("considering %s config(s) for tag update", strings.Join(composeFiles, ", ")) | ||||
| @ -30,12 +30,12 @@ func UpdateTag(pattern, image, tag, recipeName string) error { | ||||
| 		envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample") | ||||
| 		sampleEnv, err := config.ReadEnv(envSamplePath) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 			return false, err | ||||
| 		} | ||||
|  | ||||
| 		compose, err := loader.LoadComposefile(opts, sampleEnv) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 			return false, err | ||||
| 		} | ||||
|  | ||||
| 		for _, service := range compose.Services { | ||||
| @ -45,24 +45,26 @@ func UpdateTag(pattern, image, tag, recipeName string) error { | ||||
|  | ||||
| 			img, _ := reference.ParseNormalizedNamed(service.Image) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 				return false, err | ||||
| 			} | ||||
|  | ||||
| 			var composeTag string | ||||
| 			switch img.(type) { | ||||
| 			case reference.NamedTagged: | ||||
| 				composeTag = img.(reference.NamedTagged).Tag() | ||||
| 			default: | ||||
| 				// unable to parse, typically image missing tag | ||||
| 				return false, nil | ||||
| 			} | ||||
|  | ||||
| 			composeImage := reference.Path(img) | ||||
| 			if strings.Contains(composeImage, "library") { | ||||
| 				// ParseNormalizedNamed prepends 'library' to images like nginx:<tag>, | ||||
| 				// postgres:<tag>, i.e. images which do not have a username in the | ||||
| 				// first position of the string | ||||
| 				composeImage = strings.Split(composeImage, "/")[1] | ||||
| 			} | ||||
| 			composeTag := img.(reference.NamedTagged).Tag() | ||||
|  | ||||
| 			logrus.Debugf("parsed %s from %s", composeTag, service.Image) | ||||
|  | ||||
| 			if image == composeImage { | ||||
| 				bytes, err := ioutil.ReadFile(composeFile) | ||||
| 				if err != nil { | ||||
| 					return err | ||||
| 					return false, err | ||||
| 				} | ||||
|  | ||||
| 				old := fmt.Sprintf("%s:%s", composeImage, composeTag) | ||||
| @ -72,13 +74,13 @@ func UpdateTag(pattern, image, tag, recipeName string) error { | ||||
| 				logrus.Debugf("updating %s to %s in %s", old, new, compose.Filename) | ||||
|  | ||||
| 				if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0764); err != nil { | ||||
| 					return err | ||||
| 					return true, err | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| 	return false, nil | ||||
| } | ||||
|  | ||||
| // UpdateLabel updates a label in-place on file system local compose files. | ||||
|  | ||||
| @ -153,7 +153,7 @@ func LoadAppFiles(servers ...string) (AppFiles, error) { | ||||
| 		serverDir := path.Join(SERVERS_DIR, server) | ||||
| 		files, err := getAllFilesInDirectory(serverDir) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 			return nil, fmt.Errorf("server %s doesn't exist? Run \"abra server ls\" to check", server) | ||||
| 		} | ||||
| 		for _, file := range files { | ||||
| 			appName := strings.TrimSuffix(file.Name(), ".env") | ||||
|  | ||||
| @ -163,12 +163,17 @@ func (r Recipe) UpdateLabel(pattern, serviceName, label string) error { | ||||
| } | ||||
|  | ||||
| // UpdateTag updates a recipe tag | ||||
| func (r Recipe) UpdateTag(image, tag string) error { | ||||
| func (r Recipe) UpdateTag(image, tag string) (bool, error) { | ||||
| 	pattern := fmt.Sprintf("%s/%s/compose**yml", config.RECIPES_DIR, r.Name) | ||||
| 	if err := compose.UpdateTag(pattern, image, tag, r.Name); err != nil { | ||||
| 		return err | ||||
|  | ||||
| 	image = StripTagMeta(image) | ||||
|  | ||||
| 	ok, err := compose.UpdateTag(pattern, image, tag, r.Name) | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
| 	return nil | ||||
|  | ||||
| 	return ok, nil | ||||
| } | ||||
|  | ||||
| // Tags list the recipe tags | ||||
| @ -693,6 +698,10 @@ func recipeCatalogueFSIsLatest() (bool, error) { | ||||
| func ReadRecipeCatalogue() (RecipeCatalogue, error) { | ||||
| 	recipes := make(RecipeCatalogue) | ||||
|  | ||||
| 	if err := EnsureCatalogue(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	recipeFSIsLatest, err := recipeCatalogueFSIsLatest() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| @ -973,9 +982,8 @@ func GetRecipeVersions(recipeName, registryUsername, registryPassword string) (R | ||||
| 			} | ||||
|  | ||||
| 			path := reference.Path(img) | ||||
| 			if strings.Contains(path, "library") { | ||||
| 				path = strings.Split(path, "/")[1] | ||||
| 			} | ||||
|  | ||||
| 			path = StripTagMeta(path) | ||||
|  | ||||
| 			var tag string | ||||
| 			switch img.(type) { | ||||
| @ -1041,3 +1049,37 @@ func GetRecipeCatalogueVersions(recipeName string, catl RecipeCatalogue) ([]stri | ||||
|  | ||||
| 	return versions, nil | ||||
| } | ||||
|  | ||||
| // StripTagMeta strips front-matter image tag data that we don't need for parsing. | ||||
| func StripTagMeta(image string) string { | ||||
| 	originalImage := image | ||||
|  | ||||
| 	if strings.Contains(image, "docker.io") { | ||||
| 		image = strings.Split(image, "/")[1] | ||||
| 	} | ||||
|  | ||||
| 	if strings.Contains(image, "library") { | ||||
| 		image = strings.Split(image, "/")[1] | ||||
| 	} | ||||
|  | ||||
| 	if originalImage != image { | ||||
| 		logrus.Debugf("stripped %s to %s for parsing", originalImage, image) | ||||
| 	} | ||||
|  | ||||
| 	return image | ||||
| } | ||||
|  | ||||
| // EnsureCatalogue ensures that the catalogue is cloned locally & present. | ||||
| func EnsureCatalogue() error { | ||||
| 	catalogueDir := path.Join(config.ABRA_DIR, "catalogue") | ||||
| 	if _, err := os.Stat(catalogueDir); err != nil && os.IsNotExist(err) { | ||||
| 		url := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, "recipes") | ||||
| 		if err := gitPkg.Clone(catalogueDir, url); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		logrus.Debugf("cloned catalogue repository to %s", catalogueDir) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| @ -2,7 +2,7 @@ | ||||
|  | ||||
| ABRA_VERSION="0.3.0-alpha" | ||||
| ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$ABRA_VERSION" | ||||
| RC_VERSION="0.4.0-alpha-rc2" | ||||
| RC_VERSION="0.4.0-alpha-rc5" | ||||
| RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$RC_VERSION" | ||||
|  | ||||
| for arg in "$@"; do | ||||
|  | ||||
| @ -17,6 +17,8 @@ wire up for testing in an automated way. | ||||
| ## deploy, upgrade, rollback | ||||
|  | ||||
| - `abra app deploy <app>` | ||||
| - `abra app deploy --force <app>` | ||||
| - `abra app deploy --chaos <app>` | ||||
| - `abra app upgrade <app>` | ||||
| - `abra app rollback <app>` | ||||
|  | ||||
| @ -45,6 +47,7 @@ wire up for testing in an automated way. | ||||
| - `abra app run <app>` | ||||
| - `abra app secret ls <app>` | ||||
| - `abra app volume ls <app>` | ||||
| - `abra app new --secrets <recipe>` | ||||
|  | ||||
| ### hard mode | ||||
|  | ||||
|  | ||||
		Reference in New Issue
	
	Block a user