diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 7b7bd245396..b978350a96c 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -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.") diff --git a/cmd/entrypoint/main.go b/cmd/entrypoint/main.go index f138c46e2e6..e2aeba1d9d2 100644 --- a/cmd/entrypoint/main.go +++ b/cmd/entrypoint/main.go @@ -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" @@ -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//") - 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//") + 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 ( @@ -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 diff --git a/cmd/sidecarlogresults/main.go b/cmd/sidecarlogresults/main.go new file mode 100644 index 00000000000..e163ee98df4 --- /dev/null +++ b/cmd/sidecarlogresults/main.go @@ -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) + } +} diff --git a/config/controller.yaml b/config/controller.yaml index 4883d1c4865..832c7242633 100644 --- a/config/controller.yaml +++ b/config/controller.yaml @@ -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", diff --git a/internal/sidecarlogresults/sidecarlogresults.go b/internal/sidecarlogresults/sidecarlogresults.go new file mode 100644 index 00000000000..c34d1d24a0c --- /dev/null +++ b/internal/sidecarlogresults/sidecarlogresults.go @@ -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 +} diff --git a/internal/sidecarlogresults/sidecarlogresults_test.go b/internal/sidecarlogresults/sidecarlogresults_test.go new file mode 100644 index 00000000000..0a32c89b7f5 --- /dev/null +++ b/internal/sidecarlogresults/sidecarlogresults_test.go @@ -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) + } +} diff --git a/internal/sidecarlogsvalidation/sidecarlogs.go b/internal/sidecarlogsvalidation/sidecarlogs.go new file mode 100644 index 00000000000..8668267c062 --- /dev/null +++ b/internal/sidecarlogsvalidation/sidecarlogs.go @@ -0,0 +1,23 @@ +/* +Copyright 2022 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 sidecarlogsvalidation + +const ( + // ReservedResultsSidecarName is the name of the results sidecar that outputs the results to stdout + // when the results-from feature-flag is set to "sidecar-logs". + ReservedResultsSidecarName = "tekton-log-results" +) diff --git a/pkg/apis/pipeline/images.go b/pkg/apis/pipeline/images.go index e40ebfe5639..8c74410e8bc 100644 --- a/pkg/apis/pipeline/images.go +++ b/pkg/apis/pipeline/images.go @@ -26,6 +26,8 @@ import ( type Images struct { // EntrypointImage is container image containing our entrypoint binary. EntrypointImage string + // SidecarLogResultsImage is container image containing the binary that fetches results from the steps and logs it to stdout. + SidecarLogResultsImage string // NopImage is the container image used to kill sidecars. NopImage string // GitImage is the container image with Git that we use to implement the Git source step. @@ -55,6 +57,7 @@ func (i Images) Validate() error { v, name string }{ {i.EntrypointImage, "entrypoint-image"}, + {i.SidecarLogResultsImage, "sidecarlogresults-image"}, {i.NopImage, "nop-image"}, {i.GitImage, "git-image"}, {i.KubeconfigWriterImage, "kubeconfig-writer-image"}, diff --git a/pkg/apis/pipeline/images_test.go b/pkg/apis/pipeline/images_test.go index 8f66cf31985..87222aa1d2e 100644 --- a/pkg/apis/pipeline/images_test.go +++ b/pkg/apis/pipeline/images_test.go @@ -9,6 +9,7 @@ import ( func TestValidate(t *testing.T) { valid := pipeline.Images{ EntrypointImage: "set", + SidecarLogResultsImage: "set", NopImage: "set", GitImage: "set", KubeconfigWriterImage: "set", @@ -25,6 +26,7 @@ func TestValidate(t *testing.T) { invalid := pipeline.Images{ EntrypointImage: "set", + SidecarLogResultsImage: "set", NopImage: "set", GitImage: "", // unset! KubeconfigWriterImage: "set", diff --git a/pkg/entrypoint/entrypointer.go b/pkg/entrypoint/entrypointer.go index 5fc8c59f429..5e76a434f3e 100644 --- a/pkg/entrypoint/entrypointer.go +++ b/pkg/entrypoint/entrypointer.go @@ -29,6 +29,7 @@ import ( "strings" "time" + "github.com/tektoncd/pipeline/pkg/apis/config" "github.com/tektoncd/pipeline/pkg/apis/pipeline" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" "github.com/tektoncd/pipeline/pkg/spire" @@ -85,6 +86,8 @@ type Entrypointer struct { SpireWorkloadAPI spire.EntrypointerAPIClient // ResultsDirectory is the directory to find results, defaults to pipeline.DefaultResultPath ResultsDirectory string + // ResultExtractionMethod is the method using which the controller extracts the results from the task pod. + ResultExtractionMethod string } // Waiter encapsulates waiting for files to exist. @@ -221,7 +224,6 @@ func (e Entrypointer) readResultsFromDisk(ctx context.Context, resultDir string) ResultType: v1beta1.TaskRunResultType, }) } - if e.SpireWorkloadAPI != nil { signed, err := e.SpireWorkloadAPI.Sign(ctx, output) if err != nil { @@ -231,7 +233,7 @@ func (e Entrypointer) readResultsFromDisk(ctx context.Context, resultDir string) } // push output to termination path - if len(output) != 0 { + if e.ResultExtractionMethod == config.ResultExtractionMethodTerminationMessage && len(output) != 0 { if err := termination.WriteMessage(e.TerminationPath, output); err != nil { return err } diff --git a/pkg/entrypoint/entrypointer_test.go b/pkg/entrypoint/entrypointer_test.go index fe69776755f..45ab39e30fb 100644 --- a/pkg/entrypoint/entrypointer_test.go +++ b/pkg/entrypoint/entrypointer_test.go @@ -31,6 +31,7 @@ import ( "time" "github.com/google/go-cmp/cmp" + "github.com/tektoncd/pipeline/pkg/apis/config" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" "github.com/tektoncd/pipeline/pkg/spire" "github.com/tektoncd/pipeline/pkg/termination" @@ -315,8 +316,9 @@ func TestReadResultsFromDisk(t *testing.T) { } e := Entrypointer{ - Results: resultsFilePath, - TerminationPath: terminationPath, + Results: resultsFilePath, + TerminationPath: terminationPath, + ResultExtractionMethod: config.ResultExtractionMethodTerminationMessage, } if err := e.readResultsFromDisk(ctx, ""); err != nil { t.Fatal(err) @@ -544,19 +546,20 @@ func TestEntrypointerResults(t *testing.T) { } 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, + Command: append([]string{c.entrypoint}, c.args...), + WaitFiles: c.waitFiles, + PostFile: c.postFile, + Waiter: fw, + Runner: fr, + PostWriter: fpw, + Results: results, + ResultsDirectory: resultsDir, + ResultExtractionMethod: config.ResultExtractionMethodTerminationMessage, + TerminationPath: terminationPath, + Timeout: &timeout, + BreakpointOnFailure: c.breakpointOnFailure, + StepMetadataDir: c.stepDir, + SpireWorkloadAPI: signClient, }.Go() if err != nil { t.Fatalf("Entrypointer failed: %v", err) diff --git a/pkg/pod/entrypoint.go b/pkg/pod/entrypoint.go index 7a28eff6143..2bcbb62b2d5 100644 --- a/pkg/pod/entrypoint.go +++ b/pkg/pod/entrypoint.go @@ -42,7 +42,12 @@ const ( entrypointBinary = binDir + "/entrypoint" runVolumeName = "tekton-internal-run" - runDir = "/tekton/run" + + // RunDir is the directory that contains runtime variable data for TaskRuns. + // This includes files for handling container ordering, exit status codes, and more. + // See [https://github.com/tektoncd/pipeline/blob/main/docs/developers/taskruns.md#tekton] + // for more details. + RunDir = "/tekton/run" downwardVolumeName = "tekton-internal-downward" downwardMountPoint = "/tekton/downward" @@ -125,13 +130,13 @@ func orderContainers(commonExtraEntrypointArgs []string, steps []corev1.Containe ) } } else { // Not the first step - wait for previous - argsForEntrypoint = append(argsForEntrypoint, "-wait_file", filepath.Join(runDir, strconv.Itoa(i-1), "out")) + argsForEntrypoint = append(argsForEntrypoint, "-wait_file", filepath.Join(RunDir, strconv.Itoa(i-1), "out")) } argsForEntrypoint = append(argsForEntrypoint, // Start next step. - "-post_file", filepath.Join(runDir, idx, "out"), + "-post_file", filepath.Join(RunDir, idx, "out"), "-termination_path", terminationPath, - "-step_metadata_dir", filepath.Join(runDir, idx, "status"), + "-step_metadata_dir", filepath.Join(RunDir, idx, "status"), ) argsForEntrypoint = append(argsForEntrypoint, commonExtraEntrypointArgs...) if taskSpec != nil { diff --git a/pkg/pod/pod.go b/pkg/pod/pod.go index 30bf9d8fa1c..d78623ed234 100644 --- a/pkg/pod/pod.go +++ b/pkg/pod/pod.go @@ -23,7 +23,9 @@ import ( "math" "path/filepath" "strconv" + "strings" + "github.com/tektoncd/pipeline/internal/sidecarlogsvalidation" "github.com/tektoncd/pipeline/pkg/apis/config" "github.com/tektoncd/pipeline/pkg/apis/pipeline" "github.com/tektoncd/pipeline/pkg/apis/pipeline/pod" @@ -119,6 +121,7 @@ func (b *Builder) Build(ctx context.Context, taskRun *v1beta1.TaskRun, taskSpec implicitEnvVars := []corev1.EnvVar{} featureFlags := config.FromContextOrDefaults(ctx).FeatureFlags alphaAPIEnabled := featureFlags.EnableAPIFields == config.AlphaAPIFields + sidecarLogsResultsEnabled := config.FromContextOrDefaults(ctx).FeatureFlags.ResultExtractionMethod == config.ResultExtractionMethodSidecarLogs // Add our implicit volumes first, so they can be overridden by the user if they prefer. volumes = append(volumes, implicitVolumes...) @@ -127,10 +130,12 @@ func (b *Builder) Build(ctx context.Context, taskRun *v1beta1.TaskRun, taskSpec // Create Volumes and VolumeMounts for any credentials found in annotated // Secrets, along with any arguments needed by Step entrypoints to process // those secrets. + commonExtraEntrypointArgs := []string{} credEntrypointArgs, credVolumes, credVolumeMounts, err := credsInit(ctx, taskRun.Spec.ServiceAccountName, taskRun.Namespace, b.KubeClient) if err != nil { return nil, err } + commonExtraEntrypointArgs = append(commonExtraEntrypointArgs, credEntrypointArgs...) volumes = append(volumes, credVolumes...) volumeMounts = append(volumeMounts, credVolumeMounts...) @@ -147,6 +152,12 @@ func (b *Builder) Build(ctx context.Context, taskRun *v1beta1.TaskRun, taskSpec if alphaAPIEnabled && taskRun.Spec.ComputeResources != nil { tasklevel.ApplyTaskLevelComputeResources(steps, taskRun.Spec.ComputeResources) } + if sidecarLogsResultsEnabled && taskSpec.Results != nil { + // create a results sidecar + resultsSidecar := createResultsSidecar(taskSpec, b.Images.SidecarLogResultsImage) + taskSpec.Sidecars = append(taskSpec.Sidecars, resultsSidecar) + commonExtraEntrypointArgs = append(commonExtraEntrypointArgs, "-result_from", config.ResultExtractionMethodSidecarLogs) + } sidecars, err := v1beta1.MergeSidecarsWithOverrides(taskSpec.Sidecars, taskRun.Spec.SidecarOverrides) if err != nil { return nil, err @@ -192,9 +203,9 @@ func (b *Builder) Build(ctx context.Context, taskRun *v1beta1.TaskRun, taskSpec readyImmediately := isPodReadyImmediately(*featureFlags, taskSpec.Sidecars) if alphaAPIEnabled { - stepContainers, err = orderContainers(credEntrypointArgs, stepContainers, &taskSpec, taskRun.Spec.Debug, !readyImmediately) + stepContainers, err = orderContainers(commonExtraEntrypointArgs, stepContainers, &taskSpec, taskRun.Spec.Debug, !readyImmediately) } else { - stepContainers, err = orderContainers(credEntrypointArgs, stepContainers, &taskSpec, nil, !readyImmediately) + stepContainers, err = orderContainers(commonExtraEntrypointArgs, stepContainers, &taskSpec, nil, !readyImmediately) } if err != nil { return nil, err @@ -259,6 +270,28 @@ func (b *Builder) Build(ctx context.Context, taskRun *v1beta1.TaskRun, taskSpec stepContainers[i].VolumeMounts = vms } + if sidecarLogsResultsEnabled && taskSpec.Results != nil { + // Mount implicit volumes onto sidecarContainers + // so that they can access /tekton/results and /tekton/run. + for i, s := range sidecarContainers { + for j := 0; j < len(stepContainers); j++ { + s.VolumeMounts = append(s.VolumeMounts, runMount(j, true)) + } + requestedVolumeMounts := map[string]bool{} + for _, vm := range s.VolumeMounts { + requestedVolumeMounts[filepath.Clean(vm.MountPath)] = true + } + var toAdd []corev1.VolumeMount + for _, imp := range volumeMounts { + if !requestedVolumeMounts[filepath.Clean(imp.MountPath)] { + toAdd = append(toAdd, imp) + } + } + vms := append(s.VolumeMounts, toAdd...) //nolint + sidecarContainers[i].VolumeMounts = vms + } + } + // This loop: // - sets container name to add "step-" prefix or "step-unnamed-#" if not specified. // TODO(#1605): Remove this loop and make each transformation in @@ -397,7 +430,7 @@ func isPodReadyImmediately(featureFlags config.FeatureFlags, sidecars []v1beta1. func runMount(i int, ro bool) corev1.VolumeMount { return corev1.VolumeMount{ Name: fmt.Sprintf("%s-%d", runVolumeName, i), - MountPath: filepath.Join(runDir, strconv.Itoa(i)), + MountPath: filepath.Join(RunDir, strconv.Itoa(i)), ReadOnly: ro, } } @@ -435,3 +468,18 @@ func entrypointInitContainer(image string, steps []v1beta1.Step) corev1.Containe } return prepareInitContainer } + +// createResultsSidecar creates a sidecar that will run the sidecarlogresults binary. +func createResultsSidecar(taskSpec v1beta1.TaskSpec, image string) v1beta1.Sidecar { + names := make([]string, 0, len(taskSpec.Results)) + for _, r := range taskSpec.Results { + names = append(names, r.Name) + } + resultsStr := strings.Join(names, ",") + command := []string{"/ko-app/sidecarlogresults", "-results-dir", pipeline.DefaultResultPath, "-result-names", resultsStr} + return v1beta1.Sidecar{ + Name: sidecarlogsvalidation.ReservedResultsSidecarName, + Image: image, + Command: command, + } +} diff --git a/pkg/pod/pod_test.go b/pkg/pod/pod_test.go index 7e5a7817d74..6cc355d1f75 100644 --- a/pkg/pod/pod_test.go +++ b/pkg/pod/pod_test.go @@ -31,6 +31,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/tektoncd/pipeline/internal/sidecarlogsvalidation" "github.com/tektoncd/pipeline/pkg/apis/config" "github.com/tektoncd/pipeline/pkg/apis/pipeline" "github.com/tektoncd/pipeline/pkg/apis/pipeline/pod" @@ -38,6 +39,7 @@ import ( "github.com/tektoncd/pipeline/test/diff" "github.com/tektoncd/pipeline/test/names" corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" fakek8s "k8s.io/client-go/kubernetes/fake" @@ -1786,6 +1788,76 @@ _EOF_ }}, ActiveDeadlineSeconds: &defaultActiveDeadlineSeconds, }, + }, { + desc: "sidecar logs enabled", + featureFlags: map[string]string{"results-from": "sidecar-logs"}, + ts: v1beta1.TaskSpec{ + Results: []v1beta1.TaskResult{{ + Name: "foo", + Type: v1beta1.ResultsTypeString, + }}, + Steps: []v1beta1.Step{{ + Name: "name", + Image: "image", + Command: []string{"cmd"}, // avoid entrypoint lookup. + }}, + }, + want: &corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyNever, + InitContainers: []corev1.Container{ + entrypointInitContainer(images.EntrypointImage, []v1beta1.Step{{Name: "name"}}), + }, + Containers: []corev1.Container{{ + Name: "step-name", + Image: "image", + Command: []string{"/tekton/bin/entrypoint"}, + Args: []string{ + "-wait_file", + "/tekton/downward/ready", + "-wait_file_content", + "-post_file", + "/tekton/run/0/out", + "-termination_path", + "/tekton/termination", + "-step_metadata_dir", + "/tekton/run/0/status", + "-result_from", + "sidecar-logs", + "-results", + "foo", + "-entrypoint", + "cmd", + "--", + }, + VolumeMounts: append([]corev1.VolumeMount{binROMount, runMount(0, false), downwardMount, { + Name: "tekton-creds-init-home-0", + MountPath: "/tekton/creds", + }}, implicitVolumeMounts...), + TerminationMessagePath: "/tekton/termination", + }, { + Name: fmt.Sprintf("sidecar-%s", sidecarlogsvalidation.ReservedResultsSidecarName), + Image: "", + Command: []string{ + "/ko-app/sidecarlogresults", + "-results-dir", + "/tekton/results", + "-result-names", + "foo", + }, + Resources: corev1.ResourceRequirements{ + Requests: nil, + }, + VolumeMounts: append([]v1.VolumeMount{ + {Name: "tekton-internal-bin", ReadOnly: true, MountPath: "/tekton/bin"}, + {Name: "tekton-internal-run-0", ReadOnly: true, MountPath: "/tekton/run/0"}, + }, implicitVolumeMounts...), + }}, + Volumes: append(implicitVolumes, binVolume, runVolume(0), downwardVolume, corev1.Volume{ + Name: "tekton-creds-init-home-0", + VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: corev1.StorageMediumMemory}}, + }), + ActiveDeadlineSeconds: &defaultActiveDeadlineSeconds, + }, }} { t.Run(c.desc, func(t *testing.T) { names.TestingSeed() diff --git a/pkg/pod/script.go b/pkg/pod/script.go index 019d4a3e30d..99a9c6f8a02 100644 --- a/pkg/pod/script.go +++ b/pkg/pod/script.go @@ -211,10 +211,10 @@ cat > ${scriptfile} << '%s' } debugScripts := []script{{ name: "continue", - content: defaultScriptPreamble + fmt.Sprintf(debugContinueScriptTemplate, len(steps), debugInfoDir, runDir), + content: defaultScriptPreamble + fmt.Sprintf(debugContinueScriptTemplate, len(steps), debugInfoDir, RunDir), }, { name: "fail-continue", - content: defaultScriptPreamble + fmt.Sprintf(debugFailScriptTemplate, len(steps), debugInfoDir, runDir), + content: defaultScriptPreamble + fmt.Sprintf(debugFailScriptTemplate, len(steps), debugInfoDir, RunDir), }} // Add debug or breakpoint related scripts to /tekton/debug/scripts