Skip to content

Commit

Permalink
entrypoint sidecar binary and injecting the sidecar
Browse files Browse the repository at this point in the history
This PR implements a part of TEP-0127 - entrypoint and sidecar binary (see #5695).

The sidecar binary relies on the Postfiles generated by the steps in /tekton/run/<step> to determine when a step has finished running. Once all the steps are done or if one of the step fails, the sidecar binary logs results to stdout.
  • Loading branch information
chitrangpatel authored and tekton-robot committed Dec 12, 2022
1 parent e49199a commit f9021b1
Show file tree
Hide file tree
Showing 15 changed files with 518 additions and 36 deletions.
1 change: 1 addition & 0 deletions cmd/controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ func main() {

opts := &pipeline.Options{}
flag.StringVar(&opts.Images.EntrypointImage, "entrypoint-image", "", "The container image containing our entrypoint binary.")
flag.StringVar(&opts.Images.SidecarLogResultsImage, "sidecarlogresults-image", "", "The container image containing the binary for accessing results.")
flag.StringVar(&opts.Images.NopImage, "nop-image", "", "The container image used to stop sidecars")
flag.StringVar(&opts.Images.GitImage, "git-image", "", "The container image containing our Git binary.")
flag.StringVar(&opts.Images.KubeconfigWriterImage, "kubeconfig-writer-image", "", "The container image containing our kubeconfig writer binary.")
Expand Down
23 changes: 13 additions & 10 deletions cmd/entrypoint/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (

"github.com/containerd/containerd/platforms"
"github.com/tektoncd/pipeline/cmd/entrypoint/subcommands"
featureFlags "github.com/tektoncd/pipeline/pkg/apis/config"
"github.com/tektoncd/pipeline/pkg/apis/pipeline"
"github.com/tektoncd/pipeline/pkg/credentials"
"github.com/tektoncd/pipeline/pkg/credentials/dockercreds"
Expand All @@ -52,9 +53,10 @@ var (
breakpointOnFailure = flag.Bool("breakpoint_on_failure", false, "If specified, expect steps to not skip on failure")
onError = flag.String("on_error", "", "Set to \"continue\" to ignore an error and continue when a container terminates with a non-zero exit code."+
" Set to \"stopAndFail\" to declare a failure with a step error and stop executing the rest of the steps.")
stepMetadataDir = flag.String("step_metadata_dir", "", "If specified, create directory to store the step metadata e.g. /tekton/steps/<step-name>/")
enableSpire = flag.Bool("enable_spire", false, "If specified by configmap, this enables spire signing and verification")
socketPath = flag.String("spire_socket_path", "unix:///spiffe-workload-api/spire-agent.sock", "Experimental: The SPIRE agent socket for SPIFFE workload API.")
stepMetadataDir = flag.String("step_metadata_dir", "", "If specified, create directory to store the step metadata e.g. /tekton/steps/<step-name>/")
enableSpire = flag.Bool("enable_spire", false, "If specified by configmap, this enables spire signing and verification")
socketPath = flag.String("spire_socket_path", "unix:///spiffe-workload-api/spire-agent.sock", "Experimental: The SPIRE agent socket for SPIFFE workload API.")
resultExtractionMethod = flag.String("result_from", featureFlags.ResultExtractionMethodTerminationMessage, "The method using which to extract results from tasks. Default is using the termination message.")
)

const (
Expand Down Expand Up @@ -154,13 +156,14 @@ func main() {
stdoutPath: *stdoutPath,
stderrPath: *stderrPath,
},
PostWriter: &realPostWriter{},
Results: strings.Split(*results, ","),
Timeout: timeout,
BreakpointOnFailure: *breakpointOnFailure,
OnError: *onError,
StepMetadataDir: *stepMetadataDir,
SpireWorkloadAPI: spireWorkloadAPI,
PostWriter: &realPostWriter{},
Results: strings.Split(*results, ","),
Timeout: timeout,
BreakpointOnFailure: *breakpointOnFailure,
OnError: *onError,
StepMetadataDir: *stepMetadataDir,
SpireWorkloadAPI: spireWorkloadAPI,
ResultExtractionMethod: *resultExtractionMethod,
}

// Copy any creds injected by the controller into the $HOME directory of the current
Expand Down
47 changes: 47 additions & 0 deletions cmd/sidecarlogresults/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
Copyright 2019 The Tekton 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.
*/

package main

import (
"flag"
"log"
"os"
"strings"

"github.com/tektoncd/pipeline/internal/sidecarlogresults"
"github.com/tektoncd/pipeline/pkg/apis/pipeline"
"github.com/tektoncd/pipeline/pkg/pod"
)

func main() {
var resultsDir string
var resultNames 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.Parse()
if resultNames == "" {
log.Fatal("result-names were not provided")
}
expectedResults := []string{}
for _, s := range strings.Split(resultNames, ",") {
expectedResults = append(expectedResults, s)
}
err := sidecarlogresults.LookForResults(os.Stdout, pod.RunDir, resultsDir, expectedResults)
if err != nil {
log.Fatal(err)
}
}
1 change: 1 addition & 0 deletions config/controller.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ spec:
"-git-image", "ko://github.com/tektoncd/pipeline/cmd/git-init",
"-entrypoint-image", "ko://github.com/tektoncd/pipeline/cmd/entrypoint",
"-nop-image", "ko://github.com/tektoncd/pipeline/cmd/nop",
"-sidecarlogresults-image", "ko://github.com/tektoncd/pipeline/cmd/sidecarlogresults",
"-imagedigest-exporter-image", "ko://github.com/tektoncd/pipeline/cmd/imagedigestexporter",
"-pr-image", "ko://github.com/tektoncd/pipeline/cmd/pullrequest-init",
"-workingdirinit-image", "ko://github.com/tektoncd/pipeline/cmd/workingdirinit",
Expand Down
127 changes: 127 additions & 0 deletions internal/sidecarlogresults/sidecarlogresults.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
Copyright 2019 The Tekton 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.
*/

package sidecarlogresults

import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"

"golang.org/x/sync/errgroup"
)

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

func fileExists(filename string) (bool, error) {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return false, nil
} else if err != nil {
return false, fmt.Errorf("error checking for file existence %w", err)
}
return !info.IsDir(), nil
}

func encode(w io.Writer, v any) error {
return json.NewEncoder(w).Encode(v)
}

func waitForStepsToFinish(runDir string) error {
steps := make(map[string]bool)
files, err := os.ReadDir(runDir)
if err != nil {
return fmt.Errorf("error parsing the run dir %w", err)
}
for _, file := range files {
steps[filepath.Join(runDir, file.Name(), "out")] = true
}
for len(steps) > 0 {
for stepFile := range steps {
// check if there is a post file without error
exists, err := fileExists(stepFile)
if err != nil {
return fmt.Errorf("error checking for out file's existence %w", err)
}
if exists {
delete(steps, stepFile)
continue
}
// check if there is a post file with error
// if err is nil then either the out.err file does not exist or it does and there was no issue
// in either case, existence of out.err marks that the step errored and the following steps will
// not run. We want the function to break out with nil error in that case so that
// the existing results can be logged.
if exists, err = fileExists(fmt.Sprintf("%s.err", stepFile)); exists || err != nil {
return err
}
}
}
return 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 {
if err := waitForStepsToFinish(runDir); err != nil {
return fmt.Errorf("error while waiting for the steps to finish %w", err)
}
results := make(chan SidecarLogResult)
g := new(errgroup.Group)
for _, resultFile := range 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 := SidecarLogResult{
Name: resultFile,
Value: string(value),
}
results <- newResult
return nil
})
}
channelGroup := new(errgroup.Group)
channelGroup.Go(func() error {
if err := g.Wait(); err != nil {
return fmt.Errorf("error parsing results: %w", err)
}
close(results)
return nil
})

