Compare commits

..

1 Commits

Author SHA1 Message Date
3wc
aa344fc94f wip: login to docker to avoid rate limiting 2021-11-29 13:19:04 +02:00
67 changed files with 1426 additions and 1889 deletions

View File

@ -4,3 +4,5 @@ go env -w GOPRIVATE=coopcloud.tech
# export HCLOUD_TOKEN=$(pass show logins/hetzner/cicd/api_key) # export HCLOUD_TOKEN=$(pass show logins/hetzner/cicd/api_key)
# export CAPSUL_TOKEN=... # export CAPSUL_TOKEN=...
# export GITEA_TOKEN=... # export GITEA_TOKEN=...
# export DOCKER_USERNAME=...
# export DOCKER_PASSWORD=...

View File

@ -35,6 +35,5 @@ to scaling apps up and spinning them down.
appSecretCommand, appSecretCommand,
appVolumeCommand, appVolumeCommand,
appVersionCommand, appVersionCommand,
appErrorsCommand,
}, },
} }

View File

@ -10,7 +10,6 @@ import (
"strings" "strings"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -73,5 +72,16 @@ var appBackupCommand = &cli.Command{
return nil return nil
}, },
BashComplete: autocomplete.AppNameComplete, BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
} }

View File

@ -1,12 +1,12 @@
package app package app
import ( import (
"fmt"
"os" "os"
"path" "path"
"strings" "strings"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -49,5 +49,16 @@ var appCheckCommand = &cli.Command{
return nil return nil
}, },
BashComplete: autocomplete.AppNameComplete, BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
} }

View File

@ -2,11 +2,11 @@ package app
import ( import (
"errors" "errors"
"fmt"
"os" "os"
"os/exec" "os/exec"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -55,5 +55,16 @@ var appConfigCommand = &cli.Command{
return nil return nil
}, },
BashComplete: autocomplete.AppNameComplete, BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
} }

View File

@ -1,11 +1,12 @@
package app package app
import ( import (
"fmt"
"os" "os"
"strings" "strings"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/config"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -82,5 +83,16 @@ And if you want to copy that file back to your current working directory locally
return nil return nil
}, },
BashComplete: autocomplete.AppNameComplete, BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
} }

View File

@ -1,8 +1,11 @@
package app package app
import ( import (
"fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/config"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -13,7 +16,6 @@ var appDeployCommand = &cli.Command{
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.ForceFlag, internal.ForceFlag,
internal.ChaosFlag, internal.ChaosFlag,
internal.NoDomainChecksFlag,
}, },
Description: ` Description: `
This command deploys a new instance of an app. It does not support changing the This command deploys a new instance of an app. It does not support changing the
@ -27,6 +29,17 @@ Chas mode ("--chaos") will deploy your local checkout of a recipe as-is,
including unstaged changes and can be useful for live hacking and testing new including unstaged changes and can be useful for live hacking and testing new
recipes. recipes.
`, `,
Action: internal.DeployAction, Action: internal.DeployAction,
BashComplete: autocomplete.AppNameComplete, BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
} }

View File

@ -1,27 +0,0 @@
package app
import (
"coopcloud.tech/abra/pkg/autocomplete"
"github.com/urfave/cli/v2"
)
var appErrorsCommand = &cli.Command{
Name: "errors",
Usage: "List errors for a deployed app",
Description: `
This command will list errors for a deployed app. This is a best-effort
implementation and an attempt to gather a number of tips & tricks for finding
errors together into one convenient command. When an app is failing to deploy
or having issues, it could be a lot of things. This command is best accompanied
by "abra app logs <app>".
`,
Aliases: []string{"e"},
Flags: []cli.Flag{},
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error {
// TODO: entrypoint error
// TODO: ps --no-trunc errors
// TODO: failing healthcheck
return nil
},
}

View File

@ -42,25 +42,6 @@ var listAppServerFlag = &cli.StringFlag{
Destination: &listAppServer, Destination: &listAppServer,
} }
type appStatus struct {
server string
recipe string
appName string
domain string
status string
version string
upgrade string
}
type serverStatus struct {
apps []appStatus
appCount int
versionCount int
unversionedCount int
latestCount int
upgradeCount int
}
var appListCommand = &cli.Command{ var appListCommand = &cli.Command{
Name: "list", Name: "list",
Usage: "List all managed apps", Usage: "List all managed apps",
@ -88,50 +69,51 @@ can take some time.
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
sort.Sort(config.ByServerAndType(apps)) sort.Sort(config.ByServerAndType(apps))
statuses := make(map[string]map[string]string) alreadySeen := make(map[string]bool)
var catl catalogue.RecipeCatalogue for _, app := range apps {
if status { if _, ok := alreadySeen[app.Server]; !ok {
alreadySeen := make(map[string]bool) if err := ssh.EnsureHostKey(app.Server); err != nil {
for _, app := range apps { logrus.Fatal(fmt.Sprintf(internal.SSHFailMsg, app.Server))
if _, ok := alreadySeen[app.Server]; !ok {
if err := ssh.EnsureHostKey(app.Server); err != nil {
logrus.Fatal(fmt.Sprintf(internal.SSHFailMsg, app.Server))
}
alreadySeen[app.Server] = true
} }
alreadySeen[app.Server] = true
} }
}
statuses := make(map[string]map[string]string)
tableCol := []string{"Server", "Type", "App Name", "Domain"}
if status {
tableCol = append(tableCol, "Status", "Version", "Updates")
statuses, err = config.GetAppStatuses(appFiles) statuses, err = config.GetAppStatuses(appFiles)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
var err error
catl, err = catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Fatal(err)
}
} }
var totalServersCount int table := abraFormatter.CreateTable(tableCol)
var totalAppsCount int table.SetAutoMergeCellsByColumnIndex([]int{0})
allStats := make(map[string]serverStatus)
var (
versionedAppsCount int
unversionedAppsCount int
onLatestCount int
canUpgradeCount int
)
catl, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Fatal(err)
}
var appsCount int
for _, app := range apps { for _, app := range apps {
var stats serverStatus var tableRow []string
var ok bool
if stats, ok = allStats[app.Server]; !ok {
stats = serverStatus{}
totalServersCount++
}
if app.Type == appType || appType == "" { if app.Type == appType || appType == "" {
appStats := appStatus{} appsCount++
stats.appCount++
totalAppsCount++
// If type flag is set, check for it, if not, Type == ""
tableRow = []string{app.Server, app.Type, app.StackName(), app.Domain}
if status { if status {
stackName := app.StackName() stackName := app.StackName()
status := "unknown" status := "unknown"
@ -143,16 +125,16 @@ can take some time.
if statusMeta["status"] != "" { if statusMeta["status"] != "" {
status = statusMeta["status"] status = statusMeta["status"]
} }
stats.versionCount++ tableRow = append(tableRow, status, version)
versionedAppsCount++
} else { } else {
stats.unversionedCount++ tableRow = append(tableRow, status, version)
unversionedAppsCount++
} }
appStats.status = status
appStats.version = version
var newUpdates []string var newUpdates []string
if version != "unknown" { if version != "unknown" {
updates, err := catalogue.GetRecipeCatalogueVersions(app.Type, catl) updates, err := catalogue.GetRecipeCatalogueVersions(app.Type, catl)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -177,72 +159,40 @@ can take some time.
if len(newUpdates) == 0 { if len(newUpdates) == 0 {
if version == "unknown" { if version == "unknown" {
appStats.upgrade = "unknown" tableRow = append(tableRow, "unknown")
} else { } else {
appStats.upgrade = "latest" tableRow = append(tableRow, "on latest")
stats.latestCount++ onLatestCount++
} }
} else { } else {
// FIXME: jeezus golang why do you not have a list reverse function // FIXME: jeezus golang why do you not have a list reverse function
for i, j := 0, len(newUpdates)-1; i < j; i, j = i+1, j-1 { for i, j := 0, len(newUpdates)-1; i < j; i, j = i+1, j-1 {
newUpdates[i], newUpdates[j] = newUpdates[j], newUpdates[i] newUpdates[i], newUpdates[j] = newUpdates[j], newUpdates[i]
} }
appStats.upgrade = strings.Join(newUpdates, "\n") tableRow = append(tableRow, strings.Join(newUpdates, "\n"))
stats.upgradeCount++ canUpgradeCount++
} }
} }
appStats.server = app.Server
appStats.recipe = app.Type
appStats.appName = app.StackName()
appStats.domain = app.Domain
stats.apps = append(stats.apps, appStats)
} }
table.Append(tableRow)
allStats[app.Server] = stats
} }
for serverName, serverStat := range allStats { var stats string
tableCol := []string{"recipe", "app name", "domain"} if status {
if status { stats = fmt.Sprintf(
tableCol = append(tableCol, []string{"status", "version", "upgrade"}...) "Total apps: %v | Versioned: %v | Unversioned: %v | On latest: %v | Can upgrade: %v",
} appsCount,
versionedAppsCount,
table := abraFormatter.CreateTable(tableCol) unversionedAppsCount,
onLatestCount,
for _, appStat := range serverStat.apps { canUpgradeCount,
tableRow := []string{appStat.recipe, appStat.appName, appStat.domain} )
if status { } else {
tableRow = append(tableRow, []string{appStat.status, appStat.version, appStat.upgrade}...) stats = fmt.Sprintf("Total apps: %v", appsCount)
}
table.Append(tableRow)
}
table.Render()
if status {
fmt.Println(fmt.Sprintf(
"server: %s | total apps: %v | versioned: %v | unversioned: %v | latest: %v | upgrade: %v",
serverName,
serverStat.appCount,
serverStat.versionCount,
serverStat.unversionedCount,
serverStat.latestCount,
serverStat.upgradeCount,
))
} else {
fmt.Println(fmt.Sprintf("server: %s | total apps: %v", serverName, serverStat.appCount))
}
if len(allStats) > 1 {
fmt.Println() // newline separator for multiple servers
}
} }
if len(allStats) > 1 { table.SetCaption(true, stats)
fmt.Println(fmt.Sprintf("total servers: %v | total apps: %v ", totalServersCount, totalAppsCount)) table.Render()
}
return nil return nil
}, },

View File

@ -7,7 +7,6 @@ import (
"sync" "sync"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
@ -17,15 +16,6 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
var logOpts = types.ContainerLogsOptions{
Details: false,
Follow: true,
ShowStderr: true,
ShowStdout: true,
Tail: "20",
Timestamps: true,
}
// stackLogs lists logs for all stack services // stackLogs lists logs for all stack services
func stackLogs(c *cli.Context, stackName string, client *dockerClient.Client) { func stackLogs(c *cli.Context, stackName string, client *dockerClient.Client) {
filters := filters.NewArgs() filters := filters.NewArgs()
@ -40,10 +30,14 @@ func stackLogs(c *cli.Context, stackName string, client *dockerClient.Client) {
for _, service := range services { for _, service := range services {
wg.Add(1) wg.Add(1)
go func(s string) { go func(s string) {
if internal.StdErrOnly { logOpts := types.ContainerLogsOptions{
logOpts.ShowStdout = false Details: true,
Follow: true,
ShowStderr: true,
ShowStdout: true,
Tail: "20",
Timestamps: true,
} }
logs, err := client.ServiceLogs(c.Context, s, logOpts) logs, err := client.ServiceLogs(c.Context, s, logOpts)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -66,10 +60,6 @@ var appLogsCommand = &cli.Command{
Aliases: []string{"l"}, Aliases: []string{"l"},
ArgsUsage: "[<service>]", ArgsUsage: "[<service>]",
Usage: "Tail app logs", Usage: "Tail app logs",
Flags: []cli.Flag{
internal.StdErrOnlyFlag,
},
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
@ -80,47 +70,55 @@ var appLogsCommand = &cli.Command{
serviceName := c.Args().Get(1) serviceName := c.Args().Get(1)
if serviceName == "" { if serviceName == "" {
logrus.Debugf("tailing logs for all %s services", app.Type) logrus.Debug("tailing logs for all app services")
stackLogs(c, app.StackName(), cl) stackLogs(c, app.StackName(), cl)
} else { }
logrus.Debugf("tailing logs for %s", serviceName) logrus.Debugf("tailing logs for '%s'", serviceName)
if err := tailServiceLogs(c, cl, app, serviceName); err != nil {
logrus.Fatal(err) service := fmt.Sprintf("%s_%s", app.StackName(), serviceName)
} filters := filters.NewArgs()
filters.Add("name", service)
serviceOpts := types.ServiceListOptions{Filters: filters}
services, err := cl.ServiceList(c.Context, serviceOpts)
if err != nil {
logrus.Fatal(err)
}
if len(services) != 1 {
logrus.Fatalf("expected 1 service but got %v", len(services))
}
logOpts := types.ContainerLogsOptions{
Details: true,
Follow: true,
ShowStderr: true,
ShowStdout: true,
Tail: "20",
Timestamps: true,
}
logs, err := cl.ServiceLogs(c.Context, services[0].ID, logOpts)
if err != nil {
logrus.Fatal(err)
}
// defer after err check as any err returns a nil io.ReadCloser
defer logs.Close()
_, err = io.Copy(os.Stdout, logs)
if err != nil && err != io.EOF {
logrus.Fatal(err)
} }
return nil return nil
}, },
} BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
func tailServiceLogs(c *cli.Context, cl *dockerClient.Client, app config.App, serviceName string) error { if err != nil {
service := fmt.Sprintf("%s_%s", app.StackName(), serviceName) logrus.Warn(err)
filters := filters.NewArgs() }
filters.Add("name", service) if c.NArg() > 0 {
serviceOpts := types.ServiceListOptions{Filters: filters} return
services, err := cl.ServiceList(c.Context, serviceOpts) }
if err != nil { for _, a := range appNames {
logrus.Fatal(err) fmt.Println(a)
} }
if len(services) != 1 { },
logrus.Fatalf("expected 1 service but got %v", len(services))
}
if internal.StdErrOnly {
logOpts.ShowStdout = false
}
logs, err := cl.ServiceLogs(c.Context, services[0].ID, logOpts)
if err != nil {
logrus.Fatal(err)
}
// defer after err check as any err returns a nil io.ReadCloser
defer logs.Close()
_, err = io.Copy(os.Stdout, logs)
if err != nil && err != io.EOF {
logrus.Fatal(err)
}
return nil
} }

View File

@ -1,8 +1,11 @@
package app package app
import ( import (
"fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/catalogue"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -38,7 +41,18 @@ var appNewCommand = &cli.Command{
internal.PassFlag, internal.PassFlag,
internal.SecretsFlag, internal.SecretsFlag,
}, },
ArgsUsage: "<recipe>", ArgsUsage: "<recipe>",
Action: internal.NewAction, Action: internal.NewAction,
BashComplete: autocomplete.RecipeNameComplete, BashComplete: func(c *cli.Context) {
catl, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for name := range catl {
fmt.Println(name)
}
},
} }

View File

@ -1,13 +1,14 @@
package app package app
import ( import (
"fmt"
"strings" "strings"
"time" "time"
abraFormatter "coopcloud.tech/abra/cli/formatter" abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
@ -32,7 +33,6 @@ var appPsCommand = &cli.Command{
Flags: []cli.Flag{ Flags: []cli.Flag{
watchFlag, watchFlag,
}, },
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
if !watch { if !watch {
showPSOutput(c) showPSOutput(c)
@ -45,6 +45,18 @@ var appPsCommand = &cli.Command{
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
} }
}, },
BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
} }
// showPSOutput renders ps output. // showPSOutput renders ps output.
@ -64,7 +76,7 @@ func showPSOutput(c *cli.Context) {
logrus.Fatal(err) logrus.Fatal(err)
} }
tableCol := []string{"image", "created", "status", "ports"} tableCol := []string{"image", "created", "status", "ports", "app name", "services"}
table := abraFormatter.CreateTable(tableCol) table := abraFormatter.CreateTable(tableCol)
for _, container := range containers { for _, container := range containers {
@ -79,6 +91,8 @@ func showPSOutput(c *cli.Context) {
abraFormatter.HumanDuration(container.Created), abraFormatter.HumanDuration(container.Created),
container.Status, container.Status,
formatter.DisplayablePorts(container.Ports), formatter.DisplayablePorts(container.Ports),
app.StackName(),
strings.Join(containerNames, "\n"),
} }
table.Append(tableRow) table.Append(tableRow)
} }

View File

@ -5,9 +5,8 @@ import (
"os" "os"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
stack "coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/abra/pkg/config"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
@ -49,18 +48,23 @@ var appRemoveCommand = &cli.Command{
} }
} }
cl, err := client.New(app.Server) appFiles, err := config.LoadAppFiles("")
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
cl, err := client.New(app.Server)
if err != nil {
logrus.Fatal(err)
}
if !internal.Force { if !internal.Force {
isDeployed, _, err := stack.IsDeployed(c.Context, cl, app.StackName()) // FIXME: only query for app we are interested in, not all of them!
statuses, err := config.GetAppStatuses(appFiles)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if isDeployed { if statuses[app.Name]["status"] == "deployed" {
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 %s undeploy\" or pass --force", app.Name, app.Name)
} }
} }
@ -84,8 +88,6 @@ var appRemoveCommand = &cli.Command{
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?",
Help: "'x' indicates selected, enter / return to confirm, ctrl-c to exit, vim mode is enabled",
VimMode: true,
Options: secretNames, Options: secretNames,
Default: secretNames, Default: secretNames,
} }
@ -122,8 +124,6 @@ var appRemoveCommand = &cli.Command{
if !internal.Force { if !internal.Force {
volumesPrompt := &survey.MultiSelect{ volumesPrompt := &survey.MultiSelect{
Message: "which volumes do you want to remove?", Message: "which volumes do you want to remove?",
Help: "'x' indicates selected, enter / return to confirm, ctrl-c to exit, vim mode is enabled",
VimMode: true,
Options: vols, Options: vols,
Default: vols, Default: vols,
} }
@ -153,5 +153,16 @@ var appRemoveCommand = &cli.Command{
return nil return nil
}, },
BashComplete: autocomplete.AppNameComplete, BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
} }

View File

@ -6,9 +6,9 @@ import (
"time" "time"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
containerPkg "coopcloud.tech/abra/pkg/container" "coopcloud.tech/abra/pkg/config"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -37,16 +37,19 @@ var appRestartCommand = &cli.Command{
serviceFilter := fmt.Sprintf("%s_%s", app.StackName(), serviceName) serviceFilter := fmt.Sprintf("%s_%s", app.StackName(), serviceName)
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("name", serviceFilter) filters.Add("name", serviceFilter)
containerOpts := types.ContainerListOptions{Filters: filters}
targetContainer, err := containerPkg.GetContainer(c.Context, cl, filters, true) containers, err := cl.ContainerList(c.Context, containerOpts)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if len(containers) != 1 {
logrus.Fatalf("expected 1 service but got %v", len(containers))
}
logrus.Debugf("attempting to restart %s", serviceFilter) logrus.Debugf("attempting to restart %s", serviceFilter)
timeout := 30 * time.Second timeout := 30 * time.Second
if err := cl.ContainerRestart(c.Context, targetContainer.ID, &timeout); err != nil { if err := cl.ContainerRestart(c.Context, containers[0].ID, &timeout); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -54,5 +57,16 @@ var appRestartCommand = &cli.Command{
return nil return nil
}, },
BashComplete: autocomplete.AppNameComplete, BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
} }

View File

@ -3,7 +3,6 @@ package app
import ( import (
"fmt" "fmt"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/catalogue" "coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
@ -39,7 +38,18 @@ Chas mode ("--chaos") will deploy your local checkout of a recipe as-is,
including unstaged changes and can be useful for live hacking and testing new including unstaged changes and can be useful for live hacking and testing new
recipes. recipes.
`, `,
BashComplete: autocomplete.AppNameComplete, BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
stackName := app.StackName() stackName := app.StackName()
@ -93,8 +103,7 @@ recipes.
} }
if len(availableDowngrades) == 0 { if len(availableDowngrades) == 0 {
logrus.Info("no available downgrades, you're on oldest ✌️") logrus.Fatal("no available downgrades, you're on latest")
return nil
} }
} }
@ -164,7 +173,7 @@ recipes.
} }
} }
if err := stack.RunDeploy(cl, deployOpts, compose, app.Type); err != nil { if err := stack.RunDeploy(cl, deployOpts, compose); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

View File

@ -7,7 +7,6 @@ import (
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
containerPkg "coopcloud.tech/abra/pkg/container"
"coopcloud.tech/abra/pkg/upstream/container" "coopcloud.tech/abra/pkg/upstream/container"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
@ -60,11 +59,18 @@ var appRunCommand = &cli.Command{
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("name", stackAndServiceName) filters.Add("name", stackAndServiceName)
targetContainer, err := containerPkg.GetContainer(c.Context, cl, filters, true) containers, err := cl.ContainerList(c.Context, types.ContainerListOptions{Filters: filters})
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if len(containers) == 0 {
logrus.Fatalf("no containers matching '%s' found?", stackAndServiceName)
}
if len(containers) > 1 {
logrus.Fatalf("expected 1 container matching '%s' but got %d", stackAndServiceName, len(containers))
}
cmd := c.Args().Slice()[2:] cmd := c.Args().Slice()[2:]
execCreateOpts := types.ExecConfig{ execCreateOpts := types.ExecConfig{
AttachStderr: true, AttachStderr: true,
@ -92,7 +98,7 @@ var appRunCommand = &cli.Command{
logrus.Fatal(err) logrus.Fatal(err)
} }
if err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil { if err := container.RunExec(dcli, cl, containers[0].ID, &execCreateOpts); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

View File

@ -8,8 +8,8 @@ import (
abraFormatter "coopcloud.tech/abra/cli/formatter" abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/secret" "coopcloud.tech/abra/pkg/secret"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
@ -252,7 +252,18 @@ var appSecretLsCommand = &cli.Command{
table.Render() table.Render()
return nil return nil
}, },
BashComplete: autocomplete.AppNameComplete, BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
} }
var appSecretCommand = &cli.Command{ var appSecretCommand = &cli.Command{

View File

@ -1,9 +1,11 @@
package app package app
import ( import (
"fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
stack "coopcloud.tech/abra/pkg/upstream/stack" stack "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -49,5 +51,16 @@ volumes as eligiblef or pruning once undeployed.
return nil return nil
}, },
BashComplete: autocomplete.AppNameComplete, BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
} }

View File

@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/catalogue" "coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
@ -24,7 +23,6 @@ var appUpgradeCommand = &cli.Command{
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.ForceFlag, internal.ForceFlag,
internal.ChaosFlag, internal.ChaosFlag,
internal.NoDomainChecksFlag,
}, },
Description: ` Description: `
This command supports upgrading an app. You can use it to choose and roll out a This command supports upgrading an app. You can use it to choose and roll out a
@ -53,7 +51,7 @@ recipes.
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Debugf("checking whether %s is already deployed", stackName) logrus.Debugf("checking whether '%s' is already deployed", stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName) isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName)
if err != nil { if err != nil {
@ -61,7 +59,7 @@ recipes.
} }
if !isDeployed { if !isDeployed {
logrus.Fatalf("%s is not deployed?", app.Name) logrus.Fatalf("'%s' is not deployed?", app.Name)
} }
catl, err := catalogue.ReadRecipeCatalogue() catl, err := catalogue.ReadRecipeCatalogue()
@ -75,14 +73,14 @@ recipes.
} }
if len(versions) == 0 && !internal.Chaos { if len(versions) == 0 && !internal.Chaos {
logrus.Fatalf("no published releases for %s in the recipe catalogue?", app.Type) logrus.Fatalf("no versions available '%s' in recipe catalogue?", app.Type)
} }
var availableUpgrades []string var availableUpgrades []string
if deployedVersion == "" { if deployedVersion == "" {
deployedVersion = "unknown" deployedVersion = "unknown"
availableUpgrades = versions availableUpgrades = versions
logrus.Warnf("failed to determine version of deployed %s", app.Name) logrus.Warnf("failed to determine version of deployed '%s'", app.Name)
} }
if deployedVersion != "unknown" && !internal.Chaos { if deployedVersion != "unknown" && !internal.Chaos {
@ -101,8 +99,8 @@ recipes.
} }
if len(availableUpgrades) == 0 && !internal.Force { if len(availableUpgrades) == 0 && !internal.Force {
logrus.Info("no available upgrades, you're on latest ✌️") logrus.Fatal("no available upgrades, you're on latest")
return nil availableUpgrades = versions
} }
} }
@ -110,10 +108,10 @@ recipes.
if len(availableUpgrades) > 0 && !internal.Chaos { if len(availableUpgrades) > 0 && !internal.Chaos {
if internal.Force { if internal.Force {
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1] chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
logrus.Debugf("choosing %s as version to upgrade to", chosenUpgrade) logrus.Debugf("choosing '%s' as version to upgrade to", chosenUpgrade)
} else { } else {
prompt := &survey.Select{ prompt := &survey.Select{
Message: fmt.Sprintf("Please select an upgrade (current version: %s):", deployedVersion), Message: fmt.Sprintf("Please select an upgrade (current version: '%s'):", deployedVersion),
Options: availableUpgrades, Options: availableUpgrades,
} }
if err := survey.AskOne(prompt, &chosenUpgrade); err != nil { if err := survey.AskOne(prompt, &chosenUpgrade); err != nil {
@ -165,11 +163,22 @@ recipes.
logrus.Fatal(err) logrus.Fatal(err)
} }
if err := stack.RunDeploy(cl, deployOpts, compose, app.Type); err != nil { if err := stack.RunDeploy(cl, deployOpts, compose); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
return nil return nil
}, },
BashComplete: autocomplete.AppNameComplete, BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
} }

