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.
2019-04-06 21:32:14 +02:00
package exec
import (
"context"
2020-11-17 09:42:42 +02:00
"fmt"
2019-04-06 21:32:14 +02:00
"io"
2021-12-13 20:51:53 +02:00
"os"
2019-04-06 21:32:14 +02:00
"path"
"path/filepath"
"runtime"
"strings"
2019-09-14 14:21:16 +02:00
"github.com/drone/envsubst"
2024-09-24 22:49:36 +02:00
"github.com/oklog/ulid/v2"
2024-01-12 16:24:30 +02:00
"github.com/rs/zerolog/log"
2024-07-18 01:26:35 +02:00
"github.com/urfave/cli/v3"
2021-10-12 09:25:13 +02:00
2023-12-08 09:15:08 +02:00
"go.woodpecker-ci.org/woodpecker/v2/cli/common"
2024-07-03 16:22:09 +02:00
"go.woodpecker-ci.org/woodpecker/v2/cli/lint"
2023-12-08 09:15:08 +02:00
"go.woodpecker-ci.org/woodpecker/v2/pipeline"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/backend"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/docker"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/kubernetes"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/local"
2024-07-03 16:22:09 +02:00
backend_types "go.woodpecker-ci.org/woodpecker/v2/pipeline/backend/types"
2024-09-25 07:20:51 +02:00
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/metadata"
2023-12-08 09:15:08 +02:00
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/compiler"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/linter"
"go.woodpecker-ci.org/woodpecker/v2/pipeline/frontend/yaml/matrix"
2024-06-13 17:18:32 +02:00
pipelineLog "go.woodpecker-ci.org/woodpecker/v2/pipeline/log"
2024-09-01 20:41:10 +02:00
"go.woodpecker-ci.org/woodpecker/v2/shared/constant"
2023-12-08 09:15:08 +02:00
"go.woodpecker-ci.org/woodpecker/v2/shared/utils"
2019-04-06 21:32:14 +02:00
)
// Command exports the exec command.
2021-10-27 21:03:14 +02:00
var Command = & cli . Command {
2019-04-06 21:32:14 +02:00
Name : "exec" ,
2022-10-18 03:24:12 +02:00
Usage : "execute a local pipeline" ,
2023-04-29 10:12:36 +02:00
ArgsUsage : "[path/to/.woodpecker.yaml]" ,
2021-12-13 20:51:53 +02:00
Action : run ,
2023-12-08 10:36:53 +02:00
Flags : utils . MergeSlices ( flags , docker . Flags , kubernetes . Flags , local . Flags ) ,
2019-04-06 21:32:14 +02:00
}
2024-07-08 16:29:43 +02:00
var backends = [ ] backend_types . Backend {
kubernetes . New ( ) ,
docker . New ( ) ,
local . New ( ) ,
}
2024-07-18 01:26:35 +02:00
func run ( ctx context . Context , c * cli . Command ) error {
return common . RunPipelineFunc ( ctx , c , execFile , execDir )
2021-12-13 20:51:53 +02:00
}
2024-07-18 01:26:35 +02:00
func execDir ( ctx context . Context , c * cli . Command , dir string ) error {
2021-12-13 20:51:53 +02:00
// TODO: respect pipeline dependency
2024-03-21 01:19:48 +02:00
repoPath := c . String ( "repo-path" )
if repoPath != "" {
repoPath , _ = filepath . Abs ( repoPath )
} else {
repoPath , _ = filepath . Abs ( filepath . Dir ( dir ) )
}
2021-12-13 20:51:53 +02:00
if runtime . GOOS == "windows" {
repoPath = convertPathForWindows ( repoPath )
}
2024-09-25 07:20:51 +02:00
// TODO: respect depends_on and do parallel runs with output to multiple _windows_ e.g. tmux like
2021-12-13 20:51:53 +02:00
return filepath . Walk ( dir , func ( path string , info os . FileInfo , e error ) error {
if e != nil {
return e
}
// check if it is a regular file (not dir)
2023-04-29 10:12:36 +02:00
if info . Mode ( ) . IsRegular ( ) && ( strings . HasSuffix ( info . Name ( ) , ".yaml" ) || strings . HasSuffix ( info . Name ( ) , ".yml" ) ) {
2021-12-13 20:51:53 +02:00
fmt . Println ( "#" , info . Name ( ) )
2024-09-25 07:20:51 +02:00
_ = runExec ( ctx , c , path , repoPath , false ) // TODO: should we drop errors or store them and report back?
2021-12-13 20:51:53 +02:00
fmt . Println ( "" )
return nil
}
return nil
} )
}
2024-07-18 01:26:35 +02:00
func execFile ( ctx context . Context , c * cli . Command , file string ) error {
2024-03-21 01:19:48 +02:00
repoPath := c . String ( "repo-path" )
if repoPath != "" {
repoPath , _ = filepath . Abs ( repoPath )
} else {
repoPath , _ = filepath . Abs ( filepath . Dir ( file ) )
}
2021-12-13 20:51:53 +02:00
if runtime . GOOS == "windows" {
repoPath = convertPathForWindows ( repoPath )
2019-04-06 21:32:14 +02:00
}
2024-09-25 07:20:51 +02:00
return runExec ( ctx , c , file , repoPath , true )
2021-12-13 20:51:53 +02:00
}
2019-04-06 21:32:14 +02:00
2024-09-25 07:20:51 +02:00
func runExec ( ctx context . Context , c * cli . Command , file , repoPath string , singleExec bool ) error {
2022-08-25 08:39:19 +02:00
dat , err := os . ReadFile ( file )
2020-11-17 09:42:42 +02:00
if err != nil {
return err
}
axes , err := matrix . ParseString ( string ( dat ) )
if err != nil {
2023-12-29 22:19:42 +02:00
return fmt . Errorf ( "parse matrix fail" )
2020-11-17 09:42:42 +02:00
}
if len ( axes ) == 0 {
axes = append ( axes , matrix . Axis { } )
}
for _ , axis := range axes {
2024-09-25 07:20:51 +02:00
err := execWithAxis ( ctx , c , file , repoPath , axis , singleExec )
2020-11-17 09:42:42 +02:00
if err != nil {
return err
}
}
return nil
}
2024-09-25 07:20:51 +02:00
func execWithAxis ( ctx context . Context , c * cli . Command , file , repoPath string , axis matrix . Axis , singleExec bool ) error {
2024-10-09 12:17:23 +02:00
metadataWorkflow := & metadata . Workflow { }
2024-09-25 07:20:51 +02:00
if ! singleExec {
// TODO: proper try to use the engine to generate the same metadata for workflows
// https://github.com/woodpecker-ci/woodpecker/pull/3967
metadataWorkflow . Name = strings . TrimSuffix ( strings . TrimSuffix ( file , ".yaml" ) , ".yml" )
}
metadata , err := metadataFromContext ( ctx , c , axis , metadataWorkflow )
2024-09-16 22:03:24 +02:00
if err != nil {
return fmt . Errorf ( "could not create metadata: %w" , err )
2024-09-25 07:20:51 +02:00
} else if metadata == nil {
return fmt . Errorf ( "metadata is nil" )
2024-09-16 22:03:24 +02:00
}
2024-09-25 07:20:51 +02:00
2019-04-06 21:32:14 +02:00
environ := metadata . Environ ( )
2021-09-24 16:29:26 +02:00
var secrets [ ] compiler . Secret
2023-04-08 13:15:28 +02:00
for key , val := range metadata . Workflow . Matrix {
2019-04-06 21:32:14 +02:00
environ [ key ] = val
secrets = append ( secrets , compiler . Secret {
Name : key ,
Value : val ,
} )
}
2024-01-12 16:24:30 +02:00
pipelineEnv := make ( map [ string ] string )
2019-04-06 21:32:14 +02:00
for _ , env := range c . StringSlice ( "env" ) {
2024-03-15 19:00:25 +02:00
before , after , _ := strings . Cut ( env , "=" )
pipelineEnv [ before ] = after
if oldVar , exists := environ [ before ] ; exists {
2024-01-12 16:24:30 +02:00
// override existing values, but print a warning
2024-03-15 19:00:25 +02:00
log . Warn ( ) . Msgf ( "environment variable '%s' had value '%s', but got overwritten" , before , oldVar )
2022-07-30 08:06:03 +02:00
}
2024-03-15 19:00:25 +02:00
environ [ before ] = after
2019-04-06 21:32:14 +02:00
}
tmpl , err := envsubst . ParseFile ( file )
if err != nil {
return err
}
2024-05-24 22:35:04 +02:00
confStr , err := tmpl . Execute ( func ( name string ) string {
2019-04-06 21:32:14 +02:00
return environ [ name ]
} )
if err != nil {
return err
}
2024-05-24 22:35:04 +02:00
conf , err := yaml . ParseString ( confStr )
2019-04-06 21:32:14 +02:00
if err != nil {
return err
}
2024-09-24 22:49:36 +02:00
// emulate server behavior https://github.com/woodpecker-ci/woodpecker/blob/eebaa10d104cbc3fa7ce4c0e344b0b7978405135/server/pipeline/stepbuilder/stepBuilder.go#L289-L295
prefix := "wp_" + ulid . Make ( ) . String ( )
2019-04-06 21:32:14 +02:00
// configure volumes for local execution
volumes := c . StringSlice ( "volumes" )
if c . Bool ( "local" ) {
var (
workspaceBase = conf . Workspace . Base
workspacePath = conf . Workspace . Path
)
if workspaceBase == "" {
workspaceBase = c . String ( "workspace-base" )
}
if workspacePath == "" {
workspacePath = c . String ( "workspace-path" )
}
2024-09-24 22:49:36 +02:00
volumes = append ( volumes , prefix + "_default:" + workspaceBase )
2021-12-13 20:51:53 +02:00
volumes = append ( volumes , repoPath + ":" + path . Join ( workspaceBase , workspacePath ) )
2019-04-06 21:32:14 +02:00
}
2024-09-02 10:41:20 +02:00
privilegedPlugins := c . StringSlice ( "plugins-privileged" )
2019-04-06 21:32:14 +02:00
// lint the yaml file
2024-09-01 20:41:10 +02:00
err = linter . New (
2024-11-01 22:37:31 +02:00
linter . WithTrusted ( linter . TrustedConfiguration {
Security : c . Bool ( "repo-trusted-security" ) ,
Network : c . Bool ( "repo-trusted-network" ) ,
Volumes : c . Bool ( "repo-trusted-volumes" ) ,
} ) ,
2024-09-02 10:41:20 +02:00
linter . PrivilegedPlugins ( privilegedPlugins ) ,
2024-09-01 20:41:10 +02:00
linter . WithTrustedClonePlugins ( constant . TrustedClonePlugins ) ,
) . Lint ( [ ] * linter . WorkflowConfig { {
2023-11-04 16:30:47 +02:00
File : path . Base ( file ) ,
2024-05-24 22:35:04 +02:00
RawConfig : confStr ,
2023-11-04 16:30:47 +02:00
Workflow : conf ,
2024-07-03 16:22:09 +02:00
} } )
if err != nil {
2024-11-13 17:28:02 +02:00
str , err := lint . FormatLintError ( file , err , false )
2024-07-03 16:22:09 +02:00
fmt . Print ( str )
if err != nil {
return err
}
2019-04-06 21:32:14 +02:00
}
// compiles the yaml file
2022-10-06 01:49:23 +02:00
compiled , err := compiler . New (
2019-04-06 21:32:14 +02:00
compiler . WithEscalated (
2024-09-02 10:41:20 +02:00
privilegedPlugins ... ,
2019-04-06 21:32:14 +02:00
) ,
compiler . WithVolumes ( volumes ... ) ,
compiler . WithWorkspace (
c . String ( "workspace-base" ) ,
c . String ( "workspace-path" ) ,
) ,
compiler . WithNetworks (
c . StringSlice ( "network" ) ... ,
) ,
2024-09-24 22:49:36 +02:00
compiler . WithPrefix ( prefix ) ,
2023-08-07 21:13:26 +02:00
compiler . WithProxy ( compiler . ProxyOptions {
NoProxy : c . String ( "backend-no-proxy" ) ,
HTTPProxy : c . String ( "backend-http-proxy" ) ,
HTTPSProxy : c . String ( "backend-https-proxy" ) ,
} ) ,
2019-04-06 21:32:14 +02:00
compiler . WithLocal (
c . Bool ( "local" ) ,
) ,
compiler . WithNetrc (
c . String ( "netrc-username" ) ,
c . String ( "netrc-password" ) ,
c . String ( "netrc-machine" ) ,
) ,
2024-09-25 07:20:51 +02:00
compiler . WithMetadata ( * metadata ) ,
2019-04-06 21:32:14 +02:00
compiler . WithSecret ( secrets ... ) ,
2024-01-12 16:24:30 +02:00
compiler . WithEnviron ( pipelineEnv ) ,
2019-04-06 21:32:14 +02:00
) . Compile ( conf )
2022-10-06 01:49:23 +02:00
if err != nil {
return err
}
2022-02-26 04:02:42 +02:00
2024-07-18 01:26:35 +02:00
backendCtx := context . WithValue ( ctx , backend_types . CliCommand , c )
2024-02-09 01:04:43 +02:00
backendEngine , err := backend . FindBackend ( backendCtx , backends , c . String ( "backend-engine" ) )
2022-02-26 04:02:42 +02:00
if err != nil {
return err
}
2023-12-14 20:20:47 +02:00
if _ , err = backendEngine . Load ( backendCtx ) ; err != nil {
2019-04-06 21:32:14 +02:00
return err
}
2024-07-18 01:26:35 +02:00
pipelineCtx , cancel := context . WithTimeout ( context . Background ( ) , c . Duration ( "timeout" ) )
2019-04-06 21:32:14 +02:00
defer cancel ( )
2024-07-18 01:26:35 +02:00
pipelineCtx = utils . WithContextSigtermCallback ( pipelineCtx , func ( ) {
fmt . Printf ( "ctrl+c received, terminating current pipeline '%s'\n" , confStr )
2022-02-28 10:27:31 +02:00
} )
2019-04-06 21:32:14 +02:00
return pipeline . New ( compiled ,
2024-07-18 01:26:35 +02:00
pipeline . WithContext ( pipelineCtx ) , //nolint:contextcheck
2019-04-06 21:32:14 +02:00
pipeline . WithTracer ( pipeline . DefaultTracer ) ,
pipeline . WithLogger ( defaultLogger ) ,
2023-12-14 20:20:47 +02:00
pipeline . WithBackend ( backendEngine ) ,
2022-06-15 18:11:20 +02:00
pipeline . WithDescription ( map [ string ] string {
"CLI" : "exec" ,
} ) ,
2024-07-18 01:26:35 +02:00
) . Run ( ctx )
2019-04-06 21:32:14 +02:00
}
2024-03-15 19:00:25 +02:00
// convertPathForWindows converts a path to use slash separators
// for Windows. If the path is a Windows volume name like C:, it
// converts it to an absolute root path starting with slash (e.g.
// C: -> /c). Otherwise it just converts backslash separators to
// slashes.
2019-04-06 21:32:14 +02:00
func convertPathForWindows ( path string ) string {
base := filepath . VolumeName ( path )
2024-03-15 19:00:25 +02:00
// Check if path is volume name like C:
2024-05-13 22:58:21 +02:00
//nolint:mnd
2019-04-06 21:32:14 +02:00
if len ( base ) == 2 {
path = strings . TrimPrefix ( path , base )
base = strings . ToLower ( strings . TrimSuffix ( base , ":" ) )
return "/" + base + filepath . ToSlash ( path )
}
return filepath . ToSlash ( path )
}
2024-07-03 16:22:09 +02:00
var defaultLogger = pipeline . Logger ( func ( step * backend_types . Step , rc io . ReadCloser ) error {
2024-06-13 17:18:32 +02:00
logWriter := NewLineWriter ( step . Name , step . UUID )
2024-08-25 22:53:04 +02:00
return pipelineLog . CopyLineByLine ( logWriter , rc , pipeline . MaxLogLineLength )
2019-04-06 21:32:14 +02:00
} )