@@ -24,6 +24,7 @@ import (
24
24
wsk8s "github.com/gitpod-io/gitpod/common-go/kubernetes"
25
25
"github.com/gitpod-io/gitpod/common-go/log"
26
26
"github.com/gitpod-io/gitpod/common-go/tracing"
27
+ "github.com/gitpod-io/gitpod/ws-manager-mk2/pkg/activity"
27
28
"github.com/gitpod-io/gitpod/ws-manager/api"
28
29
wsmanapi "github.com/gitpod-io/gitpod/ws-manager/api"
29
30
"github.com/gitpod-io/gitpod/ws-manager/api/config"
@@ -41,24 +42,26 @@ import (
41
42
"sigs.k8s.io/controller-runtime/pkg/client"
42
43
)
43
44
44
- func NewWorkspaceManagerServer (clnt client.Client , cfg * config.Configuration , reg prometheus.Registerer ) * WorkspaceManagerServer {
45
+ func NewWorkspaceManagerServer (clnt client.Client , cfg * config.Configuration , reg prometheus.Registerer , activity * activity. WorkspaceActivity ) * WorkspaceManagerServer {
45
46
metrics := newWorkspaceMetrics ()
46
47
reg .MustRegister (metrics )
47
48
48
49
return & WorkspaceManagerServer {
49
- Client : clnt ,
50
- Config : cfg ,
51
- metrics : metrics ,
50
+ Client : clnt ,
51
+ Config : cfg ,
52
+ metrics : metrics ,
53
+ activity : activity ,
52
54
subs : subscriptions {
53
55
subscribers : make (map [string ]chan * wsmanapi.SubscribeResponse ),
54
56
},
55
57
}
56
58
}
57
59
58
60
type WorkspaceManagerServer struct {
59
- Client client.Client
60
- Config * config.Configuration
61
- metrics * workspaceMetrics
61
+ Client client.Client
62
+ Config * config.Configuration
63
+ metrics * workspaceMetrics
64
+ activity * activity.WorkspaceActivity
62
65
63
66
subs subscriptions
64
67
wsmanapi.UnimplementedWorkspaceManagerServer
@@ -280,10 +283,15 @@ func (wsm *WorkspaceManagerServer) DescribeWorkspace(ctx context.Context, req *w
280
283
return nil , status .Errorf (codes .Internal , "cannot lookup workspace: %v" , err )
281
284
}
282
285
283
- return & wsmanapi.DescribeWorkspaceResponse {
286
+ result := & wsmanapi.DescribeWorkspaceResponse {
284
287
Status : extractWorkspaceStatus (& ws ),
285
- // TODO(cw): Add lastActivity
286
- }, nil
288
+ }
289
+
290
+ lastActivity := wsm .activity .GetLastActivity (req .Id )
291
+ if lastActivity != nil {
292
+ result .LastActivity = lastActivity .UTC ().Format (time .RFC3339Nano )
293
+ }
294
+ return result , nil
287
295
}
288
296
289
297
// Subscribe streams all status updates to a client
@@ -296,8 +304,87 @@ func (m *WorkspaceManagerServer) Subscribe(req *api.SubscribeRequest, srv api.Wo
296
304
return m .subs .Subscribe (srv .Context (), sub )
297
305
}
298
306
299
- func (wsm * WorkspaceManagerServer ) MarkActive (ctx context.Context , req * wsmanapi.MarkActiveRequest ) (* wsmanapi.MarkActiveResponse , error ) {
300
- return nil , status .Errorf (codes .Unimplemented , "method MarkActive not implemented" )
307
+ // MarkActive records a workspace as being active which prevents it from timing out
308
+ func (wsm * WorkspaceManagerServer ) MarkActive (ctx context.Context , req * wsmanapi.MarkActiveRequest ) (res * wsmanapi.MarkActiveResponse , err error ) {
309
+ //nolint:ineffassign
310
+ span , ctx := tracing .FromContext (ctx , "MarkActive" )
311
+ tracing .ApplyOWI (span , log .OWI ("" , "" , req .Id ))
312
+ defer tracing .FinishSpan (span , & err )
313
+
314
+ workspaceID := req .Id
315
+
316
+ var ws workspacev1.Workspace
317
+ err = wsm .Client .Get (ctx , types.NamespacedName {Namespace : wsm .Config .Namespace , Name : req .Id }, & ws )
318
+ if errors .IsNotFound (err ) {
319
+ return nil , status .Errorf (codes .NotFound , "workspace %s does not exist" , req .Id )
320
+ }
321
+ if err != nil {
322
+ return nil , status .Errorf (codes .Internal , "cannot mark workspace: %v" , err )
323
+ }
324
+
325
+ var firstUserActivity * timestamppb.Timestamp
326
+ for _ , c := range ws .Status .Conditions {
327
+ if c .Type == string (workspacev1 .WorkspaceConditionFirstUserActivity ) {
328
+ firstUserActivity = timestamppb .New (c .LastTransitionTime .Time )
329
+ }
330
+ }
331
+
332
+ // if user already mark workspace as active and this request has IgnoreIfActive flag, just simple ignore it
333
+ if firstUserActivity != nil && req .IgnoreIfActive {
334
+ return & api.MarkActiveResponse {}, nil
335
+ }
336
+
337
+ // We do not keep the last activity in the workspace resource to limit the load we're placing
338
+ // on the K8S master in check. Thus, this state lives locally in a map.
339
+ now := time .Now ().UTC ()
340
+ wsm .activity .Store (req .Id , now )
341
+
342
+ // We do however maintain the the "closed" flag as annotation on the workspace. This flag should not change
343
+ // very often and provides a better UX if it persists across ws-manager restarts.
344
+ isMarkedClosed := conditionPresentAndTrue (ws .Status .Conditions , string (workspacev1 .WorkspaceConditionClosed ))
345
+ if req .Closed && ! isMarkedClosed {
346
+ err = wsm .modifyWorkspace (ctx , req .Id , true , func (ws * workspacev1.Workspace ) error {
347
+ ws .Status .Conditions = addUniqueCondition (ws .Status .Conditions , metav1.Condition {
348
+ Type : string (workspacev1 .WorkspaceConditionClosed ),
349
+ Status : metav1 .ConditionTrue ,
350
+ LastTransitionTime : metav1 .NewTime (now ),
351
+ Reason : "MarkActiveRequest" ,
352
+ })
353
+ return nil
354
+ })
355
+ } else if ! req .Closed && isMarkedClosed {
356
+ err = wsm .modifyWorkspace (ctx , req .Id , true , func (ws * workspacev1.Workspace ) error {
357
+ ws .Status .Conditions = addUniqueCondition (ws .Status .Conditions , metav1.Condition {
358
+ Type : string (workspacev1 .WorkspaceConditionClosed ),
359
+ Status : metav1 .ConditionFalse ,
360
+ LastTransitionTime : metav1 .NewTime (now ),
361
+ Reason : "MarkActiveRequest" ,
362
+ })
363
+ return nil
364
+ })
365
+ }
366
+ if err != nil {
367
+ log .WithError (err ).WithFields (log .OWI ("" , "" , workspaceID )).Warn ("was unable to mark workspace properly" )
368
+ }
369
+
370
+ // If it's the first call: Mark the pod with FirstUserActivity condition.
371
+ if firstUserActivity == nil {
372
+ err := wsm .modifyWorkspace (ctx , req .Id , true , func (ws * workspacev1.Workspace ) error {
373
+ ws .Status .Conditions = addUniqueCondition (ws .Status .Conditions , metav1.Condition {
374
+ Type : string (workspacev1 .WorkspaceConditionFirstUserActivity ),
375
+ Status : metav1 .ConditionTrue ,
376
+ LastTransitionTime : metav1 .NewTime (now ),
377
+ Reason : "FirstActivity" ,
378
+ })
379
+ return nil
380
+ })
381
+ if err != nil {
382
+ log .WithError (err ).WithFields (log .OWI ("" , "" , workspaceID )).Warn ("was unable to set FirstUserActivity condition on workspace" )
383
+ return nil , err
384
+ }
385
+ }
386
+
387
+ return & api.MarkActiveResponse {}, nil
301
388
}
302
389
303
390
func (wsm * WorkspaceManagerServer ) SetTimeout (ctx context.Context , req * wsmanapi.SetTimeoutRequest ) (* wsmanapi.SetTimeoutResponse , error ) {
@@ -501,7 +588,7 @@ func extractWorkspaceStatus(ws *workspacev1.Workspace) *wsmanapi.WorkspaceStatus
501
588
502
589
var timeout string
503
590
if ws .Spec .Timeout .Time != nil {
504
- timeout = ws .Spec .Timeout .Time .String ()
591
+ timeout = ws .Spec .Timeout .Time .Duration . String ()
505
592
}
506
593
507
594
var phase wsmanapi.WorkspacePhase
@@ -527,7 +614,7 @@ func extractWorkspaceStatus(ws *workspacev1.Workspace) *wsmanapi.WorkspaceStatus
527
614
528
615
var firstUserActivity * timestamppb.Timestamp
529
616
for _ , c := range ws .Status .Conditions {
530
- if c .Type == string (workspacev1 .WorkspaceConditionUserActivity ) {
617
+ if c .Type == string (workspacev1 .WorkspaceConditionFirstUserActivity ) {
531
618
firstUserActivity = timestamppb .New (c .LastTransitionTime .Time )
532
619
}
533
620
}
@@ -878,3 +965,23 @@ func (m *workspaceMetrics) Describe(ch chan<- *prometheus.Desc) {
878
965
func (m * workspaceMetrics ) Collect (ch chan <- prometheus.Metric ) {
879
966
m .totalStartsCounterVec .Collect (ch )
880
967
}
968
+
969
+ func addUniqueCondition (conds []metav1.Condition , cond metav1.Condition ) []metav1.Condition {
970
+ for i , c := range conds {
971
+ if c .Type == cond .Type {
972
+ conds [i ] = cond
973
+ return conds
974
+ }
975
+ }
976
+
977
+ return append (conds , cond )
978
+ }
979
+
980
+ func conditionPresentAndTrue (cond []metav1.Condition , tpe string ) bool {
981
+ for _ , c := range cond {
982
+ if c .Type == tpe {
983
+ return c .Status == metav1 .ConditionTrue
984
+ }
985
+ }
986
+ return false
987
+ }
0 commit comments