grim/convey

f9638cf514da
Parents 9e5fff47264d
Children 803836d06be3
Add a script/shell command that can run scripts on the host machine
--- a/REFERENCE.md Sat Dec 18 06:57:44 2021 -0600
+++ b/REFERENCE.md Mon Dec 20 11:39:14 2021 -0600
@@ -163,5 +163,6 @@
The documentation for the available task types can be found below:
* [Convey](tasks/README.md)
-* [Docker](docker/README.md) (deprecate)
+* [Docker](docker/README.md) (deprecated)
* [Podman](podman/README.md)
+* [Script](script/README.md)
--- a/config/tasks.go Sat Dec 18 06:57:44 2021 -0600
+++ b/config/tasks.go Mon Dec 20 11:39:14 2021 -0600
@@ -19,6 +19,7 @@
import (
"keep.imfreedom.org/grim/convey/docker"
"keep.imfreedom.org/grim/convey/podman"
+ "keep.imfreedom.org/grim/convey/script"
"keep.imfreedom.org/grim/convey/tasks"
)
@@ -45,4 +46,9 @@
for taskName, task := range podman.Tasks {
tasksMap[taskName] = task
}
+
+ // add the script tasks
+ for taskName, task := range script.Tasks {
+ tasksMap[taskName] = task
+ }
}
--- a/go.sum Sat Dec 18 06:57:44 2021 -0600
+++ b/go.sum Mon Dec 20 11:39:14 2021 -0600
@@ -1,7 +1,6 @@
-github.com/alecthomas/kong v0.2.17 h1:URDISCI96MIgcIlQyoCAlhOmrSw6pZScBNkctg8r0W0=
-github.com/alecthomas/kong v0.2.17/go.mod h1:ka3VZ8GZNPXv9Ov+j4YNLkI8mTuhXyr/0ktSlqIydQQ=
github.com/alecthomas/kong v0.2.22 h1:lRcQYT2/yJ+coDNA5ws0mRL0pwSqjbP/6AcRkyKhomk=
github.com/alecthomas/kong v0.2.22/go.mod h1:uzxf/HUh0tj43x1AyJROl3JT7SgsZ5m+icOv1csRhc0=
+github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48=
github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -13,8 +12,6 @@
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
-github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
@@ -23,8 +20,6 @@
github.com/mattn/go-zglob v0.0.3/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
-github.com/opencontainers/selinux v1.9.1 h1:b4VPEF3O5JLZgdTDBmGepaaIbAo0GqoF6EBRq5f/g3Y=
-github.com/opencontainers/selinux v1.9.1/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI=
github.com/opencontainers/selinux v1.10.0 h1:rAiKF8hTcgLI3w0DHm6i0ylVVcOrlgR1kK99DRLDhyU=
github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -42,8 +37,6 @@
golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac h1:oN6lz7iLW/YC7un8pq+9bOLyXrprv2+DKfkJY+2LJJw=
-golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/script/README.md Mon Dec 20 11:39:14 2021 -0600
@@ -0,0 +1,53 @@
+# Script
+
+Script is a set of tasks for running scripts on the host. These can be
+dangerous and should be used sparingly.
+
+----
+
+## script/shell
+
+Available on UNIX like platforms including BSD, macOS, and Linux, but will
+raise an error if attempted to be used on Windows.
+
+Either a filename or commands must be specified, but specifying both is an
+error as Convey will not know which one you want to prioritize. When commands
+are specified, a temporary file will be created containing the given commands.
+
+### Attributes
+
+| Name | Required | Default | Description |
+| ----------- | -------- | ------- | ----------- |
+| commands | | | A list of commands to run as the script. |
+| environment | | | A list of environment variables to set. This should be specified in a `NAME` or `NAME=VALUE` format. |
+| filename | | | The name of a script file to run. The file must exist in the directory containing the current convey configuration file or a subdirectory. |
+| shell | | | The shell to use. If not specified the users default shell will be used. |
+
+### Example
+
+Run an existing script
+
+ run-script:
+ type: script/shell
+ filename: script.sh
+
+Run an ahhoc script with a single command. The commands parameter will work
+with a single string or a list of strings
+
+ uptime:
+ type: script/shell
+ commands: uptime
+
+ uname:
+ type: script/shell
+ commands:
+ - uname -a
+
+Run an adhoc script in Z Shell
+
+ zshell:
+ type: script/shell
+ shell: /bin/zsh
+ commands:
+ - uname
+ - uptime
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/script/file.go Mon Dec 20 11:39:14 2021 -0600
@@ -0,0 +1,37 @@
+package script
+
+import (
+ "os"
+ "runtime"
+ "strings"
+
+ "keep.imfreedom.org/grim/convey/environment"
+)
+
+func sliceToFile(commands []string, path string, env environment.Environment) (string, error) {
+ script := env.Expandv(commands)
+
+ fp, err := os.CreateTemp(path, ".convey-script-")
+ if err != nil {
+ return "", err
+ }
+
+ lineEnding := "\n"
+ if runtime.GOOS == "windows" {
+ lineEnding = "\r\n"
+ }
+
+ if _, err = fp.Write([]byte(strings.Join(script, lineEnding))); err != nil {
+ return "", err
+ }
+
+ if err := fp.Chmod(0700); err != nil {
+ return "", err
+ }
+
+ if err := fp.Close(); err != nil {
+ return "", err
+ }
+
+ return fp.Name(), nil
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/script/interpreter.go Mon Dec 20 11:39:14 2021 -0600
@@ -0,0 +1,81 @@
+package script
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "os/user"
+ "runtime"
+ "strings"
+)
+
+func windowsInterpreter() (string, error) {
+ intrepreter := os.Getenv("COMSPEC")
+ if intrepreter == "" {
+ intrepreter = "cmd.exe"
+ }
+
+ return intrepreter, nil
+}
+
+func darwinInterpreter() (string, error) {
+ user, err := user.Current()
+ if err != nil {
+ return "", err
+ }
+
+ path := "Local/Default/User/" + user.Username
+ args := []string{"localhost", "-read", path, "UserShell"}
+ buffer, err := exec.Command("dscl", args...).Output()
+ if err != nil {
+ return "", err
+ }
+
+ output := strings.TrimSpace(string(buffer))
+ parts := strings.Split(output, " ")
+
+ if len(parts) != 2 {
+ return "", fmt.Errorf("unexpected output from dscl: %q", output)
+ }
+
+ return strings.TrimSpace(parts[1]), nil
+}
+
+func posixInterpreter() (string, error) {
+ user, err := user.Current()
+ if err != nil {
+ return "", err
+ }
+
+ buffer, err := exec.Command("getent", "passwd", user.Uid).Output()
+ if err != nil {
+ return "", err
+ }
+
+ output := strings.TrimSpace(string(buffer))
+ fields := strings.Split(output, ":")
+ if len(fields) < 6 {
+ return "", fmt.Errorf("unexpected output from getent: %q", output)
+ }
+
+ intrepreter := fields[6]
+ if intrepreter == "" {
+ return "", fmt.Errorf("no shell found for user %q", user.Uid)
+ }
+
+ return intrepreter, nil
+}
+
+func findInterpreter(given string) (string, error) {
+ if given != "" {
+ return given, nil
+ }
+
+ if runtime.GOOS == "windows" {
+ return windowsInterpreter()
+ } else if runtime.GOOS == "darwin" {
+ return darwinInterpreter()
+ } else {
+ return posixInterpreter()
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/script/shell.go Mon Dec 20 11:39:14 2021 -0600
@@ -0,0 +1,84 @@
+package script
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ gort "runtime"
+ "strings"
+
+ log "github.com/sirupsen/logrus"
+
+ "keep.imfreedom.org/grim/convey/environment"
+ "keep.imfreedom.org/grim/convey/exec"
+ "keep.imfreedom.org/grim/convey/runtime"
+ "keep.imfreedom.org/grim/convey/tasks"
+ "keep.imfreedom.org/grim/convey/yaml"
+)
+
+type Shell struct {
+ Commands yaml.StringOrSlice `yaml:"commands"`
+ Environment yaml.StringOrSlice `yaml:"environment"`
+ Filename string `yaml:"filename"`
+ Shell string `yaml:"shell"`
+}
+
+func (s *Shell) Execute(name string, logger *log.Entry, stageEnv environment.Environment, rt *runtime.Runtime) error {
+ // Create a new environment based on the stage's environment. Then merge
+ // the task's environment overriding anything from the stage. Finally merge
+ // the runtime enviroment which holds the environment from the command line.
+ env := stageEnv.Copy().MergeSlice(s.Environment).Merge(rt.Environment)
+
+ filename := filepath.Join(rt.ConfigPath, s.Filename)
+ if !strings.HasPrefix(filename, rt.ConfigPath) {
+ return fmt.Errorf("script is outside of the configuration files directory")
+ }
+ if filename == "" {
+ file, err := sliceToFile(s.Commands, rt.ConfigPath, env)
+ if err != nil {
+ return err
+ }
+
+ filename = file
+ defer os.Remove(filename)
+ }
+
+ shell, err := findInterpreter(s.Shell)
+ if err != nil {
+ return err
+ }
+
+ if shell == "" {
+ return fmt.Errorf("failed to determine which shell to use")
+ }
+
+ return exec.Run(name, []string{shell, filename}, rt.Timeout)
+}
+
+func (s *Shell) New() tasks.Task {
+ return &Shell{}
+}
+
+func (s *Shell) Valid() error {
+ if gort.GOOS == "windows" {
+ return fmt.Errorf("this task is not supported on windows")
+ }
+
+ if s.Shell == "" {
+ s.Shell = "/bin/sh"
+ }
+
+ if len(s.Commands) == 0 && s.Filename == "" {
+ return fmt.Errorf("a filename or at least one command must be specified")
+ }
+
+ if len(s.Commands) > 0 && s.Filename != "" {
+ return fmt.Errorf("only one of filename and commands may be specified")
+ }
+
+ return nil
+}
+
+func (s *Shell) Deprecated() error {
+ return nil
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/script/tasks.go Mon Dec 20 11:39:14 2021 -0600
@@ -0,0 +1,11 @@
+package script
+
+import (
+ "keep.imfreedom.org/grim/convey/tasks"
+)
+
+var (
+ Tasks = map[string]tasks.Task{
+ "script/shell": &Shell{},
+ }
+)