From 613d0e658e1601e73033b22c5d39c878faec9d25 Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Thu, 22 May 2025 10:58:11 +0300 Subject: [PATCH 01/22] feat: timeout the postStart hook commands Signed-off-by: Oleksii Kurinnyi --- .../devworkspaceoperatorconfig_types.go | 10 ++ .../workspace/devworkspace_controller.go | 4 +- pkg/config/sync.go | 4 + pkg/library/container/container.go | 4 +- pkg/library/lifecycle/poststart.go | 67 ++++++++++-- pkg/library/status/check.go | 102 +++++++++++++++++- 6 files changed, 173 insertions(+), 18 deletions(-) diff --git a/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go b/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go index 4d0303630..d4f68c122 100644 --- a/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go +++ b/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go @@ -189,6 +189,16 @@ type WorkspaceConfig struct { RuntimeClassName *string `json:"runtimeClassName,omitempty"` // CleanupCronJobConfig defines configuration options for a cron job that automatically cleans up stale DevWorkspaces. CleanupCronJob *CleanupCronJobConfig `json:"cleanupCronJob,omitempty"` + // PostStartTimeout defines the maximum duration the PostStart hook can run + // before it is automatically failed. This timeout is used for the postStart lifecycle hook + // that is used to run commands in the workspace container. The timeout is specified in seconds. + // If not specified, the timeout is disabled (0 seconds). + // +kubebuilder:validation:Minimum=0 + // +kubebuilder:default:=0 + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Type=integer + // +kubebuilder:validation:Format=int32 + PostStartTimeout *int32 `json:"postStartTimeout,omitempty"` } type WebhookConfig struct { diff --git a/controllers/workspace/devworkspace_controller.go b/controllers/workspace/devworkspace_controller.go index 0fecaa364..31fc05d36 100644 --- a/controllers/workspace/devworkspace_controller.go +++ b/controllers/workspace/devworkspace_controller.go @@ -327,7 +327,9 @@ func (r *DevWorkspaceReconciler) Reconcile(ctx context.Context, req ctrl.Request &workspace.Spec.Template, workspace.Config.Workspace.ContainerSecurityContext, workspace.Config.Workspace.ImagePullPolicy, - workspace.Config.Workspace.DefaultContainerResources) + workspace.Config.Workspace.DefaultContainerResources, + workspace.Config.Workspace.PostStartTimeout, + ) if err != nil { return r.failWorkspace(workspace, fmt.Sprintf("Error processing devfile: %s", err), metrics.ReasonBadRequest, reqLogger, &reconcileStatus), nil } diff --git a/pkg/config/sync.go b/pkg/config/sync.go index c5b8150f7..d1ae7acc3 100644 --- a/pkg/config/sync.go +++ b/pkg/config/sync.go @@ -431,6 +431,10 @@ func mergeConfig(from, to *controller.OperatorConfiguration) { to.Workspace.CleanupCronJob.Schedule = from.Workspace.CleanupCronJob.Schedule } } + + if from.Workspace.PostStartTimeout != nil { + to.Workspace.PostStartTimeout = from.Workspace.PostStartTimeout + } } } diff --git a/pkg/library/container/container.go b/pkg/library/container/container.go index c69bc4b40..c4c3587ae 100644 --- a/pkg/library/container/container.go +++ b/pkg/library/container/container.go @@ -45,7 +45,7 @@ import ( // rewritten as Volumes are added to PodAdditions, in order to support e.g. using one PVC to hold all volumes // // Note: Requires DevWorkspace to be flattened (i.e. the DevWorkspace contains no Parent or Components of type Plugin) -func GetKubeContainersFromDevfile(workspace *dw.DevWorkspaceTemplateSpec, securityContext *corev1.SecurityContext, pullPolicy string, defaultResources *corev1.ResourceRequirements) (*v1alpha1.PodAdditions, error) { +func GetKubeContainersFromDevfile(workspace *dw.DevWorkspaceTemplateSpec, securityContext *corev1.SecurityContext, pullPolicy string, defaultResources *corev1.ResourceRequirements, postStartTimeout *int32) (*v1alpha1.PodAdditions, error) { if !flatten.DevWorkspaceIsFlattened(workspace, nil) { return nil, fmt.Errorf("devfile is not flattened") } @@ -77,7 +77,7 @@ func GetKubeContainersFromDevfile(workspace *dw.DevWorkspaceTemplateSpec, securi podAdditions.Containers = append(podAdditions.Containers, *k8sContainer) } - if err := lifecycle.AddPostStartLifecycleHooks(workspace, podAdditions.Containers); err != nil { + if err := lifecycle.AddPostStartLifecycleHooks(workspace, podAdditions.Containers, postStartTimeout); err != nil { return nil, err } diff --git a/pkg/library/lifecycle/poststart.go b/pkg/library/lifecycle/poststart.go index 572efa60c..f88a2f6f1 100644 --- a/pkg/library/lifecycle/poststart.go +++ b/pkg/library/lifecycle/poststart.go @@ -21,12 +21,20 @@ import ( corev1 "k8s.io/api/core/v1" ) -const redirectOutputFmt = `{ -%s -} 1>/tmp/poststart-stdout.txt 2>/tmp/poststart-stderr.txt +const ( + // `tee` both stdout and stderr to files and to the main output streams. + redirectOutputFmt = `{ + # This script block ensures its exit code is preserved + # while its stdout and stderr are tee'd. + _script_to_run() { + %s # This will be replaced by scriptWithTimeout + } + _script_to_run +} 1> >(tee -a "/tmp/poststart-stdout.txt") 2> >(tee -a "/tmp/poststart-stderr.txt" >&2) ` +) -func AddPostStartLifecycleHooks(wksp *dw.DevWorkspaceTemplateSpec, containers []corev1.Container) error { +func AddPostStartLifecycleHooks(wksp *dw.DevWorkspaceTemplateSpec, containers []corev1.Container, postStartTimeout *int32) error { if wksp.Events == nil || len(wksp.Events.PostStart) == 0 { return nil } @@ -54,7 +62,7 @@ func AddPostStartLifecycleHooks(wksp *dw.DevWorkspaceTemplateSpec, containers [] return fmt.Errorf("failed to process postStart event %s: %w", commands[0].Id, err) } - postStartHandler, err := processCommandsForPostStart(commands) + postStartHandler, err := processCommandsForPostStart(commands, postStartTimeout) if err != nil { return fmt.Errorf("failed to process postStart event %s: %w", commands[0].Id, err) } @@ -79,27 +87,64 @@ func AddPostStartLifecycleHooks(wksp *dw.DevWorkspaceTemplateSpec, containers [] // - | // cd // -func processCommandsForPostStart(commands []dw.Command) (*corev1.LifecycleHandler, error) { - var dwCommands []string +func processCommandsForPostStart(commands []dw.Command, postStartTimeout *int32) (*corev1.LifecycleHandler, error) { + var commandScriptLines []string for _, command := range commands { execCmd := command.Exec if len(execCmd.Env) > 0 { return nil, fmt.Errorf("env vars in postStart command %s are unsupported", command.Id) } + var singleCommandParts []string if execCmd.WorkingDir != "" { - dwCommands = append(dwCommands, fmt.Sprintf("cd %s", execCmd.WorkingDir)) + // Safely quote the working directory path + safeWorkingDir := strings.ReplaceAll(execCmd.WorkingDir, "'", `'\''`) + singleCommandParts = append(singleCommandParts, fmt.Sprintf("cd '%s'", safeWorkingDir)) + } + if execCmd.CommandLine != "" { + singleCommandParts = append(singleCommandParts, execCmd.CommandLine) + } + if len(singleCommandParts) > 0 { + commandScriptLines = append(commandScriptLines, strings.Join(singleCommandParts, " && ")) } - dwCommands = append(dwCommands, execCmd.CommandLine) } - joinedCommands := strings.Join(dwCommands, "\n") + originalUserScript := strings.Join(commandScriptLines, "\n") + + scriptToExecute := "set -e\n" + originalUserScript + escapedUserScript := strings.ReplaceAll(scriptToExecute, "'", `'\''`) + + scriptWithTimeout := fmt.Sprintf(` +export POSTSTART_TIMEOUT_DURATION="%d" +export POSTSTART_KILL_AFTER_DURATION="5" + +echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} s, kill after: ${POSTSTART_KILL_AFTER_DURATION} s" >&2 + +# Run the user's script under the 'timeout' utility. +timeout --preserve-status --kill-after="${POSTSTART_KILL_AFTER_DURATION}" "${POSTSTART_TIMEOUT_DURATION}" /bin/sh -c '%s' +exit_code=$? + +# Check the exit code from 'timeout' +if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) + echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 +elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) + echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 +elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code, including 124 + echo "[postStart hook] Commands failed with exit code $exit_code." >&2 +else + echo "[postStart hook] Commands completed successfully within the time limit." >&2 +fi + +exit $exit_code +`, *postStartTimeout, escapedUserScript) + + finalScriptForHook := fmt.Sprintf(redirectOutputFmt, scriptWithTimeout) handler := &corev1.LifecycleHandler{ Exec: &corev1.ExecAction{ Command: []string{ "/bin/sh", "-c", - fmt.Sprintf(redirectOutputFmt, joinedCommands), + finalScriptForHook, }, }, } diff --git a/pkg/library/status/check.go b/pkg/library/status/check.go index b44843772..0c38c1021 100644 --- a/pkg/library/status/check.go +++ b/pkg/library/status/check.go @@ -18,6 +18,7 @@ package status import ( "context" "fmt" + "regexp" "strings" "github.com/devfile/devworkspace-operator/pkg/common" @@ -145,10 +146,15 @@ func CheckPodEvents(pod *corev1.Pod, workspaceID string, ignoredEvents []string, if maxCount, isUnrecoverableEvent := unrecoverablePodEventReasons[ev.Reason]; isUnrecoverableEvent { if !checkIfUnrecoverableEventIgnored(ev.Reason, ignoredEvents) && getEventCount(ev) >= maxCount { var msg string + eventMessage := ev.Message // Original Kubelet message from the event + if ev.Reason == "FailedPostStartHook" { + eventMessage = getConcisePostStartFailureMessage(ev.Message) + } + if getEventCount(ev) > 1 { - msg = fmt.Sprintf("Detected unrecoverable event %s %d times: %s.", ev.Reason, getEventCount(ev), ev.Message) + msg = fmt.Sprintf("Detected unrecoverable event %s %d times: %s", ev.Reason, getEventCount(ev), eventMessage) } else { - msg = fmt.Sprintf("Detected unrecoverable event %s: %s.", ev.Reason, ev.Message) + msg = fmt.Sprintf("Detected unrecoverable event %s: %s", ev.Reason, eventMessage) } return msg, nil } @@ -157,22 +163,110 @@ func CheckPodEvents(pod *corev1.Pod, workspaceID string, ignoredEvents []string, return "", nil } +// getConcisePostStartFailureMessage tries to parse the Kubelet's verbose message +// for a PostStartHookError into a more user-friendly one. +func getConcisePostStartFailureMessage(kubeletMsg string) string { + + /* regexes for specific messages from our postStart script's output */ + + // matches: "[postStart hook] Commands terminated by SIGTERM (likely timed out after ...s). Exit code 143." + reTerminatedSigterm := regexp.MustCompile(`(\[postStart hook\] Commands terminated by SIGTERM \(likely timed out after [^)]+?\)\. Exit code 143\.)`) + + // matches: "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ...s expired). Exit code 137." + reKilledSigkill := regexp.MustCompile(`(\[postStart hook\] Commands forcefully killed by SIGKILL \(likely after --kill-after [^)]+?\)\. Exit code 137\.)`) + + // matches: "[postStart hook] Commands failed with exit code ..." (for any other script-reported non-zero exit code) + reGenericFailedExitCode := regexp.MustCompile(`(\[postStart hook\] Commands failed with exit code \d+\.)`) + + // regex to capture Kubelet's explicit message field content if it exists + reKubeletInternalMessage := regexp.MustCompile(`message:\s*"([^"]*)"`) + + // regex to capture Kubelet's reported exit code for the hook command + reKubeletExitCode := regexp.MustCompile(`exited with (\d+):`) + + /* 1: check Kubelet's explicit `message: "..."` field for the specific output */ + + kubeletInternalMsgMatch := reKubeletInternalMessage.FindStringSubmatch(kubeletMsg) + if len(kubeletInternalMsgMatch) > 1 && kubeletInternalMsgMatch[1] != "" { + internalMsg := kubeletInternalMsgMatch[1] + if match := reTerminatedSigterm.FindString(internalMsg); match != "" { + return match + } + if match := reKilledSigkill.FindString(internalMsg); match != "" { + return match + } + if match := reGenericFailedExitCode.FindString(internalMsg); match != "" { + return match + } + } + + /* 2: parse Kubelet's reported exit code for the entire hook command */ + + matchesKubeletExitCode := reKubeletExitCode.FindStringSubmatch(kubeletMsg) + if len(matchesKubeletExitCode) > 1 { + exitCodeStr := matchesKubeletExitCode[1] + var exitCode int + fmt.Sscanf(exitCodeStr, "%d", &exitCode) + + // generate messages indicating the source is Kubelet's reported exit code + if exitCode == 143 { // SIGTERM + return "[postStart hook] Commands terminated by SIGTERM due to timeout" + } else if exitCode == 137 { // SIGKILL + return "[postStart hook] Commands forcefully killed by SIGKILL due to timeout" + } else if exitCode != 0 { // Other non-zero exit codes (e.g., 124, 127) + return fmt.Sprintf("[postStart hook] Commands failed (Kubelet reported exit code %s)", exitCodeStr) + } + } + + /* 3: try to match specific script outputs against the *entire* Kubelet message */ + + if match := reTerminatedSigterm.FindString(kubeletMsg); match != "" { + return match + } + if match := reKilledSigkill.FindString(kubeletMsg); match != "" { + return match + } + if match := reGenericFailedExitCode.FindString(kubeletMsg); match != "" { + return match + } + + /* 4: fallback */ + + return "[postStart hook] failed with an unknown error (see pod events or container logs for more details)" +} + func CheckContainerStatusForFailure(containerStatus *corev1.ContainerStatus, ignoredEvents []string) (ok bool, reason string) { if containerStatus.State.Waiting != nil { + // Explicitly check for PostStartHookError + if containerStatus.State.Waiting.Reason == "PostStartHookError" { // Kubelet uses this reason + conciseMsg := getConcisePostStartFailureMessage(containerStatus.State.Waiting.Message) + return checkIfUnrecoverableEventIgnored("FailedPostStartHook", ignoredEvents), conciseMsg + } + // Check against other generic failure reasons for _, failureReason := range containerFailureStateReasons { if containerStatus.State.Waiting.Reason == failureReason { - return checkIfUnrecoverableEventIgnored(containerStatus.State.Waiting.Reason, ignoredEvents), containerStatus.State.Waiting.Reason + return checkIfUnrecoverableEventIgnored(containerStatus.State.Waiting.Reason, ignoredEvents), + containerStatus.State.Waiting.Reason } } } if containerStatus.State.Terminated != nil { + // Check if termination was due to a generic error, which might include postStart issues + // if the container failed to run. + if containerStatus.State.Terminated.Reason == "Error" || containerStatus.State.Terminated.Reason == "ContainerCannotRun" { + return checkIfUnrecoverableEventIgnored(containerStatus.State.Terminated.Reason, ignoredEvents), + fmt.Sprintf("%s: %s", containerStatus.State.Terminated.Reason, containerStatus.State.Terminated.Message) + } + // Check against other generic failure reasons for terminated state for _, failureReason := range containerFailureStateReasons { if containerStatus.State.Terminated.Reason == failureReason { - return checkIfUnrecoverableEventIgnored(containerStatus.State.Terminated.Reason, ignoredEvents), containerStatus.State.Terminated.Reason + return checkIfUnrecoverableEventIgnored(containerStatus.State.Terminated.Reason, ignoredEvents), + containerStatus.State.Terminated.Reason } } } + return true, "" } From f6e8bc0453024564499f45cf7ccb95105d5e31d8 Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Thu, 22 May 2025 12:32:49 +0300 Subject: [PATCH 02/22] chore: run make update_devworkspace_api update_devworkspace_crds generate_all Signed-off-by: Oleksii Kurinnyi --- apis/controller/v1alpha1/zz_generated.deepcopy.go | 7 ++++++- ...troller.devfile.io_devworkspaceoperatorconfigs.yaml | 10 ++++++++++ deploy/deployment/kubernetes/combined.yaml | 10 ++++++++++ ...controller.devfile.io.CustomResourceDefinition.yaml | 10 ++++++++++ deploy/deployment/openshift/combined.yaml | 10 ++++++++++ ...controller.devfile.io.CustomResourceDefinition.yaml | 10 ++++++++++ ...troller.devfile.io_devworkspaceoperatorconfigs.yaml | 10 ++++++++++ 7 files changed, 66 insertions(+), 1 deletion(-) diff --git a/apis/controller/v1alpha1/zz_generated.deepcopy.go b/apis/controller/v1alpha1/zz_generated.deepcopy.go index b04904319..a11084352 100644 --- a/apis/controller/v1alpha1/zz_generated.deepcopy.go +++ b/apis/controller/v1alpha1/zz_generated.deepcopy.go @@ -21,7 +21,7 @@ package v1alpha1 import ( "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" - v1 "k8s.io/api/core/v1" + "k8s.io/api/core/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -788,6 +788,11 @@ func (in *WorkspaceConfig) DeepCopyInto(out *WorkspaceConfig) { *out = new(CleanupCronJobConfig) (*in).DeepCopyInto(*out) } + if in.PostStartTimeout != nil { + in, out := &in.PostStartTimeout, &out.PostStartTimeout + *out = new(int32) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkspaceConfig. diff --git a/deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml b/deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml index 4aee38cd5..48b939d97 100644 --- a/deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml +++ b/deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml @@ -2764,6 +2764,16 @@ spec: type: string type: object type: object + postStartTimeout: + default: 0 + description: |- + PostStartTimeout defines the maximum duration the PostStart hook can run + before it is automatically failed. This timeout is used for the postStart lifecycle hook + that is used to run commands in the workspace container. The timeout is specified in seconds. + If not specified, the timeout is disabled (0 seconds). + format: int32 + minimum: 0 + type: integer progressTimeout: description: |- ProgressTimeout determines the maximum duration a DevWorkspace can be in diff --git a/deploy/deployment/kubernetes/combined.yaml b/deploy/deployment/kubernetes/combined.yaml index 007fa31bd..a7c2c5d89 100644 --- a/deploy/deployment/kubernetes/combined.yaml +++ b/deploy/deployment/kubernetes/combined.yaml @@ -2900,6 +2900,16 @@ spec: type: string type: object type: object + postStartTimeout: + default: 0 + description: |- + PostStartTimeout defines the maximum duration the PostStart hook can run + before it is automatically failed. This timeout is used for the postStart lifecycle hook + that is used to run commands in the workspace container. The timeout is specified in seconds. + If not specified, the timeout is disabled (0 seconds). + format: int32 + minimum: 0 + type: integer progressTimeout: description: |- ProgressTimeout determines the maximum duration a DevWorkspace can be in diff --git a/deploy/deployment/kubernetes/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml b/deploy/deployment/kubernetes/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml index c826f4c11..0c165cb89 100644 --- a/deploy/deployment/kubernetes/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml +++ b/deploy/deployment/kubernetes/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml @@ -2900,6 +2900,16 @@ spec: type: string type: object type: object + postStartTimeout: + default: 0 + description: |- + PostStartTimeout defines the maximum duration the PostStart hook can run + before it is automatically failed. This timeout is used for the postStart lifecycle hook + that is used to run commands in the workspace container. The timeout is specified in seconds. + If not specified, the timeout is disabled (0 seconds). + format: int32 + minimum: 0 + type: integer progressTimeout: description: |- ProgressTimeout determines the maximum duration a DevWorkspace can be in diff --git a/deploy/deployment/openshift/combined.yaml b/deploy/deployment/openshift/combined.yaml index 26c4d1373..1152b92f6 100644 --- a/deploy/deployment/openshift/combined.yaml +++ b/deploy/deployment/openshift/combined.yaml @@ -2900,6 +2900,16 @@ spec: type: string type: object type: object + postStartTimeout: + default: 0 + description: |- + PostStartTimeout defines the maximum duration the PostStart hook can run + before it is automatically failed. This timeout is used for the postStart lifecycle hook + that is used to run commands in the workspace container. The timeout is specified in seconds. + If not specified, the timeout is disabled (0 seconds). + format: int32 + minimum: 0 + type: integer progressTimeout: description: |- ProgressTimeout determines the maximum duration a DevWorkspace can be in diff --git a/deploy/deployment/openshift/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml b/deploy/deployment/openshift/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml index c826f4c11..0c165cb89 100644 --- a/deploy/deployment/openshift/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml +++ b/deploy/deployment/openshift/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml @@ -2900,6 +2900,16 @@ spec: type: string type: object type: object + postStartTimeout: + default: 0 + description: |- + PostStartTimeout defines the maximum duration the PostStart hook can run + before it is automatically failed. This timeout is used for the postStart lifecycle hook + that is used to run commands in the workspace container. The timeout is specified in seconds. + If not specified, the timeout is disabled (0 seconds). + format: int32 + minimum: 0 + type: integer progressTimeout: description: |- ProgressTimeout determines the maximum duration a DevWorkspace can be in diff --git a/deploy/templates/crd/bases/controller.devfile.io_devworkspaceoperatorconfigs.yaml b/deploy/templates/crd/bases/controller.devfile.io_devworkspaceoperatorconfigs.yaml index f41ef271f..7bda60bd3 100644 --- a/deploy/templates/crd/bases/controller.devfile.io_devworkspaceoperatorconfigs.yaml +++ b/deploy/templates/crd/bases/controller.devfile.io_devworkspaceoperatorconfigs.yaml @@ -2898,6 +2898,16 @@ spec: type: string type: object type: object + postStartTimeout: + default: 0 + description: |- + PostStartTimeout defines the maximum duration the PostStart hook can run + before it is automatically failed. This timeout is used for the postStart lifecycle hook + that is used to run commands in the workspace container. The timeout is specified in seconds. + If not specified, the timeout is disabled (0 seconds). + format: int32 + minimum: 0 + type: integer progressTimeout: description: |- ProgressTimeout determines the maximum duration a DevWorkspace can be in From 0c1c3e48c192c8e13ffdc2d60bbe6362cd6c2631 Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Wed, 28 May 2025 14:45:52 +0300 Subject: [PATCH 03/22] test: add/update tests related to postStart hook commands Signed-off-by: Oleksii Kurinnyi --- pkg/library/container/container_test.go | 2 +- pkg/library/lifecycle/poststart.go | 64 ++-- pkg/library/lifecycle/poststart_test.go | 337 +++++++++++++++++- .../adds_all_postStart_commands.yaml | 73 +++- .../testdata/postStart/basic_postStart.yaml | 36 +- .../multiple_poststart_commands.yaml | 38 +- .../postStart/workingDir_postStart.yaml | 37 +- pkg/library/status/check_test.go | 107 ++++++ 8 files changed, 646 insertions(+), 48 deletions(-) create mode 100644 pkg/library/status/check_test.go diff --git a/pkg/library/container/container_test.go b/pkg/library/container/container_test.go index 6b6f23463..e23b42ac1 100644 --- a/pkg/library/container/container_test.go +++ b/pkg/library/container/container_test.go @@ -87,7 +87,7 @@ func TestGetKubeContainersFromDevfile(t *testing.T) { t.Run(tt.Name, func(t *testing.T) { // sanity check that file is read correctly. assert.True(t, len(tt.Input.Components) > 0, "Input defines no components") - gotPodAdditions, err := GetKubeContainersFromDevfile(tt.Input, nil, testImagePullPolicy, defaultResources) + gotPodAdditions, err := GetKubeContainersFromDevfile(tt.Input, nil, testImagePullPolicy, defaultResources, nil) if tt.Output.ErrRegexp != nil && assert.Error(t, err) { assert.Regexp(t, *tt.Output.ErrRegexp, err.Error(), "Error message should match") } else { diff --git a/pkg/library/lifecycle/poststart.go b/pkg/library/lifecycle/poststart.go index f88a2f6f1..46042dddd 100644 --- a/pkg/library/lifecycle/poststart.go +++ b/pkg/library/lifecycle/poststart.go @@ -76,23 +76,18 @@ func AddPostStartLifecycleHooks(wksp *dw.DevWorkspaceTemplateSpec, containers [] return nil } -// processCommandsForPostStart builds a lifecycle handler that runs the provided command(s) -// The command has the format -// -// exec: -// -// command: -// - "/bin/sh" -// - "-c" -// - | -// cd -// -func processCommandsForPostStart(commands []dw.Command, postStartTimeout *int32) (*corev1.LifecycleHandler, error) { +// buildUserScript takes a list of DevWorkspace commands and constructs a single +// shell script string that executes them sequentially. +func buildUserScript(commands []dw.Command) (string, error) { var commandScriptLines []string for _, command := range commands { execCmd := command.Exec + if execCmd == nil { + // Should be caught by earlier validation, but good to be safe + return "", fmt.Errorf("exec command is nil for command ID %s", command.Id) + } if len(execCmd.Env) > 0 { - return nil, fmt.Errorf("env vars in postStart command %s are unsupported", command.Id) + return "", fmt.Errorf("env vars in postStart command %s are unsupported", command.Id) } var singleCommandParts []string if execCmd.WorkingDir != "" { @@ -107,17 +102,18 @@ func processCommandsForPostStart(commands []dw.Command, postStartTimeout *int32) commandScriptLines = append(commandScriptLines, strings.Join(singleCommandParts, " && ")) } } + return strings.Join(commandScriptLines, "\n"), nil +} - originalUserScript := strings.Join(commandScriptLines, "\n") - - scriptToExecute := "set -e\n" + originalUserScript - escapedUserScript := strings.ReplaceAll(scriptToExecute, "'", `'\''`) - - scriptWithTimeout := fmt.Sprintf(` +// generateScriptWithTimeout wraps a given user script with timeout logic, +// environment variable exports, and specific exit code handling. +// The killAfterDurationSeconds is hardcoded to 5s within this generated script. +func generateScriptWithTimeout(escapedUserScript string, timeoutSeconds int32) string { + return fmt.Sprintf(` export POSTSTART_TIMEOUT_DURATION="%d" export POSTSTART_KILL_AFTER_DURATION="5" -echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} s, kill after: ${POSTSTART_KILL_AFTER_DURATION} s" >&2 +echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 # Run the user's script under the 'timeout' utility. timeout --preserve-status --kill-after="${POSTSTART_KILL_AFTER_DURATION}" "${POSTSTART_TIMEOUT_DURATION}" /bin/sh -c '%s' @@ -128,16 +124,38 @@ if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 -elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code, including 124 +elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code echo "[postStart hook] Commands failed with exit code $exit_code." >&2 else echo "[postStart hook] Commands completed successfully within the time limit." >&2 fi exit $exit_code -`, *postStartTimeout, escapedUserScript) +`, timeoutSeconds, escapedUserScript) +} + +// processCommandsForPostStart processes a list of DevWorkspace commands +// and generates a corev1.LifecycleHandler for the PostStart lifecycle hook. +func processCommandsForPostStart(commands []dw.Command, postStartTimeout *int32) (*corev1.LifecycleHandler, error) { + if postStartTimeout == nil { + // The 'timeout' command treats 0 as "no timeout", so it is disabled by default. + defaultTimeout := int32(0) + postStartTimeout = &defaultTimeout + } + + originalUserScript, err := buildUserScript(commands) + if err != nil { + return nil, fmt.Errorf("failed to build aggregated user script: %w", err) + } + + // The user script needs 'set -e' to ensure it exits on error. + // This script is then passed to `sh -c '...'`, so single quotes within it must be escaped. + scriptToExecute := "set -e\n" + originalUserScript + escapedUserScriptForTimeoutWrapper := strings.ReplaceAll(scriptToExecute, "'", `'\''`) + + fullScriptWithTimeout := generateScriptWithTimeout(escapedUserScriptForTimeoutWrapper, *postStartTimeout) - finalScriptForHook := fmt.Sprintf(redirectOutputFmt, scriptWithTimeout) + finalScriptForHook := fmt.Sprintf(redirectOutputFmt, fullScriptWithTimeout) handler := &corev1.LifecycleHandler{ Exec: &corev1.ExecAction{ diff --git a/pkg/library/lifecycle/poststart_test.go b/pkg/library/lifecycle/poststart_test.go index e49197163..c367689f5 100644 --- a/pkg/library/lifecycle/poststart_test.go +++ b/pkg/library/lifecycle/poststart_test.go @@ -75,7 +75,7 @@ func TestAddPostStartLifecycleHooks(t *testing.T) { tests := loadAllPostStartTestCasesOrPanic(t, "./testdata/postStart") for _, tt := range tests { t.Run(fmt.Sprintf("%s (%s)", tt.Name, tt.testPath), func(t *testing.T) { - err := AddPostStartLifecycleHooks(tt.Input.Devfile, tt.Input.Containers) + err := AddPostStartLifecycleHooks(tt.Input.Devfile, tt.Input.Containers, nil) if tt.Output.ErrRegexp != nil && assert.Error(t, err) { assert.Regexp(t, *tt.Output.ErrRegexp, err.Error(), "Error message should match") } else { @@ -87,3 +87,338 @@ func TestAddPostStartLifecycleHooks(t *testing.T) { }) } } + +func TestBuildUserScript(t *testing.T) { + tests := []struct { + name string + commands []dw.Command + expectedScript string + expectedErr string + }{ + { + name: "No commands", + commands: []dw.Command{}, + expectedScript: "", + expectedErr: "", + }, + { + name: "Single command without workingDir", + commands: []dw.Command{ + { + Id: "cmd1", + CommandUnion: dw.CommandUnion{ + Exec: &dw.ExecCommand{ + CommandLine: "echo hello", + Component: "tools", + }, + }, + }, + }, + expectedScript: "echo hello", + expectedErr: "", + }, + { + name: "Single command with workingDir", + commands: []dw.Command{ + { + Id: "cmd1", + CommandUnion: dw.CommandUnion{ + Exec: &dw.ExecCommand{ + CommandLine: "ls -la", + WorkingDir: "/projects/app", + Component: "tools", + }, + }, + }, + }, + expectedScript: "cd '/projects/app' && ls -la", + expectedErr: "", + }, + { + name: "Single command with only workingDir", + commands: []dw.Command{ + { + Id: "cmd1", + CommandUnion: dw.CommandUnion{ + Exec: &dw.ExecCommand{ + WorkingDir: "/data", + Component: "tools", + }, + }, + }, + }, + expectedScript: "cd '/data'", + expectedErr: "", + }, + { + name: "Single command with workingDir containing single quote", + commands: []dw.Command{ + { + Id: "cmd1", + CommandUnion: dw.CommandUnion{ + Exec: &dw.ExecCommand{ + CommandLine: "cat file.txt", + WorkingDir: "/projects/app's", + Component: "tools", + }, + }, + }, + }, + expectedScript: "cd '/projects/app'\\''s' && cat file.txt", + expectedErr: "", + }, + { + name: "Multiple commands", + commands: []dw.Command{ + { + Id: "cmd1", + CommandUnion: dw.CommandUnion{ + Exec: &dw.ExecCommand{ + CommandLine: "npm install", + WorkingDir: "/projects/frontend", + Component: "tools", + }, + }, + }, + { + Id: "cmd2", + CommandUnion: dw.CommandUnion{ + Exec: &dw.ExecCommand{ + CommandLine: "npm start", + Component: "tools", + }, + }, + }, + { + Id: "cmd3", + CommandUnion: dw.CommandUnion{ + Exec: &dw.ExecCommand{ + WorkingDir: "/projects/backend", + CommandLine: "mvn spring-boot:run", + Component: "tools", + }, + }, + }, + }, + expectedScript: "cd '/projects/frontend' && npm install\nnpm start\ncd '/projects/backend' && mvn spring-boot:run", + expectedErr: "", + }, + { + name: "Command with unsupported Env vars", + commands: []dw.Command{ + { + Id: "cmd-with-env", + CommandUnion: dw.CommandUnion{ + Exec: &dw.ExecCommand{ + CommandLine: "echo $MY_VAR", + Component: "tools", + Env: []dw.EnvVar{ + {Name: "MY_VAR", Value: "test"}, + }, + }, + }, + }, + }, + expectedScript: "", + expectedErr: "env vars in postStart command cmd-with-env are unsupported", + }, + { + name: "Command with nil Exec field", + commands: []dw.Command{ + { + Id: "cmd-nil-exec", + CommandUnion: dw.CommandUnion{Exec: nil}, + }, + }, + expectedScript: "", + expectedErr: "exec command is nil for command ID cmd-nil-exec", + }, + { + name: "Command with empty CommandLine and no WorkingDir", + commands: []dw.Command{ + { + Id: "cmd-empty", + CommandUnion: dw.CommandUnion{ + Exec: &dw.ExecCommand{ + CommandLine: "", + WorkingDir: "", + Component: "tools", + }, + }, + }, + { + Id: "cmd-after-empty", + CommandUnion: dw.CommandUnion{ + Exec: &dw.ExecCommand{ + CommandLine: "echo 'still works'", + Component: "tools", + }, + }, + }, + }, + expectedScript: "echo 'still works'", // The empty command should result in no line + expectedErr: "", + }, + { + name: "Command with only CommandLine (empty working dir)", + commands: []dw.Command{ + { + Id: "cmd-empty-wdir", + CommandUnion: dw.CommandUnion{ + Exec: &dw.ExecCommand{ + CommandLine: "pwd", + WorkingDir: "", + Component: "tools", + }, + }, + }, + }, + expectedScript: "pwd", + expectedErr: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + script, err := buildUserScript(tt.commands) + + if tt.expectedErr != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedErr) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedScript, script) + } + }) + } +} + +func TestGenerateScriptWithTimeout(t *testing.T) { + tests := []struct { + name string + escapedUserScript string + timeoutSeconds int32 + expectedScript string + }{ + { + name: "Basic script with timeout", + escapedUserScript: "echo 'hello world'\nsleep 1", + timeoutSeconds: 10, + expectedScript: ` +export POSTSTART_TIMEOUT_DURATION="10" +export POSTSTART_KILL_AFTER_DURATION="5" + +echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 + +# Run the user's script under the 'timeout' utility. +timeout --preserve-status --kill-after="${POSTSTART_KILL_AFTER_DURATION}" "${POSTSTART_TIMEOUT_DURATION}" /bin/sh -c 'echo 'hello world' +sleep 1' +exit_code=$? + +# Check the exit code from 'timeout' +if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) + echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 +elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) + echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 +elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code + echo "[postStart hook] Commands failed with exit code $exit_code." >&2 +else + echo "[postStart hook] Commands completed successfully within the time limit." >&2 +fi + +exit $exit_code +`, + }, + { + name: "Script with zero timeout (no timeout)", + escapedUserScript: "echo 'running indefinitely...'", + timeoutSeconds: 0, + expectedScript: ` +export POSTSTART_TIMEOUT_DURATION="0" +export POSTSTART_KILL_AFTER_DURATION="5" + +echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 + +# Run the user's script under the 'timeout' utility. +timeout --preserve-status --kill-after="${POSTSTART_KILL_AFTER_DURATION}" "${POSTSTART_TIMEOUT_DURATION}" /bin/sh -c 'echo 'running indefinitely...'' +exit_code=$? + +# Check the exit code from 'timeout' +if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) + echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 +elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) + echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 +elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code + echo "[postStart hook] Commands failed with exit code $exit_code." >&2 +else + echo "[postStart hook] Commands completed successfully within the time limit." >&2 +fi + +exit $exit_code +`, + }, + { + name: "Empty user script", + escapedUserScript: "", + timeoutSeconds: 5, + expectedScript: ` +export POSTSTART_TIMEOUT_DURATION="5" +export POSTSTART_KILL_AFTER_DURATION="5" + +echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 + +# Run the user's script under the 'timeout' utility. +timeout --preserve-status --kill-after="${POSTSTART_KILL_AFTER_DURATION}" "${POSTSTART_TIMEOUT_DURATION}" /bin/sh -c '' +exit_code=$? + +# Check the exit code from 'timeout' +if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) + echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 +elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) + echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 +elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code + echo "[postStart hook] Commands failed with exit code $exit_code." >&2 +else + echo "[postStart hook] Commands completed successfully within the time limit." >&2 +fi + +exit $exit_code +`, + }, + { + name: "User script with already escaped single quotes", + escapedUserScript: "echo 'it'\\''s complex'", + timeoutSeconds: 30, + expectedScript: ` +export POSTSTART_TIMEOUT_DURATION="30" +export POSTSTART_KILL_AFTER_DURATION="5" + +echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 + +# Run the user's script under the 'timeout' utility. +timeout --preserve-status --kill-after="${POSTSTART_KILL_AFTER_DURATION}" "${POSTSTART_TIMEOUT_DURATION}" /bin/sh -c 'echo 'it'\''s complex'' +exit_code=$? + +# Check the exit code from 'timeout' +if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) + echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 +elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) + echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 +elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code + echo "[postStart hook] Commands failed with exit code $exit_code." >&2 +else + echo "[postStart hook] Commands completed successfully within the time limit." >&2 +fi + +exit $exit_code +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + script := generateScriptWithTimeout(tt.escapedUserScript, tt.timeoutSeconds) + assert.Equal(t, tt.expectedScript, script) + }) + } +} diff --git a/pkg/library/lifecycle/testdata/postStart/adds_all_postStart_commands.yaml b/pkg/library/lifecycle/testdata/postStart/adds_all_postStart_commands.yaml index 7da46f12e..779c5afb9 100644 --- a/pkg/library/lifecycle/testdata/postStart/adds_all_postStart_commands.yaml +++ b/pkg/library/lifecycle/testdata/postStart/adds_all_postStart_commands.yaml @@ -33,25 +33,80 @@ output: postStart: exec: command: - - "/bin/sh" - - "-c" + - /bin/sh + - -c - | { - echo 'hello world 1' - } 1>/tmp/poststart-stdout.txt 2>/tmp/poststart-stderr.txt + # This script block ensures its exit code is preserved + # while its stdout and stderr are tee'd. + _script_to_run() { + + export POSTSTART_TIMEOUT_DURATION="0" + export POSTSTART_KILL_AFTER_DURATION="5" + + echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 + + # Run the user's script under the 'timeout' utility. + timeout --preserve-status --kill-after="${POSTSTART_KILL_AFTER_DURATION}" "${POSTSTART_TIMEOUT_DURATION}" /bin/sh -c 'set -e + echo '\''hello world 1'\''' + exit_code=$? + + # Check the exit code from 'timeout' + if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) + echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 + elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) + echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 + elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code + echo "[postStart hook] Commands failed with exit code $exit_code." >&2 + else + echo "[postStart hook] Commands completed successfully within the time limit." >&2 + fi + + exit $exit_code + # This will be replaced by scriptWithTimeout + } + _script_to_run + } 1> >(tee -a "/tmp/poststart-stdout.txt") 2> >(tee -a "/tmp/poststart-stderr.txt" >&2) - name: test-component-2 image: test-img lifecycle: postStart: exec: command: - - "/bin/sh" - - "-c" + - /bin/sh + - -c - | { - cd /tmp/test-dir - echo 'hello world 2' - } 1>/tmp/poststart-stdout.txt 2>/tmp/poststart-stderr.txt + # This script block ensures its exit code is preserved + # while its stdout and stderr are tee'd. + _script_to_run() { + + export POSTSTART_TIMEOUT_DURATION="0" + export POSTSTART_KILL_AFTER_DURATION="5" + + echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 + + # Run the user's script under the 'timeout' utility. + timeout --preserve-status --kill-after="${POSTSTART_KILL_AFTER_DURATION}" "${POSTSTART_TIMEOUT_DURATION}" /bin/sh -c 'set -e + cd '\''/tmp/test-dir'\'' && echo '\''hello world 2'\''' + exit_code=$? + + # Check the exit code from 'timeout' + if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) + echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 + elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) + echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 + elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code + echo "[postStart hook] Commands failed with exit code $exit_code." >&2 + else + echo "[postStart hook] Commands completed successfully within the time limit." >&2 + fi + + exit $exit_code + # This will be replaced by scriptWithTimeout + } + _script_to_run + } 1> >(tee -a "/tmp/poststart-stdout.txt") 2> >(tee -a "/tmp/poststart-stderr.txt" >&2) - name: test-component-3 image: test-img diff --git a/pkg/library/lifecycle/testdata/postStart/basic_postStart.yaml b/pkg/library/lifecycle/testdata/postStart/basic_postStart.yaml index 30ced94b2..8f6063013 100644 --- a/pkg/library/lifecycle/testdata/postStart/basic_postStart.yaml +++ b/pkg/library/lifecycle/testdata/postStart/basic_postStart.yaml @@ -22,9 +22,37 @@ output: postStart: exec: command: - - "/bin/sh" - - "-c" + - /bin/sh + - -c - | { - echo 'hello world' - } 1>/tmp/poststart-stdout.txt 2>/tmp/poststart-stderr.txt + # This script block ensures its exit code is preserved + # while its stdout and stderr are tee'd. + _script_to_run() { + + export POSTSTART_TIMEOUT_DURATION="0" + export POSTSTART_KILL_AFTER_DURATION="5" + + echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 + + # Run the user's script under the 'timeout' utility. + timeout --preserve-status --kill-after="${POSTSTART_KILL_AFTER_DURATION}" "${POSTSTART_TIMEOUT_DURATION}" /bin/sh -c 'set -e + echo '\''hello world'\''' + exit_code=$? + + # Check the exit code from 'timeout' + if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) + echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 + elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) + echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 + elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code + echo "[postStart hook] Commands failed with exit code $exit_code." >&2 + else + echo "[postStart hook] Commands completed successfully within the time limit." >&2 + fi + + exit $exit_code + # This will be replaced by scriptWithTimeout + } + _script_to_run + } 1> >(tee -a "/tmp/poststart-stdout.txt") 2> >(tee -a "/tmp/poststart-stderr.txt" >&2) diff --git a/pkg/library/lifecycle/testdata/postStart/multiple_poststart_commands.yaml b/pkg/library/lifecycle/testdata/postStart/multiple_poststart_commands.yaml index 01cfeb55e..bd9946daa 100644 --- a/pkg/library/lifecycle/testdata/postStart/multiple_poststart_commands.yaml +++ b/pkg/library/lifecycle/testdata/postStart/multiple_poststart_commands.yaml @@ -27,10 +27,38 @@ output: postStart: exec: command: - - "/bin/sh" - - "-c" + - /bin/sh + - -c - | { - echo 'hello world 1' - echo 'hello world 2' - } 1>/tmp/poststart-stdout.txt 2>/tmp/poststart-stderr.txt + # This script block ensures its exit code is preserved + # while its stdout and stderr are tee'd. + _script_to_run() { + + export POSTSTART_TIMEOUT_DURATION="0" + export POSTSTART_KILL_AFTER_DURATION="5" + + echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 + + # Run the user's script under the 'timeout' utility. + timeout --preserve-status --kill-after="${POSTSTART_KILL_AFTER_DURATION}" "${POSTSTART_TIMEOUT_DURATION}" /bin/sh -c 'set -e + echo '\''hello world 1'\'' + echo '\''hello world 2'\''' + exit_code=$? + + # Check the exit code from 'timeout' + if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) + echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 + elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) + echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 + elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code + echo "[postStart hook] Commands failed with exit code $exit_code." >&2 + else + echo "[postStart hook] Commands completed successfully within the time limit." >&2 + fi + + exit $exit_code + # This will be replaced by scriptWithTimeout + } + _script_to_run + } 1> >(tee -a "/tmp/poststart-stdout.txt") 2> >(tee -a "/tmp/poststart-stderr.txt" >&2) diff --git a/pkg/library/lifecycle/testdata/postStart/workingDir_postStart.yaml b/pkg/library/lifecycle/testdata/postStart/workingDir_postStart.yaml index 57646070c..7cd1ca669 100644 --- a/pkg/library/lifecycle/testdata/postStart/workingDir_postStart.yaml +++ b/pkg/library/lifecycle/testdata/postStart/workingDir_postStart.yaml @@ -23,10 +23,37 @@ output: postStart: exec: command: - - "/bin/sh" - - "-c" + - /bin/sh + - -c - | { - cd /tmp/test-dir - echo 'hello world' - } 1>/tmp/poststart-stdout.txt 2>/tmp/poststart-stderr.txt + # This script block ensures its exit code is preserved + # while its stdout and stderr are tee'd. + _script_to_run() { + + export POSTSTART_TIMEOUT_DURATION="0" + export POSTSTART_KILL_AFTER_DURATION="5" + + echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 + + # Run the user's script under the 'timeout' utility. + timeout --preserve-status --kill-after="${POSTSTART_KILL_AFTER_DURATION}" "${POSTSTART_TIMEOUT_DURATION}" /bin/sh -c 'set -e + cd '\''/tmp/test-dir'\'' && echo '\''hello world'\''' + exit_code=$? + + # Check the exit code from 'timeout' + if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) + echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 + elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) + echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 + elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code + echo "[postStart hook] Commands failed with exit code $exit_code." >&2 + else + echo "[postStart hook] Commands completed successfully within the time limit." >&2 + fi + + exit $exit_code + # This will be replaced by scriptWithTimeout + } + _script_to_run + } 1> >(tee -a "/tmp/poststart-stdout.txt") 2> >(tee -a "/tmp/poststart-stderr.txt" >&2) diff --git a/pkg/library/status/check_test.go b/pkg/library/status/check_test.go new file mode 100644 index 000000000..aa3498368 --- /dev/null +++ b/pkg/library/status/check_test.go @@ -0,0 +1,107 @@ +package status + +import ( + "testing" +) + +func TestGetConcisePostStartFailureMessage(t *testing.T) { + tests := []struct { + name string + kubeletMsg string + expectedMsg string + }{ + { + name: "Kubelet internal message - SIGTERM", + kubeletMsg: `PostStartHookError: rpc error: code = Unknown desc = command error: command terminated by SIGTERM, message: "[postStart hook] Commands terminated by SIGTERM (likely timed out after 30s). Exit code 143."`, + expectedMsg: "[postStart hook] Commands terminated by SIGTERM (likely timed out after 30s). Exit code 143.", + }, + { + name: "Kubelet internal message - SIGKILL", + kubeletMsg: `PostStartHookError: rpc error: code = Unknown desc = command error: command terminated by SIGKILL, message: "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after 10s expired). Exit code 137."`, + expectedMsg: "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after 10s expired). Exit code 137.", + }, + { + name: "Kubelet internal message - Generic Fail", + kubeletMsg: `PostStartHookError: rpc error: code = Unknown desc = command error: command failed, message: "[postStart hook] Commands failed with exit code 1."`, + expectedMsg: "[postStart hook] Commands failed with exit code 1.", + }, + { + name: "Kubelet internal message - No match, fall through to Kubelet exit code", + kubeletMsg: `PostStartHookError: rpc error: code = Unknown desc = command error: command terminated by signal: SIGTERM, message: "Container command \\\'sleep 60\\\' was terminated by signal SIGTERM"\nexited with 143: ...`, + expectedMsg: "[postStart hook] Commands terminated by SIGTERM due to timeout", + }, + { + name: "Kubelet reported exit code - 143 (SIGTERM)", + kubeletMsg: `PostStartHookError: command 'sh -c ...' exited with 143: ...`, + expectedMsg: "[postStart hook] Commands terminated by SIGTERM due to timeout", + }, + { + name: "Kubelet exit code - 137 (SIGKILL)", + kubeletMsg: `PostStartHookError: command 'sh -c ...' exited with 137: ...`, + expectedMsg: "[postStart hook] Commands forcefully killed by SIGKILL due to timeout", + }, + { + name: "Kubelet exit code - 1 (Generic)", + kubeletMsg: `PostStartHookError: command 'sh -c ...' exited with 1: ...`, + expectedMsg: "[postStart hook] Commands failed (Kubelet reported exit code 1)", + }, + { + name: "Kubelet exit code - 124 (e.g. timeout command itself)", + kubeletMsg: `PostStartHookError: command 'sh -c ...' exited with 124: ...`, + expectedMsg: "[postStart hook] Commands failed (Kubelet reported exit code 124)", + }, + { + name: "Full Kubelet message match - SIGTERM (no internal message field, no Kubelet exit code first part)", + kubeletMsg: `PostStartHookError: Error executing postStart hook: [postStart hook] Commands terminated by SIGTERM (likely timed out after 45s). Exit code 143.`, + expectedMsg: "[postStart hook] Commands terminated by SIGTERM (likely timed out after 45s). Exit code 143.", + }, + { + name: "Full Kubelet message match - SIGKILL (no internal message field, no Kubelet exit code first part)", + kubeletMsg: `PostStartHookError: Error executing postStart hook: [postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after 5s expired). Exit code 137.`, + expectedMsg: "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after 5s expired). Exit code 137.", + }, + { + name: "Full Kubelet message match - Generic Fail (no internal message field, no Kubelet exit code first part)", + kubeletMsg: `PostStartHookError: Error executing postStart hook: [postStart hook] Commands failed with exit code 2.`, + expectedMsg: "[postStart hook] Commands failed with exit code 2.", + }, + { + name: "Kubelet internal message with escaped quotes and script output", + kubeletMsg: `PostStartHookError: rpc error: code = Unknown desc = failed to exec in container: command /bin/sh -c export POSTSTART_TIMEOUT_DURATION="30s"; export POSTSTART_KILL_AFTER_DURATION="10s"; echo "[postStart hook] Executing user commands with timeout ${POSTSTART_TIMEOUT_DURATION}, kill after ${POSTSTART_KILL_AFTER_DURATION}..."; _script_to_run() { set -e\\necho \\\'hello\\\' >&2\\nexit 1\\n }; timeout --preserve-status --kill-after="${POSTSTART_KILL_AFTER_DURATION}" "${POSTSTART_TIMEOUT_DURATION}" /bin/sh -c "_script_to_run" 1> >(tee -a "/tmp/poststart-stdout.txt") 2> >(tee -a "/tmp/poststart-stderr.txt" >&2); exit_code=$?; if [ $exit_code -eq 143 ]; then echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}). Exit code 143." >&2; elif [ $exit_code -eq 137 ]; then echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION} expired). Exit code 137." >&2; elif [ $exit_code -ne 0 ]; then echo "[postStart hook] Commands failed with exit code ${exit_code}." >&2; fi; exit $exit_code: exit status 1, message: "[postStart hook] Commands failed with exit code 1."`, + expectedMsg: "[postStart hook] Commands failed with exit code 1.", + }, + { + name: "Fallback - Unrecognized Kubelet message", + kubeletMsg: "PostStartHookError: An unexpected error occurred.", + expectedMsg: "[postStart hook] failed with an unknown error (see pod events or container logs for more details)", + }, + { + name: "Fallback - Empty Kubelet message", + kubeletMsg: "", + expectedMsg: "[postStart hook] failed with an unknown error (see pod events or container logs for more details)", + }, + { + name: "Kubelet internal message - SIGTERM - with leading/trailing spaces in message", + kubeletMsg: `PostStartHookError: rpc error: code = Unknown desc = command error: command terminated by SIGTERM, message: " [postStart hook] Commands terminated by SIGTERM (likely timed out after 30s). Exit code 143. "`, + expectedMsg: "[postStart hook] Commands terminated by SIGTERM (likely timed out after 30s). Exit code 143.", + }, + { + name: "Kubelet exit code - 143 - with surrounding text", + kubeletMsg: `FailedPostStartHook: container "theia-ide" postStart hook failed: command 'sh -c mycommand' exited with 143:`, + expectedMsg: "[postStart hook] Commands terminated by SIGTERM due to timeout", + }, + { + name: "Fallback - Kubelet message with exit code 0 but error text", + kubeletMsg: `PostStartHookError: command "sh -c echo hello && exit 0" exited with 0: "unexpected error"`, + expectedMsg: "[postStart hook] failed with an unknown error (see pod events or container logs for more details)", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getConcisePostStartFailureMessage(tt.kubeletMsg); got != tt.expectedMsg { + t.Errorf("getConcisePostStartFailureMessage() = %v, want %v", got, tt.expectedMsg) + } + }) + } +} From 4a999c8022229e77da537221e2bb288a2bd243fb Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Thu, 29 May 2025 16:55:32 +0300 Subject: [PATCH 04/22] fixup! chore: run make update_devworkspace_api update_devworkspace_crds generate_all Signed-off-by: Oleksii Kurinnyi --- apis/controller/v1alpha1/zz_generated.deepcopy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apis/controller/v1alpha1/zz_generated.deepcopy.go b/apis/controller/v1alpha1/zz_generated.deepcopy.go index a11084352..792ee1633 100644 --- a/apis/controller/v1alpha1/zz_generated.deepcopy.go +++ b/apis/controller/v1alpha1/zz_generated.deepcopy.go @@ -21,7 +21,7 @@ package v1alpha1 import ( "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) From 095b2f2f01805844dd0dd93411a239d143ce48ee Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Thu, 29 May 2025 16:59:53 +0300 Subject: [PATCH 05/22] fixup! test: add/update tests related to postStart hook commands Signed-off-by: Oleksii Kurinnyi --- pkg/library/status/check_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pkg/library/status/check_test.go b/pkg/library/status/check_test.go index aa3498368..b9eabae03 100644 --- a/pkg/library/status/check_test.go +++ b/pkg/library/status/check_test.go @@ -1,3 +1,16 @@ +// Copyright (c) 2019-2025 Red Hat, Inc. +// 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 status import ( From c0ee548e055a4e561f878a36ddbb09c14bb2715c Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Fri, 30 May 2025 12:22:48 +0300 Subject: [PATCH 06/22] fixup! feat: timeout the postStart hook commands Signed-off-by: Oleksii Kurinnyi --- pkg/library/status/check.go | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/pkg/library/status/check.go b/pkg/library/status/check.go index 0c38c1021..c3dca5930 100644 --- a/pkg/library/status/check.go +++ b/pkg/library/status/check.go @@ -31,6 +31,23 @@ import ( k8sclient "sigs.k8s.io/controller-runtime/pkg/client" ) +var ( + // reTerminatedSigterm matches: "[postStart hook] Commands terminated by SIGTERM (likely timed out after ...s). Exit code 143." + reTerminatedSigterm = regexp.MustCompile(`(\\[postStart hook\\] Commands terminated by SIGTERM \\(likely timed out after [^)]+?\\)\\. Exit code 143\\.)`) + + // reKilledSigkill matches: "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ...s expired). Exit code 137." + reKilledSigkill = regexp.MustCompile(`(\\[postStart hook\\] Commands forcefully killed by SIGKILL \\(likely after --kill-after [^)]+?\\)\\. Exit code 137\\.)`) + + // reGenericFailedExitCode matches: "[postStart hook] Commands failed with exit code ..." (for any other script-reported non-zero exit code) + reGenericFailedExitCode = regexp.MustCompile(`(\\[postStart hook\\] Commands failed with exit code \\d+\\.)`) + + // reKubeletInternalMessage regex to capture Kubelet's explicit message field content if it exists + reKubeletInternalMessage = regexp.MustCompile(`message:\\s*"([^"]*)"`) + + // reKubeletExitCode regex to capture Kubelet's reported exit code for the hook command + reKubeletExitCode = regexp.MustCompile(`exited with (\\d+):`) +) + var containerFailureStateReasons = []string{ "CrashLoopBackOff", "ImagePullBackOff", @@ -166,25 +183,7 @@ func CheckPodEvents(pod *corev1.Pod, workspaceID string, ignoredEvents []string, // getConcisePostStartFailureMessage tries to parse the Kubelet's verbose message // for a PostStartHookError into a more user-friendly one. func getConcisePostStartFailureMessage(kubeletMsg string) string { - - /* regexes for specific messages from our postStart script's output */ - - // matches: "[postStart hook] Commands terminated by SIGTERM (likely timed out after ...s). Exit code 143." - reTerminatedSigterm := regexp.MustCompile(`(\[postStart hook\] Commands terminated by SIGTERM \(likely timed out after [^)]+?\)\. Exit code 143\.)`) - - // matches: "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ...s expired). Exit code 137." - reKilledSigkill := regexp.MustCompile(`(\[postStart hook\] Commands forcefully killed by SIGKILL \(likely after --kill-after [^)]+?\)\. Exit code 137\.)`) - - // matches: "[postStart hook] Commands failed with exit code ..." (for any other script-reported non-zero exit code) - reGenericFailedExitCode := regexp.MustCompile(`(\[postStart hook\] Commands failed with exit code \d+\.)`) - - // regex to capture Kubelet's explicit message field content if it exists - reKubeletInternalMessage := regexp.MustCompile(`message:\s*"([^"]*)"`) - - // regex to capture Kubelet's reported exit code for the hook command - reKubeletExitCode := regexp.MustCompile(`exited with (\d+):`) - - /* 1: check Kubelet's explicit `message: "..."` field for the specific output */ + /* 1: check Kubelet's explicit 'message: "..."' field for the specific output */ kubeletInternalMsgMatch := reKubeletInternalMessage.FindStringSubmatch(kubeletMsg) if len(kubeletInternalMsgMatch) > 1 && kubeletInternalMsgMatch[1] != "" { From 7d064b9d124ccf74b0b249335f022576f0e9120d Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Fri, 30 May 2025 14:20:57 +0300 Subject: [PATCH 07/22] fixup! fixup! test: add/update tests related to postStart hook commands Signed-off-by: Oleksii Kurinnyi --- pkg/library/status/check.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/library/status/check.go b/pkg/library/status/check.go index c3dca5930..bd8006143 100644 --- a/pkg/library/status/check.go +++ b/pkg/library/status/check.go @@ -33,19 +33,19 @@ import ( var ( // reTerminatedSigterm matches: "[postStart hook] Commands terminated by SIGTERM (likely timed out after ...s). Exit code 143." - reTerminatedSigterm = regexp.MustCompile(`(\\[postStart hook\\] Commands terminated by SIGTERM \\(likely timed out after [^)]+?\\)\\. Exit code 143\\.)`) + reTerminatedSigterm = regexp.MustCompile(`\[postStart hook\] Commands terminated by SIGTERM \(likely timed out after \d+[^\)]+?\)\. Exit code 143\.`) // reKilledSigkill matches: "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ...s expired). Exit code 137." - reKilledSigkill = regexp.MustCompile(`(\\[postStart hook\\] Commands forcefully killed by SIGKILL \\(likely after --kill-after [^)]+?\\)\\. Exit code 137\\.)`) + reKilledSigkill = regexp.MustCompile(`\[postStart hook\] Commands forcefully killed by SIGKILL \(likely after --kill-after \d+[^\)]+?\)\. Exit code 137\.`) // reGenericFailedExitCode matches: "[postStart hook] Commands failed with exit code ..." (for any other script-reported non-zero exit code) - reGenericFailedExitCode = regexp.MustCompile(`(\\[postStart hook\\] Commands failed with exit code \\d+\\.)`) + reGenericFailedExitCode = regexp.MustCompile(`\[postStart hook\] Commands failed with exit code \d+\.`) // reKubeletInternalMessage regex to capture Kubelet's explicit message field content if it exists - reKubeletInternalMessage = regexp.MustCompile(`message:\\s*"([^"]*)"`) + reKubeletInternalMessage = regexp.MustCompile(`message:\s*"([^"]*)"`) // reKubeletExitCode regex to capture Kubelet's reported exit code for the hook command - reKubeletExitCode = regexp.MustCompile(`exited with (\\d+):`) + reKubeletExitCode = regexp.MustCompile(`exited with (\d+):`) ) var containerFailureStateReasons = []string{ From 838b06be3f351cb947410a47bfd79361e1af9846 Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Tue, 3 Jun 2025 14:12:11 +0300 Subject: [PATCH 08/22] fixup! fixup! feat: timeout the postStart hook commands Signed-off-by: Oleksii Kurinnyi --- pkg/library/lifecycle/poststart.go | 40 ++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/pkg/library/lifecycle/poststart.go b/pkg/library/lifecycle/poststart.go index 46042dddd..e4eec539a 100644 --- a/pkg/library/lifecycle/poststart.go +++ b/pkg/library/lifecycle/poststart.go @@ -108,26 +108,44 @@ func buildUserScript(commands []dw.Command) (string, error) { // generateScriptWithTimeout wraps a given user script with timeout logic, // environment variable exports, and specific exit code handling. // The killAfterDurationSeconds is hardcoded to 5s within this generated script. +// It conditionally prefixes the user script with the timeout command if available. func generateScriptWithTimeout(escapedUserScript string, timeoutSeconds int32) string { return fmt.Sprintf(` export POSTSTART_TIMEOUT_DURATION="%d" export POSTSTART_KILL_AFTER_DURATION="5" -echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 +_TIMEOUT_COMMAND_PART="" +_WAS_TIMEOUT_USED="false" # Use strings "true" or "false" for shell boolean -# Run the user's script under the 'timeout' utility. -timeout --preserve-status --kill-after="${POSTSTART_KILL_AFTER_DURATION}" "${POSTSTART_TIMEOUT_DURATION}" /bin/sh -c '%s' +if command -v timeout >/dev/null 2>&1; then + echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 + _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=\"${POSTSTART_KILL_AFTER_DURATION}\" \"${POSTSTART_TIMEOUT_DURATION}\"" + _WAS_TIMEOUT_USED="true" +else + echo "[postStart hook] WARNING: 'timeout' utility not found. Executing commands without timeout." >&2 +fi + +# Execute the user's script +${_TIMEOUT_COMMAND_PART} /bin/sh -c '%s' exit_code=$? -# Check the exit code from 'timeout' -if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) - echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 -elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) - echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 -elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code - echo "[postStart hook] Commands failed with exit code $exit_code." >&2 +# Check the exit code based on whether timeout was attempted +if [ "$_WAS_TIMEOUT_USED" = "true" ]; then + if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) + echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 + elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) + echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 + elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code + echo "[postStart hook] Commands failed with exit code $exit_code." >&2 + else + echo "[postStart hook] Commands completed successfully within the time limit." >&2 + fi else - echo "[postStart hook] Commands completed successfully within the time limit." >&2 + if [ $exit_code -ne 0 ]; then + echo "[postStart hook] Commands failed with exit code $exit_code (no timeout)." >&2 + else + echo "[postStart hook] Commands completed successfully (no timeout)." >&2 + fi fi exit $exit_code From d50e932e285b45694533e7c12a2bdbf8de8b7a7f Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Tue, 3 Jun 2025 14:14:35 +0300 Subject: [PATCH 09/22] fixup! fixup! fixup! test: add/update tests related to postStart hook commands Signed-off-by: Oleksii Kurinnyi --- pkg/library/lifecycle/poststart_test.go | 156 +++++++++++++----- .../adds_all_postStart_commands.yaml | 78 ++++++--- .../testdata/postStart/basic_postStart.yaml | 39 +++-- .../multiple_poststart_commands.yaml | 39 +++-- .../postStart/workingDir_postStart.yaml | 39 +++-- 5 files changed, 252 insertions(+), 99 deletions(-) diff --git a/pkg/library/lifecycle/poststart_test.go b/pkg/library/lifecycle/poststart_test.go index c367689f5..8f9851b0a 100644 --- a/pkg/library/lifecycle/poststart_test.go +++ b/pkg/library/lifecycle/poststart_test.go @@ -308,22 +308,39 @@ func TestGenerateScriptWithTimeout(t *testing.T) { export POSTSTART_TIMEOUT_DURATION="10" export POSTSTART_KILL_AFTER_DURATION="5" -echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 +_TIMEOUT_COMMAND_PART="" +_WAS_TIMEOUT_USED="false" # Use strings "true" or "false" for shell boolean -# Run the user's script under the 'timeout' utility. -timeout --preserve-status --kill-after="${POSTSTART_KILL_AFTER_DURATION}" "${POSTSTART_TIMEOUT_DURATION}" /bin/sh -c 'echo 'hello world' +if command -v timeout >/dev/null 2>&1; then + echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 + _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=\"${POSTSTART_KILL_AFTER_DURATION}\" \"${POSTSTART_TIMEOUT_DURATION}\"" + _WAS_TIMEOUT_USED="true" +else + echo "[postStart hook] WARNING: 'timeout' utility not found. Executing commands without timeout." >&2 +fi + +# Execute the user's script +${_TIMEOUT_COMMAND_PART} /bin/sh -c 'echo 'hello world' sleep 1' exit_code=$? -# Check the exit code from 'timeout' -if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) - echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 -elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) - echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 -elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code - echo "[postStart hook] Commands failed with exit code $exit_code." >&2 +# Check the exit code based on whether timeout was attempted +if [ "$_WAS_TIMEOUT_USED" = "true" ]; then + if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) + echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 + elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) + echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 + elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code + echo "[postStart hook] Commands failed with exit code $exit_code." >&2 + else + echo "[postStart hook] Commands completed successfully within the time limit." >&2 + fi else - echo "[postStart hook] Commands completed successfully within the time limit." >&2 + if [ $exit_code -ne 0 ]; then + echo "[postStart hook] Commands failed with exit code $exit_code (no timeout)." >&2 + else + echo "[postStart hook] Commands completed successfully (no timeout)." >&2 + fi fi exit $exit_code @@ -337,21 +354,38 @@ exit $exit_code export POSTSTART_TIMEOUT_DURATION="0" export POSTSTART_KILL_AFTER_DURATION="5" -echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 +_TIMEOUT_COMMAND_PART="" +_WAS_TIMEOUT_USED="false" # Use strings "true" or "false" for shell boolean + +if command -v timeout >/dev/null 2>&1; then + echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 + _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=\"${POSTSTART_KILL_AFTER_DURATION}\" \"${POSTSTART_TIMEOUT_DURATION}\"" + _WAS_TIMEOUT_USED="true" +else + echo "[postStart hook] WARNING: 'timeout' utility not found. Executing commands without timeout." >&2 +fi -# Run the user's script under the 'timeout' utility. -timeout --preserve-status --kill-after="${POSTSTART_KILL_AFTER_DURATION}" "${POSTSTART_TIMEOUT_DURATION}" /bin/sh -c 'echo 'running indefinitely...'' +# Execute the user's script +${_TIMEOUT_COMMAND_PART} /bin/sh -c 'echo 'running indefinitely...'' exit_code=$? -# Check the exit code from 'timeout' -if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) - echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 -elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) - echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 -elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code - echo "[postStart hook] Commands failed with exit code $exit_code." >&2 +# Check the exit code based on whether timeout was attempted +if [ "$_WAS_TIMEOUT_USED" = "true" ]; then + if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) + echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 + elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) + echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 + elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code + echo "[postStart hook] Commands failed with exit code $exit_code." >&2 + else + echo "[postStart hook] Commands completed successfully within the time limit." >&2 + fi else - echo "[postStart hook] Commands completed successfully within the time limit." >&2 + if [ $exit_code -ne 0 ]; then + echo "[postStart hook] Commands failed with exit code $exit_code (no timeout)." >&2 + else + echo "[postStart hook] Commands completed successfully (no timeout)." >&2 + fi fi exit $exit_code @@ -365,21 +399,38 @@ exit $exit_code export POSTSTART_TIMEOUT_DURATION="5" export POSTSTART_KILL_AFTER_DURATION="5" -echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 +_TIMEOUT_COMMAND_PART="" +_WAS_TIMEOUT_USED="false" # Use strings "true" or "false" for shell boolean -# Run the user's script under the 'timeout' utility. -timeout --preserve-status --kill-after="${POSTSTART_KILL_AFTER_DURATION}" "${POSTSTART_TIMEOUT_DURATION}" /bin/sh -c '' +if command -v timeout >/dev/null 2>&1; then + echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 + _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=\"${POSTSTART_KILL_AFTER_DURATION}\" \"${POSTSTART_TIMEOUT_DURATION}\"" + _WAS_TIMEOUT_USED="true" +else + echo "[postStart hook] WARNING: 'timeout' utility not found. Executing commands without timeout." >&2 +fi + +# Execute the user's script +${_TIMEOUT_COMMAND_PART} /bin/sh -c '' exit_code=$? -# Check the exit code from 'timeout' -if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) - echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 -elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) - echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 -elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code - echo "[postStart hook] Commands failed with exit code $exit_code." >&2 +# Check the exit code based on whether timeout was attempted +if [ "$_WAS_TIMEOUT_USED" = "true" ]; then + if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) + echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 + elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) + echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 + elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code + echo "[postStart hook] Commands failed with exit code $exit_code." >&2 + else + echo "[postStart hook] Commands completed successfully within the time limit." >&2 + fi else - echo "[postStart hook] Commands completed successfully within the time limit." >&2 + if [ $exit_code -ne 0 ]; then + echo "[postStart hook] Commands failed with exit code $exit_code (no timeout)." >&2 + else + echo "[postStart hook] Commands completed successfully (no timeout)." >&2 + fi fi exit $exit_code @@ -393,21 +444,38 @@ exit $exit_code export POSTSTART_TIMEOUT_DURATION="30" export POSTSTART_KILL_AFTER_DURATION="5" -echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 +_TIMEOUT_COMMAND_PART="" +_WAS_TIMEOUT_USED="false" # Use strings "true" or "false" for shell boolean + +if command -v timeout >/dev/null 2>&1; then + echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 + _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=\"${POSTSTART_KILL_AFTER_DURATION}\" \"${POSTSTART_TIMEOUT_DURATION}\"" + _WAS_TIMEOUT_USED="true" +else + echo "[postStart hook] WARNING: 'timeout' utility not found. Executing commands without timeout." >&2 +fi -# Run the user's script under the 'timeout' utility. -timeout --preserve-status --kill-after="${POSTSTART_KILL_AFTER_DURATION}" "${POSTSTART_TIMEOUT_DURATION}" /bin/sh -c 'echo 'it'\''s complex'' +# Execute the user's script +${_TIMEOUT_COMMAND_PART} /bin/sh -c 'echo 'it'\''s complex'' exit_code=$? -# Check the exit code from 'timeout' -if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) - echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 -elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) - echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 -elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code - echo "[postStart hook] Commands failed with exit code $exit_code." >&2 +# Check the exit code based on whether timeout was attempted +if [ "$_WAS_TIMEOUT_USED" = "true" ]; then + if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) + echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 + elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) + echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 + elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code + echo "[postStart hook] Commands failed with exit code $exit_code." >&2 + else + echo "[postStart hook] Commands completed successfully within the time limit." >&2 + fi else - echo "[postStart hook] Commands completed successfully within the time limit." >&2 + if [ $exit_code -ne 0 ]; then + echo "[postStart hook] Commands failed with exit code $exit_code (no timeout)." >&2 + else + echo "[postStart hook] Commands completed successfully (no timeout)." >&2 + fi fi exit $exit_code diff --git a/pkg/library/lifecycle/testdata/postStart/adds_all_postStart_commands.yaml b/pkg/library/lifecycle/testdata/postStart/adds_all_postStart_commands.yaml index 779c5afb9..4feb759a7 100644 --- a/pkg/library/lifecycle/testdata/postStart/adds_all_postStart_commands.yaml +++ b/pkg/library/lifecycle/testdata/postStart/adds_all_postStart_commands.yaml @@ -44,22 +44,39 @@ output: export POSTSTART_TIMEOUT_DURATION="0" export POSTSTART_KILL_AFTER_DURATION="5" - echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 + _TIMEOUT_COMMAND_PART="" + _WAS_TIMEOUT_USED="false" # Use strings "true" or "false" for shell boolean - # Run the user's script under the 'timeout' utility. - timeout --preserve-status --kill-after="${POSTSTART_KILL_AFTER_DURATION}" "${POSTSTART_TIMEOUT_DURATION}" /bin/sh -c 'set -e + if command -v timeout >/dev/null 2>&1; then + echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 + _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=\"${POSTSTART_KILL_AFTER_DURATION}\" \"${POSTSTART_TIMEOUT_DURATION}\"" + _WAS_TIMEOUT_USED="true" + else + echo "[postStart hook] WARNING: 'timeout' utility not found. Executing commands without timeout." >&2 + fi + + # Execute the user's script + ${_TIMEOUT_COMMAND_PART} /bin/sh -c 'set -e echo '\''hello world 1'\''' exit_code=$? - # Check the exit code from 'timeout' - if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) - echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 - elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) - echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 - elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code - echo "[postStart hook] Commands failed with exit code $exit_code." >&2 + # Check the exit code based on whether timeout was attempted + if [ "$_WAS_TIMEOUT_USED" = "true" ]; then + if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) + echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 + elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) + echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 + elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code + echo "[postStart hook] Commands failed with exit code $exit_code." >&2 + else + echo "[postStart hook] Commands completed successfully within the time limit." >&2 + fi else - echo "[postStart hook] Commands completed successfully within the time limit." >&2 + if [ $exit_code -ne 0 ]; then + echo "[postStart hook] Commands failed with exit code $exit_code (no timeout)." >&2 + else + echo "[postStart hook] Commands completed successfully (no timeout)." >&2 + fi fi exit $exit_code @@ -84,22 +101,39 @@ output: export POSTSTART_TIMEOUT_DURATION="0" export POSTSTART_KILL_AFTER_DURATION="5" - echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 + _TIMEOUT_COMMAND_PART="" + _WAS_TIMEOUT_USED="false" # Use strings "true" or "false" for shell boolean + + if command -v timeout >/dev/null 2>&1; then + echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 + _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=\"${POSTSTART_KILL_AFTER_DURATION}\" \"${POSTSTART_TIMEOUT_DURATION}\"" + _WAS_TIMEOUT_USED="true" + else + echo "[postStart hook] WARNING: 'timeout' utility not found. Executing commands without timeout." >&2 + fi - # Run the user's script under the 'timeout' utility. - timeout --preserve-status --kill-after="${POSTSTART_KILL_AFTER_DURATION}" "${POSTSTART_TIMEOUT_DURATION}" /bin/sh -c 'set -e + # Execute the user's script + ${_TIMEOUT_COMMAND_PART} /bin/sh -c 'set -e cd '\''/tmp/test-dir'\'' && echo '\''hello world 2'\''' exit_code=$? - # Check the exit code from 'timeout' - if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) - echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 - elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) - echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 - elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code - echo "[postStart hook] Commands failed with exit code $exit_code." >&2 + # Check the exit code based on whether timeout was attempted + if [ "$_WAS_TIMEOUT_USED" = "true" ]; then + if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) + echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 + elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) + echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 + elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code + echo "[postStart hook] Commands failed with exit code $exit_code." >&2 + else + echo "[postStart hook] Commands completed successfully within the time limit." >&2 + fi else - echo "[postStart hook] Commands completed successfully within the time limit." >&2 + if [ $exit_code -ne 0 ]; then + echo "[postStart hook] Commands failed with exit code $exit_code (no timeout)." >&2 + else + echo "[postStart hook] Commands completed successfully (no timeout)." >&2 + fi fi exit $exit_code diff --git a/pkg/library/lifecycle/testdata/postStart/basic_postStart.yaml b/pkg/library/lifecycle/testdata/postStart/basic_postStart.yaml index 8f6063013..79187a9f3 100644 --- a/pkg/library/lifecycle/testdata/postStart/basic_postStart.yaml +++ b/pkg/library/lifecycle/testdata/postStart/basic_postStart.yaml @@ -33,22 +33,39 @@ output: export POSTSTART_TIMEOUT_DURATION="0" export POSTSTART_KILL_AFTER_DURATION="5" - echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 + _TIMEOUT_COMMAND_PART="" + _WAS_TIMEOUT_USED="false" # Use strings "true" or "false" for shell boolean - # Run the user's script under the 'timeout' utility. - timeout --preserve-status --kill-after="${POSTSTART_KILL_AFTER_DURATION}" "${POSTSTART_TIMEOUT_DURATION}" /bin/sh -c 'set -e + if command -v timeout >/dev/null 2>&1; then + echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 + _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=\"${POSTSTART_KILL_AFTER_DURATION}\" \"${POSTSTART_TIMEOUT_DURATION}\"" + _WAS_TIMEOUT_USED="true" + else + echo "[postStart hook] WARNING: 'timeout' utility not found. Executing commands without timeout." >&2 + fi + + # Execute the user's script + ${_TIMEOUT_COMMAND_PART} /bin/sh -c 'set -e echo '\''hello world'\''' exit_code=$? - # Check the exit code from 'timeout' - if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) - echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 - elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) - echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 - elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code - echo "[postStart hook] Commands failed with exit code $exit_code." >&2 + # Check the exit code based on whether timeout was attempted + if [ "$_WAS_TIMEOUT_USED" = "true" ]; then + if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) + echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 + elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) + echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 + elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code + echo "[postStart hook] Commands failed with exit code $exit_code." >&2 + else + echo "[postStart hook] Commands completed successfully within the time limit." >&2 + fi else - echo "[postStart hook] Commands completed successfully within the time limit." >&2 + if [ $exit_code -ne 0 ]; then + echo "[postStart hook] Commands failed with exit code $exit_code (no timeout)." >&2 + else + echo "[postStart hook] Commands completed successfully (no timeout)." >&2 + fi fi exit $exit_code diff --git a/pkg/library/lifecycle/testdata/postStart/multiple_poststart_commands.yaml b/pkg/library/lifecycle/testdata/postStart/multiple_poststart_commands.yaml index bd9946daa..5b3043eb5 100644 --- a/pkg/library/lifecycle/testdata/postStart/multiple_poststart_commands.yaml +++ b/pkg/library/lifecycle/testdata/postStart/multiple_poststart_commands.yaml @@ -38,23 +38,40 @@ output: export POSTSTART_TIMEOUT_DURATION="0" export POSTSTART_KILL_AFTER_DURATION="5" - echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 + _TIMEOUT_COMMAND_PART="" + _WAS_TIMEOUT_USED="false" # Use strings "true" or "false" for shell boolean - # Run the user's script under the 'timeout' utility. - timeout --preserve-status --kill-after="${POSTSTART_KILL_AFTER_DURATION}" "${POSTSTART_TIMEOUT_DURATION}" /bin/sh -c 'set -e + if command -v timeout >/dev/null 2>&1; then + echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 + _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=\"${POSTSTART_KILL_AFTER_DURATION}\" \"${POSTSTART_TIMEOUT_DURATION}\"" + _WAS_TIMEOUT_USED="true" + else + echo "[postStart hook] WARNING: 'timeout' utility not found. Executing commands without timeout." >&2 + fi + + # Execute the user's script + ${_TIMEOUT_COMMAND_PART} /bin/sh -c 'set -e echo '\''hello world 1'\'' echo '\''hello world 2'\''' exit_code=$? - # Check the exit code from 'timeout' - if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) - echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 - elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) - echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 - elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code - echo "[postStart hook] Commands failed with exit code $exit_code." >&2 + # Check the exit code based on whether timeout was attempted + if [ "$_WAS_TIMEOUT_USED" = "true" ]; then + if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) + echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 + elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) + echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 + elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code + echo "[postStart hook] Commands failed with exit code $exit_code." >&2 + else + echo "[postStart hook] Commands completed successfully within the time limit." >&2 + fi else - echo "[postStart hook] Commands completed successfully within the time limit." >&2 + if [ $exit_code -ne 0 ]; then + echo "[postStart hook] Commands failed with exit code $exit_code (no timeout)." >&2 + else + echo "[postStart hook] Commands completed successfully (no timeout)." >&2 + fi fi exit $exit_code diff --git a/pkg/library/lifecycle/testdata/postStart/workingDir_postStart.yaml b/pkg/library/lifecycle/testdata/postStart/workingDir_postStart.yaml index 7cd1ca669..30358fba9 100644 --- a/pkg/library/lifecycle/testdata/postStart/workingDir_postStart.yaml +++ b/pkg/library/lifecycle/testdata/postStart/workingDir_postStart.yaml @@ -34,22 +34,39 @@ output: export POSTSTART_TIMEOUT_DURATION="0" export POSTSTART_KILL_AFTER_DURATION="5" - echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 + _TIMEOUT_COMMAND_PART="" + _WAS_TIMEOUT_USED="false" # Use strings "true" or "false" for shell boolean - # Run the user's script under the 'timeout' utility. - timeout --preserve-status --kill-after="${POSTSTART_KILL_AFTER_DURATION}" "${POSTSTART_TIMEOUT_DURATION}" /bin/sh -c 'set -e + if command -v timeout >/dev/null 2>&1; then + echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 + _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=\"${POSTSTART_KILL_AFTER_DURATION}\" \"${POSTSTART_TIMEOUT_DURATION}\"" + _WAS_TIMEOUT_USED="true" + else + echo "[postStart hook] WARNING: 'timeout' utility not found. Executing commands without timeout." >&2 + fi + + # Execute the user's script + ${_TIMEOUT_COMMAND_PART} /bin/sh -c 'set -e cd '\''/tmp/test-dir'\'' && echo '\''hello world'\''' exit_code=$? - # Check the exit code from 'timeout' - if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) - echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 - elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) - echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 - elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code - echo "[postStart hook] Commands failed with exit code $exit_code." >&2 + # Check the exit code based on whether timeout was attempted + if [ "$_WAS_TIMEOUT_USED" = "true" ]; then + if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) + echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 + elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) + echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 + elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code + echo "[postStart hook] Commands failed with exit code $exit_code." >&2 + else + echo "[postStart hook] Commands completed successfully within the time limit." >&2 + fi else - echo "[postStart hook] Commands completed successfully within the time limit." >&2 + if [ $exit_code -ne 0 ]; then + echo "[postStart hook] Commands failed with exit code $exit_code (no timeout)." >&2 + else + echo "[postStart hook] Commands completed successfully (no timeout)." >&2 + fi fi exit $exit_code From 95baeb88e4eeb611e221b2242314be87a50695b5 Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Tue, 3 Jun 2025 16:11:20 +0300 Subject: [PATCH 10/22] fixup! fixup! fixup! feat: timeout the postStart hook commands Signed-off-by: Oleksii Kurinnyi --- pkg/library/lifecycle/poststart.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/library/lifecycle/poststart.go b/pkg/library/lifecycle/poststart.go index e4eec539a..d1dabc240 100644 --- a/pkg/library/lifecycle/poststart.go +++ b/pkg/library/lifecycle/poststart.go @@ -119,7 +119,7 @@ _WAS_TIMEOUT_USED="false" # Use strings "true" or "false" for shell boolean if command -v timeout >/dev/null 2>&1; then echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 - _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=\"${POSTSTART_KILL_AFTER_DURATION}\" \"${POSTSTART_TIMEOUT_DURATION}\"" + _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=${POSTSTART_KILL_AFTER_DURATION} ${POSTSTART_TIMEOUT_DURATION}" _WAS_TIMEOUT_USED="true" else echo "[postStart hook] WARNING: 'timeout' utility not found. Executing commands without timeout." >&2 From 732bd4ea6b564898188a93d435eb908bd51df753 Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Tue, 3 Jun 2025 16:12:17 +0300 Subject: [PATCH 11/22] fixup! fixup! fixup! fixup! test: add/update tests related to postStart hook commands Signed-off-by: Oleksii Kurinnyi --- pkg/library/lifecycle/poststart_test.go | 8 ++++---- .../testdata/postStart/adds_all_postStart_commands.yaml | 4 ++-- .../lifecycle/testdata/postStart/basic_postStart.yaml | 2 +- .../testdata/postStart/multiple_poststart_commands.yaml | 2 +- .../testdata/postStart/workingDir_postStart.yaml | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/library/lifecycle/poststart_test.go b/pkg/library/lifecycle/poststart_test.go index 8f9851b0a..53e3f0768 100644 --- a/pkg/library/lifecycle/poststart_test.go +++ b/pkg/library/lifecycle/poststart_test.go @@ -313,7 +313,7 @@ _WAS_TIMEOUT_USED="false" # Use strings "true" or "false" for shell boolean if command -v timeout >/dev/null 2>&1; then echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 - _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=\"${POSTSTART_KILL_AFTER_DURATION}\" \"${POSTSTART_TIMEOUT_DURATION}\"" + _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=${POSTSTART_KILL_AFTER_DURATION} ${POSTSTART_TIMEOUT_DURATION}" _WAS_TIMEOUT_USED="true" else echo "[postStart hook] WARNING: 'timeout' utility not found. Executing commands without timeout." >&2 @@ -359,7 +359,7 @@ _WAS_TIMEOUT_USED="false" # Use strings "true" or "false" for shell boolean if command -v timeout >/dev/null 2>&1; then echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 - _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=\"${POSTSTART_KILL_AFTER_DURATION}\" \"${POSTSTART_TIMEOUT_DURATION}\"" + _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=${POSTSTART_KILL_AFTER_DURATION} ${POSTSTART_TIMEOUT_DURATION}" _WAS_TIMEOUT_USED="true" else echo "[postStart hook] WARNING: 'timeout' utility not found. Executing commands without timeout." >&2 @@ -404,7 +404,7 @@ _WAS_TIMEOUT_USED="false" # Use strings "true" or "false" for shell boolean if command -v timeout >/dev/null 2>&1; then echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 - _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=\"${POSTSTART_KILL_AFTER_DURATION}\" \"${POSTSTART_TIMEOUT_DURATION}\"" + _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=${POSTSTART_KILL_AFTER_DURATION} ${POSTSTART_TIMEOUT_DURATION}" _WAS_TIMEOUT_USED="true" else echo "[postStart hook] WARNING: 'timeout' utility not found. Executing commands without timeout." >&2 @@ -449,7 +449,7 @@ _WAS_TIMEOUT_USED="false" # Use strings "true" or "false" for shell boolean if command -v timeout >/dev/null 2>&1; then echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 - _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=\"${POSTSTART_KILL_AFTER_DURATION}\" \"${POSTSTART_TIMEOUT_DURATION}\"" + _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=${POSTSTART_KILL_AFTER_DURATION} ${POSTSTART_TIMEOUT_DURATION}" _WAS_TIMEOUT_USED="true" else echo "[postStart hook] WARNING: 'timeout' utility not found. Executing commands without timeout." >&2 diff --git a/pkg/library/lifecycle/testdata/postStart/adds_all_postStart_commands.yaml b/pkg/library/lifecycle/testdata/postStart/adds_all_postStart_commands.yaml index 4feb759a7..ad0267e43 100644 --- a/pkg/library/lifecycle/testdata/postStart/adds_all_postStart_commands.yaml +++ b/pkg/library/lifecycle/testdata/postStart/adds_all_postStart_commands.yaml @@ -49,7 +49,7 @@ output: if command -v timeout >/dev/null 2>&1; then echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 - _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=\"${POSTSTART_KILL_AFTER_DURATION}\" \"${POSTSTART_TIMEOUT_DURATION}\"" + _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=${POSTSTART_KILL_AFTER_DURATION} ${POSTSTART_TIMEOUT_DURATION}" _WAS_TIMEOUT_USED="true" else echo "[postStart hook] WARNING: 'timeout' utility not found. Executing commands without timeout." >&2 @@ -106,7 +106,7 @@ output: if command -v timeout >/dev/null 2>&1; then echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 - _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=\"${POSTSTART_KILL_AFTER_DURATION}\" \"${POSTSTART_TIMEOUT_DURATION}\"" + _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=${POSTSTART_KILL_AFTER_DURATION} ${POSTSTART_TIMEOUT_DURATION}" _WAS_TIMEOUT_USED="true" else echo "[postStart hook] WARNING: 'timeout' utility not found. Executing commands without timeout." >&2 diff --git a/pkg/library/lifecycle/testdata/postStart/basic_postStart.yaml b/pkg/library/lifecycle/testdata/postStart/basic_postStart.yaml index 79187a9f3..94052b769 100644 --- a/pkg/library/lifecycle/testdata/postStart/basic_postStart.yaml +++ b/pkg/library/lifecycle/testdata/postStart/basic_postStart.yaml @@ -38,7 +38,7 @@ output: if command -v timeout >/dev/null 2>&1; then echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 - _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=\"${POSTSTART_KILL_AFTER_DURATION}\" \"${POSTSTART_TIMEOUT_DURATION}\"" + _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=${POSTSTART_KILL_AFTER_DURATION} ${POSTSTART_TIMEOUT_DURATION}" _WAS_TIMEOUT_USED="true" else echo "[postStart hook] WARNING: 'timeout' utility not found. Executing commands without timeout." >&2 diff --git a/pkg/library/lifecycle/testdata/postStart/multiple_poststart_commands.yaml b/pkg/library/lifecycle/testdata/postStart/multiple_poststart_commands.yaml index 5b3043eb5..4619954ca 100644 --- a/pkg/library/lifecycle/testdata/postStart/multiple_poststart_commands.yaml +++ b/pkg/library/lifecycle/testdata/postStart/multiple_poststart_commands.yaml @@ -43,7 +43,7 @@ output: if command -v timeout >/dev/null 2>&1; then echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 - _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=\"${POSTSTART_KILL_AFTER_DURATION}\" \"${POSTSTART_TIMEOUT_DURATION}\"" + _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=${POSTSTART_KILL_AFTER_DURATION} ${POSTSTART_TIMEOUT_DURATION}" _WAS_TIMEOUT_USED="true" else echo "[postStart hook] WARNING: 'timeout' utility not found. Executing commands without timeout." >&2 diff --git a/pkg/library/lifecycle/testdata/postStart/workingDir_postStart.yaml b/pkg/library/lifecycle/testdata/postStart/workingDir_postStart.yaml index 30358fba9..039c93512 100644 --- a/pkg/library/lifecycle/testdata/postStart/workingDir_postStart.yaml +++ b/pkg/library/lifecycle/testdata/postStart/workingDir_postStart.yaml @@ -39,7 +39,7 @@ output: if command -v timeout >/dev/null 2>&1; then echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 - _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=\"${POSTSTART_KILL_AFTER_DURATION}\" \"${POSTSTART_TIMEOUT_DURATION}\"" + _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=${POSTSTART_KILL_AFTER_DURATION} ${POSTSTART_TIMEOUT_DURATION}" _WAS_TIMEOUT_USED="true" else echo "[postStart hook] WARNING: 'timeout' utility not found. Executing commands without timeout." >&2 From c090c8ac4ea3a8f617ba3c4408c092808ad61cb1 Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Thu, 10 Jul 2025 15:04:59 +0300 Subject: [PATCH 12/22] fix: fall back to the original behaviour if timeout is disabled Signed-off-by: Oleksii Kurinnyi --- pkg/library/lifecycle/poststart.go | 50 ++++++++++- pkg/library/lifecycle/poststart_test.go | 3 +- .../adds_all_postStart_commands.yaml | 86 +------------------ .../testdata/postStart/basic_postStart.yaml | 42 +-------- .../multiple_poststart_commands.yaml | 44 +--------- .../postStart/workingDir_postStart.yaml | 43 +--------- 6 files changed, 56 insertions(+), 212 deletions(-) diff --git a/pkg/library/lifecycle/poststart.go b/pkg/library/lifecycle/poststart.go index d1dabc240..aafdacb56 100644 --- a/pkg/library/lifecycle/poststart.go +++ b/pkg/library/lifecycle/poststart.go @@ -31,6 +31,11 @@ const ( } _script_to_run } 1> >(tee -a "/tmp/poststart-stdout.txt") 2> >(tee -a "/tmp/poststart-stderr.txt" >&2) +` + + noTimeoutRedirectOutputFmt = `{ +%s +} 1>/tmp/poststart-stdout.txt 2>/tmp/poststart-stderr.txt ` ) @@ -155,10 +160,9 @@ exit $exit_code // processCommandsForPostStart processes a list of DevWorkspace commands // and generates a corev1.LifecycleHandler for the PostStart lifecycle hook. func processCommandsForPostStart(commands []dw.Command, postStartTimeout *int32) (*corev1.LifecycleHandler, error) { - if postStartTimeout == nil { - // The 'timeout' command treats 0 as "no timeout", so it is disabled by default. - defaultTimeout := int32(0) - postStartTimeout = &defaultTimeout + if postStartTimeout == nil || *postStartTimeout == 0 { + // use the fallback if no timeout propagated + return processCommandsWithoutTimeoutFallback(commands) } originalUserScript, err := buildUserScript(commands) @@ -186,3 +190,41 @@ func processCommandsForPostStart(commands []dw.Command, postStartTimeout *int32) } return handler, nil } + +// processCommandsForPostStart builds a lifecycle handler that runs the provided command(s) +// The command has the format +// +// exec: +// +// command: +// - "/bin/sh" +// - "-c" +// - | +// cd +// +func processCommandsWithoutTimeoutFallback(commands []dw.Command) (*corev1.LifecycleHandler, error) { + var dwCommands []string + for _, command := range commands { + execCmd := command.Exec + if len(execCmd.Env) > 0 { + return nil, fmt.Errorf("env vars in postStart command %s are unsupported", command.Id) + } + if execCmd.WorkingDir != "" { + dwCommands = append(dwCommands, fmt.Sprintf("cd %s", execCmd.WorkingDir)) + } + dwCommands = append(dwCommands, execCmd.CommandLine) + } + + joinedCommands := strings.Join(dwCommands, "\n") + + handler := &corev1.LifecycleHandler{ + Exec: &corev1.ExecAction{ + Command: []string{ + "/bin/sh", + "-c", + fmt.Sprintf(redirectOutputFmt, joinedCommands), + }, + }, + } + return handler, nil +} diff --git a/pkg/library/lifecycle/poststart_test.go b/pkg/library/lifecycle/poststart_test.go index 53e3f0768..32b609a66 100644 --- a/pkg/library/lifecycle/poststart_test.go +++ b/pkg/library/lifecycle/poststart_test.go @@ -75,7 +75,8 @@ func TestAddPostStartLifecycleHooks(t *testing.T) { tests := loadAllPostStartTestCasesOrPanic(t, "./testdata/postStart") for _, tt := range tests { t.Run(fmt.Sprintf("%s (%s)", tt.Name, tt.testPath), func(t *testing.T) { - err := AddPostStartLifecycleHooks(tt.Input.Devfile, tt.Input.Containers, nil) + var timeout int32 + err := AddPostStartLifecycleHooks(tt.Input.Devfile, tt.Input.Containers, &timeout) if tt.Output.ErrRegexp != nil && assert.Error(t, err) { assert.Regexp(t, *tt.Output.ErrRegexp, err.Error(), "Error message should match") } else { diff --git a/pkg/library/lifecycle/testdata/postStart/adds_all_postStart_commands.yaml b/pkg/library/lifecycle/testdata/postStart/adds_all_postStart_commands.yaml index ad0267e43..ef71dbbba 100644 --- a/pkg/library/lifecycle/testdata/postStart/adds_all_postStart_commands.yaml +++ b/pkg/library/lifecycle/testdata/postStart/adds_all_postStart_commands.yaml @@ -40,47 +40,7 @@ output: # This script block ensures its exit code is preserved # while its stdout and stderr are tee'd. _script_to_run() { - - export POSTSTART_TIMEOUT_DURATION="0" - export POSTSTART_KILL_AFTER_DURATION="5" - - _TIMEOUT_COMMAND_PART="" - _WAS_TIMEOUT_USED="false" # Use strings "true" or "false" for shell boolean - - if command -v timeout >/dev/null 2>&1; then - echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 - _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=${POSTSTART_KILL_AFTER_DURATION} ${POSTSTART_TIMEOUT_DURATION}" - _WAS_TIMEOUT_USED="true" - else - echo "[postStart hook] WARNING: 'timeout' utility not found. Executing commands without timeout." >&2 - fi - - # Execute the user's script - ${_TIMEOUT_COMMAND_PART} /bin/sh -c 'set -e - echo '\''hello world 1'\''' - exit_code=$? - - # Check the exit code based on whether timeout was attempted - if [ "$_WAS_TIMEOUT_USED" = "true" ]; then - if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) - echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 - elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) - echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 - elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code - echo "[postStart hook] Commands failed with exit code $exit_code." >&2 - else - echo "[postStart hook] Commands completed successfully within the time limit." >&2 - fi - else - if [ $exit_code -ne 0 ]; then - echo "[postStart hook] Commands failed with exit code $exit_code (no timeout)." >&2 - else - echo "[postStart hook] Commands completed successfully (no timeout)." >&2 - fi - fi - - exit $exit_code - # This will be replaced by scriptWithTimeout + echo 'hello world 1' # This will be replaced by scriptWithTimeout } _script_to_run } 1> >(tee -a "/tmp/poststart-stdout.txt") 2> >(tee -a "/tmp/poststart-stderr.txt" >&2) @@ -97,50 +57,10 @@ output: # This script block ensures its exit code is preserved # while its stdout and stderr are tee'd. _script_to_run() { - - export POSTSTART_TIMEOUT_DURATION="0" - export POSTSTART_KILL_AFTER_DURATION="5" - - _TIMEOUT_COMMAND_PART="" - _WAS_TIMEOUT_USED="false" # Use strings "true" or "false" for shell boolean - - if command -v timeout >/dev/null 2>&1; then - echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 - _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=${POSTSTART_KILL_AFTER_DURATION} ${POSTSTART_TIMEOUT_DURATION}" - _WAS_TIMEOUT_USED="true" - else - echo "[postStart hook] WARNING: 'timeout' utility not found. Executing commands without timeout." >&2 - fi - - # Execute the user's script - ${_TIMEOUT_COMMAND_PART} /bin/sh -c 'set -e - cd '\''/tmp/test-dir'\'' && echo '\''hello world 2'\''' - exit_code=$? - - # Check the exit code based on whether timeout was attempted - if [ "$_WAS_TIMEOUT_USED" = "true" ]; then - if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) - echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 - elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) - echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 - elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code - echo "[postStart hook] Commands failed with exit code $exit_code." >&2 - else - echo "[postStart hook] Commands completed successfully within the time limit." >&2 - fi - else - if [ $exit_code -ne 0 ]; then - echo "[postStart hook] Commands failed with exit code $exit_code (no timeout)." >&2 - else - echo "[postStart hook] Commands completed successfully (no timeout)." >&2 - fi - fi - - exit $exit_code - # This will be replaced by scriptWithTimeout + cd /tmp/test-dir + echo 'hello world 2' # This will be replaced by scriptWithTimeout } _script_to_run } 1> >(tee -a "/tmp/poststart-stdout.txt") 2> >(tee -a "/tmp/poststart-stderr.txt" >&2) - - name: test-component-3 image: test-img diff --git a/pkg/library/lifecycle/testdata/postStart/basic_postStart.yaml b/pkg/library/lifecycle/testdata/postStart/basic_postStart.yaml index 94052b769..a99fb7cce 100644 --- a/pkg/library/lifecycle/testdata/postStart/basic_postStart.yaml +++ b/pkg/library/lifecycle/testdata/postStart/basic_postStart.yaml @@ -29,47 +29,7 @@ output: # This script block ensures its exit code is preserved # while its stdout and stderr are tee'd. _script_to_run() { - - export POSTSTART_TIMEOUT_DURATION="0" - export POSTSTART_KILL_AFTER_DURATION="5" - - _TIMEOUT_COMMAND_PART="" - _WAS_TIMEOUT_USED="false" # Use strings "true" or "false" for shell boolean - - if command -v timeout >/dev/null 2>&1; then - echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 - _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=${POSTSTART_KILL_AFTER_DURATION} ${POSTSTART_TIMEOUT_DURATION}" - _WAS_TIMEOUT_USED="true" - else - echo "[postStart hook] WARNING: 'timeout' utility not found. Executing commands without timeout." >&2 - fi - - # Execute the user's script - ${_TIMEOUT_COMMAND_PART} /bin/sh -c 'set -e - echo '\''hello world'\''' - exit_code=$? - - # Check the exit code based on whether timeout was attempted - if [ "$_WAS_TIMEOUT_USED" = "true" ]; then - if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) - echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 - elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) - echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 - elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code - echo "[postStart hook] Commands failed with exit code $exit_code." >&2 - else - echo "[postStart hook] Commands completed successfully within the time limit." >&2 - fi - else - if [ $exit_code -ne 0 ]; then - echo "[postStart hook] Commands failed with exit code $exit_code (no timeout)." >&2 - else - echo "[postStart hook] Commands completed successfully (no timeout)." >&2 - fi - fi - - exit $exit_code - # This will be replaced by scriptWithTimeout + echo 'hello world' # This will be replaced by scriptWithTimeout } _script_to_run } 1> >(tee -a "/tmp/poststart-stdout.txt") 2> >(tee -a "/tmp/poststart-stderr.txt" >&2) diff --git a/pkg/library/lifecycle/testdata/postStart/multiple_poststart_commands.yaml b/pkg/library/lifecycle/testdata/postStart/multiple_poststart_commands.yaml index 4619954ca..df005793c 100644 --- a/pkg/library/lifecycle/testdata/postStart/multiple_poststart_commands.yaml +++ b/pkg/library/lifecycle/testdata/postStart/multiple_poststart_commands.yaml @@ -34,48 +34,8 @@ output: # This script block ensures its exit code is preserved # while its stdout and stderr are tee'd. _script_to_run() { - - export POSTSTART_TIMEOUT_DURATION="0" - export POSTSTART_KILL_AFTER_DURATION="5" - - _TIMEOUT_COMMAND_PART="" - _WAS_TIMEOUT_USED="false" # Use strings "true" or "false" for shell boolean - - if command -v timeout >/dev/null 2>&1; then - echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 - _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=${POSTSTART_KILL_AFTER_DURATION} ${POSTSTART_TIMEOUT_DURATION}" - _WAS_TIMEOUT_USED="true" - else - echo "[postStart hook] WARNING: 'timeout' utility not found. Executing commands without timeout." >&2 - fi - - # Execute the user's script - ${_TIMEOUT_COMMAND_PART} /bin/sh -c 'set -e - echo '\''hello world 1'\'' - echo '\''hello world 2'\''' - exit_code=$? - - # Check the exit code based on whether timeout was attempted - if [ "$_WAS_TIMEOUT_USED" = "true" ]; then - if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) - echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 - elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) - echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 - elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code - echo "[postStart hook] Commands failed with exit code $exit_code." >&2 - else - echo "[postStart hook] Commands completed successfully within the time limit." >&2 - fi - else - if [ $exit_code -ne 0 ]; then - echo "[postStart hook] Commands failed with exit code $exit_code (no timeout)." >&2 - else - echo "[postStart hook] Commands completed successfully (no timeout)." >&2 - fi - fi - - exit $exit_code - # This will be replaced by scriptWithTimeout + echo 'hello world 1' + echo 'hello world 2' # This will be replaced by scriptWithTimeout } _script_to_run } 1> >(tee -a "/tmp/poststart-stdout.txt") 2> >(tee -a "/tmp/poststart-stderr.txt" >&2) diff --git a/pkg/library/lifecycle/testdata/postStart/workingDir_postStart.yaml b/pkg/library/lifecycle/testdata/postStart/workingDir_postStart.yaml index 039c93512..440424ae6 100644 --- a/pkg/library/lifecycle/testdata/postStart/workingDir_postStart.yaml +++ b/pkg/library/lifecycle/testdata/postStart/workingDir_postStart.yaml @@ -30,47 +30,8 @@ output: # This script block ensures its exit code is preserved # while its stdout and stderr are tee'd. _script_to_run() { - - export POSTSTART_TIMEOUT_DURATION="0" - export POSTSTART_KILL_AFTER_DURATION="5" - - _TIMEOUT_COMMAND_PART="" - _WAS_TIMEOUT_USED="false" # Use strings "true" or "false" for shell boolean - - if command -v timeout >/dev/null 2>&1; then - echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 - _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=${POSTSTART_KILL_AFTER_DURATION} ${POSTSTART_TIMEOUT_DURATION}" - _WAS_TIMEOUT_USED="true" - else - echo "[postStart hook] WARNING: 'timeout' utility not found. Executing commands without timeout." >&2 - fi - - # Execute the user's script - ${_TIMEOUT_COMMAND_PART} /bin/sh -c 'set -e - cd '\''/tmp/test-dir'\'' && echo '\''hello world'\''' - exit_code=$? - - # Check the exit code based on whether timeout was attempted - if [ "$_WAS_TIMEOUT_USED" = "true" ]; then - if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) - echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 - elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) - echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 - elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code - echo "[postStart hook] Commands failed with exit code $exit_code." >&2 - else - echo "[postStart hook] Commands completed successfully within the time limit." >&2 - fi - else - if [ $exit_code -ne 0 ]; then - echo "[postStart hook] Commands failed with exit code $exit_code (no timeout)." >&2 - else - echo "[postStart hook] Commands completed successfully (no timeout)." >&2 - fi - fi - - exit $exit_code - # This will be replaced by scriptWithTimeout + cd /tmp/test-dir + echo 'hello world' # This will be replaced by scriptWithTimeout } _script_to_run } 1> >(tee -a "/tmp/poststart-stdout.txt") 2> >(tee -a "/tmp/poststart-stderr.txt" >&2) From f356edf420266ba2d0c1b7c6a62a6ef01d81fbfd Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Mon, 14 Jul 2025 14:43:56 +0300 Subject: [PATCH 13/22] fix: add postStartTimeout Signed-off-by: Oleksii Kurinnyi --- pkg/config/sync.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/config/sync.go b/pkg/config/sync.go index d1ae7acc3..4c1123692 100644 --- a/pkg/config/sync.go +++ b/pkg/config/sync.go @@ -605,6 +605,9 @@ func GetCurrentConfigString(currConfig *controller.OperatorConfiguration) string if workspace.IdleTimeout != defaultConfig.Workspace.IdleTimeout { config = append(config, fmt.Sprintf("workspace.idleTimeout=%s", workspace.IdleTimeout)) } + if workspace.PostStartTimeout != nil && workspace.PostStartTimeout != defaultConfig.Workspace.PostStartTimeout { + config = append(config, fmt.Sprintf("workspace.postStartTimeout=%d", *workspace.PostStartTimeout)) + } if workspace.ProgressTimeout != "" && workspace.ProgressTimeout != defaultConfig.Workspace.ProgressTimeout { config = append(config, fmt.Sprintf("workspace.progressTimeout=%s", workspace.ProgressTimeout)) } From 3b0e379746df5db5931ed29f35a36a7d91ea072a Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Mon, 14 Jul 2025 15:54:18 +0300 Subject: [PATCH 14/22] fix: remove default postStartTimeout value Signed-off-by: Oleksii Kurinnyi --- apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go | 1 - .../bases/controller.devfile.io_devworkspaceoperatorconfigs.yaml | 1 - 2 files changed, 2 deletions(-) diff --git a/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go b/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go index d4f68c122..102cec96e 100644 --- a/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go +++ b/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go @@ -194,7 +194,6 @@ type WorkspaceConfig struct { // that is used to run commands in the workspace container. The timeout is specified in seconds. // If not specified, the timeout is disabled (0 seconds). // +kubebuilder:validation:Minimum=0 - // +kubebuilder:default:=0 // +kubebuilder:validation:Optional // +kubebuilder:validation:Type=integer // +kubebuilder:validation:Format=int32 diff --git a/deploy/templates/crd/bases/controller.devfile.io_devworkspaceoperatorconfigs.yaml b/deploy/templates/crd/bases/controller.devfile.io_devworkspaceoperatorconfigs.yaml index 7bda60bd3..9400673f0 100644 --- a/deploy/templates/crd/bases/controller.devfile.io_devworkspaceoperatorconfigs.yaml +++ b/deploy/templates/crd/bases/controller.devfile.io_devworkspaceoperatorconfigs.yaml @@ -2899,7 +2899,6 @@ spec: type: object type: object postStartTimeout: - default: 0 description: |- PostStartTimeout defines the maximum duration the PostStart hook can run before it is automatically failed. This timeout is used for the postStart lifecycle hook From d2532e21b34d98ce3f0e5f491a7433f85c0523dd Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Mon, 14 Jul 2025 16:21:22 +0300 Subject: [PATCH 15/22] fixup! fix: remove default postStartTimeout value Signed-off-by: Oleksii Kurinnyi --- .../controller.devfile.io_devworkspaceoperatorconfigs.yaml | 1 - deploy/deployment/kubernetes/combined.yaml | 1 - ...orconfigs.controller.devfile.io.CustomResourceDefinition.yaml | 1 - deploy/deployment/openshift/combined.yaml | 1 - ...orconfigs.controller.devfile.io.CustomResourceDefinition.yaml | 1 - 5 files changed, 5 deletions(-) diff --git a/deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml b/deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml index 48b939d97..d879a1a2f 100644 --- a/deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml +++ b/deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml @@ -2765,7 +2765,6 @@ spec: type: object type: object postStartTimeout: - default: 0 description: |- PostStartTimeout defines the maximum duration the PostStart hook can run before it is automatically failed. This timeout is used for the postStart lifecycle hook diff --git a/deploy/deployment/kubernetes/combined.yaml b/deploy/deployment/kubernetes/combined.yaml index a7c2c5d89..14e0d8156 100644 --- a/deploy/deployment/kubernetes/combined.yaml +++ b/deploy/deployment/kubernetes/combined.yaml @@ -2901,7 +2901,6 @@ spec: type: object type: object postStartTimeout: - default: 0 description: |- PostStartTimeout defines the maximum duration the PostStart hook can run before it is automatically failed. This timeout is used for the postStart lifecycle hook diff --git a/deploy/deployment/kubernetes/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml b/deploy/deployment/kubernetes/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml index 0c165cb89..6979d7c94 100644 --- a/deploy/deployment/kubernetes/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml +++ b/deploy/deployment/kubernetes/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml @@ -2901,7 +2901,6 @@ spec: type: object type: object postStartTimeout: - default: 0 description: |- PostStartTimeout defines the maximum duration the PostStart hook can run before it is automatically failed. This timeout is used for the postStart lifecycle hook diff --git a/deploy/deployment/openshift/combined.yaml b/deploy/deployment/openshift/combined.yaml index 1152b92f6..3e197941b 100644 --- a/deploy/deployment/openshift/combined.yaml +++ b/deploy/deployment/openshift/combined.yaml @@ -2901,7 +2901,6 @@ spec: type: object type: object postStartTimeout: - default: 0 description: |- PostStartTimeout defines the maximum duration the PostStart hook can run before it is automatically failed. This timeout is used for the postStart lifecycle hook diff --git a/deploy/deployment/openshift/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml b/deploy/deployment/openshift/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml index 0c165cb89..6979d7c94 100644 --- a/deploy/deployment/openshift/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml +++ b/deploy/deployment/openshift/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml @@ -2901,7 +2901,6 @@ spec: type: object type: object postStartTimeout: - default: 0 description: |- PostStartTimeout defines the maximum duration the PostStart hook can run before it is automatically failed. This timeout is used for the postStart lifecycle hook From 281340812b5cd65e9e3d62b8e2b9b933dd73dee2 Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Wed, 16 Jul 2025 15:25:42 +0300 Subject: [PATCH 16/22] fixup! fix: fall back to the original behaviour if timeout is disabled Signed-off-by: Oleksii Kurinnyi --- pkg/library/lifecycle/poststart.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/library/lifecycle/poststart.go b/pkg/library/lifecycle/poststart.go index aafdacb56..d47b9e063 100644 --- a/pkg/library/lifecycle/poststart.go +++ b/pkg/library/lifecycle/poststart.go @@ -222,7 +222,7 @@ func processCommandsWithoutTimeoutFallback(commands []dw.Command) (*corev1.Lifec Command: []string{ "/bin/sh", "-c", - fmt.Sprintf(redirectOutputFmt, joinedCommands), + fmt.Sprintf(noTimeoutRedirectOutputFmt, joinedCommands), }, }, } From 443872cba6ef8be6d153bf23cb0a5cd8cea1975f Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Wed, 16 Jul 2025 15:50:03 +0300 Subject: [PATCH 17/22] fixup! fixup! fix: fall back to the original behaviour if timeout is disabled Signed-off-by: Oleksii Kurinnyi --- .../adds_all_postStart_commands.yaml | 20 +++++-------------- .../testdata/postStart/basic_postStart.yaml | 9 ++------- .../multiple_poststart_commands.yaml | 11 +++------- .../postStart/workingDir_postStart.yaml | 11 +++------- 4 files changed, 13 insertions(+), 38 deletions(-) diff --git a/pkg/library/lifecycle/testdata/postStart/adds_all_postStart_commands.yaml b/pkg/library/lifecycle/testdata/postStart/adds_all_postStart_commands.yaml index ef71dbbba..e2a9bf371 100644 --- a/pkg/library/lifecycle/testdata/postStart/adds_all_postStart_commands.yaml +++ b/pkg/library/lifecycle/testdata/postStart/adds_all_postStart_commands.yaml @@ -37,13 +37,8 @@ output: - -c - | { - # This script block ensures its exit code is preserved - # while its stdout and stderr are tee'd. - _script_to_run() { - echo 'hello world 1' # This will be replaced by scriptWithTimeout - } - _script_to_run - } 1> >(tee -a "/tmp/poststart-stdout.txt") 2> >(tee -a "/tmp/poststart-stderr.txt" >&2) + echo 'hello world 1' + } 1>/tmp/poststart-stdout.txt 2>/tmp/poststart-stderr.txt - name: test-component-2 image: test-img lifecycle: @@ -54,13 +49,8 @@ output: - -c - | { - # This script block ensures its exit code is preserved - # while its stdout and stderr are tee'd. - _script_to_run() { - cd /tmp/test-dir - echo 'hello world 2' # This will be replaced by scriptWithTimeout - } - _script_to_run - } 1> >(tee -a "/tmp/poststart-stdout.txt") 2> >(tee -a "/tmp/poststart-stderr.txt" >&2) + cd /tmp/test-dir + echo 'hello world 2' + } 1>/tmp/poststart-stdout.txt 2>/tmp/poststart-stderr.txt - name: test-component-3 image: test-img diff --git a/pkg/library/lifecycle/testdata/postStart/basic_postStart.yaml b/pkg/library/lifecycle/testdata/postStart/basic_postStart.yaml index a99fb7cce..acbf1ae36 100644 --- a/pkg/library/lifecycle/testdata/postStart/basic_postStart.yaml +++ b/pkg/library/lifecycle/testdata/postStart/basic_postStart.yaml @@ -26,10 +26,5 @@ output: - -c - | { - # This script block ensures its exit code is preserved - # while its stdout and stderr are tee'd. - _script_to_run() { - echo 'hello world' # This will be replaced by scriptWithTimeout - } - _script_to_run - } 1> >(tee -a "/tmp/poststart-stdout.txt") 2> >(tee -a "/tmp/poststart-stderr.txt" >&2) + echo 'hello world' + } 1>/tmp/poststart-stdout.txt 2>/tmp/poststart-stderr.txt diff --git a/pkg/library/lifecycle/testdata/postStart/multiple_poststart_commands.yaml b/pkg/library/lifecycle/testdata/postStart/multiple_poststart_commands.yaml index df005793c..db14a94bf 100644 --- a/pkg/library/lifecycle/testdata/postStart/multiple_poststart_commands.yaml +++ b/pkg/library/lifecycle/testdata/postStart/multiple_poststart_commands.yaml @@ -31,11 +31,6 @@ output: - -c - | { - # This script block ensures its exit code is preserved - # while its stdout and stderr are tee'd. - _script_to_run() { - echo 'hello world 1' - echo 'hello world 2' # This will be replaced by scriptWithTimeout - } - _script_to_run - } 1> >(tee -a "/tmp/poststart-stdout.txt") 2> >(tee -a "/tmp/poststart-stderr.txt" >&2) + echo 'hello world 1' + echo 'hello world 2' + } 1>/tmp/poststart-stdout.txt 2>/tmp/poststart-stderr.txt diff --git a/pkg/library/lifecycle/testdata/postStart/workingDir_postStart.yaml b/pkg/library/lifecycle/testdata/postStart/workingDir_postStart.yaml index 440424ae6..dfe976b4e 100644 --- a/pkg/library/lifecycle/testdata/postStart/workingDir_postStart.yaml +++ b/pkg/library/lifecycle/testdata/postStart/workingDir_postStart.yaml @@ -27,11 +27,6 @@ output: - -c - | { - # This script block ensures its exit code is preserved - # while its stdout and stderr are tee'd. - _script_to_run() { - cd /tmp/test-dir - echo 'hello world' # This will be replaced by scriptWithTimeout - } - _script_to_run - } 1> >(tee -a "/tmp/poststart-stdout.txt") 2> >(tee -a "/tmp/poststart-stderr.txt" >&2) + cd /tmp/test-dir + echo 'hello world' + } 1>/tmp/poststart-stdout.txt 2>/tmp/poststart-stderr.txt From 0dddc52ffc7691b4dbbd274196863ad1c87bd54f Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Mon, 28 Jul 2025 13:01:36 +0300 Subject: [PATCH 18/22] fixup! fixup! fixup! fix: fall back to the original behaviour if timeout is disabled Signed-off-by: Oleksii Kurinnyi --- pkg/library/lifecycle/poststart.go | 144 ++++++++++++++--------------- 1 file changed, 72 insertions(+), 72 deletions(-) diff --git a/pkg/library/lifecycle/poststart.go b/pkg/library/lifecycle/poststart.go index d47b9e063..ccf574b93 100644 --- a/pkg/library/lifecycle/poststart.go +++ b/pkg/library/lifecycle/poststart.go @@ -81,6 +81,78 @@ func AddPostStartLifecycleHooks(wksp *dw.DevWorkspaceTemplateSpec, containers [] return nil } +// processCommandsForPostStart processes a list of DevWorkspace commands +// and generates a corev1.LifecycleHandler for the PostStart lifecycle hook. +func processCommandsForPostStart(commands []dw.Command, postStartTimeout *int32) (*corev1.LifecycleHandler, error) { + if postStartTimeout == nil || *postStartTimeout == 0 { + // use the fallback if no timeout propagated + return processCommandsWithoutTimeoutFallback(commands) + } + + originalUserScript, err := buildUserScript(commands) + if err != nil { + return nil, fmt.Errorf("failed to build aggregated user script: %w", err) + } + + // The user script needs 'set -e' to ensure it exits on error. + // This script is then passed to `sh -c '...'`, so single quotes within it must be escaped. + scriptToExecute := "set -e\n" + originalUserScript + escapedUserScriptForTimeoutWrapper := strings.ReplaceAll(scriptToExecute, "'", `'\''`) + + fullScriptWithTimeout := generateScriptWithTimeout(escapedUserScriptForTimeoutWrapper, *postStartTimeout) + + finalScriptForHook := fmt.Sprintf(redirectOutputFmt, fullScriptWithTimeout) + + handler := &corev1.LifecycleHandler{ + Exec: &corev1.ExecAction{ + Command: []string{ + "/bin/sh", + "-c", + finalScriptForHook, + }, + }, + } + return handler, nil +} + +// processCommandsWithoutTimeoutFallback builds a lifecycle handler that runs the provided command(s) +// The command has the format +// +// exec: +// +// command: +// - "/bin/sh" +// - "-c" +// - | +// cd +// +func processCommandsWithoutTimeoutFallback(commands []dw.Command) (*corev1.LifecycleHandler, error) { + var dwCommands []string + for _, command := range commands { + execCmd := command.Exec + if len(execCmd.Env) > 0 { + return nil, fmt.Errorf("env vars in postStart command %s are unsupported", command.Id) + } + if execCmd.WorkingDir != "" { + dwCommands = append(dwCommands, fmt.Sprintf("cd %s", execCmd.WorkingDir)) + } + dwCommands = append(dwCommands, execCmd.CommandLine) + } + + joinedCommands := strings.Join(dwCommands, "\n") + + handler := &corev1.LifecycleHandler{ + Exec: &corev1.ExecAction{ + Command: []string{ + "/bin/sh", + "-c", + fmt.Sprintf(noTimeoutRedirectOutputFmt, joinedCommands), + }, + }, + } + return handler, nil +} + // buildUserScript takes a list of DevWorkspace commands and constructs a single // shell script string that executes them sequentially. func buildUserScript(commands []dw.Command) (string, error) { @@ -156,75 +228,3 @@ fi exit $exit_code `, timeoutSeconds, escapedUserScript) } - -// processCommandsForPostStart processes a list of DevWorkspace commands -// and generates a corev1.LifecycleHandler for the PostStart lifecycle hook. -func processCommandsForPostStart(commands []dw.Command, postStartTimeout *int32) (*corev1.LifecycleHandler, error) { - if postStartTimeout == nil || *postStartTimeout == 0 { - // use the fallback if no timeout propagated - return processCommandsWithoutTimeoutFallback(commands) - } - - originalUserScript, err := buildUserScript(commands) - if err != nil { - return nil, fmt.Errorf("failed to build aggregated user script: %w", err) - } - - // The user script needs 'set -e' to ensure it exits on error. - // This script is then passed to `sh -c '...'`, so single quotes within it must be escaped. - scriptToExecute := "set -e\n" + originalUserScript - escapedUserScriptForTimeoutWrapper := strings.ReplaceAll(scriptToExecute, "'", `'\''`) - - fullScriptWithTimeout := generateScriptWithTimeout(escapedUserScriptForTimeoutWrapper, *postStartTimeout) - - finalScriptForHook := fmt.Sprintf(redirectOutputFmt, fullScriptWithTimeout) - - handler := &corev1.LifecycleHandler{ - Exec: &corev1.ExecAction{ - Command: []string{ - "/bin/sh", - "-c", - finalScriptForHook, - }, - }, - } - return handler, nil -} - -// processCommandsForPostStart builds a lifecycle handler that runs the provided command(s) -// The command has the format -// -// exec: -// -// command: -// - "/bin/sh" -// - "-c" -// - | -// cd -// -func processCommandsWithoutTimeoutFallback(commands []dw.Command) (*corev1.LifecycleHandler, error) { - var dwCommands []string - for _, command := range commands { - execCmd := command.Exec - if len(execCmd.Env) > 0 { - return nil, fmt.Errorf("env vars in postStart command %s are unsupported", command.Id) - } - if execCmd.WorkingDir != "" { - dwCommands = append(dwCommands, fmt.Sprintf("cd %s", execCmd.WorkingDir)) - } - dwCommands = append(dwCommands, execCmd.CommandLine) - } - - joinedCommands := strings.Join(dwCommands, "\n") - - handler := &corev1.LifecycleHandler{ - Exec: &corev1.ExecAction{ - Command: []string{ - "/bin/sh", - "-c", - fmt.Sprintf(noTimeoutRedirectOutputFmt, joinedCommands), - }, - }, - } - return handler, nil -} From 14629eb55faa6daf73fb7565472b54eb68536bcb Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Thu, 31 Jul 2025 15:25:21 +0300 Subject: [PATCH 19/22] fix: support env var for postStart commands Signed-off-by: Oleksii Kurinnyi --- pkg/library/lifecycle/poststart.go | 11 +++++------ pkg/library/lifecycle/poststart_test.go | 14 +++++++------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/pkg/library/lifecycle/poststart.go b/pkg/library/lifecycle/poststart.go index ccf574b93..4b78332d8 100644 --- a/pkg/library/lifecycle/poststart.go +++ b/pkg/library/lifecycle/poststart.go @@ -163,14 +163,13 @@ func buildUserScript(commands []dw.Command) (string, error) { // Should be caught by earlier validation, but good to be safe return "", fmt.Errorf("exec command is nil for command ID %s", command.Id) } - if len(execCmd.Env) > 0 { - return "", fmt.Errorf("env vars in postStart command %s are unsupported", command.Id) - } var singleCommandParts []string + for _, envVar := range execCmd.Env { + singleCommandParts = append(singleCommandParts, fmt.Sprintf("export %s=%q", envVar.Name, envVar.Value)) + } + if execCmd.WorkingDir != "" { - // Safely quote the working directory path - safeWorkingDir := strings.ReplaceAll(execCmd.WorkingDir, "'", `'\''`) - singleCommandParts = append(singleCommandParts, fmt.Sprintf("cd '%s'", safeWorkingDir)) + singleCommandParts = append(singleCommandParts, fmt.Sprintf("cd %q", execCmd.WorkingDir)) } if execCmd.CommandLine != "" { singleCommandParts = append(singleCommandParts, execCmd.CommandLine) diff --git a/pkg/library/lifecycle/poststart_test.go b/pkg/library/lifecycle/poststart_test.go index 32b609a66..693365132 100644 --- a/pkg/library/lifecycle/poststart_test.go +++ b/pkg/library/lifecycle/poststart_test.go @@ -132,7 +132,7 @@ func TestBuildUserScript(t *testing.T) { }, }, }, - expectedScript: "cd '/projects/app' && ls -la", + expectedScript: "cd \"/projects/app\" && ls -la", expectedErr: "", }, { @@ -148,7 +148,7 @@ func TestBuildUserScript(t *testing.T) { }, }, }, - expectedScript: "cd '/data'", + expectedScript: "cd \"/data\"", expectedErr: "", }, { @@ -165,7 +165,7 @@ func TestBuildUserScript(t *testing.T) { }, }, }, - expectedScript: "cd '/projects/app'\\''s' && cat file.txt", + expectedScript: "cd \"/projects/app's\" && cat file.txt", expectedErr: "", }, { @@ -201,11 +201,11 @@ func TestBuildUserScript(t *testing.T) { }, }, }, - expectedScript: "cd '/projects/frontend' && npm install\nnpm start\ncd '/projects/backend' && mvn spring-boot:run", + expectedScript: "cd \"/projects/frontend\" && npm install\nnpm start\ncd \"/projects/backend\" && mvn spring-boot:run", expectedErr: "", }, { - name: "Command with unsupported Env vars", + name: "Command with Env vars", commands: []dw.Command{ { Id: "cmd-with-env", @@ -220,8 +220,8 @@ func TestBuildUserScript(t *testing.T) { }, }, }, - expectedScript: "", - expectedErr: "env vars in postStart command cmd-with-env are unsupported", + expectedScript: "export MY_VAR=\"test\" && echo $MY_VAR", + expectedErr: "", }, { name: "Command with nil Exec field", From affe6c51c75f52dbe76aa944e70ad1ea7c979464 Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Mon, 4 Aug 2025 12:03:28 +0300 Subject: [PATCH 20/22] fix: change type `*int32` to `string` Signed-off-by: Oleksii Kurinnyi --- .../devworkspaceoperatorconfig_types.go | 8 +-- pkg/config/sync.go | 6 +- pkg/library/container/container.go | 2 +- pkg/library/container/container_test.go | 2 +- pkg/library/lifecycle/poststart.go | 24 ++++++-- pkg/library/lifecycle/poststart_test.go | 61 ++++++++++++++++--- 6 files changed, 80 insertions(+), 23 deletions(-) diff --git a/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go b/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go index 102cec96e..81aa5987e 100644 --- a/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go +++ b/apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go @@ -192,12 +192,10 @@ type WorkspaceConfig struct { // PostStartTimeout defines the maximum duration the PostStart hook can run // before it is automatically failed. This timeout is used for the postStart lifecycle hook // that is used to run commands in the workspace container. The timeout is specified in seconds. - // If not specified, the timeout is disabled (0 seconds). - // +kubebuilder:validation:Minimum=0 + // Duration should be specified in a format parseable by Go's time package, e.g. "20s", "2m". + // If not specified or "0", the timeout is disabled. // +kubebuilder:validation:Optional - // +kubebuilder:validation:Type=integer - // +kubebuilder:validation:Format=int32 - PostStartTimeout *int32 `json:"postStartTimeout,omitempty"` + PostStartTimeout string `json:"postStartTimeout,omitempty"` } type WebhookConfig struct { diff --git a/pkg/config/sync.go b/pkg/config/sync.go index 4c1123692..3b774f129 100644 --- a/pkg/config/sync.go +++ b/pkg/config/sync.go @@ -432,7 +432,7 @@ func mergeConfig(from, to *controller.OperatorConfiguration) { } } - if from.Workspace.PostStartTimeout != nil { + if from.Workspace.PostStartTimeout != "" { to.Workspace.PostStartTimeout = from.Workspace.PostStartTimeout } } @@ -605,8 +605,8 @@ func GetCurrentConfigString(currConfig *controller.OperatorConfiguration) string if workspace.IdleTimeout != defaultConfig.Workspace.IdleTimeout { config = append(config, fmt.Sprintf("workspace.idleTimeout=%s", workspace.IdleTimeout)) } - if workspace.PostStartTimeout != nil && workspace.PostStartTimeout != defaultConfig.Workspace.PostStartTimeout { - config = append(config, fmt.Sprintf("workspace.postStartTimeout=%d", *workspace.PostStartTimeout)) + if workspace.PostStartTimeout != defaultConfig.Workspace.PostStartTimeout { + config = append(config, fmt.Sprintf("workspace.postStartTimeout=%s", workspace.PostStartTimeout)) } if workspace.ProgressTimeout != "" && workspace.ProgressTimeout != defaultConfig.Workspace.ProgressTimeout { config = append(config, fmt.Sprintf("workspace.progressTimeout=%s", workspace.ProgressTimeout)) diff --git a/pkg/library/container/container.go b/pkg/library/container/container.go index c4c3587ae..03132b90c 100644 --- a/pkg/library/container/container.go +++ b/pkg/library/container/container.go @@ -45,7 +45,7 @@ import ( // rewritten as Volumes are added to PodAdditions, in order to support e.g. using one PVC to hold all volumes // // Note: Requires DevWorkspace to be flattened (i.e. the DevWorkspace contains no Parent or Components of type Plugin) -func GetKubeContainersFromDevfile(workspace *dw.DevWorkspaceTemplateSpec, securityContext *corev1.SecurityContext, pullPolicy string, defaultResources *corev1.ResourceRequirements, postStartTimeout *int32) (*v1alpha1.PodAdditions, error) { +func GetKubeContainersFromDevfile(workspace *dw.DevWorkspaceTemplateSpec, securityContext *corev1.SecurityContext, pullPolicy string, defaultResources *corev1.ResourceRequirements, postStartTimeout string) (*v1alpha1.PodAdditions, error) { if !flatten.DevWorkspaceIsFlattened(workspace, nil) { return nil, fmt.Errorf("devfile is not flattened") } diff --git a/pkg/library/container/container_test.go b/pkg/library/container/container_test.go index e23b42ac1..36bf47738 100644 --- a/pkg/library/container/container_test.go +++ b/pkg/library/container/container_test.go @@ -87,7 +87,7 @@ func TestGetKubeContainersFromDevfile(t *testing.T) { t.Run(tt.Name, func(t *testing.T) { // sanity check that file is read correctly. assert.True(t, len(tt.Input.Components) > 0, "Input defines no components") - gotPodAdditions, err := GetKubeContainersFromDevfile(tt.Input, nil, testImagePullPolicy, defaultResources, nil) + gotPodAdditions, err := GetKubeContainersFromDevfile(tt.Input, nil, testImagePullPolicy, defaultResources, "") if tt.Output.ErrRegexp != nil && assert.Error(t, err) { assert.Regexp(t, *tt.Output.ErrRegexp, err.Error(), "Error message should match") } else { diff --git a/pkg/library/lifecycle/poststart.go b/pkg/library/lifecycle/poststart.go index 4b78332d8..f2a78f232 100644 --- a/pkg/library/lifecycle/poststart.go +++ b/pkg/library/lifecycle/poststart.go @@ -16,9 +16,11 @@ package lifecycle import ( "fmt" "strings" + "time" dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/log" ) const ( @@ -39,7 +41,7 @@ const ( ` ) -func AddPostStartLifecycleHooks(wksp *dw.DevWorkspaceTemplateSpec, containers []corev1.Container, postStartTimeout *int32) error { +func AddPostStartLifecycleHooks(wksp *dw.DevWorkspaceTemplateSpec, containers []corev1.Container, postStartTimeout string) error { if wksp.Events == nil || len(wksp.Events.PostStart) == 0 { return nil } @@ -83,8 +85,8 @@ func AddPostStartLifecycleHooks(wksp *dw.DevWorkspaceTemplateSpec, containers [] // processCommandsForPostStart processes a list of DevWorkspace commands // and generates a corev1.LifecycleHandler for the PostStart lifecycle hook. -func processCommandsForPostStart(commands []dw.Command, postStartTimeout *int32) (*corev1.LifecycleHandler, error) { - if postStartTimeout == nil || *postStartTimeout == 0 { +func processCommandsForPostStart(commands []dw.Command, postStartTimeout string) (*corev1.LifecycleHandler, error) { + if postStartTimeout == "" { // use the fallback if no timeout propagated return processCommandsWithoutTimeoutFallback(commands) } @@ -99,7 +101,7 @@ func processCommandsForPostStart(commands []dw.Command, postStartTimeout *int32) scriptToExecute := "set -e\n" + originalUserScript escapedUserScriptForTimeoutWrapper := strings.ReplaceAll(scriptToExecute, "'", `'\''`) - fullScriptWithTimeout := generateScriptWithTimeout(escapedUserScriptForTimeoutWrapper, *postStartTimeout) + fullScriptWithTimeout := generateScriptWithTimeout(escapedUserScriptForTimeoutWrapper, postStartTimeout) finalScriptForHook := fmt.Sprintf(redirectOutputFmt, fullScriptWithTimeout) @@ -185,7 +187,19 @@ func buildUserScript(commands []dw.Command) (string, error) { // environment variable exports, and specific exit code handling. // The killAfterDurationSeconds is hardcoded to 5s within this generated script. // It conditionally prefixes the user script with the timeout command if available. -func generateScriptWithTimeout(escapedUserScript string, timeoutSeconds int32) string { +func generateScriptWithTimeout(escapedUserScript string, postStartTimeout string) string { + // Convert `postStartTimeout` into the `timeout` format + var timeoutSeconds int64 + if postStartTimeout != "" && postStartTimeout != "0" { + duration, err := time.ParseDuration(postStartTimeout) + if err != nil { + log.Log.Error(err, "Could not parse post-start timeout, disabling timeout", "value", postStartTimeout) + timeoutSeconds = 0 + } else { + timeoutSeconds = int64(duration.Seconds()) + } + } + return fmt.Sprintf(` export POSTSTART_TIMEOUT_DURATION="%d" export POSTSTART_KILL_AFTER_DURATION="5" diff --git a/pkg/library/lifecycle/poststart_test.go b/pkg/library/lifecycle/poststart_test.go index 693365132..b487858ea 100644 --- a/pkg/library/lifecycle/poststart_test.go +++ b/pkg/library/lifecycle/poststart_test.go @@ -75,8 +75,8 @@ func TestAddPostStartLifecycleHooks(t *testing.T) { tests := loadAllPostStartTestCasesOrPanic(t, "./testdata/postStart") for _, tt := range tests { t.Run(fmt.Sprintf("%s (%s)", tt.Name, tt.testPath), func(t *testing.T) { - var timeout int32 - err := AddPostStartLifecycleHooks(tt.Input.Devfile, tt.Input.Containers, &timeout) + var timeout string + err := AddPostStartLifecycleHooks(tt.Input.Devfile, tt.Input.Containers, timeout) if tt.Output.ErrRegexp != nil && assert.Error(t, err) { assert.Regexp(t, *tt.Output.ErrRegexp, err.Error(), "Error message should match") } else { @@ -298,13 +298,13 @@ func TestGenerateScriptWithTimeout(t *testing.T) { tests := []struct { name string escapedUserScript string - timeoutSeconds int32 + timeout string expectedScript string }{ { name: "Basic script with timeout", escapedUserScript: "echo 'hello world'\nsleep 1", - timeoutSeconds: 10, + timeout: "10s", expectedScript: ` export POSTSTART_TIMEOUT_DURATION="10" export POSTSTART_KILL_AFTER_DURATION="5" @@ -350,7 +350,7 @@ exit $exit_code { name: "Script with zero timeout (no timeout)", escapedUserScript: "echo 'running indefinitely...'", - timeoutSeconds: 0, + timeout: "0s", expectedScript: ` export POSTSTART_TIMEOUT_DURATION="0" export POSTSTART_KILL_AFTER_DURATION="5" @@ -395,7 +395,7 @@ exit $exit_code { name: "Empty user script", escapedUserScript: "", - timeoutSeconds: 5, + timeout: "5s", expectedScript: ` export POSTSTART_TIMEOUT_DURATION="5" export POSTSTART_KILL_AFTER_DURATION="5" @@ -440,7 +440,7 @@ exit $exit_code { name: "User script with already escaped single quotes", escapedUserScript: "echo 'it'\\''s complex'", - timeoutSeconds: 30, + timeout: "30s", expectedScript: ` export POSTSTART_TIMEOUT_DURATION="30" export POSTSTART_KILL_AFTER_DURATION="5" @@ -479,6 +479,51 @@ else fi fi +exit $exit_code +`, + }, + { + name: "User script with minute timeout", + escapedUserScript: "echo 'wait for it...'", + timeout: "2m", + expectedScript: ` +export POSTSTART_TIMEOUT_DURATION="120" +export POSTSTART_KILL_AFTER_DURATION="5" + +_TIMEOUT_COMMAND_PART="" +_WAS_TIMEOUT_USED="false" # Use strings "true" or "false" for shell boolean + +if command -v timeout >/dev/null 2>&1; then + echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds, kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds" >&2 + _TIMEOUT_COMMAND_PART="timeout --preserve-status --kill-after=${POSTSTART_KILL_AFTER_DURATION} ${POSTSTART_TIMEOUT_DURATION}" + _WAS_TIMEOUT_USED="true" +else + echo "[postStart hook] WARNING: 'timeout' utility not found. Executing commands without timeout." >&2 +fi + +# Execute the user's script +${_TIMEOUT_COMMAND_PART} /bin/sh -c 'echo 'wait for it...'' +exit_code=$? + +# Check the exit code based on whether timeout was attempted +if [ "$_WAS_TIMEOUT_USED" = "true" ]; then + if [ $exit_code -eq 143 ]; then # 128 + 15 (SIGTERM) + echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2 + elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL) + echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2 + elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code + echo "[postStart hook] Commands failed with exit code $exit_code." >&2 + else + echo "[postStart hook] Commands completed successfully within the time limit." >&2 + fi +else + if [ $exit_code -ne 0 ]; then + echo "[postStart hook] Commands failed with exit code $exit_code (no timeout)." >&2 + else + echo "[postStart hook] Commands completed successfully (no timeout)." >&2 + fi +fi + exit $exit_code `, }, @@ -486,7 +531,7 @@ exit $exit_code for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - script := generateScriptWithTimeout(tt.escapedUserScript, tt.timeoutSeconds) + script := generateScriptWithTimeout(tt.escapedUserScript, tt.timeout) assert.Equal(t, tt.expectedScript, script) }) } From 2d2ad37a43d0417dd180592c76668f71071045a4 Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Mon, 4 Aug 2025 09:12:54 +0000 Subject: [PATCH 21/22] fixup! fix: change type `*int32` to `string` Signed-off-by: Oleksii Kurinnyi --- apis/controller/v1alpha1/zz_generated.deepcopy.go | 7 +------ .../controller.devfile.io_devworkspaceoperatorconfigs.yaml | 7 +++---- deploy/deployment/kubernetes/combined.yaml | 7 +++---- ...igs.controller.devfile.io.CustomResourceDefinition.yaml | 7 +++---- deploy/deployment/openshift/combined.yaml | 7 +++---- ...igs.controller.devfile.io.CustomResourceDefinition.yaml | 7 +++---- .../controller.devfile.io_devworkspaceoperatorconfigs.yaml | 7 +++---- 7 files changed, 19 insertions(+), 30 deletions(-) diff --git a/apis/controller/v1alpha1/zz_generated.deepcopy.go b/apis/controller/v1alpha1/zz_generated.deepcopy.go index 792ee1633..1b7c05d01 100644 --- a/apis/controller/v1alpha1/zz_generated.deepcopy.go +++ b/apis/controller/v1alpha1/zz_generated.deepcopy.go @@ -21,7 +21,7 @@ package v1alpha1 import ( "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" - v1 "k8s.io/api/core/v1" + "k8s.io/api/core/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -788,11 +788,6 @@ func (in *WorkspaceConfig) DeepCopyInto(out *WorkspaceConfig) { *out = new(CleanupCronJobConfig) (*in).DeepCopyInto(*out) } - if in.PostStartTimeout != nil { - in, out := &in.PostStartTimeout, &out.PostStartTimeout - *out = new(int32) - **out = **in - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkspaceConfig. diff --git a/deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml b/deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml index d879a1a2f..073cd8f7a 100644 --- a/deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml +++ b/deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml @@ -2769,10 +2769,9 @@ spec: PostStartTimeout defines the maximum duration the PostStart hook can run before it is automatically failed. This timeout is used for the postStart lifecycle hook that is used to run commands in the workspace container. The timeout is specified in seconds. - If not specified, the timeout is disabled (0 seconds). - format: int32 - minimum: 0 - type: integer + Duration should be specified in a format parseable by Go's time package, e.g. "20s", "2m". + If not specified or "0", the timeout is disabled. + type: string progressTimeout: description: |- ProgressTimeout determines the maximum duration a DevWorkspace can be in diff --git a/deploy/deployment/kubernetes/combined.yaml b/deploy/deployment/kubernetes/combined.yaml index 14e0d8156..98c860a86 100644 --- a/deploy/deployment/kubernetes/combined.yaml +++ b/deploy/deployment/kubernetes/combined.yaml @@ -2905,10 +2905,9 @@ spec: PostStartTimeout defines the maximum duration the PostStart hook can run before it is automatically failed. This timeout is used for the postStart lifecycle hook that is used to run commands in the workspace container. The timeout is specified in seconds. - If not specified, the timeout is disabled (0 seconds). - format: int32 - minimum: 0 - type: integer + Duration should be specified in a format parseable by Go's time package, e.g. "20s", "2m". + If not specified or "0", the timeout is disabled. + type: string progressTimeout: description: |- ProgressTimeout determines the maximum duration a DevWorkspace can be in diff --git a/deploy/deployment/kubernetes/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml b/deploy/deployment/kubernetes/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml index 6979d7c94..2718a363e 100644 --- a/deploy/deployment/kubernetes/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml +++ b/deploy/deployment/kubernetes/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml @@ -2905,10 +2905,9 @@ spec: PostStartTimeout defines the maximum duration the PostStart hook can run before it is automatically failed. This timeout is used for the postStart lifecycle hook that is used to run commands in the workspace container. The timeout is specified in seconds. - If not specified, the timeout is disabled (0 seconds). - format: int32 - minimum: 0 - type: integer + Duration should be specified in a format parseable by Go's time package, e.g. "20s", "2m". + If not specified or "0", the timeout is disabled. + type: string progressTimeout: description: |- ProgressTimeout determines the maximum duration a DevWorkspace can be in diff --git a/deploy/deployment/openshift/combined.yaml b/deploy/deployment/openshift/combined.yaml index 3e197941b..dfae8130f 100644 --- a/deploy/deployment/openshift/combined.yaml +++ b/deploy/deployment/openshift/combined.yaml @@ -2905,10 +2905,9 @@ spec: PostStartTimeout defines the maximum duration the PostStart hook can run before it is automatically failed. This timeout is used for the postStart lifecycle hook that is used to run commands in the workspace container. The timeout is specified in seconds. - If not specified, the timeout is disabled (0 seconds). - format: int32 - minimum: 0 - type: integer + Duration should be specified in a format parseable by Go's time package, e.g. "20s", "2m". + If not specified or "0", the timeout is disabled. + type: string progressTimeout: description: |- ProgressTimeout determines the maximum duration a DevWorkspace can be in diff --git a/deploy/deployment/openshift/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml b/deploy/deployment/openshift/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml index 6979d7c94..2718a363e 100644 --- a/deploy/deployment/openshift/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml +++ b/deploy/deployment/openshift/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml @@ -2905,10 +2905,9 @@ spec: PostStartTimeout defines the maximum duration the PostStart hook can run before it is automatically failed. This timeout is used for the postStart lifecycle hook that is used to run commands in the workspace container. The timeout is specified in seconds. - If not specified, the timeout is disabled (0 seconds). - format: int32 - minimum: 0 - type: integer + Duration should be specified in a format parseable by Go's time package, e.g. "20s", "2m". + If not specified or "0", the timeout is disabled. + type: string progressTimeout: description: |- ProgressTimeout determines the maximum duration a DevWorkspace can be in diff --git a/deploy/templates/crd/bases/controller.devfile.io_devworkspaceoperatorconfigs.yaml b/deploy/templates/crd/bases/controller.devfile.io_devworkspaceoperatorconfigs.yaml index 9400673f0..6927c9195 100644 --- a/deploy/templates/crd/bases/controller.devfile.io_devworkspaceoperatorconfigs.yaml +++ b/deploy/templates/crd/bases/controller.devfile.io_devworkspaceoperatorconfigs.yaml @@ -2903,10 +2903,9 @@ spec: PostStartTimeout defines the maximum duration the PostStart hook can run before it is automatically failed. This timeout is used for the postStart lifecycle hook that is used to run commands in the workspace container. The timeout is specified in seconds. - If not specified, the timeout is disabled (0 seconds). - format: int32 - minimum: 0 - type: integer + Duration should be specified in a format parseable by Go's time package, e.g. "20s", "2m". + If not specified or "0", the timeout is disabled. + type: string progressTimeout: description: |- ProgressTimeout determines the maximum duration a DevWorkspace can be in From 7272e1d69d56b7400dea441b67b195e7e98920fb Mon Sep 17 00:00:00 2001 From: Oleksii Kurinnyi Date: Mon, 4 Aug 2025 09:21:14 +0000 Subject: [PATCH 22/22] fixup! fixup! fix: change type `*int32` to `string` Signed-off-by: Oleksii Kurinnyi --- apis/controller/v1alpha1/zz_generated.deepcopy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apis/controller/v1alpha1/zz_generated.deepcopy.go b/apis/controller/v1alpha1/zz_generated.deepcopy.go index 1b7c05d01..b04904319 100644 --- a/apis/controller/v1alpha1/zz_generated.deepcopy.go +++ b/apis/controller/v1alpha1/zz_generated.deepcopy.go @@ -21,7 +21,7 @@ package v1alpha1 import ( "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" runtime "k8s.io/apimachinery/pkg/runtime" )