for result := range results {
if err := encode(w, result); err != nil {
return fmt.Errorf("error writing results: %w", err)
}
}
if err := channelGroup.Wait(); err != nil {
return err
}
return nil
}
145 changes: 145 additions & 0 deletions internal/sidecarlogresults/sidecarlogresults_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package sidecarlogresults

import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"sort"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/tektoncd/pipeline/test/diff"
)

func TestLookForResults_FanOutAndWait(t *testing.T) {
for _, c := range []struct {
desc string
results []SidecarLogResult
}{{
desc: "multiple results",
results: []SidecarLogResult{{
Name: "foo",
Value: "bar",
}, {
Name: "foo2",
Value: "bar2",
}},
}} {
t.Run(c.desc, func(t *testing.T) {
dir := t.TempDir()
resultNames := []string{}
wantResults := []byte{}
for _, result := range c.results {
createResult(t, dir, result.Name, result.Value)
resultNames = append(resultNames, result.Name)
encodedResult, err := json.Marshal(result)
if err != nil {
t.Error(err)
}
// encode adds a newline character at the end.
// We need to do the same before comparing
encodedResult = append(encodedResult, '\n')
wantResults = append(wantResults, encodedResult...)
}
dir2 := t.TempDir()
go createRun(t, dir2, false)
got := new(bytes.Buffer)
err := LookForResults(got, dir2, dir, resultNames)
if err != nil {
t.Fatalf("Did not expect any error but got: %v", err)
}
// sort because the order of results is not always the same because of go routines.
sort.Slice(wantResults, func(i int, j int) bool { return wantResults[i] < wantResults[j] })
sort.Slice(got.Bytes(), func(i int, j int) bool { return got.Bytes()[i] < got.Bytes()[j] })
if d := cmp.Diff(wantResults, got.Bytes()); d != "" {
t.Errorf(diff.PrintWantGot(d))
}
})
}
}

