mirror of
https://github.com/SAP/jenkins-library.git
synced 2025-01-04 04:07:16 +02:00
Provide an ExecRunner implementation for running commands in docker (#1606)
* ExecRunner implementation for executing commands within docker * Add whole-file example as documentation
This commit is contained in:
parent
cf9a41850e
commit
f90a4f9eae
94
pkg/mock/dockerRunner.go
Normal file
94
pkg/mock/dockerRunner.go
Normal file
@ -0,0 +1,94 @@
|
||||
// +build !release
|
||||
|
||||
package mock
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
type baseRunner interface {
|
||||
SetDir(d string)
|
||||
SetEnv(e []string)
|
||||
Stdout(out io.Writer)
|
||||
Stderr(err io.Writer)
|
||||
RunExecutable(e string, p ...string) error
|
||||
}
|
||||
|
||||
// DockerExecConfig is the configuration for an individual tool that shall be executed in docker.
|
||||
type DockerExecConfig struct {
|
||||
// Image is the fully qualified docker image name that is passed to docker for this tool.
|
||||
Image string
|
||||
// Workspace is the (optional) directory to which the current working directory is mapped
|
||||
// within the docker container.
|
||||
Workspace string
|
||||
}
|
||||
|
||||
// DockerExecRunner can be used "in place" of another ExecRunner in order to transparently
|
||||
// execute commands within a docker container. One use-case is to test locally with tools
|
||||
// that are not available on the current platform. When entering the run*() function of a
|
||||
// step implementation, a DockerExecRunner can be wrapped around a command.Command{}
|
||||
// an be configured to run certain executables within docker.
|
||||
type DockerExecRunner struct {
|
||||
// Runner is the ExecRunner to which all executions are forwarded in the end.
|
||||
Runner baseRunner
|
||||
executablesToWrap map[string]DockerExecConfig
|
||||
}
|
||||
|
||||
// SetDir directly forwards to the provided BaseRunner.
|
||||
func (d *DockerExecRunner) SetDir(dir string) {
|
||||
d.Runner.SetDir(dir)
|
||||
}
|
||||
|
||||
// SetEnv directly forwards to the provided BaseRunner.
|
||||
func (d *DockerExecRunner) SetEnv(env []string) {
|
||||
d.Runner.SetEnv(env)
|
||||
}
|
||||
|
||||
// Stdout directly forwards to the provided BaseRunner.
|
||||
func (d *DockerExecRunner) Stdout(out io.Writer) {
|
||||
d.Runner.Stdout(out)
|
||||
}
|
||||
|
||||
// Stderr directly forwards to the provided BaseRunner.
|
||||
func (d *DockerExecRunner) Stderr(err io.Writer) {
|
||||
d.Runner.Stderr(err)
|
||||
}
|
||||
|
||||
// AddExecConfig needs to be called to store a configuration for a specific executable, in order
|
||||
// to run this executable within docker.
|
||||
func (d *DockerExecRunner) AddExecConfig(executable string, config DockerExecConfig) error {
|
||||
if executable == "" {
|
||||
return fmt.Errorf("'executable' needs to be provided")
|
||||
}
|
||||
if config.Image == "" {
|
||||
return fmt.Errorf("the DockerExecConfig must specify a docker image")
|
||||
}
|
||||
if d.executablesToWrap == nil {
|
||||
d.executablesToWrap = map[string]DockerExecConfig{}
|
||||
}
|
||||
d.executablesToWrap[executable] = config
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunExecutable runs the provided executable within docker, if a DockerExecConfig has been previously
|
||||
// associated with this executable via AddExecConfig(). Otherwise runs it directly. The BaseRunner is
|
||||
// used for execution in any case.
|
||||
func (d *DockerExecRunner) RunExecutable(executable string, parameters ...string) error {
|
||||
if config, ok := d.executablesToWrap[executable]; ok {
|
||||
wrappedParameters := []string{"run", "--entrypoint=" + executable}
|
||||
if config.Workspace != "" {
|
||||
currentDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current directory for mounting in docker: %w", err)
|
||||
}
|
||||
wrappedParameters = append(wrappedParameters, "-v", currentDir+":"+config.Workspace)
|
||||
}
|
||||
wrappedParameters = append(wrappedParameters, config.Image)
|
||||
wrappedParameters = append(wrappedParameters, parameters...)
|
||||
executable = "docker"
|
||||
parameters = wrappedParameters
|
||||
}
|
||||
return d.Runner.RunExecutable(executable, parameters...)
|
||||
}
|
75
pkg/mock/dockerRunner_test.go
Normal file
75
pkg/mock/dockerRunner_test.go
Normal file
@ -0,0 +1,75 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDockerExecRunnerAddExecConfig(t *testing.T) {
|
||||
t.Run("no executable provided results in error", func(t *testing.T) {
|
||||
dockerRunner := DockerExecRunner{}
|
||||
err := dockerRunner.AddExecConfig("", DockerExecConfig{})
|
||||
assert.Error(t, err, "'executable' needs to be provided")
|
||||
assert.Nil(t, dockerRunner.executablesToWrap)
|
||||
})
|
||||
t.Run("no image provided results in error", func(t *testing.T) {
|
||||
dockerRunner := DockerExecRunner{}
|
||||
err := dockerRunner.AddExecConfig("useful-tool", DockerExecConfig{})
|
||||
assert.Error(t, err, "the DockerExecConfig must specify a docker image")
|
||||
})
|
||||
t.Run("success case", func(t *testing.T) {
|
||||
dockerRunner := DockerExecRunner{}
|
||||
config := DockerExecConfig{Image: "image", Workspace: "/var/home"}
|
||||
err := dockerRunner.AddExecConfig("useful-tool", config)
|
||||
assert.NoError(t, err)
|
||||
if assert.NotNil(t, dockerRunner.executablesToWrap) {
|
||||
assert.Len(t, dockerRunner.executablesToWrap, 1)
|
||||
assert.Equal(t, config, dockerRunner.executablesToWrap["useful-tool"])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDockerExecRunnerRunExecutable(t *testing.T) {
|
||||
dockerRunner := DockerExecRunner{}
|
||||
_ = dockerRunner.AddExecConfig("useful-tool", DockerExecConfig{
|
||||
Image: "image",
|
||||
Workspace: "some/path",
|
||||
})
|
||||
t.Run("tool execution is wrapped", func(t *testing.T) {
|
||||
mockRunner := ExecMockRunner{}
|
||||
dockerRunner.Runner = &mockRunner
|
||||
currentDir, err := os.Getwd()
|
||||
assert.NoError(t, err)
|
||||
err = dockerRunner.RunExecutable("useful-tool", "param", "--flag")
|
||||
assert.NoError(t, err)
|
||||
if assert.Equal(t, 1, len(mockRunner.Calls)) {
|
||||
assert.Equal(t, ExecCall{
|
||||
Exec: "docker",
|
||||
Params: []string{"run", "--entrypoint=useful-tool", "-v", fmt.Sprintf("%s:some/path", currentDir), "image", "param", "--flag"},
|
||||
}, mockRunner.Calls[0])
|
||||
}
|
||||
})
|
||||
t.Run("tool execution is not wrapped", func(t *testing.T) {
|
||||
mockRunner := ExecMockRunner{}
|
||||
dockerRunner.Runner = &mockRunner
|
||||
err := dockerRunner.RunExecutable("another-tool", "param", "--flag")
|
||||
assert.NoError(t, err)
|
||||
if assert.Equal(t, 1, len(mockRunner.Calls)) {
|
||||
assert.Equal(t, ExecCall{
|
||||
Exec: "another-tool",
|
||||
Params: []string{"param", "--flag"},
|
||||
}, mockRunner.Calls[0])
|
||||
}
|
||||
})
|
||||
t.Run("error case", func(t *testing.T) {
|
||||
mockRunner := ExecMockRunner{}
|
||||
mockRunner.ShouldFailOnCommand = map[string]error{}
|
||||
mockRunner.ShouldFailOnCommand["some-tool"] = errors.New("failed")
|
||||
dockerRunner.Runner = &mockRunner
|
||||
err := dockerRunner.RunExecutable("some-tool")
|
||||
assert.Error(t, err, "failed")
|
||||
})
|
||||
}
|
48
pkg/mock/example_dockerRunner_test.go
Normal file
48
pkg/mock/example_dockerRunner_test.go
Normal file
@ -0,0 +1,48 @@
|
||||
package mock_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/SAP/jenkins-library/pkg/command"
|
||||
"github.com/SAP/jenkins-library/pkg/mock"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ExecRunner interface {
|
||||
SetDir(d string)
|
||||
SetEnv(e []string)
|
||||
Stdout(out io.Writer)
|
||||
Stderr(err io.Writer)
|
||||
RunExecutable(e string, p ...string) error
|
||||
}
|
||||
|
||||
func getMavenVersion(runner ExecRunner) (string, error) {
|
||||
output := bytes.Buffer{}
|
||||
runner.Stdout(&output)
|
||||
err := runner.RunExecutable("mvn", "--version")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to run maven: %w", err)
|
||||
}
|
||||
logLines := strings.Split(output.String(), "\n")
|
||||
if len(logLines) < 1 {
|
||||
return "", fmt.Errorf("failed to obtain maven output")
|
||||
}
|
||||
return logLines[0], nil
|
||||
}
|
||||
|
||||
func ExampleDockerExecRunner_RunExecutable() {
|
||||
// getMavenVersion(runner ExecRunner) executes the command "mvn --version"
|
||||
// and returns the command output as string
|
||||
runner := command.Command{}
|
||||
localMavenVersion, _ := getMavenVersion(&runner)
|
||||
|
||||
dockerRunner := mock.DockerExecRunner{Runner: &runner}
|
||||
_ = dockerRunner.AddExecConfig("mvn", mock.DockerExecConfig{
|
||||
Image: "maven:3.6.1-jdk-8",
|
||||
})
|
||||
|
||||
dockerMavenVersion, _ := getMavenVersion(&dockerRunner)
|
||||
|
||||
fmt.Printf("Your local mvn version is %v, while the version in docker is %v", localMavenVersion, dockerMavenVersion)
|
||||
}
|
Loading…
Reference in New Issue
Block a user