View File

@ -1,13 +1,14 @@
package app package app
import ( import (
"fmt"
"strings" "strings"
abraFormatter "coopcloud.tech/abra/cli/formatter" abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/catalogue" "coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/upstream/stack" "coopcloud.tech/abra/pkg/upstream/stack"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -20,14 +21,11 @@ func getImagePath(image string) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
path := reference.Path(img) path := reference.Path(img)
if strings.Contains(path, "library") { if strings.Contains(path, "library") {
path = strings.Split(path, "/")[1] path = strings.Split(path, "/")[1]
} }
logrus.Debugf("parsed '%s' from '%s'", path, image)
logrus.Debugf("parsed %s from %s", path, image)
return path, nil return path, nil
} }
@ -49,7 +47,7 @@ Cloud recipe version.
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Debugf("checking whether %s is already deployed", stackName) logrus.Debugf("checking whether '%s' is already deployed", stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName) isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName)
if err != nil { if err != nil {
@ -57,11 +55,11 @@ Cloud recipe version.
} }
if deployedVersion == "" { if deployedVersion == "" {
logrus.Fatalf("failed to determine version of deployed %s", app.Name) logrus.Fatalf("failed to determine version of deployed '%s'", app.Name)
} }
if !isDeployed { if !isDeployed {
logrus.Fatalf("%s is not deployed?", app.Name) logrus.Fatalf("'%s' is not deployed?", app.Name)
} }
recipeMeta, err := catalogue.GetRecipeMeta(app.Type) recipeMeta, err := catalogue.GetRecipeMeta(app.Type)
@ -77,20 +75,30 @@ Cloud recipe version.
} }
if len(versionsMeta) == 0 { if len(versionsMeta) == 0 {
logrus.Fatalf("could not retrieve deployed version (%s) from recipe catalogue?", deployedVersion) logrus.Fatalf("PANIC: could not retrieve deployed version ('%s') from recipe catalogue?", deployedVersion)
} }
tableCol := []string{"version", "service", "image", "digest"} tableCol := []string{"name", "image", "version", "tag", "digest"}
table := abraFormatter.CreateTable(tableCol) table := abraFormatter.CreateTable(tableCol)
table.SetAutoMergeCellsByColumnIndex([]int{0})
for serviceName, versionMeta := range versionsMeta { for serviceName, versionMeta := range versionsMeta {
table.Append([]string{deployedVersion, serviceName, versionMeta.Image, versionMeta.Digest}) table.Append([]string{serviceName, versionMeta.Image, deployedVersion, versionMeta.Tag, versionMeta.Digest})
} }
table.Render() table.Render()
return nil return nil
}, },
BashComplete: autocomplete.AppNameComplete, BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
} }

View File

