2022-10-18 01:24:12 +00: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 21:07:02 +00:00
|
|
|
package local
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2023-02-01 23:08:02 +00:00
|
|
|
"errors"
|
2022-11-06 12:36:34 +00:00
|
|
|
"fmt"
|
2022-03-10 21:07:02 +00:00
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
2023-04-02 14:47:22 +00:00
|
|
|
"path/filepath"
|
2022-03-10 21:07:02 +00:00
|
|
|
"strings"
|
|
|
|
|
2022-11-06 12:36:34 +00:00
|
|
|
"github.com/alessio/shellescape"
|
|
|
|
"golang.org/x/exp/slices"
|
|
|
|
|
2022-03-10 21:07:02 +00:00
|
|
|
"github.com/woodpecker-ci/woodpecker/pipeline/backend/types"
|
2022-04-06 13:30:49 +00:00
|
|
|
"github.com/woodpecker-ci/woodpecker/shared/constant"
|
2022-03-10 21:07:02 +00:00
|
|
|
)
|
|
|
|
|
2022-11-06 12:36:34 +00:00
|
|
|
// notAllowedEnvVarOverwrites are all env vars that can not be overwritten by step config
|
|
|
|
var notAllowedEnvVarOverwrites = []string{
|
|
|
|
"CI_NETRC_MACHINE",
|
|
|
|
"CI_NETRC_USERNAME",
|
|
|
|
"CI_NETRC_PASSWORD",
|
|
|
|
"CI_SCRIPT",
|
|
|
|
"HOME",
|
|
|
|
"SHELL",
|
|
|
|
}
|
|
|
|
|
2023-05-17 12:53:23 +00:00
|
|
|
type workflowState struct {
|
|
|
|
stepCMDs map[string]*exec.Cmd
|
|
|
|
baseDir string
|
|
|
|
homeDir string
|
|
|
|
workspaceDir string
|
|
|
|
}
|
|
|
|
|
2022-03-10 21:07:02 +00:00
|
|
|
type local struct {
|
2023-05-17 12:53:23 +00:00
|
|
|
workflows map[string]*workflowState
|
|
|
|
output io.ReadCloser
|
2022-03-10 21:07:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// New returns a new local Engine.
|
|
|
|
func New() types.Engine {
|
2023-05-17 12:53:23 +00:00
|
|
|
return &local{
|
|
|
|
workflows: make(map[string]*workflowState),
|
|
|
|
}
|
2022-03-10 21:07:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (e *local) Name() string {
|
|
|
|
return "local"
|
|
|
|
}
|
|
|
|
|
2023-03-19 19:24:43 +00:00
|
|
|
func (e *local) IsAvailable(context.Context) bool {
|
2022-03-10 21:07:02 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2023-03-19 19:24:43 +00:00
|
|
|
func (e *local) Load(context.Context) error {
|
2023-05-17 12:53:23 +00:00
|
|
|
// TODO: download plugin-git binary if not exist
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Setup the pipeline environment.
|
|
|
|
func (e *local) Setup(_ context.Context, c *types.Config) error {
|
|
|
|
baseDir, err := os.MkdirTemp("", "woodpecker-local-*")
|
2023-04-19 22:56:03 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-05-17 12:53:23 +00:00
|
|
|
state := &workflowState{
|
|
|
|
stepCMDs: make(map[string]*exec.Cmd),
|
|
|
|
baseDir: baseDir,
|
|
|
|
workspaceDir: filepath.Join(baseDir, "workspace"),
|
|
|
|
homeDir: filepath.Join(baseDir, "home"),
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := os.Mkdir(state.homeDir, 0o700); err != nil {
|
2023-04-19 22:56:03 +00:00
|
|
|
return err
|
|
|
|
}
|
2022-03-10 21:07:02 +00:00
|
|
|
|
2023-05-17 12:53:23 +00:00
|
|
|
if err := os.Mkdir(state.workspaceDir, 0o700); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: copy plugin-git binary to homeDir and set PATH
|
|
|
|
|
|
|
|
workflowID, err := e.getWorkflowIDFromConfig(c)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
e.workflows[workflowID] = state
|
|
|
|
|
2022-03-10 21:07:02 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Exec the pipeline step.
|
2022-10-28 15:38:53 +00:00
|
|
|
func (e *local) Exec(ctx context.Context, step *types.Step) error {
|
2023-05-17 12:53:23 +00:00
|
|
|
state, err := e.getWorkflowStateFromStep(step)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-03-10 21:07:02 +00:00
|
|
|
// Get environment variables
|
2022-10-30 23:26:49 +00:00
|
|
|
env := os.Environ()
|
2022-10-28 15:38:53 +00:00
|
|
|
for a, b := range step.Environment {
|
2022-11-06 12:36:34 +00:00
|
|
|
// append allowed env vars to command env
|
|
|
|
if !slices.Contains(notAllowedEnvVarOverwrites, a) {
|
2022-10-30 23:26:49 +00:00
|
|
|
env = append(env, a+"="+b)
|
2022-03-10 21:07:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-02 14:47:22 +00:00
|
|
|
// Set HOME
|
2023-05-17 12:53:23 +00:00
|
|
|
env = append(env, "HOME="+state.homeDir)
|
2023-04-02 14:47:22 +00:00
|
|
|
|
2022-11-05 12:44:33 +00:00
|
|
|
var command []string
|
2022-10-28 15:38:53 +00:00
|
|
|
if step.Image == constant.DefaultCloneImage {
|
2022-03-10 21:07:02 +00:00
|
|
|
// Default clone step
|
2023-04-02 14:47:22 +00:00
|
|
|
// TODO: use tmp HOME and insert netrc and delete it after clone
|
2023-05-17 12:53:23 +00:00
|
|
|
env = append(env, "CI_WORKSPACE="+state.workspaceDir)
|
2022-10-30 23:26:49 +00:00
|
|
|
command = append(command, "plugin-git")
|
2022-03-10 21:07:02 +00:00
|
|
|
} else {
|
|
|
|
// Use "image name" as run command
|
2022-10-30 23:26:49 +00:00
|
|
|
command = append(command, step.Image)
|
|
|
|
command = append(command, "-c")
|
2022-03-10 21:07:02 +00:00
|
|
|
|
2022-10-30 23:26:49 +00:00
|
|
|
// TODO: use commands directly
|
2022-11-06 12:36:34 +00:00
|
|
|
script := ""
|
|
|
|
for _, cmd := range step.Commands {
|
|
|
|
script += fmt.Sprintf("echo + %s\n%s\n\n", shellescape.Quote(cmd), cmd)
|
|
|
|
}
|
|
|
|
script = strings.TrimSpace(script)
|
|
|
|
|
2022-03-10 21:07:02 +00:00
|
|
|
// Deleting the initial lines removes netrc support but adds compatibility for more shells like fish
|
2022-11-06 12:36:34 +00:00
|
|
|
command = append(command, script)
|
2022-03-10 21:07:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Prepare command
|
2023-05-17 12:53:23 +00:00
|
|
|
cmd := exec.CommandContext(ctx, command[0], command[1:]...)
|
|
|
|
cmd.Env = env
|
|
|
|
cmd.Dir = state.workspaceDir
|
2022-03-10 21:07:02 +00:00
|
|
|
|
|
|
|
// Get output and redirect Stderr to Stdout
|
2023-05-17 12:53:23 +00:00
|
|
|
e.output, _ = cmd.StdoutPipe()
|
|
|
|
cmd.Stderr = cmd.Stdout
|
2022-03-10 21:07:02 +00:00
|
|
|
|
2023-05-17 12:53:23 +00:00
|
|
|
state.stepCMDs[step.Name] = cmd
|
|
|
|
|
|
|
|
return cmd.Start()
|
2022-03-10 21:07:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Wait for the pipeline step to complete and returns
|
|
|
|
// the completion results.
|
2023-05-17 12:53:23 +00:00
|
|
|
func (e *local) Wait(_ context.Context, step *types.Step) (*types.State, error) {
|
|
|
|
state, err := e.getWorkflowStateFromStep(step)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
cmd, ok := state.stepCMDs[step.Name]
|
|
|
|
if !ok {
|
|
|
|
return nil, fmt.Errorf("step cmd %s not found", step.Name)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = cmd.Wait()
|
2022-07-02 13:56:08 +00:00
|
|
|
ExitCode := 0
|
2023-02-01 23:08:02 +00:00
|
|
|
|
|
|
|
var execExitError *exec.ExitError
|
|
|
|
if errors.As(err, &execExitError) {
|
|
|
|
ExitCode = execExitError.ExitCode()
|
2022-10-18 01:24:12 +00:00
|
|
|
// Non-zero exit code is a pipeline failure, but not an agent error.
|
2022-07-02 13:56:08 +00:00
|
|
|
err = nil
|
|
|
|
}
|
2023-02-01 23:08:02 +00:00
|
|
|
|
2022-03-10 21:07:02 +00:00
|
|
|
return &types.State{
|
2022-07-02 13:56:08 +00:00
|
|
|
Exited: true,
|
|
|
|
ExitCode: ExitCode,
|
|
|
|
}, err
|
2022-03-10 21:07:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Tail the pipeline step logs.
|
|
|
|
func (e *local) Tail(context.Context, *types.Step) (io.ReadCloser, error) {
|
|
|
|
return e.output, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Destroy the pipeline environment.
|
2023-05-17 12:53:23 +00:00
|
|
|
func (e *local) Destroy(_ context.Context, c *types.Config) error {
|
|
|
|
state, err := e.getWorkflowStateFromConfig(c)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = os.RemoveAll(state.baseDir)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
workflowID, err := e.getWorkflowIDFromConfig(c)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
delete(e.workflows, workflowID)
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *local) getWorkflowIDFromStep(step *types.Step) (string, error) {
|
2023-06-08 01:33:23 +00:00
|
|
|
sep := "_step_"
|
|
|
|
if strings.Contains(step.Name, sep) {
|
|
|
|
prefix := strings.Split(step.Name, sep)
|
|
|
|
if len(prefix) == 2 {
|
|
|
|
return prefix[0], nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
sep = "_clone"
|
|
|
|
if strings.Contains(step.Name, sep) {
|
|
|
|
prefix := strings.Split(step.Name, sep)
|
|
|
|
if len(prefix) == 2 {
|
|
|
|
return prefix[0], nil
|
|
|
|
}
|
2023-05-17 12:53:23 +00:00
|
|
|
}
|
|
|
|
|
2023-06-08 01:33:23 +00:00
|
|
|
return "", fmt.Errorf("invalid step name (%s) %s", sep, step.Name)
|
2023-05-17 12:53:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (e *local) getWorkflowIDFromConfig(c *types.Config) (string, error) {
|
|
|
|
if len(c.Volumes) < 1 {
|
|
|
|
return "", fmt.Errorf("no volumes found in config")
|
|
|
|
}
|
|
|
|
|
|
|
|
prefix := strings.Replace(c.Volumes[0].Name, "_default", "", 1)
|
|
|
|
return prefix, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *local) getWorkflowStateFromConfig(c *types.Config) (*workflowState, error) {
|
|
|
|
workflowID, err := e.getWorkflowIDFromConfig(c)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
state, ok := e.workflows[workflowID]
|
|
|
|
if !ok {
|
|
|
|
return nil, fmt.Errorf("workflow %s not found", workflowID)
|
|
|
|
}
|
|
|
|
|
|
|
|
return state, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *local) getWorkflowStateFromStep(step *types.Step) (*workflowState, error) {
|
|
|
|
workflowID, err := e.getWorkflowIDFromStep(step)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
state, ok := e.workflows[workflowID]
|
|
|
|
if !ok {
|
|
|
|
return nil, fmt.Errorf("workflow %s not found", workflowID)
|
|
|
|
}
|
|
|
|
|
|
|
|
return state, nil
|
2022-03-10 21:07:02 +00:00
|
|
|
}
|