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