Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cmd/entrypoint/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ The following flags are available:
same value as `{{stdout_path}}` so both streams are copied to the same
file. However, there is no ordering guarantee on data copied from both
streams.
- `-enable_spire`: If set will enable signing of the results by SPIRE. Signing
results by SPIRE ensures that no process other than the current process can
tamper the results and go undetected.
- `-spire_socket_path`: This flag makes sense only when enable_spire is set.
When enable_spire is set, spire_socket_path is used to point to the
SPIRE agent socket for SPIFFE workload API.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jagathprakash you mentioned that the list here is incomplete - i wonder in the long run if it would make the most sense to avoid listing the individual commands here and just talk more generally about the features (and rely on the docs for the command itself vs trying to keep this in sync) - e.g. a list of features that the entrypoint binary supports such as signing with spire, etc. vs. specifics about these flags. In that case I could see there being a small section about SPIRE below. Anyway this is a good start in at least having some docs.


Any extra positional arguments are passed to the original entrypoint command.

Expand Down
13 changes: 13 additions & 0 deletions cmd/entrypoint/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import (
"github.com/tektoncd/pipeline/pkg/credentials/dockercreds"
"github.com/tektoncd/pipeline/pkg/credentials/gitcreds"
"github.com/tektoncd/pipeline/pkg/entrypoint"
"github.com/tektoncd/pipeline/pkg/spire"
"github.com/tektoncd/pipeline/pkg/spire/config"
"github.com/tektoncd/pipeline/pkg/termination"
)

Expand All @@ -51,6 +53,8 @@ var (
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.")
)

const (
Expand Down Expand Up @@ -131,6 +135,14 @@ func main() {
}
}

var spireWorkloadAPI spire.EntrypointerAPIClient
if enableSpire != nil && *enableSpire && socketPath != nil && *socketPath != "" {
spireConfig := config.SpireConfig{
SocketPath: *socketPath,
}
spireWorkloadAPI = spire.NewEntrypointerAPIClient(&spireConfig)
}

e := entrypoint.Entrypointer{
Command: append(cmd, commandArgs...),
WaitFiles: strings.Split(*waitFiles, ","),
Expand All @@ -148,6 +160,7 @@ func main() {
BreakpointOnFailure: *breakpointOnFailure,
OnError: *onError,
StepMetadataDir: *stepMetadataDir,
SpireWorkloadAPI: spireWorkloadAPI,
}

// Copy any creds injected by the controller into the $HOME directory of the current
Expand Down
25 changes: 22 additions & 3 deletions pkg/entrypoint/entrypointer.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (

"github.com/tektoncd/pipeline/pkg/apis/pipeline"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
"github.com/tektoncd/pipeline/pkg/spire"
"github.com/tektoncd/pipeline/pkg/termination"
"go.uber.org/zap"
)
Expand Down Expand Up @@ -80,6 +81,10 @@ type Entrypointer struct {
OnError string
// StepMetadataDir is the directory for a step where the step related metadata can be stored
StepMetadataDir string
// SpireWorkloadAPI connects to spire and does obtains SVID based on taskrun
SpireWorkloadAPI spire.EntrypointerAPIClient
// ResultsDirectory is the directory to find results, defaults to pipeline.DefaultResultPath
ResultsDirectory string
}

// Waiter encapsulates waiting for files to exist.
Expand Down Expand Up @@ -136,13 +141,14 @@ func (e Entrypointer) Go() error {
ResultType: v1beta1.InternalTektonResultType,
})

ctx := context.Background()
var err error

if e.Timeout != nil && *e.Timeout < time.Duration(0) {
err = fmt.Errorf("negative timeout specified")
}

