woodpecker/pipeline/backend/dummy/dummy.go

241 lines
7.4 KiB
Go

// Copyright 2024 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.
//go:build test
// +build test
package dummy
import (
"context"
"fmt"
"io"
"strconv"
"strings"
"sync"
"time"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v3"
backend "go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types"
)
type dummy struct {
kv sync.Map
}
const (
// Step names to control behavior of dummy backend.
WorkflowSetupFailUUID = "WorkflowSetupShouldFail"
EnvKeyStepSleep = "SLEEP"
EnvKeyStepType = "EXPECT_TYPE"
EnvKeyStepStartFail = "STEP_START_FAIL"
EnvKeyStepExitCode = "STEP_EXIT_CODE"
EnvKeyStepTailFail = "STEP_TAIL_FAIL"
EnvKeyStepOOMKilled = "STEP_OOM_KILLED"
// Internal const.
stepStateStarted = "started"
stepStateDone = "done"
testServiceTimeout = 1 * time.Second
)
// New returns a dummy backend.
func New() backend.Backend {
return &dummy{
kv: sync.Map{},
}
}
func (e *dummy) Name() string {
return "dummy"
}
func (e *dummy) IsAvailable(_ context.Context) bool {
return true
}
func (e *dummy) Flags() []cli.Flag {
return nil
}
// Load new client for Docker Backend using environment variables.
func (e *dummy) Load(_ context.Context) (*backend.BackendInfo, error) {
return &backend.BackendInfo{
Platform: "dummy",
}, nil
}
func (e *dummy) SetupWorkflow(_ context.Context, _ *backend.Config, taskUUID string) error {
if taskUUID == WorkflowSetupFailUUID {
return fmt.Errorf("expected fail to setup workflow")
}
log.Trace().Str("taskUUID", taskUUID).Msg("create workflow environment")
e.kv.Store("task_"+taskUUID, "setup")
return nil
}
func (e *dummy) StartStep(_ context.Context, step *backend.Step, taskUUID string) error {
log.Trace().Str("taskUUID", taskUUID).Msgf("start step %s", step.Name)
// internal state checks
_, exist := e.kv.Load("task_" + taskUUID)
if !exist {
return fmt.Errorf("expect env of workflow %s to exist but found none to destroy", taskUUID)
}
stepState, stepExist := e.kv.Load(fmt.Sprintf("task_%s_step_%s", taskUUID, step.UUID))
if stepExist {
// Detect issues like https://github.com/woodpecker-ci/woodpecker/issues/3494
return fmt.Errorf("StartStep detected already started step '%s' (%s) in state: %s", step.Name, step.UUID, stepState)
}
if stepStartFail, _ := strconv.ParseBool(step.Environment[EnvKeyStepStartFail]); stepStartFail {
return fmt.Errorf("expected fail to start step")
}
expectStepType, testStepType := step.Environment[EnvKeyStepType]
if testStepType && string(step.Type) != expectStepType {
return fmt.Errorf("expected step type '%s' but got '%s'", expectStepType, step.Type)
}
e.kv.Store(fmt.Sprintf("task_%s_step_%s", taskUUID, step.UUID), stepStateStarted)
return nil
}
func (e *dummy) WaitStep(ctx context.Context, step *backend.Step, taskUUID string) (*backend.State, error) {
log.Trace().Str("taskUUID", taskUUID).Msgf("wait for step %s", step.Name)
_, exist := e.kv.Load("task_" + taskUUID)
if !exist {
err := fmt.Errorf("expect env of workflow %s to exist but found none to destroy", taskUUID)
return &backend.State{Error: err}, err
}
// check state
stepState, stepExist := e.kv.Load(fmt.Sprintf("task_%s_step_%s", taskUUID, step.UUID))
if !stepExist {
err := fmt.Errorf("WaitStep expect step '%s' (%s) to be created but found none", step.Name, step.UUID)
return &backend.State{Error: err}, err
}
if stepState != stepStateStarted {
err := fmt.Errorf("WaitStep expect step '%s' (%s) to be '%s' but it is: %s", step.Name, step.UUID, stepStateStarted, stepState)
return &backend.State{Error: err}, err
}
// extend wait time logic
if sleep, sleepExist := step.Environment[EnvKeyStepSleep]; sleepExist {
toSleep, err := time.ParseDuration(sleep)
if err != nil {
err = fmt.Errorf("WaitStep fail to parse sleep duration: %w", err)
return &backend.State{Error: err}, err
}
time.Sleep(toSleep)
} else {
if step.Type == backend.StepTypeService {
select {
case <-time.NewTimer(testServiceTimeout).C:
err := fmt.Errorf("WaitStep fail due to timeout of service after 1 second")
return &backend.State{Error: err}, err
case <-ctx.Done():
// context for service closed ... we can move forward
}
} else {
time.Sleep(time.Nanosecond)
}
}
e.kv.Store(fmt.Sprintf("task_%s_step_%s", taskUUID, step.UUID), stepStateDone)
oomKilled, _ := strconv.ParseBool(step.Environment[EnvKeyStepOOMKilled])
exitCode := 0
if code, exist := step.Environment[EnvKeyStepExitCode]; exist {
exitCode, _ = strconv.Atoi(strings.TrimSpace(code))
}
return &backend.State{
ExitCode: exitCode,
Exited: true,
OOMKilled: oomKilled,
}, nil
}
func (e *dummy) TailStep(_ context.Context, step *backend.Step, taskUUID string) (io.ReadCloser, error) {
log.Trace().Str("taskUUID", taskUUID).Msgf("tail logs of step %s", step.Name)
_, exist := e.kv.Load("task_" + taskUUID)
if !exist {
return nil, fmt.Errorf("expect env of workflow %s to exist but found none to destroy", taskUUID)
}
// check state
stepState, stepExist := e.kv.Load(fmt.Sprintf("task_%s_step_%s", taskUUID, step.UUID))
if !stepExist {
return nil, fmt.Errorf("WaitStep expect step '%s' (%s) to be created but found none", step.Name, step.UUID)
}
if stepState != stepStateStarted {
return nil, fmt.Errorf("WaitStep expect step '%s' (%s) to be '%s' but it is: %s", step.Name, step.UUID, stepStateStarted, stepState)
}
if tailShouldFail, _ := strconv.ParseBool(step.Environment[EnvKeyStepTailFail]); tailShouldFail {
return nil, fmt.Errorf("expected fail to read stdout of step")
}
return io.NopCloser(strings.NewReader(dummyExecStepOutput(step))), nil
}
func (e *dummy) DestroyStep(_ context.Context, step *backend.Step, taskUUID string) error {
log.Trace().Str("taskUUID", taskUUID).Msgf("stop step %s", step.Name)
_, exist := e.kv.Load("task_" + taskUUID)
if !exist {
return fmt.Errorf("expect env of workflow %s to exist but found none to destroy", taskUUID)
}
// check state
stepState, stepExist := e.kv.Load(fmt.Sprintf("task_%s_step_%s", taskUUID, step.UUID))
if !stepExist {
return fmt.Errorf("WaitStep expect step '%s' (%s) to be created but found none", step.Name, step.UUID)
}
if stepState != stepStateDone {
return fmt.Errorf("WaitStep expect step '%s' (%s) to be '%s' but it is: %s", step.Name, step.UUID, stepStateDone, stepState)
}
e.kv.Delete(fmt.Sprintf("task_%s_step_%s", taskUUID, step.UUID))
return nil
}
func (e *dummy) DestroyWorkflow(_ context.Context, _ *backend.Config, taskUUID string) error {
log.Trace().Str("taskUUID", taskUUID).Msgf("delete workflow environment")
_, exist := e.kv.Load("task_" + taskUUID)
if !exist {
return fmt.Errorf("expect env of workflow %s to exist but found none to destroy", taskUUID)
}
e.kv.Delete("task_" + taskUUID)
return nil
}
func dummyExecStepOutput(step *backend.Step) string {
return fmt.Sprintf(`StepName: %s
StepType: %s
StepUUID: %s
StepCommands:
------------------
%s
------------------
`, step.Name, step.Type, step.UUID, strings.Join(step.Commands, "\n"))
}