Skip to content

Commit 3b4e04e

Browse files
committed
[TEP-0089] Modify entrypoint to sign the results.
Breaking down PR #4759 originally proposed by @pxp928 to address TEP-0089 according @lumjjb suggestions. Plan for breaking down PR is PR 1.1: api PR 1.2: entrypointer (+cmd line + test/entrypointer) Entrypoint takes results and signs the results (termination message). PR 1.3: reconciler + pod + cmd/controller + integration tests Controller will verify the signed result. This commit corresponds to 1.2 above.
1 parent 7189162 commit 3b4e04e

File tree

3 files changed

+253
-4
lines changed

3 files changed

+253
-4
lines changed

cmd/entrypoint/main.go

+13
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import (
3434
"github.com/tektoncd/pipeline/pkg/credentials/dockercreds"
3535
"github.com/tektoncd/pipeline/pkg/credentials/gitcreds"
3636
"github.com/tektoncd/pipeline/pkg/entrypoint"
37+
"github.com/tektoncd/pipeline/pkg/spire"
38+
"github.com/tektoncd/pipeline/pkg/spire/config"
3739
"github.com/tektoncd/pipeline/pkg/termination"
3840
)
3941

@@ -51,6 +53,8 @@ var (
5153
onError = flag.String("on_error", "", "Set to \"continue\" to ignore an error and continue when a container terminates with a non-zero exit code."+
5254
" Set to \"stopAndFail\" to declare a failure with a step error and stop executing the rest of the steps.")
5355
stepMetadataDir = flag.String("step_metadata_dir", "", "If specified, create directory to store the step metadata e.g. /tekton/steps/<step-name>/")
56+
enableSpire = flag.Bool("enable_spire", false, "If specified by configmap, this enables spire signing and verification")
57+
socketPath = flag.String("spire_socket_path", "unix:///spiffe-workload-api/spire-agent.sock", "Experimental: The SPIRE agent socket for SPIFFE workload API.")
5458
)
5559

5660
const (
@@ -131,6 +135,14 @@ func main() {
131135
}
132136
}
133137

138+
var spireWorkloadAPI spire.EntrypointerAPIClient
139+
if enableSpire != nil && *enableSpire && socketPath != nil && *socketPath != "" {
140+
spireConfig := config.SpireConfig{
141+
SocketPath: *socketPath,
142+
}
143+
spireWorkloadAPI = spire.NewEntrypointerAPIClient(&spireConfig)
144+
}
145+
134146
e := entrypoint.Entrypointer{
135147
Command: append(cmd, commandArgs...),
136148
WaitFiles: strings.Split(*waitFiles, ","),
@@ -148,6 +160,7 @@ func main() {
148160
BreakpointOnFailure: *breakpointOnFailure,
149161
OnError: *onError,
150162
StepMetadataDir: *stepMetadataDir,
163+
SpireWorkloadAPI: spireWorkloadAPI,
151164
}
152165

153166
// Copy any creds injected by the controller into the $HOME directory of the current

pkg/entrypoint/entrypointer.go

+22-3
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131

3232
"github.com/tektoncd/pipeline/pkg/apis/pipeline"
3333
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
34+
"github.com/tektoncd/pipeline/pkg/spire"
3435
"github.com/tektoncd/pipeline/pkg/termination"
3536
"go.uber.org/zap"
3637
)
@@ -80,6 +81,10 @@ type Entrypointer struct {
8081
OnError string
8182
// StepMetadataDir is the directory for a step where the step related metadata can be stored
8283
StepMetadataDir string
84+
// SpireWorkloadAPI connects to spire and does obtains SVID based on taskrun
85+
SpireWorkloadAPI spire.EntrypointerAPIClient
86+
// ResultsDirectory is the directory to find results, defaults to pipeline.DefaultResultPath
87+
ResultsDirectory string
8388
}
8489

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

