forked from toolshed/abra
.gitea
cli
app
app.go
backup.go
check.go
cmd.go
cmd_test.go
config.go
cp.go
deploy.go
errors.go
list.go
logs.go
new.go
ps.go
remove.go
restart.go
restore.go
rollback.go
run.go
secret.go
services.go
undeploy.go
upgrade.go
version.go
volume.go
catalogue
internal
recipe
record
server
updater
cli.go
cmd
pkg
scripts
tests
.dockerignore
.drone.yml
.e2e.env.sample
.envrc.sample
.gitignore
.goreleaser.yml
AUTHORS.md
Dockerfile
LICENSE
Makefile
README.md
go.mod
go.sum
renovate.json
250 lines
6.9 KiB
Go
250 lines
6.9 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"strings"
|
|
|
|
"coopcloud.tech/abra/cli/internal"
|
|
"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"
|
|
)
|
|
|
|
var appCmdCommand = cli.Command{
|
|
Name: "command",
|
|
Aliases: []string{"cmd"},
|
|
Usage: "Run app commands",
|
|
Description: `
|
|
Run an app specific command.
|
|
|
|
These commands are bash functions, defined in the abra.sh of the recipe itself.
|
|
They can be run within the context of a service (e.g. app) or locally on your
|
|
work station by passing "--local". Arguments can be passed into these functions
|
|
using the "-- <args>" syntax.
|
|
|
|
Example:
|
|
|
|
abra app cmd example.com app create_user -- me@example.com
|
|
`,
|
|
ArgsUsage: "<domain> [<service>] <command> [-- <args>]",
|
|
Flags: []cli.Flag{
|
|
internal.DebugFlag,
|
|
internal.LocalCmdFlag,
|
|
internal.RemoteUserFlag,
|
|
internal.TtyFlag,
|
|
},
|
|
BashComplete: autocomplete.AppNameComplete,
|
|
Before: internal.SubCommandBefore,
|
|
Action: func(c *cli.Context) error {
|
|
app := internal.ValidateApp(c)
|
|
|
|
cl, err := client.New(app.Server)
|
|
if err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
|
|
if internal.LocalCmd && internal.RemoteUser != "" {
|
|
internal.ShowSubcommandHelpAndError(c, errors.New("cannot use --local & --user together"))
|
|
}
|
|
|
|
hasCmdArgs, parsedCmdArgs := parseCmdArgs(c.Args(), internal.LocalCmd)
|
|
|
|
abraSh := path.Join(config.RECIPES_DIR, app.Recipe, "abra.sh")
|
|
if _, err := os.Stat(abraSh); err != nil {
|
|
if os.IsNotExist(err) {
|
|
logrus.Fatalf("%s does not exist for %s?", abraSh, app.Name)
|
|
}
|
|
logrus.Fatal(err)
|
|
}
|
|
|
|
if internal.LocalCmd {
|
|
cmdName := c.Args().Get(1)
|
|
if err := ensureCommand(abraSh, app.Recipe, cmdName); err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
|
|
logrus.Debugf("--local detected, running %s on local work station", cmdName)
|
|
|
|
var exportEnv string
|
|
for k, v := range app.Env {
|
|
exportEnv = exportEnv + fmt.Sprintf("%s='%s'; ", k, v)
|
|
}
|
|
var sourceAndExec string
|
|
if hasCmdArgs {
|
|
logrus.Debugf("parsed following command arguments: %s", parsedCmdArgs)
|
|
sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; STACK_NAME=%s; %s . %s; %s %s", app.Name, app.StackName(), exportEnv, abraSh, cmdName, parsedCmdArgs)
|
|
} else {
|
|
logrus.Debug("did not detect any command arguments")
|
|
sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; STACK_NAME=%s; %s . %s; %s", app.Name, app.StackName(), exportEnv, abraSh, cmdName)
|
|
}
|
|
|
|
shell := "/bin/bash"
|
|
if _, err := os.Stat(shell); errors.Is(err, os.ErrNotExist) {
|
|
logrus.Debugf("%s does not exist locally, use /bin/sh as fallback", shell)
|
|
shell = "/bin/sh"
|
|
}
|
|
cmd := exec.Command(shell, "-c", sourceAndExec)
|
|
|
|
if err := internal.RunCmd(cmd); err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
} else {
|
|
targetServiceName := c.Args().Get(1)
|
|
|
|
cmdName := c.Args().Get(2)
|
|
if err := ensureCommand(abraSh, app.Recipe, cmdName); err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
|
|
serviceNames, err := config.GetAppServiceNames(app.Name)
|
|
if err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
|
|
matchingServiceName := false
|
|
for _, serviceName := range serviceNames {
|
|
if serviceName == targetServiceName {
|
|
matchingServiceName = true
|
|
}
|
|
}
|
|
|
|
if !matchingServiceName {
|
|
logrus.Fatalf("no service %s for %s?", targetServiceName, app.Name)
|
|
}
|
|
|
|
logrus.Debugf("running command %s within the context of %s_%s", cmdName, app.StackName(), targetServiceName)
|
|
|
|
if hasCmdArgs {
|
|
logrus.Debugf("parsed following command arguments: %s", parsedCmdArgs)
|
|
} else {
|
|
logrus.Debug("did not detect any command arguments")
|
|
}
|
|
|
|
if err := runCmdRemote(cl, app, abraSh, targetServiceName, cmdName, parsedCmdArgs); err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
func parseCmdArgs(args []string, isLocal bool) (bool, string) {
|
|
var (
|
|
parsedCmdArgs string
|
|
hasCmdArgs bool
|
|
)
|
|
|
|
if isLocal {
|
|
if len(args) > 2 {
|
|
return true, fmt.Sprintf("%s ", strings.Join(args[2:], " "))
|
|
}
|
|
} else {
|
|
if len(args) > 3 {
|
|
return true, fmt.Sprintf("%s ", strings.Join(args[3:], " "))
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|