mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-11-25 11:21:02 +00:00
Allow alter trusted clone plugins and filter them via tag (#4074)
This commit is contained in:
parent
8e0af15e85
commit
3c8204a0e0
17 changed files with 151 additions and 58 deletions
|
@ -41,6 +41,7 @@ import (
|
|||
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/linter"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/matrix"
|
||||
pipelineLog "go.woodpecker-ci.org/woodpecker/v2/pipeline/log"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/shared/constant"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/shared/utils"
|
||||
)
|
||||
|
||||
|
@ -185,7 +186,10 @@ func execWithAxis(ctx context.Context, c *cli.Command, file, repoPath string, ax
|
|||
}
|
||||
|
||||
// lint the yaml file
|
||||
err = linter.New(linter.WithTrusted(true)).Lint([]*linter.WorkflowConfig{{
|
||||
err = linter.New(
|
||||
linter.WithTrusted(true),
|
||||
linter.WithTrustedClonePlugins(constant.TrustedClonePlugins),
|
||||
).Lint([]*linter.WorkflowConfig{{
|
||||
File: path.Base(file),
|
||||
RawConfig: confStr,
|
||||
Workflow: conf,
|
||||
|
|
|
@ -27,6 +27,7 @@ import (
|
|||
"go.woodpecker-ci.org/woodpecker/v2/cli/common"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/linter"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/shared/constant"
|
||||
)
|
||||
|
||||
// Command exports the info command.
|
||||
|
@ -35,6 +36,14 @@ var Command = &cli.Command{
|
|||
Usage: "lint a pipeline configuration file",
|
||||
ArgsUsage: "[path/to/.woodpecker.yaml]",
|
||||
Action: lint,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringSliceFlag{
|
||||
Sources: cli.EnvVars("WOODPECKER_PLUGINS_TRUSTED_CLONE"),
|
||||
Name: "plugins-trusted-clone",
|
||||
Usage: "Plugins witch are trusted to handle the netrc info in clone steps",
|
||||
Value: constant.TrustedClonePlugins,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func lint(ctx context.Context, c *cli.Command) error {
|
||||
|
@ -69,7 +78,7 @@ func lintDir(ctx context.Context, c *cli.Command, dir string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func lintFile(_ context.Context, _ *cli.Command, file string) error {
|
||||
func lintFile(_ context.Context, c *cli.Command, file string) error {
|
||||
fi, err := os.Open(file)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -83,7 +92,7 @@ func lintFile(_ context.Context, _ *cli.Command, file string) error {
|
|||
|
||||
rawConfig := string(buf)
|
||||
|
||||
c, err := yaml.ParseString(rawConfig)
|
||||
parsedConfig, err := yaml.ParseString(rawConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -91,11 +100,14 @@ func lintFile(_ context.Context, _ *cli.Command, file string) error {
|
|||
config := &linter.WorkflowConfig{
|
||||
File: path.Base(file),
|
||||
RawConfig: rawConfig,
|
||||
Workflow: c,
|
||||
Workflow: parsedConfig,
|
||||
}
|
||||
|
||||
// TODO: lint multiple files at once to allow checks for sth like "depends_on" to work
|
||||
err = linter.New(linter.WithTrusted(true)).Lint([]*linter.WorkflowConfig{config})
|
||||
err = linter.New(
|
||||
linter.WithTrusted(true),
|
||||
linter.WithTrustedClonePlugins(c.StringSlice("plugins-trusted-clone")),
|
||||
).Lint([]*linter.WorkflowConfig{config})
|
||||
if err != nil {
|
||||
str, err := FormatLintError(config.File, err)
|
||||
|
||||
|
|
|
@ -135,10 +135,11 @@ var flags = append([]cli.Flag{
|
|||
Value: []string{"push", "pull_request"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Sources: cli.EnvVars("WOODPECKER_DEFAULT_CLONE_IMAGE"),
|
||||
Name: "default-clone-image",
|
||||
Sources: cli.EnvVars("WOODPECKER_DEFAULT_CLONE_PLUGIN", "WOODPECKER_DEFAULT_CLONE_IMAGE"),
|
||||
Name: "default-clone-plugin",
|
||||
Aliases: []string{"default-clone-image"},
|
||||
Usage: "The default docker image to be used when cloning the repo",
|
||||
Value: constant.DefaultCloneImage,
|
||||
Value: constant.DefaultClonePlugin,
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Sources: cli.EnvVars("WOODPECKER_DEFAULT_PIPELINE_TIMEOUT"),
|
||||
|
@ -164,6 +165,12 @@ var flags = append([]cli.Flag{
|
|||
Usage: "Allow plugins to run in privileged mode, if environment variable is defined but empty there will be none",
|
||||
Value: constant.PrivilegedPlugins,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Sources: cli.EnvVars("WOODPECKER_PLUGINS_TRUSTED_CLONE"),
|
||||
Name: "plugins-trusted-clone",
|
||||
Usage: "Plugins witch are trusted to handle the netrc info in clone steps",
|
||||
Value: constant.TrustedClonePlugins,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Sources: cli.EnvVars("WOODPECKER_VOLUME"),
|
||||
Name: "volume",
|
||||
|
|
|
@ -43,7 +43,6 @@ import (
|
|||
"go.woodpecker-ci.org/woodpecker/v2/server/store"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/store/datastore"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/server/store/types"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/shared/constant"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -165,8 +164,9 @@ func setupEvilGlobals(ctx context.Context, c *cli.Command, s store.Store) error
|
|||
server.Config.Pipeline.AuthenticatePublicRepos = c.Bool("authenticate-public-repos")
|
||||
|
||||
// Cloning
|
||||
server.Config.Pipeline.DefaultCloneImage = c.String("default-clone-image")
|
||||
constant.TrustedCloneImages = append(constant.TrustedCloneImages, server.Config.Pipeline.DefaultCloneImage)
|
||||
server.Config.Pipeline.DefaultClonePlugin = c.String("default-clone-plugin")
|
||||
server.Config.Pipeline.TrustedClonePlugins = c.StringSlice("plugins-trusted-clone")
|
||||
server.Config.Pipeline.TrustedClonePlugins = append(server.Config.Pipeline.TrustedClonePlugins, server.Config.Pipeline.DefaultClonePlugin)
|
||||
|
||||
// Execution
|
||||
_events := c.StringSlice("default-cancel-previous-pipeline-events")
|
||||
|
|
|
@ -319,11 +319,13 @@ Always use authentication to clone repositories even if they are public. Needed
|
|||
|
||||
List of event names that will be canceled when a new pipeline for the same context (tag, branch) is created.
|
||||
|
||||
### `WOODPECKER_DEFAULT_CLONE_IMAGE`
|
||||
### `WOODPECKER_DEFAULT_CLONE_PLUGIN`
|
||||
|
||||
> Default is defined in [shared/constant/constant.go](https://github.com/woodpecker-ci/woodpecker/blob/main/shared/constant/constant.go)
|
||||
|
||||
The default docker image to be used when cloning the repo
|
||||
The default docker image to be used when cloning the repo.
|
||||
|
||||
It is also added to the trusted clone plugin list.
|
||||
|
||||
### `WOODPECKER_DEFAULT_PIPELINE_TIMEOUT`
|
||||
|
||||
|
@ -352,6 +354,15 @@ a user can log into Woodpecker, without re-authentication.
|
|||
|
||||
Docker images to run in privileged mode. Only change if you are sure what you do!
|
||||
|
||||
### WOODPECKER_PLUGINS_TRUSTED_CLONE
|
||||
|
||||
> Defaults are defined in [shared/constant/constant.go](https://github.com/woodpecker-ci/woodpecker/blob/main/shared/constant/constant.go)
|
||||
|
||||
Plugins witch are trusted to handle the netrc info in clone steps.
|
||||
If a clone step use an image not in this list, the netrc will not be injected and an user has to use other methods (e.g. secrets) to clone non public repos.
|
||||
|
||||
You should specify the tag of your images too, as this enforces exact matches.
|
||||
|
||||
<!--
|
||||
### `WOODPECKER_VOLUME`
|
||||
> Default: empty
|
||||
|
|
|
@ -4,6 +4,8 @@ Some versions need some changes to the server configuration or the pipeline conf
|
|||
|
||||
## `next`
|
||||
|
||||
- `WOODPECKER_DEFAULT_CLONE_IMAGE` got depricated use `WOODPECKER_DEFAULT_CLONE_PLUGIN`
|
||||
- Check trusted-clone-plugins by image name and tag (if tag is set)
|
||||
- Remove `plugins/docker`, `plugins/gcr` and `plugins/ecr` from the default list of privileged plugins ([modify the list via config if needed](./30-administration/10-server-config.md#woodpecker_escalate)).
|
||||
- Secret filters for plugins now check against tag if specified
|
||||
- Removed `WOODPECKER_DEV_OAUTH_HOST` and `WOODPECKER_DEV_GITEA_OAUTH_URL` use `WOODPECKER_EXPERT_FORGE_OAUTH_HOST`
|
||||
|
|
|
@ -105,7 +105,8 @@ type Compiler struct {
|
|||
registries []Registry
|
||||
secrets map[string]Secret
|
||||
reslimit ResourceLimit
|
||||
defaultCloneImage string
|
||||
defaultClonePlugin string
|
||||
trustedClonePlugins []string
|
||||
trustedPipeline bool
|
||||
netrcOnlyTrusted bool
|
||||
}
|
||||
|
@ -116,6 +117,8 @@ func New(opts ...Option) *Compiler {
|
|||
env: map[string]string{},
|
||||
cloneEnv: map[string]string{},
|
||||
secrets: map[string]Secret{},
|
||||
defaultClonePlugin: constant.DefaultClonePlugin,
|
||||
trustedClonePlugins: constant.TrustedClonePlugins,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(compiler)
|
||||
|
@ -163,20 +166,15 @@ func (c *Compiler) Compile(conf *yaml_types.Workflow) (*backend_types.Config, er
|
|||
c.workspacePath = path.Clean(conf.Workspace.Path)
|
||||
}
|
||||
|
||||
cloneImage := constant.DefaultCloneImage
|
||||
if len(c.defaultCloneImage) > 0 {
|
||||
cloneImage = c.defaultCloneImage
|
||||
}
|
||||
|
||||
// add default clone step
|
||||
if !c.local && len(conf.Clone.ContainerList) == 0 && !conf.SkipClone {
|
||||
if !c.local && len(conf.Clone.ContainerList) == 0 && !conf.SkipClone && len(c.defaultClonePlugin) != 0 {
|
||||
cloneSettings := map[string]any{"depth": "0"}
|
||||
if c.metadata.Curr.Event == metadata.EventTag {
|
||||
cloneSettings["tags"] = "true"
|
||||
}
|
||||
container := &yaml_types.Container{
|
||||
Name: defaultCloneName,
|
||||
Image: cloneImage,
|
||||
Image: c.defaultClonePlugin,
|
||||
Settings: cloneSettings,
|
||||
Environment: make(map[string]any),
|
||||
}
|
||||
|
@ -208,7 +206,7 @@ func (c *Compiler) Compile(conf *yaml_types.Workflow) (*backend_types.Config, er
|
|||
}
|
||||
|
||||
// only inject netrc if it's a trusted repo or a trusted plugin
|
||||
if !c.netrcOnlyTrusted || c.trustedPipeline || (container.IsPlugin() && container.IsTrustedCloneImage()) {
|
||||
if !c.netrcOnlyTrusted || c.trustedPipeline || (container.IsPlugin() && container.IsTrustedCloneImage(c.trustedClonePlugins)) {
|
||||
for k, v := range c.cloneEnv {
|
||||
step.Environment[k] = v
|
||||
}
|
||||
|
@ -265,7 +263,7 @@ func (c *Compiler) Compile(conf *yaml_types.Workflow) (*backend_types.Config, er
|
|||
}
|
||||
|
||||
// inject netrc if it's a trusted repo or a trusted clone-plugin
|
||||
if c.trustedPipeline || (container.IsPlugin() && container.IsTrustedCloneImage()) {
|
||||
if c.trustedPipeline || (container.IsPlugin() && container.IsTrustedCloneImage(c.trustedClonePlugins)) {
|
||||
for k, v := range c.cloneEnv {
|
||||
step.Environment[k] = v
|
||||
}
|
||||
|
|
|
@ -92,7 +92,7 @@ func TestCompilerCompile(t *testing.T) {
|
|||
Steps: []*backend_types.Step{{
|
||||
Name: "clone",
|
||||
Type: backend_types.StepTypeClone,
|
||||
Image: constant.DefaultCloneImage,
|
||||
Image: constant.DefaultClonePlugin,
|
||||
OnSuccess: true,
|
||||
Failure: "fail",
|
||||
Volumes: []string{defaultVolumes[0].Name + ":/woodpecker"},
|
||||
|
|
|
@ -172,9 +172,15 @@ func WithResourceLimit(swap, mem, shmSize, cpuQuota, cpuShares int64, cpuSet str
|
|||
}
|
||||
}
|
||||
|
||||
func WithDefaultCloneImage(cloneImage string) Option {
|
||||
func WithDefaultClonePlugin(cloneImage string) Option {
|
||||
return func(compiler *Compiler) {
|
||||
compiler.defaultCloneImage = cloneImage
|
||||
compiler.defaultClonePlugin = cloneImage
|
||||
}
|
||||
}
|
||||
|
||||
func WithTrustedClonePlugins(images []string) Option {
|
||||
return func(compiler *Compiler) {
|
||||
compiler.trustedClonePlugins = images
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/metadata"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/shared/constant"
|
||||
)
|
||||
|
||||
func TestWithWorkspace(t *testing.T) {
|
||||
|
@ -166,9 +167,17 @@ func TestWithEnviron(t *testing.T) {
|
|||
assert.Equal(t, "true", compiler.env["SHOW"])
|
||||
}
|
||||
|
||||
func TestWithDefaultCloneImage(t *testing.T) {
|
||||
func TestDefaultClonePlugin(t *testing.T) {
|
||||
compiler := New(
|
||||
WithDefaultCloneImage("not-an-image"),
|
||||
WithDefaultClonePlugin("not-an-image"),
|
||||
)
|
||||
assert.Equal(t, "not-an-image", compiler.defaultCloneImage)
|
||||
assert.Equal(t, "not-an-image", compiler.defaultClonePlugin)
|
||||
}
|
||||
|
||||
func TestWithTrustedClonePlugins(t *testing.T) {
|
||||
compiler := New(WithTrustedClonePlugins([]string{"not-an-image"}))
|
||||
assert.ElementsMatch(t, []string{"not-an-image"}, compiler.trustedClonePlugins)
|
||||
|
||||
compiler = New()
|
||||
assert.ElementsMatch(t, constant.TrustedClonePlugins, compiler.trustedClonePlugins)
|
||||
}
|
||||
|
|
|
@ -25,12 +25,14 @@ import (
|
|||
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/linter/schema"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/types"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/utils"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/shared/constant"
|
||||
)
|
||||
|
||||
// A Linter lints a pipeline configuration.
|
||||
type Linter struct {
|
||||
trusted bool
|
||||
privilegedPlugins *[]string
|
||||
trustedClonePlugins *[]string
|
||||
}
|
||||
|
||||
// New creates a new Linter with options.
|
||||
|
@ -73,6 +75,10 @@ func (l *Linter) lintFile(config *WorkflowConfig) error {
|
|||
linterErr = multierr.Append(linterErr, newLinterError("Invalid or missing steps section", config.File, "steps", false))
|
||||
}
|
||||
|
||||
if err := l.lintCloneSteps(config); err != nil {
|
||||
linterErr = multierr.Append(linterErr, err)
|
||||
}
|
||||
|
||||
if err := l.lintContainers(config, "clone"); err != nil {
|
||||
linterErr = multierr.Append(linterErr, err)
|
||||
}
|
||||
|
@ -96,6 +102,29 @@ func (l *Linter) lintFile(config *WorkflowConfig) error {
|
|||
return linterErr
|
||||
}
|
||||
|
||||
func (l *Linter) lintCloneSteps(config *WorkflowConfig) error {
|
||||
if len(config.Workflow.Clone.ContainerList) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
trustedClonePlugins := constant.TrustedClonePlugins
|
||||
if l.trustedClonePlugins != nil {
|
||||
trustedClonePlugins = *l.trustedClonePlugins
|
||||
}
|
||||
|
||||
var linterErr error
|
||||
for _, container := range config.Workflow.Clone.ContainerList {
|
||||
if !utils.MatchImageDynamic(container.Image, trustedClonePlugins...) {
|
||||
linterErr = multierr.Append(linterErr,
|
||||
newLinterError(
|
||||
"Specified clone image does not match allow list, netrc will not be injected",
|
||||
config.File, fmt.Sprintf("clone.%s", container.Name), true),
|
||||
)
|
||||
}
|
||||
}
|
||||
return linterErr
|
||||
}
|
||||
|
||||
func (l *Linter) lintContainers(config *WorkflowConfig, area string) error {
|
||||
var linterErr error
|
||||
|
||||
|
|
|
@ -173,6 +173,10 @@ func TestLintErrors(t *testing.T) {
|
|||
from: "{steps: { build: { image: plugins/docker, settings: { test: 'true' } } }, when: { branch: main, event: push } } }",
|
||||
want: "Cannot use once privileged plugins removed from WOODPECKER_ESCALATE, use 'woodpeckerci/plugin-docker-buildx' instead",
|
||||
},
|
||||
{
|
||||
from: "{steps: { build: { image: golang, settings: { test: 'true' } } }, when: { branch: main, event: push }, clone: { git: { image: some-other/plugin-git:v1.1.0 } } }",
|
||||
want: "Specified clone image does not match allow list, netrc will not be injected",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testdata {
|
||||
|
|
|
@ -30,3 +30,10 @@ func PrivilegedPlugins(plugins []string) Option {
|
|||
linter.privilegedPlugins = &plugins
|
||||
}
|
||||
}
|
||||
|
||||
// WithTrustedClonePlugins adds the list of trusted clone plugins.
|
||||
func WithTrustedClonePlugins(plugins []string) Option {
|
||||
return func(linter *Linter) {
|
||||
linter.trustedClonePlugins = &plugins
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@ import (
|
|||
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/constraint"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/types/base"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/utils"
|
||||
"go.woodpecker-ci.org/woodpecker/v2/shared/constant"
|
||||
)
|
||||
|
||||
type (
|
||||
|
@ -125,6 +124,6 @@ func (c *Container) IsPlugin() bool {
|
|||
len(c.Secrets) == 0
|
||||
}
|
||||
|
||||
func (c *Container) IsTrustedCloneImage() bool {
|
||||
return c.IsPlugin() && utils.MatchImage(c.Image, constant.TrustedCloneImages...)
|
||||
func (c *Container) IsTrustedCloneImage(trustedClonePlugins []string) bool {
|
||||
return c.IsPlugin() && utils.MatchImageDynamic(c.Image, trustedClonePlugins...)
|
||||
}
|
||||
|
|
|
@ -64,7 +64,8 @@ var Config = struct {
|
|||
Pipeline struct {
|
||||
AuthenticatePublicRepos bool
|
||||
DefaultCancelPreviousPipelineEvents []model.WebhookEvent
|
||||
DefaultCloneImage string
|
||||
DefaultClonePlugin string
|
||||
TrustedClonePlugins []string
|
||||
Limits model.ResourceLimit
|
||||
Volumes []string
|
||||
Networks []string
|
||||
|
|
|
@ -143,6 +143,7 @@ func (b *StepBuilder) genItemForWorkflow(workflow *model.Workflow, axis matrix.A
|
|||
errorsAndWarnings = multierr.Append(errorsAndWarnings, linter.New(
|
||||
linter.WithTrusted(b.Repo.IsTrusted),
|
||||
linter.PrivilegedPlugins(server.Config.Pipeline.PrivilegedPlugins),
|
||||
linter.WithTrustedClonePlugins(server.Config.Pipeline.TrustedClonePlugins),
|
||||
).Lint([]*linter.WorkflowConfig{{
|
||||
Workflow: parsed,
|
||||
File: workflow.Name,
|
||||
|
@ -281,7 +282,8 @@ func (b *StepBuilder) toInternalRepresentation(parsed *yaml_types.Workflow, envi
|
|||
),
|
||||
b.Repo.IsSCMPrivate || server.Config.Pipeline.AuthenticatePublicRepos,
|
||||
),
|
||||
compiler.WithDefaultCloneImage(server.Config.Pipeline.DefaultCloneImage),
|
||||
compiler.WithDefaultClonePlugin(server.Config.Pipeline.DefaultClonePlugin),
|
||||
compiler.WithTrustedClonePlugins(server.Config.Pipeline.TrustedClonePlugins),
|
||||
compiler.WithRegistry(registries...),
|
||||
compiler.WithSecret(secrets...),
|
||||
compiler.WithPrefix(
|
||||
|
|
|
@ -29,12 +29,14 @@ var DefaultConfigOrder = [...]string{
|
|||
}
|
||||
|
||||
const (
|
||||
// DefaultCloneImage can be changed by 'WOODPECKER_DEFAULT_CLONE_IMAGE' at runtime.
|
||||
// DefaultClonePlugin can be changed by 'WOODPECKER_DEFAULT_CLONE_PLUGIN' at runtime.
|
||||
// renovate: datasource=docker depName=woodpeckerci/plugin-git
|
||||
DefaultCloneImage = "docker.io/woodpeckerci/plugin-git:2.5.2"
|
||||
DefaultClonePlugin = "docker.io/woodpeckerci/plugin-git:2.5.2"
|
||||
)
|
||||
|
||||
var TrustedCloneImages = []string{
|
||||
DefaultCloneImage,
|
||||
// TrustedClonePlugins can be changed by 'WOODPECKER_PLUGINS_TRUSTED_CLONE' at runtime.
|
||||
var TrustedClonePlugins = []string{
|
||||
DefaultClonePlugin,
|
||||
"docker.io/woodpeckerci/plugin-git",
|
||||
"quay.io/woodpeckerci/plugin-git",
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue