Compare commits

...

33 Commits

Author SHA1 Message Date
knoflook 6ef15e0a26
fix: remove fish from autocomplete 2021-11-24 12:11:35 +01:00
decentral1se dd0f328a65
fix: dont throw away changes
Part of coop-cloud/organising#226.
2021-11-22 21:11:59 +01:00
decentral1se aea5cc69c3
fix: include ignored files
Part of coop-cloud/organising#226.
2021-11-22 21:11:59 +01:00
3wc b02475eca5 Merge branch 'catalogue-metadata' 2021-11-22 20:41:34 +02:00
3wc d0a30f6b7b refactor: code style / error handling improvements 2021-11-22 20:37:12 +02:00
3wc 8635922b9f fix: don't clobber recipe changes during `generate`
Closes #255
2021-11-22 20:37:12 +02:00
3wc 9d62fff074 feat: recipe generate: load category and features 2021-11-22 20:37:12 +02:00
decentral1se 711c4e5ee8
fix: warn on invalid envs for catalogue generation
Closes coop-cloud/organising#256.
2021-11-22 18:38:59 +01:00
decentral1se cb32e88cde
fix: support retryable http clients
Closes coop-cloud/organising#257.
2021-11-22 18:28:18 +01:00
decentral1se a18729bf98
fix: ensure changes are check for
Part of coop-cloud/organising#255.
2021-11-22 17:49:31 +01:00
decentral1se dbf84b7640
fix: validate this recipe
Part of coop-cloud/organising#255.
2021-11-22 17:49:14 +01:00
3wc 75db249053 fix: don't include traefik-cert-dumper in catalogue 2021-11-22 16:15:51 +02:00
decentral1se fdf4fc6737
fix: ensure validation takes place
Part of coop-cloud/organising#243 (comment).
2021-11-21 15:00:04 +01:00
decentral1se ef6a9abba9
fix: ensure clean slate for re-deploy 2021-11-21 14:42:38 +01:00
decentral1se ce57d5ed54
fix: merge messages 2021-11-21 14:42:22 +01:00
decentral1se 3b01b1bb2e
docs: explain docker context also 2021-11-21 14:11:27 +01:00
decentral1se fbdb792795
fix: add app name to ps output + docs
Part of coop-cloud/organising#252.
2021-11-21 14:07:19 +01:00
decentral1se 900f40f07a
fix: add app name to list output
Part of coop-cloud/organising#252.
2021-11-21 13:43:21 +01:00
decentral1se ecd2a63f0a
fix: counts apps + drop versions meta without -S 2021-11-21 13:40:23 +01:00
decentral1se 304b70639f
fix: only check catalogue once 2021-11-19 15:50:29 +01:00
decentral1se d821975aa2
fix: dont check servers so many times 2021-11-19 15:50:17 +01:00
decentral1se 1b836dbab6
fix: better borked ssh config message
See coop-cloud/organising#243.
2021-11-19 15:29:54 +01:00
decentral1se fc51cf7775
docs: improve wording [ci skip] 2021-11-19 15:29:54 +01:00
decentral1se a7ebcd8950
chore: bump for new RC 2021-11-18 21:18:40 +01:00
decentral1se e589709cb0
fix: attempt to include IdentityFile if available
This is part of trying to debug:

    coop-cloud/organising#250

And also part of:

    coop-cloud/docs.coopcloud.tech#27

Where I now try to specify the same logic as `ssh -i <my-key-path>` in
the underlying connection logic. This should help with being more
explicit about what key is being used via the SSH config file.
2021-11-18 21:16:10 +01:00
decentral1se 56c3e070f5
fix: log what keys are loaded with the ssh-agent
Closes coop-cloud/organising#249.
2021-11-18 20:04:57 +01:00
decentral1se cc37615d83
refactor: move debug to internal 2021-11-18 20:04:40 +01:00
decentral1se 0b37f63248
chore(deps): go mod tidy 2021-11-18 09:49:25 +01:00
Comrade Renovate Bot 9c3a06a7d9
chore(deps): update module github.com/docker/docker to v20.10.11 2021-11-18 09:49:25 +01:00
Comrade Renovate Bot cdef8b5ea5
chore(deps): update module github.com/docker/cli to v20.10.11 2021-11-18 09:49:25 +01:00
Comrade Renovate Bot cba261b18c
chore(deps): update module github.com/hetznercloud/hcloud-go to v1.33.1 2021-11-18 09:49:25 +01:00
decentral1se 1f6e4fa4a3
fix: ensure to init/commit the new recipe repo
Part of coop-cloud/organising#247.
2021-11-15 18:55:13 +01:00
decentral1se 4a245c3e02
fix: ensure .git repo exists
Part of coop-cloud/organising#247.
2021-11-15 18:55:13 +01:00
26 changed files with 586 additions and 84 deletions

View File

