@@ -76,23 +76,18 @@ func AddPostStartLifecycleHooks(wksp *dw.DevWorkspaceTemplateSpec, containers []
7676 return nil
7777}
7878
79- // processCommandsForPostStart builds a lifecycle handler that runs the provided command(s)
80- // The command has the format
81- //
82- // exec:
83- //
84- // command:
85- // - "/bin/sh"
86- // - "-c"
87- // - |
88- // cd <workingDir>
89- // <commandline>
90- func processCommandsForPostStart (commands []dw.Command , postStartTimeout * int32 ) (* corev1.LifecycleHandler , error ) {
79+ // buildUserScript takes a list of DevWorkspace commands and constructs a single
80+ // shell script string that executes them sequentially.
81+ func buildUserScript (commands []dw.Command ) (string , error ) {
9182 var commandScriptLines []string
9283 for _ , command := range commands {
9384 execCmd := command .Exec
85+ if execCmd == nil {
86+ // Should be caught by earlier validation, but good to be safe
87+ return "" , fmt .Errorf ("exec command is nil for command ID %s" , command .Id )
88+ }
9489 if len (execCmd .Env ) > 0 {
95- return nil , fmt .Errorf ("env vars in postStart command %s are unsupported" , command .Id )
90+ return "" , fmt .Errorf ("env vars in postStart command %s are unsupported" , command .Id )
9691 }
9792 var singleCommandParts []string
9893 if execCmd .WorkingDir != "" {
@@ -107,17 +102,18 @@ func processCommandsForPostStart(commands []dw.Command, postStartTimeout *int32)
107102 commandScriptLines = append (commandScriptLines , strings .Join (singleCommandParts , " && " ))
108103 }
109104 }
105+ return strings .Join (commandScriptLines , "\n " ), nil
106+ }
110107
111- originalUserScript := strings .Join (commandScriptLines , "\n " )
112-
113- scriptToExecute := "set -e\n " + originalUserScript
114- escapedUserScript := strings .ReplaceAll (scriptToExecute , "'" , `'\''` )
115-
116- scriptWithTimeout := fmt .Sprintf (`
108+ // generateScriptWithTimeout wraps a given user script with timeout logic,
109+ // environment variable exports, and specific exit code handling.
110+ // The killAfterDurationSeconds is hardcoded to 5s within this generated script.
111+ func generateScriptWithTimeout (escapedUserScript string , timeoutSeconds int32 ) string {
112+ return fmt .Sprintf (`
117113export POSTSTART_TIMEOUT_DURATION="%d"
118114export POSTSTART_KILL_AFTER_DURATION="5"
119115
120- echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} s , kill after: ${POSTSTART_KILL_AFTER_DURATION} s " >&2
116+ echo "[postStart hook] Executing commands with timeout: ${POSTSTART_TIMEOUT_DURATION} seconds , kill after: ${POSTSTART_KILL_AFTER_DURATION} seconds " >&2
121117
122118# Run the user's script under the 'timeout' utility.
123119timeout --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)
128124 echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2
129125elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL)
130126 echo "[postStart hook] Commands forcefully killed by SIGKILL (likely after --kill-after ${POSTSTART_KILL_AFTER_DURATION}s expired). Exit code 137." >&2
131- elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code, including 124
127+ elif [ $exit_code -ne 0 ]; then # Catches any other non-zero exit code
132128 echo "[postStart hook] Commands failed with exit code $exit_code." >&2
133129else
134130 echo "[postStart hook] Commands completed successfully within the time limit." >&2
135131fi
136132
137133exit $exit_code
138- ` , * postStartTimeout , escapedUserScript )
134+ ` , timeoutSeconds , escapedUserScript )
135+ }
136+
137+ // processCommandsForPostStart processes a list of DevWorkspace commands
138+ // and generates a corev1.LifecycleHandler for the PostStart lifecycle hook.
139+ func processCommandsForPostStart (commands []dw.Command , postStartTimeout * int32 ) (* corev1.LifecycleHandler , error ) {
140+ if postStartTimeout == nil {
141+ // The 'timeout' command treats 0 as "no timeout", so it is disabled by default.
142+ defaultTimeout := int32 (0 )
143+ postStartTimeout = & defaultTimeout
144+ }
145+
146+ originalUserScript , err := buildUserScript (commands )
147+ if err != nil {
148+ return nil , fmt .Errorf ("failed to build aggregated user script: %w" , err )
149+ }
150+
151+ // The user script needs 'set -e' to ensure it exits on error.
152+ // This script is then passed to `sh -c '...'`, so single quotes within it must be escaped.
153+ scriptToExecute := "set -e\n " + originalUserScript
154+ escapedUserScriptForTimeoutWrapper := strings .ReplaceAll (scriptToExecute , "'" , `'\''` )
155+
156+ fullScriptWithTimeout := generateScriptWithTimeout (escapedUserScriptForTimeoutWrapper , * postStartTimeout )
139157
140- finalScriptForHook := fmt .Sprintf (redirectOutputFmt , scriptWithTimeout )
158+ finalScriptForHook := fmt .Sprintf (redirectOutputFmt , fullScriptWithTimeout )
141159
142160 handler := & corev1.LifecycleHandler {
143161 Exec : & corev1.ExecAction {
0 commit comments