Skip to content

Commit fcba522

Browse files
pritidesaitekton-robot
authored andcommitted
Added signal handling in SidecarLog results
In the native Kubernetes sidecar model, sidecars are implemented as init containers with a `RestartPolicy: Always`, which means they run for the entire duration of the pod's lifecycle. However, the current Tekton sidecar log results implementation doesn't account for this behavior and exits after processing results, causing the container to restart repeatedly. Signed-off-by: Priti Desai <[email protected]>
1 parent b56477c commit fcba522

File tree

4 files changed

+404
-0
lines changed

4 files changed

+404
-0
lines changed

cmd/sidecarlogresults/main.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ import (
2121
"flag"
2222
"log"
2323
"os"
24+
"os/signal"
2425
"strings"
26+
"syscall"
2527

2628
"github.com/tektoncd/pipeline/internal/sidecarlogresults"
2729
"github.com/tektoncd/pipeline/pkg/apis/pipeline"
@@ -33,13 +35,37 @@ func main() {
3335
var resultNames string
3436
var stepResultsStr string
3537
var stepNames string
38+
var kubernetesNativeSidecar bool
3639

3740
flag.StringVar(&resultsDir, "results-dir", pipeline.DefaultResultPath, "Path to the results directory. Default is /tekton/results")
3841
flag.StringVar(&resultNames, "result-names", "", "comma separated result names to expect from the steps running in the pod. eg. foo,bar,baz")
3942
flag.StringVar(&stepResultsStr, "step-results", "", "json containing a map of step Name as key and list of result Names. eg. {\"stepName\":[\"foo\",\"bar\",\"baz\"]}")
4043
flag.StringVar(&stepNames, "step-names", "", "comma separated step names. eg. foo,bar,baz")
44+
flag.BoolVar(&kubernetesNativeSidecar, "kubernetes-sidecar-mode", false, "If true, wait indefinitely after processing results (for Kubernetes native sidecar support)")
4145
flag.Parse()
4246

47+
var done chan bool
48+
// If kubernetesNativeSidecar is true, wait indefinitely to prevent container from exiting
49+
// This is needed for Kubernetes native sidecar support
50+
if kubernetesNativeSidecar {
51+
// Set up signal handling for graceful shutdown
52+
// Create a channel to receive OS signals.
53+
sigCh := make(chan os.Signal, 1)
54+
55+
// Register the channel to receive notifications for specific signals.
56+
// In this case, we are interested in SIGINT (Ctrl+C) and SIGTERM.
57+
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)
58+
59+
// Create a channel to signal that the program should exit gracefully.
60+
done = make(chan bool, 1)
61+
62+
// Start a goroutine to handle incoming signals.
63+
go func() {
64+
<-sigCh // Block until a signal is received.
65+
done <- true // Signal that cleanup is done and the program can exit.
66+
}()
67+
}
68+
4369
var expectedResults []string
4470
// strings.Split returns [""] instead of [] for empty string, we don't want pass [""] to other methods.
4571
if len(resultNames) > 0 {
@@ -62,4 +88,9 @@ func main() {
6288
if err != nil {
6389
log.Fatal(err)
6490
}
91+
92+
if kubernetesNativeSidecar && done != nil {
93+
// Wait for a signal to be received.
94+
<-done
95+
}
6596
}

cmd/sidecarlogresults/main_test.go

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/*
2+
Copyright 2025 The Tekton Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package main
18+
19+
import (
20+
"flag"
21+
"os"
22+
"os/signal"
23+
"syscall"
24+
"testing"
25+
"time"
26+
)
27+
28+
func TestParseFlags(t *testing.T) {
29+
// Save original command line arguments and restore them after the test
30+
oldArgs := os.Args
31+
defer func() { os.Args = oldArgs }()
32+
33+
// Save original flagset and restore after test
34+
oldFlagCommandLine := flag.CommandLine
35+
defer func() { flag.CommandLine = oldFlagCommandLine }()
36+
37+
testCases := []struct {
38+
name string
39+
args []string
40+
wantResultsDir string
41+
wantResultNames string
42+
wantStepResults string
43+
wantStepNames string
44+
wantKubernetesSidecarMode bool
45+
}{
46+
{
47+
name: "default values",
48+
args: []string{"cmd"},
49+
wantResultsDir: "/tekton/results",
50+
wantResultNames: "",
51+
wantStepResults: "",
52+
wantStepNames: "",
53+
wantKubernetesSidecarMode: false,
54+
},
55+
{
56+
name: "custom values",
57+
args: []string{"cmd", "-results-dir", "/custom/results", "-result-names", "foo,bar", "-step-results", "{\"step1\":[\"res1\"]}", "-step-names", "step1,step2", "-kubernetes-sidecar-mode", "true"},
58+
wantResultsDir: "/custom/results",
59+
wantResultNames: "foo,bar",
60+
wantStepResults: "{\"step1\":[\"res1\"]}",
61+
wantStepNames: "step1,step2",
62+
wantKubernetesSidecarMode: true,
63+
},
64+
}
65+
66+
for _, tc := range testCases {
67+
t.Run(tc.name, func(t *testing.T) {
68+
// Reset flag.CommandLine to simulate fresh flag parsing
69+
flag.CommandLine = flag.NewFlagSet(tc.args[0], flag.ExitOnError)
70+
71+
// Set up the test arguments
72+
os.Args = tc.args
73+
74+
// Define the variables that would be set by flag.Parse()
75+
var resultsDir string
76+
var resultNames string
77+
var stepResultsStr string
78+
var stepNames string
79+
var kubernetesNativeSidecar bool
80+
81+
// Define the flags
82+
flag.StringVar(&resultsDir, "results-dir", "/tekton/results", "Path to the results directory")
83+
flag.StringVar(&resultNames, "result-names", "", "comma separated result names")
84+
flag.StringVar(&stepResultsStr, "step-results", "", "json containing map of step name to results")
85+
flag.StringVar(&stepNames, "step-names", "", "comma separated step names")
86+
flag.BoolVar(&kubernetesNativeSidecar, "kubernetes-sidecar-mode", false, "If true, run in Kubernetes native sidecar mode")
87+
88+
// Parse the flags
89+
flag.Parse()
90+
91+
// Check the results
92+
if resultsDir != tc.wantResultsDir {
93+
t.Errorf("resultsDir = %q, want %q", resultsDir, tc.wantResultsDir)
94+
}
95+
if resultNames != tc.wantResultNames {
96+
t.Errorf("resultNames = %q, want %q", resultNames, tc.wantResultNames)
97+
}
98+
if stepResultsStr != tc.wantStepResults {
99+
t.Errorf("stepResultsStr = %q, want %q", stepResultsStr, tc.wantStepResults)
100+
}
101+
if stepNames != tc.wantStepNames {
102+
t.Errorf("stepNames = %q, want %q", stepNames, tc.wantStepNames)
103+
}
104+
if kubernetesNativeSidecar != tc.wantKubernetesSidecarMode {
105+
t.Errorf("kubernetesNativeSidecar = %v, want %v", kubernetesNativeSidecar, tc.wantKubernetesSidecarMode)
106+
}
107+
})
108+
}
109+
}
110+
111+
// This test is a bit tricky since it involves an infinite loop when kubernetesNativeSidecar is true.
112+
// We'll use a timeout mechanism to verify the behavior.
113+
func TestKubernetesSidecarMode(t *testing.T) {
114+
// Create a channel to signal completion
115+
done := make(chan bool)
116+
117+
// Start a goroutine that simulates the kubernetes sidecar mode behavior
118+
go func() {
119+
// Simulate the kubernetes sidecar mode behavior
120+
if true {
121+
// In the real code, this would be an infinite select{} loop
122+
// For testing, we'll just signal that we've reached this point
123+
done <- true
124+
// Then wait to simulate the infinite loop
125+
time.Sleep(100 * time.Millisecond)
126+
}
127+
// This should not be reached when kubernetesNativeSidecar is true
128+
done <- false
129+
}()
130+
131+
// Wait for the goroutine to signal or timeout
132+
select {
133+
case reached := <-done:
134+
if !reached {
135+
t.Error("kubernetes sidecar mode code path was not executed correctly")
136+
}
137+
case <-time.After(50 * time.Millisecond):
138+
t.Error("Timed out waiting for kubernetes sidecar mode code path")
139+
}
140+
}
141+
142+
// TestSignalHandling tests that the signal handling works correctly
143+
func TestSignalHandling(t *testing.T) {
144+
// Create channels for test coordination
145+
setupDone := make(chan bool)
146+
signalProcessed := make(chan bool)
147+
148+
// Start a goroutine that simulates the signal handling behavior
149+
go func() {
150+
// Set up signal handling
151+
sigCh := make(chan os.Signal, 1)
152+
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)
153+
154+
// Signal that setup is complete
155+
setupDone <- true
156+
157+
// Wait for signal
158+
sig := <-sigCh
159+
160+
// Verify we got the expected signal
161+
if sig == syscall.SIGTERM {
162+
signalProcessed <- true
163+
} else {
164+
signalProcessed <- false
165+
}
166+
}()
167+
168+
// Wait for signal handling setup to complete
169+
select {
170+
case <-setupDone:
171+
// Setup completed successfully
172+
case <-time.After(100 * time.Millisecond):
173+
t.Fatal("Timed out waiting for signal handler setup")
174+
}
175+
176+
// Send a SIGTERM signal to the process
177+
// Note: In a real test environment, we'd use a process.Signal() call
178+
// but for this test we'll directly send to the channel
179+
p, err := os.FindProcess(os.Getpid())
180+
if err != nil {
181+
t.Fatalf("Failed to find process: %v", err)
182+
}
183+
184+
// Send SIGTERM to the process
185+
err = p.Signal(syscall.SIGTERM)
186+
if err != nil {
187+
t.Fatalf("Failed to send signal: %v", err)
188+
}
189+
190+
// Wait for signal to be processed or timeout
191+
select {
192+
case success := <-signalProcessed:
193+
if !success {
194+
t.Error("Signal handler received unexpected signal type")
195+
}
196+
case <-time.After(100 * time.Millisecond):
197+
t.Error("Timed out waiting for signal to be processed")
198+
}
199+
}

pkg/pod/pod.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,22 @@ func (b *Builder) Build(ctx context.Context, taskRun *v1.TaskRun, taskSpec v1.Ta
441441
sc := &sidecarContainers[i]
442442
always := corev1.ContainerRestartPolicyAlways
443443
sc.RestartPolicy = &always
444+
445+
// For the results sidecar specifically, ensure it has the kubernetes-sidecar-mode flag
446+
// to prevent it from exiting and restarting
447+
if sc.Name == pipeline.ReservedResultsSidecarName {
448+
kubernetesSidecarModeFound := false
449+
for j, arg := range sc.Command {
450+
if arg == "-kubernetes-sidecar-mode" && j+1 < len(sc.Command) {
451+
kubernetesSidecarModeFound = true
452+
break
453+
}
454+
}
455+
if !kubernetesSidecarModeFound {
456+
sc.Command = append(sc.Command, "-kubernetes-sidecar-mode", "true")
457+
}
458+
}
459+
444460
sc.Name = names.SimpleNameGenerator.RestrictLength(fmt.Sprintf("%v%v", sidecarPrefix, sc.Name))
445461
mergedPodInitContainers = append(mergedPodInitContainers, *sc)
446462
}
@@ -658,6 +674,13 @@ func createResultsSidecar(taskSpec v1.TaskSpec, image string, securityContext Se
658674
if len(stepResultsBytes) > 0 {
659675
command = append(command, "-step-results", string(stepResultsBytes))
660676
}
677+
678+
// When using Kubernetes native sidecar support, add the kubernetes-sidecar-mode flag
679+
// to prevent the sidecar from exiting after processing results
680+
if config.FromContextOrDefaults(context.Background()).FeatureFlags.EnableKubernetesSidecar {
681+
command = append(command, "-kubernetes-sidecar-mode", "true")
682+
}
683+
661684
sidecar := v1.Sidecar{
662685
Name: pipeline.ReservedResultsSidecarName,
663686
Image: image,

0 commit comments

Comments
 (0)