@@ -389,6 +389,119 @@ var _ = Describe("WorkspaceController", func() {
389389 })
390390 })
391391
392+ It ("pod rejection should result in a retry" , func () {
393+ ws := newWorkspace (uuid .NewString (), "default" )
394+
395+ // ### prepare block start
396+ By ("creating workspace" )
397+ // Simulate pod getting scheduled to a node.
398+ var node corev1.Node
399+ node .Name = uuid .NewString ()
400+ Expect (k8sClient .Create (ctx , & node )).To (Succeed ())
401+ // Manually create the workspace pod with the node name.
402+ // We can't update the pod with the node name, as this operation
403+ // is only allowed for the scheduler. So as a hack, we manually
404+ // create the workspace's pod.
405+ pod := & corev1.Pod {
406+ ObjectMeta : metav1.ObjectMeta {
407+ Name : fmt .Sprintf ("ws-%s" , ws .Name ),
408+ Namespace : ws .Namespace ,
409+ Finalizers : []string {workspacev1 .GitpodFinalizerName },
410+ Labels : map [string ]string {
411+ wsk8s .WorkspaceManagedByLabel : constants .ManagedBy ,
412+ },
413+ },
414+ Spec : corev1.PodSpec {
415+ NodeName : node .Name ,
416+ Containers : []corev1.Container {{
417+ Name : "workspace" ,
418+ Image : "someimage" ,
419+ }},
420+ },
421+ }
422+
423+ Expect (k8sClient .Create (ctx , pod )).To (Succeed ())
424+ pod = createWorkspaceExpectPod (ws )
425+ updateObjWithRetries (k8sClient , pod , false , func (pod * corev1.Pod ) {
426+ Expect (ctrl .SetControllerReference (ws , pod , k8sClient .Scheme ())).To (Succeed ())
427+ })
428+ // Wait until controller has reconciled at least once (by waiting for the runtime status to get updated).
429+ // This is necessary for the metrics to get recorded correctly. If we don't wait, the first reconciliation
430+ // might be once the Pod is already in a running state, and hence the metric state might not record e.g. content
431+ // restore.
432+ // This is only necessary because we manually created the pod, normally the Pod creation is the controller's
433+ // first reconciliation which ensures the metrics are recorded from the workspace's initial state.
434+
435+ Eventually (func (g Gomega ) {
436+ g .Expect (k8sClient .Get (ctx , types.NamespacedName {Name : ws .Name , Namespace : ws .Namespace }, ws )).To (Succeed ())
437+ g .Expect (ws .Status .Runtime ).ToNot (BeNil ())
438+ g .Expect (ws .Status .Runtime .PodName ).To (Equal (pod .Name ))
439+ }, timeout , interval ).Should (Succeed ())
440+
441+ // Await "deployed" condition, and check we are good
442+ expectConditionEventually (ws , string (workspacev1 .WorkspaceConditionDeployed ), metav1 .ConditionTrue , "" )
443+ Eventually (func (g Gomega ) {
444+ g .Expect (k8sClient .Get (ctx , types.NamespacedName {Name : ws .Name , Namespace : ws .Namespace }, ws )).To (Succeed ())
445+ g .Expect (ws .Status .PodStarts ).To (Equal (1 ))
446+ g .Expect (ws .Status .PodRecreated ).To (Equal (0 ))
447+ }, timeout , interval ).Should (Succeed ())
448+
449+ // ### prepare block end
450+
451+ // ### trigger block start
452+ // Make pod be rejected 🪄
453+ By ("rejecting pod" )
454+ rejectPod (pod )
455+
456+ expectConditionEventually (ws , string (workspacev1 .WorkspaceConditionPodRejected ), metav1 .ConditionFalse , "" )
457+
458+ By ("await pod deleted" )
459+ Eventually (func (g Gomega ) {
460+ g .Expect (k8sClient .Get (ctx , types.NamespacedName {Name : pod .Name , Namespace : pod .Namespace }, pod )).To (MatchError (ContainSubstring ("not found" )))
461+ }, timeout , interval ).Should (Succeed ())
462+
463+ By ("await pod recreation" )
464+ Eventually (func (g Gomega ) {
465+ g .Expect (k8sClient .Get (ctx , types.NamespacedName {Name : ws .Name , Namespace : ws .Namespace }, ws )).To (Succeed ())
466+ g .Expect (ws .Status .PodRecreated ).To (Equal (1 ))
467+ g .Expect (ws .Status .Phase ).To (Equal (workspacev1 .WorkspacePhasePending ))
468+ }, timeout , interval ).Should (Succeed ())
469+ // ### trigger block end
470+
471+ // ### retry block start
472+ // Transition Pod to pending, and expect workspace to reach Creating phase.
473+ // This should also cause create time metrics to be recorded.
474+ updateObjWithRetries (k8sClient , pod , true , func (pod * corev1.Pod ) {
475+ pod .Status .Phase = corev1 .PodPending
476+ pod .Status .ContainerStatuses = []corev1.ContainerStatus {{
477+ State : corev1.ContainerState {
478+ Waiting : & corev1.ContainerStateWaiting {
479+ Reason : "ContainerCreating" ,
480+ },
481+ },
482+ Name : "workspace" ,
483+ }}
484+ })
485+
486+ expectPhaseEventually (ws , workspacev1 .WorkspacePhaseCreating )
487+
488+ // Transition Pod to running, and expect workspace to reach Running phase.
489+ // This should also cause e.g. startup time metrics to be recorded.
490+ updateObjWithRetries (k8sClient , pod , true , func (pod * corev1.Pod ) {
491+ pod .Status .Phase = corev1 .PodRunning
492+ pod .Status .ContainerStatuses = []corev1.ContainerStatus {{
493+ Name : "workspace" ,
494+ Ready : true ,
495+ }}
496+ })
497+
498+ updateObjWithRetries (k8sClient , ws , true , func (ws * workspacev1.Workspace ) {
499+ ws .Status .SetCondition (workspacev1 .NewWorkspaceConditionContentReady (metav1 .ConditionTrue , workspacev1 .ReasonInitializationSuccess , "" ))
500+ })
501+
502+ expectPhaseEventually (ws , workspacev1 .WorkspacePhaseRunning )
503+ // ### retry block end
504+ })
392505 })
393506
394507 Context ("with headless workspaces" , func () {
@@ -634,6 +747,16 @@ func requestStop(ws *workspacev1.Workspace) {
634747 })
635748}
636749
750+ func rejectPod (pod * corev1.Pod ) {
751+ GinkgoHelper ()
752+ By ("adding pod rejected condition" )
753+ updateObjWithRetries (k8sClient , pod , true , func (pod * corev1.Pod ) {
754+ pod .Status .Phase = corev1 .PodFailed
755+ pod .Status .Reason = "NodeAffinity"
756+ pod .Status .Message = "Pod was rejected"
757+ })
758+ }
759+
637760func markReady (ws * workspacev1.Workspace ) {
638761 GinkgoHelper ()
639762 By ("adding content ready condition" )
0 commit comments