From 18615eaaefff6daaa768efd48dcca3938cfa4c33 Mon Sep 17 00:00:00 2001 From: moritz Date: Fri, 14 Apr 2023 14:12:31 +0000 Subject: [PATCH] Post-deploy abra.sh hooks (!292) This solves https://git.coopcloud.tech/coop-cloud/organising/issues/235 Co-authored-by: Moritz Reviewed-on: https://git.coopcloud.tech/coop-cloud/abra/pulls/292 --- cli/app/cmd.go | 100 ++-------------------------------------- cli/app/upgrade.go | 8 ++++ cli/internal/command.go | 99 +++++++++++++++++++++++++++++++++++++++ cli/internal/deploy.go | 63 +++++++++++++++++++++++++ go.mod | 2 + go.sum | 8 ++++ 6 files changed, 183 insertions(+), 97 deletions(-) diff --git a/cli/app/cmd.go b/cli/app/cmd.go index a0e383f9..a3bdf22e 100644 --- a/cli/app/cmd.go +++ b/cli/app/cmd.go @@ -1,10 +1,8 @@ package app import ( - "context" "errors" "fmt" - "io/ioutil" "os" "os/exec" "path" @@ -14,14 +12,6 @@ import ( "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/config" - containerPkg "coopcloud.tech/abra/pkg/container" - "coopcloud.tech/abra/pkg/formatter" - "coopcloud.tech/abra/pkg/upstream/container" - "github.com/docker/cli/cli/command" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - dockerClient "github.com/docker/docker/client" - "github.com/docker/docker/pkg/archive" "github.com/sirupsen/logrus" "github.com/urfave/cli" ) @@ -75,7 +65,7 @@ Example: if internal.LocalCmd { cmdName := c.Args().Get(1) - if err := ensureCommand(abraSh, app.Recipe, cmdName); err != nil { + if err := internal.EnsureCommand(abraSh, app.Recipe, cmdName); err != nil { logrus.Fatal(err) } @@ -108,7 +98,7 @@ Example: targetServiceName := c.Args().Get(1) cmdName := c.Args().Get(2) - if err := ensureCommand(abraSh, app.Recipe, cmdName); err != nil { + if err := internal.EnsureCommand(abraSh, app.Recipe, cmdName); err != nil { logrus.Fatal(err) } @@ -136,7 +126,7 @@ Example: logrus.Debug("did not detect any command arguments") } - if err := runCmdRemote(cl, app, abraSh, targetServiceName, cmdName, parsedCmdArgs); err != nil { + if err := internal.RunCmdRemote(cl, app, abraSh, targetServiceName, cmdName, parsedCmdArgs); err != nil { logrus.Fatal(err) } } @@ -163,87 +153,3 @@ func parseCmdArgs(args []string, isLocal bool) (bool, string) { return hasCmdArgs, parsedCmdArgs } - -func ensureCommand(abraSh, recipeName, execCmd string) error { - bytes, err := ioutil.ReadFile(abraSh) - if err != nil { - return err - } - - if !strings.Contains(string(bytes), execCmd) { - return fmt.Errorf("%s doesn't have a %s function", recipeName, execCmd) - } - - return nil -} - -func runCmdRemote(cl *dockerClient.Client, app config.App, abraSh, serviceName, cmdName, cmdArgs string) error { - filters := filters.NewArgs() - filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName)) - - targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, true) - if err != nil { - return err - } - - logrus.Debugf("retrieved %s as target container on %s", formatter.ShortenID(targetContainer.ID), app.Server) - - toTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip} - content, err := archive.TarWithOptions(abraSh, toTarOpts) - if err != nil { - return err - } - - copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false} - if err := cl.CopyToContainer(context.Background(), targetContainer.ID, "/tmp", content, copyOpts); err != nil { - return err - } - - // FIXME: avoid instantiating a new CLI - dcli, err := command.NewDockerCli() - if err != nil { - return err - } - - shell := "/bin/bash" - findShell := []string{"test", "-e", shell} - execCreateOpts := types.ExecConfig{ - AttachStderr: true, - AttachStdin: true, - AttachStdout: true, - Cmd: findShell, - Detach: false, - Tty: false, - } - - if err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil { - logrus.Infof("%s does not exist for %s, use /bin/sh as fallback", shell, app.Name) - shell = "/bin/sh" - } - - var cmd []string - if cmdArgs != "" { - cmd = []string{shell, "-c", fmt.Sprintf("TARGET=%s; APP_NAME=%s; STACK_NAME=%s; . /tmp/abra.sh; %s %s", serviceName, app.Name, app.StackName(), cmdName, cmdArgs)} - } else { - cmd = []string{shell, "-c", fmt.Sprintf("TARGET=%s; APP_NAME=%s; STACK_NAME=%s; . /tmp/abra.sh; %s", serviceName, app.Name, app.StackName(), cmdName)} - } - - logrus.Debugf("running command: %s", strings.Join(cmd, " ")) - - if internal.RemoteUser != "" { - logrus.Debugf("running command with user %s", internal.RemoteUser) - execCreateOpts.User = internal.RemoteUser - } - - execCreateOpts.Cmd = cmd - execCreateOpts.Tty = true - if internal.Tty { - execCreateOpts.Tty = false - } - - if err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil { - return err - } - - return nil -} diff --git a/cli/app/upgrade.go b/cli/app/upgrade.go index df903d40..65b66cb5 100644 --- a/cli/app/upgrade.go +++ b/cli/app/upgrade.go @@ -205,6 +205,14 @@ recipes. logrus.Fatal(err) } + postDeployCmds, ok := app.Env["POST_UPGRADE_CMDS"] + if ok && !internal.DontWaitConverge { + logrus.Debugf("run the following post-deploy commands: %s", postDeployCmds) + if err := internal.PostCmds(cl, app, postDeployCmds); err != nil { + logrus.Fatalf("attempting to run post deploy commands, saw: %s", err) + } + } + return nil }, BashComplete: autocomplete.AppNameComplete, diff --git a/cli/internal/command.go b/cli/internal/command.go index e3d69fd6..c6e4a35d 100644 --- a/cli/internal/command.go +++ b/cli/internal/command.go @@ -2,10 +2,109 @@ package internal import ( "bufio" + "context" "fmt" + "io/ioutil" "os/exec" + "strings" + + "coopcloud.tech/abra/pkg/config" + containerPkg "coopcloud.tech/abra/pkg/container" + "coopcloud.tech/abra/pkg/formatter" + "coopcloud.tech/abra/pkg/upstream/container" + "github.com/docker/cli/cli/command" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + dockerClient "github.com/docker/docker/client" + "github.com/docker/docker/pkg/archive" + "github.com/sirupsen/logrus" ) +// RunCmdRemote executes an abra.sh command in the target service +func RunCmdRemote(cl *dockerClient.Client, app config.App, abraSh, serviceName, cmdName, cmdArgs string) error { + filters := filters.NewArgs() + filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName)) + + targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, true) + if err != nil { + return err + } + + logrus.Debugf("retrieved %s as target container on %s", formatter.ShortenID(targetContainer.ID), app.Server) + + toTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip} + content, err := archive.TarWithOptions(abraSh, toTarOpts) + if err != nil { + return err + } + + copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false} + if err := cl.CopyToContainer(context.Background(), targetContainer.ID, "/tmp", content, copyOpts); err != nil { + return err + } + + // FIXME: avoid instantiating a new CLI + dcli, err := command.NewDockerCli() + if err != nil { + return err + } + + shell := "/bin/bash" + findShell := []string{"test", "-e", shell} + execCreateOpts := types.ExecConfig{ + AttachStderr: true, + AttachStdin: true, + AttachStdout: true, + Cmd: findShell, + Detach: false, + Tty: false, + } + + if err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil { + logrus.Infof("%s does not exist for %s, use /bin/sh as fallback", shell, app.Name) + shell = "/bin/sh" + } + + var cmd []string + if cmdArgs != "" { + cmd = []string{shell, "-c", fmt.Sprintf("TARGET=%s; APP_NAME=%s; STACK_NAME=%s; . /tmp/abra.sh; %s %s", serviceName, app.Name, app.StackName(), cmdName, cmdArgs)} + } else { + cmd = []string{shell, "-c", fmt.Sprintf("TARGET=%s; APP_NAME=%s; STACK_NAME=%s; . /tmp/abra.sh; %s", serviceName, app.Name, app.StackName(), cmdName)} + } + + logrus.Debugf("running command: %s", strings.Join(cmd, " ")) + + if RemoteUser != "" { + logrus.Debugf("running command with user %s", RemoteUser) + execCreateOpts.User = RemoteUser + } + + execCreateOpts.Cmd = cmd + execCreateOpts.Tty = true + if Tty { + execCreateOpts.Tty = false + } + + if err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil { + return err + } + + return nil +} + +func EnsureCommand(abraSh, recipeName, execCmd string) error { + bytes, err := ioutil.ReadFile(abraSh) + if err != nil { + return err + } + + if !strings.Contains(string(bytes), execCmd) { + return fmt.Errorf("%s doesn't have a %s function", recipeName, execCmd) + } + + return nil +} + // RunCmd runs a shell command and streams stdout/stderr in real-time. func RunCmd(cmd *exec.Cmd) error { r, err := cmd.StdoutPipe() diff --git a/cli/internal/deploy.go b/cli/internal/deploy.go index 34794606..e1d7eff6 100644 --- a/cli/internal/deploy.go +++ b/cli/internal/deploy.go @@ -18,6 +18,7 @@ import ( "coopcloud.tech/abra/pkg/runtime" "coopcloud.tech/abra/pkg/upstream/stack" "github.com/AlecAivazis/survey/v2" + dockerClient "github.com/docker/docker/client" "github.com/sirupsen/logrus" "github.com/urfave/cli" ) @@ -164,6 +165,68 @@ func DeployAction(c *cli.Context) error { logrus.Fatal(err) } + postDeployCmds, ok := app.Env["POST_DEPLOY_CMDS"] + if ok && !DontWaitConverge { + logrus.Debugf("run the following post-deploy commands: %s", postDeployCmds) + if err := PostCmds(cl, app, postDeployCmds); err != nil { + logrus.Fatalf("attempting to run post deploy commands, saw: %s", err) + } + } + + return nil +} + +// PostCmds parses a string of commands and executes them inside of the respective services +// the commands string must have the following format: +// " | |... " +func PostCmds(cl *dockerClient.Client, app config.App, commands string) error { + abraSh := path.Join(config.RECIPES_DIR, app.Recipe, "abra.sh") + if _, err := os.Stat(abraSh); err != nil { + if os.IsNotExist(err) { + return fmt.Errorf(fmt.Sprintf("%s does not exist for %s?", abraSh, app.Name)) + } + return err + } + + for _, command := range strings.Split(commands, "|") { + commandParts := strings.Split(command, " ") + if len(commandParts) < 2 { + return fmt.Errorf(fmt.Sprintf("not enough arguments: %s", command)) + } + targetServiceName := commandParts[0] + cmdName := commandParts[1] + parsedCmdArgs := "" + if len(commandParts) > 2 { + parsedCmdArgs = fmt.Sprintf("%s ", strings.Join(commandParts[2:], " ")) + } + logrus.Infof("running post-command '%s %s' in container %s", cmdName, parsedCmdArgs, targetServiceName) + + if err := EnsureCommand(abraSh, app.Recipe, cmdName); err != nil { + return err + } + + serviceNames, err := config.GetAppServiceNames(app.Name) + if err != nil { + return err + } + + matchingServiceName := false + for _, serviceName := range serviceNames { + if serviceName == targetServiceName { + matchingServiceName = true + } + } + + if !matchingServiceName { + return fmt.Errorf(fmt.Sprintf("no service %s for %s?", targetServiceName, app.Name)) + } + + logrus.Debugf("running command %s %s within the context of %s_%s", cmdName, parsedCmdArgs, app.StackName(), targetServiceName) + + if err := RunCmdRemote(cl, app, abraSh, targetServiceName, cmdName, parsedCmdArgs); err != nil { + return err + } + } return nil } diff --git a/go.mod b/go.mod index 12e978c1..85e4af59 100644 --- a/go.mod +++ b/go.mod @@ -30,11 +30,13 @@ require ( github.com/containers/storage v1.38.2 // indirect github.com/decentral1se/passgen v1.0.1 github.com/docker/docker-credential-helpers v0.6.4 // indirect + github.com/docker/go-connections v0.4.0 // indirect github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect github.com/fvbommel/sortorder v1.0.2 // indirect 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.2 + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/klauspost/pgzip v1.2.5 github.com/libdns/gandi v1.0.2 github.com/libdns/libdns v0.2.1 diff --git a/go.sum b/go.sum index 6fb1a0e9..c93d322a 100644 --- a/go.sum +++ b/go.sum @@ -610,6 +610,8 @@ github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKEN github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/hetznercloud/hcloud-go v1.41.0 h1:KJGFRRc68QiVu4PrEP5BmCQVveCP2CM26UGQUKGpIUs= github.com/hetznercloud/hcloud-go v1.41.0/go.mod h1:NaHg47L6C77mngZhwBG652dTAztYrsZ2/iITJKhQkHA= +github.com/hetznercloud/hcloud-go v1.42.0 h1:Es/CDOForQN3nOOP5Vxh1N/YHjpCg386iYEX5zCgi+A= +github.com/hetznercloud/hcloud-go v1.42.0/go.mod h1:YADL8AbmQYH0Eo+1lkuyoc8LutT0UeMvaKP47nNUb+Y= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -1190,6 +1192,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= 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-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1352,6 +1356,8 @@ golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= 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.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1365,6 +1371,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 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-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=