Make sure plugins only mount the workspace base in a predefinde location (#3933)

This commit is contained in:
6543 2024-07-18 22:52:22 +02:00 committed by GitHub
parent d6e3ebf051
commit 764329ed1d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 75 additions and 24 deletions

View file

@ -523,7 +523,9 @@ For more details check the [services docs](./60-services.md).
## `workspace` ## `workspace`
The workspace defines the shared volume and working directory shared by all workflow steps. The default workspace matches the pattern `/woodpecker/src/github.com/octocat/hello-world`, based on your repository URL. The workspace defines the shared volume and working directory shared by all workflow steps.
The default workspace base is `/woodpecker` and the path is extended with the repository URL (`src/{url-without-schema}`).
So an example would be `/woodpecker/src/github.com/octocat/hello-world`.
The workspace can be customized using the workspace block in the YAML file: The workspace can be customized using the workspace block in the YAML file:
@ -540,6 +542,10 @@ The workspace can be customized using the workspace block in the YAML file:
- go test - go test
``` ```
:::note
Plugins will always have the workspace base at `/woodpecker`
:::
The base attribute defines a shared base volume available to all steps. This ensures your source code, dependencies and compiled binaries are persisted and shared between steps. The base attribute defines a shared base volume available to all steps. This ensures your source code, dependencies and compiled binaries are persisted and shared between steps.
```diff ```diff

View file

@ -47,6 +47,11 @@ steps:
## Plugin Isolation ## Plugin Isolation
Plugins are just pipeline steps. They share the build workspace, mounted as a volume, and therefore have access to your source tree. Plugins are just pipeline steps. They share the build workspace, mounted as a volume, and therefore have access to your source tree.
While normal steps are all about arbitrary code execution, plugins should only allow the functions intended by the plugin author.
So there are a few limitations, like the workspace base is always mounted at `/woodpecker`, but the working directory is dynamically adjusted acordingly. So as user of a plugin you should not have to care about this.
Also instead of using environment variables the plugin should only care about one prefixed with `PLUGIN_` witch are the internaml representation of the **settings** ([read more](./20-creating-plugins.md)).
## Finding Plugins ## Finding Plugins

View file

@ -523,7 +523,9 @@ For more details check the [services docs](./60-services.md).
## `workspace` ## `workspace`
The workspace defines the shared volume and working directory shared by all workflow steps. The default workspace matches the pattern `/woodpecker/src/github.com/octocat/hello-world`, based on your repository URL. The workspace defines the shared volume and working directory shared by all workflow steps.
The default workspace base is `/woodpecker` and the path is extended with the repository URL (`src/{url-without-schema}`).
So an example would be `/woodpecker/src/github.com/octocat/hello-world`.
The workspace can be customized using the workspace block in the YAML file: The workspace can be customized using the workspace block in the YAML file:
@ -540,6 +542,10 @@ The workspace can be customized using the workspace block in the YAML file:
- go test - go test
``` ```
:::note
Plugins will always have the workspace base at `/woodpecker`
:::
The base attribute defines a shared base volume available to all steps. This ensures your source code, dependencies and compiled binaries are persisted and shared between steps. The base attribute defines a shared base volume available to all steps. This ensures your source code, dependencies and compiled binaries are persisted and shared between steps.
```diff ```diff

View file

@ -47,6 +47,11 @@ steps:
## Plugin Isolation ## Plugin Isolation
Plugins are just pipeline steps. They share the build workspace, mounted as a volume, and therefore have access to your source tree. Plugins are just pipeline steps. They share the build workspace, mounted as a volume, and therefore have access to your source tree.
While normal steps are all about arbitrary code execution, plugins should only allow the functions intended by the plugin author.
So there are a few limitations, like the workspace base is always mounted at `/woodpecker`, but the working directory is dynamically adjusted acordingly. So as user of a plugin you should not have to care about this.
Also instead of using environment variables the plugin should only care about one prefixed with `PLUGIN_` witch are the internaml representation of the **settings** ([read more](./20-creating-plugins.md)).
## Finding Plugins ## Finding Plugins

View file

@ -16,6 +16,7 @@ package compiler
import ( import (
"fmt" "fmt"
"path"
backend_types "go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/types" backend_types "go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/types"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/metadata" "go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/metadata"
@ -98,8 +99,8 @@ type Compiler struct {
networks []string networks []string
env map[string]string env map[string]string
cloneEnv map[string]string cloneEnv map[string]string
base string workspaceBase string
path string workspacePath string
metadata metadata.Metadata metadata metadata.Metadata
registries []Registry registries []Registry
secrets map[string]Secret secrets map[string]Secret
@ -156,10 +157,10 @@ func (c *Compiler) Compile(conf *yaml_types.Workflow) (*backend_types.Config, er
// overrides the default workspace paths when specified // overrides the default workspace paths when specified
// in the YAML file. // in the YAML file.
if len(conf.Workspace.Base) != 0 { if len(conf.Workspace.Base) != 0 {
c.base = conf.Workspace.Base c.workspaceBase = path.Clean(conf.Workspace.Base)
} }
if len(conf.Workspace.Path) != 0 { if len(conf.Workspace.Path) != 0 {
c.path = conf.Workspace.Path c.workspacePath = path.Clean(conf.Workspace.Path)
} }
cloneImage := constant.DefaultCloneImage cloneImage := constant.DefaultCloneImage

View file

@ -61,13 +61,14 @@ func TestSecretAvailable(t *testing.T) {
} }
func TestCompilerCompile(t *testing.T) { func TestCompilerCompile(t *testing.T) {
repoURL := "https://github.com/octocat/hello-world"
compiler := New( compiler := New(
WithMetadata(metadata.Metadata{ WithMetadata(metadata.Metadata{
Repo: metadata.Repo{ Repo: metadata.Repo{
Owner: "octacat", Owner: "octacat",
Name: "hello-world", Name: "hello-world",
Private: true, Private: true,
ForgeURL: "https://github.com/octocat/hello-world", ForgeURL: repoURL,
CloneURL: "https://github.com/octocat/hello-world.git", CloneURL: "https://github.com/octocat/hello-world.git",
}, },
}), }),
@ -76,6 +77,8 @@ func TestCompilerCompile(t *testing.T) {
"COLORED": "true", "COLORED": "true",
}), }),
WithPrefix("test"), WithPrefix("test"),
// we use "/test" as custom workspace base to ensure the enforcement of the pluginWorkspaceBase is applied
WithWorkspaceFromURL("/test", repoURL),
) )
defaultNetworks := []*backend_types.Network{{ defaultNetworks := []*backend_types.Network{{
@ -92,7 +95,8 @@ func TestCompilerCompile(t *testing.T) {
Image: constant.DefaultCloneImage, Image: constant.DefaultCloneImage,
OnSuccess: true, OnSuccess: true,
Failure: "fail", Failure: "fail",
Volumes: []string{defaultVolumes[0].Name + ":"}, Volumes: []string{defaultVolumes[0].Name + ":/woodpecker"},
WorkingDir: "/woodpecker/src/github.com/octocat/hello-world",
Networks: []backend_types.Conn{{Name: "test_default", Aliases: []string{"clone"}}}, Networks: []backend_types.Conn{{Name: "test_default", Aliases: []string{"clone"}}},
ExtraHosts: []backend_types.HostAlias{}, ExtraHosts: []backend_types.HostAlias{},
}}, }},
@ -137,7 +141,8 @@ func TestCompilerCompile(t *testing.T) {
Image: "dummy_img", Image: "dummy_img",
OnSuccess: true, OnSuccess: true,
Failure: "fail", Failure: "fail",
Volumes: []string{defaultVolumes[0].Name + ":"}, Volumes: []string{defaultVolumes[0].Name + ":/woodpecker"},
WorkingDir: "/woodpecker/src/github.com/octocat/hello-world",
Networks: []backend_types.Conn{{Name: "test_default", Aliases: []string{"dummy"}}}, Networks: []backend_types.Conn{{Name: "test_default", Aliases: []string{"dummy"}}},
ExtraHosts: []backend_types.HostAlias{}, ExtraHosts: []backend_types.HostAlias{},
}}, }},
@ -172,7 +177,8 @@ func TestCompilerCompile(t *testing.T) {
Commands: []string{"env"}, Commands: []string{"env"},
OnSuccess: true, OnSuccess: true,
Failure: "fail", Failure: "fail",
Volumes: []string{defaultVolumes[0].Name + ":"}, Volumes: []string{defaultVolumes[0].Name + ":/test"},
WorkingDir: "/test/src/github.com/octocat/hello-world",
Networks: []backend_types.Conn{{Name: "test_default", Aliases: []string{"echo env"}}}, Networks: []backend_types.Conn{{Name: "test_default", Aliases: []string{"echo env"}}},
ExtraHosts: []backend_types.HostAlias{}, ExtraHosts: []backend_types.HostAlias{},
}}, }},
@ -184,7 +190,8 @@ func TestCompilerCompile(t *testing.T) {
Commands: []string{"echo 1"}, Commands: []string{"echo 1"},
OnSuccess: true, OnSuccess: true,
Failure: "fail", Failure: "fail",
Volumes: []string{defaultVolumes[0].Name + ":"}, Volumes: []string{defaultVolumes[0].Name + ":/test"},
WorkingDir: "/test/src/github.com/octocat/hello-world",
Networks: []backend_types.Conn{{Name: "test_default", Aliases: []string{"parallel echo 1"}}}, Networks: []backend_types.Conn{{Name: "test_default", Aliases: []string{"parallel echo 1"}}},
ExtraHosts: []backend_types.HostAlias{}, ExtraHosts: []backend_types.HostAlias{},
}, { }, {
@ -194,7 +201,8 @@ func TestCompilerCompile(t *testing.T) {
Commands: []string{"echo 2"}, Commands: []string{"echo 2"},
OnSuccess: true, OnSuccess: true,
Failure: "fail", Failure: "fail",
Volumes: []string{defaultVolumes[0].Name + ":"}, Volumes: []string{defaultVolumes[0].Name + ":/test"},
WorkingDir: "/test/src/github.com/octocat/hello-world",
Networks: []backend_types.Conn{{Name: "test_default", Aliases: []string{"parallel echo 2"}}}, Networks: []backend_types.Conn{{Name: "test_default", Aliases: []string{"parallel echo 2"}}},
ExtraHosts: []backend_types.HostAlias{}, ExtraHosts: []backend_types.HostAlias{},
}}, }},
@ -228,7 +236,8 @@ func TestCompilerCompile(t *testing.T) {
Commands: []string{"env"}, Commands: []string{"env"},
OnSuccess: true, OnSuccess: true,
Failure: "fail", Failure: "fail",
Volumes: []string{defaultVolumes[0].Name + ":"}, Volumes: []string{defaultVolumes[0].Name + ":/test"},
WorkingDir: "/test/src/github.com/octocat/hello-world",
Networks: []backend_types.Conn{{Name: "test_default", Aliases: []string{"echo env"}}}, Networks: []backend_types.Conn{{Name: "test_default", Aliases: []string{"echo env"}}},
ExtraHosts: []backend_types.HostAlias{}, ExtraHosts: []backend_types.HostAlias{},
}, { }, {
@ -238,7 +247,8 @@ func TestCompilerCompile(t *testing.T) {
Commands: []string{"echo 2"}, Commands: []string{"echo 2"},
OnSuccess: true, OnSuccess: true,
Failure: "fail", Failure: "fail",
Volumes: []string{defaultVolumes[0].Name + ":"}, Volumes: []string{defaultVolumes[0].Name + ":/test"},
WorkingDir: "/test/src/github.com/octocat/hello-world",
Networks: []backend_types.Conn{{Name: "test_default", Aliases: []string{"echo 2"}}}, Networks: []backend_types.Conn{{Name: "test_default", Aliases: []string{"echo 2"}}},
ExtraHosts: []backend_types.HostAlias{}, ExtraHosts: []backend_types.HostAlias{},
}}, }},
@ -250,7 +260,8 @@ func TestCompilerCompile(t *testing.T) {
Commands: []string{"echo 1"}, Commands: []string{"echo 1"},
OnSuccess: true, OnSuccess: true,
Failure: "fail", Failure: "fail",
Volumes: []string{defaultVolumes[0].Name + ":"}, Volumes: []string{defaultVolumes[0].Name + ":/test"},
WorkingDir: "/test/src/github.com/octocat/hello-world",
Networks: []backend_types.Conn{{Name: "test_default", Aliases: []string{"echo 1"}}}, Networks: []backend_types.Conn{{Name: "test_default", Aliases: []string{"echo 1"}}},
ExtraHosts: []backend_types.HostAlias{}, ExtraHosts: []backend_types.HostAlias{},
}}, }},

