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
31 changes: 31 additions & 0 deletions cmd/sidecarlogresults/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import (
"flag"
"log"
"os"
"os/signal"
"strings"
"syscall"

"github.com/tektoncd/pipeline/internal/sidecarlogresults"
"github.com/tektoncd/pipeline/pkg/apis/pipeline"
Expand All @@ -33,13 +35,37 @@ func main() {
var resultNames string
var stepResultsStr string
var stepNames string
var kubernetesNativeSidecar bool

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.StringVar(&stepResultsStr, "step-results", "", "json containing a map of step Name as key and list of result Names. eg. {\"stepName\":[\"foo\",\"bar\",\"baz\"]}")
flag.StringVar(&stepNames, "step-names", "", "comma separated step names. eg. foo,bar,baz")
flag.BoolVar(&kubernetesNativeSidecar, "kubernetes-sidecar-mode", false, "If true, wait indefinitely after processing results (for Kubernetes native sidecar support)")
flag.Parse()

var done chan bool
// If kubernetesNativeSidecar is true, wait indefinitely to prevent container from exiting
// This is needed for Kubernetes native sidecar support
if kubernetesNativeSidecar {
// Set up signal handling for graceful shutdown
// Create a channel to receive OS signals.
sigCh := make(chan os.Signal, 1)

// Register the channel to receive notifications for specific signals.
// In this case, we are interested in SIGINT (Ctrl+C) and SIGTERM.
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)

// Create a channel to signal that the program should exit gracefully.
done = make(chan bool, 1)

// Start a goroutine to handle incoming signals.
go func() {
<-sigCh // Block until a signal is received.
done <- true // Signal that cleanup is done and the program can exit.
}()
}

var expectedResults []string
// strings.Split returns [""] instead of [] for empty string, we don't want pass [""] to other methods.
if len(resultNames) > 0 {
Expand All @@ -62,4 +88,9 @@ func main() {
if err != nil {
log.Fatal(err)
}

if kubernetesNativeSidecar && done != nil {
// Wait for a signal to be received.
<-done
}
}
199 changes: 199 additions & 0 deletions cmd/sidecarlogresults/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/*
Copyright 2025 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"
"os"
"os/signal"
"syscall"
"testing"
"time"
)

func TestParseFlags(t *testing.T) {
// Save original command line arguments and restore them after the test
oldArgs := os.Args
defer func() { os.Args = oldArgs }()

// Save original flagset and restore after test
oldFlagCommandLine := flag.CommandLine
defer func() { flag.CommandLine = oldFlagCommandLine }()

testCases := []struct {
name string
args []string
wantResultsDir string
wantResultNames string
wantStepResults string
wantStepNames string
wantKubernetesSidecarMode bool
}{
{
name: "default values",
args: []string{"cmd"},
wantResultsDir: "/tekton/results",
wantResultNames: "",
wantStepResults: "",
wantStepNames: "",
wantKubernetesSidecarMode: false,
},
{
name: "custom values",
args: []string{"cmd", "-results-dir", "/custom/results", "-result-names", "foo,bar", "-step-results", "{\"step1\":[\"res1\"]}", "-step-names", "step1,step2", "-kubernetes-sidecar-mode", "true"},
wantResultsDir: "/custom/results",
wantResultNames: "foo,bar",
wantStepResults: "{\"step1\":[\"res1\"]}",
wantStepNames: "step1,step2",
wantKubernetesSidecarMode: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Reset flag.CommandLine to simulate fresh flag parsing
flag.CommandLine = flag.NewFlagSet(tc.args[0], flag.ExitOnError)

// Set up the test arguments
os.Args = tc.args

// Define the variables that would be set by flag.Parse()
var resultsDir string
var resultNames string
var stepResultsStr string
var stepNames string
var kubernetesNativeSidecar bool

// Define the flags
flag.StringVar(&resultsDir, "results-dir", "/tekton/results", "Path to the results directory")
flag.StringVar(&resultNames, "result-names", "", "comma separated result names")
flag.StringVar(&stepResultsStr, "step-results", "", "json containing map of step name to results")
flag.StringVar(&stepNames, "step-names", "", "comma separated step names")
flag.BoolVar(&kubernetesNativeSidecar, "kubernetes-sidecar-mode", false, "If true, run in Kubernetes native sidecar mode")

// Parse the flags
flag.Parse()

// Check the results
if resultsDir != tc.wantResultsDir {
t.Errorf("resultsDir = %q, want %q", resultsDir, tc.wantResultsDir)
}
if resultNames != tc.wantResultNames {
t.Errorf("resultNames = %q, want %q", resultNames, tc.wantResultNames)
}
if stepResultsStr != tc.wantStepResults {
t.Errorf("stepResultsStr = %q, want %q", stepResultsStr, tc.wantStepResults)
}
if stepNames != tc.wantStepNames {
t.Errorf("stepNames = %q, want %q", stepNames, tc.wantStepNames)
}
if kubernetesNativeSidecar != tc.wantKubernetesSidecarMode {
t.Errorf("kubernetesNativeSidecar = %v, want %v", kubernetesNativeSidecar, tc.wantKubernetesSidecarMode)
}
})
}
}

// This test is a bit tricky since it involves an infinite loop when kubernetesNativeSidecar is true.
// We'll use a timeout mechanism to verify the behavior.
func TestKubernetesSidecarMode(t *testing.T) {
// Create a channel to signal completion
done := make(chan bool)

// Start a goroutine that simulates the kubernetes sidecar mode behavior
go func() {
// Simulate the kubernetes sidecar mode behavior
if true {
// In the real code, this would be an infinite select{} loop
// For testing, we'll just signal that we've reached this point
done <- true
// Then wait to simulate the infinite loop
time.Sleep(100 * time.Millisecond)
}
// This should not be reached when kubernetesNativeSidecar is true
done <- false
}()

// Wait for the goroutine to signal or timeout
select {
case reached := <-done:
if !reached {
t.Error("kubernetes sidecar mode code path was not executed correctly")
}
case <-time.After(50 * time.Millisecond):
t.Error("Timed out waiting for kubernetes sidecar mode code path")
}
}

// TestSignalHandling tests that the signal handling works correctly
func TestSignalHandling(t *testing.T) {
// Create channels for test coordination
setupDone := make(chan bool)
signalProcessed := make(chan bool)

// Start a goroutine that simulates the signal handling behavior
go func() {
// Set up signal handling
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)

// Signal that setup is complete
setupDone <- true

// Wait for signal
sig := <-sigCh

// Verify we got the expected signal
if sig == syscall.SIGTERM {
signalProcessed <- true
} else {
signalProcessed <- false
}
}()

// Wait for signal handling setup to complete
select {
case <-setupDone:
// Setup completed successfully
case <-time.After(100 * time.Millisecond):
t.Fatal("Timed out waiting for signal handler setup")
}

// Send a SIGTERM signal to the process
// Note: In a real test environment, we'd use a process.Signal() call
// but for this test we'll directly send to the channel
p, err := os.FindProcess(os.Getpid())
if err != nil {
t.Fatalf("Failed to find process: %v", err)
}

// Send SIGTERM to the process
err = p.Signal(syscall.SIGTERM)
if err != nil {
t.Fatalf("Failed to send signal: %v", err)
}

// Wait for signal to be processed or timeout
select {
case success := <-signalProcessed:
if !success {
t.Error("Signal handler received unexpected signal type")
}
case <-time.After(100 * time.Millisecond):
t.Error("Timed out waiting for signal to be processed")
}
}
23 changes: 23 additions & 0 deletions pkg/pod/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,22 @@ func (b *Builder) Build(ctx context.Context, taskRun *v1.TaskRun, taskSpec v1.Ta
sc := &sidecarContainers[i]
always := corev1.ContainerRestartPolicyAlways
sc.RestartPolicy = &always

// For the results sidecar specifically, ensure it has the kubernetes-sidecar-mode flag
// to prevent it from exiting and restarting
if sc.Name == pipeline.ReservedResultsSidecarName {
kubernetesSidecarModeFound := false
for j, arg := range sc.Command {
if arg == "-kubernetes-sidecar-mode" && j+1 < len(sc.Command) {
kubernetesSidecarModeFound = true
break
}
}
if !kubernetesSidecarModeFound {
sc.Command = append(sc.Command, "-kubernetes-sidecar-mode", "true")
}
}

sc.Name = names.SimpleNameGenerator.RestrictLength(fmt.Sprintf("%v%v", sidecarPrefix, sc.Name))
mergedPodInitContainers = append(mergedPodInitContainers, *sc)
}
Expand Down Expand Up @@ -658,6 +674,13 @@ func createResultsSidecar(taskSpec v1.TaskSpec, image string, securityContext Se
if len(stepResultsBytes) > 0 {
command = append(command, "-step-results", string(stepResultsBytes))
}

// When using Kubernetes native sidecar support, add the kubernetes-sidecar-mode flag
// to prevent the sidecar from exiting after processing results
if config.FromContextOrDefaults(context.Background()).FeatureFlags.EnableKubernetesSidecar {
command = append(command, "-kubernetes-sidecar-mode", "true")
}

sidecar := v1.Sidecar{
Name: pipeline.ReservedResultsSidecarName,
Image: image,
Expand Down
Loading
Loading