1
0
mirror of https://github.com/woodpecker-ci/woodpecker.git synced 2026-06-03 16:35:37 +02:00
Files
woodpecker/pipeline/frontend/yaml/compiler/compiler_test.go
T

541 lines
17 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"
"go.woodpecker-ci.org/woodpecker/v3/pipeline/frontend/yaml/constraint"
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: []metadata.Event{"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: []metadata.Event{"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),
)
defaultNetwork := "test_default"
defaultVolume := "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{defaultVolume + ":/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{
Network: defaultNetwork,
Volume: defaultVolume,
},
},
{
name: "empty workflow, default clone",
fronConf: &yaml_types.Workflow{},
backConf: &backend_types.Config{
Network: defaultNetwork,
Volume: defaultVolume,
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{
Network: defaultNetwork,
Volume: defaultVolume,
Stages: []*backend_types.Stage{defaultCloneStage, {
Steps: []*backend_types.Step{{
Name: "dummy",
Type: backend_types.StepTypePlugin,
Image: "dummy_img",
OnSuccess: true,
Failure: "fail",
Volumes: []string{defaultVolume + ":/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{
Network: defaultNetwork,
Volume: defaultVolume,
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{defaultVolume + ":/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{defaultVolume + ":/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{defaultVolume + ":/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: constraint.DependsOn{{Name: "echo env"}, {Name: "echo 2"}},
}, {
Name: "echo 2",
Image: "bash",
Commands: []string{"echo 2"},
}}}},
backConf: &backend_types.Config{
Network: defaultNetwork,
Volume: defaultVolume,
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{defaultVolume + ":/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{defaultVolume + ":/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{defaultVolume + ":/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: map[string]any{
"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: constraint.DependsOn{{Name: "not exist"}},
}}}},
backConf: nil,
expectedErr: "step 'dummy' depends on unknown step 'not exist'",
},
{
name: "workflow with step depending on filtered-out step",
fronConf: &yaml_types.Workflow{Steps: yaml_types.ContainerList{ContainerList: []*yaml_types.Container{
{
Name: "build",
Image: "bash",
When: constraint.When{Constraints: []constraint.Constraint{{
Event: yaml_base_types.StringOrSlice{"tag"},
}}},
},
{
Name: "deploy",
Image: "bash",
DependsOn: constraint.DependsOn{{Name: "build"}},
},
}}},
backConf: nil,
expectedErr: "step 'deploy' depends on step 'build' which is filtered out by its conditions",
},
}
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
s.SecretMapping = nil
}
}
// check if we get an expected backend config based on a frontend config
assert.EqualValues(t, *test.backConf, *backConf)
}
})
}
}
func TestCompilerCompileWithFromSecret(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",
}),
WithSecret(Secret{
Name: "secret_name",
Value: "VERY_SECRET",
}),
WithPrefix("test"),
// we use "/test" as custom workspace base to ensure the enforcement of the pluginWorkspaceBase is applied
WithWorkspaceFromURL("/test", repoURL),
)
defaultNetwork := "test_default"
defaultVolume := "test_default"
defaultCloneStage := &backend_types.Stage{
Steps: []*backend_types.Step{{
Name: "clone",
Type: backend_types.StepTypeClone,
Image: constant.DefaultClonePlugin,
OnSuccess: true,
Failure: "fail",
WorkingDir: "/woodpecker/src/github.com/octocat/hello-world",
WorkspaceBase: "/woodpecker",
Volumes: []string{defaultVolume + ":/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: "workflow with missing secret",
fronConf: &yaml_types.Workflow{Steps: yaml_types.ContainerList{ContainerList: []*yaml_types.Container{{
Name: "step",
Image: "bash",
Commands: []string{"env"},
Environment: map[string]any{
"SECRET": map[string]any{"from_secret": "secret_name"},
},
}}}},
backConf: &backend_types.Config{
Stages: []*backend_types.Stage{defaultCloneStage, {
Steps: []*backend_types.Step{{
Name: "step",
Type: backend_types.StepTypeCommands,
Image: "bash",
Commands: []string{"env"},
OnSuccess: true,
Failure: "fail",
WorkingDir: "/test/src/github.com/octocat/hello-world",
WorkspaceBase: "/test",
Volumes: []string{defaultVolume + ":/test"},
Networks: []backend_types.Conn{{Name: "test_default", Aliases: []string{"step"}}},
ExtraHosts: []backend_types.HostAlias{},
SecretMapping: map[string]string{
"SECRET": "VERY_SECRET",
},
}},
}},
Volume: defaultVolume,
Network: defaultNetwork,
Secrets: []*backend_types.Secret{{
Name: "secret_name",
Value: "VERY_SECRET",
}},
},
expectedErr: "",
},
}
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
if len(s.SecretMapping) == 0 {
s.SecretMapping = 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 metadata.Event
match bool
}{
{
name: "should match event",
secret: Secret{Events: []metadata.Event{"pull_request"}},
event: "pull_request",
match: true,
},
{
name: "should not match event",
secret: Secret{Events: []metadata.Event{"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: []metadata.Event{"pull_request"}},
event: "pull_request_closed",
match: true,
},
{
name: "pull metadata change should match pull",
secret: Secret{Events: []metadata.Event{"pull_request"}},
event: "pull_request_metadata",
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: constraint.DependsOn{}, // 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)
}