144+
ctx := context.Background()
139145
var err error
146+
140147
if e.Timeout != nil && *e.Timeout < time.Duration(0) {
141148
err = fmt.Errorf("negative timeout specified")
142149
}
143150

144151
if err == nil {
145-
ctx := context.Background()
146152
var cancel context.CancelFunc
147153
if e.Timeout != nil && *e.Timeout != time.Duration(0) {
148154
ctx, cancel = context.WithTimeout(ctx, *e.Timeout)
@@ -184,15 +190,19 @@ func (e Entrypointer) Go() error {
184190
// strings.Split(..) with an empty string returns an array that contains one element, an empty string.
185191
// This creates an error when trying to open the result folder as a file.
186192
if len(e.Results) >= 1 && e.Results[0] != "" {
187-
if err := e.readResultsFromDisk(pipeline.DefaultResultPath); err != nil {
193+
resultPath := pipeline.DefaultResultPath
194+
if e.ResultsDirectory != "" {
195+
resultPath = e.ResultsDirectory
196+
}
197+
if err := e.readResultsFromDisk(ctx, resultPath); err != nil {
188198
logger.Fatalf("Error while handling results: %s", err)
189199
}
190200
}
191201

192202
return err
193203
}
194204

195-
func (e Entrypointer) readResultsFromDisk(resultDir string) error {
205+
func (e Entrypointer) readResultsFromDisk(ctx context.Context, resultDir string) error {
196206
output := []v1beta1.PipelineResourceResult{}
197207
for _, resultFile := range e.Results {
198208
if resultFile == "" {
@@ -211,6 +221,15 @@ func (e Entrypointer) readResultsFromDisk(resultDir string) error {
211221
ResultType: v1beta1.TaskRunResultType,
212222
})
213223
}
224+
225+
if e.SpireWorkloadAPI != nil {
226+
signed, err := e.SpireWorkloadAPI.Sign(ctx, output)
227+
if err != nil {
228+
return err
229+
}
230+
output = append(output, signed...)
231+
}
232+
214233
// push output to termination path
215234
if len(output) != 0 {
216235
if err := termination.WriteMessage(e.TerminationPath, output); err != nil {

pkg/entrypoint/entrypointer_test.go

+218-1
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,18 @@ import (
2424
"io/ioutil"
2525
"os"
2626
"os/exec"
27+
"path"
2728
"path/filepath"
2829
"reflect"
2930
"testing"
3031
"time"
3132

3233
"github.com/google/go-cmp/cmp"
3334
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
35+
"github.com/tektoncd/pipeline/pkg/spire"
3436
"github.com/tektoncd/pipeline/pkg/termination"
3537
"github.com/tektoncd/pipeline/test/diff"
38+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3639
"knative.dev/pkg/logging"
3740
)
3841

@@ -284,6 +287,7 @@ func TestReadResultsFromDisk(t *testing.T) {
284287
},
285288
} {
286289
t.Run(c.desc, func(t *testing.T) {
290+
ctx := context.Background()
287291
terminationPath := "termination"
288292
if terminationFile, err := ioutil.TempFile("", "termination"); err != nil {
289293
t.Fatalf("unexpected error creating temporary termination file: %v", err)
@@ -314,7 +318,7 @@ func TestReadResultsFromDisk(t *testing.T) {
314318
Results: resultsFilePath,
315319
TerminationPath: terminationPath,
316320
}
317-
if err := e.readResultsFromDisk(""); err != nil {
321+
if err := e.readResultsFromDisk(ctx, ""); err != nil {
318322
t.Fatal(err)
319323
}
320324
msg, err := ioutil.ReadFile(terminationPath)
@@ -434,6 +438,167 @@ func TestEntrypointer_OnError(t *testing.T) {
434438
}
435439
}
436440

441+
func TestEntrypointerResults(t *testing.T) {
442+
for _, c := range []struct {
443+
desc, entrypoint, postFile, stepDir, stepDirLink string
444+
waitFiles, args []string
445+
resultsToWrite map[string]string
446+
resultsOverride []string
447+
breakpointOnFailure bool
448+
sign bool
449+
signVerify bool
450+
}{{
451+
desc: "do nothing",
452+
}, {
453+
desc: "no results",
454+
entrypoint: "echo",
455+
}, {
456+
desc: "write single result",
457+
entrypoint: "echo",
458+
resultsToWrite: map[string]string{
459+
"foo": "abc",
460+
},
461+
}, {
462+
desc: "write multiple result",
463+
entrypoint: "echo",
464+
resultsToWrite: map[string]string{
465+
"foo": "abc",
466+
"bar": "def",
467+
},
468+
}, {
469+
// These next two tests show that if not results are defined in the entrypointer, then no signature is produced
470+
// indicating that no signature was created. However, it is important to note that results were defined,
471+
// but no results were created, that signature is still produced.
472+
desc: "no results signed",
473+
entrypoint: "echo",
474+
sign: true,
475+
signVerify: false,
476+
}, {
477+
desc: "defined results but no results produced signed",
478+
entrypoint: "echo",
479+
resultsOverride: []string{"foo"},
480+
sign: true,
481+
signVerify: true,
482+
}, {
483+
desc: "write single result",
484+
entrypoint: "echo",
485+
resultsToWrite: map[string]string{
486+
"foo": "abc",
487+
},
488+
sign: true,
489+
signVerify: true,
490+
}, {
491+
desc: "write multiple result",
492+
entrypoint: "echo",
493+
resultsToWrite: map[string]string{
494+
"foo": "abc",
495+
"bar": "def",
496+
},
497+
sign: true,
498+
signVerify: true,
499+
}, {
500+
desc: "write n/m results",
501+
entrypoint: "echo",
502+
resultsToWrite: map[string]string{
503+
"foo": "abc",
504+
},
505+
resultsOverride: []string{"foo", "bar"},
506+
sign: true,
507+
signVerify: true,
508+
}} {
509+
t.Run(c.desc, func(t *testing.T) {
510+
ctx := context.Background()
511+
fw, fpw := &fakeWaiter{}, &fakePostWriter{}
512+
var fr Runner = &fakeRunner{}
513+
timeout := time.Duration(0)
514+
terminationPath := "termination"
515+
if terminationFile, err := ioutil.TempFile("", "termination"); err != nil {
516+
t.Fatalf("unexpected error creating temporary termination file: %v", err)
517+
} else {
518+
terminationPath = terminationFile.Name()
519+
defer os.Remove(terminationFile.Name())
520+
}
521+
522+
resultsDir := createTmpDir(t, "results")
523+
var results []string
524+
if c.resultsToWrite != nil {
525+
tmpResultsToWrite := map[string]string{}
526+
for k, v := range c.resultsToWrite {
527+
resultFile := path.Join(resultsDir, k)
528+
tmpResultsToWrite[resultFile] = v
529+
results = append(results, k)
530+
}
531+
532+
fr = &fakeResultsWriter{
533+
resultsToWrite: tmpResultsToWrite,
534+
}
535+
}
536+
537+
signClient, verifyClient, tr := getMockSpireClient(ctx)
538+
if !c.sign {
539+
signClient = nil
540+
}
541+
542+
if c.resultsOverride != nil {
543+
results = c.resultsOverride
544+
}
545+
546+
err := Entrypointer{
547+
Command: append([]string{c.entrypoint}, c.args...),
548+
WaitFiles: c.waitFiles,
549+
PostFile: c.postFile,
550+
Waiter: fw,
551+
Runner: fr,
552+
PostWriter: fpw,
553+
Results: results,
554+
ResultsDirectory: resultsDir,
555+
TerminationPath: terminationPath,
556+
Timeout: &timeout,
557+
BreakpointOnFailure: c.breakpointOnFailure,
558+
StepMetadataDir: c.stepDir,
559+
SpireWorkloadAPI: signClient,
560+
}.Go()
561+
if err != nil {
562+
t.Fatalf("Entrypointer failed: %v", err)
563+
}
564+
565+
fileContents, err := ioutil.ReadFile(terminationPath)
566+
if err == nil {
567+
resultCheck := map[string]bool{}
568+
var entries []v1beta1.PipelineResourceResult
569+
if err := json.Unmarshal(fileContents, &entries); err != nil {
570+
t.Fatalf("failed to unmarshal results: %v", err)
571+
}
572+
573+
for _, result := range entries {
574+
if _, ok := c.resultsToWrite[result.Key]; ok {
575+
if c.resultsToWrite[result.Key] == result.Value {
576+
resultCheck[result.Key] = true
577+
} else {
578+
t.Errorf("expected result (%v) to have value %v, got %v", result.Key, result.Value, c.resultsToWrite[result.Key])
579+
}
580+
}
581+
}
582+
583+
if len(resultCheck) != len(c.resultsToWrite) {
584+
t.Error("number of results matching did not add up")
585+
}
586+
587+
// Check signature
588+
verified := verifyClient.VerifyTaskRunResults(ctx, entries, tr) == nil
589+
if verified != c.signVerify {
590+
t.Errorf("expected signature verify result %v, got %v", c.signVerify, verified)
591+
}
592+
} else if !os.IsNotExist(err) {
593+
t.Error("Wanted termination file written, got nil")
594+
}
595+
if err := os.Remove(terminationPath); err != nil {
596+
t.Errorf("Could not remove termination path: %s", err)
597+
}
598+
})
599+
}
600+
}
601+
437602
type fakeWaiter struct{ waited []string }
438603

439604
func (f *fakeWaiter) Wait(file string, _ bool, _ bool) error {
@@ -503,3 +668,55 @@ func (f *fakeExitErrorRunner) Run(ctx context.Context, args ...string) error {
503668
f.args = &args
504669
return exec.Command("ls", "/bogus/path").Run()
505670
}
671+
672+
type fakeResultsWriter struct {
673+
args *[]string
674+
resultsToWrite map[string]string
675+
}
676+
677+
func (f *fakeResultsWriter) Run(ctx context.Context, args ...string) error {
678+
f.args = &args
679+
for k, v := range f.resultsToWrite {
680+
err := ioutil.WriteFile(k, []byte(v), 0666)
681+
if err != nil {
682+
return err
683+
}
684+
}
685+
return nil
686+
}
687+
688+
func createTmpDir(t *testing.T, name string) string {
689+
tmpDir, err := ioutil.TempDir("", name)
690+
if err != nil {
691+
t.Fatalf("unexpected error creating temporary dir: %v", err)
692+
}
693+
return tmpDir
694+
}
695+
696+
func getMockSpireClient(ctx context.Context) (spire.EntrypointerAPIClient, spire.ControllerAPIClient, *v1beta1.TaskRun) {
697+
tr := &v1beta1.TaskRun{
698+
ObjectMeta: metav1.ObjectMeta{
699+
Name: "taskrun-example",
700+
Namespace: "foo",
701+
},
702+
Spec: v1beta1.TaskRunSpec{
703+
TaskRef: &v1beta1.TaskRef{
704+
Name: "taskname",
705+
APIVersion: "a1",
706+
},
707+
ServiceAccountName: "test-sa",
708+
},
709+
}
710+
711+
sc := &spire.MockClient{}
712+
713+
_ = sc.CreateEntries(ctx, tr, nil, 10000)
714+
715+
// bootstrap with about 20 calls to sign which should be enough for testing
716+
id := sc.GetIdentity(tr)
717+
for i := 0; i < 20; i++ {
718+
sc.SignIdentities = append(sc.SignIdentities, id)
719+
}
720+
721+
return sc, sc, tr
722+
}

0 commit comments

Comments
 (0)