diff options
-rw-r--r-- | cmd/podman/container.go | 1 | ||||
-rw-r--r-- | cmd/podman/runlabel.go | 188 | ||||
-rw-r--r-- | cmd/podman/shared/funcs.go | 57 | ||||
-rw-r--r-- | cmd/podman/shared/funcs_test.go | 89 | ||||
-rw-r--r-- | commands.md | 1 | ||||
-rw-r--r-- | completions/bash/podman | 30 | ||||
-rw-r--r-- | docs/podman-container-runlabel.1.md | 102 | ||||
-rw-r--r-- | libpod/image/image.go | 14 | ||||
-rw-r--r-- | libpod/oci.go | 14 | ||||
-rw-r--r-- | test/e2e/libpod_suite_test.go | 3 | ||||
-rw-r--r-- | utils/utils.go | 5 |
11 files changed, 495 insertions, 9 deletions
diff --git a/cmd/podman/container.go b/cmd/podman/container.go index b73fb7a94..82c1c824d 100644 --- a/cmd/podman/container.go +++ b/cmd/podman/container.go @@ -25,6 +25,7 @@ var ( restartCommand, rmCommand, runCommand, + runlabelCommand, startCommand, statsCommand, stopCommand, diff --git a/cmd/podman/runlabel.go b/cmd/podman/runlabel.go new file mode 100644 index 000000000..c5dd98ee6 --- /dev/null +++ b/cmd/podman/runlabel.go @@ -0,0 +1,188 @@ +package main + +import ( + "fmt" + "io" + "os" + "strings" + + "github.com/containers/libpod/cmd/podman/libpodruntime" + "github.com/containers/libpod/cmd/podman/shared" + "github.com/containers/libpod/libpod/image" + "github.com/containers/libpod/utils" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/urfave/cli" +) + +var ( + runlabelFlags = []cli.Flag{ + cli.StringFlag{ + Name: "authfile", + Usage: "Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json", + }, + cli.BoolFlag{ + Name: "display", + Usage: "preview the command that `podman install` would execute", + }, + cli.StringFlag{ + Name: "cert-dir", + Usage: "`pathname` of a directory containing TLS certificates and keys", + }, + cli.StringFlag{ + Name: "creds", + Usage: "`credentials` (USERNAME:PASSWORD) to use for authenticating to a registry", + }, + cli.StringFlag{ + Name: "name", + Usage: "Assign a name to the container", + }, + cli.StringFlag{ + Name: "opt1", + Usage: "Optional parameter to pass for install", + Hidden: true, + }, + cli.StringFlag{ + Name: "opt2", + Usage: "Optional parameter to pass for install", + Hidden: true, + }, + cli.StringFlag{ + Name: "opt3", + Usage: "Optional parameter to pass for install", + Hidden: true, + }, + cli.BoolFlag{ + Name: "quiet, q", + Usage: "Suppress output information when installing images", + }, + cli.BoolFlag{ + Name: "pull, p", + Usage: "pull the image if it does not exist locally prior to executing the label contents", + }, + cli.StringFlag{ + Name: "signature-policy", + Usage: "`pathname` of signature policy file (not usually used)", + }, + cli.BoolTFlag{ + Name: "tls-verify", + Usage: "require HTTPS and verify certificates when contacting registries (default: true)", + }, + } + + runlabelDescription = ` +Executes a command as described by a container image label. +` + runlabelCommand = cli.Command{ + Name: "runlabel", + Usage: "Execute the command described by an image label", + Description: runlabelDescription, + Flags: runlabelFlags, + Action: runlabelCmd, + ArgsUsage: "", + OnUsageError: usageErrorHandler, + } +) + +// installCmd gets the data from the command line and calls installImage +// to copy an image from a registry to a local machine +func runlabelCmd(c *cli.Context) error { + var ( + imageName string + stdErr, stdOut io.Writer + stdIn io.Reader + newImage *image.Image + ) + + opts := make(map[string]string) + runtime, err := libpodruntime.GetRuntime(c) + if err != nil { + return errors.Wrapf(err, "could not get runtime") + } + defer runtime.Shutdown(false) + + args := c.Args() + if len(args) == 0 { + logrus.Errorf("an image name must be specified") + return nil + } + if len(args) < 2 { + logrus.Errorf("the runlabel command requires at least 2 arguments") + return nil + } + if err := validateFlags(c, runlabelFlags); err != nil { + return err + } + if c.Bool("display") && c.Bool("quiet") { + return errors.Errorf("the display and quiet flags cannot be used together.") + } + + pull := c.Bool("pull") + label := args[0] + + runlabelImage := args[1] + + if c.IsSet("opts1") { + opts["opts1"] = c.String("opts1") + } + if c.IsSet("opts2") { + opts["opts2"] = c.String("opts2") + } + if c.IsSet("opts3") { + opts["opts3"] = c.String("opts3") + } + + ctx := getContext() + rtc := runtime.GetConfig() + + stdErr = os.Stderr + stdOut = os.Stdout + stdIn = os.Stdin + + if c.Bool("quiet") { + stdErr = nil + stdOut = nil + stdIn = nil + } + + if pull { + newImage, err = runtime.ImageRuntime().New(ctx, runlabelImage, rtc.SignaturePolicyPath, "", stdOut, nil, image.SigningOptions{}, false, false) + } else { + newImage, err = runtime.ImageRuntime().NewFromLocal(runlabelImage) + } + if err != nil { + return errors.Wrapf(err, "unable to find image") + } + + if len(newImage.Names()) < 1 { + imageName = newImage.ID() + } else { + imageName = newImage.Names()[0] + } + + runLabel, err := newImage.GetLabel(ctx, label) + if err != nil { + return err + } + + // If no label to execute, we return + if runLabel == "" { + return nil + } + + // The user provided extra arguments that need to be tacked onto the label's command + if len(args) > 2 { + runLabel = fmt.Sprintf("%s %s", runLabel, strings.Join(args[2:], " ")) + } + + cmd := shared.GenerateCommand(runLabel, imageName, c.String("name")) + env := shared.GenerateRunEnvironment(c.String("name"), imageName, opts) + + if !c.Bool("quiet") { + fmt.Printf("Command: %s\n", strings.Join(cmd, " ")) + if c.Bool("display") { + return nil + } + } + return utils.ExecCmdWithStdStreams(stdIn, stdOut, stdErr, env, cmd[0], cmd[1:]...) +} diff --git a/cmd/podman/shared/funcs.go b/cmd/podman/shared/funcs.go new file mode 100644 index 000000000..5c401634c --- /dev/null +++ b/cmd/podman/shared/funcs.go @@ -0,0 +1,57 @@ +package shared + +import ( + "fmt" + "os" + "strings" +) + +// GenerateCommand takes a label (string) and converts it to an executable command +func GenerateCommand(command, imageName, name string) []string { + var ( + newCommand []string + ) + if name == "" { + name = imageName + } + cmd := strings.Split(command, " ") + // Replace the first position of cmd with podman whether + // it is docker, /usr/bin/docker, or podman + newCommand = append(newCommand, "podman") + for _, arg := range cmd[1:] { + var newArg string + switch arg { + case "IMAGE": + newArg = imageName + case "IMAGE=IMAGE": + newArg = fmt.Sprintf("IMAGE=%s", imageName) + case "NAME": + newArg = name + case "NAME=NAME": + newArg = fmt.Sprintf("NAME=%s", name) + default: + newArg = arg + } + newCommand = append(newCommand, newArg) + } + return newCommand +} + +// GenerateRunEnvironment merges the current environment variables with optional +// environment variables provided by the user +func GenerateRunEnvironment(name, imageName string, opts map[string]string) []string { + newEnv := os.Environ() + newEnv = append(newEnv, fmt.Sprintf("NAME=%s", name)) + newEnv = append(newEnv, fmt.Sprintf("IMAGE=%s", imageName)) + + if opts["opt1"] != "" { + newEnv = append(newEnv, fmt.Sprintf("OPT1=%s", opts["opt1"])) + } + if opts["opt2"] != "" { + newEnv = append(newEnv, fmt.Sprintf("OPT2=%s", opts["opt2"])) + } + if opts["opt3"] != "" { + newEnv = append(newEnv, fmt.Sprintf("OPT3=%s", opts["opt3"])) + } + return newEnv +} diff --git a/cmd/podman/shared/funcs_test.go b/cmd/podman/shared/funcs_test.go new file mode 100644 index 000000000..3d0ac005f --- /dev/null +++ b/cmd/podman/shared/funcs_test.go @@ -0,0 +1,89 @@ +package shared + +import ( + "strings" + "testing" + + "github.com/containers/libpod/pkg/util" + "github.com/stretchr/testify/assert" +) + +var ( + name = "foo" + imageName = "bar" +) + +func TestGenerateCommand(t *testing.T) { + inputCommand := "docker run -it --name NAME -e NAME=NAME -e IMAGE=IMAGE IMAGE echo install" + correctCommand := "podman run -it --name bar -e NAME=bar -e IMAGE=foo foo echo install" + newCommand := GenerateCommand(inputCommand, "foo", "bar") + assert.Equal(t, correctCommand, strings.Join(newCommand, " ")) +} + +func TestGenerateCommandPath(t *testing.T) { + inputCommand := "/usr/bin/docker run -it --name NAME -e NAME=NAME -e IMAGE=IMAGE IMAGE echo install" + correctCommand := "podman run -it --name bar -e NAME=bar -e IMAGE=foo foo echo install" + newCommand := GenerateCommand(inputCommand, "foo", "bar") + assert.Equal(t, correctCommand, strings.Join(newCommand, " ")) +} + +func TestGenerateCommandNoSetName(t *testing.T) { + inputCommand := "docker run -it --name NAME -e NAME=NAME -e IMAGE=IMAGE IMAGE echo install" + correctCommand := "podman run -it --name foo -e NAME=foo -e IMAGE=foo foo echo install" + newCommand := GenerateCommand(inputCommand, "foo", "") + assert.Equal(t, correctCommand, strings.Join(newCommand, " ")) +} + +func TestGenerateCommandNoName(t *testing.T) { + inputCommand := "docker run -it -e IMAGE=IMAGE IMAGE echo install" + correctCommand := "podman run -it -e IMAGE=foo foo echo install" + newCommand := GenerateCommand(inputCommand, "foo", "") + assert.Equal(t, correctCommand, strings.Join(newCommand, " ")) +} + +func TestGenerateCommandAlreadyPodman(t *testing.T) { + inputCommand := "podman run -it --name NAME -e NAME=NAME -e IMAGE=IMAGE IMAGE echo install" + correctCommand := "podman run -it --name bar -e NAME=bar -e IMAGE=foo foo echo install" + newCommand := GenerateCommand(inputCommand, "foo", "bar") + assert.Equal(t, correctCommand, strings.Join(newCommand, " ")) +} + +func TestGenerateRunEnvironment(t *testing.T) { + opts := make(map[string]string) + opts["opt1"] = "one" + opts["opt2"] = "two" + opts["opt3"] = "three" + envs := GenerateRunEnvironment(name, imageName, opts) + assert.True(t, util.StringInSlice("OPT1=one", envs)) + assert.True(t, util.StringInSlice("OPT2=two", envs)) + assert.True(t, util.StringInSlice("OPT3=three", envs)) +} + +func TestGenerateRunEnvironmentNoOpts(t *testing.T) { + opts := make(map[string]string) + envs := GenerateRunEnvironment(name, imageName, opts) + assert.False(t, util.StringInSlice("OPT1=", envs)) + assert.False(t, util.StringInSlice("OPT2=", envs)) + assert.False(t, util.StringInSlice("OPT3=", envs)) +} + +func TestGenerateRunEnvironmentSingleOpt(t *testing.T) { + opts := make(map[string]string) + opts["opt1"] = "one" + envs := GenerateRunEnvironment(name, imageName, opts) + assert.True(t, util.StringInSlice("OPT1=one", envs)) + assert.False(t, util.StringInSlice("OPT2=", envs)) + assert.False(t, util.StringInSlice("OPT3=", envs)) +} + +func TestGenerateRunEnvironmentName(t *testing.T) { + opts := make(map[string]string) + envs := GenerateRunEnvironment(name, imageName, opts) + assert.True(t, util.StringInSlice("NAME=foo", envs)) +} + +func TestGenerateRunEnvironmentImage(t *testing.T) { + opts := make(map[string]string) + envs := GenerateRunEnvironment(name, imageName, opts) + assert.True(t, util.StringInSlice("IMAGE=bar", envs)) +} diff --git a/commands.md b/commands.md index a0a97f9de..c84938e64 100644 --- a/commands.md +++ b/commands.md @@ -50,6 +50,7 @@ | [podman-rm(1)](/docs/podman-rm.1.md) | Removes one or more containers |[![...](/docs/play.png)](https://asciinema.org/a/7EMk22WrfGtKWmgHJX9Nze1Qp)| | [podman-rmi(1)](/docs/podman-rmi.1.md) | Removes one or more images |[![...](/docs/play.png)](https://asciinema.org/a/133799)| | [podman-run(1)](/docs/podman-run.1.md) | Run a command in a container || +| [podman-runlabel(1)](/docs/podman-container-runlabel.1.md) | Executes the command of a container image's label || | [podman-save(1)](/docs/podman-save.1.md) | Saves an image to an archive |[![...](/docs/play.png)](https://asciinema.org/a/kp8kOaexEhEa20P1KLZ3L5X4g)| | [podman-search(1)](/docs/podman-search.1.md) | Search a registry for an image || | [podman-start(1)](/docs/podman-start.1.md) | Starts one or more containers diff --git a/completions/bash/podman b/completions/bash/podman index b97c4b0d5..b5425e702 100644 --- a/completions/bash/podman +++ b/completions/bash/podman @@ -2084,6 +2084,36 @@ _podman_logout() { _complete_ "$options_with_args" "$boolean_options" } +_podman_container_runlabel() { + local options_with_args=" + --authfile + --cert-dir + --creds + --name + --signature-policy + " + + local boolean_options=" + --display + --help + -h + -p + --pull + -q + --quiet + --tls-verify + " + + case "$cur" in + -*) + COMPREPLY=($(compgen -W "$boolean_options $options_with_args" -- "$cur")) + ;; + *) + __podman_complete_images --id + ;; + esac +} + _podman_pod_create() { local options_with_args=" --cgroup-parent diff --git a/docs/podman-container-runlabel.1.md b/docs/podman-container-runlabel.1.md new file mode 100644 index 000000000..889a5fb03 --- /dev/null +++ b/docs/podman-container-runlabel.1.md @@ -0,0 +1,102 @@ +% PODMAN(1) Podman Man Pages +% Brent Baude +% September 2018 +# NAME +podman-container-runlabel - Execute Image Label Method + +# SYNOPSIS +**podman container runlabel** +[**-h**|**--help**] +[**--display**] +[**-n**][**--name**[=*NAME*]] +[**-p**][[**--pull**]] +[**--rootfs**=*ROOTFS*] +[**--set**=*NAME*=*VALUE*] +[**--storage**] +LABEL IMAGE [ARG...] + +# DESCRIPTION +**podman container runlabel** reads the provided `LABEL` field in the container +IMAGE and executes the provided value for the label as a command. If this field does not +exist, `podman container runlabel` will just exit. + +If the container image has a LABEL INSTALL instruction like the following: + +`LABEL INSTALL /usr/bin/podman run -t -i --rm \${OPT1} --privileged -v /:/host --net=host --ipc=host --pid=host -e HOST=/host -e NAME=\${NAME} -e IMAGE=\${IMAGE} -e CONFDIR=\/etc/${NAME} -e LOGDIR=/var/log/\${NAME} -e DATADIR=/var/lib/\${NAME} \${IMAGE} \${OPT2} /bin/install.sh \${OPT3}` + +`podman container runlabel` will set the following environment variables for use in the command: + +Note: Podman will always ensure that `podman` is the first argument of the command being executed. + +**NAME** +The name specified via the command. NAME will be replaced with IMAGE if it is not specified. + +**IMAGE** +Image name specified via the command. + +**SUDO_UID** +The `SUDO_UID` environment variable. This is useful with the podman +`-u` option for user space tools. If the environment variable is +not available, the value of `/proc/self/loginuid` is used. + +**SUDO_GID** +The `SUDO_GID` environment variable. This is useful with the podman +`-u` option for user space tools. If the environment variable is +not available, the default GID of the value for `SUDO_UID` is used. +If this value is not available, the value of `/proc/self/loginuid` +is used. + +Any additional arguments will be appended to the command. + +# OPTIONS: +**--authfile** + +Path of the authentication file. Default is ${XDG_RUNTIME\_DIR}/containers/auth.json, which is set using `podman login`. +If the authorization state is not found there, $HOME/.docker/config.json is checked, which is set using `docker login`. + +**--display** + +Display the label's value of the image having populated its environment variables. +The runlabel command will not execute if --display is specified. + +**--cert-dir** *path* + +Use certificates at *path* (\*.crt, \*.cert, \*.key) to connect to the registry. +Default certificates directory is _/etc/containers/certs.d_. + +**--creds** + +The [username[:password]] to use to authenticate with the registry if required. +If one or both values are not supplied, a command line prompt will appear and the +value can be entered. The password is entered without echo. + +**-h** **--help** +Print usage statement + +**-n** **--name**="" + Use this name for creating content for the container. NAME will default to the IMAGENAME if it is not specified. + +**p** **--pull** + Pull the image if it cannot be found in local storage. + +**--quiet, -q** + +Suppress output information when pulling images + +**--signature-policy="PATHNAME"** + +Pathname of a signature policy file to use. It is not recommended that this +option be used, as the default behavior of using the system-wide default policy +(frequently */etc/containers/policy.json*) is most often preferred + +**--tls-verify** + +Require HTTPS and verify certificates when contacting registries (default: true). If explicitly set to true, +then tls verification will be used, If set to false then tls verification will not be used. If not specified +tls verification will be used unless the target registry is listed as an insecure registry in registries.conf + +## SEE ALSO +podman(1) + +# HISTORY +September 2018, Originally compiled by Brent Baude (bbaude at redhat dot com) diff --git a/libpod/image/image.go b/libpod/image/image.go index 197a83dc1..f39b1d78d 100644 --- a/libpod/image/image.go +++ b/libpod/image/image.go @@ -744,6 +744,20 @@ func (i *Image) Labels(ctx context.Context) (map[string]string, error) { return imgInspect.Labels, nil } +// GetLabel Returns a case-insensitive match of a given label +func (i *Image) GetLabel(ctx context.Context, label string) (string, error) { + imageLabels, err := i.Labels(ctx) + if err != nil { + return "", err + } + for k, v := range imageLabels { + if strings.ToLower(k) == strings.ToLower(label) { + return v, nil + } + } + return "", nil +} + // Annotations returns the annotations of an image func (i *Image) Annotations(ctx context.Context) (map[string]string, error) { manifest, manifestType, err := i.Manifest(ctx) diff --git a/libpod/oci.go b/libpod/oci.go index 3838394cb..17f0630c4 100644 --- a/libpod/oci.go +++ b/libpod/oci.go @@ -535,7 +535,7 @@ func (r *OCIRuntime) updateContainerStatus(ctr *Container) error { // Sets time the container was started, but does not save it. func (r *OCIRuntime) startContainer(ctr *Container) error { // TODO: streams should probably *not* be our STDIN/OUT/ERR - redirect to buffers? - if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, r.path, "start", ctr.ID()); err != nil { + if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, nil, r.path, "start", ctr.ID()); err != nil { return err } @@ -547,7 +547,7 @@ func (r *OCIRuntime) startContainer(ctr *Container) error { // killContainer sends the given signal to the given container func (r *OCIRuntime) killContainer(ctr *Container, signal uint) error { logrus.Debugf("Sending signal %d to container %s", signal, ctr.ID()) - if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, r.path, "kill", ctr.ID(), fmt.Sprintf("%d", signal)); err != nil { + if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, nil, r.path, "kill", ctr.ID(), fmt.Sprintf("%d", signal)); err != nil { return errors.Wrapf(err, "error sending signal to container %s", ctr.ID()) } @@ -605,7 +605,7 @@ func (r *OCIRuntime) stopContainer(ctr *Container, timeout uint) error { args = []string{"kill", "--all", ctr.ID(), "KILL"} } - if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, r.path, args...); err != nil { + if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, nil, r.path, args...); err != nil { // Again, check if the container is gone. If it is, exit cleanly. err := unix.Kill(ctr.state.PID, 0) if err == unix.ESRCH { @@ -631,12 +631,12 @@ func (r *OCIRuntime) deleteContainer(ctr *Container) error { // pauseContainer pauses the given container func (r *OCIRuntime) pauseContainer(ctr *Container) error { - return utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, r.path, "pause", ctr.ID()) + return utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, nil, r.path, "pause", ctr.ID()) } // unpauseContainer unpauses the given container func (r *OCIRuntime) unpauseContainer(ctr *Container) error { - return utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, r.path, "resume", ctr.ID()) + return utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, nil, r.path, "resume", ctr.ID()) } // execContainer executes a command in a running container @@ -740,7 +740,7 @@ func (r *OCIRuntime) execStopContainer(ctr *Container, timeout uint) error { // Stop using SIGTERM by default // Use SIGSTOP after a timeout logrus.Debugf("Killing all processes in container %s with SIGTERM", ctr.ID()) - if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, r.path, "kill", "--all", ctr.ID(), "TERM"); err != nil { + if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, nil, r.path, "kill", "--all", ctr.ID(), "TERM"); err != nil { return errors.Wrapf(err, "error sending SIGTERM to container %s processes", ctr.ID()) } @@ -755,7 +755,7 @@ func (r *OCIRuntime) execStopContainer(ctr *Container, timeout uint) error { // Send SIGKILL logrus.Debugf("Killing all processes in container %s with SIGKILL", ctr.ID()) - if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, r.path, "kill", "--all", ctr.ID(), "KILL"); err != nil { + if err := utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, nil, r.path, "kill", "--all", ctr.ID(), "KILL"); err != nil { return errors.Wrapf(err, "error sending SIGKILL to container %s processes", ctr.ID()) } diff --git a/test/e2e/libpod_suite_test.go b/test/e2e/libpod_suite_test.go index a1e9ba57a..485b14ba5 100644 --- a/test/e2e/libpod_suite_test.go +++ b/test/e2e/libpod_suite_test.go @@ -31,7 +31,7 @@ var ( CGROUP_MANAGER = "systemd" STORAGE_OPTIONS = "--storage-driver vfs" ARTIFACT_DIR = "/tmp/.artifacts" - CACHE_IMAGES = []string{ALPINE, BB, fedoraMinimal, nginx, redis, registry, infra} + CACHE_IMAGES = []string{ALPINE, BB, fedoraMinimal, nginx, redis, registry, infra, labels} RESTORE_IMAGES = []string{ALPINE, BB} ALPINE = "docker.io/library/alpine:latest" BB = "docker.io/library/busybox:latest" @@ -41,6 +41,7 @@ var ( redis = "docker.io/library/redis:alpine" registry = "docker.io/library/registry:2" infra = "k8s.gcr.io/pause:3.1" + labels = "quay.io/baude/alpine_labels:latest" defaultWaitTimeout = 90 ) diff --git a/utils/utils.go b/utils/utils.go index 9b7cebfea..c7c5ab5cf 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -29,11 +29,14 @@ func ExecCmd(name string, args ...string) (string, error) { } // ExecCmdWithStdStreams execute a command with the specified standard streams. -func ExecCmdWithStdStreams(stdin io.Reader, stdout, stderr io.Writer, name string, args ...string) error { +func ExecCmdWithStdStreams(stdin io.Reader, stdout, stderr io.Writer, env []string, name string, args ...string) error { cmd := exec.Command(name, args...) cmd.Stdin = stdin cmd.Stdout = stdout cmd.Stderr = stderr + if env != nil { + cmd.Env = env + } err := cmd.Run() if err != nil { |