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" "github.com/docker/docker/pkg/archive" "github.com/sirupsen/logrus" "github.com/urfave/cli" ) var localCmd bool var localCmdFlag = &cli.BoolFlag{ Name: "local, l", Usage: "Run command locally", Destination: &localCmd, } var remoteUser string var remoteUserFlag = &cli.StringFlag{ Name: "user, u", Value: "", Usage: "User to run command within a service context", Destination: &remoteUser, } var appCmdCommand = cli.Command{ Name: "command", Aliases: []string{"cmd"}, Usage: "Run app commands", Description: ` This command runs app specific commands. 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 "-- " syntax. Example: abra app cmd example.com app create_user -- me@example.com `, ArgsUsage: " [] ", Flags: []cli.Flag{ internal.DebugFlag, localCmdFlag, remoteUserFlag, }, BashComplete: autocomplete.AppNameComplete, Before: internal.SubCommandBefore, Action: func(c *cli.Context) error { app := internal.ValidateApp(c) if len(c.Args()) <= 2 && !localCmd { internal.ShowSubcommandHelpAndError(c, errors.New("missing /? did you mean to pass --local?")) } if len(c.Args()) > 2 && localCmd { internal.ShowSubcommandHelpAndError(c, errors.New("cannot specify and --local together")) } 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 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) sourceAndExec := fmt.Sprintf("TARGET=local; APP_NAME=%s; . %s; %s", app.StackName(), abraSh, cmdName) cmd := exec.Command("/bin/sh", "-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) var parsedCmdArgs string var cmdArgsIdx int var hasCmdArgs bool for idx, arg := range c.Args() { if arg == "--" { cmdArgsIdx = idx hasCmdArgs = true } if hasCmdArgs && idx > cmdArgsIdx { parsedCmdArgs += fmt.Sprintf("%s ", c.Args().Get(idx)) } } if hasCmdArgs { logrus.Debugf("parsed following command arguments: %s", parsedCmdArgs) } else { logrus.Debug("did not detect any command arguments") } if err := runCmdRemote(app, abraSh, targetServiceName, cmdName, parsedCmdArgs); err != nil { logrus.Fatal(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 } func runCmdRemote(app config.App, abraSh, serviceName, cmdName, cmdArgs string) error { cl, err := client.New(app.Server) if err != nil { return err } 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 } var cmd []string if cmdArgs != "" { cmd = []string{"/bin/sh", "-c", fmt.Sprintf("TARGET=%s; APP_NAME=%s; . /tmp/abra.sh; %s %s", serviceName, app.StackName(), cmdName, cmdArgs)} } else { cmd = []string{"/bin/sh", "-c", fmt.Sprintf("TARGET=%s; APP_NAME=%s; . /tmp/abra.sh; %s", serviceName, app.StackName(), cmdName)} } logrus.Debugf("running command: %s", strings.Join(cmd, " ")) execCreateOpts := types.ExecConfig{ AttachStderr: true, AttachStdin: true, AttachStdout: true, Cmd: cmd, Detach: false, Tty: true, } if remoteUser != "" { logrus.Debugf("running command with user %s", remoteUser) execCreateOpts.User = remoteUser } // FIXME: avoid instantiating a new CLI dcli, err := command.NewDockerCli() if err != nil { return err } if err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil { return err } return nil }