From d1d2e9723dc9c1559e0046fbbe072b1d54542e26 Mon Sep 17 00:00:00 2001 From: qwerty287 <80460567+qwerty287@users.noreply.github.com> Date: Fri, 19 Jan 2024 05:34:02 +0100 Subject: [PATCH] Support custom steps entrypoint (#2985) Closes https://github.com/woodpecker-ci/woodpecker/issues/278 --------- Co-authored-by: Anbraten Co-authored-by: 6543 <6543@obermui.de> --- docs/docs/20-usage/20-workflow-syntax.md | 4 ++++ pipeline/backend/common/script.go | 6 +++--- pipeline/backend/common/script_test.go | 4 ++-- pipeline/backend/docker/convert.go | 5 ++++- pipeline/backend/kubernetes/pod.go | 5 ++++- pipeline/backend/kubernetes/pod_test.go | 1 + pipeline/backend/local/local.go | 1 + pipeline/frontend/metadata/const.go | 2 +- pipeline/frontend/yaml/compiler/convert.go | 1 + pipeline/frontend/yaml/types/container.go | 5 +++-- pipeline/frontend/yaml/types/container_test.go | 8 ++++++++ 11 files changed, 32 insertions(+), 10 deletions(-) diff --git a/docs/docs/20-usage/20-workflow-syntax.md b/docs/docs/20-usage/20-workflow-syntax.md index 6b762fde0..d79ec6a39 100644 --- a/docs/docs/20-usage/20-workflow-syntax.md +++ b/docs/docs/20-usage/20-workflow-syntax.md @@ -156,6 +156,10 @@ docker run --entrypoint=build.sh golang Only build steps can define commands. You cannot use commands with plugins or services. ::: +### `entrypoint` + +Allows you to specify the entrypoint for containers. Note that this must be a list of the command and its arguments (e.g. `["/bin/sh", "-c"]`). + ### `environment` Woodpecker provides the ability to pass environment variables to individual steps. diff --git a/pipeline/backend/common/script.go b/pipeline/backend/common/script.go index 1df8742dd..9da62b92d 100644 --- a/pipeline/backend/common/script.go +++ b/pipeline/backend/common/script.go @@ -18,20 +18,20 @@ import ( "encoding/base64" ) -func GenerateContainerConf(commands []string, goos string) (env map[string]string, entry, cmd []string) { +func GenerateContainerConf(commands []string, goos string) (env map[string]string, entry []string, cmd string) { env = make(map[string]string) if goos == "windows" { env["CI_SCRIPT"] = base64.StdEncoding.EncodeToString([]byte(generateScriptWindows(commands))) env["HOME"] = "c:\\root" env["SHELL"] = "powershell.exe" entry = []string{"powershell", "-noprofile", "-noninteractive", "-command"} - cmd = []string{"[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Env:CI_SCRIPT)) | iex"} + cmd = "[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Env:CI_SCRIPT)) | iex" } else { env["CI_SCRIPT"] = base64.StdEncoding.EncodeToString([]byte(generateScriptPosix(commands))) env["HOME"] = "/root" env["SHELL"] = "/bin/sh" entry = []string{"/bin/sh", "-c"} - cmd = []string{"echo $CI_SCRIPT | base64 -d | /bin/sh -e"} + cmd = "echo $CI_SCRIPT | base64 -d | /bin/sh -e" } return env, entry, cmd diff --git a/pipeline/backend/common/script_test.go b/pipeline/backend/common/script_test.go index 15da65189..3114195a9 100644 --- a/pipeline/backend/common/script_test.go +++ b/pipeline/backend/common/script_test.go @@ -17,11 +17,11 @@ func TestGenerateContainerConf(t *testing.T) { assert.Equal(t, "c:\\root", gotEnv["HOME"]) assert.Equal(t, "powershell.exe", gotEnv["SHELL"]) assert.Equal(t, []string{"powershell", "-noprofile", "-noninteractive", "-command"}, gotEntry) - assert.Equal(t, []string{"[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Env:CI_SCRIPT)) | iex"}, gotCmd) + assert.Equal(t, "[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Env:CI_SCRIPT)) | iex", gotCmd) gotEnv, gotEntry, gotCmd = GenerateContainerConf([]string{"echo hello world"}, "linux") assert.Equal(t, posixScriptBase64, gotEnv["CI_SCRIPT"]) assert.Equal(t, "/root", gotEnv["HOME"]) assert.Equal(t, "/bin/sh", gotEnv["SHELL"]) assert.Equal(t, []string{"/bin/sh", "-c"}, gotEntry) - assert.Equal(t, []string{"echo $CI_SCRIPT | base64 -d | /bin/sh -e"}, gotCmd) + assert.Equal(t, "echo $CI_SCRIPT | base64 -d | /bin/sh -e", gotCmd) } diff --git a/pipeline/backend/docker/convert.go b/pipeline/backend/docker/convert.go index 46ab26ced..86afd070f 100644 --- a/pipeline/backend/docker/convert.go +++ b/pipeline/backend/docker/convert.go @@ -47,8 +47,11 @@ func (e *docker) toConfig(step *types.Step) *container.Config { for k, v := range env { configEnv[k] = v } + if len(step.Entrypoint) > 0 { + entry = step.Entrypoint + } config.Entrypoint = entry - config.Cmd = cmd + config.Cmd = []string{cmd} } if len(configEnv) != 0 { diff --git a/pipeline/backend/kubernetes/pod.go b/pipeline/backend/kubernetes/pod.go index 92f02db4f..49f7ca6e0 100644 --- a/pipeline/backend/kubernetes/pod.go +++ b/pipeline/backend/kubernetes/pod.go @@ -129,8 +129,11 @@ func podContainer(step *types.Step, podName, goos string) (v1.Container, error) if len(step.Commands) != 0 { scriptEnv, command, args := common.GenerateContainerConf(step.Commands, goos) + if len(step.Entrypoint) > 0 { + command = step.Entrypoint + } container.Command = command - container.Args = args + container.Args = []string{args} maps.Copy(step.Environment, scriptEnv) } diff --git a/pipeline/backend/kubernetes/pod_test.go b/pipeline/backend/kubernetes/pod_test.go index 08d1c40b4..68cd89942 100644 --- a/pipeline/backend/kubernetes/pod_test.go +++ b/pipeline/backend/kubernetes/pod_test.go @@ -310,6 +310,7 @@ func TestFullPod(t *testing.T) { Pull: true, Privileged: true, Commands: []string{"go get", "go test"}, + Entrypoint: []string{"/bin/sh", "-c"}, Volumes: []string{"woodpecker-cache:/woodpecker/src/cache"}, Environment: map[string]string{"CGO": "0"}, ExtraHosts: hostAliases, diff --git a/pipeline/backend/local/local.go b/pipeline/backend/local/local.go index a4bda9ecf..aee231c65 100644 --- a/pipeline/backend/local/local.go +++ b/pipeline/backend/local/local.go @@ -146,6 +146,7 @@ func (e *local) StartStep(ctx context.Context, step *types.Step, taskUUID string // execCommands use step.Image as shell and run the commands in it func (e *local) execCommands(ctx context.Context, step *types.Step, state *workflowState, env []string) error { // Prepare commands + // TODO support `entrypoint` from pipeline config args, err := e.genCmdByShell(step.Image, step.Commands) if err != nil { return fmt.Errorf("could not convert commands into args: %w", err) diff --git a/pipeline/frontend/metadata/const.go b/pipeline/frontend/metadata/const.go index 545fe5eca..d7afbea07 100644 --- a/pipeline/frontend/metadata/const.go +++ b/pipeline/frontend/metadata/const.go @@ -14,7 +14,7 @@ package metadata -// Event types corresponding to scm hooks. +// Event types corresponding to forge hooks. const ( EventPush = "push" EventPull = "pull_request" diff --git a/pipeline/frontend/yaml/compiler/convert.go b/pipeline/frontend/yaml/compiler/convert.go index a44a17d3c..c5b3739f4 100644 --- a/pipeline/frontend/yaml/compiler/convert.go +++ b/pipeline/frontend/yaml/compiler/convert.go @@ -185,6 +185,7 @@ func (c *Compiler) createProcess(container *yaml_types.Container, stepType backe WorkingDir: workingdir, Environment: environment, Commands: container.Commands, + Entrypoint: container.Entrypoint, ExtraHosts: extraHosts, Volumes: volumes, Tmpfs: container.Tmpfs, diff --git a/pipeline/frontend/yaml/types/container.go b/pipeline/frontend/yaml/types/container.go index d08a1292c..810cd88c8 100644 --- a/pipeline/frontend/yaml/types/container.go +++ b/pipeline/frontend/yaml/types/container.go @@ -35,6 +35,7 @@ type ( Container struct { BackendOptions BackendOptions `yaml:"backend_options,omitempty"` Commands base.StringOrSlice `yaml:"commands,omitempty"` + Entrypoint base.StringOrSlice `yaml:"entrypoint,omitempty"` Detached bool `yaml:"detach,omitempty"` Directory string `yaml:"directory,omitempty"` Environment base.SliceOrMap `yaml:"environment,omitempty"` @@ -50,7 +51,7 @@ type ( Ports []string `yaml:"ports,omitempty"` DependsOn base.StringOrSlice `yaml:"depends_on,omitempty"` - // Docker Specific + // Docker and Kubernetes Specific Privileged bool `yaml:"privileged,omitempty"` // Undocumented @@ -119,7 +120,7 @@ func (c *ContainerList) UnmarshalYAML(value *yaml.Node) error { } func (c *Container) IsPlugin() bool { - return len(c.Commands) == 0 + return len(c.Commands) == 0 && len(c.Entrypoint) == 0 } func (c *Container) IsTrustedCloneImage() bool { diff --git a/pipeline/frontend/yaml/types/container_test.go b/pipeline/frontend/yaml/types/container_test.go index f81d510e4..74a38ac3b 100644 --- a/pipeline/frontend/yaml/types/container_test.go +++ b/pipeline/frontend/yaml/types/container_test.go @@ -39,6 +39,7 @@ devices: directory: example/ dns: 8.8.8.8 dns_search: example.com +entrypoint: [/bin/sh, -c] environment: - RACK_ENV=development - SHOW=true @@ -86,6 +87,7 @@ func TestUnmarshalContainer(t *testing.T) { Directory: "example/", DNS: base.StringOrSlice{"8.8.8.8"}, DNSSearch: base.StringOrSlice{"example.com"}, + Entrypoint: []string{"/bin/sh", "-c"}, Environment: base.SliceOrMap{"RACK_ENV": "development", "SHOW": "true"}, ExtraHosts: []string{"somehost:162.242.195.82", "otherhost:50.31.209.229", "ipv6:2001:db8::10"}, Image: "golang:latest", @@ -308,4 +310,10 @@ func TestIsPlugin(t *testing.T) { assert.False(t, (&Container{ Commands: base.StringOrSlice(strslice.StrSlice{"echo 'this is not a plugin'"}), }).IsPlugin()) + assert.True(t, (&Container{ + Entrypoint: base.StringOrSlice(strslice.StrSlice{}), + }).IsPlugin()) + assert.False(t, (&Container{ + Entrypoint: base.StringOrSlice(strslice.StrSlice{"echo 'this is not a plugin'"}), + }).IsPlugin()) }