Compare commits

...

21 Commits

Author SHA1 Message Date
69a7d37fb7 chore: release 0.4.0-alpha-rc4 2022-01-06 10:04:43 +01:00
87649cbbd0 docs: more manual test cases [ci skip] 2022-01-05 19:37:41 +01:00
4b7ec6384c fix: fix chaos mode for deployment 2022-01-05 19:21:41 +01:00
b22b63c2ba fix: only output if volumes selected for removal 2022-01-05 19:00:09 +01:00
d9f3a11265 fix: gracefully handle missing tag for syncing 2022-01-05 18:04:46 +01:00
d7cf11b876 fix: further fixes for gracefully handling missing tag
Follows 1b37d2d5f5.
2022-01-05 17:58:15 +01:00
d7e1b2947a fix: skip failed image parse for upgrade and move on 2022-01-05 17:57:11 +01:00
1b37d2d5f5 fix: handle tags without images gracefully 2022-01-05 17:32:58 +01:00
74dfb12fd6 refactor: centralise tag meta stripping 2022-01-05 17:32:33 +01:00
49ccf2d204 fix: also show skip for non semver tags 2022-01-04 22:49:36 +01:00
76adc45431 docs: match typically log message style 2022-01-04 22:49:23 +01:00
e38a0078f3 chore: publish 0.4.0-alpha-rc3 2022-01-04 15:34:10 +01:00
25b44dc54e refactor!: use lowercase option to match others 2022-01-04 12:25:45 +01:00
0c2f6fb676 fix: app autocomplete for secret commands 2022-01-04 12:24:37 +01:00
10e4a8b97f fix: handle StackName/AppName correctly for new app creation 2022-01-04 11:56:29 +01:00
eed2756784 fix: new app table colume matches usual order now 2022-01-04 11:56:17 +01:00
b61b8f0d2a fix: always check for deployed status when removing
You can't delete regardless of -f if an app is deployed, the runtime
will error out. Best just deal with this for all cases then on our side.
2022-01-04 11:38:07 +01:00
763e7b5bff fix: use StackName for querying via Docker 2022-01-04 11:37:45 +01:00
d5ab9aedbf docs: match other abort command outputs 2022-01-04 11:37:35 +01:00
2ebb00c9d4 docs: confirm prompt matches language of command 2022-01-04 11:37:04 +01:00
6d76b3646a fix: use spaces like the rest [ci skip] 2022-01-03 18:41:11 +01:00
13 changed files with 133 additions and 92 deletions

View File