func TestLookForResults(t *testing.T) {
for _, c := range []struct {
desc string
resultName string
resultValue string
createResult bool
stepError bool
}{{
desc: "good result",
resultName: "foo",
resultValue: "bar",
createResult: true,
stepError: false,
}, {
desc: "empty result",
resultName: "foo",
resultValue: "",
createResult: true,
stepError: true,
}, {
desc: "missing result",
resultName: "missing",
resultValue: "",
createResult: false,
stepError: false,
}} {
t.Run(c.desc, func(t *testing.T) {
dir := t.TempDir()
if c.createResult == true {
createResult(t, dir, c.resultName, c.resultValue)
}
dir2 := t.TempDir()
createRun(t, dir2, c.stepError)

var want []byte
if c.createResult == true {
// This is the expected result
result := SidecarLogResult{
Name: c.resultName,
Value: c.resultValue,
}
encodedResult, err := json.Marshal(result)
if err != nil {
t.Error(err)
}
// encode adds a newline character at the end.
// We need to do the same before comparing
encodedResult = append(encodedResult, '\n')
want = encodedResult
}
got := new(bytes.Buffer)
err := LookForResults(got, dir2, dir, []string{c.resultName})
if err != nil {
t.Fatalf("Did not expect any error but got: %v", err)
}
if d := cmp.Diff(want, got.Bytes()); d != "" {
t.Errorf(diff.PrintWantGot(d))
}
})
}
}

func createResult(t *testing.T, dir string, resultName string, resultValue string) {
t.Helper()
resultFile := filepath.Join(dir, resultName)
err := os.WriteFile(resultFile, []byte(resultValue), 0644)
if err != nil {
t.Fatal(err)
}
}

func createRun(t *testing.T, dir string, causeErr bool) {
t.Helper()
stepFile := filepath.Join(dir, "1")
_ = os.Mkdir(stepFile, 0755)
stepFile = filepath.Join(stepFile, "out")
if causeErr {
stepFile += ".err"
}
err := os.WriteFile(stepFile, []byte(""), 0644)
if err != nil {
t.Fatal(err)
}
}
Loading

0 comments on commit f9021b1

Please sign in to comment.