Skip to content

Commit

Permalink
Passing StepResults between Steps
Browse files Browse the repository at this point in the history
  • Loading branch information
chitrangpatel committed Dec 5, 2023
1 parent 30540fc commit d217d36
Show file tree
Hide file tree
Showing 7 changed files with 295 additions and 23 deletions.
34 changes: 34 additions & 0 deletions examples/v1/taskruns/alpha/stepaction-passing-results.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
apiVersion: tekton.dev/v1alpha1
kind: StepAction
metadata:
name: step-action
spec:
params:
- name: param1
type: array
image: bash:3.2
command: ["echo"]
args: [
"$(params.param1[*])",
]
---
apiVersion: tekton.dev/v1
kind: TaskRun
metadata:
name: step-action-run
spec:
TaskSpec:
steps:
- name: inline-step
results:
- name: result1
type: array
image: alpine
script: |
echo -n "[\"image1\", \"image2\", \"image3\"]" | tee $(step.results.result1.path)
- name: action-runner
ref:
name: step-action
params:
- name: param1
value: $(steps.inline-step.results.result1[*])
2 changes: 1 addition & 1 deletion pkg/apis/pipeline/v1/pipeline_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,7 @@ func taskContainsResult(resultExpression string, pipelineTaskNames sets.String,
for _, expression := range split {
if expression != "" {
value := stripVarSubExpression("$" + expression)
pipelineTaskName, _, _, _, err := parseExpression(value)
pipelineTaskName, _, _, _, _, err := parseExpression(value)

if err != nil {
return false
Expand Down
69 changes: 51 additions & 18 deletions pkg/apis/pipeline/v1/resultref.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ const (
// If a string result name contains a dot, brackets should be used to differentiate it from an object result.
// https://github.com/tektoncd/community/blob/main/teps/0075-object-param-and-result-types.md#collisions-with-builtin-variable-replacement
objectResultExpressionFormat = "tasks.<taskName>.results.<objectResultName>.<individualAttribute>"
// ResultStepPart Constant used to define the "steps" part of a step result reference
ResultStepPart = "steps"
// ResultTaskPart Constant used to define the "tasks" part of a pipeline result reference
ResultTaskPart = "tasks"
// ResultFinallyPart Constant used to define the "finally" part of a pipeline result reference
Expand Down Expand Up @@ -69,9 +71,9 @@ var arrayIndexingRegex = regexp.MustCompile(arrayIndexing)
func NewResultRefs(expressions []string) []*ResultRef {
var resultRefs []*ResultRef
for _, expression := range expressions {
pipelineTask, result, index, property, err := parseExpression(expression)
pipelineTask, result, index, property, _, err := parseTaskExpression(expression)
// If the expression isn't a result but is some other expression,
// parseExpression will return an error, in which case we just skip that expression,
// parseTaskExpression will return an error, in which case we just skip that expression,
// since although it's not a result ref, it might be some other kind of reference
if err == nil {
resultRefs = append(resultRefs, &ResultRef{
Expand Down Expand Up @@ -105,6 +107,13 @@ func looksLikeResultRef(expression string) bool {
return len(subExpressions) >= 4 && (subExpressions[0] == ResultTaskPart || subExpressions[0] == ResultFinallyPart) && subExpressions[2] == ResultResultPart
}

// looksLikeStepResultRef attempts to check if the given string looks like it contains any
// step result references. Returns true if it does, false otherwise
func looksLikeStepResultRef(expression string) bool {
subExpressions := strings.Split(expression, ".")
return len(subExpressions) >= 4 && subExpressions[0] == ResultStepPart && subExpressions[2] == ResultResultPart
}

func validateString(value string) []string {
expressions := VariableSubstitutionRegex.FindAllString(value, -1)
if expressions == nil {
Expand All @@ -121,38 +130,62 @@ func stripVarSubExpression(expression string) string {
return strings.TrimSuffix(strings.TrimPrefix(expression, "$("), ")")
}

// parseExpression parses "task name", "result name", "array index" (iff it's an array result) and "object key name" (iff it's an object result)
// parseExpression parses "task name" or "step name" , "result name", "array index" (iff it's an array result) and "object key name" (iff it's an object result)
// Valid Example 1:
// - Input: tasks.myTask.results.aStringResult
// - Output: "myTask", "aStringResult", -1, "", nil
// - Input: steps.myStep.results.aStringResult
// - Output: "myStep", "aStringResult", -1, "", nil
// Valid Example 2:
// - Input: tasks.myTask.results.anObjectResult.key1
// - Output: "myTask", "anObjectResult", 0, "key1", nil
// - Input: steps.myStep.results.anObjectResult.key1
// - Output: "myStep", "anObjectResult", 0, "key1", nil
// Valid Example 3:
// - Input: tasks.myTask.results.anArrayResult[1]
// - Output: "myTask", "anArrayResult", 1, "", nil
// - Input: steps.myStep.results.anArrayResult[1]
// - Output: "myStep", "anArrayResult", 1, "", nil
// Invalid Example 1:
// - Input: tasks.myTask.results.resultName.foo.bar
// - Output: "", "", 0, "", error
// - Input: steps.myStep.results.resultName.foo.bar
// - Output: "", "", 0, "", error
// TODO: may use regex for each type to handle possible reference formats
func parseExpression(substitutionExpression string) (string, string, int, string, error) {
if looksLikeResultRef(substitutionExpression) {
subExpressions := strings.Split(substitutionExpression, ".")
// For string result: tasks.<taskName>.results.<stringResultName>
// For array result: tasks.<taskName>.results.<arrayResultName>[index]
if len(subExpressions) == 4 {
resultName, stringIdx := ParseResultName(subExpressions[3])
if stringIdx != "" {
intIdx, _ := strconv.Atoi(stringIdx)
return subExpressions[1], resultName, intIdx, "", nil
func parseExpression(substitutionExpression string) (string, string, int, string, ParamType, error) {
subExpressions := strings.Split(substitutionExpression, ".")
// For string result: tasks.<taskName>.results.<stringResultName> or steps.<stepName>.results.<stringResultName>
// For array result: tasks.<taskName>.results.<arrayResultName>[index] or steps.<stepName>.results.<arrayResultName>[index]
if len(subExpressions) == 4 {
resultName, stringIdx := ParseResultName(subExpressions[3])
if stringIdx != "" {
if stringIdx == "*" {
return subExpressions[1], resultName, -1, "", ParamTypeArray, nil
}
return subExpressions[1], resultName, 0, "", nil
} else if len(subExpressions) == 5 {
// For object type result: tasks.<taskName>.results.<objectResultName>.<individualAttribute>
return subExpressions[1], subExpressions[3], 0, subExpressions[4], nil
intIdx, _ := strconv.Atoi(stringIdx)
return subExpressions[1], resultName, intIdx, "", ParamTypeArray, nil
}
return subExpressions[1], resultName, 0, "", ParamTypeString, nil
} else if len(subExpressions) == 5 {
// For object type result: tasks.<taskName>.results.<objectResultName>.<individualAttribute>
// or steps.<stepName>.results.<objectResultName>.<individualAttribute>
return subExpressions[1], subExpressions[3], 0, subExpressions[4], ParamTypeObject, nil
}
return "", "", 0, "", ParamTypeString, fmt.Errorf("must be one of the form 1). %q; 2). %q", resultExpressionFormat, objectResultExpressionFormat)
}

func parseTaskExpression(substitutionExpression string) (string, string, int, string, ParamType, error) {
if looksLikeResultRef(substitutionExpression) {
return parseExpression(substitutionExpression)
}
return "", "", 0, "", ParamTypeString, fmt.Errorf("must be one of the form 1). %q; 2). %q", resultExpressionFormat, objectResultExpressionFormat)
}

func ParseStepExpression(substitutionExpression string) (string, string, int, string, ParamType, error) {
if looksLikeStepResultRef(substitutionExpression) {
return parseExpression(substitutionExpression)
}
return "", "", 0, "", fmt.Errorf("must be one of the form 1). %q; 2). %q", resultExpressionFormat, objectResultExpressionFormat)
return "", "", 0, "", ParamTypeString, fmt.Errorf("must be one of the form 1). %q; 2). %q", resultExpressionFormat, objectResultExpressionFormat)
}

// ParseResultName parse the input string to extract resultName and result index.
Expand Down
120 changes: 120 additions & 0 deletions pkg/entrypoint/entrypointer.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ import (
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"syscall"
"time"

"github.com/tektoncd/pipeline/pkg/apis/config"
"github.com/tektoncd/pipeline/pkg/apis/pipeline"
v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
"github.com/tektoncd/pipeline/pkg/pod"
"github.com/tektoncd/pipeline/pkg/result"
"github.com/tektoncd/pipeline/pkg/spire"
Expand Down Expand Up @@ -182,6 +184,9 @@ func (e Entrypointer) Go() error {
ctx := context.Background()
var cancel context.CancelFunc
if err == nil {
if err := e.applyStepResultSubstitutions(); err != nil {
logger.Error("Error while substituting step results: ", err)
}
ctx, cancel = context.WithCancel(ctx)
if e.Timeout != nil && *e.Timeout > time.Duration(0) {
ctx, cancel = context.WithTimeout(ctx, *e.Timeout)
Expand Down Expand Up @@ -336,3 +341,118 @@ func (e Entrypointer) waitingCancellation(ctx context.Context, cancel context.Ca
cancel()
return nil
}

// loadStepResult reads the step result file and returns the string, array or object result value.
func loadStepResult(stepName string, resultName string) (v1.ResultValue, error) {
v := v1.ResultValue{}
fileContents, err := os.ReadFile(getStepResultPath(pod.GetContainerName(stepName), resultName))
if err != nil {
return v, err
}
err = v.UnmarshalJSON(fileContents)
if err != nil {
return v, err
}
return v, nil
}

// getStepResultPath gets the path to the step result
func getStepResultPath(stepName string, resultName string) string {
return filepath.Join(pipeline.StepsDir, stepName, "results", resultName)
}

func findReplacement(s string) (string, []string, error) {
value := strings.TrimSuffix(strings.TrimPrefix(s, "$("), ")")
stepName, resultName, arrayIdx, objectKey, paramType, err := v1.ParseStepExpression(value)
if err != nil {
return "", nil, err
}
result, err := loadStepResult(stepName, resultName)
if err != nil {
return "", nil, err
}
replaceWithArray := []string{}
replaceWithString := ""
if paramType == v1.ParamTypeObject && objectKey != "" {
replaceWithString = result.ObjectVal[objectKey]
} else if paramType == v1.ParamTypeArray {
if arrayIdx == -1 {
replaceWithArray = append(replaceWithArray, result.ArrayVal...)
} else {
replaceWithString = result.ArrayVal[arrayIdx]
}
} else {
replaceWithString = result.StringVal
}
return replaceWithString, replaceWithArray, nil
}

// applyStepResultSubstitutions applies the runtime step result substitutions in env.
func (e *Entrypointer) applyStepResultSubstitutions() error {
pattern := `\$\(steps\..*\.results\..*\)`
stepResultRegex := regexp.MustCompile(pattern)
// env
for _, e := range os.Environ() {
pair := strings.SplitN(e, "=", 2)
matches := stepResultRegex.FindAllStringSubmatch(pair[1], -1)
for _, m := range matches {
replaceWith, _, err := findReplacement(m[0])
if err != nil {
return err
}
os.Setenv(pair[0], strings.ReplaceAll(pair[1], m[0], replaceWith))
}
}
// script
if strings.HasPrefix(e.Command[0], "/tekton/scripts") {
fileContentBytes, err := os.ReadFile(e.Command[0])
if err != nil {
return err
}
fileContents := string(fileContentBytes)
matches := stepResultRegex.FindAllStringSubmatch(fileContents, -1)
if len(matches) > 0 {
for _, m := range matches {
replaceWith, _, err := findReplacement(m[0])
if err != nil {
return err
}
fileContents = strings.ReplaceAll(fileContents, m[0], replaceWith)
}
// copy the modified contents to a different file.
newFilePath := filepath.Join(e.StepMetadataDir, "script")
err := os.WriteFile(newFilePath, []byte(fileContents), 0755)
if err != nil {
return err
}
// set the command to execute the new file.
e.Command[0] = newFilePath
}
}
// command + args
newCommand := []string{}
for _, c := range e.Command {
matches := stepResultRegex.FindAllStringSubmatch(c, -1)
if len(matches) > 0 {
for _, m := range matches {
replaceWithString, replaceWithArray, err := findReplacement(m[0])
if err != nil {
return err
}
// if replacing with an array
if len(replaceWithArray) > 1 {
// append with the array items
newCommand = append(newCommand, replaceWithArray...)
} else {
// append with replaced string
c = strings.ReplaceAll(c, m[0], replaceWithString)
newCommand = append(newCommand, c)
}
}
} else {
newCommand = append(newCommand, c)
}
}
e.Command = newCommand
return nil
}
4 changes: 2 additions & 2 deletions pkg/pod/entrypoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,11 +353,11 @@ func TrimSidecarPrefix(name string) string { return strings.TrimPrefix(name, sid
// returns "step-unnamed-<step-index>" if not specified
func StepName(name string, i int) string {
if name != "" {
return getContainerName(name)
return GetContainerName(name)
}
return fmt.Sprintf("%sunnamed-%d", stepPrefix, i)
}

func getContainerName(name string) string {
func GetContainerName(name string) string {
return fmt.Sprintf("%s%s", stepPrefix, name)
}
4 changes: 2 additions & 2 deletions pkg/pod/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ func setTaskRunStatusBasedOnStepStatus(ctx context.Context, logger *zap.SugaredL
stepResults := []v1.StepResult{}
if ts != nil {
for _, step := range ts.Steps {
if getContainerName(step.Name) == s.Name {
if GetContainerName(step.Name) == s.Name {
stepResults = append(stepResults, step.Results...)
}
}
Expand Down Expand Up @@ -359,7 +359,7 @@ func findStepResultsFetchedByTask(containerName string, specResults []v1.TaskRes
return nil, err
}
// Only look at named results - referencing unnamed steps is unsupported.
if getContainerName(sName) == containerName {
if GetContainerName(sName) == containerName {
neededStepResults[resultName] = r.Name
}
}
Expand Down
Loading

0 comments on commit d217d36

Please sign in to comment.