Skip to content

Commit d4754c4

Browse files
committed
[ws-manager] Sketch of re-creating workspace pods
1 parent abb191f commit d4754c4

File tree

5 files changed

+68
-6
lines changed

5 files changed

+68
-6
lines changed

components/ws-manager-api/go/config/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,11 @@ type Configuration struct {
142142

143143
// SSHGatewayCAPublicKey is a CA public key
144144
SSHGatewayCAPublicKey string
145+
146+
// PodRecreationMaxRetries
147+
PodRecreationMaxRetries int `json:"podRecreationMaxRetries,omitempty"`
148+
// PodRecreationBackoff
149+
PodRecreationBackoff util.Duration `json:"podRecreationBackoff,omitempty"`
145150
}
146151

147152
type WorkspaceClass struct {

components/ws-manager-api/go/crd/v1/workspace_types.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,10 @@ func (ps PortSpec) Equal(other PortSpec) bool {
170170

171171
// WorkspaceStatus defines the observed state of Workspace
172172
type WorkspaceStatus struct {
173-
PodStarts int `json:"podStarts"`
174-
URL string `json:"url,omitempty" scrub:"redact"`
175-
OwnerToken string `json:"ownerToken,omitempty" scrub:"redact"`
173+
PodStarts int `json:"podStarts"`
174+
PodRecreated int `json:"podRecreated,omitempty"`
175+
URL string `json:"url,omitempty" scrub:"redact"`
176+
OwnerToken string `json:"ownerToken,omitempty" scrub:"redact"`
176177

177178
// +kubebuilder:default=Unknown
178179
Phase WorkspacePhase `json:"phase,omitempty"`
@@ -263,6 +264,9 @@ const (
263264
// WorkspaceContainerRunning is true if the workspace container is running.
264265
// Used to determine if a backup can be taken, only once the container is stopped.
265266
WorkspaceConditionContainerRunning WorkspaceCondition = "WorkspaceContainerRunning"
267+
268+
// WorkspaceConditionPodRejected is true if we detected that the pod was rejected by the node
269+
WorkspaceConditionPodRejected WorkspaceCondition = "PodRejected"
266270
)
267271

268272
func NewWorkspaceConditionDeployed() metav1.Condition {
@@ -291,6 +295,15 @@ func NewWorkspaceConditionFailed(message string) metav1.Condition {
291295
}
292296
}
293297

298+
func NewWorkspaceConditionPodRejected(message string, status metav1.ConditionStatus) metav1.Condition {
299+
return metav1.Condition{
300+
Type: string(WorkspaceConditionPodRejected),
301+
LastTransitionTime: metav1.Now(),
302+
Status: status,
303+
Message: message,
304+
}
305+
}
306+
294307
func NewWorkspaceConditionTimeout(message string) metav1.Condition {
295308
return metav1.Condition{
296309
Type: string(WorkspaceConditionTimeout),

components/ws-manager-mk2/controllers/status.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,16 @@ func (r *WorkspaceReconciler) updateWorkspaceStatus(ctx context.Context, workspa
123123
workspace.Status.Phase = *phase
124124
}
125125

126+
if failure != "" && !workspace.IsConditionTrue(workspacev1.WorkspaceConditionFailed) {
127+
// Check: A situation where we want to retry?
128+
if pod.Status.Phase == corev1.PodFailed && (pod.Status.Reason == "NodeAffinity" || pod.Status.Reason == "OutOfCPU") && strings.HasPrefix(pod.Status.Message, "Pod was rejected") {
129+
// This is a situation where we want to re-create the pod!
130+
log.Info("workspace scheduling failed", "workspace", workspace.Name, "reason", failure)
131+
workspace.Status.SetCondition(workspacev1.NewWorkspaceConditionPodRejected(failure, metav1.ConditionTrue))
132+
r.Recorder.Event(workspace, corev1.EventTypeWarning, "PodRejected", failure)
133+
}
134+
}
135+
126136
if failure != "" && !workspace.IsConditionTrue(workspacev1.WorkspaceConditionFailed) {
127137
// workspaces can fail only once - once there is a failed condition set, stick with it
128138
log.Info("workspace failed", "workspace", workspace.Name, "reason", failure)

components/ws-manager-mk2/controllers/subscriber_controller.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ func (r *SubscriberReconciler) Reconcile(ctx context.Context, req ctrl.Request)
6161
workspace.Status.Conditions = []metav1.Condition{}
6262
}
6363

64+
if workspace.IsConditionTrue(workspacev1.WorkspaceConditionPodRejected) {
65+
// In this situation, we are about to re-create the pod. We don't want clients to see all the "stopping, stopped, starting" chatter, so we hide it here.
66+
// TODO(gpl) Is this a sane approach?
67+
return ctrl.Result{}, nil
68+
}
69+
6470
if r.OnReconcile != nil {
6571
r.OnReconcile(ctx, &workspace)
6672
}

components/ws-manager-mk2/controllers/workspace_controller.go

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,6 @@ func (r *WorkspaceReconciler) actOnStatus(ctx context.Context, workspace *worksp
204204
log.Error(err, "unable to create Pod for Workspace", "pod", pod)
205205
return ctrl.Result{Requeue: true}, err
206206
} else {
207-
// TODO(cw): replicate the startup mechanism where pods can fail to be scheduled,
208-
// need to be deleted and re-created
209207
// Must increment and persist the pod starts, and ensure we retry on conflict.
210208
// If we fail to persist this value, it's possible that the Pod gets recreated
211209
// when the workspace stops, due to PodStarts still being 0 when the original Pod
@@ -221,6 +219,34 @@ func (r *WorkspaceReconciler) actOnStatus(ctx context.Context, workspace *worksp
221219
r.Recorder.Event(workspace, corev1.EventTypeNormal, "Creating", "")
222220
}
223221

222+
case workspace.Status.Phase == workspacev1.WorkspacePhaseStopped && workspace.IsConditionTrue(workspacev1.WorkspaceConditionPodRejected):
223+
if workspace.Status.PodRecreated > r.Config.PodRecreationMaxRetries {
224+
workspace.Status.SetCondition(workspacev1.NewWorkspaceConditionPodRejected(fmt.Sprintf("Pod reached maximum recreations %d, failing", workspace.Status.PodRecreated), metav1.ConditionFalse))
225+
return ctrl.Result{Requeue: true}, nil // requeue so we end up in the "Stopped" case below
226+
}
227+
228+
// Must persist the modification pod starts, and ensure we retry on conflict.
229+
// If we fail to persist this value, it's possible that the Pod gets recreated endlessly
230+
// when the workspace stops, due to PodStarts still being 0 when the original Pod
231+
// disappears.
232+
// Use a Patch instead of an Update, to prevent conflicts.
233+
patch := client.MergeFrom(workspace.DeepCopy())
234+
workspace.Status.PodStarts = 0
235+
workspace.Status.PodRecreated++
236+
workspace.Status.SetCondition(workspacev1.NewWorkspaceConditionPodRejected(fmt.Sprintf("Recreating pod... (%d retry)", workspace.Status.PodRecreated), metav1.ConditionFalse))
237+
if err := r.Status().Patch(ctx, workspace, patch); err != nil {
238+
log.Error(err, "Failed to patch PodStarts=0,PodRecreated++ in workspace status")
239+
return ctrl.Result{}, err
240+
}
241+
242+
requeueAfter := 5 * time.Second
243+
if r.Config.PodRecreationBackoff != 0 {
244+
requeueAfter = time.Duration(r.Config.PodRecreationBackoff)
245+
}
246+
247+
r.Recorder.Event(workspace, corev1.EventTypeNormal, "Recreating", "")
248+
return ctrl.Result{Requeue: true, RequeueAfter: requeueAfter}, nil
249+
224250
case workspace.Status.Phase == workspacev1.WorkspacePhaseStopped:
225251
if err := r.deleteWorkspaceSecrets(ctx, workspace); err != nil {
226252
return ctrl.Result{}, err
@@ -403,7 +429,9 @@ func isStartFailure(ws *workspacev1.Workspace) bool {
403429
isAborted := ws.IsConditionTrue(workspacev1.WorkspaceConditionAborted)
404430
// Also ignore workspaces that are requested to be stopped before they became ready.
405431
isStoppedByRequest := ws.IsConditionTrue(workspacev1.WorkspaceConditionStoppedByRequest)
406-
return !everReady && !isAborted && !isStoppedByRequest
432+
// Also ignore pods that got rejected by the node
433+
isPodRejected := ws.IsConditionTrue(workspacev1.WorkspaceConditionPodRejected)
434+
return !everReady && !isAborted && !isStoppedByRequest && !isPodRejected
407435
}
408436

409437
func (r *WorkspaceReconciler) emitPhaseEvents(ctx context.Context, ws *workspacev1.Workspace, old *workspacev1.WorkspaceStatus) {

0 commit comments

Comments
 (0)