@ -39,13 +39,13 @@ var appRemoveCommand = &cli.Command{
if !internal.Force { if !internal.Force {
response := false response := false
prompt := &survey.Confirm{ 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 { if err := survey.AskOne(prompt, &response); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if !response { if !response {
logrus.Fatal("user aborted app removal") logrus.Fatal("aborting as requested")
} }
} }
@ -54,7 +54,6 @@ var appRemoveCommand = &cli.Command{
logrus.Fatal(err) logrus.Fatal(err)
} }
if !internal.Force {
isDeployed, _, err := stack.IsDeployed(c.Context, cl, app.StackName()) isDeployed, _, err := stack.IsDeployed(c.Context, cl, app.StackName())
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -62,10 +61,9 @@ var appRemoveCommand = &cli.Command{
if isDeployed { if isDeployed {
logrus.Fatalf("%s is still deployed. Run \"abra app undeploy %s \" or pass --force", app.Name, app.Name) logrus.Fatalf("%s is still deployed. Run \"abra app undeploy %s \" or pass --force", app.Name, app.Name)
} }
}
fs := filters.NewArgs() fs := filters.NewArgs()
fs.Add("name", app.Name) fs.Add("name", app.StackName())
secretList, err := cl.SecretList(c.Context, types.SecretListOptions{Filters: fs}) secretList, err := cl.SecretList(c.Context, types.SecretListOptions{Filters: fs})
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -81,6 +79,7 @@ var appRemoveCommand = &cli.Command{
if len(secrets) > 0 { if len(secrets) > 0 {
var secretNamesToRemove []string var secretNamesToRemove []string
if !internal.Force { if !internal.Force {
secretsPrompt := &survey.MultiSelect{ secretsPrompt := &survey.MultiSelect{
Message: "which secrets do you want to remove?", Message: "which secrets do you want to remove?",
@ -142,8 +141,10 @@ var appRemoveCommand = &cli.Command{
logrus.Info("no volumes were removed") logrus.Info("no volumes were removed")
} }
} else { } else {
if Volumes {
logrus.Info("no volumes to remove") logrus.Info("no volumes to remove")
} }
}
err = os.Remove(app.Path) err = os.Remove(app.Path)
if err != nil { if err != nil {

View File

@ -20,7 +20,7 @@ import (
var allSecrets bool var allSecrets bool
var allSecretsFlag = &cli.BoolFlag{ var allSecretsFlag = &cli.BoolFlag{
Name: "all", Name: "all",
Aliases: []string{"A"}, Aliases: []string{"a"},
Value: false, Value: false,
Destination: &allSecrets, Destination: &allSecrets,
Usage: "Generate all secrets", Usage: "Generate all secrets",
@ -32,6 +32,7 @@ var appSecretGenerateCommand = &cli.Command{
Usage: "Generate secrets", Usage: "Generate secrets",
ArgsUsage: "<secret> <version>", ArgsUsage: "<secret> <version>",
Flags: []cli.Flag{allSecretsFlag, internal.PassFlag}, Flags: []cli.Flag{allSecretsFlag, internal.PassFlag},
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
@ -100,6 +101,7 @@ var appSecretInsertCommand = &cli.Command{
Usage: "Insert secret", Usage: "Insert secret",
Flags: []cli.Flag{internal.PassFlag}, Flags: []cli.Flag{internal.PassFlag},
ArgsUsage: "<app> <secret-name> <version> <data>", ArgsUsage: "<app> <secret-name> <version> <data>",
BashComplete: autocomplete.AppNameComplete,
Description: ` Description: `
This command inserts a secret into an app environment. This command inserts a secret into an app environment.
@ -144,6 +146,7 @@ var appSecretRmCommand = &cli.Command{
Aliases: []string{"rm"}, Aliases: []string{"rm"},
Flags: []cli.Flag{allSecretsFlag, internal.PassFlag}, Flags: []cli.Flag{allSecretsFlag, internal.PassFlag},
ArgsUsage: "<app> <secret-name>", ArgsUsage: "<app> <secret-name>",
BashComplete: autocomplete.AppNameComplete,
Description: ` Description: `
This command removes a secret from an app environment. This command removes a secret from an app environment.

View File

@ -1,8 +1,6 @@
package app package app
import ( import (
"strings"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
@ -22,9 +20,8 @@ func getImagePath(image string) (string, error) {
} }
path := reference.Path(img) 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) logrus.Debugf("parsed %s from %s", path, image)

View File

@ -24,9 +24,11 @@ import (
func DeployAction(c *cli.Context) error { func DeployAction(c *cli.Context) error {
app := ValidateApp(c) app := ValidateApp(c)
if !Chaos {
if err := recipe.EnsureUpToDate(app.Type); err != nil { if err := recipe.EnsureUpToDate(app.Type); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
}
r, err := recipe.Get(app.Type) r, err := recipe.Get(app.Type)
if err != nil { if err != nil {

View File

@ -163,9 +163,9 @@ func NewAction(c *cli.Context) error {
NewAppServer = "local" NewAppServer = "local"
} }
tableCol := []string{"Name", "Domain", "Type", "Server"} tableCol := []string{"server", "type", "domain", "app name"}
table := formatter.CreateTable(tableCol) 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.Println(fmt.Sprintf("A new %s app has been created! Here is an overview:", recipe.Name)) 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() table.Render()
fmt.Println("") fmt.Println("")
fmt.Println("You can configure this app by running the following:") 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("")
fmt.Println("You can deploy this app by running the following:") 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("") fmt.Println("")
return nil return nil

View File

@ -2,9 +2,9 @@ package internal
import ( import (
"fmt" "fmt"
"strings"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -94,9 +94,7 @@ func GetMainAppImage(recipe recipe.Recipe) (string, error) {
} }
path = reference.Path(img) path = reference.Path(img)
if strings.Contains(path, "library") { path = recipePkg.StripTagMeta(path)
path = strings.Split(path, "/")[1]
}
return path, nil return path, nil
} }

View File

@ -127,6 +127,7 @@ your SSH keys configured on your account.
func getImageVersions(recipe recipe.Recipe) (map[string]string, error) { func getImageVersions(recipe recipe.Recipe) (map[string]string, error) {
var services = make(map[string]string) var services = make(map[string]string)
missingTag := false
for _, service := range recipe.Config.Services { for _, service := range recipe.Config.Services {
if service.Image == "" { if service.Image == "" {
continue continue
@ -138,21 +139,27 @@ func getImageVersions(recipe recipe.Recipe) (map[string]string, error) {
} }
path := reference.Path(img) path := reference.Path(img)
if strings.Contains(path, "library") {
path = strings.Split(path, "/")[1] path = recipePkg.StripTagMeta(path)
}
var tag string var tag string
switch img.(type) { switch img.(type) {
case reference.NamedTagged: case reference.NamedTagged:
tag = img.(reference.NamedTagged).Tag() tag = img.(reference.NamedTagged).Tag()
case reference.Named: case reference.Named:
return services, fmt.Errorf("%s service is missing image tag?", path) if service.Name == "app" {
missingTag = true
}
continue
} }
services[path] = tag services[path] = tag
} }
if missingTag {
return services, fmt.Errorf("app service is missing image tag?")
}
return services, nil return services, nil
} }

View File

@ -115,23 +115,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) logrus.Debugf("retrieved %s from remote registry for %s", regVersions, image)
if strings.Contains(image, "library") { image = recipePkg.StripTagMeta(image)
// ParseNormalizedNamed prepends 'library' to images like nginx:<tag>,
// postgres:<tag>, i.e. images which do not have a username in the switch img.(type) {
// first position of the string case reference.NamedTagged:
image = strings.Split(image, "/")[1]
}
semverLikeTag := true
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) { if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) {
logrus.Debugf("%s not considered semver-like", img.(reference.NamedTagged).Tag()) logrus.Debugf("%s not considered semver-like", img.(reference.NamedTagged).Tag())
semverLikeTag = false }
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()) tag, err := tagcmp.Parse(img.(reference.NamedTagged).Tag())
if err != nil && semverLikeTag { if err != nil {
logrus.Fatal(err) 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) logrus.Debugf("parsed %s for %s", tag, service.Name)
var compatible []tagcmp.Tag var compatible []tagcmp.Tag
for _, regVersion := range regVersions { for _, regVersion := range regVersions {
other, err := tagcmp.Parse(regVersion.Name) other, err := tagcmp.Parse(regVersion.Name)
@ -148,7 +151,7 @@ You may invoke this command in "wizard" mode and be prompted for input:
sort.Sort(tagcmp.ByTagDesc(compatible)) sort.Sort(tagcmp.ByTagDesc(compatible))
if len(compatible) == 0 && semverLikeTag { if len(compatible) == 0 {
logrus.Info(fmt.Sprintf("no new versions available for %s, %s is the latest", image, tag)) 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 continue // skip on to the next tag and don't update any compose files
} }
@ -188,13 +191,13 @@ You may invoke this command in "wizard" mode and be prompted for input:
} }
} }
if contains { 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 { } else {
logrus.Infof("service %s, image %s pinned to %s, no compatible upgrade found", service.Name, servicePins[service.Name].image, pinnedTagString) logrus.Infof("service %s, image %s pinned to %s, no compatible upgrade found", service.Name, servicePins[service.Name].image, pinnedTagString)
continue continue
} }
} else { } 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 continue
} }
} else { } else {
@ -211,7 +214,7 @@ You may invoke this command in "wizard" mode and be prompted for input:
} }
} }
if upgradeTag == "" { 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 continue
} }
} else { } else {
@ -220,7 +223,7 @@ You may invoke this command in "wizard" mode and be prompted for input:
tag := img.(reference.NamedTagged).Tag() tag := img.(reference.NamedTagged).Tag()
logrus.Warning(fmt.Sprintf("unable to determine versioning semantics of %s, listing all tags", tag)) 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) msg = fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
compatibleStrings = []string{} compatibleStrings = []string{"skip"}
for _, regVersion := range regVersions { for _, regVersion := range regVersions {
compatibleStrings = append(compatibleStrings, regVersion.Name) compatibleStrings = append(compatibleStrings, regVersion.Name)
} }
@ -238,10 +241,13 @@ You may invoke this command in "wizard" mode and be prompted for input:
} }
} }
if upgradeTag != "skip" { if upgradeTag != "skip" {
if err := recipe.UpdateTag(image, upgradeTag); err != nil { ok, err := recipe.UpdateTag(image, upgradeTag)
if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if ok {
logrus.Infof("tag upgraded from %s to %s for %s", tag.String(), upgradeTag, image) logrus.Infof("tag upgraded from %s to %s for %s", tag.String(), upgradeTag, image)
}
} else { } else {
logrus.Warnf("not upgrading %s, skipping as requested", image) logrus.Warnf("not upgrading %s, skipping as requested", image)
} }

View File

@ -16,10 +16,10 @@ import (
) )
// UpdateTag updates an image tag in-place on file system local compose files. // 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) composeFiles, err := filepath.Glob(pattern)
if err != nil { if err != nil {
return err return false, err
} }
logrus.Debugf("considering %s config(s) for tag update", strings.Join(composeFiles, ", ")) 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") envSamplePath := path.Join(config.RECIPES_DIR, recipeName, ".env.sample")
sampleEnv, err := config.ReadEnv(envSamplePath) sampleEnv, err := config.ReadEnv(envSamplePath)
if err != nil { if err != nil {
return err return false, err
} }
compose, err := loader.LoadComposefile(opts, sampleEnv) compose, err := loader.LoadComposefile(opts, sampleEnv)
if err != nil { if err != nil {
return err return false, err
} }
for _, service := range compose.Services { for _, service := range compose.Services {
@ -45,24 +45,26 @@ func UpdateTag(pattern, image, tag, recipeName string) error {
img, _ := reference.ParseNormalizedNamed(service.Image) img, _ := reference.ParseNormalizedNamed(service.Image)
if err != nil { 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) 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) logrus.Debugf("parsed %s from %s", composeTag, service.Image)
if image == composeImage { if image == composeImage {
bytes, err := ioutil.ReadFile(composeFile) bytes, err := ioutil.ReadFile(composeFile)
if err != nil { if err != nil {
return err return false, err
} }
old := fmt.Sprintf("%s:%s", composeImage, composeTag) 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) logrus.Debugf("updating %s to %s in %s", old, new, compose.Filename)
if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0764); err != nil { 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. // UpdateLabel updates a label in-place on file system local compose files.

View File

@ -163,12 +163,17 @@ func (r Recipe) UpdateLabel(pattern, serviceName, label string) error {
} }
// UpdateTag updates a recipe tag // 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) 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 // Tags list the recipe tags
@ -973,9 +978,8 @@ func GetRecipeVersions(recipeName, registryUsername, registryPassword string) (R
} }
path := reference.Path(img) path := reference.Path(img)
if strings.Contains(path, "library") {
path = strings.Split(path, "/")[1] path = StripTagMeta(path)
}
var tag string var tag string
switch img.(type) { switch img.(type) {
@ -1041,3 +1045,22 @@ func GetRecipeCatalogueVersions(recipeName string, catl RecipeCatalogue) ([]stri
return versions, nil 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
}

View File

@ -2,7 +2,7 @@
ABRA_VERSION="0.3.0-alpha" ABRA_VERSION="0.3.0-alpha"
ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$ABRA_VERSION" 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-rc4"
RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$RC_VERSION" RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$RC_VERSION"
for arg in "$@"; do for arg in "$@"; do

View File

@ -17,6 +17,8 @@ wire up for testing in an automated way.
## deploy, upgrade, rollback ## deploy, upgrade, rollback
- `abra app deploy <app>` - `abra app deploy <app>`
- `abra app deploy --force <app>`
- `abra app deploy --chaos <app>`
- `abra app upgrade <app>` - `abra app upgrade <app>`
- `abra app rollback <app>` - `abra app rollback <app>`