Skip to content

Commit

Permalink
TEP-0142: Surface step results via sidecar logs
Browse files Browse the repository at this point in the history
Prior to this, we enabled surfacing step results via termination message. This PR does the same thing via sidecar logs.
  • Loading branch information
chitrangpatel authored and tekton-robot committed Nov 24, 2023
1 parent b395663 commit 30540fc
Show file tree
Hide file tree
Showing 9 changed files with 512 additions and 57 deletions.
9 changes: 8 additions & 1 deletion cmd/sidecarlogresults/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package main

import (
"encoding/json"
"flag"
"log"
"os"
Expand All @@ -30,14 +31,20 @@ import (
func main() {
var resultsDir string
var resultNames string
var stepResultsStr string
flag.StringVar(&resultsDir, "results-dir", pipeline.DefaultResultPath, "Path to the results directory. Default is /tekton/results")
flag.StringVar(&resultNames, "result-names", "", "comma separated result names to expect from the steps running in the pod. eg. foo,bar,baz")
flag.StringVar(&stepResultsStr, "step-results", "", "json containing a map of step Name as key and list of result Names. eg. {\"stepName\":[\"foo\",\"bar\",\"baz\"]}")
flag.Parse()
if resultNames == "" {
log.Fatal("result-names were not provided")
}
expectedResults := strings.Split(resultNames, ",")
err := sidecarlogresults.LookForResults(os.Stdout, pod.RunDir, resultsDir, expectedResults)
expectedStepResults := map[string][]string{}
if err := json.Unmarshal([]byte(stepResultsStr), &expectedStepResults); err != nil {
log.Fatal(err)
}
err := sidecarlogresults.LookForResults(os.Stdout, pod.RunDir, resultsDir, expectedResults, pipeline.StepsDir, expectedStepResults)
if err != nil {
log.Fatal(err)
}
Expand Down
5 changes: 0 additions & 5 deletions docs/stepactions.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ weight: 201
- [Specifying Remote StepActions](#specifying-remote-stepactions)
- [Known Limitations](#known-limitations)
- [Cannot pass Step Results between Steps](#cannot-pass-step-results-between-steps)
- [Cannot extract Step Results via Sidecar logs](#cannot-extract-step-results-via-sidecar-logs)

## Overview
:warning: This feature is in a preview mode.
Expand Down Expand Up @@ -430,7 +429,3 @@ The default resolver type can be configured by the `default-resolver-type` field
### Cannot pass Step Results between Steps

It's not currently possible to pass results produced by a `Step` into following `Steps`. We are working on this feature and will be made available soon.

### Cannot extract Step Results via Sidecar logs

Currently, we only support Step Results via `Termination Message` (the default method of result extraction). The ability to extract `Step` results from `sidecar-logs` is not yet available. We are working on enabling this soon.
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ spec:
image: alpine
results:
- name: result1
- name: result2
script: |
echo "I am a Step Action!!!" >> $(step.results.result1.path)
echo "I am a hidden step action!!!" >> $(step.results.result2.path)
---
apiVersion: tekton.dev/v1
kind: TaskRun
Expand Down
90 changes: 80 additions & 10 deletions internal/sidecarlogresults/sidecarlogresults.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"io"
"os"
"path/filepath"
"strings"

"github.com/tektoncd/pipeline/pkg/apis/config"
"github.com/tektoncd/pipeline/pkg/result"
Expand All @@ -36,10 +37,19 @@ import (
// ErrSizeExceeded indicates that the result exceeded its maximum allowed size
var ErrSizeExceeded = errors.New("results size exceeds configured limit")

type SidecarLogResultType string

const (
taskResultType SidecarLogResultType = "task"
stepResultType SidecarLogResultType = "step"
sidecarResultNameSeparator string = "."
)

// SidecarLogResult holds fields for storing extracted results
type SidecarLogResult struct {
Name string
Value string
Type SidecarLogResultType
}

func fileExists(filename string) (bool, error) {
Expand Down Expand Up @@ -89,10 +99,42 @@ func waitForStepsToFinish(runDir string) error {
return nil
}

func createSidecarResultName(stepName, resultName string) string {
return fmt.Sprintf("%s%s%s", stepName, sidecarResultNameSeparator, resultName)
}

// ExtractStepAndResultFromSidecarResultName splits the result name to extract the step
// and result name from it. It only works if the format is <stepName>.<resultName>
func ExtractStepAndResultFromSidecarResultName(sidecarResultName string) (string, string, error) {
splitString := strings.SplitN(sidecarResultName, sidecarResultNameSeparator, 2)
if len(splitString) != 2 {
return "", "", fmt.Errorf("invalid string %s : expected somtthing that looks like <stepName>.<resultName>", sidecarResultName)
}
return splitString[0], splitString[1], nil
}

func readResults(resultsDir, resultFile, stepName string, resultType SidecarLogResultType) (SidecarLogResult, error) {
value, err := os.ReadFile(filepath.Join(resultsDir, resultFile))
if os.IsNotExist(err) {
return SidecarLogResult{}, nil
} else if err != nil {
return SidecarLogResult{}, fmt.Errorf("error reading the results file %w", err)
}
resultName := resultFile
if resultType == stepResultType {
resultName = createSidecarResultName(stepName, resultFile)
}
return SidecarLogResult{
Name: resultName,
Value: string(value),
Type: resultType,
}, nil
}

// LookForResults waits for results to be written out by the steps
// in their results path and prints them in a structured way to its
// stdout so that the reconciler can parse those logs.
func LookForResults(w io.Writer, runDir string, resultsDir string, resultNames []string) error {
func LookForResults(w io.Writer, runDir string, resultsDir string, resultNames []string, stepResultsDir string, stepResults map[string][]string) error {
if err := waitForStepsToFinish(runDir); err != nil {
return fmt.Errorf("error while waiting for the steps to finish %w", err)
}
Expand All @@ -102,20 +144,39 @@ func LookForResults(w io.Writer, runDir string, resultsDir string, resultNames [
resultFile := resultFile

g.Go(func() error {
value, err := os.ReadFile(filepath.Join(resultsDir, resultFile))
if os.IsNotExist(err) {
return nil
} else if err != nil {
return fmt.Errorf("error reading the results file %w", err)
newResult, err := readResults(resultsDir, resultFile, "", taskResultType)
if err != nil {
return err
}
newResult := SidecarLogResult{
Name: resultFile,
Value: string(value),
if newResult.Name == "" {
return nil
}
results <- newResult
return nil
})
}

for sName, sresults := range stepResults {
sresults := sresults
sName := sName
for _, resultName := range sresults {
resultName := resultName
stepResultsDir := filepath.Join(stepResultsDir, sName, "results")

g.Go(func() error {
newResult, err := readResults(stepResultsDir, resultName, sName, stepResultType)
if err != nil {
return err
}
if newResult.Name == "" {
return nil
}
results <- newResult
return nil
})
}
}

channelGroup := new(errgroup.Group)
channelGroup.Go(func() error {
if err := g.Wait(); err != nil {
Expand Down Expand Up @@ -183,10 +244,19 @@ func parseResults(resultBytes []byte, maxResultLimit int) (result.RunResult, err
if len(resultBytes) > maxResultLimit {
return runResult, fmt.Errorf("invalid result \"%s\": %w of %d", res.Name, ErrSizeExceeded, maxResultLimit)
}
var resultType result.ResultType
switch res.Type {
case taskResultType:
resultType = result.TaskRunResultType
case stepResultType:
resultType = result.StepResultType
default:
return result.RunResult{}, fmt.Errorf("invalid sidecar result type %v. Must be %v or %v", res.Type, taskResultType, stepResultType)
}
runResult = result.RunResult{
Key: res.Name,
Value: res.Value,
ResultType: result.TaskRunResultType,
ResultType: resultType,
}
return runResult, nil
}
Loading

0 comments on commit 30540fc

Please sign in to comment.