@@ -76,23 +76,18 @@ func AddPostStartLifecycleHooks(wksp *dw.DevWorkspaceTemplateSpec, containers []
76
76
return nil
77
77
}
78
78
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 ) {
91
82
var commandScriptLines []string
92
83
for _ , command := range commands {
93
84
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
+ }
94
89
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 )
96
91
}
97
92
var singleCommandParts []string
98
93
if execCmd .WorkingDir != "" {
@@ -107,17 +102,18 @@ func processCommandsForPostStart(commands []dw.Command, postStartTimeout *int32)
107
102
commandScriptLines = append (commandScriptLines , strings .Join (singleCommandParts , " && " ))
108
103
}
109
104
}
105
+ return strings .Join (commandScriptLines , "\n " ), nil
106
+ }
110
107
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 (`
117
113
export POSTSTART_TIMEOUT_DURATION="%d"
118
114
export POSTSTART_KILL_AFTER_DURATION="5"
119
115
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
121
117
122
118
# Run the user's script under the 'timeout' utility.
123
119
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)
128
124
echo "[postStart hook] Commands terminated by SIGTERM (likely timed out after ${POSTSTART_TIMEOUT_DURATION}s). Exit code 143." >&2
129
125
elif [ $exit_code -eq 137 ]; then # 128 + 9 (SIGKILL)
130
126
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
132
128
echo "[postStart hook] Commands failed with exit code $exit_code." >&2
133
129
else
134
130
echo "[postStart hook] Commands completed successfully within the time limit." >&2
135
131
fi
136
132
137
133
exit $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 )
139
157
140
- finalScriptForHook := fmt .Sprintf (redirectOutputFmt , scriptWithTimeout )
158
+ finalScriptForHook := fmt .Sprintf (redirectOutputFmt , fullScriptWithTimeout )
141
159
142
160
handler := & corev1.LifecycleHandler {
143
161
Exec : & corev1.ExecAction {
0 commit comments