@ -1,20 +1,21 @@
package app package app
import ( import (
"fmt"
abraFormatter "coopcloud.tech/abra/cli/formatter" abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
var appVolumeListCommand = &cli.Command{ var appVolumeListCommand = &cli.Command{
Name: "list", Name: "list",
Usage: "List volumes associated with an app", Usage: "List volumes associated with an app",
Aliases: []string{"ls"}, Aliases: []string{"ls"},
BashComplete: autocomplete.AppNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
app := internal.ValidateApp(c) app := internal.ValidateApp(c)
@ -41,19 +42,8 @@ var appVolumeListCommand = &cli.Command{
} }
var appVolumeRemoveCommand = &cli.Command{ var appVolumeRemoveCommand = &cli.Command{
Name: "remove", Name: "remove",
Usage: "Remove volume(s) associated with an app", Usage: "Remove volume(s) associated with an app",
Description: `
This command supports removing volumes associated with an app. The app in
question must be undeployed before you try to remove volumes. See "abra app
undeploy <app>" for more.
The command is interactive and will show a multiple select input which allows
you to make a seclection. Use the "?" key to see more help on navigating this
interface.
Passing "--force" will select all volumes for removal. Be careful.
`,
Aliases: []string{"rm"}, Aliases: []string{"rm"},
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.ForceFlag, internal.ForceFlag,
@ -71,8 +61,6 @@ Passing "--force" will select all volumes for removal. Be careful.
if !internal.Force { if !internal.Force {
volumesPrompt := &survey.MultiSelect{ volumesPrompt := &survey.MultiSelect{
Message: "which volumes do you want to remove?", Message: "which volumes do you want to remove?",
Help: "'x' indicates selected, enter / return to confirm, ctrl-c to exit, vim mode is enabled",
VimMode: true,
Options: volumeNames, Options: volumeNames,
Default: volumeNames, Default: volumeNames,
} }
@ -92,7 +80,18 @@ Passing "--force" will select all volumes for removal. Be careful.
return nil return nil
}, },
BashComplete: autocomplete.AppNameComplete, BashComplete: func(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
},
} }
var appVolumeCommand = &cli.Command{ var appVolumeCommand = &cli.Command{

View File

@ -3,16 +3,43 @@ package cli
import ( import (
"errors" "errors"
"fmt" "fmt"
"io"
"net/http"
"os" "os"
"path" "path"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/web"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
// downloadFile downloads a file brah
func downloadFile(filepath string, url string) (err error) {
out, err := os.Create(filepath)
if err != nil {
return err
}
defer out.Close()
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("bad status: %s", resp.Status)
}
_, err = io.Copy(out, resp.Body)
if err != nil {
return err
}
return nil
}
// AutoCompleteCommand helps people set up auto-complete in their shells // AutoCompleteCommand helps people set up auto-complete in their shells
var AutoCompleteCommand = &cli.Command{ var AutoCompleteCommand = &cli.Command{
Name: "autocomplete", Name: "autocomplete",
@ -32,7 +59,6 @@ Supported shells are as follows:
fizsh fizsh
zsh zsh
bash bash
`, `,
ArgsUsage: "<shell>", ArgsUsage: "<shell>",
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
@ -57,18 +83,18 @@ Supported shells are as follows:
} }
autocompletionDir := path.Join(config.ABRA_DIR, "autocompletion") autocompletionDir := path.Join(config.ABRA_DIR, "autocompletion")
if err := os.Mkdir(autocompletionDir, 0764); err != nil { if err := os.Mkdir(autocompletionDir, 0755); err != nil {
if !os.IsExist(err) { if !os.IsExist(err) {
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Debugf("%s already created", autocompletionDir) logrus.Debugf("'%s' already created, moving on...", autocompletionDir)
} }
autocompletionFile := path.Join(config.ABRA_DIR, "autocompletion", shellType) autocompletionFile := path.Join(config.ABRA_DIR, "autocompletion", shellType)
if _, err := os.Stat(autocompletionFile); err != nil && os.IsNotExist(err) { if _, err := os.Stat(autocompletionFile); err != nil && os.IsNotExist(err) {
url := fmt.Sprintf("https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/autocomplete/%s", shellType) url := fmt.Sprintf("https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/autocomplete/%s", shellType)
logrus.Infof("fetching %s", url) logrus.Infof("fetching %s", url)
if err := web.GetFile(autocompletionFile, url); err != nil { if err := downloadFile(autocompletionFile, url); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
@ -77,10 +103,9 @@ Supported shells are as follows:
case "bash": case "bash":
fmt.Println(fmt.Sprintf(` fmt.Println(fmt.Sprintf(`
# Run the following commands to install autocompletion # Run the following commands to install autocompletion
sudo mkdir /etc/bash_completion.d/ sudo mkdir /etc/bash/completion.d/
sudo cp %s /etc/bash_completion.d/abra sudo cp %s /etc/bash_completion.d/abra
echo "source /etc/bash_completion.d/abra" >> ~/.bashrc echo "source /etc/bash/completion.d/abra" >> ~/.bashrc
# And finally run "abra app ps <hit tab key>" to test things are working, you should see app names listed!
`, autocompletionFile)) `, autocompletionFile))
case "zsh": case "zsh":
fmt.Println(fmt.Sprintf(` fmt.Println(fmt.Sprintf(`
@ -88,7 +113,6 @@ echo "source /etc/bash_completion.d/abra" >> ~/.bashrc
sudo mkdir /etc/zsh/completion.d/ sudo mkdir /etc/zsh/completion.d/
sudo cp %s /etc/zsh/completion.d/abra sudo cp %s /etc/zsh/completion.d/abra
echo "PROG=abra\n_CLI_ZSH_AUTOCOMPLETE_HACK=1\nsource /etc/zsh/completion.d/abra" >> ~/.zshrc echo "PROG=abra\n_CLI_ZSH_AUTOCOMPLETE_HACK=1\nsource /etc/zsh/completion.d/abra" >> ~/.zshrc
# And finally run "abra app ps <hit tab key>" to test things are working, you should see app names listed!
`, autocompletionFile)) `, autocompletionFile))
} }

View File

@ -1,255 +1,13 @@
package catalogue package catalogue
import ( import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/config"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/limit"
"github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
// CatalogueSkipList is all the repos that are not recipes.
var CatalogueSkipList = map[string]bool{
"abra": true,
"abra-apps": true,
"abra-aur": true,
"abra-bash": true,
"abra-capsul": true,
"abra-gandi": true,
"abra-hetzner": true,
"apps": true,
"aur-abra-git": true,
"auto-apps-json": true,
"auto-mirror": true,
"backup-bot": true,
"backup-bot-two": true,
"comrade-renovate-bot": true,
"coopcloud.tech": true,
"coturn": true,
"docker-cp-deploy": true,
"docker-dind-bats-kcov": true,
"docs.coopcloud.tech": true,
"drone-abra": true,
"example": true,
"gardening": true,
"go-abra": true,
"organising": true,
"outline-with-patch": true,
"pyabra": true,
"radicle-seed-node": true,
"recipes": true,
"stack-ssh-deploy": true,
"swarm-cronjob": true,
"tagcmp": true,
"traefik-cert-dumper": true,
"tyop": true,
}
var catalogueGenerateCommand = &cli.Command{
Name: "generate",
Aliases: []string{"g"},
Usage: "Generate a new copy of the catalogue",
Flags: []cli.Flag{
internal.PushFlag,
internal.CommitFlag,
internal.CommitMessageFlag,
internal.DryFlag,
},
Description: `
This command generates a new copy of the recipe catalogue which can be found on:
https://recipes.coopcloud.tech
It polls the entire git.coopcloud.tech/coop-cloud/... recipe repository
listing, parses README and tags to produce recipe metadata and produces a
apps.json file which is placed in your ~/.abra/catalogue/recipes.json.
It is possible to generate new metadata for a single recipe by passing
<recipe>. The existing local catalogue will be updated, not overwritten.
A new catalogue copy can be published to the recipes repository by passing the
"--commit" and "--push" flags. The recipes repository is available here:
https://git.coopcloud.tech/coop-cloud/recipes
`,
ArgsUsage: "[<recipe>]",
Action: func(c *cli.Context) error {
recipeName := c.Args().First()
if recipeName != "" {
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 := catalogue.ReadReposMetadata()
if err != nil {
logrus.Fatal(err)
}
logrus.Debugf("ensuring '%v' recipe(s) are locally present and up-to-date", len(repos))
var barLength int
if recipeName != "" {
barLength = 1
} else {
barLength = len(repos)
}
cloneLimiter := limit.New(10)
retrieveBar := formatter.CreateProgressbar(len(repos), "retrieving recipes from recipes.coopcloud.tech...")
ch := make(chan string, barLength)
for _, repoMeta := range repos {
go func(rm catalogue.RepoMeta) {
cloneLimiter.Begin()
defer cloneLimiter.End()
if recipeName != "" && recipeName != rm.Name {
ch <- rm.Name
retrieveBar.Add(1)
return
}
if _, exists := CatalogueSkipList[rm.Name]; exists {
ch <- rm.Name
retrieveBar.Add(1)
return
}
recipeDir := path.Join(config.ABRA_DIR, "apps", rm.Name)
if err := gitPkg.Clone(recipeDir, rm.SSHURL); err != nil {
logrus.Fatal(err)
}
isClean, err := gitPkg.IsClean(rm.Name)
if err != nil {
logrus.Fatal(err)
}
if !isClean {
logrus.Fatalf("'%s' has locally unstaged changes", rm.Name)
}
if err := gitPkg.EnsureUpToDate(recipeDir); err != nil {
logrus.Fatal(err)
}
ch <- rm.Name
retrieveBar.Add(1)
}(repoMeta)
}
for range repos {
<-ch // wait for everything
}
catl := make(catalogue.RecipeCatalogue)
catlBar := formatter.CreateProgressbar(barLength, "generating catalogue metadata...")
for _, recipeMeta := range repos {
if recipeName != "" && recipeName != recipeMeta.Name {
catlBar.Add(1)
continue
}
if _, exists := CatalogueSkipList[recipeMeta.Name]; exists {
catlBar.Add(1)
continue
}
versions, err := catalogue.GetRecipeVersions(recipeMeta.Name)
if err != nil {
logrus.Fatal(err)
}
features, category, err := catalogue.GetRecipeFeaturesAndCategory(recipeMeta.Name)
if err != nil {
logrus.Warn(err)
}
catl[recipeMeta.Name] = catalogue.RecipeMeta{
Name: recipeMeta.Name,
Repository: recipeMeta.CloneURL,
Icon: recipeMeta.AvatarURL,
DefaultBranch: recipeMeta.DefaultBranch,
Description: recipeMeta.Description,
Website: recipeMeta.Website,
Versions: versions,
Category: category,
Features: features,
}
catlBar.Add(1)
}
recipesJSON, err := json.MarshalIndent(catl, "", " ")
if err != nil {
logrus.Fatal(err)
}
if _, err := os.Stat(config.APPS_JSON); err != nil && os.IsNotExist(err) {
if err := ioutil.WriteFile(config.APPS_JSON, recipesJSON, 0764); err != nil {
logrus.Fatal(err)
}
} else {
if recipeName != "" {
catlFS, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Fatal(err)
}
catlFS[recipeName] = catl[recipeName]
updatedRecipesJSON, err := json.MarshalIndent(catlFS, "", " ")
if err != nil {
logrus.Fatal(err)
}
if err := ioutil.WriteFile(config.APPS_JSON, updatedRecipesJSON, 0764); err != nil {
logrus.Fatal(err)
}
}
}
logrus.Infof("generated new recipe catalogue in %s", config.APPS_JSON)
if internal.Commit {
if internal.CommitMessage == "" && !internal.NoInput {
prompt := &survey.Input{
Message: "commit message",
Default: fmt.Sprintf("chore: publish catalogue changes"),
}
if err := survey.AskOne(prompt, &internal.CommitMessage); err != nil {
logrus.Fatal(err)
}
}
cataloguePath := path.Join(config.ABRA_DIR, "catalogue")
if err := gitPkg.Commit(cataloguePath, "**.json", internal.CommitMessage, internal.Dry, internal.Push); err != nil {
logrus.Fatal(err)
}
}
return nil
},
BashComplete: autocomplete.RecipeNameComplete,
}
// CatalogueCommand defines the `abra catalogue` command and sub-commands. // CatalogueCommand defines the `abra catalogue` command and sub-commands.
var CatalogueCommand = &cli.Command{ var CatalogueCommand = &cli.Command{
Name: "catalogue", Name: "catalogue",
Usage: "Manage the recipe catalogue", Usage: "Manage the recipe catalogue (for maintainers)",
Aliases: []string{"c"}, Aliases: []string{"c"},
ArgsUsage: "<recipe>", ArgsUsage: "<recipe>",
Description: "This command helps recipe packagers interact with the recipe catalogue", Description: "This command helps recipe packagers interact with the recipe catalogue",

282
cli/catalogue/generate.go Normal file
View File

@ -0,0 +1,282 @@
package catalogue
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/config"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/limit"
"github.com/AlecAivazis/survey/v2"
"github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
// CatalogueSkipList is all the repos that are not recipes.
var CatalogueSkipList = map[string]bool{
"abra": true,
"abra-apps": true,
"abra-aur": true,
"abra-bash": true,
"abra-capsul": true,
"abra-gandi": true,
"abra-hetzner": true,
"apps": true,
"aur-abra-git": true,
"auto-apps-json": true,
"auto-mirror": true,
"backup-bot": true,
"backup-bot-two": true,
"comrade-renovate-bot": true,
"coopcloud.tech": true,
"coturn": true,
"docker-cp-deploy": true,
"docker-dind-bats-kcov": true,
"docs.coopcloud.tech": true,
"drone-abra": true,
"example": true,
"gardening": true,
"go-abra": true,
"organising": true,
"pyabra": true,
"radicle-seed-node": true,
"recipes": true,
"stack-ssh-deploy": true,
"swarm-cronjob": true,
"tagcmp": true,
"traefik-cert-dumper": true,
"tyop": true,
}
var commit bool
var commitFlag = &cli.BoolFlag{
Name: "commit",
Usage: "Commits new generated catalogue changes",
Value: false,
Aliases: []string{"c"},
Destination: &commit,
}
var catalogueGenerateCommand = &cli.Command{
Name: "generate",
Aliases: []string{"g"},
Usage: "Generate a new copy of the catalogue",
Flags: []cli.Flag{
internal.PushFlag,
commitFlag,
internal.CommitMessageFlag,
},
Description: `
This command generates a new copy of the recipe catalogue which can be found on:
https://recipes.coopcloud.tech
It polls the entire git.coopcloud.tech/coop-cloud/... recipe repository
listing, parses README and tags to produce recipe metadata and produces a
apps.json file which is placed in your ~/.abra/catalogue/recipes.json.
It is possible to generate new metadata for a single recipe by passing
<recipe>. The existing local catalogue will be updated, not overwritten.
A new catalogue copy can be published to the recipes repository by passing the
"--commit" and "--push" flags. The recipes repository is available here:
https://git.coopcloud.tech/coop-cloud/recipes
`,
ArgsUsage: "[<recipe>]",
Action: func(c *cli.Context) error {
recipeName := c.Args().First()
if recipeName != "" {
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 := catalogue.ReadReposMetadata()
if err != nil {
logrus.Fatal(err)
}
logrus.Debugf("ensuring '%v' recipe(s) are locally present and up-to-date", len(repos))
cloneLimiter := limit.New(10)
retrieveBar := formatter.CreateProgressbar(len(repos), "retrieving recipes...")
ch := make(chan string, len(repos))
for _, repoMeta := range repos {
go func(rm catalogue.RepoMeta) {
cloneLimiter.Begin()
defer cloneLimiter.End()
if recipeName != "" && recipeName != rm.Name {
ch <- rm.Name
retrieveBar.Add(1)
return
}
if _, exists := CatalogueSkipList[rm.Name]; exists {
ch <- rm.Name
retrieveBar.Add(1)
return
}
recipeDir := path.Join(config.ABRA_DIR, "apps", rm.Name)
if err := gitPkg.Clone(recipeDir, rm.SSHURL); err != nil {
logrus.Fatal(err)
}
isClean, err := gitPkg.IsClean(rm.Name)
if err != nil {
logrus.Fatal(err)
}
if !isClean {
logrus.Fatalf("'%s' has locally unstaged changes", rm.Name)
}
if err := gitPkg.EnsureUpToDate(recipeDir); err != nil {
logrus.Fatal(err)
}
ch <- rm.Name
retrieveBar.Add(1)
}(repoMeta)
}
for range repos {
<-ch // wait for everything
}
catl := make(catalogue.RecipeCatalogue)
catlBar := formatter.CreateProgressbar(len(repos), "generating catalogue...")
for _, recipeMeta := range repos {
if recipeName != "" && recipeName != recipeMeta.Name {
catlBar.Add(1)
continue
}
if _, exists := CatalogueSkipList[recipeMeta.Name]; exists {
catlBar.Add(1)
continue
}
versions, err := catalogue.GetRecipeVersions(recipeMeta.Name)
if err != nil {
logrus.Fatal(err)
}
features, category, err := catalogue.GetRecipeFeaturesAndCategory(recipeMeta.Name)
if err != nil {
logrus.Warn(err)
}
catl[recipeMeta.Name] = catalogue.RecipeMeta{
Name: recipeMeta.Name,
Repository: recipeMeta.CloneURL,
Icon: recipeMeta.AvatarURL,
DefaultBranch: recipeMeta.DefaultBranch,
Description: recipeMeta.Description,
Website: recipeMeta.Website,
Versions: versions,
Category: category,
Features: features,
}
catlBar.Add(1)
}
recipesJSON, err := json.MarshalIndent(catl, "", " ")
if err != nil {
logrus.Fatal(err)
}
if _, err := os.Stat(config.APPS_JSON); err != nil && os.IsNotExist(err) {
if err := ioutil.WriteFile(config.APPS_JSON, recipesJSON, 0644); err != nil {
logrus.Fatal(err)
}
} else {
if recipeName != "" {
catlFS, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Fatal(err)
}
catlFS[recipeName] = catl[recipeName]
updatedRecipesJSON, err := json.MarshalIndent(catlFS, "", " ")
if err != nil {
logrus.Fatal(err)
}
if err := ioutil.WriteFile(config.APPS_JSON, updatedRecipesJSON, 0644); err != nil {
logrus.Fatal(err)
}
}
}
cataloguePath := path.Join(config.ABRA_DIR, "catalogue", "recipes.json")
logrus.Infof("generated new recipe catalogue in %s", cataloguePath)
if commit {
repoPath := path.Join(config.ABRA_DIR, "catalogue")
commitRepo, err := git.PlainOpen(repoPath)
if err != nil {
logrus.Fatal(err)
}
commitWorktree, err := commitRepo.Worktree()
if err != nil {
logrus.Fatal(err)
}
if internal.CommitMessage == "" {
prompt := &survey.Input{
Message: "commit message",
Default: "chore: publish new catalogue changes",
}
if err := survey.AskOne(prompt, &internal.CommitMessage); err != nil {
logrus.Fatal(err)
}
}
err = commitWorktree.AddGlob("**.json")
if err != nil {
logrus.Fatal(err)
}
logrus.Debug("staged **.json for commit")
_, err = commitWorktree.Commit(internal.CommitMessage, &git.CommitOptions{})
if err != nil {
logrus.Fatal(err)
}
logrus.Info("changes commited")
if err := commitRepo.Push(&git.PushOptions{}); err != nil {
logrus.Fatal(err)
}
logrus.Info("changes pushed")
}
return nil
},
BashComplete: func(c *cli.Context) {
catl, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for name := range catl {
fmt.Println(name)
}
},
}

View File

@ -18,19 +18,29 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
// Verbose stores the variable from VerboseFlag.
var Verbose bool
// VerboseFlag turns on/off verbose logging down to the INFO level.
var VerboseFlag = &cli.BoolFlag{
Name: "verbose",
Aliases: []string{"V"},
Value: false,
Destination: &Verbose,
Usage: "Show INFO messages",
}
func newAbraApp(version, commit string) *cli.App { func newAbraApp(version, commit string) *cli.App {
app := &cli.App{ app := &cli.App{
Name: "abra", Name: "abra",
Usage: `The Co-op Cloud command-line utility belt 🎩🐇 Usage: `The Co-op Cloud command-line utility belt 🎩🐇
____ ____ _ _ ____ ____ _ _
/ ___|___ ___ _ __ / ___| | ___ _ _ __| | / ___|___ ___ _ __ / ___| | ___ _ _ __| |
| | / _ \ _____ / _ \| '_ \ | | | |/ _ \| | | |/ _' | | | / _ \ _____ / _ \| '_ \ | | | |/ _ \| | | |/ _' |
| |__| (_) |_____| (_) | |_) | | |___| | (_) | |_| | (_| | | |__| (_) |_____| (_) | |_) | | |___| | (_) | |_| | (_| |
\____\___/ \___/| .__/ \____|_|\___/ \__,_|\__,_| \____\___/ \___/| .__/ \____|_|\___/ \__,_|\__,_|
|_| |_|
If you haven't already done so, consider setting up autocompletion for a more
convenient command-line experience. See "abra autocomplete -h" for more.
`, `,
Version: fmt.Sprintf("%s-%s", version, commit[:7]), Version: fmt.Sprintf("%s-%s", version, commit[:7]),
Commands: []*cli.Command{ Commands: []*cli.Command{
@ -43,14 +53,11 @@ convenient command-line experience. See "abra autocomplete -h" for more.
AutoCompleteCommand, AutoCompleteCommand,
}, },
Flags: []cli.Flag{ Flags: []cli.Flag{
internal.VerboseFlag, VerboseFlag,
internal.DebugFlag, internal.DebugFlag,
internal.NoInputFlag, internal.NoInputFlag,
}, },
Authors: []*cli.Author{ Authors: []*cli.Author{
// If you're looking at this and you hack on Abra and you're not listed
// here, please do add yourself! This is a community project, let's show
// some love
{Name: "3wordchant"}, {Name: "3wordchant"},
{Name: "decentral1se"}, {Name: "decentral1se"},
{Name: "knoflook"}, {Name: "knoflook"},
@ -76,12 +83,14 @@ convenient command-line experience. See "abra autocomplete -h" for more.
} }
for _, path := range paths { for _, path := range paths {
if err := os.Mkdir(path, 0764); err != nil { if err := os.Mkdir(path, 0755); err != nil {
if !os.IsExist(err) { if !os.IsExist(err) {
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Debugf("'%s' already created, moving on...", path)
continue continue
} }
logrus.Debugf("'%s' is missing, creating...", path)
} }
logrus.Debugf("abra version '%s', commit '%s'", version, commit) logrus.Debugf("abra version '%s', commit '%s'", version, commit)

View File

@ -272,153 +272,6 @@ var DebugFlag = &cli.BoolFlag{
Usage: "Show DEBUG messages", Usage: "Show DEBUG messages",
} }
// Verbose stores the variable from VerboseFlag.
var Verbose bool
// VerboseFlag turns on/off verbose logging down to the INFO level.
var VerboseFlag = &cli.BoolFlag{
Name: "verbose",
Aliases: []string{"V"},
Value: false,
Destination: &Verbose,
Usage: "Show INFO messages",
}
// RC signifies the latest release candidate
var RC bool
// RCFlag chooses the latest release candidate for install
var RCFlag = &cli.BoolFlag{
Name: "rc",
Value: false,
Destination: &RC,
Usage: "Insatll the latest release candidate",
}
var Major bool
var MajorFlag = &cli.BoolFlag{
Name: "major",
Usage: "Increase the major part of the version",
Value: false,
Aliases: []string{"ma", "x"},
Destination: &Major,
}
var Minor bool
var MinorFlag = &cli.BoolFlag{
Name: "minor",
Usage: "Increase the minor part of the version",
Value: false,
Aliases: []string{"mi", "y"},
Destination: &Minor,
}
var Patch bool
var PatchFlag = &cli.BoolFlag{
Name: "patch",
Usage: "Increase the patch part of the version",
Value: false,
Aliases: []string{"p", "z"},
Destination: &Patch,
}
var Dry bool
var DryFlag = &cli.BoolFlag{
Name: "dry-run",
Usage: "Only reports changes that would be made",
Value: false,
Aliases: []string{"d"},
Destination: &Dry,
}
var Push bool
var PushFlag = &cli.BoolFlag{
Name: "push",
Usage: "Git push changes",
Value: false,
Aliases: []string{"P"},
Destination: &Push,
}
var CommitMessage string
var CommitMessageFlag = &cli.StringFlag{
Name: "commit-message",
Usage: "Commit message (implies --commit)",
Aliases: []string{"cm"},
Destination: &CommitMessage,
}
var Commit bool
var CommitFlag = &cli.BoolFlag{
Name: "commit",
Usage: "Commit new changes",
Value: false,
Aliases: []string{"c"},
Destination: &Commit,
}
var TagMessage string
var TagMessageFlag = &cli.StringFlag{
Name: "tag-comment",
Usage: "Description for release tag",
Aliases: []string{"t", "tm"},
Destination: &TagMessage,
}
var Domain string
var DomainFlag = &cli.StringFlag{
Name: "domain",
Aliases: []string{"d"},
Value: "",
Usage: "Choose a domain name",
Destination: &Domain,
}
var NewAppServer string
var NewAppServerFlag = &cli.StringFlag{
Name: "server",
Aliases: []string{"s"},
Value: "",
Usage: "Show apps of a specific server",
Destination: &NewAppServer,
}
var NewAppName string
var NewAppNameFlag = &cli.StringFlag{
Name: "app-name",
Aliases: []string{"a"},
Value: "",
Usage: "Choose an app name",
Destination: &NewAppName,
}
var NoDomainChecks bool
var NoDomainChecksFlag = &cli.BoolFlag{
Name: "no-domain-checks",
Aliases: []string{"nd"},
Value: false,
Usage: "Disable app domain sanity checks",
Destination: &NoDomainChecks,
}
var StdErrOnly bool
var StdErrOnlyFlag = &cli.BoolFlag{
Name: "stderr",
Aliases: []string{"s"},
Value: false,
Usage: "Only tail stderr",
Destination: &StdErrOnly,
}
var AutoDNSRecord bool
var AutoDNSRecordFlag = &cli.BoolFlag{
Name: "auto",
Aliases: []string{"a"},
Value: false,
Usage: "Automatically configure DNS records",
Destination: &StdErrOnly,
}
// SSHFailMsg is a hopefully helpful SSH failure message // SSHFailMsg is a hopefully helpful SSH failure message
var SSHFailMsg = ` var SSHFailMsg = `
Woops, Abra is unable to connect to connect to %s. Woops, Abra is unable to connect to connect to %s.
@ -441,10 +294,6 @@ If your SSH private key loaded? You can check by running the following command:
ssh-add -L ssh-add -L
If, you can add it with:
ssh-add ~/.ssh/<private-key-part>
If you are using a non-default public/private key, you can configure this in If you are using a non-default public/private key, you can configure this in
your ~/.ssh/config file which Abra will read in order to figure out connection your ~/.ssh/config file which Abra will read in order to figure out connection
details: details:

View File

@ -7,7 +7,6 @@ import (
"coopcloud.tech/abra/cli/formatter" "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/container"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/archive"
@ -33,17 +32,21 @@ func ConfigureAndCp(c *cli.Context, app config.App, srcPath string, dstPath stri
filters := filters.NewArgs() filters := filters.NewArgs()
filters.Add("name", fmt.Sprintf("%s_%s", appEnv.StackName(), service)) filters.Add("name", fmt.Sprintf("%s_%s", appEnv.StackName(), service))
containers, err := cl.ContainerList(c.Context, types.ContainerListOptions{Filters: filters})
container, err := container.GetContainer(c.Context, cl, filters, true)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Debugf("retrieved %s as target container on %s", formatter.ShortenID(container.ID), app.Server) if len(containers) != 1 {
logrus.Fatalf("expected 1 container but got %v", len(containers))
}
container := containers[0]
logrus.Debugf("retrieved '%s' as target container on '%s'", formatter.ShortenID(container.ID), app.Server)
if isToContainer { if isToContainer {
if _, err := os.Stat(srcPath); err != nil { if _, err := os.Stat(srcPath); err != nil {
logrus.Fatalf("%s does not exist?", srcPath) logrus.Fatalf("'%s' does not exist?", srcPath)
} }
toTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip} toTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip}

View File

@ -26,7 +26,7 @@ func DeployAction(c *cli.Context) error {
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Debugf("checking whether %s is already deployed", stackName) logrus.Debugf("checking whether '%s' is already deployed", stackName)
isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName) isDeployed, deployedVersion, err := stack.IsDeployed(c.Context, cl, stackName)
if err != nil { if err != nil {
@ -35,9 +35,9 @@ func DeployAction(c *cli.Context) error {
if isDeployed { if isDeployed {
if Force || Chaos { if Force || Chaos {
logrus.Warnf("%s is already deployed but continuing (--force/--chaos)", stackName) logrus.Warnf("'%s' is already deployed but continuing (--force/--chaos)", stackName)
} else { } else {
logrus.Fatalf("%s is already deployed", stackName) logrus.Fatalf("'%s' is already deployed", stackName)
} }
} }
@ -53,7 +53,7 @@ func DeployAction(c *cli.Context) error {
} }
if len(versions) > 0 { if len(versions) > 0 {
version = versions[len(versions)-1] version = versions[len(versions)-1]
logrus.Debugf("choosing %s as version to deploy", version) logrus.Debugf("choosing '%s' as version to deploy", version)
if err := recipe.EnsureVersion(app.Type, version); err != nil { if err := recipe.EnsureVersion(app.Type, version); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -67,7 +67,7 @@ func DeployAction(c *cli.Context) error {
} }
if version == "" && !Chaos { if version == "" && !Chaos {
logrus.Debugf("choosing %s as version to deploy", version) logrus.Debugf("choosing '%s' as version to deploy", version)
if err := recipe.EnsureVersion(app.Type, version); err != nil { if err := recipe.EnsureVersion(app.Type, version); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -116,21 +116,17 @@ func DeployAction(c *cli.Context) error {
logrus.Fatal(err) logrus.Fatal(err)
} }
if !NoDomainChecks { domainName := app.Env["DOMAIN"]
domainName := app.Env["DOMAIN"] ipv4, err := dns.EnsureIPv4(domainName)
ipv4, err := dns.EnsureIPv4(domainName) if err != nil || ipv4 == "" {
if err != nil || ipv4 == "" { logrus.Fatalf("could not find an IP address assigned to %s?", domainName)
logrus.Fatalf("could not find an IP address assigned to %s?", domainName)
}
if _, err = dns.EnsureDomainsResolveSameIPv4(domainName, app.Server); err != nil {
logrus.Fatal(err)
}
} else {
logrus.Warn("skipping domain checks as requested")
} }
if err := stack.RunDeploy(cl, deployOpts, compose, app.Env["TYPE"]); err != nil { if _, err = dns.EnsureDomainsResolveSameIPv4(domainName, app.Server); err != nil {
logrus.Fatal(err)
}
if err := stack.RunDeploy(cl, deployOpts, compose); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

View File

@ -0,0 +1,38 @@
package internal
import (
"github.com/urfave/cli/v2"
)
// Testing functions that call os.Exit
// https://stackoverflow.com/questions/26225513/how-to-test-os-exit-scenarios-in-go
// https://talks.golang.org/2014/testing.slide#23
var testapp = &cli.App{
Name: "abra",
Usage: `The Co-op Cloud command-line utility belt 🎩🐇`,
}
// not testing output as that changes. just if it exits with code 1
// does not work because of some weird errors on cli's part. Its a hard lib to test effectively.
// func TestShowSubcommandHelpAndError(t *testing.T) {
// if os.Getenv("HelpAndError") == "1" {
// ShowSubcommandHelpAndError(cli.NewContext(testapp, nil, nil), errors.New("Test error"))
// return
// }
// cmd := exec.Command(os.Args[0], "-test.run=TestShowSubcommandHelpAndError")
// cmd.Env = append(os.Environ(), "HelpAndError=1")
// var out bytes.Buffer
// cmd.Stderr = &out
// err := cmd.Run()
// println(out.String())
// if !strings.Contains(out.String(), "Test error") {
// t.Fatalf("expected command to show the error causing the exit, did not get correct stdout output")
// }
// if e, ok := err.(*exec.ExitError); ok && !e.Success() {
// return
// }
// t.Fatalf("process ran with err %v, want exit status 1", err)
// }

View File

@ -14,9 +14,35 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
// AppSecrets represents all app secrest
type AppSecrets map[string]string type AppSecrets map[string]string
var Domain string
var DomainFlag = &cli.StringFlag{
Name: "domain",
Aliases: []string{"d"},
Value: "",
Usage: "Choose a domain name",
Destination: &Domain,
}
var NewAppServer string
var NewAppServerFlag = &cli.StringFlag{
Name: "server",
Aliases: []string{"s"},
Value: "",
Usage: "Show apps of a specific server",
Destination: &NewAppServer,
}
var NewAppName string
var NewAppNameFlag = &cli.StringFlag{
Name: "app-name",
Aliases: []string{"a"},
Value: "",
Usage: "Choose an app name",
Destination: &NewAppName,
}
// RecipeName is used for configuring recipe name programmatically // RecipeName is used for configuring recipe name programmatically
var RecipeName string var RecipeName string
@ -129,11 +155,11 @@ func NewAction(c *cli.Context) error {
sanitisedAppName := config.SanitiseAppName(NewAppName) sanitisedAppName := config.SanitiseAppName(NewAppName)
if len(sanitisedAppName) > 45 { if len(sanitisedAppName) > 45 {
logrus.Fatalf("%s cannot be longer than 45 characters", sanitisedAppName) logrus.Fatalf("'%s' cannot be longer than 45 characters", sanitisedAppName)
} }
logrus.Debugf("%s sanitised as %s for new app", NewAppName, sanitisedAppName) logrus.Debugf("'%s' sanitised as '%s' for new app", NewAppName, sanitisedAppName)
if err := config.TemplateAppEnvSample(recipe.Name, NewAppName, NewAppServer, Domain); err != nil { if err := config.TemplateAppEnvSample(recipe.Name, NewAppName, NewAppServer, Domain, recipe.Name); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }

View File

@ -7,30 +7,99 @@ import (
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
) )
var Major bool
var MajorFlag = &cli.BoolFlag{
Name: "major",
Usage: "Increase the major part of the version",
Value: false,
Aliases: []string{"ma", "x"},
Destination: &Major,
}
var Minor bool
var MinorFlag = &cli.BoolFlag{
Name: "minor",
Usage: "Increase the minor part of the version",
Value: false,
Aliases: []string{"mi", "y"},
Destination: &Minor,
}
var Patch bool
var PatchFlag = &cli.BoolFlag{
Name: "patch",
Usage: "Increase the patch part of the version",
Value: false,
Aliases: []string{"p", "z"},
Destination: &Patch,
}
var Dry bool
var DryFlag = &cli.BoolFlag{
Name: "dry-run",
Usage: "No changes are made, only reports changes that would be made",
Value: false,
Aliases: []string{"d"},
Destination: &Dry,
}
var Push bool
var PushFlag = &cli.BoolFlag{
Name: "push",
Usage: "Git push changes",
Value: false,
Aliases: []string{"P"},
Destination: &Push,
}
var CommitMessage string
var CommitMessageFlag = &cli.StringFlag{
Name: "commit-message",
Usage: "Commit message (implies --commit)",
Aliases: []string{"cm"},
Destination: &CommitMessage,
}
var Commit bool
var CommitFlag = &cli.BoolFlag{
Name: "commit",
Usage: "Commits compose.**yml file changes to recipe repository",
Value: false,
Aliases: []string{"c"},
Destination: &Commit,
}
var TagMessage string
var TagMessageFlag = &cli.StringFlag{
Name: "tag-comment",
Usage: "Description for release tag",
Aliases: []string{"t", "tm"},
Destination: &TagMessage,
}
// PromptBumpType prompts for version bump type // PromptBumpType prompts for version bump type
func PromptBumpType(tagString string) error { func PromptBumpType(tagString string) error {
if (!Major && !Minor && !Patch) && tagString == "" { if (!Major && !Minor && !Patch) && tagString == "" {
fmt.Printf(`semver cheat sheet (more via semver.org): fmt.Printf(`
semver cheat sheet (more via semver.org):
major: new features/bug fixes, backwards incompatible major: new features/bug fixes, backwards incompatible
minor: new features/bug fixes, backwards compatible minor: new features/bug fixes, backwards compatible
patch: bug fixes, backwards compatible patch: bug fixes, backwards compatible
`)
`)
var chosenBumpType string var chosenBumpType string
prompt := &survey.Select{ prompt := &survey.Select{
Message: fmt.Sprintf("select recipe version increment type"), Message: fmt.Sprintf("select recipe version increment type"),
Options: []string{"major", "minor", "patch"}, Options: []string{"major", "minor", "patch"},
} }
if err := survey.AskOne(prompt, &chosenBumpType); err != nil { if err := survey.AskOne(prompt, &chosenBumpType); err != nil {
return err return err
} }
SetBumpType(chosenBumpType) SetBumpType(chosenBumpType)
} }
return nil return nil
} }

View File

@ -28,9 +28,6 @@ func ValidateRecipe(c *cli.Context) recipe.Recipe {
recipe, err := recipe.Get(recipeName) recipe, err := recipe.Get(recipeName)
if err != nil { if err != nil {
if c.Command.Name == "generate" { if c.Command.Name == "generate" {
if strings.Contains(err.Error(), "missing a compose") {
logrus.Fatal(err)
}
logrus.Warn(err) logrus.Warn(err)
} else { } else {
logrus.Fatal(err) logrus.Fatal(err)
@ -48,33 +45,14 @@ func ValidateRecipeWithPrompt(c *cli.Context) recipe.Recipe {
recipeName := c.Args().First() recipeName := c.Args().First()
if recipeName == "" && !NoInput { if recipeName == "" && !NoInput {
var recipes []string
catl, err := catalogue.ReadRecipeCatalogue() catl, err := catalogue.ReadRecipeCatalogue()
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
var recipes []string
knownRecipes := make(map[string]bool)
for name := range catl { for name := range catl {
knownRecipes[name] = true recipes = append(recipes, name)
} }
localRecipes, err := recipe.GetRecipesLocal()
if err != nil {
logrus.Fatal(err)
}
for _, recipeLocal := range localRecipes {
if _, ok := knownRecipes[recipeLocal]; !ok {
knownRecipes[recipeLocal] = true
}
}
for recipeName := range knownRecipes {
recipes = append(recipes, recipeName)
}
prompt := &survey.Select{ prompt := &survey.Select{
Message: "Select recipe", Message: "Select recipe",
Options: recipes, Options: recipes,
@ -98,7 +76,7 @@ func ValidateRecipeWithPrompt(c *cli.Context) recipe.Recipe {
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Debugf("validated %s as recipe argument", recipeName) logrus.Debugf("validated '%s' as recipe argument", recipeName)
return recipe return recipe
} }
@ -129,7 +107,7 @@ func ValidateApp(c *cli.Context) config.App {
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Debugf("validated %s as app argument", appName) logrus.Debugf("validated '%s' as app argument", appName)
return app return app
} }
@ -152,7 +130,7 @@ func ValidateDomain(c *cli.Context) (string, error) {
ShowSubcommandHelpAndError(c, errors.New("no domain provided")) ShowSubcommandHelpAndError(c, errors.New("no domain provided"))
} }
logrus.Debugf("validated %s as domain argument", domainName) logrus.Debugf("validated '%s' as domain argument", domainName)
return domainName, nil return domainName, nil
} }
@ -194,7 +172,7 @@ func ValidateServer(c *cli.Context) (string, error) {
ShowSubcommandHelpAndError(c, errors.New("no server provided")) ShowSubcommandHelpAndError(c, errors.New("no server provided"))
} }
logrus.Debugf("validated %s as server argument", serverName) logrus.Debugf("validated '%s' as server argument", serverName)
return serverName, nil return serverName, nil
} }

View File

@ -7,7 +7,7 @@ import (
"coopcloud.tech/abra/cli/formatter" "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
@ -98,5 +98,16 @@ var recipeLintCommand = &cli.Command{
return nil return nil
}, },
BashComplete: autocomplete.RecipeNameComplete, BashComplete: func(c *cli.Context) {
catl, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for name := range catl {
fmt.Println(name)
}
},
} }

View File

@ -41,7 +41,7 @@ The new example repository is cloned to ~/.abra/apps/<recipe>.
directory := path.Join(config.APPS_DIR, recipeName) directory := path.Join(config.APPS_DIR, recipeName)
if _, err := os.Stat(directory); !os.IsNotExist(err) { if _, err := os.Stat(directory); !os.IsNotExist(err) {
logrus.Fatalf("%s recipe directory already exists?", directory) logrus.Fatalf("'%s' recipe directory already exists?", directory)
} }
url := fmt.Sprintf("%s/example.git", config.REPOS_BASE_URL) url := fmt.Sprintf("%s/example.git", config.REPOS_BASE_URL)
@ -53,7 +53,7 @@ The new example repository is cloned to ~/.abra/apps/<recipe>.
if err := os.RemoveAll(gitRepo); err != nil { if err := os.RemoveAll(gitRepo); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Debugf("removed git repo in %s", gitRepo) logrus.Debugf("removed git repo in '%s'", gitRepo)
toParse := []string{ toParse := []string{
path.Join(config.APPS_DIR, recipeName, "README.md"), path.Join(config.APPS_DIR, recipeName, "README.md"),
@ -61,7 +61,7 @@ The new example repository is cloned to ~/.abra/apps/<recipe>.
path.Join(config.APPS_DIR, recipeName, ".drone.yml"), path.Join(config.APPS_DIR, recipeName, ".drone.yml"),
} }
for _, path := range toParse { for _, path := range toParse {
file, err := os.OpenFile(path, os.O_RDWR, 0664) file, err := os.OpenFile(path, os.O_RDWR, 0755)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -88,7 +88,7 @@ The new example repository is cloned to ~/.abra/apps/<recipe>.
} }
logrus.Infof( logrus.Infof(
"new recipe %s created in %s, happy hacking!\n", "new recipe '%s' created in %s, happy hacking!\n",
recipeName, path.Join(config.APPS_DIR, recipeName), recipeName, path.Join(config.APPS_DIR, recipeName),
) )

View File

@ -7,18 +7,13 @@ import (
// RecipeCommand defines all recipe related sub-commands. // RecipeCommand defines all recipe related sub-commands.
var RecipeCommand = &cli.Command{ var RecipeCommand = &cli.Command{
Name: "recipe", Name: "recipe",
Usage: "Manage recipes", Usage: "Manage recipes (for maintainers)",
ArgsUsage: "<recipe>", ArgsUsage: "<recipe>",
Aliases: []string{"r"}, Aliases: []string{"r"},
Description: ` Description: `
A recipe is a blueprint for an app. It is a bunch of config files which A recipe is a blueprint for an app. It is a bunch of configuration files which
describe how to deploy and maintain an app. Recipes are maintained by the Co-op describe how to deploy and maintain an app. Recipes are maintained by the Co-op
Cloud community and you can use Abra to read them and create apps for you. Cloud community and you can use Abra to read them and create apps for you.
Anyone who uses a recipe can become a maintainer. Maintainers typically make
sure the recipe is in good working order and the config upgraded in a timely
manner. Abra supports convenient automation for recipe maintainenace, see the
"abra recipe upgrade", "abra recipe sync" and "abra recipe release" commands.
`, `,
Subcommands: []*cli.Command{ Subcommands: []*cli.Command{
recipeListCommand, recipeListCommand,

View File

@ -8,16 +8,14 @@ import (
abraFormatter "coopcloud.tech/abra/cli/formatter" abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe" recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
configPkg "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -36,9 +34,9 @@ These tags take the following form:
a.b.c+x.y.z a.b.c+x.y.z
Where the "a.b.c" part is a semantic version determined by the maintainer. And Where the "a.b.c" part is maintained as a semantic version of the recipe by the
the "x.y.z" part is the image tag of the recipe "app" service (the main recipe maintainer. And the "x.y.z" part is the image tag of the recipe "app"
container which contains the software to be used). service (the main container which contains the software to be used).
We maintain a semantic versioning scheme ("a.b.c") alongside the libre app We maintain a semantic versioning scheme ("a.b.c") alongside the libre app
versioning scheme in order to maximise the chances that the nature of recipe versioning scheme in order to maximise the chances that the nature of recipe
@ -63,77 +61,235 @@ You may invoke this command in "wizard" mode and be prompted for input:
internal.CommitMessageFlag, internal.CommitMessageFlag,
internal.TagMessageFlag, internal.TagMessageFlag,
}, },
BashComplete: autocomplete.RecipeNameComplete,
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipeWithPrompt(c) recipe := internal.ValidateRecipeWithPrompt(c)
directory := path.Join(config.APPS_DIR, recipe.Name)
tagString := c.Args().Get(1)
mainApp := internal.GetMainApp(recipe)
imagesTmp, err := getImageVersions(recipe) imagesTmp, err := getImageVersions(recipe)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
mainApp := internal.GetMainApp(recipe)
mainAppVersion := imagesTmp[mainApp] mainAppVersion := imagesTmp[mainApp]
if mainAppVersion == "" {
logrus.Fatalf("main app service version for %s is empty?", recipe.Name) if err := recipePkg.EnsureExists(recipe.Name); err != nil {
logrus.Fatal(err)
}
if mainAppVersion == "" {
logrus.Fatalf("main 'app' service version for %s is empty?", recipe.Name)
} }
tagString := c.Args().Get(1)
if tagString != "" { if tagString != "" {
if _, err := tagcmp.Parse(tagString); err != nil { if _, err := tagcmp.Parse(tagString); err != nil {
logrus.Fatalf("cannot parse %s, invalid tag specified?", tagString) logrus.Fatal("invalid tag specified")
} }
} }
if (!internal.Major && !internal.Minor && !internal.Patch) && tagString != "" {
logrus.Fatal("please specify <version> or bump type (--major/--minor/--patch)")
}
if (internal.Major || internal.Minor || internal.Patch) && tagString != "" { if (internal.Major || internal.Minor || internal.Patch) && tagString != "" {
logrus.Fatal("cannot specify tag and bump type at the same time") logrus.Fatal("cannot specify tag and bump type at the same time")
} }
if tagString != "" { // bumpType is used to decide what part of the tag should be incremented
if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil { bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
if bumpType != 0 {
// a bitwise check if the number is a power of 2
if (bumpType & (bumpType - 1)) != 0 {
logrus.Fatal("you can only use one of: --major, --minor, --patch.")
}
}
if err := internal.PromptBumpType(tagString); err != nil {
logrus.Fatal(err)
}
if internal.TagMessage == "" {
prompt := &survey.Input{
Message: "tag message",
Default: fmt.Sprintf("chore: publish new %s version", internal.GetBumpType()),
}
if err := survey.AskOne(prompt, &internal.TagMessage); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} }
tags, err := recipe.Tags() var createTagOptions git.CreateTagOptions
if err != nil { createTagOptions.Message = internal.TagMessage
return err
if !internal.Commit {
prompt := &survey.Confirm{
Message: "git commit changes also?",
}
if err := survey.AskOne(prompt, &internal.Commit); err != nil {
return err
}
} }
if len(tags) > 0 { if !internal.Push {
logrus.Warnf("previous git tags detected, assuming this is a new semver release") prompt := &survey.Confirm{
if err := createReleaseFromPreviousTag(tagString, mainAppVersion, recipe, tags); err != nil { Message: "git push changes also?",
}
if err := survey.AskOne(prompt, &internal.Push); err != nil {
return err
}
}
if internal.Commit || internal.CommitMessage != "" {
commitRepo, err := git.PlainOpen(directory)
if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
} else { commitWorktree, err := commitRepo.Worktree()
logrus.Warnf("no tag specified and no previous tag available for %s, assuming this is the initial release", recipe.Name)
initTag, err := recipePkg.GetVersionLabelLocal(recipe)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Warnf("discovered %s as currently synced recipe label", initTag) if internal.CommitMessage == "" {
prompt := &survey.Input{
prompt := &survey.Confirm{ Message: "commit message",
Message: fmt.Sprintf("use %s as the initial release?", initTag), Default: fmt.Sprintf("chore: publish new %s version", internal.GetBumpType()),
}
var response bool
if err := survey.AskOne(prompt, &response); err != nil {
return err
}
if !response {
logrus.Fatalf("please fix your synced label for %s and re-run this command", recipe.Name)
}
if err := createReleaseFromTag(recipe, initTag, mainAppVersion); err != nil {
if cleanUpErr := cleanUpTag(initTag, recipe.Name); err != nil {
logrus.Fatal(cleanUpErr)
} }
if err := survey.AskOne(prompt, &internal.CommitMessage); err != nil {
logrus.Fatal(err)
}
}
err = commitWorktree.AddGlob("compose.**yml")
if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Debug("staged compose.**yml for commit")
if !internal.Dry {
_, err = commitWorktree.Commit(internal.CommitMessage, &git.CommitOptions{})
if err != nil {
logrus.Fatal(err)
}
logrus.Info("changes commited")
} else {
logrus.Info("dry run only: NOT committing changes")
}
}
repo, err := git.PlainOpen(directory)
if err != nil {
logrus.Fatal(err)
}
head, err := repo.Head()
if err != nil {
logrus.Fatal(err)
}
if tagString != "" {
tag, err := tagcmp.Parse(tagString)
if err != nil {
logrus.Fatal(err)
}
if tag.MissingMinor {
tag.Minor = "0"
tag.MissingMinor = false
}
if tag.MissingPatch {
tag.Patch = "0"
tag.MissingPatch = false
}
tagString = fmt.Sprintf("%s+%s", tag.String(), mainAppVersion)
if internal.Dry {
hash := abraFormatter.SmallSHA(head.Hash().String())
logrus.Info(fmt.Sprintf("dry run only: NOT creating tag %s at %s", tagString, hash))
return nil
}
repo.CreateTag(tagString, head.Hash(), &createTagOptions)
hash := abraFormatter.SmallSHA(head.Hash().String())
logrus.Info(fmt.Sprintf("created tag %s at %s", tagString, hash))
if internal.Push && !internal.Dry {
if err := repo.Push(&git.PushOptions{}); err != nil {
logrus.Fatal(err)
}
logrus.Info(fmt.Sprintf("pushed tag %s to remote", tagString))
} else {
logrus.Info("dry run only: NOT pushing changes")
}
return nil
}
// get the latest tag with its hash, name etc
var lastGitTag tagcmp.Tag
iter, err := repo.Tags()
if err != nil {
logrus.Fatal(err)
}
if err := iter.ForEach(func(ref *plumbing.Reference) error {
obj, err := repo.TagObject(ref.Hash())
if err != nil {
return err
}
tagcmpTag, err := tagcmp.Parse(obj.Name)
if err != nil {
return err
}
if (lastGitTag == tagcmp.Tag{}) {
lastGitTag = tagcmpTag
} else if tagcmpTag.IsGreaterThan(lastGitTag) {
lastGitTag = tagcmpTag
}
return nil
}); err != nil {
logrus.Fatal(err)
}
newTag := lastGitTag
var newtagString string
if bumpType > 0 {
if internal.Patch {
now, err := strconv.Atoi(newTag.Patch)
if err != nil {
logrus.Fatal(err)
}
newTag.Patch = strconv.Itoa(now + 1)
} else if internal.Minor {
now, err := strconv.Atoi(newTag.Minor)
if err != nil {
logrus.Fatal(err)
}
newTag.Patch = "0"
newTag.Minor = strconv.Itoa(now + 1)
} else if internal.Major {
now, err := strconv.Atoi(newTag.Major)
if err != nil {
logrus.Fatal(err)
}
newTag.Patch = "0"
newTag.Minor = "0"
newTag.Major = strconv.Itoa(now + 1)
}
}
newTag.Metadata = mainAppVersion
newtagString = newTag.String()
if internal.Dry {
hash := abraFormatter.SmallSHA(head.Hash().String())
logrus.Info(fmt.Sprintf("dry run only: NOT creating tag %s at %s", newtagString, hash))
return nil
}
repo.CreateTag(newtagString, head.Hash(), &createTagOptions)
hash := abraFormatter.SmallSHA(head.Hash().String())
logrus.Info(fmt.Sprintf("created tag %s at %s", newtagString, hash))
if internal.Push && !internal.Dry {
if err := repo.Push(&git.PushOptions{}); err != nil {
logrus.Fatal(err)
}
logrus.Info(fmt.Sprintf("pushed tag %s to remote", newtagString))
} else {
logrus.Info("gry run only: NOT pushing changes")
} }
return nil return nil
@ -164,7 +320,7 @@ func getImageVersions(recipe recipe.Recipe) (map[string]string, error) {
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) logrus.Fatalf("%s service is missing image tag?", path)
} }
services[path] = tag services[path] = tag
@ -173,50 +329,6 @@ func getImageVersions(recipe recipe.Recipe) (map[string]string, error) {
return services, nil return services, nil
} }
// createReleaseFromTag creates a new release based on a supplied recipe version string
func createReleaseFromTag(recipe recipe.Recipe, tagString, mainAppVersion string) error {
var err error
directory := path.Join(config.APPS_DIR, recipe.Name)
repo, err := git.PlainOpen(directory)
if err != nil {
return err
}
tag, err := tagcmp.Parse(tagString)
if err != nil {
return err
}
if tag.MissingMinor {
tag.Minor = "0"
tag.MissingMinor = false
}
if tag.MissingPatch {
tag.Patch = "0"
tag.MissingPatch = false
}
if err := commitRelease(recipe); err != nil {
logrus.Fatal(err)
}
if tagString == "" {
tagString = fmt.Sprintf("%s+%s", tag.String(), mainAppVersion)
}
if err := tagRelease(tagString, repo); err != nil {
logrus.Fatal(err)
}
if err := pushRelease(tagString, repo); err != nil {
logrus.Fatal(err)
}
return nil
}
// btoi converts a boolean value into an integer // btoi converts a boolean value into an integer
func btoi(b bool) int { func btoi(b bool) int {
if b { if b {
@ -225,208 +337,3 @@ func btoi(b bool) int {
return 0 return 0
} }
// getTagCreateOptions constructs git tag create options
func getTagCreateOptions() (git.CreateTagOptions, error) {
if internal.TagMessage == "" && !internal.NoInput {
prompt := &survey.Input{
Message: "git tag message",
Default: "chore: publish new release",
}
if err := survey.AskOne(prompt, &internal.TagMessage); err != nil {
return git.CreateTagOptions{}, err
}
}
return git.CreateTagOptions{Message: internal.TagMessage}, nil
}
func commitRelease(recipe recipe.Recipe) error {
if internal.Dry {
logrus.Info("dry run: no changed committed")
return nil
}
if !internal.Commit && !internal.NoInput {
prompt := &survey.Confirm{
Message: "git commit changes?",
}
if err := survey.AskOne(prompt, &internal.Commit); err != nil {
return err
}
}
if internal.CommitMessage == "" && !internal.NoInput && internal.Commit {
prompt := &survey.Input{
Message: "commit message",
Default: "chore: publish new version",
}
if err := survey.AskOne(prompt, &internal.CommitMessage); err != nil {
return err
}
}
if internal.Commit {
repoPath := path.Join(config.APPS_DIR, recipe.Name)
if err := gitPkg.Commit(repoPath, "compose.**yml", internal.CommitMessage, internal.Dry, false); err != nil {
return err
}
}
return nil
}
func tagRelease(tagString string, repo *git.Repository) error {
if internal.Dry {
logrus.Infof("dry run: no git tag created (%s)", tagString)
return nil
}
head, err := repo.Head()
if err != nil {
return err
}
createTagOptions, err := getTagCreateOptions()
if err != nil {
return err
}
_, err = repo.CreateTag(tagString, head.Hash(), &createTagOptions)
if err != nil {
return err
}
hash := abraFormatter.SmallSHA(head.Hash().String())
logrus.Info(fmt.Sprintf("created tag %s at %s", tagString, hash))
return nil
}
func pushRelease(tagString string, repo *git.Repository) error {
if internal.Dry {
logrus.Info("dry run: no changes pushed")
return nil
}
if !internal.Push && !internal.NoInput {
prompt := &survey.Confirm{
Message: "git push changes?",
}
if err := survey.AskOne(prompt, &internal.Push); err != nil {
return err
}
}
if internal.Push {
tagRef := fmt.Sprintf("+refs/tags/%s:refs/tags/%s", tagString, tagString)
pushOpts := &git.PushOptions{
RefSpecs: []configPkg.RefSpec{
configPkg.RefSpec(tagRef),
},
}
if err := repo.Push(pushOpts); err != nil {
return err
}
logrus.Info(fmt.Sprintf("pushed tag %s to remote", tagString))
}
return nil
}
func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recipe.Recipe, tags []string) error {
directory := path.Join(config.APPS_DIR, recipe.Name)
repo, err := git.PlainOpen(directory)
if err != nil {
return err
}
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
if bumpType != 0 {
if (bumpType & (bumpType - 1)) != 0 {
return fmt.Errorf("you can only use one of: --major, --minor, --patch")
}
}
if err := internal.PromptBumpType(tagString); err != nil {
return err
}
var lastGitTag tagcmp.Tag
for _, tag := range tags {
parsed, err := tagcmp.Parse(tag)
if err != nil {
return err
}
if (lastGitTag == tagcmp.Tag{}) {
lastGitTag = parsed
} else if parsed.IsGreaterThan(lastGitTag) {
lastGitTag = parsed
}
}
newTag := lastGitTag
if internal.Patch {
now, err := strconv.Atoi(newTag.Patch)
if err != nil {
return err
}
newTag.Patch = strconv.Itoa(now + 1)
} else if internal.Minor {
now, err := strconv.Atoi(newTag.Minor)
if err != nil {
return err
}
newTag.Patch = "0"
newTag.Minor = strconv.Itoa(now + 1)
} else if internal.Major {
now, err := strconv.Atoi(newTag.Major)
if err != nil {
return err
}
newTag.Patch = "0"
newTag.Minor = "0"
newTag.Major = strconv.Itoa(now + 1)
}
if err := commitRelease(recipe); err != nil {
logrus.Fatal(err)
}
newTag.Metadata = mainAppVersion
newTagString := newTag.String()
if err := tagRelease(newTagString, repo); err != nil {
logrus.Fatal(err)
}
if err := pushRelease(newTagString, repo); err != nil {
logrus.Fatal(err)
}
return nil
}
// cleanUpTag removes a freshly created tag
func cleanUpTag(tag, recipeName string) error {
directory := path.Join(config.APPS_DIR, recipeName)
repo, err := git.PlainOpen(directory)
if err != nil {
return err
}
if err := repo.DeleteTag(tag); err != nil {
return err
}
logrus.Warn("removed freshly created tag %s")
return nil
}

View File

@ -6,7 +6,7 @@ import (
"strconv" "strconv"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/tagcmp" "coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
@ -51,7 +51,6 @@ You may invoke this command in "wizard" mode and be prompted for input:
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
mainAppVersion := imagesTmp[mainApp] mainAppVersion := imagesTmp[mainApp]
tags, err := recipe.Tags() tags, err := recipe.Tags()
@ -61,30 +60,15 @@ You may invoke this command in "wizard" mode and be prompted for input:
nextTag := c.Args().Get(1) nextTag := c.Args().Get(1)
if len(tags) == 0 && nextTag == "" { if len(tags) == 0 && nextTag == "" {
logrus.Warnf("no git tags found for %s", recipe.Name) logrus.Warnf("no tags found for %s", recipe.Name)
fmt.Println(fmt.Sprintf(`
The following options are two types of initial version that you can pick for
the first published version of %s that will be in the recipe catalogue. This
follows the semver convention (more on semver.org), here is a short cheatsheet
0.1.0 -> development release, still hacking
1.0.0 -> public release, assumed to be working
In other words, if you want people to be able alpha test your current config
for %s but don't think it is quite ready and reliable, go with 0.1.0 and people
will know that things are likely to change.
`, recipe.Name, recipe.Name))
var chosenVersion string var chosenVersion string
edPrompt := &survey.Select{ edPrompt := &survey.Select{
Message: "which version do you want to begin with?", Message: "which version do you want to begin with?",
Options: []string{"0.1.0", "1.0.0"}, Options: []string{"0.1.0", "1.0.0"},
} }
if err := survey.AskOne(edPrompt, &chosenVersion); err != nil { if err := survey.AskOne(edPrompt, &chosenVersion); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
nextTag = fmt.Sprintf("%s+%s", chosenVersion, mainAppVersion) nextTag = fmt.Sprintf("%s+%s", chosenVersion, mainAppVersion)
} }
@ -100,30 +84,25 @@ will know that things are likely to change.
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
var lastGitTag tagcmp.Tag var lastGitTag tagcmp.Tag
iter, err := repo.Tags() iter, err := repo.Tags()
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
if err := iter.ForEach(func(ref *plumbing.Reference) error { if err := iter.ForEach(func(ref *plumbing.Reference) error {
obj, err := repo.TagObject(ref.Hash()) obj, err := repo.TagObject(ref.Hash())
if err != nil { if err != nil {
return err return err
} }
tagcmpTag, err := tagcmp.Parse(obj.Name) tagcmpTag, err := tagcmp.Parse(obj.Name)
if err != nil { if err != nil {
return err return err
} }
if (lastGitTag == tagcmp.Tag{}) { if (lastGitTag == tagcmp.Tag{}) {
lastGitTag = tagcmpTag lastGitTag = tagcmpTag
} else if tagcmpTag.IsGreaterThan(lastGitTag) { } else if tagcmpTag.IsGreaterThan(lastGitTag) {
lastGitTag = tagcmpTag lastGitTag = tagcmpTag
} }
return nil return nil
}); err != nil { }); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -134,7 +113,7 @@ will know that things are likely to change.
if bumpType != 0 { if bumpType != 0 {
// a bitwise check if the number is a power of 2 // a bitwise check if the number is a power of 2
if (bumpType & (bumpType - 1)) != 0 { if (bumpType & (bumpType - 1)) != 0 {
logrus.Fatal("you can only use one version flag: --major, --minor or --patch") logrus.Fatal("you can only use one of: --major, --minor, --patch.")
} }
} }
@ -145,14 +124,12 @@ will know that things are likely to change.
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
newTag.Patch = strconv.Itoa(now + 1) newTag.Patch = strconv.Itoa(now + 1)
} else if internal.Minor { } else if internal.Minor {
now, err := strconv.Atoi(newTag.Minor) now, err := strconv.Atoi(newTag.Minor)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
newTag.Patch = "0" newTag.Patch = "0"
newTag.Minor = strconv.Itoa(now + 1) newTag.Minor = strconv.Itoa(now + 1)
} else if internal.Major { } else if internal.Major {
@ -160,7 +137,6 @@ will know that things are likely to change.
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
newTag.Patch = "0" newTag.Patch = "0"
newTag.Minor = "0" newTag.Minor = "0"
newTag.Major = strconv.Itoa(now + 1) newTag.Major = strconv.Itoa(now + 1)
@ -177,16 +153,44 @@ will know that things are likely to change.
} }
mainService := "app" mainService := "app"
var services []string
hasAppService := false
for _, service := range recipe.Config.Services {
services = append(services, service.Name)
if service.Name == "app" {
hasAppService = true
logrus.Debugf("detected app service in %s", recipe.Name)
}
}
if !hasAppService {
logrus.Fatalf("%s has no main 'app' service?", recipe.Name)
}
logrus.Debugf("selecting %s as the service to sync version label", mainService)
label := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", nextTag) label := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", nextTag)
if !internal.Dry { if !internal.Dry {
if err := recipe.UpdateLabel("compose.y*ml", mainService, label); err != nil { if err := recipe.UpdateLabel(mainService, label); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Infof("synced label '%s' to service '%s'", label, mainService)
} else { } else {
logrus.Infof("dry run: not syncing label %s for recipe %s", nextTag, recipe.Name) logrus.Infof("dry run only: NOT syncing label %s for recipe %s", nextTag, recipe.Name)
} }
return nil return nil
}, },
BashComplete: autocomplete.RecipeNameComplete, BashComplete: func(c *cli.Context) {
catl, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for name := range catl {
fmt.Println(name)
}
},
} }

View File

@ -97,6 +97,11 @@ You may invoke this command in "wizard" mode and be prompted for input:
} }
for _, service := range recipe.Config.Services { for _, service := range recipe.Config.Services {
catlVersions, err := catalogue.VersionsOfService(recipe.Name, service.Name)
if err != nil {
logrus.Fatal(err)
}
img, err := reference.ParseNormalizedNamed(service.Image) img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@ -107,7 +112,7 @@ You may invoke this command in "wizard" mode and be prompted for input:
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
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") { if strings.Contains(image, "library") {
// ParseNormalizedNamed prepends 'library' to images like nginx:<tag>, // ParseNormalizedNamed prepends 'library' to images like nginx:<tag>,
@ -117,7 +122,7 @@ You may invoke this command in "wizard" mode and be prompted for input:
} }
semverLikeTag := true 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 semverLikeTag = false
} }
@ -125,7 +130,7 @@ You may invoke this command in "wizard" mode and be prompted for input:
if err != nil && semverLikeTag { if err != nil && semverLikeTag {
logrus.Fatal(err) logrus.Fatal(err)
} }
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)
@ -138,20 +143,15 @@ You may invoke this command in "wizard" mode and be prompted for input:
} }
} }
logrus.Debugf("detected potential upgradable tags %s for %s", compatible, service.Name) logrus.Debugf("detected potential upgradable tags '%s' for '%s'", compatible, service.Name)
sort.Sort(tagcmp.ByTagDesc(compatible)) sort.Sort(tagcmp.ByTagDesc(compatible))
if len(compatible) == 0 && semverLikeTag { if len(compatible) == 0 && semverLikeTag {
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
} }
catlVersions, err := catalogue.VersionsOfService(recipe.Name, service.Name)
if err != nil {
logrus.Fatal(err)
}
var compatibleStrings []string var compatibleStrings []string
for _, compat := range compatible { for _, compat := range compatible {
skip := false skip := false
@ -165,7 +165,7 @@ You may invoke this command in "wizard" mode and be prompted for input:
} }
} }
logrus.Debugf("detected compatible upgradable tags %s for %s", compatibleStrings, service.Name) logrus.Debugf("detected compatible upgradable tags '%s' for '%s'", compatibleStrings, service.Name)
var upgradeTag string var upgradeTag string
_, ok := servicePins[service.Name] _, ok := servicePins[service.Name]
@ -205,14 +205,14 @@ 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 {
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)
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) { if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) {
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{}
for _, regVersion := range regVersions { for _, regVersion := range regVersions {
@ -232,7 +232,7 @@ You may invoke this command in "wizard" mode and be prompted for input:
if err := recipe.UpdateTag(image, upgradeTag); err != nil { if err := recipe.UpdateTag(image, upgradeTag); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
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)
} }
return nil return nil

View File

@ -7,7 +7,6 @@ import (
abraFormatter "coopcloud.tech/abra/cli/formatter" abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/dns"
gandiPkg "coopcloud.tech/abra/pkg/dns/gandi" gandiPkg "coopcloud.tech/abra/pkg/dns/gandi"
"github.com/libdns/gandi" "github.com/libdns/gandi"
"github.com/libdns/libdns" "github.com/libdns/libdns"
@ -28,7 +27,6 @@ var RecordNewCommand = &cli.Command{
internal.DNSValueFlag, internal.DNSValueFlag,
internal.DNSTTLFlag, internal.DNSTTLFlag,
internal.DNSPriorityFlag, internal.DNSPriorityFlag,
internal.AutoDNSRecordFlag,
}, },
Description: ` Description: `
This command creates a new domain name record for a specific zone. This command creates a new domain name record for a specific zone.
@ -40,12 +38,6 @@ Example:
abra record new foo.com -p gandi -t A -n myapp -v 192.168.178.44 abra record new foo.com -p gandi -t A -n myapp -v 192.168.178.44
Typically, you need two records, an A record which points at the zone (@.) and
a wildcard record for your apps (*.). Pass "--auto" to have Abra automatically
set this up.
abra record new --auto
You may also invoke this command in "wizard" mode and be prompted for input You may also invoke this command in "wizard" mode and be prompted for input
abra record new abra record new
@ -71,14 +63,6 @@ You may also invoke this command in "wizard" mode and be prompted for input
logrus.Fatalf("'%s' is not a supported DNS provider", internal.DNSProvider) logrus.Fatalf("'%s' is not a supported DNS provider", internal.DNSProvider)
} }
if internal.AutoDNSRecord {
logrus.Infof("automatically configuring @./*. A records for %s (--auto)", zone)
if err := autoConfigure(c, &provider, zone); err != nil {
logrus.Fatal(err)
}
return nil
}
if err := internal.EnsureDNSTypeFlag(c); err != nil { if err := internal.EnsureDNSTypeFlag(c); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -111,7 +95,7 @@ You may also invoke this command in "wizard" mode and be prompted for input
if existingRecord.Type == record.Type && if existingRecord.Type == record.Type &&
existingRecord.Name == record.Name && existingRecord.Name == record.Name &&
existingRecord.Value == record.Value { existingRecord.Value == record.Value {
logrus.Fatalf("%s record for %s already exists?", record.Type, zone) logrus.Fatal("provider library reports that this record already exists?")
} }
} }
@ -120,9 +104,6 @@ You may also invoke this command in "wizard" mode and be prompted for input
zone, zone,
[]libdns.Record{record}, []libdns.Record{record},
) )
if err != nil {
logrus.Fatal(err)
}
if len(createdRecords) == 0 { if len(createdRecords) == 0 {
logrus.Fatal("provider library reports that no record was created?") logrus.Fatal("provider library reports that no record was created?")
@ -153,79 +134,3 @@ You may also invoke this command in "wizard" mode and be prompted for input
return nil return nil
}, },
} }
func autoConfigure(c *cli.Context, provider *gandi.Provider, zone string) error {
ipv4, err := dns.EnsureIPv4(zone)
if err != nil {
return err
}
atRecord := libdns.Record{
Type: "A",
Name: "@",
Value: ipv4,
TTL: time.Duration(internal.DNSTTL),
}
wildcardRecord := libdns.Record{
Type: "A",
Name: "*",
Value: ipv4,
TTL: time.Duration(internal.DNSTTL),
}
records := []libdns.Record{atRecord, wildcardRecord}
tableCol := []string{"type", "name", "value", "TTL", "priority"}
table := abraFormatter.CreateTable(tableCol)
for _, record := range records {
existingRecords, err := provider.GetRecords(c.Context, zone)
if err != nil {
return err
}
for _, existingRecord := range existingRecords {
if existingRecord.Type == record.Type &&
existingRecord.Name == record.Name &&
existingRecord.Value == record.Value {
logrus.Warnf("%s record for %s already exists?", record.Type, zone)
continue
}
}
createdRecords, err := provider.SetRecords(
c.Context,
zone,
[]libdns.Record{record},
)
if err != nil {
return err
}
if len(createdRecords) == 0 {
return fmt.Errorf("provider library reports that no record was created?")
}
createdRecord := createdRecords[0]
value := createdRecord.Value
if len(createdRecord.Value) > 30 {
value = fmt.Sprintf("%s...", createdRecord.Value[:30])
}
table.Append([]string{
createdRecord.Type,
createdRecord.Name,
value,
createdRecord.TTL.String(),
strconv.Itoa(createdRecord.Priority),
})
}
if table.NumLines() > 0 {
table.Render()
}
return nil
}

View File

@ -10,6 +10,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
@ -116,17 +117,7 @@ func installDockerLocal(c *cli.Context) error {
logrus.Fatal("exiting as requested") logrus.Fatal("exiting as requested")
} }
for _, exe := range []string{"wget", "bash"} { cmd := exec.Command("bash", "-c", "curl -s https://get.docker.com | bash")
exists, err := ensureLocalExecutable(exe)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("%s missing, please install it", exe)
}
}
cmd := exec.Command("bash", "-c", "wget -O- https://get.docker.com | bash")
if err := internal.RunCmd(cmd); err != nil { if err := internal.RunCmd(cmd); err != nil {
return err return err
} }
@ -145,17 +136,15 @@ func newLocalServer(c *cli.Context, domainName string) error {
} }
if provision { if provision {
exists, err := ensureLocalExecutable("docker") out, err := exec.Command("which", "docker").Output()
if err != nil { if err != nil {
return err return err
} }
if string(out) == "" {
if !exists {
if err := installDockerLocal(c); err != nil { if err := installDockerLocal(c); err != nil {
return err return err
} }
} }
if err := initSwarmLocal(c, cl, domainName); err != nil { if err := initSwarmLocal(c, cl, domainName); err != nil {
if !strings.Contains(err.Error(), "proxy already exists") { if !strings.Contains(err.Error(), "proxy already exists") {
logrus.Fatal(err) logrus.Fatal(err)
@ -206,127 +195,59 @@ func newClient(c *cli.Context, domainName string) (*dockerClient.Client, error)
} }
func installDocker(c *cli.Context, cl *dockerClient.Client, sshCl *ssh.Client, domainName string) error { func installDocker(c *cli.Context, cl *dockerClient.Client, sshCl *ssh.Client, domainName string) error {
exists, err := ensureRemoteExecutable("docker", sshCl) result, err := sshCl.Exec("which docker")
if err != nil { if err != nil && string(result) != "" {
return err return err
} }
if !exists { if string(result) == "" {
fmt.Println(fmt.Sprintf(dockerInstallMsg, domainName)) fmt.Println(fmt.Sprintf(dockerInstallMsg, domainName))
response := false response := false
prompt := &survey.Confirm{ prompt := &survey.Confirm{
Message: fmt.Sprintf("attempt install docker on %s?", domainName), Message: fmt.Sprintf("attempt install docker on %s?", domainName),
} }
if err := survey.AskOne(prompt, &response); err != nil { if err := survey.AskOne(prompt, &response); err != nil {
return err return err
} }
if !response { if !response {
logrus.Fatal("exiting as requested") logrus.Fatal("exiting as requested")
} }
exes := []string{"wget", "bash"} cmd := "curl -s https://get.docker.com | bash"
if askSudoPass {
exes = append(exes, "ssh-askpass")
}
for _, exe := range exes {
exists, err := ensureRemoteExecutable(exe, sshCl)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("%s missing on remote, please install it", exe)
}
}
var sudoPass string var sudoPass string
if askSudoPass { if askSudoPass {
cmd := "wget -O- https://get.docker.com | bash"
prompt := &survey.Password{ prompt := &survey.Password{
Message: "sudo password?", Message: "sudo password?",
} }
if err := survey.AskOne(prompt, &sudoPass); err != nil { if err := survey.AskOne(prompt, &sudoPass); err != nil {
return err return err
} }
logrus.Debugf("running '%s' on %s now with sudo password", cmd, domainName)
logrus.Debugf("running %s on %s now with sudo password", cmd, domainName)
if sudoPass == "" {
return fmt.Errorf("missing sudo password but requested --ask-sudo-pass?")
}
logrus.Warn("installing docker, this could take some time...")
if err := ssh.RunSudoCmd(cmd, sudoPass, sshCl); err != nil { if err := ssh.RunSudoCmd(cmd, sudoPass, sshCl); err != nil {
fmt.Print(fmt.Sprintf(`
Abra was unable to bootstrap Docker, see below for logs:
%s
If nothing works, you try running the Docker install script manually on your server:
wget -O- https://get.docker.com | bash
`, string(err.Error())))
logrus.Fatal("Process exited with status 1")
}
logrus.Infof("docker is installed on %s", domainName)
remoteUser := sshCl.SSHClient.Conn.User()
logrus.Infof("adding %s to docker group", remoteUser)
permsCmd := fmt.Sprintf("sudo usermod -aG docker %s", remoteUser)
if err := ssh.RunSudoCmd(permsCmd, sudoPass, sshCl); err != nil {
return err return err
} }
} else { } else {
cmd := "wget -O- https://get.docker.com | bash" logrus.Debugf("running '%s' on %s now without sudo password", cmd, domainName)
if err := ssh.Exec(cmd, sshCl); err != nil {
logrus.Debugf("running %s on %s now without sudo password", cmd, domainName) return err
logrus.Warn("installing docker, this could take some time...")
if out, err := sshCl.Exec(cmd); err != nil {
fmt.Print(fmt.Sprintf(`
Abra was unable to bootstrap Docker, see below for logs:
%s
This could be due to a number of things but one of the most common is that your
server user account does not have sudo access, and if it does, you need to pass
"--ask-sudo-pass" in order to supply Abra with your password.
If nothing works, you try running the Docker install script manually on your server:
wget -O- https://get.docker.com | bash
`, string(out)))
logrus.Fatal(err)
} }
logrus.Infof("docker is installed on %s", domainName)
} }
} }
logrus.Infof("docker is installed on %s", domainName)
return nil return nil
} }
func initSwarmLocal(c *cli.Context, cl *dockerClient.Client, domainName string) error { func initSwarmLocal(c *cli.Context, cl *dockerClient.Client, domainName string) error {
initReq := swarm.InitRequest{ListenAddr: "0.0.0.0:2377"} initReq := swarm.InitRequest{ListenAddr: "0.0.0.0:2377"}
if _, err := cl.SwarmInit(c.Context, initReq); err != nil { if _, err := cl.SwarmInit(c.Context, initReq); err != nil {
if strings.Contains(err.Error(), "is already part of a swarm") || if !strings.Contains(err.Error(), "is already part of a swarm") {
strings.Contains(err.Error(), "must specify a listening address") {
logrus.Infof("swarm mode already initialised on %s", domainName)
} else {
return err return err
} }
logrus.Info("swarm mode already initialised on local server")
} else { } else {
logrus.Infof("initialised swarm mode on local server") logrus.Infof("initialised swarm mode on local server")
} }
@ -355,12 +276,10 @@ func initSwarm(c *cli.Context, cl *dockerClient.Client, domainName string) error
AdvertiseAddr: ipv4, AdvertiseAddr: ipv4,
} }
if _, err := cl.SwarmInit(c.Context, initReq); err != nil { if _, err := cl.SwarmInit(c.Context, initReq); err != nil {
if strings.Contains(err.Error(), "is already part of a swarm") || if !strings.Contains(err.Error(), "is already part of a swarm") {
strings.Contains(err.Error(), "must specify a listening address") {
logrus.Infof("swarm mode already initialised on %s", domainName)
} else {
return err return err
} }
logrus.Infof("swarm mode already initialised on %s", domainName)
} else { } else {
logrus.Infof("initialised swarm mode on %s", domainName) logrus.Infof("initialised swarm mode on %s", domainName)
} }
@ -397,8 +316,16 @@ func deployTraefik(c *cli.Context, cl *dockerClient.Client, domainName string) e
internal.NewAppName = fmt.Sprintf("%s_%s", "traefik", config.SanitiseAppName(domainName)) internal.NewAppName = fmt.Sprintf("%s_%s", "traefik", config.SanitiseAppName(domainName))
appEnvPath := path.Join(config.ABRA_DIR, "servers", internal.Domain, fmt.Sprintf("%s.env", internal.NewAppName)) appEnvPath := path.Join(config.ABRA_DIR, "servers", internal.Domain, fmt.Sprintf("%s.env", internal.NewAppName))
if _, err := os.Stat(appEnvPath); os.IsNotExist(err) { if _, err := os.Stat(appEnvPath); !os.IsNotExist(err) {
logrus.Info(fmt.Sprintf("-t/--traefik specified, automatically deploying traefik to %s", internal.NewAppServer)) fmt.Println(fmt.Sprintf(`
You specified "--traefik/-t" and that means that Abra will now try to
automatically create a new Traefik app on %s.
`, internal.NewAppServer))
tableCol := []string{"recipe", "domain", "server", "name"}
table := abraFormatter.CreateTable(tableCol)
table.Append([]string{internal.RecipeName, internal.Domain, internal.NewAppServer, internal.NewAppName})
if err := internal.NewAction(c); err != nil { if err := internal.NewAction(c); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
@ -478,12 +405,12 @@ You may omit flags to avoid performing this provisioning logic.
ArgsUsage: "<domain> [<user>] [<port>]", ArgsUsage: "<domain> [<user>] [<port>]",
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
if c.Args().Len() > 0 && local || !internal.ValidateSubCmdFlags(c) { if c.Args().Len() > 0 && local || !internal.ValidateSubCmdFlags(c) {
err := errors.New("cannot use <domain> and --local together") err := errors.New("cannot use '<domain>' and '--local' together")
internal.ShowSubcommandHelpAndError(c, err) internal.ShowSubcommandHelpAndError(c, err)
} }
if sshAuth != "password" && sshAuth != "identity-file" { if sshAuth != "password" && sshAuth != "identity-file" {
err := errors.New("--ssh-auth only accepts identity-file or password") err := errors.New("--ssh-auth only accepts 'identity-file' or 'password'")
internal.ShowSubcommandHelpAndError(c, err) internal.ShowSubcommandHelpAndError(c, err)
} }
@ -557,23 +484,3 @@ You may omit flags to avoid performing this provisioning logic.
return nil return nil
}, },
} }
// ensureLocalExecutable ensures that an executable is present on the local machine
func ensureLocalExecutable(exe string) (bool, error) {
out, err := exec.Command("which", exe).Output()
if err != nil {
return false, err
}
return string(out) != "", nil
}
// ensureRemoteExecutable ensures that an executable is present on a remote machine
func ensureRemoteExecutable(exe string, sshCl *ssh.Client) (bool, error) {
out, err := sshCl.Exec(fmt.Sprintf("which %s", exe))
if err != nil && string(out) != "" {
return false, err
}
return string(out) != "", nil
}

View File

@ -96,21 +96,9 @@ Please note, this server is not managed by Abra yet (i.e. "abra server ls" will
not list this server)! You will need to assign a domain name record ("abra not list this server)! You will need to assign a domain name record ("abra
record new") and add the server to your Abra configuration ("abra server add") record new") and add the server to your Abra configuration ("abra server add")
to have a working server that you can deploy Co-op Cloud apps to. to have a working server that you can deploy Co-op Cloud apps to.
When setting up domain name records, you probably want to set up the following
2 A records. This supports deploying apps to your root domain (e.g.
example.com) and other apps on sub-domains (e.g. foo.example.com,
bar.example.com).
@ 1800 IN A %s
* 1800 IN A %s
"abra record new --auto" can help you do this quickly if you use a supported
DNS provider.
`, `,
internal.HetznerCloudName, ip, rootPassword, internal.HetznerCloudName, ip, rootPassword,
ip, ip, ip, ip,
)) ))
return nil return nil
@ -181,15 +169,6 @@ Please note, this server is not managed by Abra yet (i.e. "abra server ls" will
not list this server)! You will need to assign a domain name record ("abra not list this server)! You will need to assign a domain name record ("abra
record new") and add the server to your Abra configuration ("abra server add") record new") and add the server to your Abra configuration ("abra server add")
to have a working server that you can deploy Co-op Cloud apps to. to have a working server that you can deploy Co-op Cloud apps to.
When setting up domain name records, you probably want to set up the following
2 A records. This supports deploying apps to your root domain (e.g.
example.com) and other apps on sub-domains (e.g. foo.example.com,
bar.example.com).
@ 1800 IN A <your-capsul-ip>
* 1800 IN A <your-capsul-ip>
`, internal.CapsulName, resp.ID, internal.CapsulInstanceURL)) `, internal.CapsulName, resp.ID, internal.CapsulInstanceURL))
return nil return nil

View File

@ -128,22 +128,6 @@ like tears in rain.
logrus.Fatal(err) logrus.Fatal(err)
} }
if !rmServer {
logrus.Warn("did not pass -s/--server for actual server deletion, prompting")
response := false
prompt := &survey.Confirm{
Message: "prompt to actual server deletion?",
}
if err := survey.AskOne(prompt, &response); err != nil {
logrus.Fatal(err)
}
if response {
logrus.Info("setting -s/--server and attempting to remove actual server")
rmServer = true
}
}
if rmServer { if rmServer {
if err := internal.EnsureServerProvider(); err != nil { if err := internal.EnsureServerProvider(); err != nil {
logrus.Fatal(err) logrus.Fatal(err)

View File

@ -1,7 +1,6 @@
package cli package cli
import ( import (
"fmt"
"os/exec" "os/exec"
"coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/cli/internal"
@ -9,36 +8,28 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
var mainURL = "https://install.abra.coopcloud.tech" var RC bool
var RCFlag = &cli.BoolFlag{
var releaseCandidateURL = "https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/installer/installer" Name: "rc",
Value: false,
Destination: &RC,
Usage: "Insatll the latest Release Candidate",
}
// UpgradeCommand upgrades abra in-place. // UpgradeCommand upgrades abra in-place.
var UpgradeCommand = &cli.Command{ var UpgradeCommand = &cli.Command{
Name: "upgrade", Name: "upgrade",
Usage: "Upgrade Abra", Usage: "Upgrade abra",
Description: ` Flags: []cli.Flag{RCFlag},
This command allows you to upgrade Abra in-place with the latest stable or
release candidate.
If you would like to install the latest release candidate, please pass the
"--rc" option. Please bear in mind that the latest release candidate may have
some catastrophic bugs contained in it. In any case, thank you very much for
testing efforts!
`,
Flags: []cli.Flag{internal.RCFlag},
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
cmd := exec.Command("bash", "-c", fmt.Sprintf("curl -s %s | bash", mainURL)) cmd := exec.Command("bash", "-c", "curl -s https://install.abra.coopcloud.tech | bash")
if internal.RC { if RC {
cmd = exec.Command("bash", "-c", fmt.Sprintf("curl -s %s | bash -s -- --rc", releaseCandidateURL)) cmd = exec.Command("bash", "-c", "curl -s https://git.coopcloud.tech/coop-cloud/abra/raw/branch/main/scripts/installer/installer | bash -s -- --rc")
} }
logrus.Debugf("attempting to run '%s'", cmd) logrus.Debugf("attempting to run '%s'", cmd)
if err := internal.RunCmd(cmd); err != nil { if err := internal.RunCmd(cmd); err != nil {
logrus.Fatal(err) logrus.Fatal(err)
} }
return nil return nil
}, },
} }

View File

@ -12,6 +12,7 @@ var Version string
var Commit string var Commit string
func main() { func main() {
// If not set in the ld-flags
if Version == "" { if Version == "" {
Version = "dev" Version = "dev"
} }

10
go.mod
View File

@ -7,9 +7,9 @@ require (
github.com/AlecAivazis/survey/v2 v2.3.2 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.20210731170023-c37c0920d1a4
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4 github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4
github.com/docker/cli v20.10.12+incompatible github.com/docker/cli v20.10.11+incompatible
github.com/docker/distribution v2.7.1+incompatible github.com/docker/distribution v2.7.1+incompatible
github.com/docker/docker v20.10.12+incompatible github.com/docker/docker v20.10.11+incompatible
github.com/docker/go-units v0.4.0 github.com/docker/go-units v0.4.0
github.com/go-git/go-git/v5 v5.4.2 github.com/go-git/go-git/v5 v5.4.2
github.com/hetznercloud/hcloud-go v1.33.1 github.com/hetznercloud/hcloud-go v1.33.1
@ -17,7 +17,7 @@ require (
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6
github.com/olekukonko/tablewriter v0.0.5 github.com/olekukonko/tablewriter v0.0.5
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/schollz/progressbar/v3 v3.8.5 github.com/schollz/progressbar/v3 v3.8.3
github.com/schultz-is/passgen v1.0.1 github.com/schultz-is/passgen v1.0.1
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1
github.com/urfave/cli/v2 v2.3.0 github.com/urfave/cli/v2 v2.3.0
@ -43,6 +43,6 @@ require (
github.com/opencontainers/runc v1.0.2 // indirect github.com/opencontainers/runc v1.0.2 // indirect
github.com/theupdateframework/notary v0.7.0 // indirect github.com/theupdateframework/notary v0.7.0 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359
) )

31
go.sum
View File

@ -260,14 +260,14 @@ github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/docker/cli v20.10.12+incompatible h1:lZlz0uzG+GH+c0plStMUdF/qk3ppmgnswpR5EbqzVGA= github.com/docker/cli v20.10.11+incompatible h1:tXU1ezXcruZQRrMP8RN2z9N91h+6egZTS1gsPsKantc=
github.com/docker/cli v20.10.12+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v20.10.11+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v20.10.12+incompatible h1:CEeNmFM0QZIsJCZKMkZx0ZcahTiewkrgiwfYD+dfl1U= github.com/docker/docker v20.10.11+incompatible h1:OqzI/g/W54LczvhnccGqniFoQghHx3pklbLuhfXpqGo=
github.com/docker/docker v20.10.12+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v20.10.11+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.6.4 h1:axCks+yV+2MR3/kZhAmy07yC56WZ2Pwu/fKWtKuZB0o= github.com/docker/docker-credential-helpers v0.6.4 h1:axCks+yV+2MR3/kZhAmy07yC56WZ2Pwu/fKWtKuZB0o=
github.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c= github.com/docker/docker-credential-helpers v0.6.4/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0= github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
@ -695,8 +695,8 @@ github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/schollz/progressbar/v3 v3.8.5 h1:VcmmNRO+eFN3B0m5dta6FXYXY+MEJmXdWoIS+jjssQM= github.com/schollz/progressbar/v3 v3.8.3 h1:FnLGl3ewlDUP+YdSwveXBaXs053Mem/du+wr7XSYKl8=
github.com/schollz/progressbar/v3 v3.8.5/go.mod h1:ewO25kD7ZlaJFTvMeOItkOZa8kXu1UvFs379htE8HMQ= github.com/schollz/progressbar/v3 v3.8.3/go.mod h1:pWnVCjSBZsT2X3nx9HfRdnCDrpbevliMeoEVhStwHko=
github.com/schultz-is/passgen v1.0.1 h1:wUINzqW1Xmmy3yREHR6YTj+83VlFYjj2DIDMHzIi5TQ= github.com/schultz-is/passgen v1.0.1 h1:wUINzqW1Xmmy3yREHR6YTj+83VlFYjj2DIDMHzIi5TQ=
github.com/schultz-is/passgen v1.0.1/go.mod h1:NnqzT2aSfvyheNQvBtlLUa0YlPFLDj60Jw2DZVwqiJk= github.com/schultz-is/passgen v1.0.1/go.mod h1:NnqzT2aSfvyheNQvBtlLUa0YlPFLDj60Jw2DZVwqiJk=
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
@ -829,8 +829,8 @@ golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWP
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -896,9 +896,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210326060303-6b1517762897 h1:KrsHThm5nFk34YtATK1LsThyGhGbGe1olrte/HInHvs=
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -976,29 +975,27 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 h1:2B5p2L5IfGiD7+b9BOoRMC6DgObAVZV+Fsp050NqXik=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

View File

@ -57,9 +57,9 @@ func DeployedVersions(ctx context.Context, cl *apiclient.Client, app config.App)
deployed := len(services) > 0 deployed := len(services) > 0
if deployed { if deployed {
logrus.Debugf("detected %s as deployed versions of %s", appSpec, app.Name) logrus.Debugf("detected '%s' as deployed versions of '%s'", appSpec, app.Name)
} else { } else {
logrus.Debugf("detected %s as not deployed", app.Name) logrus.Debugf("detected '%s' as not deployed", app.Name)
} }
return appSpec, len(services) > 0, nil return appSpec, len(services) > 0, nil
@ -71,15 +71,15 @@ func ParseVersionLabel(label string) (string, string) {
idx := strings.LastIndex(label, "-") idx := strings.LastIndex(label, "-")
version := label[:idx] version := label[:idx]
digest := label[idx+1:] digest := label[idx+1:]
logrus.Debugf("parsed %s as version from %s", version, label) logrus.Debugf("parsed '%s' as version from '%s'", version, label)
logrus.Debugf("parsed %s as digest from %s", digest, label) logrus.Debugf("parsed '%s' as digest from '%s'", digest, label)
return version, digest return version, digest
} }
// ParseServiceName parses a $STACK_NAME_$SERVICE_NAME service label. // ParseVersionName parses a $STACK_NAME_$SERVICE_NAME service label.
func ParseServiceName(label string) string { func ParseServiceName(label string) string {
idx := strings.LastIndex(label, "_") idx := strings.LastIndex(label, "_")
serviceName := label[idx+1:] serviceName := label[idx+1:]
logrus.Debugf("parsed %s as service name from %s", serviceName, label) logrus.Debugf("parsed '%s' as service name from '%s'", serviceName, label)
return serviceName return serviceName
} }

View File

@ -1,42 +0,0 @@
package autocomplete
import (
"fmt"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/config"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
// AppNameComplete copletes app names
func AppNameComplete(c *cli.Context) {
appNames, err := config.GetAppNames()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for _, a := range appNames {
fmt.Println(a)
}
}
// RecipeNameComplete completes recipe names
func RecipeNameComplete(c *cli.Context) {
catl, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Warn(err)
}
if c.NArg() > 0 {
return
}
for name := range catl {
fmt.Println(name)
}
}

View File

@ -12,7 +12,6 @@ import (
"strings" "strings"
"time" "time"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config" "coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/recipe" "coopcloud.tech/abra/pkg/recipe"
@ -89,7 +88,7 @@ func (r RecipeMeta) LatestVersion() string {
version = tag version = tag
} }
logrus.Debugf("choosing %s as latest version of %s", version, r.Name) logrus.Debugf("choosing '%s' as latest version of '%s'", version, r.Name)
return version return version
} }
@ -152,7 +151,7 @@ func recipeCatalogueFSIsLatest() (bool, error) {
return false, nil return false, nil
} }
logrus.Debug("file system cached recipe catalogue is now up-to-date") logrus.Debug("file system cached recipe catalogue is up-to-date")
return true, nil return true, nil
} }
@ -193,7 +192,7 @@ func readRecipeCatalogueFS(target interface{}) error {
return err return err
} }
logrus.Debugf("read recipe catalogue from file system cache in %s", config.APPS_JSON) logrus.Debugf("read recipe catalogue from file system cache in '%s'", config.APPS_JSON)
return nil return nil
} }
@ -209,19 +208,17 @@ func readRecipeCatalogueWeb(target interface{}) error {
return err return err
} }
if err := ioutil.WriteFile(config.APPS_JSON, recipesJSON, 0764); err != nil { if err := ioutil.WriteFile(config.APPS_JSON, recipesJSON, 0644); err != nil {
return err return err
} }
logrus.Debugf("read recipe catalogue from web at %s", RecipeCatalogueURL) logrus.Debugf("read recipe catalogue from web at '%s'", RecipeCatalogueURL)
return nil return nil
} }
// VersionsOfService lists the version of a service. // VersionsOfService lists the version of a service.
func VersionsOfService(recipe, serviceName string) ([]string, error) { func VersionsOfService(recipe, serviceName string) ([]string, error) {
var versions []string
catalogue, err := ReadRecipeCatalogue() catalogue, err := ReadRecipeCatalogue()
if err != nil { if err != nil {
return nil, err return nil, err
@ -229,9 +226,10 @@ func VersionsOfService(recipe, serviceName string) ([]string, error) {
rec, ok := catalogue[recipe] rec, ok := catalogue[recipe]
if !ok { if !ok {
return versions, nil return nil, fmt.Errorf("recipe '%s' does not exist?", recipe)
} }
versions := []string{}
alreadySeen := make(map[string]bool) alreadySeen := make(map[string]bool)
for _, serviceVersion := range rec.Versions { for _, serviceVersion := range rec.Versions {
for tag := range serviceVersion { for tag := range serviceVersion {
@ -242,7 +240,7 @@ func VersionsOfService(recipe, serviceName string) ([]string, error) {
} }
} }
logrus.Debugf("detected versions %s for %s", strings.Join(versions, ", "), recipe) logrus.Debugf("detected versions '%s' for '%s'", strings.Join(versions, ", "), recipe)
return versions, nil return versions, nil
} }
@ -256,7 +254,7 @@ func GetRecipeMeta(recipeName string) (RecipeMeta, error) {
recipeMeta, ok := catl[recipeName] recipeMeta, ok := catl[recipeName]
if !ok { if !ok {
err := fmt.Errorf("recipe %s does not exist?", recipeName) err := fmt.Errorf("recipe '%s' does not exist?", recipeName)
return RecipeMeta{}, err return RecipeMeta{}, err
} }
@ -264,7 +262,7 @@ func GetRecipeMeta(recipeName string) (RecipeMeta, error) {
return RecipeMeta{}, err return RecipeMeta{}, err
} }
logrus.Debugf("recipe metadata retrieved for %s", recipeName) logrus.Debugf("recipe metadata retrieved for '%s'", recipeName)
return recipeMeta, nil return recipeMeta, nil
} }
@ -351,20 +349,18 @@ func ReadReposMetadata() (RepoCatalogue, error) {
reposMeta := make(RepoCatalogue) reposMeta := make(RepoCatalogue)
pageIdx := 1 pageIdx := 1
bar := formatter.CreateProgressbar(-1, "retrieving recipe repos list from git.coopcloud.tech...")
for { for {
var reposList []RepoMeta var reposList []RepoMeta
pagedURL := fmt.Sprintf("%s?page=%v", ReposMetadataURL, pageIdx) pagedURL := fmt.Sprintf("%s?page=%v", ReposMetadataURL, pageIdx)
logrus.Debugf("fetching repo metadata from %s", pagedURL) logrus.Debugf("fetching repo metadata from '%s'", pagedURL)
if err := web.ReadJSON(pagedURL, &reposList); err != nil { if err := web.ReadJSON(pagedURL, &reposList); err != nil {
return reposMeta, err return reposMeta, err
} }
if len(reposList) == 0 { if len(reposList) == 0 {
bar.Add(1)
break break
} }
@ -373,7 +369,6 @@ func ReadReposMetadata() (RepoCatalogue, error) {
} }
pageIdx++ pageIdx++
bar.Add(1)
} }
return reposMeta, nil return reposMeta, nil
@ -393,7 +388,7 @@ func GetStringInBetween(str, start, end string) (result string, err error) {
return str[s : s+e], nil return str[s : s+e], nil
} }
func GetImageMetadata(imageRowString, recipeName string) (image, error) { func GetImageMetadata(imageRowString string) (image, error) {
img := image{} img := image{}
imgFields := strings.Split(imageRowString, ",") imgFields := strings.Split(imageRowString, ",")
@ -403,11 +398,7 @@ func GetImageMetadata(imageRowString, recipeName string) (image, error) {
} }
if len(imgFields) < 3 { if len(imgFields) < 3 {
if imageRowString != "" { logrus.Warnf("image string has incorrect format: %s", imageRowString)
logrus.Warnf("%s image meta has incorrect format: %s", recipeName, imageRowString)
} else {
logrus.Warnf("%s image meta is empty?", recipeName)
}
return img, nil return img, nil
} }
@ -438,7 +429,7 @@ func GetRecipeFeaturesAndCategory(recipeName string) (features, string, error) {
readmePath := path.Join(config.ABRA_DIR, "apps", recipeName, "README.md") readmePath := path.Join(config.ABRA_DIR, "apps", recipeName, "README.md")
logrus.Debugf("attempting to open %s for recipe metadata parsing", readmePath) logrus.Debugf("attempting to open '%s'", readmePath)
readmeFS, err := ioutil.ReadFile(readmePath) readmeFS, err := ioutil.ReadFile(readmePath)
if err != nil { if err != nil {
@ -493,7 +484,7 @@ func GetRecipeFeaturesAndCategory(recipeName string) (features, string, error) {
if strings.Contains(val, "**Image**") { if strings.Contains(val, "**Image**") {
imageMetadata, err := GetImageMetadata(strings.TrimSpace( imageMetadata, err := GetImageMetadata(strings.TrimSpace(
strings.TrimPrefix(val, "* **Image**:"), strings.TrimPrefix(val, "* **Image**:"),
), recipeName) ))
if err != nil { if err != nil {
continue continue
} }
@ -510,7 +501,7 @@ func GetRecipeVersions(recipeName string) (RecipeVersions, error) {
recipeDir := path.Join(config.ABRA_DIR, "apps", recipeName) recipeDir := path.Join(config.ABRA_DIR, "apps", recipeName)
logrus.Debugf("attempting to open git repository in %s", recipeDir) logrus.Debugf("attempting to open git repository in '%s'", recipeDir)
repo, err := git.PlainOpen(recipeDir) repo, err := git.PlainOpen(recipeDir)
if err != nil { if err != nil {
@ -530,30 +521,25 @@ func GetRecipeVersions(recipeName string) (RecipeVersions, error) {
if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) { if err := gitTags.ForEach(func(ref *plumbing.Reference) (err error) {
tag := strings.TrimPrefix(string(ref.Name()), "refs/tags/") tag := strings.TrimPrefix(string(ref.Name()), "refs/tags/")
logrus.Debugf("processing %s for %s", tag, recipeName) logrus.Debugf("processing '%s' for '%s'", tag, recipeName)
checkOutOpts := &git.CheckoutOptions{ checkOutOpts := &git.CheckoutOptions{
Create: false, Create: false,
Force: true, Keep: true,
Branch: plumbing.ReferenceName(ref.Name()), Branch: plumbing.ReferenceName(ref.Name()),
} }
if err := worktree.Checkout(checkOutOpts); err != nil { if err := worktree.Checkout(checkOutOpts); err != nil {
logrus.Debugf("failed to check out %s in %s", tag, recipeDir) logrus.Debugf("failed to check out '%s' in '%s'", tag, recipeDir)
return err return err
} }
logrus.Debugf("successfully checked out %s in %s", ref.Name(), recipeDir) logrus.Debugf("successfully checked out '%s' in '%s'", ref.Name(), recipeDir)
recipe, err := recipe.Get(recipeName) recipe, err := recipe.Get(recipeName)
if err != nil { if err != nil {
return err return err
} }
cl, err := client.New("default") // only required for docker.io registry calls
if err != nil {
logrus.Fatal(err)
}
versionMeta := make(map[string]ServiceMeta) versionMeta := make(map[string]ServiceMeta)
for _, service := range recipe.Config.Services { for _, service := range recipe.Config.Services {
@ -576,10 +562,11 @@ func GetRecipeVersions(recipeName string) (RecipeVersions, error) {
continue continue
} }
logrus.Debugf("looking up image: %s from %s", img, path) logrus.Debugf("looking up image: '%s' from '%s'", img, path)
digest, err := client.GetTagDigest(cl, img) digest, err := client.GetTagDigest(img)
if err != nil { if err != nil {
// return err
logrus.Warn(err) logrus.Warn(err)
continue continue
} }
@ -590,7 +577,7 @@ func GetRecipeVersions(recipeName string) (RecipeVersions, error) {
Tag: img.(reference.NamedTagged).Tag(), Tag: img.(reference.NamedTagged).Tag(),
} }
logrus.Debugf("collecting digest: %s, image: %s, tag: %s", digest, path, tag) logrus.Debugf("collecting digest: '%s', image: '%s', tag: '%s'", digest, path, tag)
} }
versions = append(versions, map[string]map[string]ServiceMeta{tag: versionMeta}) versions = append(versions, map[string]map[string]ServiceMeta{tag: versionMeta})
@ -603,7 +590,7 @@ func GetRecipeVersions(recipeName string) (RecipeVersions, error) {
branch := "master" branch := "master"
if _, err := repo.Branch("master"); err != nil { if _, err := repo.Branch("master"); err != nil {
if _, err := repo.Branch("main"); err != nil { if _, err := repo.Branch("main"); err != nil {
logrus.Debugf("failed to select branch in %s", recipeDir) logrus.Debugf("failed to select branch in '%s'", recipeDir)
logrus.Fatal(err) logrus.Fatal(err)
} }
branch = "main" branch = "main"
@ -612,16 +599,16 @@ func GetRecipeVersions(recipeName string) (RecipeVersions, error) {
refName := fmt.Sprintf("refs/heads/%s", branch) refName := fmt.Sprintf("refs/heads/%s", branch)
checkOutOpts := &git.CheckoutOptions{ checkOutOpts := &git.CheckoutOptions{
Create: false, Create: false,
Force: true, Keep: true,
Branch: plumbing.ReferenceName(refName), Branch: plumbing.ReferenceName(refName),
} }
if err := worktree.Checkout(checkOutOpts); err != nil { if err := worktree.Checkout(checkOutOpts); err != nil {
logrus.Debugf("failed to check out %s in %s", branch, recipeDir) logrus.Debugf("failed to check out '%s' in '%s'", branch, recipeDir)
logrus.Fatal(err) logrus.Fatal(err)
} }
logrus.Debugf("switched back to %s in %s", branch, recipeDir) logrus.Debugf("switched back to '%s' in '%s'", branch, recipeDir)
logrus.Debugf("collected %s for %s", versions, recipeName) logrus.Debugf("collected '%s' for '%s'", versions, recipeName)
return versions, nil return versions, nil
} }

View File

@ -1,17 +1,19 @@
package client package client
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"os"
"strings" "strings"
"os"
"coopcloud.tech/abra/pkg/web" "coopcloud.tech/abra/pkg/web"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/docker/docker/client"
"github.com/hashicorp/go-retryablehttp" "github.com/hashicorp/go-retryablehttp"
dockerClient "github.com/docker/docker/client"
"github.com/docker/docker/api/types"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -36,20 +38,10 @@ func GetRegistryTags(image string) (RawTags, error) {
} }
// getRegv2Token retrieves a registry v2 authentication token. // getRegv2Token retrieves a registry v2 authentication token.
func getRegv2Token(cl *client.Client, image reference.Named) (string, error) { func getRegv2Token(image reference.Named) (string, error) {
img := reference.Path(image) img := reference.Path(image)
tokenURL := "https://auth.docker.io/token" authTokenURL := fmt.Sprintf("https://auth.docker.io/token?service=registry.docker.io&scope=repository:%s:pull", img)
values := fmt.Sprintf("service=registry.docker.io&scope=repository:%s:pull", img) req, err := retryablehttp.NewRequest("GET", authTokenURL, nil)
username, userOk := os.LookupEnv("DOCKER_USERNAME")
password, passOk := os.LookupEnv("DOCKER_PASSWORD")
if userOk && passOk {
logrus.Debugf("using docker log in credentials for registry token request")
values = fmt.Sprintf("%s&grant_type=password&client_id=coopcloud.tech&username=%s&password=%s", values, username, password)
}
fullURL := fmt.Sprintf("%s?%s", tokenURL, values)
req, err := retryablehttp.NewRequest("GET", fullURL, nil)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -74,10 +66,9 @@ func getRegv2Token(cl *client.Client, image reference.Named) (string, error) {
} }
tokenRes := struct { tokenRes := struct {
AccessToken string `json:"access_token"` Token string
Expiry int `json:"expires_in"` Expiry string
Issued string `json:"issued_at"` Issued string
Token string `json:"token"`
}{} }{}
if err := json.Unmarshal(body, &tokenRes); err != nil { if err := json.Unmarshal(body, &tokenRes); err != nil {
@ -88,7 +79,7 @@ func getRegv2Token(cl *client.Client, image reference.Named) (string, error) {
} }
// GetTagDigest retrieves an image digest from a v2 registry // GetTagDigest retrieves an image digest from a v2 registry
func GetTagDigest(cl *client.Client, image reference.Named) (string, error) { func GetTagDigest(image reference.Named) (string, error) {
img := reference.Path(image) img := reference.Path(image)
tag := image.(reference.NamedTagged).Tag() tag := image.(reference.NamedTagged).Tag()
manifestURL := fmt.Sprintf("https://index.docker.io/v2/%s/manifests/%s", img, tag) manifestURL := fmt.Sprintf("https://index.docker.io/v2/%s/manifests/%s", img, tag)
@ -98,13 +89,23 @@ func GetTagDigest(cl *client.Client, image reference.Named) (string, error) {
return "", err return "", err
} }
token, err := getRegv2Token(cl, image) dClient := dockerClient.Client{}
if err != nil {
return "", err username, username_ok := os.LookupEnv("DOCKER_USERNAME")
password, password_ok := os.LookupEnv("DOCKER_PASSWORD")
if username_ok && password_ok {
logrus.Debugf("docker login: %s, %s", username, password)
dClient.RegistryLogin(context.Background(), types.AuthConfig{
Username: username,
Password: password,
ServerAddress: "docker.io",
})
return "", nil
} }
if token == "" { token, err := getRegv2Token(image)
return "", fmt.Errorf("unable to retrieve registry token?") if err != nil {
return "", err
} }
req.Header = http.Header{ req.Header = http.Header{

View File

@ -71,7 +71,7 @@ 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), 0644); err != nil {
return err return err
} }
} }
@ -88,7 +88,7 @@ func UpdateLabel(pattern, serviceName, label, recipeName string) error {
return err return err
} }
logrus.Debugf("considering %s config(s) for label update", strings.Join(composeFiles, ", ")) logrus.Debugf("considering '%s' config(s) for label update", strings.Join(composeFiles, ", "))
for _, composeFile := range composeFiles { for _, composeFile := range composeFiles {
opts := stack.Deploy{Composefiles: []string{composeFile}} opts := stack.Deploy{Composefiles: []string{composeFile}}
@ -130,25 +130,19 @@ func UpdateLabel(pattern, serviceName, label, recipeName string) error {
old := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", value) old := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", value)
replacedBytes := strings.Replace(string(bytes), old, label, -1) replacedBytes := strings.Replace(string(bytes), old, label, -1)
if old == label {
logrus.Warnf("%s is already set, nothing to do?", label)
return nil
}
logrus.Debugf("updating %s to %s in %s", old, label, compose.Filename) logrus.Debugf("updating %s to %s in %s", old, label, compose.Filename)
if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0764); err != nil { if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0644); err != nil {
return err return err
} }
logrus.Infof("synced label %s to service %s", label, serviceName)
} }
} }
if !discovered { if !discovered {
logrus.Warn("no existing label found, cannot continue...") logrus.Warn("no existing label found, cannot continue...")
logrus.Fatalf("add \"%s\" manually, automagic insertion not supported yet", label) logrus.Fatalf("add '%s' manually, automagic insertion not supported yet", label)
} }
} }
return nil return nil

View File

@ -1,6 +1,7 @@
package config package config
import ( import (
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
@ -111,17 +112,17 @@ func readAppEnvFile(appFile AppFile, name AppName) (App, error) {
// newApp creates new App object // newApp creates new App object
func newApp(env AppEnv, name string, appFile AppFile) (App, error) { func newApp(env AppEnv, name string, appFile AppFile) (App, error) {
// Checking for type as it is required - apps wont work without it
domain := env["DOMAIN"] domain := env["DOMAIN"]
apptype, ok := env["TYPE"]
appType, exists := env["TYPE"] if !ok {
if !exists { return App{}, errors.New("missing TYPE variable")
return App{}, fmt.Errorf("%s is missing the TYPE env var", name)
} }
return App{ return App{
Name: name, Name: name,
Domain: domain, Domain: domain,
Type: appType, Type: apptype,
Env: env, Env: env,
Server: appFile.Server, Server: appFile.Server,
Path: appFile.Path, Path: appFile.Path,
@ -135,14 +136,14 @@ func LoadAppFiles(servers ...string) (AppFiles, error) {
if servers[0] == "" { if servers[0] == "" {
// Empty servers flag, one string will always be passed // Empty servers flag, one string will always be passed
var err error var err error
servers, err = GetAllFoldersInDirectory(ABRA_SERVER_FOLDER) servers, err = getAllFoldersInDirectory(ABRA_SERVER_FOLDER)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
} }
logrus.Debugf("collecting metadata from %v servers: %s", len(servers), strings.Join(servers, ", ")) logrus.Debugf("collecting metadata from '%v' servers: '%s'", len(servers), strings.Join(servers, ", "))
for _, server := range servers { for _, server := range servers {
serverDir := path.Join(ABRA_SERVER_FOLDER, server) serverDir := path.Join(ABRA_SERVER_FOLDER, server)
@ -168,7 +169,7 @@ func LoadAppFiles(servers ...string) (AppFiles, error) {
func GetApp(apps AppFiles, name AppName) (App, error) { func GetApp(apps AppFiles, name AppName) (App, error) {
appFile, exists := apps[name] appFile, exists := apps[name]
if !exists { if !exists {
return App{}, fmt.Errorf("cannot find app with name %s", name) return App{}, fmt.Errorf("cannot find app with name '%s'", name)
} }
app, err := readAppEnvFile(appFile, name) app, err := readAppEnvFile(appFile, name)
@ -248,27 +249,27 @@ func GetAppNames() ([]string, error) {
} }
// TemplateAppEnvSample copies the example env file for the app into the users env files // TemplateAppEnvSample copies the example env file for the app into the users env files
func TemplateAppEnvSample(recipe, appName, server, domain string) error { func TemplateAppEnvSample(appType, appName, server, domain, recipe string) error {
envSamplePath := path.Join(ABRA_DIR, "apps", recipe, ".env.sample") envSamplePath := path.Join(ABRA_DIR, "apps", appType, ".env.sample")
envSample, err := ioutil.ReadFile(envSamplePath) envSample, err := ioutil.ReadFile(envSamplePath)
if err != nil { if err != nil {
return err return err
} }
appEnvPath := path.Join(ABRA_DIR, "servers", server, fmt.Sprintf("%s.env", appName)) appEnvPath := path.Join(ABRA_DIR, "servers", server, fmt.Sprintf("%s.env", appName))
if _, err := os.Stat(appEnvPath); os.IsExist(err) { if _, err := os.Stat(appEnvPath); err == nil {
return fmt.Errorf("%s already exists?", appEnvPath) return fmt.Errorf("%s already exists?", appEnvPath)
} }
envSample = []byte(strings.Replace(string(envSample), fmt.Sprintf("%s.example.com", recipe), domain, -1)) envSample = []byte(strings.Replace(string(envSample), fmt.Sprintf("%s.example.com", recipe), domain, -1))
envSample = []byte(strings.Replace(string(envSample), "example.com", domain, -1)) envSample = []byte(strings.Replace(string(envSample), "example.com", domain, -1))
err = ioutil.WriteFile(appEnvPath, envSample, 0664) err = ioutil.WriteFile(appEnvPath, envSample, 0755)
if err != nil { if err != nil {
return err return err
} }
logrus.Debugf("copied %s to %s", envSamplePath, appEnvPath) logrus.Debugf("copied '%s' to '%s'", envSamplePath, appEnvPath)
return nil return nil
} }
@ -324,7 +325,7 @@ func GetAppStatuses(appFiles AppFiles) (map[string]map[string]string, error) {
} }
} }
logrus.Debugf("retrieved app statuses: %s", statuses) logrus.Debugf("retrieved app statuses: '%s'", statuses)
return statuses, nil return statuses, nil
} }
@ -343,13 +344,13 @@ func GetAppComposeFiles(recipe string, appEnv AppEnv) ([]string, error) {
composeFileEnvVar := appEnv["COMPOSE_FILE"] composeFileEnvVar := appEnv["COMPOSE_FILE"]
envVars := strings.Split(composeFileEnvVar, ":") envVars := strings.Split(composeFileEnvVar, ":")
logrus.Debugf("COMPOSE_FILE detected (%s), loading %s", composeFileEnvVar, strings.Join(envVars, ", ")) logrus.Debugf("COMPOSE_FILE detected ('%s'), loading '%s'", composeFileEnvVar, strings.Join(envVars, ", "))
for _, file := range strings.Split(composeFileEnvVar, ":") { for _, file := range strings.Split(composeFileEnvVar, ":") {
path := fmt.Sprintf("%s/%s/%s", APPS_DIR, recipe, file) path := fmt.Sprintf("%s/%s/%s", APPS_DIR, recipe, file)
composeFiles = append(composeFiles, path) composeFiles = append(composeFiles, path)
} }
logrus.Debugf("retrieved %s configs for %s", strings.Join(composeFiles, ", "), recipe) logrus.Debugf("retrieved '%s' configs for '%s'", strings.Join(composeFiles, ", "), recipe)
return composeFiles, nil return composeFiles, nil
} }
@ -363,7 +364,7 @@ func GetAppComposeConfig(recipe string, opts stack.Deploy, appEnv AppEnv) (*comp
return &composetypes.Config{}, err return &composetypes.Config{}, err
} }
logrus.Debugf("retrieved %s for %s", compose.Filename, recipe) logrus.Debugf("retrieved '%s' for '%s'", compose.Filename, recipe)
return compose, nil return compose, nil
} }

View File

@ -16,7 +16,7 @@ import (
var ABRA_DIR = os.ExpandEnv("$HOME/.abra") var ABRA_DIR = os.ExpandEnv("$HOME/.abra")
var ABRA_SERVER_FOLDER = path.Join(ABRA_DIR, "servers") var ABRA_SERVER_FOLDER = path.Join(ABRA_DIR, "servers")
var APPS_JSON = path.Join(ABRA_DIR, "catalogue", "recipes.json") var APPS_JSON = path.Join(ABRA_DIR, "apps.json")
var APPS_DIR = path.Join(ABRA_DIR, "apps") var APPS_DIR = path.Join(ABRA_DIR, "apps")
var REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud" var REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud"
@ -24,12 +24,12 @@ var REPOS_BASE_URL = "https://git.coopcloud.tech/coop-cloud"
func GetServers() ([]string, error) { func GetServers() ([]string, error) {
var servers []string var servers []string
servers, err := GetAllFoldersInDirectory(ABRA_SERVER_FOLDER) servers, err := getAllFoldersInDirectory(ABRA_SERVER_FOLDER)
if err != nil { if err != nil {
return servers, err return servers, err
} }
logrus.Debugf("retrieved %v servers: %s", len(servers), servers) logrus.Debugf("retrieved '%v' servers: '%s'", len(servers), servers)
return servers, nil return servers, nil
} }
@ -43,20 +43,20 @@ func ReadEnv(filePath string) (AppEnv, error) {
return nil, err return nil, err
} }
logrus.Debugf("read %s from %s", envFile, filePath) logrus.Debugf("read '%s' from '%s'", envFile, filePath)
return envFile, nil return envFile, nil
} }
// ReadServerNames retrieves all server names. // ReadServerNames retrieves all server names.
func ReadServerNames() ([]string, error) { func ReadServerNames() ([]string, error) {
serverNames, err := GetAllFoldersInDirectory(ABRA_SERVER_FOLDER) serverNames, err := getAllFoldersInDirectory(ABRA_SERVER_FOLDER)
if err != nil { if err != nil {
return nil, err return nil, err
} }
logrus.Debugf("read %s from %s", strings.Join(serverNames, ","), ABRA_SERVER_FOLDER) logrus.Debugf("read '%s' from '%s'", strings.Join(serverNames, ","), ABRA_SERVER_FOLDER)
return serverNames, nil return serverNames, nil
} }
@ -80,7 +80,7 @@ func getAllFilesInDirectory(directory string) ([]fs.FileInfo, error) {
realPath, err := filepath.EvalSymlinks(filePath) realPath, err := filepath.EvalSymlinks(filePath)
if err != nil { if err != nil {
logrus.Warningf("broken symlink in your abra config folders: %s", filePath) logrus.Warningf("broken symlink in your abra config folders: '%s'", filePath)
} else { } else {
realFile, err := os.Stat(realPath) realFile, err := os.Stat(realPath)
if err != nil { if err != nil {
@ -95,8 +95,8 @@ func getAllFilesInDirectory(directory string) ([]fs.FileInfo, error) {
return realFiles, nil return realFiles, nil
} }
// GetAllFoldersInDirectory returns both folder and symlink paths // getAllFoldersInDirectory returns both folder and symlink paths
func GetAllFoldersInDirectory(directory string) ([]string, error) { func getAllFoldersInDirectory(directory string) ([]string, error) {
var folders []string var folders []string
files, err := ioutil.ReadDir(directory) files, err := ioutil.ReadDir(directory)
@ -104,7 +104,7 @@ func GetAllFoldersInDirectory(directory string) ([]string, error) {
return nil, err return nil, err
} }
if len(files) == 0 { if len(files) == 0 {
return nil, fmt.Errorf("directory is empty: %s", directory) return nil, fmt.Errorf("directory is empty: '%s'", directory)
} }
for _, file := range files { for _, file := range files {
@ -113,7 +113,7 @@ func GetAllFoldersInDirectory(directory string) ([]string, error) {
filePath := path.Join(directory, file.Name()) filePath := path.Join(directory, file.Name())
realDir, err := filepath.EvalSymlinks(filePath) realDir, err := filepath.EvalSymlinks(filePath)
if err != nil { if err != nil {
logrus.Warningf("broken symlink in your abra config folders: %s", filePath) logrus.Warningf("broken symlink in your abra config folders: '%s'", filePath)
} else if stat, err := os.Stat(realDir); err == nil && stat.IsDir() { } else if stat, err := os.Stat(realDir); err == nil && stat.IsDir() {
// path is a directory // path is a directory
folders = append(folders, file.Name()) folders = append(folders, file.Name())
@ -127,8 +127,8 @@ func GetAllFoldersInDirectory(directory string) ([]string, error) {
// EnsureAbraDirExists checks for the abra config folder and throws error if not // EnsureAbraDirExists checks for the abra config folder and throws error if not
func EnsureAbraDirExists() error { func EnsureAbraDirExists() error {
if _, err := os.Stat(ABRA_DIR); os.IsNotExist(err) { if _, err := os.Stat(ABRA_DIR); os.IsNotExist(err) {
logrus.Debugf("%s does not exist, creating it", ABRA_DIR) logrus.Debugf("'%s' does not exist, creating it", ABRA_DIR)
if err := os.Mkdir(ABRA_DIR, 0764); err != nil { if err := os.Mkdir(ABRA_DIR, 0777); err != nil {
return err return err
} }
} }
@ -161,7 +161,7 @@ func ReadAbraShEnvVars(abraSh string) (map[string]string, error) {
} }
} }
logrus.Debugf("read %s from %s", envVars, abraSh) logrus.Debugf("read '%s' from '%s'", envVars, abraSh)
return envVars, nil return envVars, nil
} }

View File

@ -1,70 +0,0 @@
package container
import (
"context"
"fmt"
"strings"
abraFormatter "coopcloud.tech/abra/cli/formatter"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
"github.com/sirupsen/logrus"
)
// GetContainer retrieves a container. If prompt is true and the retrievd count
// of containers does not match expectedN, then a prompt is presented to let
// the user choose.
func GetContainer(c context.Context, cl *client.Client, filters filters.Args, prompt bool) (types.Container, error) {
containerOpts := types.ContainerListOptions{Filters: filters}
containers, err := cl.ContainerList(c, containerOpts)
if err != nil {
return types.Container{}, err
}
if len(containers) == 0 {
filter := filters.Get("name")[0]
return types.Container{}, fmt.Errorf("no containers matching the %v filter found?", filter)
}
if len(containers) != 1 {
var containersRaw []string
for _, container := range containers {
containerName := strings.Join(container.Names, " ")
trimmed := strings.TrimPrefix(containerName, "/")
created := abraFormatter.HumanDuration(container.Created)
containersRaw = append(containersRaw, fmt.Sprintf("%s (created %v)", trimmed, created))
}
if !prompt {
err := fmt.Errorf("expected 1 container but found %v: %s", len(containers), strings.Join(containersRaw, " "))
return types.Container{}, err
}
logrus.Warnf("ambiguous container list received, prompting for input")
var response string
prompt := &survey.Select{
Message: "which container are you looking for?",
Options: containersRaw,
}
if err := survey.AskOne(prompt, &response); err != nil {
return types.Container{}, err
}
chosenContainer := strings.TrimSpace(strings.Split(response, " ")[0])
for _, container := range containers {
containerName := strings.TrimSpace(strings.Join(container.Names, " "))
trimmed := strings.TrimPrefix(containerName, "/")
if trimmed == chosenContainer {
return container, nil
}
}
logrus.Panic("failed to match chosen container")
}
return containers[0], nil
}

View File

@ -76,7 +76,7 @@ func EnsureUpToDate(dir string) error {
refName := fmt.Sprintf("refs/heads/%s", branch) refName := fmt.Sprintf("refs/heads/%s", branch)
checkOutOpts := &git.CheckoutOptions{ checkOutOpts := &git.CheckoutOptions{
Create: false, Create: false,
Force: true, Keep: true,
Branch: plumbing.ReferenceName(refName), Branch: plumbing.ReferenceName(refName),
} }
if err := worktree.Checkout(checkOutOpts); err != nil { if err := worktree.Checkout(checkOutOpts); err != nil {

View File

@ -1,65 +0,0 @@
package git
import (
"fmt"
"github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus"
)
// Commit runs a git commit
func Commit(repoPath, glob, commitMessage string, dryRun, push bool) error {
if commitMessage == "" {
return fmt.Errorf("no commit message specified?")
}
commitRepo, err := git.PlainOpen(repoPath)
if err != nil {
return err
}
commitWorktree, err := commitRepo.Worktree()
if err != nil {
return err
}
patterns, err := GetExcludesFiles()
if err != nil {
return err
}
if len(patterns) > 0 {
commitWorktree.Excludes = append(patterns, commitWorktree.Excludes...)
}
if !dryRun {
err = commitWorktree.AddGlob(glob)
if err != nil {
return err
}
logrus.Debugf("staged %s for commit", glob)
} else {
logrus.Debugf("dry run: did not stage %s for commit", glob)
}
if !dryRun {
_, err = commitWorktree.Commit(commitMessage, &git.CommitOptions{})
if err != nil {
return err
}
logrus.Info("changes commited")
} else {
logrus.Info("dry run: no changes commited")
}
if !dryRun && push {
if err := commitRepo.Push(&git.PushOptions{}); err != nil {
return err
}
logrus.Info("changes pushed")
} else {
logrus.Info("dry run: no changes pushed")
}
return nil
}

View File

@ -24,7 +24,7 @@ func Init(repoPath string, commit bool) error {
logrus.Fatal(err) logrus.Fatal(err)
} }
if err := commitWorktree.AddWithOptions(&git.AddOptions{All: true}); err != nil { if err := commitWorktree.AddGlob("**"); err != nil {
return err return err
} }

View File

@ -2,7 +2,6 @@ package git
import ( import (
"io/ioutil" "io/ioutil"
"os"
"os/user" "os/user"
"path" "path"
"path/filepath" "path/filepath"
@ -51,10 +50,7 @@ func IsClean(recipeName string) (bool, error) {
if err != nil { if err != nil {
return false, err return false, err
} }
worktree.Excludes = append(patterns, worktree.Excludes...)
if len(patterns) > 0 {
worktree.Excludes = append(patterns, worktree.Excludes...)
}
status, err := worktree.Status() status, err := worktree.Status()
if err != nil { if err != nil {
@ -70,7 +66,7 @@ func IsClean(recipeName string) (bool, error) {
return status.IsClean(), nil return status.IsClean(), nil
} }
// GetExcludesFiles reads the exlude files from a global gitignore // GetExcludesFiles reads the exlude files from a global git ignore
func GetExcludesFiles() ([]gitignore.Pattern, error) { func GetExcludesFiles() ([]gitignore.Pattern, error) {
var err error var err error
var patterns []gitignore.Pattern var patterns []gitignore.Pattern
@ -97,16 +93,7 @@ func parseGitConfig() (*gitConfigPkg.Config, error) {
return nil, err return nil, err
} }
globalGitConfig := filepath.Join(usr.HomeDir, ".gitconfig") b, err := ioutil.ReadFile(usr.HomeDir + "/.gitconfig")
if _, err := os.Stat(globalGitConfig); err != nil {
if os.IsNotExist(err) {
logrus.Debugf("no %s exists, not reading any global gitignore config", globalGitConfig)
return cfg, nil
}
return cfg, err
}
b, err := ioutil.ReadFile(globalGitConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -128,41 +115,27 @@ func getExcludesFile(cfg *gitConfigPkg.Config) string {
} }
} }
} }
return ""
return "~/.gitignore"
} }
func parseExcludesFile(excludesfile string) ([]gitignore.Pattern, error) { func parseExcludesFile(excludesfile string) ([]gitignore.Pattern, error) {
var ps []gitignore.Pattern
excludesfile, err := expandTilde(excludesfile) excludesfile, err := expandTilde(excludesfile)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if _, err := os.Stat(excludesfile); err != nil {
if os.IsNotExist(err) {
logrus.Debugf("no %s exists, skipping reading gitignore paths", excludesfile)
return ps, nil
}
return ps, err
}
data, err := ioutil.ReadFile(excludesfile) data, err := ioutil.ReadFile(excludesfile)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var pathsRaw []string var ps []gitignore.Pattern
for _, s := range strings.Split(string(data), "\n") { for _, s := range strings.Split(string(data), "\n") {
if !strings.HasPrefix(s, "#") && len(strings.TrimSpace(s)) > 0 { if !strings.HasPrefix(s, "#") && len(strings.TrimSpace(s)) > 0 {
pathsRaw = append(pathsRaw, s)
ps = append(ps, gitignore.ParsePattern(s, nil)) ps = append(ps, gitignore.ParsePattern(s, nil))
} }
} }
logrus.Debugf("read global ignore paths: %s", strings.Join(pathsRaw, " "))
return ps, nil return ps, nil
} }
@ -170,13 +143,11 @@ func expandTilde(path string) (string, error) {
if !strings.HasPrefix(path, "~") { if !strings.HasPrefix(path, "~") {
return path, nil return path, nil
} }
var paths []string var paths []string
u, err := user.Current() u, err := user.Current()
if err != nil { if err != nil {
return "", err return "", err
} }
for _, p := range strings.Split(path, string(filepath.Separator)) { for _, p := range strings.Split(path, string(filepath.Separator)) {
if p == "~" { if p == "~" {
paths = append(paths, u.HomeDir) paths = append(paths, u.HomeDir)
@ -184,6 +155,5 @@ func expandTilde(path string) (string, error) {
paths = append(paths, p) paths = append(paths, p)
} }
} }
return "/" + filepath.Join(paths...), nil
return filepath.Join(paths...), nil
} }

View File

@ -25,9 +25,9 @@ type Recipe struct {
} }
// UpdateLabel updates a recipe label // UpdateLabel updates a recipe label
func (r Recipe) UpdateLabel(pattern, serviceName, label string) error { func (r Recipe) UpdateLabel(serviceName, label string) error {
fullPattern := fmt.Sprintf("%s/%s/%s", config.APPS_DIR, r.Name, pattern) pattern := fmt.Sprintf("%s/%s/compose**yml", config.APPS_DIR, r.Name)
if err := compose.UpdateLabel(fullPattern, serviceName, label, r.Name); err != nil { if err := compose.UpdateLabel(pattern, serviceName, label, r.Name); err != nil {
return err return err
} }
return nil return nil
@ -81,10 +81,6 @@ func Get(recipeName string) (Recipe, error) {
return Recipe{}, err return Recipe{}, err
} }
if len(composeFiles) == 0 {
return Recipe{}, fmt.Errorf("%s is missing a compose.yml or compose.*.yml file?", recipeName)
}
envSamplePath := path.Join(config.ABRA_DIR, "apps", recipeName, ".env.sample") envSamplePath := path.Join(config.ABRA_DIR, "apps", recipeName, ".env.sample")
sampleEnv, err := config.ReadEnv(envSamplePath) sampleEnv, err := config.ReadEnv(envSamplePath)
if err != nil { if err != nil {
@ -158,11 +154,10 @@ func EnsureVersion(recipeName, version string) error {
return err return err
} }
logrus.Debugf("read %s as tags for recipe %s", strings.Join(parsedTags, ", "), recipeName) logrus.Debugf("read '%s' as tags for recipe '%s'", strings.Join(parsedTags, ", "), recipeName)
if tagRef.String() == "" { if tagRef.String() == "" {
logrus.Warnf("%s recipe has no local tag: %s? this recipe version is not released?", recipeName, version) return fmt.Errorf("%s is not available?", version)
return nil
} }
worktree, err := repo.Worktree() worktree, err := repo.Worktree()
@ -173,13 +168,13 @@ func EnsureVersion(recipeName, version string) error {
opts := &git.CheckoutOptions{ opts := &git.CheckoutOptions{
Branch: tagRef, Branch: tagRef,
Create: false, Create: false,
Force: true, Keep: true,
} }
if err := worktree.Checkout(opts); err != nil { if err := worktree.Checkout(opts); err != nil {
return err return err
} }
logrus.Debugf("successfully checked %s out to %s in %s", recipeName, tagRef.Short(), recipeDir) logrus.Debugf("successfully checked '%s' out to '%s' in '%s'", recipeName, tagRef.Short(), recipeDir)
return nil return nil
} }
@ -194,14 +189,14 @@ func EnsureLatest(recipeName string) error {
} }
if !isClean { if !isClean {
return fmt.Errorf("%s has locally unstaged changes", recipeName) return fmt.Errorf("'%s' has locally unstaged changes", recipeName)
} }
if err := gitPkg.EnsureGitRepo(recipeDir); err != nil { if err := gitPkg.EnsureGitRepo(recipeDir); err != nil {
return err return err
} }
logrus.Debugf("attempting to open git repository in %s", recipeDir) logrus.Debugf("attempting to open git repository in '%s'", recipeDir)
repo, err := git.PlainOpen(recipeDir) repo, err := git.PlainOpen(recipeDir)
if err != nil { if err != nil {
@ -216,7 +211,7 @@ func EnsureLatest(recipeName string) error {
branch := "master" branch := "master"
if _, err := repo.Branch("master"); err != nil { if _, err := repo.Branch("master"); err != nil {
if _, err := repo.Branch("main"); err != nil { if _, err := repo.Branch("main"); err != nil {
logrus.Debugf("failed to select branch in %s", path.Join(config.APPS_DIR, recipeName)) logrus.Debugf("failed to select branch in '%s'", path.Join(config.APPS_DIR, recipeName))
return err return err
} }
branch = "main" branch = "main"
@ -225,12 +220,12 @@ func EnsureLatest(recipeName string) error {
refName := fmt.Sprintf("refs/heads/%s", branch) refName := fmt.Sprintf("refs/heads/%s", branch)
checkOutOpts := &git.CheckoutOptions{ checkOutOpts := &git.CheckoutOptions{
Create: false, Create: false,
Force: true, Keep: true,
Branch: plumbing.ReferenceName(refName), Branch: plumbing.ReferenceName(refName),
} }
if err := worktree.Checkout(checkOutOpts); err != nil { if err := worktree.Checkout(checkOutOpts); err != nil {
logrus.Debugf("failed to check out %s in %s", branch, recipeDir) logrus.Debugf("failed to check out '%s' in '%s'", branch, recipeDir)
return err return err
} }
@ -259,34 +254,3 @@ func ChaosVersion(recipeName string) (string, error) {
return version, nil return version, nil
} }
// GetRecipesLocal retrieves all local recipe directories
func GetRecipesLocal() ([]string, error) {
var recipes []string
recipes, err := config.GetAllFoldersInDirectory(config.APPS_DIR)
if err != nil {
return recipes, err
}
return recipes, nil
}
// GetVersionLabelLocal retrieves the version label on the local recipe config
func GetVersionLabelLocal(recipe Recipe) (string, error) {
var label string
for _, service := range recipe.Config.Services {
for label, value := range service.Deploy.Labels {
if strings.HasPrefix(label, "coop-cloud") {
return value, nil
}
}
}
if label == "" {
return label, fmt.Errorf("unable to retrieve synced version label for %s", recipe.Name)
}
return label, nil
}

View File

@ -140,12 +140,7 @@ func GenerateSecrets(secretEnvVars map[string]string, appName, server string) (m
return return
} }
if err := client.StoreSecret(secretRemoteName, passwords[0], server); err != nil { if err := client.StoreSecret(secretRemoteName, passwords[0], server); err != nil {
if strings.Contains(err.Error(), "AlreadyExists") { ch <- err
logrus.Warnf("%s already exists, moving on...", secretRemoteName)
ch <- nil
} else {
ch <- err
}
return return
} }
secrets[secretName] = passwords[0] secrets[secretName] = passwords[0]
@ -156,13 +151,7 @@ func GenerateSecrets(secretEnvVars map[string]string, appName, server string) (m
return return
} }
if err := client.StoreSecret(secretRemoteName, passphrases[0], server); err != nil { if err := client.StoreSecret(secretRemoteName, passphrases[0], server); err != nil {
if strings.Contains(err.Error(), "AlreadyExists") { ch <- err
logrus.Warnf("%s already exists, moving on...", secretRemoteName)
ch <- nil
} else {
ch <- err
}
return
} }
secrets[secretName] = passphrases[0] secrets[secretName] = passphrases[0]
} }

View File

@ -12,7 +12,7 @@ import (
func CreateServerDir(serverName string) error { func CreateServerDir(serverName string) error {
serverPath := path.Join(config.ABRA_DIR, "servers", serverName) serverPath := path.Join(config.ABRA_DIR, "servers", serverName)
if err := os.Mkdir(serverPath, 0764); err != nil { if err := os.Mkdir(serverPath, 0755); err != nil {
if !os.IsExist(err) { if !os.IsExist(err) {
return err return err
} }

View File

@ -111,7 +111,7 @@ type sudoWriter struct {
// Write satisfies the write interface for sudoWriter // Write satisfies the write interface for sudoWriter
func (w *sudoWriter) Write(p []byte) (int, error) { func (w *sudoWriter) Write(p []byte) (int, error) {
if strings.Contains(string(p), "sudo_password") { if string(p) == "sudo_password" {
w.stdin.Write([]byte(w.pw + "\n")) w.stdin.Write([]byte(w.pw + "\n"))
w.pw = "" w.pw = ""
return len(p), nil return len(p), nil
@ -131,9 +131,11 @@ func RunSudoCmd(cmd, passwd string, cl *Client) error {
} }
defer session.Close() defer session.Close()
sudoCmd := fmt.Sprintf("SSH_ASKPASS=/usr/bin/ssh-askpass; sudo -p sudo_password -S %s", cmd) cmd = "sudo -p " + "sudo_password" + " -S " + cmd
w := &sudoWriter{pw: passwd} w := &sudoWriter{
pw: passwd,
}
w.stdin, err = session.StdinPipe() w.stdin, err = session.StdinPipe()
if err != nil { if err != nil {
return err return err
@ -142,19 +144,79 @@ func RunSudoCmd(cmd, passwd string, cl *Client) error {
session.Stdout = w session.Stdout = w
session.Stderr = w session.Stderr = w
modes := ssh.TerminalModes{ done := make(chan struct{})
ssh.ECHO: 0, scanner := bufio.NewScanner(session.Stdin)
ssh.TTY_OP_ISPEED: 14400,
ssh.TTY_OP_OSPEED: 14400, go func() {
for scanner.Scan() {
line := scanner.Text()
fmt.Println(line)
}
done <- struct{}{}
}()
if err := session.Start(cmd); err != nil {
return err
} }
err = session.RequestPty("xterm", 80, 40, modes) <-done
if err := session.Wait(); err != nil {
return err
}
return err
}
// Exec runs a command on a remote and streams output
func Exec(cmd string, cl *Client) error {
session, err := cl.SSHClient.NewSession()
if err != nil {
return err
}
defer session.Close()
stdout, err := session.StdoutPipe()
if err != nil { if err != nil {
return err return err
} }
if err := session.Run(sudoCmd); err != nil { stderr, err := session.StdoutPipe()
return fmt.Errorf("%s", string(w.b.Bytes())) if err != nil {
return err
}
stdoutDone := make(chan struct{})
stdoutScanner := bufio.NewScanner(stdout)
go func() {
for stdoutScanner.Scan() {
line := stdoutScanner.Text()
fmt.Println(line)
}
stdoutDone <- struct{}{}
}()
stderrDone := make(chan struct{})
stderrScanner := bufio.NewScanner(stderr)
go func() {
for stderrScanner.Scan() {
line := stderrScanner.Text()
fmt.Println(line)
}
stderrDone <- struct{}{}
}()
if err := session.Start(cmd); err != nil {
return err
}
<-stdoutDone
<-stderrDone
if err := session.Wait(); err != nil {
return err
} }
return nil return nil
@ -258,7 +320,7 @@ func HostKeyAddCallback(hostnameAndPort string, remote net.Addr, pubKey ssh.Publ
if exists { if exists {
hostname := strings.Split(hostnameAndPort, ":")[0] hostname := strings.Split(hostnameAndPort, ":")[0]
logrus.Debugf("server SSH host key found for %s", hostname) logrus.Debugf("server SSH host key found for %s, moving on", hostname)
return nil return nil
} }

View File

@ -29,7 +29,7 @@ func LoadComposefile(opts Deploy, appEnv map[string]string) (*composetypes.Confi
config, err := loader.Load(configDetails, DontSkipValidation) config, err := loader.Load(configDetails, DontSkipValidation)
if err != nil { if err != nil {
if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok { if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok {
return nil, fmt.Errorf("compose file contains unsupported options: %s", return nil, fmt.Errorf("compose file contains unsupported options:\n\n%s",
propertyWarnings(fpe.Properties)) propertyWarnings(fpe.Properties))
} }
return nil, err return nil, err
@ -37,14 +37,14 @@ func LoadComposefile(opts Deploy, appEnv map[string]string) (*composetypes.Confi
unsupportedProperties := loader.GetUnsupportedProperties(dicts...) unsupportedProperties := loader.GetUnsupportedProperties(dicts...)
if len(unsupportedProperties) > 0 { if len(unsupportedProperties) > 0 {
logrus.Warnf("%s: ignoring unsupported options: %s", logrus.Warnf("Ignoring unsupported options: %s\n\n",
appEnv["TYPE"], strings.Join(unsupportedProperties, ", ")) strings.Join(unsupportedProperties, ", "))
} }
deprecatedProperties := loader.GetDeprecatedProperties(dicts...) deprecatedProperties := loader.GetDeprecatedProperties(dicts...)
if len(deprecatedProperties) > 0 { if len(deprecatedProperties) > 0 {
logrus.Warnf("%s: ignoring deprecated options: %s", logrus.Warnf("Ignoring deprecated options:\n\n%s\n\n",
appEnv["TYPE"], propertyWarnings(deprecatedProperties)) propertyWarnings(deprecatedProperties))
} }
return config, nil return config, nil
} }

View File

@ -158,7 +158,7 @@ func pruneServices(ctx context.Context, cl *dockerclient.Client, namespace conve
} }
// RunDeploy is the swarm implementation of docker stack deploy // RunDeploy is the swarm implementation of docker stack deploy
func RunDeploy(cl *dockerclient.Client, opts Deploy, cfg *composetypes.Config, recipeName string) error { func RunDeploy(cl *dockerclient.Client, opts Deploy, cfg *composetypes.Config) error {
ctx := context.Background() ctx := context.Background()
if err := validateResolveImageFlag(&opts); err != nil { if err := validateResolveImageFlag(&opts); err != nil {
@ -170,7 +170,7 @@ func RunDeploy(cl *dockerclient.Client, opts Deploy, cfg *composetypes.Config, r
opts.ResolveImage = ResolveImageNever opts.ResolveImage = ResolveImageNever
} }
return deployCompose(ctx, cl, opts, cfg, recipeName) return deployCompose(ctx, cl, opts, cfg)
} }
// validateResolveImageFlag validates the opts.resolveImage command line option // validateResolveImageFlag validates the opts.resolveImage command line option
@ -183,7 +183,7 @@ func validateResolveImageFlag(opts *Deploy) error {
} }
} }
func deployCompose(ctx context.Context, cl *dockerclient.Client, opts Deploy, config *composetypes.Config, recipeName string) error { func deployCompose(ctx context.Context, cl *dockerclient.Client, opts Deploy, config *composetypes.Config) error {
namespace := convert.NewNamespace(opts.Namespace) namespace := convert.NewNamespace(opts.Namespace)
if opts.Prune { if opts.Prune {
@ -224,7 +224,7 @@ func deployCompose(ctx context.Context, cl *dockerclient.Client, opts Deploy, co
return err return err
} }
return deployServices(ctx, cl, services, namespace, opts.SendRegistryAuth, opts.ResolveImage, recipeName) return deployServices(ctx, cl, services, namespace, opts.SendRegistryAuth, opts.ResolveImage)
} }
func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} { func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} {
@ -339,8 +339,7 @@ func deployServices(
services map[string]swarm.ServiceSpec, services map[string]swarm.ServiceSpec,
namespace convert.Namespace, namespace convert.Namespace,
sendAuth bool, sendAuth bool,
resolveImage string, resolveImage string) error {
recipeName string) error {
existingServices, err := GetStackServices(ctx, cl, namespace.Name()) existingServices, err := GetStackServices(ctx, cl, namespace.Name())
if err != nil { if err != nil {
return err return err
@ -351,7 +350,7 @@ func deployServices(
existingServiceMap[service.Spec.Name] = service existingServiceMap[service.Spec.Name] = service
} }
serviceIDs := make(map[string]string) var serviceIDs []string
for internalName, serviceSpec := range services { for internalName, serviceSpec := range services {
var ( var (
name = namespace.Scope(internalName) name = namespace.Scope(internalName)
@ -411,7 +410,7 @@ func deployServices(
return errors.Wrapf(err, "failed to update service %s", name) return errors.Wrapf(err, "failed to update service %s", name)
} }
serviceIDs[service.ID] = name serviceIDs = append(serviceIDs, service.ID)
for _, warning := range response.Warnings { for _, warning := range response.Warnings {
logrus.Warn(warning) logrus.Warn(warning)
@ -431,22 +430,18 @@ func deployServices(
return errors.Wrapf(err, "failed to create service %s", name) return errors.Wrapf(err, "failed to create service %s", name)
} }
serviceIDs[serviceCreateResponse.ID] = name serviceIDs = append(serviceIDs, serviceCreateResponse.ID)
} }
} }
var serviceNames []string logrus.Infof("waiting for services to converge: %s", strings.Join(serviceIDs, ", "))
for _, serviceName := range serviceIDs {
serviceNames = append(serviceNames, serviceName)
}
logrus.Infof("waiting for services to converge: %s", strings.Join(serviceNames, ", "))
ch := make(chan error, len(serviceIDs)) ch := make(chan error, len(serviceIDs))
for serviceID, serviceName := range serviceIDs { for _, serviceID := range serviceIDs {
logrus.Debugf("waiting on %s to converge", serviceName) logrus.Debugf("waiting on %s to converge", serviceID)
go func(sID, sName, rName string) { go func(s string) {
ch <- waitOnService(ctx, cl, sID, sName, rName) ch <- waitOnService(ctx, cl, s)
}(serviceID, serviceName, recipeName) }(serviceID)
} }
for _, serviceID := range serviceIDs { for _, serviceID := range serviceIDs {
@ -476,7 +471,7 @@ func getStackConfigs(ctx context.Context, dockerclient client.APIClient, namespa
// https://github.com/docker/cli/blob/master/cli/command/service/helpers.go // https://github.com/docker/cli/blob/master/cli/command/service/helpers.go
// https://github.com/docker/cli/blob/master/cli/command/service/progress/progress.go // https://github.com/docker/cli/blob/master/cli/command/service/progress/progress.go
func waitOnService(ctx context.Context, cl *dockerclient.Client, serviceID, serviceName, recipeName string) error { func waitOnService(ctx context.Context, cl *dockerclient.Client, serviceID string) error {
errChan := make(chan error, 1) errChan := make(chan error, 1)
pipeReader, pipeWriter := io.Pipe() pipeReader, pipeWriter := io.Pipe()
@ -492,21 +487,6 @@ func waitOnService(ctx context.Context, cl *dockerclient.Client, serviceID, serv
case err := <-errChan: case err := <-errChan:
return err return err
case <-time.After(timeout): case <-time.After(timeout):
return fmt.Errorf(fmt.Sprintf(` return fmt.Errorf("%s has still not converged (%s second timeout)?", serviceID, timeout)
%s has still not converged (%s second timeout reached)
This does not necessarily mean your deployment has failed, it may just be that
the app is taking longer to deploy based on your server resources or network
latency. Please run the following the inspect the logs of your deployed app:
abra app logs %s
If a service is failing to even start (run "abra app ps %s" to see what
services are running) there could be a few things. The follow command will
try to smoke those out for you:
abra app errors %s
`, recipeName, timeout, recipeName, recipeName, recipeName))
} }
} }

View File

@ -7,12 +7,10 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
// customLeveledLogger is custom logger with logrus baked in
type customLeveledLogger struct { type customLeveledLogger struct {
retryablehttp.Logger retryablehttp.Logger
} }
// Printf wires up logrus into the custom retryablehttp logger
func (l customLeveledLogger) Printf(msg string, args ...interface{}) { func (l customLeveledLogger) Printf(msg string, args ...interface{}) {
logrus.Debugf(fmt.Sprintf(msg, args...)) logrus.Debugf(fmt.Sprintf(msg, args...))
} }

View File

@ -3,10 +3,6 @@ package web
import ( import (
"encoding/json" "encoding/json"
"fmt"
"io"
"net/http"
"os"
"time" "time"
) )
@ -24,29 +20,3 @@ func ReadJSON(url string, target interface{}) error {
defer res.Body.Close() defer res.Body.Close()
return json.NewDecoder(res.Body).Decode(target) return json.NewDecoder(res.Body).Decode(target)
} }
// GetFile downloads a file and saves it to a filepath
func GetFile(filepath string, url string) (err error) {
out, err := os.Create(filepath)
if err != nil {
return err
}
defer out.Close()
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("bad status: %s", resp.Status)
}
_, err = io.Copy(out, resp.Body)
if err != nil {
return err
}
return nil
}

View File

@ -1,8 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
ABRA_VERSION="0.4.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" RC_VERSION="0.3.1-alpha-rc2"
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
@ -28,7 +28,7 @@ function show_banner {
} }
function print_checksum_error { function print_checksum_error {
echo "$(tput setaf 1)ERROR: the checksum of downloaded file doesn't match the checksum in release! Either the file was corrupted during download or the file has been changed during transport$(tput sgr0)" echo "$(tput setaf 1)ERROR: the checksum of downloaded file doesn't match the checksum in release!!! Either the file was corrupted during download or the file has been changed during transport!$(tput sgr0)"
echo "expected checksum: $checksum" echo "expected checksum: $checksum"
echo "checksum of downloaded file: $localsum" echo "checksum of downloaded file: $localsum"
echo "abra was NOT installed/upgraded" echo "abra was NOT installed/upgraded"
@ -76,19 +76,17 @@ function install_abra_release {
p=$HOME/.local/bin p=$HOME/.local/bin
com="echo PATH=\$PATH:$p" com="echo PATH=\$PATH:$p"
if [[ $SHELL =~ "bash" ]]; then if [[ $SHELL =~ "bash" ]]; then
echo "$com >> $HOME/.bashrc" echo "echo $com >> $HOME/.bashrc"
elif [[ $SHELL =~ "fizsh" ]]; then elif [[ $SHELL =~ "fizsh" ]]; then
echo "$com >> $HOME/.fizsh/.fizshrc" echo "echo $com >> $HOME/.fizsh/.fizshrc"
elif [[ $SHELL =~ "zsh" ]]; then elif [[ $SHELL =~ "zsh" ]]; then
echo "$com >> $HOME/.zshrc" echo "echo $com >> $HOME/.zshrc"
else else
echo "$com >> $HOME/.profile" echo "echo $com >> $HOME/.profile"
fi fi
fi fi
echo "abra installed to $HOME/.local/bin/abra" echo "abra installed to $HOME/.local/bin/abra"
echo "test your installation is working by running \"abra\" on your command-line"
echo "run \"abra autocomplete -h\" to see how to set up command-line autocompletion"
} }