View file

@ -30,6 +30,13 @@ import (
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/utils" "go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/utils"
) )
const (
// The pluginWorkspaceBase should not be changed, only if you are sure what you do.
pluginWorkspaceBase = "/woodpecker"
// DefaultWorkspaceBase is set if not altered by the user.
DefaultWorkspaceBase = pluginWorkspaceBase
)
func (c *Compiler) createProcess(container *yaml_types.Container, stepType backend_types.StepType) (*backend_types.Step, error) { func (c *Compiler) createProcess(container *yaml_types.Container, stepType backend_types.StepType) (*backend_types.Step, error) {
var ( var (
uuid = ulid.Make() uuid = ulid.Make()
@ -37,11 +44,17 @@ func (c *Compiler) createProcess(container *yaml_types.Container, stepType backe
detached bool detached bool
workingDir string workingDir string
workspace = fmt.Sprintf("%s_default:%s", c.prefix, c.base)
privileged = container.Privileged privileged = container.Privileged
networkMode = container.NetworkMode networkMode = container.NetworkMode
) )
workspaceBase := c.workspaceBase
if container.IsPlugin() {
// plugins have a predefined workspace base to not tamper with entrypoint executables
workspaceBase = pluginWorkspaceBase
}
workspaceVolume := fmt.Sprintf("%s_default:%s", c.prefix, workspaceBase)
networks := []backend_types.Conn{ networks := []backend_types.Conn{
{ {
Name: fmt.Sprintf("%s_default", c.prefix), Name: fmt.Sprintf("%s_default", c.prefix),
@ -66,7 +79,7 @@ func (c *Compiler) createProcess(container *yaml_types.Container, stepType backe
var volumes []string var volumes []string
if !c.local { if !c.local {
volumes = append(volumes, workspace) volumes = append(volumes, workspaceVolume)
} }
volumes = append(volumes, c.volumes...) volumes = append(volumes, c.volumes...)
for _, volume := range container.Volumes.Volumes { for _, volume := range container.Volumes.Volumes {
@ -77,7 +90,7 @@ func (c *Compiler) createProcess(container *yaml_types.Container, stepType backe
environment := map[string]string{} environment := map[string]string{}
maps.Copy(environment, c.env) maps.Copy(environment, c.env)
environment["CI_WORKSPACE"] = path.Join(c.base, c.path) environment["CI_WORKSPACE"] = path.Join(workspaceBase, c.workspacePath)
if stepType == backend_types.StepTypeService || container.Detached { if stepType == backend_types.StepTypeService || container.Detached {
detached = true detached = true
@ -219,7 +232,11 @@ func (c *Compiler) stepWorkingDir(container *yaml_types.Container) string {
if path.IsAbs(container.Directory) { if path.IsAbs(container.Directory) {
return container.Directory return container.Directory
} }
return path.Join(c.base, c.path, container.Directory) base := c.workspaceBase
if container.IsPlugin() {
base = pluginWorkspaceBase
}
return path.Join(base, c.workspacePath, container.Directory)
} }
func convertPort(portDef string) (backend_types.Port, error) { func convertPort(portDef string) (backend_types.Port, error) {

View file

@ -97,8 +97,8 @@ func WithNetrc(username, password, machine string) Option {
// plugin steps in the pipeline. // plugin steps in the pipeline.
func WithWorkspace(base, path string) Option { func WithWorkspace(base, path string) Option {
return func(compiler *Compiler) { return func(compiler *Compiler) {
compiler.base = base compiler.workspaceBase = base
compiler.path = path compiler.workspacePath = path
} }
} }

View file

@ -29,8 +29,8 @@ func TestWithWorkspace(t *testing.T) {
"src/github.com/octocat/hello-world", "src/github.com/octocat/hello-world",
), ),
) )
assert.Equal(t, "/pipeline", compiler.base) assert.Equal(t, "/pipeline", compiler.workspaceBase)
assert.Equal(t, "src/github.com/octocat/hello-world", compiler.path) assert.Equal(t, "src/github.com/octocat/hello-world", compiler.workspacePath)
} }
func TestWithEscalated(t *testing.T) { func TestWithEscalated(t *testing.T) {

View file

@ -291,7 +291,7 @@ func (b *StepBuilder) toInternalRepresentation(parsed *yaml_types.Workflow, envi
), ),
), ),
compiler.WithProxy(b.ProxyOpts), compiler.WithProxy(b.ProxyOpts),
compiler.WithWorkspaceFromURL("/woodpecker", b.Repo.ForgeURL), compiler.WithWorkspaceFromURL(compiler.DefaultWorkspaceBase, b.Repo.ForgeURL),
compiler.WithMetadata(metadata), compiler.WithMetadata(metadata),
compiler.WithTrusted(b.Repo.IsTrusted), compiler.WithTrusted(b.Repo.IsTrusted),
compiler.WithNetrcOnlyTrusted(b.Repo.NetrcOnlyTrusted), compiler.WithNetrcOnlyTrusted(b.Repo.NetrcOnlyTrusted),