@ -6,6 +6,7 @@ import (
"strings"
abraFormatter "coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/ssh"
@ -70,14 +71,18 @@ can take some time.
}
sort.Sort(config.ByServerAndType(apps))
alreadySeen := make(map[string]bool)
for _, app := range apps {
if err := ssh.EnsureHostKey(app.Server); err != nil {
logrus.Fatal(err)
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
}
}
statuses := make(map[string]map[string]string)
tableCol := []string{"Server", "Type", "Domain"}
tableCol := []string{"Server", "Type", "App Name", "Domain"}
if status {
tableCol = append(tableCol, "Status", "Version", "Updates")
statuses, err = config.GetAppStatuses(appFiles)
@ -96,11 +101,19 @@ can take some time.
canUpgradeCount int
)
catl, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Fatal(err)
}
var appsCount int
for _, app := range apps {
var tableRow []string
if app.Type == appType || appType == "" {
appsCount++
// If type flag is set, check for it, if not, Type == ""
tableRow = []string{app.Server, app.Type, app.Domain}
tableRow = []string{app.Server, app.Type, app.StackName(), app.Domain}
if status {
stackName := app.StackName()
status := "unknown"
@ -121,7 +134,8 @@ can take some time.
var newUpdates []string
if version != "unknown" {
updates, err := catalogue.GetRecipeCatalogueVersions(app.Type)
updates, err := catalogue.GetRecipeCatalogueVersions(app.Type, catl)
if err != nil {
logrus.Fatal(err)
}
@ -163,14 +177,19 @@ can take some time.
table.Append(tableRow)
}
stats := fmt.Sprintf(
"Total apps: %v | Versioned: %v | Unversioned: %v | On latest: %v | Can upgrade: %v",
len(apps),
versionedAppsCount,
unversionedAppsCount,
onLatestCount,
canUpgradeCount,
)
var stats string
if status {
stats = fmt.Sprintf(
"Total apps: %v | Versioned: %v | Unversioned: %v | On latest: %v | Can upgrade: %v",
appsCount,
versionedAppsCount,
unversionedAppsCount,
onLatestCount,
canUpgradeCount,
)
} else {
stats = fmt.Sprintf("Total apps: %v", appsCount)
}
table.SetCaption(true, stats)
table.Render()

View File

@ -26,9 +26,10 @@ var watchFlag = &cli.BoolFlag{
}
var appPsCommand = &cli.Command{
Name: "ps",
Usage: "Check app status",
Aliases: []string{"p"},
Name: "ps",
Usage: "Check app status",
Description: "This command shows a more detailed status output of a specific deployed app.",
Aliases: []string{"p"},
Flags: []cli.Flag{
watchFlag,
},
@ -75,7 +76,7 @@ func showPSOutput(c *cli.Context) {
logrus.Fatal(err)
}
tableCol := []string{"image", "created", "status", "ports", "names"}
tableCol := []string{"image", "created", "status", "ports", "app name", "services"}
table := abraFormatter.CreateTable(tableCol)
for _, container := range containers {
@ -90,6 +91,7 @@ func showPSOutput(c *cli.Context) {
abraFormatter.HumanDuration(container.Created),
container.Status,
formatter.DisplayablePorts(container.Ports),
app.StackName(),
strings.Join(containerNames, "\n"),
}
table.Append(tableRow)

View File

@ -70,7 +70,12 @@ recipes.
logrus.Fatalf("'%s' is not deployed?", app.Name)
}
versions, err := catalogue.GetRecipeCatalogueVersions(app.Type)
catl, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Fatal(err)
}
versions, err := catalogue.GetRecipeCatalogueVersions(app.Type, catl)
if err != nil {
logrus.Fatal(err)
}

View File

@ -62,7 +62,12 @@ recipes.
logrus.Fatalf("'%s' is not deployed?", app.Name)
}
versions, err := catalogue.GetRecipeCatalogueVersions(app.Type)
catl, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Fatal(err)
}
versions, err := catalogue.GetRecipeCatalogueVersions(app.Type, catl)
if err != nil {
logrus.Fatal(err)
}

View File

@ -56,7 +56,7 @@ Example:
Supported shells are as follows:
fish
fizsh
zsh
bash
`,
@ -69,16 +69,16 @@ Supported shells are as follows:
}
supportedShells := map[string]bool{
"bash": true,
"zsh": true,
"fish": true,
"bash": true,
"zsh": true,
"fizsh": true,
}
if _, ok := supportedShells[shellType]; !ok {
logrus.Fatalf("%s is not a supported shell right now, sorry", shellType)
}
if shellType == "fish" {
if shellType == "fizsh" {
shellType = "zsh" // handled the same on the autocompletion side
}

View File

@ -49,6 +49,7 @@ var CatalogueSkipList = map[string]bool{
"stack-ssh-deploy": true,
"swarm-cronjob": true,
"tagcmp": true,
"traefik-cert-dumper": true,
"tyop": true,
}
@ -91,6 +92,9 @@ A new catalogue copy can be published to the recipes repository by passing the
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")
@ -130,6 +134,15 @@ A new catalogue copy can be published to the recipes repository by passing the
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)
}
@ -161,6 +174,11 @@ A new catalogue copy can be published to the recipes repository by passing the
logrus.Fatal(err)
}
features, category, err := catalogue.GetRecipeFeaturesAndCategory(recipeMeta.Name)
if err != nil {
logrus.Fatal(err)
}
catl[recipeMeta.Name] = catalogue.RecipeMeta{
Name: recipeMeta.Name,
Repository: recipeMeta.CloneURL,
@ -169,8 +187,8 @@ A new catalogue copy can be published to the recipes repository by passing the
Description: recipeMeta.Description,
Website: recipeMeta.Website,
Versions: versions,
// Category: ..., // FIXME: parse & load
// Features: ..., // FIXME: parse & load
Category: category,
Features: features,
}
catlBar.Add(1)
}

View File

@ -30,18 +30,6 @@ var VerboseFlag = &cli.BoolFlag{
Usage: "Show INFO messages",
}
// Debug stores the variable from DebugFlag.
var Debug bool
// DebugFlag turns on/off verbose logging down to the DEBUG level.
var DebugFlag = &cli.BoolFlag{
Name: "debug",
Aliases: []string{"d"},
Value: false,
Destination: &Debug,
Usage: "Show DEBUG messages",
}
func newAbraApp(version, commit string) *cli.App {
app := &cli.App{
Name: "abra",
@ -66,7 +54,7 @@ func newAbraApp(version, commit string) *cli.App {
},
Flags: []cli.Flag{
VerboseFlag,
DebugFlag,
internal.DebugFlag,
internal.NoInputFlag,
},
Authors: []*cli.Author{
@ -80,7 +68,7 @@ func newAbraApp(version, commit string) *cli.App {
app.EnableBashCompletion = true
app.Before = func(c *cli.Context) error {
if Debug {
if internal.Debug {
logrus.SetLevel(logrus.DebugLevel)
logrus.SetFormatter(&logrus.TextFormatter{})
logrus.SetOutput(os.Stderr)

View File

@ -259,3 +259,60 @@ var HetznerCloudAPITokenFlag = &cli.StringFlag{
EnvVars: []string{"HCLOUD_TOKEN"},
Destination: &HetznerCloudAPIToken,
}
// Debug stores the variable from DebugFlag.
var Debug bool
// DebugFlag turns on/off verbose logging down to the DEBUG level.
var DebugFlag = &cli.BoolFlag{
Name: "debug",
Aliases: []string{"d"},
Value: false,
Destination: &Debug,
Usage: "Show DEBUG messages",
}
// SSHFailMsg is a hopefully helpful SSH failure message
var SSHFailMsg = `
Woops, Abra is unable to connect to connect to %s.
Here are a few tips for debugging your local SSH config. Abra uses plain 'ol
SSH to make connections to servers, so if your SSH config is working, Abra is
working.
In the first place, Abra will always try to read your Docker context connection
string for SSH connection details. You can view your server context configs
with the following command. Are they correct?
abra server ls
Is your ssh-agent running? You can start it by running the following command:
eval "$(ssh-agent)"
If your SSH private key loaded? You can check by running the following command:
ssh-add -L
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
details:
Host foo.coopcloud.tech
Hostname foo.coopcloud.tech
User bar
Port 12345
IdentityFile ~/.ssh/bar@foo.coopcloud.tech
If you're only using password authentication, you can use the following config:
Host foo.coopcloud.tech
Hostname foo.coopcloud.tech
User bar
Port 12345
PreferredAuthentications=password
PubkeyAuthentication=no
Good luck!
`

View File

@ -34,10 +34,8 @@ func DeployAction(c *cli.Context) error {
}
if isDeployed {
if Force {
logrus.Warnf("'%s' already deployed but continuing (--force)", stackName)
} else if Chaos {
logrus.Warnf("'%s' already deployed but continuing (--chaos)", stackName)
if Force || Chaos {
logrus.Warnf("'%s' is already deployed but continuing (--force/--chaos)", stackName)
} else {
logrus.Fatalf("'%s' is already deployed", stackName)
}
@ -45,7 +43,11 @@ func DeployAction(c *cli.Context) error {
version := deployedVersion
if version == "" && !Chaos {
versions, err := catalogue.GetRecipeCatalogueVersions(app.Type)
catl, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Fatal(err)
}
versions, err := catalogue.GetRecipeCatalogueVersions(app.Type, catl)
if err != nil {
logrus.Fatal(err)
}
@ -71,6 +73,12 @@ func DeployAction(c *cli.Context) error {
}
}
if version != "" && !Chaos {
if err := recipe.EnsureVersion(app.Type, version); err != nil {
logrus.Fatal(err)
}
}
if Chaos {
logrus.Warnf("chaos mode engaged")
var err error

View File

@ -27,7 +27,11 @@ func ValidateRecipe(c *cli.Context) recipe.Recipe {
recipe, err := recipe.Get(recipeName)
if err != nil {
logrus.Fatal(err)
if c.Command.Name == "generate" {
logrus.Warn(err)
} else {
logrus.Fatal(err)
}
}
logrus.Debugf("validated '%s' as recipe argument", recipeName)

View File

@ -82,6 +82,11 @@ The new example repository is cloned to ~/.abra/apps/<recipe>.
}
}
newGitRepo := path.Join(config.APPS_DIR, recipeName)
if err := git.Init(newGitRepo, true); err != nil {
logrus.Fatal(err)
}
logrus.Infof(
"new recipe '%s' created in %s, happy hacking!\n",
recipeName, path.Join(config.APPS_DIR, recipeName),

7
go.mod
View File

@ -7,12 +7,12 @@ require (
github.com/AlecAivazis/survey/v2 v2.3.2
github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731170023-c37c0920d1a4
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4
github.com/docker/cli v20.10.10+incompatible
github.com/docker/cli v20.10.11+incompatible
github.com/docker/distribution v2.7.1+incompatible
github.com/docker/docker v20.10.10+incompatible
github.com/docker/docker v20.10.11+incompatible
github.com/docker/go-units v0.4.0
github.com/go-git/go-git/v5 v5.4.2
github.com/hetznercloud/hcloud-go v1.33.0
github.com/hetznercloud/hcloud-go v1.33.1
github.com/moby/sys/signal v0.6.0
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6
github.com/olekukonko/tablewriter v0.0.5
@ -34,6 +34,7 @@ require (
github.com/gliderlabs/ssh v0.3.3
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/hashicorp/go-retryablehttp v0.7.0
github.com/kevinburke/ssh_config v1.1.0
github.com/libdns/gandi v1.0.2
github.com/libdns/libdns v0.2.1

18
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/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/docker/cli v20.10.10+incompatible h1:kcbwdgWbrBOH8QwQzaJmyriHwF7XIl4HT1qh0HTRys4=
github.com/docker/cli v20.10.10+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v20.10.11+incompatible h1:tXU1ezXcruZQRrMP8RN2z9N91h+6egZTS1gsPsKantc=
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 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/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v20.10.10+incompatible h1:GKkP0T7U4ks6X3lmmHKC2QDprnpRJor2Z5a8m62R9ZM=
github.com/docker/docker v20.10.10+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v20.10.11+incompatible h1:OqzI/g/W54LczvhnccGqniFoQghHx3pklbLuhfXpqGo=
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/go.mod h1:ofX3UI0Gz1TteYBjtgs07O36Pyasyp66D2uKT7H8W1c=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
@ -444,14 +444,20 @@ github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-retryablehttp v0.7.0 h1:eu1EI/mbirUgP5C8hVsTNaGZreBDlYiwC1FZWkvQPQ4=
github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hetznercloud/hcloud-go v1.33.0 h1:cHsRgZv5JUX+I9g69KNTSUBRoPEHKgMHV38u4QKhnjQ=
github.com/hetznercloud/hcloud-go v1.33.0/go.mod h1:XX/TQub3ge0yWR2yHWmnDVIrB+MQbda1pHxkUmDlUME=
github.com/hetznercloud/hcloud-go v1.33.1 h1:W1HdO2bRLTKU4WsyqAasDSpt54fYO4WNckWYfH5AuCQ=
github.com/hetznercloud/hcloud-go v1.33.1/go.mod h1:XX/TQub3ge0yWR2yHWmnDVIrB+MQbda1pHxkUmDlUME=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=

View File

@ -7,7 +7,6 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"path"
"strings"
@ -46,6 +45,7 @@ type features struct {
Image image `json:"image"`
Status int `json:"status"`
Tests string `json:"tests"`
SSO string `json:"sso"`
}
// tag represents a git tag.
@ -122,7 +122,7 @@ func (r ByRecipeName) Less(i, j int) bool {
// recipeCatalogueFSIsLatest checks whether the recipe catalogue stored locally
// is up to date.
func recipeCatalogueFSIsLatest() (bool, error) {
httpClient := &http.Client{Timeout: web.Timeout}
httpClient := web.NewHTTPRetryClient()
res, err := httpClient.Head(RecipeCatalogueURL)
if err != nil {
return false, err
@ -374,6 +374,127 @@ func ReadReposMetadata() (RepoCatalogue, error) {
return reposMeta, nil
}
func GetStringInBetween(str, start, end string) (result string, err error) {
// GetStringInBetween returns empty string if no start or end string found
s := strings.Index(str, start)
if s == -1 {
return "", fmt.Errorf("marker string '%s' not found", start)
}
s += len(start)
e := strings.Index(str[s:], end)
if e == -1 {
return "", fmt.Errorf("end marker '%s' not found", end)
}
return str[s : s+e], nil
}
func GetImageMetadata(imageRowString string) (image, error) {
img := image{}
imgFields := strings.Split(imageRowString, ",")
for i, elem := range imgFields {
imgFields[i] = strings.TrimSpace(elem)
}
if len(imgFields) < 3 {
logrus.Warnf("image string has incorrect format: %s", imageRowString)
return img, nil
}
img.Rating = imgFields[1]
img.Source = imgFields[2]
imgString := imgFields[0]
imageName, err := GetStringInBetween(imgString, "[", "]")
if err != nil {
logrus.Fatal(err)
}
img.Image = imageName
imageURL, err := GetStringInBetween(imgString, "(", ")")
if err != nil {
logrus.Fatal(err)
}
img.URL = imageURL
return img, nil
}
func GetRecipeFeaturesAndCategory(recipeName string) (features, string, error) {
feat := features{}
var category string
readmePath := path.Join(config.ABRA_DIR, "apps", recipeName, "README.md")
logrus.Debugf("attempting to open '%s'", readmePath)
readmeFS, err := ioutil.ReadFile(readmePath)
if err != nil {
return feat, category, err
}
readmeMetadata, err := GetStringInBetween( // Find text between delimiters
string(readmeFS),
"<!-- metadata -->", "<!-- endmetadata -->",
)
if err != nil {
logrus.Fatal(err)
}
readmeLines := strings.Split( // Array item from lines
strings.ReplaceAll( // Remove \t tabs
readmeMetadata, "\t", "",
),
"\n")
for _, val := range readmeLines {
if strings.Contains(val, "**Category**") {
category = strings.TrimSpace(
strings.TrimPrefix(val, "* **Category**:"),
)
}
if strings.Contains(val, "**Backups**") {
feat.Backups = strings.TrimSpace(
strings.TrimPrefix(val, "* **Backups**:"),
)
}
if strings.Contains(val, "**Email**") {
feat.Email = strings.TrimSpace(
strings.TrimPrefix(val, "* **Email**:"),
)
}
if strings.Contains(val, "**SSO**") {
feat.SSO = strings.TrimSpace(
strings.TrimPrefix(val, "* **SSO**:"),
)
}
if strings.Contains(val, "**Healthcheck**") {
feat.Healthcheck = strings.TrimSpace(
strings.TrimPrefix(val, "* **Healthcheck**:"),
)
}
if strings.Contains(val, "**Tests**") {
feat.Tests = strings.TrimSpace(
strings.TrimPrefix(val, "* **Tests**:"),
)
}
if strings.Contains(val, "**Image**") {
imageMetadata, err := GetImageMetadata(strings.TrimSpace(
strings.TrimPrefix(val, "* **Image**:"),
))
if err != nil {
continue
}
feat.Image = imageMetadata
}
}
return feat, category, nil
}
// GetRecipeVersions retrieves all recipe versions.
func GetRecipeVersions(recipeName string) (RecipeVersions, error) {
versions := RecipeVersions{}
@ -404,7 +525,7 @@ func GetRecipeVersions(recipeName string) (RecipeVersions, error) {
checkOutOpts := &git.CheckoutOptions{
Create: false,
Force: true,
Keep: true,
Branch: plumbing.ReferenceName(ref.Name()),
}
if err := worktree.Checkout(checkOutOpts); err != nil {
@ -465,7 +586,7 @@ func GetRecipeVersions(recipeName string) (RecipeVersions, error) {
refName := fmt.Sprintf("refs/heads/%s", branch)
checkOutOpts := &git.CheckoutOptions{
Create: false,
Force: true,
Keep: true,
Branch: plumbing.ReferenceName(refName),
}
if err := worktree.Checkout(checkOutOpts); err != nil {
@ -480,14 +601,9 @@ func GetRecipeVersions(recipeName string) (RecipeVersions, error) {
}
// GetRecipeCatalogueVersions list the recipe versions listed in the recipe catalogue.
func GetRecipeCatalogueVersions(recipeName string) ([]string, error) {
func GetRecipeCatalogueVersions(recipeName string, catl RecipeCatalogue) ([]string, error) {
var versions []string
catl, err := ReadRecipeCatalogue()
if err != nil {
return versions, err
}
if recipeMeta, exists := catl[recipeName]; exists {
for _, versionMeta := range recipeMeta.Versions {
for tag := range versionMeta {

View File

@ -9,6 +9,7 @@ import (
"coopcloud.tech/abra/pkg/web"
"github.com/docker/distribution/reference"
"github.com/hashicorp/go-retryablehttp"
)
type RawTag struct {
@ -35,12 +36,12 @@ func GetRegistryTags(image string) (RawTags, error) {
func getRegv2Token(image reference.Named) (string, error) {
img := reference.Path(image)
authTokenURL := fmt.Sprintf("https://auth.docker.io/token?service=registry.docker.io&scope=repository:%s:pull", img)
req, err := http.NewRequest("GET", authTokenURL, nil)
req, err := retryablehttp.NewRequest("GET", authTokenURL, nil)
if err != nil {
return "", err
}
client := &http.Client{Timeout: web.Timeout}
client := web.NewHTTPRetryClient()
res, err := client.Do(req)
if err != nil {
return "", err
@ -78,7 +79,7 @@ func GetTagDigest(image reference.Named) (string, error) {
tag := image.(reference.NamedTagged).Tag()
manifestURL := fmt.Sprintf("https://index.docker.io/v2/%s/manifests/%s", img, tag)
req, err := http.NewRequest("GET", manifestURL, nil)
req, err := retryablehttp.NewRequest("GET", manifestURL, nil)
if err != nil {
return "", err
}
@ -96,7 +97,7 @@ func GetTagDigest(image reference.Named) (string, error) {
"Authorization": []string{fmt.Sprintf("Bearer %s", token)},
}
client := &http.Client{Timeout: web.Timeout}
client := web.NewHTTPRetryClient()
res, err := client.Do(req)
if err != nil {
return "", err

View File

@ -47,6 +47,16 @@ func EnsureUpToDate(dir string) error {
return err
}
recipeName := filepath.Base(dir)
isClean, err := IsClean(recipeName)
if err != nil {
return err
}
if !isClean {
return fmt.Errorf("'%s' has locally unstaged changes", recipeName)
}
branch := "master"
if _, err := repo.Branch("master"); err != nil {
if _, err := repo.Branch("main"); err != nil {
@ -66,7 +76,7 @@ func EnsureUpToDate(dir string) error {
refName := fmt.Sprintf("refs/heads/%s", branch)
checkOutOpts := &git.CheckoutOptions{
Create: false,
Force: true,
Keep: true,
Branch: plumbing.ReferenceName(refName),
}
if err := worktree.Checkout(checkOutOpts); err != nil {

14
pkg/git/common.go Normal file
View File

@ -0,0 +1,14 @@
package git
import (
"fmt"
"os"
)
// EnsureGitRepo ensures a git repo .git folder exists
func EnsureGitRepo(repoPath string) error {
if _, err := os.Stat(repoPath); os.IsNotExist(err) {
return fmt.Errorf("no .git directory in %s?", repoPath)
}
return nil
}

38
pkg/git/init.go Normal file
View File

@ -0,0 +1,38 @@
package git
import (
"github.com/go-git/go-git/v5"
gitPkg "github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus"
)
// Init inits a new repo and commits all the stuff if you want
func Init(repoPath string, commit bool) error {
if _, err := gitPkg.PlainInit(repoPath, false); err != nil {
logrus.Fatal(err)
}
logrus.Debugf("initialised new git repo in %s", repoPath)
if commit {
commitRepo, err := git.PlainOpen(repoPath)
if err != nil {
logrus.Fatal(err)
}
commitWorktree, err := commitRepo.Worktree()
if err != nil {
logrus.Fatal(err)
}
if err := commitWorktree.AddGlob("**"); err != nil {
return err
}
if _, err = commitWorktree.Commit("init", &git.CommitOptions{}); err != nil {
return err
}
logrus.Debugf("init committed all files for new git repo in %s", repoPath)
}
return nil
}

View File

@ -1,11 +1,17 @@
package git
import (
"io/ioutil"
"os/user"
"path"
"path/filepath"
"strings"
"coopcloud.tech/abra/pkg/config"
"github.com/go-git/go-git/v5"
gitConfigPkg "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/format/gitignore"
"github.com/sirupsen/logrus"
)
@ -40,6 +46,12 @@ func IsClean(recipeName string) (bool, error) {
return false, err
}
patterns, err := GetExcludesFiles()
if err != nil {
return false, err
}
worktree.Excludes = append(patterns, worktree.Excludes...)
status, err := worktree.Status()
if err != nil {
return false, err
@ -53,3 +65,95 @@ func IsClean(recipeName string) (bool, error) {
return status.IsClean(), nil
}
// GetExcludesFiles reads the exlude files from a global git ignore
func GetExcludesFiles() ([]gitignore.Pattern, error) {
var err error
var patterns []gitignore.Pattern
cfg, err := parseGitConfig()
if err != nil {
return patterns, err
}
excludesfile := getExcludesFile(cfg)
patterns, err = parseExcludesFile(excludesfile)
if err != nil {
return patterns, err
}
return patterns, nil
}
func parseGitConfig() (*gitConfigPkg.Config, error) {
cfg := gitConfigPkg.NewConfig()
usr, err := user.Current()
if err != nil {
return nil, err
}
b, err := ioutil.ReadFile(usr.HomeDir + "/.gitconfig")
if err != nil {
return nil, err
}
if err := cfg.Unmarshal(b); err != nil {
return nil, err
}
return cfg, err
}
func getExcludesFile(cfg *gitConfigPkg.Config) string {
for _, sec := range cfg.Raw.Sections {
if sec.Name == "core" {
for _, opt := range sec.Options {
if opt.Key == "excludesfile" {
return opt.Value
}
}
}
}
return ""
}
func parseExcludesFile(excludesfile string) ([]gitignore.Pattern, error) {
excludesfile, err := expandTilde(excludesfile)
if err != nil {
return nil, err
}
data, err := ioutil.ReadFile(excludesfile)
if err != nil {
return nil, err
}
var ps []gitignore.Pattern
for _, s := range strings.Split(string(data), "\n") {
if !strings.HasPrefix(s, "#") && len(strings.TrimSpace(s)) > 0 {
ps = append(ps, gitignore.ParsePattern(s, nil))
}
}
return ps, nil
}
func expandTilde(path string) (string, error) {
if !strings.HasPrefix(path, "~") {
return path, nil
}
var paths []string
u, err := user.Current()
if err != nil {
return "", err
}
for _, p := range strings.Split(path, string(filepath.Separator)) {
if p == "~" {
paths = append(paths, u.HomeDir)
} else {
paths = append(paths, p)
}
}
return "/" + filepath.Join(paths...), nil
}

View File

@ -84,7 +84,7 @@ func Get(recipeName string) (Recipe, error) {
envSamplePath := path.Join(config.ABRA_DIR, "apps", recipeName, ".env.sample")
sampleEnv, err := config.ReadEnv(envSamplePath)
if err != nil {
logrus.Fatal(err)
return Recipe{}, err
}
opts := stack.Deploy{Composefiles: composeFiles}
@ -108,6 +108,10 @@ func EnsureExists(recipe string) error {
}
}
if err := gitPkg.EnsureGitRepo(recipeDir); err != nil {
return err
}
return nil
}
@ -124,6 +128,10 @@ func EnsureVersion(recipeName, version string) error {
return fmt.Errorf("'%s' has locally unstaged changes", recipeName)
}
if err := gitPkg.EnsureGitRepo(recipeDir); err != nil {
return err
}
repo, err := git.PlainOpen(recipeDir)
if err != nil {
return err
@ -160,7 +168,7 @@ func EnsureVersion(recipeName, version string) error {
opts := &git.CheckoutOptions{
Branch: tagRef,
Create: false,
Force: true,
Keep: true,
}
if err := worktree.Checkout(opts); err != nil {
return err
@ -184,6 +192,10 @@ func EnsureLatest(recipeName string) error {
return fmt.Errorf("'%s' has locally unstaged changes", recipeName)
}
if err := gitPkg.EnsureGitRepo(recipeDir); err != nil {
return err
}
logrus.Debugf("attempting to open git repository in '%s'", recipeDir)
repo, err := git.PlainOpen(recipeDir)
@ -208,7 +220,7 @@ func EnsureLatest(recipeName string) error {
refName := fmt.Sprintf("refs/heads/%s", branch)
checkOutOpts := &git.CheckoutOptions{
Create: false,
Force: true,
Keep: true,
Branch: plumbing.ReferenceName(refName),
}

View File

@ -330,9 +330,9 @@ func HostKeyAddCallback(hostnameAndPort string, remote net.Addr, pubKey ssh.Publ
fmt.Printf(fmt.Sprintf(`
You are attempting to make an SSH connection to a server but there is no entry
in your ~/.ssh/known_hosts file which confirms that this is indeed the server
you want to connect to. Please take a moment to validate the following SSH host
key, it is important.
in your ~/.ssh/known_hosts file which confirms that you have already validated
that this is indeed the server you want to connect to. Please take a moment to
validate the following SSH host key, it is important.
Host: %s
Fingerprint: %s
@ -409,12 +409,31 @@ func connect(username, host, port string, authMethod ssh.AuthMethod, timeout tim
}
func connectWithAgentTimeout(host, username, port string, timeout time.Duration) (*Client, error) {
logrus.Debugf("using ssh-agent to make an SSH connection for %s", host)
sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK"))
if err != nil {
return nil, err
}
authMethod := ssh.PublicKeysCallback(agent.NewClient(sshAgent).Signers)
agentCl := agent.NewClient(sshAgent)
authMethod := ssh.PublicKeysCallback(agentCl.Signers)
loadedKeys, err := agentCl.List()
if err != nil {
return nil, err
}
var convertedKeys []string
for _, key := range loadedKeys {
convertedKeys = append(convertedKeys, key.String())
}
if len(convertedKeys) > 0 {
logrus.Debugf("ssh-agent has these keys loaded: %s", strings.Join(convertedKeys, ","))
} else {
logrus.Debug("ssh-agent has no keys loaded")
}
return connect(username, host, port, authMethod, timeout)
}
@ -544,11 +563,16 @@ func GetHostConfig(hostname, username, port string) (HostConfig, error) {
}
idf = ssh_config.Get(hostname, "IdentityFile")
hostConfig.Host = host
if idf != "" {
var err error
idf, err = identityFileAbsPath(idf)
if err != nil {
return hostConfig, err
}
hostConfig.IdentityFile = idf
}
hostConfig.Host = host
hostConfig.Port = port
hostConfig.User = username
@ -556,3 +580,25 @@ func GetHostConfig(hostname, username, port string) (HostConfig, error) {
return hostConfig, nil
}
func identityFileAbsPath(relPath string) (string, error) {
var err error
var absPath string
if strings.HasPrefix(relPath, "~/") {
systemUser, err := user.Current()
if err != nil {
return absPath, err
}
absPath = filepath.Join(systemUser.HomeDir, relPath[2:])
} else {
absPath, err = filepath.Abs(relPath)
if err != nil {
return absPath, err
}
}
logrus.Debugf("resolved %s to %s to read the ssh identity file", relPath, absPath)
return absPath, nil
}

View File

@ -2,6 +2,7 @@ package commandconn
import (
"context"
"fmt"
"net"
"net/url"
@ -34,9 +35,25 @@ func getConnectionHelper(daemonURL string, sshFlags []string) (*connhelper.Conne
if err != nil {
return nil, errors.Wrap(err, "ssh host connection is not valid")
}
if err := sshPkg.EnsureHostKey(ctxConnDetails.Host); err != nil {
return nil, err
}
hostConfig, err := sshPkg.GetHostConfig(
ctxConnDetails.Host,
ctxConnDetails.User,
ctxConnDetails.Port,
)
if err != nil {
return nil, err
}
if hostConfig.IdentityFile != "" {
msg := "discovered %s as identity file for %s, using for ssh connection"
logrus.Debugf(msg, hostConfig.IdentityFile, ctxConnDetails.Host)
sshFlags = append(sshFlags, fmt.Sprintf("-o IdentityFile=%s", hostConfig.IdentityFile))
}
return &connhelper.ConnectionHelper{
Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) {
return New(ctx, "ssh", append(sshFlags, ctxConnDetails.Args("docker", "system", "dial-stdio")...)...)

View File

@ -13,6 +13,11 @@ import (
"github.com/sirupsen/logrus"
)
// DontSkipValidation ensures validation is done for compose file loading
func DontSkipValidation(opts *loader.Options) {
opts.SkipValidation = false
}
// LoadComposefile parse the composefile specified in the cli and returns its Config and version.
func LoadComposefile(opts Deploy, appEnv map[string]string) (*composetypes.Config, error) {
configDetails, err := getConfigDetails(opts.Composefiles, appEnv)
@ -21,13 +26,12 @@ func LoadComposefile(opts Deploy, appEnv map[string]string) (*composetypes.Confi
}
dicts := getDictsFrom(configDetails.ConfigFiles)
config, err := loader.Load(configDetails)
config, err := loader.Load(configDetails, DontSkipValidation)
if err != nil {
if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok {
return nil, fmt.Errorf("compose file contains unsupported options:\n\n%s",
propertyWarnings(fpe.Properties))
}
return nil, err
}

23
pkg/web/client.go Normal file
View File

@ -0,0 +1,23 @@
package web
import (
"fmt"
"github.com/hashicorp/go-retryablehttp"
"github.com/sirupsen/logrus"
)
type customLeveledLogger struct {
retryablehttp.Logger
}
func (l customLeveledLogger) Printf(msg string, args ...interface{}) {
logrus.Debugf(fmt.Sprintf(msg, args...))
}
// NewHTTPRetryClient instantiates a new http client with retries baked in
func NewHTTPRetryClient() *retryablehttp.Client {
retryClient := retryablehttp.NewClient()
retryClient.Logger = customLeveledLogger{}
return retryClient
}

View File

@ -3,7 +3,6 @@ package web
import (
"encoding/json"
"net/http"
"time"
)
@ -13,7 +12,7 @@ const Timeout = 10 * time.Second
// ReadJSON reads JSON and parses it into your chosen interface pointer
func ReadJSON(url string, target interface{}) error {
httpClient := &http.Client{Timeout: Timeout}
httpClient := NewHTTPRetryClient()
res, err := httpClient.Get(url)
if err != nil {
return err

View File

@ -2,7 +2,7 @@
ABRA_VERSION="0.3.0-alpha"
ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$ABRA_VERSION"
RC_VERSION="0.3.1-alpha-rc1"
RC_VERSION="0.3.1-alpha-rc2"
RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/coop-cloud/abra/releases/tags/$RC_VERSION"
for arg in "$@"; do