cmd/run/run.go (204 lines of code) (raw):
package run
import (
"bytes"
ctx "context"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/spf13/cobra"
"google.golang.org/protobuf/encoding/prototext"
"gopkg.in/yaml.v3"
"gitlab.com/gitlab-org/step-runner/pkg/di"
"gitlab.com/gitlab-org/step-runner/pkg/report"
"gitlab.com/gitlab-org/step-runner/pkg/runner"
"gitlab.com/gitlab-org/step-runner/proto"
"gitlab.com/gitlab-org/step-runner/schema/v1"
)
type Options struct {
Step string
OCIRegistry string
OCIRepository string
OCITag string
OCIDir string
OCIFilename string
GitURL string
GitRev string
GitDir string
Inputs map[string]string
Env map[string]string
Job map[string]string
TextProtoStepFile string
WriteStepResults bool
StepResultsFile string
StepResultsFormat string
}
func NewCmd() *cobra.Command {
options := &Options{}
cmd := &cobra.Command{
Use: "run [local or remote step, step starting with 'step: [location]', or omit if using git flags]",
Short: "Run a step locally",
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) > 0 {
options.Step = args[0]
}
return run(options)
},
}
cmd.Flags().StringVar(&options.OCIRegistry, "oci-registry", "", "oci registry of step, <host>[:<port>]")
cmd.Flags().StringVar(&options.OCIRepository, "oci-repository", "", "oci repository of step")
cmd.Flags().StringVar(&options.OCITag, "oci-tag", "", "oci tag of step")
cmd.Flags().StringVar(&options.OCIDir, "oci-dir", "", "oci dir of step")
cmd.Flags().StringVar(&options.OCIFilename, "oci-filename", "", "oci filename of step yaml")
cmd.Flags().StringVar(&options.GitURL, "git-url", "", "git url of step")
cmd.Flags().StringVar(&options.GitRev, "git-rev", "", "git revision of step")
cmd.Flags().StringVar(&options.GitDir, "git-dir", "", "git directory of step")
cmd.Flags().StringToStringVar(&options.Inputs, "inputs", make(map[string]string), "provide inputs to step")
cmd.Flags().StringToStringVar(&options.Env, "env", make(map[string]string), "provide environment to step")
cmd.Flags().StringToStringVar(&options.Job, "job", make(map[string]string), "provide job variables to step")
cmd.Flags().StringVar(&options.TextProtoStepFile, "text-proto-step-file", "", "file containing a text protobuf definition of a step")
defaultWriteStepsFile, _ := strconv.ParseBool(os.Getenv("CI_STEPS_DEBUG"))
cmd.Flags().BoolVar(&options.WriteStepResults, "write-steps-results", defaultWriteStepsFile, "write step-results.json file, note this file may contain secrets")
cmd.Flags().StringVar(&options.StepResultsFile, "step-results-file", "", "file to write step results")
cmd.Flags().StringVar(&options.StepResultsFormat, "step-results-format", "json", "format in which to write step results (`json` or `prototext`)")
return cmd
}
func run(options *Options) error {
var specDef *runner.SpecDefinition
if options.TextProtoStepFile != "" {
data, err := os.ReadFile(options.TextProtoStepFile)
if err != nil {
return err
}
defn := &proto.Definition{}
err = prototext.Unmarshal(data, defn)
if err != nil {
return err
}
spec := &proto.Spec{Spec: &proto.Spec_Content{}}
dir := filepath.Dir(options.TextProtoStepFile)
specDef = runner.NewSpecDefinition(spec, defn, dir)
} else {
yml, err := yamlStep(options)
if err != nil {
return err
}
def, err := wrapStepsInSingleStep(yml)
if err != nil {
return err
}
protoDef, err := def.Compile()
if err != nil {
return err
}
spec := &proto.Spec{Spec: &proto.Spec_Content{}}
specDef = runner.NewSpecDefinition(spec, protoDef, "")
}
diContainer := di.NewContainer()
globalCtx, err := createGlobalCtx(options)
if err != nil {
return err
}
stepParser, err := diContainer.StepParser()
if err != nil {
return err
}
params := &runner.Params{}
step, err := stepParser.Parse(globalCtx, specDef, params, runner.StepDefinedInGitLabJob)
if err != nil {
return err
}
inputs := params.NewInputsWithDefault(specDef.SpecInputs())
stepsCtx, err := runner.NewStepsContext(globalCtx, specDef.Dir(), inputs, globalCtx.Env())
if err != nil {
return err
}
defer stepsCtx.Cleanup()
result, err := step.Run(ctx.Background(), stepsCtx)
if options.WriteStepResults || options.StepResultsFile != "" {
reptErr := report.NewStepResultReport(
options.StepResultsFile,
report.Format(options.StepResultsFormat),
).Write(result)
if reptErr != nil {
fmt.Println(reptErr)
}
}
return err
}
func createGlobalCtx(options *Options) (*runner.GlobalContext, error) {
env, err := runner.NewEnvironmentFromOS(excludeJobVars)
if err != nil {
return nil, err
}
env, err = runner.GlobalEnvironment(env, options.Job)
if err != nil {
return nil, err
}
workDir, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("failed to create work dir: %w", err)
}
globalCtx := runner.NewGlobalContext(workDir, options.Job, env, os.Stdout, os.Stderr)
return globalCtx, nil
}
func excludeJobVars(envName string) bool {
return strings.HasPrefix(envName, "CI_") ||
strings.HasPrefix(envName, "GITLAB_") ||
strings.HasPrefix(envName, "FF_") ||
strings.HasPrefix(envName, "DOCKER_ENV_")
}
func wrapStepsInSingleStep(ymlSteps []byte) (*schema.Step, error) {
def := &schema.Step{}
err := yaml.Unmarshal(ymlSteps, &def.Run)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal step: %w", err)
}
return def, nil
}
func yamlStep(options *Options) ([]byte, error) {
yml := bytes.NewBufferString("")
if strings.HasPrefix(options.Step, "step:") {
yml.WriteString(fmt.Sprintf("- %s\n", options.Step))
} else if options.Step != "" {
yml.WriteString(fmt.Sprintf("- step: %s\n", options.Step))
} else if options.GitURL != "" {
yml.WriteString("- step:\n")
yml.WriteString(" git:\n")
yml.WriteString(fmt.Sprintf(" url: %s\n", options.GitURL))
if options.GitRev != "" {
yml.WriteString(fmt.Sprintf(" rev: %s\n", options.GitRev))
}
if options.GitDir != "" {
yml.WriteString(fmt.Sprintf(" dir: %s\n", options.GitDir))
}
} else if options.OCIRegistry != "" {
yml.WriteString("- step:\n")
yml.WriteString(" oci:\n")
yml.WriteString(fmt.Sprintf(" registry: %s\n", options.OCIRegistry))
yml.WriteString(fmt.Sprintf(" repository: %s\n", options.OCIRepository))
yml.WriteString(fmt.Sprintf(" tag: %s\n", options.OCITag))
if options.OCIDir != "" {
yml.WriteString(fmt.Sprintf(" dir: %s\n", options.OCIDir))
}
if options.OCIFilename != "" {
yml.WriteString(fmt.Sprintf(" file: %s\n", options.OCIFilename))
}
} else {
return nil, fmt.Errorf("no step specified")
}
yml.WriteString(yamlObject("inputs", options.Inputs))
yml.WriteString(yamlObject("env", options.Env))
return yml.Bytes(), nil
}
func yamlObject(name string, values map[string]string) string {
if len(values) == 0 {
return ""
}
yml := bytes.NewBufferString(fmt.Sprintf(" %s:\n", name))
for name, value := range values {
yml.WriteString(fmt.Sprintf(" %s: %s\n", name, value))
}
return yml.String()
}