405 lines
13 KiB
Go
405 lines
13 KiB
Go
// Copyright 2023 Woodpecker Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package compiler
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
backend_types "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types"
|
|
"go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/metadata"
|
|
yaml_types "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/types"
|
|
yaml_base_types "go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/types/base"
|
|
"go.woodpecker-ci.org/woodpecker/v3/shared/constant"
|
|
)
|
|
|
|
func TestSecretAvailable(t *testing.T) {
|
|
secret := Secret{
|
|
AllowedPlugins: []string{},
|
|
Events: []string{"push"},
|
|
}
|
|
assert.NoError(t, secret.Available("push", &yaml_types.Container{
|
|
Image: "golang",
|
|
Commands: yaml_base_types.StringOrSlice{"echo 'this is not a plugin'"},
|
|
}))
|
|
|
|
// secret only available for "golang" plugin
|
|
secret = Secret{
|
|
Name: "foo",
|
|
AllowedPlugins: []string{"golang"},
|
|
Events: []string{"push"},
|
|
}
|
|
assert.NoError(t, secret.Available("push", &yaml_types.Container{
|
|
Name: "step",
|
|
Image: "golang",
|
|
Commands: yaml_base_types.StringOrSlice{},
|
|
}))
|
|
assert.ErrorContains(t, secret.Available("push", &yaml_types.Container{
|
|
Image: "golang",
|
|
Commands: yaml_base_types.StringOrSlice{"echo 'this is not a plugin'"},
|
|
}), "is only allowed to be used by plugins (a filter has been set on the secret). Note: Image filters do not work for normal steps")
|
|
assert.ErrorContains(t, secret.Available("push", &yaml_types.Container{
|
|
Image: "not-golang",
|
|
Commands: yaml_base_types.StringOrSlice{},
|
|
}), "not allowed to be used with image ")
|
|
assert.ErrorContains(t, secret.Available("pull_request", &yaml_types.Container{
|
|
Image: "golang",
|
|
}), "not allowed to be used with pipeline event ")
|
|
}
|
|
|
|
func TestCompilerCompile(t *testing.T) {
|
|
repoURL := "https://github.com/octocat/hello-world"
|
|
compiler := New(
|
|
WithMetadata(metadata.Metadata{
|
|
Repo: metadata.Repo{
|
|
Owner: "octacat",
|
|
Name: "hello-world",
|
|
Private: true,
|
|
ForgeURL: repoURL,
|
|
CloneURL: "https://github.com/octocat/hello-world.git",
|
|
},
|
|
}),
|
|
WithEnviron(map[string]string{
|
|
"VERBOSE": "true",
|
|
"COLORED": "true",
|
|
}),
|
|
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{{
|
|
Name: "test_default",
|
|
}}
|
|
defaultVolumes := []*backend_types.Volume{{
|
|
Name: "test_default",
|
|
}}
|
|
|
|
defaultCloneStage := &backend_types.Stage{
|
|
Steps: []*backend_types.Step{{
|
|
Name: "clone",
|
|
Type: backend_types.StepTypeClone,
|
|
Image: constant.DefaultClonePlugin,
|
|
OnSuccess: true,
|
|
Failure: "fail",
|
|
Volumes: []string{defaultVolumes[0].Name + ":/woodpecker"},
|
|
WorkingDir: "/woodpecker/src/github.com/octocat/hello-world",
|
|
WorkspaceBase: "/woodpecker",
|
|
Networks: []backend_types.Conn{{Name: "test_default", Aliases: []string{"clone"}}},
|
|
ExtraHosts: []backend_types.HostAlias{},
|
|
}},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
fronConf *yaml_types.Workflow
|
|
backConf *backend_types.Config
|
|
expectedErr string
|
|
}{
|
|
{
|
|
name: "empty workflow, no clone",
|
|
fronConf: &yaml_types.Workflow{SkipClone: true},
|
|
backConf: &backend_types.Config{
|
|
Networks: defaultNetworks,
|
|
Volumes: defaultVolumes,
|
|
},
|
|
},
|
|
{
|
|
name: "empty workflow, default clone",
|
|
fronConf: &yaml_types.Workflow{},
|
|
backConf: &backend_types.Config{
|
|
Networks: defaultNetworks,
|
|
Volumes: defaultVolumes,
|
|
Stages: []*backend_types.Stage{defaultCloneStage},
|
|
},
|
|
},
|
|
{
|
|
name: "workflow with one dummy step",
|
|
fronConf: &yaml_types.Workflow{Steps: yaml_types.ContainerList{ContainerList: []*yaml_types.Container{{
|
|
Name: "dummy",
|
|
Image: "dummy_img",
|
|
}}}},
|
|
backConf: &backend_types.Config{
|
|
Networks: defaultNetworks,
|
|
Volumes: defaultVolumes,
|
|
Stages: []*backend_types.Stage{defaultCloneStage, {
|
|
Steps: []*backend_types.Step{{
|
|
Name: "dummy",
|
|
Type: backend_types.StepTypePlugin,
|
|
Image: "dummy_img",
|
|
OnSuccess: true,
|
|
Failure: "fail",
|
|
Volumes: []string{defaultVolumes[0].Name + ":/woodpecker"},
|
|
WorkingDir: "/woodpecker/src/github.com/octocat/hello-world",
|
|
WorkspaceBase: "/woodpecker",
|
|
Networks: []backend_types.Conn{{Name: "test_default", Aliases: []string{"dummy"}}},
|
|
ExtraHosts: []backend_types.HostAlias{},
|
|
}},
|
|
}},
|
|
},
|
|
},
|
|
{
|
|
name: "workflow with three steps",
|
|
fronConf: &yaml_types.Workflow{Steps: yaml_types.ContainerList{ContainerList: []*yaml_types.Container{{
|
|
Name: "echo env",
|
|
Image: "bash",
|
|
Commands: []string{"env"},
|
|
}, {
|
|
Name: "parallel echo 1",
|
|
Image: "bash",
|
|
Commands: []string{"echo 1"},
|
|
}, {
|
|
Name: "parallel echo 2",
|
|
Image: "bash",
|
|
Commands: []string{"echo 2"},
|
|
}}}},
|
|
backConf: &backend_types.Config{
|
|
Networks: defaultNetworks,
|
|
Volumes: defaultVolumes,
|
|
Stages: []*backend_types.Stage{
|
|
defaultCloneStage, {
|
|
Steps: []*backend_types.Step{{
|
|
Name: "echo env",
|
|
Type: backend_types.StepTypeCommands,
|
|
Image: "bash",
|
|
Commands: []string{"env"},
|
|
OnSuccess: true,
|
|
Failure: "fail",
|
|
Volumes: []string{defaultVolumes[0].Name + ":/test"},
|
|
WorkingDir: "/test/src/github.com/octocat/hello-world",
|
|
WorkspaceBase: "/test",
|
|
Networks: []backend_types.Conn{{Name: "test_default", Aliases: []string{"echo env"}}},
|
|
ExtraHosts: []backend_types.HostAlias{},
|
|
}},
|
|
}, {
|
|
Steps: []*backend_types.Step{{
|
|
Name: "parallel echo 1",
|
|
Type: backend_types.StepTypeCommands,
|
|
Image: "bash",
|
|
Commands: []string{"echo 1"},
|
|
OnSuccess: true,
|
|
Failure: "fail",
|
|
Volumes: []string{defaultVolumes[0].Name + ":/test"},
|
|
WorkingDir: "/test/src/github.com/octocat/hello-world",
|
|
WorkspaceBase: "/test",
|
|
Networks: []backend_types.Conn{{Name: "test_default", Aliases: []string{"parallel echo 1"}}},
|
|
ExtraHosts: []backend_types.HostAlias{},
|
|
}},
|
|
}, {
|
|
Steps: []*backend_types.Step{{
|
|
Name: "parallel echo 2",
|
|
Type: backend_types.StepTypeCommands,
|
|
Image: "bash",
|
|
Commands: []string{"echo 2"},
|
|
OnSuccess: true,
|
|
Failure: "fail",
|
|
Volumes: []string{defaultVolumes[0].Name + ":/test"},
|
|
WorkingDir: "/test/src/github.com/octocat/hello-world",
|
|
WorkspaceBase: "/test",
|
|
Networks: []backend_types.Conn{{Name: "test_default", Aliases: []string{"parallel echo 2"}}},
|
|
ExtraHosts: []backend_types.HostAlias{},
|
|
}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "workflow with three steps and depends_on",
|
|
fronConf: &yaml_types.Workflow{Steps: yaml_types.ContainerList{ContainerList: []*yaml_types.Container{{
|
|
Name: "echo env",
|
|
Image: "bash",
|
|
Commands: []string{"env"},
|
|
}, {
|
|
Name: "echo 1",
|
|
Image: "bash",
|
|
Commands: []string{"echo 1"},
|
|
DependsOn: []string{"echo env", "echo 2"},
|
|
}, {
|
|
Name: "echo 2",
|
|
Image: "bash",
|
|
Commands: []string{"echo 2"},
|
|
}}}},
|
|
backConf: &backend_types.Config{
|
|
Networks: defaultNetworks,
|
|
Volumes: defaultVolumes,
|
|
Stages: []*backend_types.Stage{defaultCloneStage, {
|
|
Steps: []*backend_types.Step{{
|
|
Name: "echo env",
|
|
Type: backend_types.StepTypeCommands,
|
|
Image: "bash",
|
|
Commands: []string{"env"},
|
|
OnSuccess: true,
|
|
Failure: "fail",
|
|
Volumes: []string{defaultVolumes[0].Name + ":/test"},
|
|
WorkingDir: "/test/src/github.com/octocat/hello-world",
|
|
WorkspaceBase: "/test",
|
|
Networks: []backend_types.Conn{{Name: "test_default", Aliases: []string{"echo env"}}},
|
|
ExtraHosts: []backend_types.HostAlias{},
|
|
}, {
|
|
Name: "echo 2",
|
|
Type: backend_types.StepTypeCommands,
|
|
Image: "bash",
|
|
Commands: []string{"echo 2"},
|
|
OnSuccess: true,
|
|
Failure: "fail",
|
|
Volumes: []string{defaultVolumes[0].Name + ":/test"},
|
|
WorkingDir: "/test/src/github.com/octocat/hello-world",
|
|
WorkspaceBase: "/test",
|
|
Networks: []backend_types.Conn{{Name: "test_default", Aliases: []string{"echo 2"}}},
|
|
ExtraHosts: []backend_types.HostAlias{},
|
|
}},
|
|
}, {
|
|
Steps: []*backend_types.Step{{
|
|
Name: "echo 1",
|
|
Type: backend_types.StepTypeCommands,
|
|
Image: "bash",
|
|
Commands: []string{"echo 1"},
|
|
OnSuccess: true,
|
|
Failure: "fail",
|
|
Volumes: []string{defaultVolumes[0].Name + ":/test"},
|
|
WorkingDir: "/test/src/github.com/octocat/hello-world",
|
|
WorkspaceBase: "/test",
|
|
Networks: []backend_types.Conn{{Name: "test_default", Aliases: []string{"echo 1"}}},
|
|
ExtraHosts: []backend_types.HostAlias{},
|
|
}},
|
|
}},
|
|
},
|
|
},
|
|
{
|
|
name: "workflow with missing secret",
|
|
fronConf: &yaml_types.Workflow{Steps: yaml_types.ContainerList{ContainerList: []*yaml_types.Container{{
|
|
Name: "step",
|
|
Image: "bash",
|
|
Commands: []string{"env"},
|
|
Environment: yaml_base_types.EnvironmentMap{
|
|
"MISSING": map[string]any{"from_secret": "missing"},
|
|
},
|
|
}}}},
|
|
backConf: nil,
|
|
expectedErr: "secret \"missing\" not found",
|
|
},
|
|
{
|
|
name: "workflow with broken step dependency",
|
|
fronConf: &yaml_types.Workflow{Steps: yaml_types.ContainerList{ContainerList: []*yaml_types.Container{{
|
|
Name: "dummy",
|
|
Image: "dummy_img",
|
|
DependsOn: []string{"not exist"},
|
|
}}}},
|
|
backConf: nil,
|
|
expectedErr: "step 'dummy' depends on unknown step 'not exist'",
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
backConf, err := compiler.Compile(test.fronConf)
|
|
if test.expectedErr != "" {
|
|
assert.Error(t, err)
|
|
assert.Equal(t, test.expectedErr, err.Error())
|
|
} else {
|
|
// we ignore uuids in steps and only check if global env got set ...
|
|
for _, st := range backConf.Stages {
|
|
for _, s := range st.Steps {
|
|
s.UUID = ""
|
|
assert.Truef(t, s.Environment["VERBOSE"] == "true", "expected to get value of global set environment")
|
|
assert.Truef(t, len(s.Environment) > 10, "expected to have a lot of built-in variables")
|
|
s.Environment = nil
|
|
}
|
|
}
|
|
// check if we get an expected backend config based on a frontend config
|
|
assert.EqualValues(t, *test.backConf, *backConf)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSecretMatch(t *testing.T) {
|
|
tcl := []*struct {
|
|
name string
|
|
secret Secret
|
|
event string
|
|
match bool
|
|
}{
|
|
{
|
|
name: "should match event",
|
|
secret: Secret{Events: []string{"pull_request"}},
|
|
event: "pull_request",
|
|
match: true,
|
|
},
|
|
{
|
|
name: "should not match event",
|
|
secret: Secret{Events: []string{"pull_request"}},
|
|
event: "push",
|
|
match: false,
|
|
},
|
|
{
|
|
name: "should match when no event filters defined",
|
|
secret: Secret{},
|
|
event: "pull_request",
|
|
match: true,
|
|
},
|
|
{
|
|
name: "pull close should match pull",
|
|
secret: Secret{Events: []string{"pull_request"}},
|
|
event: "pull_request_closed",
|
|
match: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tcl {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
assert.Equal(t, tc.match, tc.secret.Match(tc.event))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCompilerCompilePrivileged(t *testing.T) {
|
|
compiler := New(
|
|
WithEscalated("test/image"),
|
|
)
|
|
|
|
fronConf := &yaml_types.Workflow{
|
|
SkipClone: true,
|
|
Steps: yaml_types.ContainerList{
|
|
ContainerList: []*yaml_types.Container{
|
|
{
|
|
Name: "privileged-plugin",
|
|
Image: "test/image",
|
|
DependsOn: []string{}, // no dependencies => enable dag mode & all steps are executed in parallel
|
|
},
|
|
{
|
|
Name: "no-plugin",
|
|
Image: "test/image",
|
|
Commands: []string{"echo 'i am not a plugin anymore'"},
|
|
},
|
|
{
|
|
Name: "not-privileged-image",
|
|
Image: "some/other-image",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
backConf, err := compiler.Compile(fronConf)
|
|
assert.NoError(t, err)
|
|
|
|
assert.Len(t, backConf.Stages, 1)
|
|
assert.Len(t, backConf.Stages[0].Steps, 3)
|
|
assert.True(t, backConf.Stages[0].Steps[0].Privileged)
|
|
assert.False(t, backConf.Stages[0].Steps[1].Privileged)
|
|
assert.False(t, backConf.Stages[0].Steps[2].Privileged)
|
|
}
|