2022-10-18 03:24:12 +02:00
|
|
|
// Copyright 2022 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.
|
|
|
|
|
|
2022-03-10 15:07:02 -06:00
|
|
|
package local
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2023-02-02 00:08:02 +01:00
|
|
|
"errors"
|
2022-11-06 13:36:34 +01:00
|
|
|
"fmt"
|
2022-03-10 15:07:02 -06:00
|
|
|
"io"
|
|
|
|
|
"os"
|
|
|
|
|
"os/exec"
|
2023-04-02 15:47:22 +01:00
|
|
|
"path/filepath"
|
2023-10-28 13:38:47 +02:00
|
|
|
"runtime"
|
2023-10-09 09:11:08 +02:00
|
|
|
"slices"
|
2023-08-07 15:39:58 +02:00
|
|
|
"sync"
|
2022-03-10 15:07:02 -06:00
|
|
|
|
2023-07-20 20:39:20 +02:00
|
|
|
"github.com/rs/zerolog/log"
|
2024-07-17 16:26:35 -07:00
|
|
|
"github.com/urfave/cli/v3"
|
2022-11-06 13:36:34 +01:00
|
|
|
|
2024-12-22 11:44:34 +02:00
|
|
|
"go.woodpecker-ci.org/woodpecker/v3/pipeline/backend/types"
|
2022-03-10 15:07:02 -06:00
|
|
|
)
|
|
|
|
|
|
2023-05-17 14:53:23 +02:00
|
|
|
type workflowState struct {
|
2025-10-01 16:58:37 +02:00
|
|
|
stepState sync.Map // map of *stepState
|
2023-08-07 15:39:58 +02:00
|
|
|
baseDir string
|
|
|
|
|
homeDir string
|
|
|
|
|
workspaceDir string
|
|
|
|
|
pluginGitBinary string
|
2023-05-17 14:53:23 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-01 16:58:37 +02:00
|
|
|
type stepState struct {
|
|
|
|
|
cmd *exec.Cmd
|
|
|
|
|
output io.ReadCloser
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-10 15:07:02 -06:00
|
|
|
type local struct {
|
2023-11-02 15:45:18 +01:00
|
|
|
tempDir string
|
2023-08-07 15:39:58 +02:00
|
|
|
workflows sync.Map
|
|
|
|
|
pluginGitBinary string
|
2023-11-02 07:58:32 +01:00
|
|
|
os, arch string
|
2022-03-10 15:07:02 -06:00
|
|
|
}
|
|
|
|
|
|
2025-10-27 13:12:26 +01:00
|
|
|
var CLIWorkaroundExecAtDir string // To handle edge case for running local backend via cli exec
|
|
|
|
|
|
2023-12-14 19:20:47 +01:00
|
|
|
// New returns a new local Backend.
|
|
|
|
|
func New() types.Backend {
|
2023-11-02 07:58:32 +01:00
|
|
|
return &local{
|
|
|
|
|
os: runtime.GOOS,
|
|
|
|
|
arch: runtime.GOARCH,
|
|
|
|
|
}
|
2022-03-10 15:07:02 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *local) Name() string {
|
|
|
|
|
return "local"
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-07 21:11:55 +02:00
|
|
|
func (e *local) IsAvailable(ctx context.Context) bool {
|
|
|
|
|
if c, ok := ctx.Value(types.CliCommand).(*cli.Command); ok {
|
|
|
|
|
if c.String("backend-engine") == e.Name() {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-08-07 13:04:10 +03:00
|
|
|
_, inContainer := os.LookupEnv("WOODPECKER_IN_CONTAINER")
|
2024-08-07 21:11:55 +02:00
|
|
|
return !inContainer
|
2022-03-10 15:07:02 -06:00
|
|
|
}
|
|
|
|
|
|
2024-02-08 16:33:22 +01:00
|
|
|
func (e *local) Flags() []cli.Flag {
|
|
|
|
|
return Flags
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-14 19:20:47 +01:00
|
|
|
func (e *local) Load(ctx context.Context) (*types.BackendInfo, error) {
|
2024-07-17 16:26:35 -07:00
|
|
|
c, ok := ctx.Value(types.CliCommand).(*cli.Command)
|
2023-11-02 15:45:18 +01:00
|
|
|
if ok {
|
|
|
|
|
e.tempDir = c.String("backend-local-temp-dir")
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-07 15:39:58 +02:00
|
|
|
e.loadClone()
|
2023-05-17 14:53:23 +02:00
|
|
|
|
2023-12-14 19:20:47 +01:00
|
|
|
return &types.BackendInfo{
|
2023-11-02 07:58:32 +01:00
|
|
|
Platform: e.os + "/" + e.arch,
|
2023-11-01 15:38:37 +01:00
|
|
|
}, nil
|
2023-05-17 14:53:23 +02:00
|
|
|
}
|
|
|
|
|
|
2023-08-07 15:39:58 +02:00
|
|
|
func (e *local) SetupWorkflow(_ context.Context, _ *types.Config, taskUUID string) error {
|
2023-07-20 20:39:20 +02:00
|
|
|
log.Trace().Str("taskUUID", taskUUID).Msg("create workflow environment")
|
|
|
|
|
|
2023-11-02 15:45:18 +01:00
|
|
|
baseDir, err := os.MkdirTemp(e.tempDir, "woodpecker-local-*")
|
2023-04-20 00:56:03 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-17 14:53:23 +02:00
|
|
|
state := &workflowState{
|
2025-10-27 13:12:26 +01:00
|
|
|
baseDir: baseDir,
|
|
|
|
|
homeDir: filepath.Join(baseDir, "home"),
|
2023-05-17 14:53:23 +02:00
|
|
|
}
|
2025-10-27 13:12:26 +01:00
|
|
|
e.workflows.Store(taskUUID, state)
|
2023-05-17 14:53:23 +02:00
|
|
|
|
|
|
|
|
if err := os.Mkdir(state.homeDir, 0o700); err != nil {
|
2023-04-20 00:56:03 +02:00
|
|
|
return err
|
|
|
|
|
}
|
2022-03-10 15:07:02 -06:00
|
|
|
|
2025-10-27 13:12:26 +01:00
|
|
|
// normal workspace setup case
|
|
|
|
|
if CLIWorkaroundExecAtDir == "" {
|
|
|
|
|
state.workspaceDir = filepath.Join(baseDir, "workspace")
|
|
|
|
|
if err := os.Mkdir(state.workspaceDir, 0o700); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
} else
|
|
|
|
|
// setup workspace via internal flag signaled from cli exec to a specific dir
|
|
|
|
|
{
|
|
|
|
|
state.workspaceDir = CLIWorkaroundExecAtDir
|
|
|
|
|
if stat, err := os.Stat(CLIWorkaroundExecAtDir); os.IsNotExist(err) {
|
|
|
|
|
log.Debug().Msgf("create workspace directory '%s' set by internal flag", CLIWorkaroundExecAtDir)
|
|
|
|
|
if err := os.Mkdir(state.workspaceDir, 0o700); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
} else if !stat.IsDir() {
|
|
|
|
|
//nolint:forbidigo
|
|
|
|
|
log.Fatal().Msg("This should never happen! internalExecDir was set to an non directory path!")
|
|
|
|
|
}
|
2023-05-17 14:53:23 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-01 16:58:37 +02:00
|
|
|
e.workflows.Store(taskUUID, state)
|
2023-05-17 14:53:23 +02:00
|
|
|
|
2022-03-10 15:07:02 -06:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-20 20:39:20 +02:00
|
|
|
func (e *local) StartStep(ctx context.Context, step *types.Step, taskUUID string) error {
|
|
|
|
|
log.Trace().Str("taskUUID", taskUUID).Msgf("start step %s", step.Name)
|
|
|
|
|
|
2025-10-01 16:58:37 +02:00
|
|
|
state, err := e.getWorkflowState(taskUUID)
|
2023-05-17 14:53:23 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-10 15:07:02 -06:00
|
|
|
// Get environment variables
|
2022-10-31 00:26:49 +01:00
|
|
|
env := os.Environ()
|
2022-10-28 21:08:53 +05:30
|
|
|
for a, b := range step.Environment {
|
2022-11-06 13:36:34 +01:00
|
|
|
// append allowed env vars to command env
|
|
|
|
|
if !slices.Contains(notAllowedEnvVarOverwrites, a) {
|
2022-10-31 00:26:49 +01:00
|
|
|
env = append(env, a+"="+b)
|
2022-03-10 15:07:02 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-23 12:54:10 +02:00
|
|
|
// Set HOME and CI_WORKSPACE
|
2023-05-17 14:53:23 +02:00
|
|
|
env = append(env, "HOME="+state.homeDir)
|
2023-10-01 13:56:49 +01:00
|
|
|
env = append(env, "USERPROFILE="+state.homeDir)
|
2023-10-23 12:54:10 +02:00
|
|
|
env = append(env, "CI_WORKSPACE="+state.workspaceDir)
|
2023-04-02 15:47:22 +01:00
|
|
|
|
2023-08-07 15:39:58 +02:00
|
|
|
switch step.Type {
|
|
|
|
|
case types.StepTypeClone:
|
|
|
|
|
return e.execClone(ctx, step, state, env)
|
|
|
|
|
case types.StepTypeCommands:
|
|
|
|
|
return e.execCommands(ctx, step, state, env)
|
2023-08-22 22:00:32 +02:00
|
|
|
case types.StepTypePlugin:
|
|
|
|
|
return e.execPlugin(ctx, step, state, env)
|
2023-08-07 15:39:58 +02:00
|
|
|
default:
|
|
|
|
|
return ErrUnsupportedStepType
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-11-06 13:36:34 +01:00
|
|
|
|
2023-07-20 20:39:20 +02:00
|
|
|
func (e *local) WaitStep(_ context.Context, step *types.Step, taskUUID string) (*types.State, error) {
|
|
|
|
|
log.Trace().Str("taskUUID", taskUUID).Msgf("wait for step %s", step.Name)
|
|
|
|
|
|
2025-10-01 16:58:37 +02:00
|
|
|
state, err := e.getStepState(taskUUID, step.UUID)
|
2023-05-17 14:53:23 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-01 16:58:37 +02:00
|
|
|
// normally we use cmd.Wait() to wait for *exec.Cmd, but cmd.StdoutPipe() tells us not
|
|
|
|
|
// as Wait() would close the io pipe even if not all logs where read and send back
|
|
|
|
|
// so we have to do use the underlying functions
|
|
|
|
|
if state.cmd.Process == nil {
|
|
|
|
|
return nil, errors.New("exec: not started")
|
2023-05-17 14:53:23 +02:00
|
|
|
}
|
2025-10-01 16:58:37 +02:00
|
|
|
if state.cmd.ProcessState == nil {
|
|
|
|
|
cmdState, err := state.cmd.Process.Wait()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
state.cmd.ProcessState = cmdState
|
2022-07-02 15:56:08 +02:00
|
|
|
}
|
2023-02-02 00:08:02 +01:00
|
|
|
|
2022-03-10 15:07:02 -06:00
|
|
|
return &types.State{
|
2022-07-02 15:56:08 +02:00
|
|
|
Exited: true,
|
2025-10-01 16:58:37 +02:00
|
|
|
ExitCode: state.cmd.ProcessState.ExitCode(),
|
2022-07-02 15:56:08 +02:00
|
|
|
}, err
|
2022-03-10 15:07:02 -06:00
|
|
|
}
|
|
|
|
|
|
2023-07-20 20:39:20 +02:00
|
|
|
func (e *local) TailStep(_ context.Context, step *types.Step, taskUUID string) (io.ReadCloser, error) {
|
2025-10-01 16:58:37 +02:00
|
|
|
state, err := e.getStepState(taskUUID, step.UUID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
} else if state.output == nil {
|
|
|
|
|
return nil, ErrStepReaderNotFound
|
|
|
|
|
}
|
|
|
|
|
return state.output, nil
|
2022-03-10 15:07:02 -06:00
|
|
|
}
|
|
|
|
|
|
2025-10-01 16:58:37 +02:00
|
|
|
func (e *local) DestroyStep(_ context.Context, step *types.Step, taskUUID string) error {
|
|
|
|
|
state, err := e.getStepState(taskUUID, step.UUID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// As WaitStep can not use cmd.Wait() witch ensures the process already finished and
|
|
|
|
|
// the io pipe is closed on process end, we make sure it is done.
|
|
|
|
|
_ = state.output.Close()
|
|
|
|
|
state.output = nil
|
|
|
|
|
_ = state.cmd.Cancel()
|
|
|
|
|
state.cmd = nil
|
|
|
|
|
workflowState, _ := e.getWorkflowState(taskUUID)
|
|
|
|
|
workflowState.stepState.Delete(step.UUID)
|
|
|
|
|
|
2023-11-01 09:35:11 +01:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-07 15:39:58 +02:00
|
|
|
func (e *local) DestroyWorkflow(_ context.Context, _ *types.Config, taskUUID string) error {
|
2024-01-10 20:57:12 +01:00
|
|
|
log.Trace().Str("taskUUID", taskUUID).Msg("delete workflow environment")
|
2023-07-20 20:39:20 +02:00
|
|
|
|
2025-10-01 16:58:37 +02:00
|
|
|
state, err := e.getWorkflowState(taskUUID)
|
2023-05-17 14:53:23 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-01 16:58:37 +02:00
|
|
|
// clean up steps not cleaned up because of context cancel or detached function
|
|
|
|
|
state.stepState.Range(func(_, value any) bool {
|
|
|
|
|
state, _ := value.(*stepState)
|
|
|
|
|
_ = state.output.Close()
|
|
|
|
|
state.output = nil
|
|
|
|
|
_ = state.cmd.Cancel()
|
|
|
|
|
state.cmd = nil
|
|
|
|
|
return true
|
|
|
|
|
})
|
|
|
|
|
|
2023-05-17 14:53:23 +02:00
|
|
|
err = os.RemoveAll(state.baseDir)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-01 16:58:37 +02:00
|
|
|
// hint for the gc to clean stuff
|
|
|
|
|
state.stepState.Clear()
|
|
|
|
|
e.workflows.Delete(taskUUID)
|
2023-05-17 14:53:23 +02:00
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-01 16:58:37 +02:00
|
|
|
func (e *local) getWorkflowState(taskUUID string) (*workflowState, error) {
|
2023-08-07 15:39:58 +02:00
|
|
|
state, ok := e.workflows.Load(taskUUID)
|
2023-05-17 14:53:23 +02:00
|
|
|
if !ok {
|
2023-08-07 15:39:58 +02:00
|
|
|
return nil, ErrWorkflowStateNotFound
|
2023-05-17 14:53:23 +02:00
|
|
|
}
|
2024-01-12 02:01:02 +01:00
|
|
|
|
|
|
|
|
s, ok := state.(*workflowState)
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, fmt.Errorf("could not parse state: %v", state)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return s, nil
|
2023-05-17 14:53:23 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-01 16:58:37 +02:00
|
|
|
func (e *local) getStepState(taskUUID, stepUUID string) (*stepState, error) {
|
|
|
|
|
wState, err := e.getWorkflowState(taskUUID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2023-05-17 14:53:23 +02:00
|
|
|
|
2025-10-01 16:58:37 +02:00
|
|
|
state, ok := wState.stepState.Load(stepUUID)
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, ErrStepStateNotFound
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s, ok := state.(*stepState)
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, fmt.Errorf("could not parse state: %v", state)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return s, nil
|
2022-03-10 15:07:02 -06:00
|
|
|
}
|