if err == nil {
ctx := context.Background()
var cancel context.CancelFunc
if e.Timeout != nil && *e.Timeout != time.Duration(0) {
ctx, cancel = context.WithTimeout(ctx, *e.Timeout)
Expand Down Expand Up @@ -184,15 +190,19 @@ func (e Entrypointer) Go() error {
// strings.Split(..) with an empty string returns an array that contains one element, an empty string.
// This creates an error when trying to open the result folder as a file.
if len(e.Results) >= 1 && e.Results[0] != "" {
if err := e.readResultsFromDisk(pipeline.DefaultResultPath); err != nil {
resultPath := pipeline.DefaultResultPath
if e.ResultsDirectory != "" {
resultPath = e.ResultsDirectory
}
if err := e.readResultsFromDisk(ctx, resultPath); err != nil {
logger.Fatalf("Error while handling results: %s", err)
}
}

return err
}

func (e Entrypointer) readResultsFromDisk(resultDir string) error {
func (e Entrypointer) readResultsFromDisk(ctx context.Context, resultDir string) error {
output := []v1beta1.PipelineResourceResult{}
for _, resultFile := range e.Results {
if resultFile == "" {
Expand All @@ -211,6 +221,15 @@ func (e Entrypointer) readResultsFromDisk(resultDir string) error {
ResultType: v1beta1.TaskRunResultType,
})
}

if e.SpireWorkloadAPI != nil {
signed, err := e.SpireWorkloadAPI.Sign(ctx, output)
if err != nil {
return err
}
output = append(output, signed...)
}

// push output to termination path
if len(output) != 0 {
if err := termination.WriteMessage(e.TerminationPath, output); err != nil {
Expand Down
219 changes: 218 additions & 1 deletion pkg/entrypoint/entrypointer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,18 @@ import (
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"reflect"
"testing"
"time"

"github.com/google/go-cmp/cmp"
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
"github.com/tektoncd/pipeline/pkg/spire"
"github.com/tektoncd/pipeline/pkg/termination"
"github.com/tektoncd/pipeline/test/diff"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"knative.dev/pkg/logging"
)

Expand Down Expand Up @@ -284,6 +287,7 @@ func TestReadResultsFromDisk(t *testing.T) {
},
} {
t.Run(c.desc, func(t *testing.T) {
ctx := context.Background()
terminationPath := "termination"
if terminationFile, err := ioutil.TempFile("", "termination"); err != nil {
t.Fatalf("unexpected error creating temporary termination file: %v", err)
Expand Down Expand Up @@ -314,7 +318,7 @@ func TestReadResultsFromDisk(t *testing.T) {
Results: resultsFilePath,
TerminationPath: terminationPath,
}
if err := e.readResultsFromDisk(""); err != nil {
if err := e.readResultsFromDisk(ctx, ""); err != nil {
t.Fatal(err)
}
msg, err := ioutil.ReadFile(terminationPath)
Expand Down Expand Up @@ -434,6 +438,167 @@ func TestEntrypointer_OnError(t *testing.T) {
}
}

func TestEntrypointerResults(t *testing.T) {
for _, c := range []struct {
desc, entrypoint, postFile, stepDir, stepDirLink string
waitFiles, args []string
resultsToWrite map[string]string
resultsOverride []string
breakpointOnFailure bool
sign bool
signVerify bool
}{{
desc: "do nothing",
}, {
desc: "no results",
entrypoint: "echo",
}, {
desc: "write single result",
entrypoint: "echo",
resultsToWrite: map[string]string{
"foo": "abc",
},
}, {
desc: "write multiple result",
entrypoint: "echo",
resultsToWrite: map[string]string{
"foo": "abc",
"bar": "def",
},
}, {
// These next two tests show that if not results are defined in the entrypointer, then no signature is produced
// indicating that no signature was created. However, it is important to note that results were defined,
// but no results were created, that signature is still produced.
desc: "no results signed",
entrypoint: "echo",
sign: true,
signVerify: false,
}, {
desc: "defined results but no results produced signed",
entrypoint: "echo",
resultsOverride: []string{"foo"},
sign: true,
signVerify: true,
}, {
desc: "write single result",
entrypoint: "echo",
resultsToWrite: map[string]string{
"foo": "abc",
},
sign: true,
signVerify: true,
}, {
desc: "write multiple result",
entrypoint: "echo",
resultsToWrite: map[string]string{
"foo": "abc",
"bar": "def",
},
sign: true,
signVerify: true,
}, {
desc: "write n/m results",
entrypoint: "echo",
resultsToWrite: map[string]string{
"foo": "abc",
},
resultsOverride: []string{"foo", "bar"},
sign: true,
signVerify: true,
}} {
t.Run(c.desc, func(t *testing.T) {
ctx := context.Background()
fw, fpw := &fakeWaiter{}, &fakePostWriter{}
var fr Runner = &fakeRunner{}
timeout := time.Duration(0)
terminationPath := "termination"
if terminationFile, err := ioutil.TempFile("", "termination"); err != nil {
t.Fatalf("unexpected error creating temporary termination file: %v", err)
} else {
terminationPath = terminationFile.Name()
defer os.Remove(terminationFile.Name())
}

resultsDir := createTmpDir(t, "results")
var results []string
if c.resultsToWrite != nil {
tmpResultsToWrite := map[string]string{}
for k, v := range c.resultsToWrite {
resultFile := path.Join(resultsDir, k)
tmpResultsToWrite[resultFile] = v
results = append(results, k)
}

fr = &fakeResultsWriter{
resultsToWrite: tmpResultsToWrite,
}
}

signClient, verifyClient, tr := getMockSpireClient(ctx)
if !c.sign {
signClient = nil
}

if c.resultsOverride != nil {
results = c.resultsOverride
}

err := Entrypointer{
Command: append([]string{c.entrypoint}, c.args...),
WaitFiles: c.waitFiles,
PostFile: c.postFile,
Waiter: fw,
Runner: fr,
PostWriter: fpw,
Results: results,
ResultsDirectory: resultsDir,
TerminationPath: terminationPath,
Timeout: &timeout,
BreakpointOnFailure: c.breakpointOnFailure,
StepMetadataDir: c.stepDir,
SpireWorkloadAPI: signClient,
}.Go()
if err != nil {
t.Fatalf("Entrypointer failed: %v", err)
}

fileContents, err := ioutil.ReadFile(terminationPath)
if err == nil {
resultCheck := map[string]bool{}
var entries []v1beta1.PipelineResourceResult
if err := json.Unmarshal(fileContents, &entries); err != nil {
t.Fatalf("failed to unmarshal results: %v", err)
}

for _, result := range entries {
if _, ok := c.resultsToWrite[result.Key]; ok {
if c.resultsToWrite[result.Key] == result.Value {
resultCheck[result.Key] = true
} else {
t.Errorf("expected result (%v) to have value %v, got %v", result.Key, result.Value, c.resultsToWrite[result.Key])
}
}
}

if len(resultCheck) != len(c.resultsToWrite) {
t.Error("number of results matching did not add up")
}

// Check signature
verified := verifyClient.VerifyTaskRunResults(ctx, entries, tr) == nil
if verified != c.signVerify {
t.Errorf("expected signature verify result %v, got %v", c.signVerify, verified)
}
} else if !os.IsNotExist(err) {
t.Error("Wanted termination file written, got nil")
}
if err := os.Remove(terminationPath); err != nil {
t.Errorf("Could not remove termination path: %s", err)
}
})
}
}

type fakeWaiter struct{ waited []string }

func (f *fakeWaiter) Wait(file string, _ bool, _ bool) error {
Expand Down Expand Up @@ -503,3 +668,55 @@ func (f *fakeExitErrorRunner) Run(ctx context.Context, args ...string) error {
f.args = &args
return exec.Command("ls", "/bogus/path").Run()
}

type fakeResultsWriter struct {
args *[]string
resultsToWrite map[string]string
}

func (f *fakeResultsWriter) Run(ctx context.Context, args ...string) error {
f.args = &args
for k, v := range f.resultsToWrite {
err := ioutil.WriteFile(k, []byte(v), 0666)
if err != nil {
return err
}
}
return nil
}

func createTmpDir(t *testing.T, name string) string {
tmpDir, err := ioutil.TempDir("", name)
if err != nil {
t.Fatalf("unexpected error creating temporary dir: %v", err)
}
return tmpDir
}

func getMockSpireClient(ctx context.Context) (spire.EntrypointerAPIClient, spire.ControllerAPIClient, *v1beta1.TaskRun) {
tr := &v1beta1.TaskRun{
ObjectMeta: metav1.ObjectMeta{
Name: "taskrun-example",
Namespace: "foo",
},
Spec: v1beta1.TaskRunSpec{
TaskRef: &v1beta1.TaskRef{
Name: "taskname",
APIVersion: "a1",
},
ServiceAccountName: "test-sa",
},
}

sc := &spire.MockClient{}

_ = sc.CreateEntries(ctx, tr, nil, 10000)

// bootstrap with about 20 calls to sign which should be enough for testing
id := sc.GetIdentity(tr)
for i := 0; i < 20; i++ {
sc.SignIdentities = append(sc.SignIdentities, id)
}

return sc, sc, tr
}