@@ -24,11 +24,13 @@ import (
2424 wsk8s "github.com/gitpod-io/gitpod/common-go/kubernetes"
2525 "github.com/gitpod-io/gitpod/common-go/log"
2626 "github.com/gitpod-io/gitpod/common-go/tracing"
27+ "github.com/gitpod-io/gitpod/ws-manager-mk2/pkg/activity"
2728 "github.com/gitpod-io/gitpod/ws-manager/api"
2829 wsmanapi "github.com/gitpod-io/gitpod/ws-manager/api"
2930 "github.com/gitpod-io/gitpod/ws-manager/api/config"
3031 workspacev1 "github.com/gitpod-io/gitpod/ws-manager/api/crd/v1"
3132
33+ "github.com/sirupsen/logrus"
3234 corev1 "k8s.io/api/core/v1"
3335 "k8s.io/apimachinery/pkg/api/errors"
3436 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -41,24 +43,26 @@ import (
4143 "sigs.k8s.io/controller-runtime/pkg/client"
4244)
4345
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 {
4547 metrics := newWorkspaceMetrics ()
4648 reg .MustRegister (metrics )
4749
4850 return & WorkspaceManagerServer {
49- Client : clnt ,
50- Config : cfg ,
51- metrics : metrics ,
51+ Client : clnt ,
52+ Config : cfg ,
53+ metrics : metrics ,
54+ activity : activity ,
5255 subs : subscriptions {
5356 subscribers : make (map [string ]chan * wsmanapi.SubscribeResponse ),
5457 },
5558 }
5659}
5760
5861type 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
6266
6367 subs subscriptions
6468 wsmanapi.UnimplementedWorkspaceManagerServer
@@ -280,10 +284,15 @@ func (wsm *WorkspaceManagerServer) DescribeWorkspace(ctx context.Context, req *w
280284 return nil , status .Errorf (codes .Internal , "cannot lookup workspace: %v" , err )
281285 }
282286
283- return & wsmanapi.DescribeWorkspaceResponse {
287+ result := & wsmanapi.DescribeWorkspaceResponse {
284288 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
287296}
288297
289298// Subscribe streams all status updates to a client
@@ -296,8 +305,91 @@ func (m *WorkspaceManagerServer) Subscribe(req *api.SubscribeRequest, srv api.Wo
296305 return m .subs .Subscribe (srv .Context (), sub )
297306}
298307
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
301393}
302394
303395func (wsm * WorkspaceManagerServer ) SetTimeout (ctx context.Context , req * wsmanapi.SetTimeoutRequest ) (* wsmanapi.SetTimeoutResponse , error ) {
@@ -501,7 +593,7 @@ func extractWorkspaceStatus(ws *workspacev1.Workspace) *wsmanapi.WorkspaceStatus
501593
502594 var timeout string
503595 if ws .Spec .Timeout .Time != nil {
504- timeout = ws .Spec .Timeout .Time .String ()
596+ timeout = ws .Spec .Timeout .Time .Duration . String ()
505597 }
506598
507599 var phase wsmanapi.WorkspacePhase
@@ -527,7 +619,7 @@ func extractWorkspaceStatus(ws *workspacev1.Workspace) *wsmanapi.WorkspaceStatus
527619
528620 var firstUserActivity * timestamppb.Timestamp
529621 for _ , c := range ws .Status .Conditions {
530- if c .Type == string (workspacev1 .WorkspaceConditionUserActivity ) {
622+ if c .Type == string (workspacev1 .WorkspaceConditionFirstUserActivity ) {
531623 firstUserActivity = timestamppb .New (c .LastTransitionTime .Time )
532624 }
533625 }
@@ -878,3 +970,23 @@ func (m *workspaceMetrics) Describe(ch chan<- *prometheus.Desc) {
878970func (m * workspaceMetrics ) Collect (ch chan <- prometheus.Metric ) {
879971 m .totalStartsCounterVec .Collect (ch )
880972}
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