Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/ws-daemon/pkg/controller/workspace_controller.go
2500 views
1
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License-AGPL.txt in the project root for license information.
4
5
package controller
6
7
import (
8
"context"
9
"fmt"
10
"time"
11
12
wsk8s "github.com/gitpod-io/gitpod/common-go/kubernetes"
13
glog "github.com/gitpod-io/gitpod/common-go/log"
14
"github.com/gitpod-io/gitpod/common-go/tracing"
15
csapi "github.com/gitpod-io/gitpod/content-service/api"
16
"github.com/gitpod-io/gitpod/content-service/pkg/storage"
17
"github.com/gitpod-io/gitpod/ws-daemon/pkg/container"
18
"github.com/gitpod-io/gitpod/ws-daemon/pkg/content"
19
"github.com/gitpod-io/gitpod/ws-daemon/pkg/iws"
20
workspacev1 "github.com/gitpod-io/gitpod/ws-manager/api/crd/v1"
21
"github.com/opentracing/opentracing-go"
22
"github.com/prometheus/client_golang/prometheus"
23
"github.com/sirupsen/logrus"
24
25
"google.golang.org/protobuf/proto"
26
corev1 "k8s.io/api/core/v1"
27
"k8s.io/apimachinery/pkg/api/errors"
28
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29
"k8s.io/apimachinery/pkg/types"
30
"k8s.io/apimachinery/pkg/util/wait"
31
"k8s.io/client-go/tools/record"
32
"k8s.io/client-go/util/retry"
33
ctrl "sigs.k8s.io/controller-runtime"
34
"sigs.k8s.io/controller-runtime/pkg/client"
35
"sigs.k8s.io/controller-runtime/pkg/controller"
36
"sigs.k8s.io/controller-runtime/pkg/event"
37
"sigs.k8s.io/controller-runtime/pkg/log"
38
"sigs.k8s.io/controller-runtime/pkg/predicate"
39
)
40
41
var retryParams = wait.Backoff{
42
Steps: 10,
43
Duration: 10 * time.Millisecond,
44
Factor: 2.0,
45
Jitter: 0.2,
46
}
47
48
type WorkspaceControllerOpts struct {
49
NodeName string
50
ContentConfig content.Config
51
UIDMapperConfig iws.UidmapperConfig
52
ContainerRuntime container.Runtime
53
CGroupMountPoint string
54
MetricsRegistry prometheus.Registerer
55
}
56
57
type WorkspaceController struct {
58
client.Client
59
NodeName string
60
maxConcurrentReconciles int
61
operations WorkspaceOperations
62
metrics *workspaceMetrics
63
secretNamespace string
64
recorder record.EventRecorder
65
runtime container.Runtime
66
}
67
68
func NewWorkspaceController(c client.Client, recorder record.EventRecorder, nodeName, secretNamespace string, maxConcurrentReconciles int, ops WorkspaceOperations, reg prometheus.Registerer, runtime container.Runtime) (*WorkspaceController, error) {
69
metrics := newWorkspaceMetrics()
70
reg.Register(metrics)
71
72
return &WorkspaceController{
73
Client: c,
74
NodeName: nodeName,
75
maxConcurrentReconciles: maxConcurrentReconciles,
76
operations: ops,
77
metrics: metrics,
78
secretNamespace: secretNamespace,
79
recorder: recorder,
80
runtime: runtime,
81
}, nil
82
}
83
84
// SetupWithManager sets up the controller with the Manager.
85
func (wsc *WorkspaceController) SetupWithManager(mgr ctrl.Manager) error {
86
return ctrl.NewControllerManagedBy(mgr).
87
Named("workspace").
88
WithOptions(controller.Options{
89
MaxConcurrentReconciles: wsc.maxConcurrentReconciles,
90
}).
91
For(&workspacev1.Workspace{}).
92
WithEventFilter(eventFilter(wsc.NodeName)).
93
Complete(wsc)
94
}
95
96
func eventFilter(nodeName string) predicate.Predicate {
97
return predicate.Funcs{
98
CreateFunc: func(e event.CreateEvent) bool {
99
return workspaceFilter(e.Object, nodeName)
100
},
101
102
UpdateFunc: func(e event.UpdateEvent) bool {
103
return workspaceFilter(e.ObjectNew, nodeName)
104
},
105
DeleteFunc: func(e event.DeleteEvent) bool {
106
return false
107
},
108
}
109
}
110
111
func workspaceFilter(object client.Object, nodeName string) bool {
112
if ws, ok := object.(*workspacev1.Workspace); ok {
113
return ws.Status.Runtime != nil && ws.Status.Runtime.NodeName == nodeName
114
}
115
return false
116
}
117
118
func (wsc *WorkspaceController) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) {
119
span, ctx := opentracing.StartSpanFromContext(ctx, "Reconcile")
120
defer tracing.FinishSpan(span, &err)
121
122
var workspace workspacev1.Workspace
123
if err := wsc.Get(ctx, req.NamespacedName, &workspace); err != nil {
124
// ignore not-found errors, since they can't be fixed by an immediate
125
// requeue (we'll need to wait for a new notification).
126
return ctrl.Result{}, client.IgnoreNotFound(err)
127
}
128
129
if workspace.Status.Phase == workspacev1.WorkspacePhaseCreating ||
130
workspace.Status.Phase == workspacev1.WorkspacePhaseInitializing {
131
132
result, err = wsc.handleWorkspaceInit(ctx, &workspace, req)
133
return result, err
134
}
135
136
if workspace.Status.Phase == workspacev1.WorkspacePhaseRunning {
137
result, err = wsc.handleWorkspaceRunning(ctx, &workspace, req)
138
return result, err
139
}
140
141
if workspace.Status.Phase == workspacev1.WorkspacePhaseStopping {
142
result, err = wsc.handleWorkspaceStop(ctx, &workspace, req)
143
return result, err
144
}
145
146
return ctrl.Result{}, nil
147
}
148
149
// latestWorkspace checks if the we have the latest generation of the workspace CR. We do this because
150
// the cache could be stale and we retrieve a workspace CR that does not have the content init/backup
151
// conditions even though we have set them previously. This will lead to us performing these operations
152
// again. To prevent this we wait until we have the latest workspace CR.
153
func (wsc *WorkspaceController) latestWorkspace(ctx context.Context, ws *workspacev1.Workspace) error {
154
ws.Status.SetCondition(workspacev1.NewWorkspaceConditionRefresh())
155
156
err := wsc.Client.Status().Update(ctx, ws)
157
if err != nil && !errors.IsConflict(err) {
158
glog.WithFields(ws.OWI()).Warnf("could not refresh workspace: %v", err)
159
}
160
161
return err
162
}
163
164
func (wsc *WorkspaceController) handleWorkspaceInit(ctx context.Context, ws *workspacev1.Workspace, req ctrl.Request) (result ctrl.Result, err error) {
165
log := log.FromContext(ctx)
166
span, ctx := opentracing.StartSpanFromContext(ctx, "handleWorkspaceInit")
167
defer tracing.FinishSpan(span, &err)
168
169
if c := wsk8s.GetCondition(ws.Status.Conditions, string(workspacev1.WorkspaceConditionContentReady)); c == nil {
170
if wsc.latestWorkspace(ctx, ws) != nil {
171
return ctrl.Result{Requeue: true, RequeueAfter: 100 * time.Millisecond}, nil
172
}
173
174
glog.WithFields(ws.OWI()).WithField("workspace", req.NamespacedName).WithField("phase", ws.Status.Phase).Info("handle workspace init")
175
176
init, err := wsc.prepareInitializer(ctx, ws)
177
if err != nil {
178
return ctrl.Result{}, fmt.Errorf("failed to prepare initializer: %w", err)
179
}
180
181
initStart := time.Now()
182
stats, failure, initErr := wsc.operations.InitWorkspace(ctx, InitOptions{
183
Meta: WorkspaceMeta{
184
Owner: ws.Spec.Ownership.Owner,
185
WorkspaceID: ws.Spec.Ownership.WorkspaceID,
186
InstanceID: ws.Name,
187
},
188
Initializer: init,
189
Headless: ws.IsHeadless(),
190
StorageQuota: ws.Spec.StorageQuota,
191
})
192
193
initMetrics := initializerMetricsFromInitializerStats(stats)
194
err = retry.RetryOnConflict(retryParams, func() error {
195
if err := wsc.Get(ctx, req.NamespacedName, ws); err != nil {
196
return err
197
}
198
199
// persist init failure/success
200
if failure != "" {
201
log.Error(initErr, "could not initialize workspace", "name", ws.Name)
202
ws.Status.SetCondition(workspacev1.NewWorkspaceConditionContentReady(metav1.ConditionFalse, workspacev1.ReasonInitializationFailure, failure))
203
} else {
204
ws.Status.SetCondition(workspacev1.NewWorkspaceConditionContentReady(metav1.ConditionTrue, workspacev1.ReasonInitializationSuccess, ""))
205
}
206
207
// persist initializer metrics
208
if initMetrics != nil {
209
ws.Status.InitializerMetrics = initMetrics
210
}
211
212
return wsc.Status().Update(ctx, ws)
213
})
214
215
if err == nil {
216
wsc.metrics.recordInitializeTime(time.Since(initStart).Seconds(), ws)
217
} else {
218
err = fmt.Errorf("failed to set content ready condition (failure: '%s'): %w", failure, err)
219
}
220
221
wsc.emitEvent(ws, "Content init", initErr)
222
return ctrl.Result{}, err
223
}
224
225
return ctrl.Result{}, nil
226
}
227
228
func initializerMetricsFromInitializerStats(stats *csapi.InitializerMetrics) *workspacev1.InitializerMetrics {
229
if stats == nil || len(*stats) == 0 {
230
return nil
231
}
232
233
result := workspacev1.InitializerMetrics{}
234
for _, metric := range *stats {
235
switch metric.Type {
236
case "git":
237
result.Git = &workspacev1.InitializerStepMetric{
238
Duration: &metav1.Duration{Duration: metric.Duration},
239
Size: metric.Size,
240
}
241
case "fileDownload":
242
result.FileDownload = &workspacev1.InitializerStepMetric{
243
Duration: &metav1.Duration{Duration: metric.Duration},
244
Size: metric.Size,
245
}
246
case "snapshot":
247
result.Snapshot = &workspacev1.InitializerStepMetric{
248
Duration: &metav1.Duration{Duration: metric.Duration},
249
Size: metric.Size,
250
}
251
case "fromBackup":
252
result.Backup = &workspacev1.InitializerStepMetric{
253
Duration: &metav1.Duration{Duration: metric.Duration},
254
Size: metric.Size,
255
}
256
case "composite":
257
result.Composite = &workspacev1.InitializerStepMetric{
258
Duration: &metav1.Duration{Duration: metric.Duration},
259
Size: metric.Size,
260
}
261
case "prebuild":
262
result.Prebuild = &workspacev1.InitializerStepMetric{
263
Duration: &metav1.Duration{Duration: metric.Duration},
264
Size: metric.Size,
265
}
266
}
267
}
268
269
return &result
270
}
271
272
func (wsc *WorkspaceController) handleWorkspaceRunning(ctx context.Context, ws *workspacev1.Workspace, req ctrl.Request) (result ctrl.Result, err error) {
273
span, ctx := opentracing.StartSpanFromContext(ctx, "handleWorkspaceRunning")
274
defer tracing.FinishSpan(span, &err)
275
276
var imageInfo *workspacev1.WorkspaceImageInfo = nil
277
if ws.Status.ImageInfo == nil {
278
getImageInfo := func() (*workspacev1.WorkspaceImageInfo, error) {
279
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
280
defer cancel()
281
id, err := wsc.runtime.WaitForContainer(ctx, ws.Name)
282
if err != nil {
283
return nil, fmt.Errorf("failed to wait for container: %w", err)
284
}
285
info, err := wsc.runtime.GetContainerImageInfo(ctx, id)
286
if err != nil {
287
return nil, fmt.Errorf("failed to get container image info: %w", err)
288
}
289
290
err = retry.RetryOnConflict(retryParams, func() error {
291
if err := wsc.Get(ctx, req.NamespacedName, ws); err != nil {
292
return err
293
}
294
ws.Status.ImageInfo = info
295
return wsc.Status().Update(ctx, ws)
296
})
297
if err != nil {
298
return info, fmt.Errorf("failed to update workspace with image info: %w", err)
299
}
300
return info, nil
301
}
302
imageInfo, err = getImageInfo()
303
if err != nil {
304
glog.WithFields(ws.OWI()).WithField("workspace", req.NamespacedName).Errorf("failed to get image info: %v", err)
305
} else {
306
glog.WithFields(ws.OWI()).WithField("workspace", req.NamespacedName).WithField("imageInfo", glog.TrustedValueWrap{Value: imageInfo}).Info("updated image info")
307
}
308
}
309
return ctrl.Result{}, wsc.operations.SetupWorkspace(ctx, ws.Name, imageInfo)
310
}
311
312
func (wsc *WorkspaceController) handleWorkspaceStop(ctx context.Context, ws *workspacev1.Workspace, req ctrl.Request) (result ctrl.Result, err error) {
313
span, ctx := opentracing.StartSpanFromContext(ctx, "handleWorkspaceStop")
314
defer tracing.FinishSpan(span, &err)
315
316
if ws.IsConditionTrue(workspacev1.WorkspaceConditionPodRejected) {
317
// edge case only exercised for rejected workspace pods
318
if ws.IsConditionPresent(workspacev1.WorkspaceConditionStateWiped) {
319
// we are done here
320
return ctrl.Result{}, nil
321
}
322
323
return wsc.doWipeWorkspace(ctx, ws, req)
324
}
325
326
// regular case
327
return wsc.doWorkspaceContentBackup(ctx, span, ws, req)
328
}
329
330
func (wsc *WorkspaceController) doWipeWorkspace(ctx context.Context, ws *workspacev1.Workspace, req ctrl.Request) (result ctrl.Result, err error) {
331
log := log.FromContext(ctx)
332
333
// in this case we are not interested in any backups, but instead are concerned with completely wiping all state that might be dangling somewhere
334
if ws.IsConditionTrue(workspacev1.WorkspaceConditionContainerRunning) {
335
// Container is still running, we need to wait for it to stop.
336
// We should get an event when the condition changes, but requeue
337
// anyways to make sure we act on it in time.
338
return ctrl.Result{RequeueAfter: 500 * time.Millisecond}, nil
339
}
340
341
if wsc.latestWorkspace(ctx, ws) != nil {
342
return ctrl.Result{Requeue: true, RequeueAfter: 100 * time.Millisecond}, nil
343
}
344
345
setStateWipedCondition := func(success bool) {
346
err := retry.RetryOnConflict(retryParams, func() error {
347
if err := wsc.Get(ctx, req.NamespacedName, ws); err != nil {
348
return err
349
}
350
351
if success {
352
ws.Status.SetCondition(workspacev1.NewWorkspaceConditionStateWiped("", metav1.ConditionTrue))
353
} else {
354
ws.Status.SetCondition(workspacev1.NewWorkspaceConditionStateWiped("", metav1.ConditionFalse))
355
}
356
return wsc.Client.Status().Update(ctx, ws)
357
})
358
if err != nil {
359
log.Error(err, "failed to set StateWiped condition")
360
}
361
}
362
log.Info("handling workspace stop - wiping mode")
363
defer log.Info("handling workspace stop - wiping done.")
364
365
err = wsc.operations.WipeWorkspace(ctx, ws.Name)
366
if err != nil {
367
setStateWipedCondition(false)
368
wsc.emitEvent(ws, "Wiping", fmt.Errorf("failed to wipe workspace: %w", err))
369
return ctrl.Result{}, fmt.Errorf("failed to wipe workspace: %w", err)
370
}
371
372
setStateWipedCondition(true)
373
374
return ctrl.Result{}, nil
375
}
376
377
func (wsc *WorkspaceController) doWorkspaceContentBackup(ctx context.Context, span opentracing.Span, ws *workspacev1.Workspace, req ctrl.Request) (result ctrl.Result, err error) {
378
log := log.FromContext(ctx)
379
380
if c := wsk8s.GetCondition(ws.Status.Conditions, string(workspacev1.WorkspaceConditionContentReady)); c == nil || c.Status == metav1.ConditionFalse {
381
return ctrl.Result{}, fmt.Errorf("workspace content was never ready")
382
}
383
384
if ws.IsConditionTrue(workspacev1.WorkspaceConditionBackupComplete) {
385
return ctrl.Result{}, nil
386
}
387
388
if ws.IsConditionTrue(workspacev1.WorkspaceConditionBackupFailure) {
389
return ctrl.Result{}, nil
390
}
391
392
if ws.IsConditionTrue(workspacev1.WorkspaceConditionAborted) {
393
span.LogKV("event", "workspace was aborted")
394
return ctrl.Result{}, nil
395
}
396
397
if ws.Spec.Type == workspacev1.WorkspaceTypeImageBuild {
398
// No disposal for image builds.
399
return ctrl.Result{}, nil
400
}
401
402
if ws.IsConditionTrue(workspacev1.WorkspaceConditionContainerRunning) {
403
// Container is still running, we need to wait for it to stop.
404
// We will wait for this situation for up to 5 minutes.
405
// If the container is still in a running state after that,
406
// there may be an issue with state synchronization.
407
// We should start backup anyway to avoid data loss.
408
if !(ws.Status.PodStoppingTime != nil && time.Since(ws.Status.PodStoppingTime.Time) > 5*time.Minute) {
409
// We should get an event when the condition changes, but requeue
410
// anyways to make sure we act on it in time.
411
return ctrl.Result{RequeueAfter: 500 * time.Millisecond}, nil
412
}
413
414
if !ws.IsConditionTrue(workspacev1.WorkspaceConditionForceKilledTask) {
415
err = wsc.forceKillContainerTask(ctx, ws)
416
if err != nil {
417
glog.WithFields(ws.OWI()).WithField("workspace", req.NamespacedName).Errorf("failed to force kill task: %v", err)
418
}
419
err = retry.RetryOnConflict(retryParams, func() error {
420
if err := wsc.Get(ctx, req.NamespacedName, ws); err != nil {
421
return err
422
}
423
ws.Status.SetCondition(workspacev1.NewWorkspaceConditionForceKilledTask())
424
return wsc.Client.Status().Update(ctx, ws)
425
})
426
if err != nil {
427
return ctrl.Result{}, fmt.Errorf("failed to set force killed task condition: %w", err)
428
}
429
return ctrl.Result{Requeue: true, RequeueAfter: 2 * time.Second}, nil
430
}
431
432
if time.Since(wsk8s.GetCondition(ws.Status.Conditions, string(workspacev1.WorkspaceConditionForceKilledTask)).LastTransitionTime.Time) < 2*time.Second {
433
return ctrl.Result{Requeue: true, RequeueAfter: 2 * time.Second}, nil
434
}
435
436
glog.WithFields(ws.OWI()).WithField("workspace", req.NamespacedName).Warn("workspace container is still running after 5 minutes of deletion, starting backup anyway")
437
err = wsc.dumpWorkspaceContainerInfo(ctx, ws)
438
if err != nil {
439
glog.WithFields(ws.OWI()).WithField("workspace", req.NamespacedName).Errorf("failed to dump container info: %v", err)
440
}
441
}
442
443
if wsc.latestWorkspace(ctx, ws) != nil {
444
return ctrl.Result{Requeue: true, RequeueAfter: 100 * time.Millisecond}, nil
445
}
446
447
glog.WithFields(ws.OWI()).WithField("workspace", req.NamespacedName).WithField("phase", ws.Status.Phase).Info("handle workspace stop")
448
449
disposeStart := time.Now()
450
var snapshotName string
451
var snapshotUrl string
452
if ws.Spec.Type == workspacev1.WorkspaceTypeRegular {
453
snapshotName = storage.DefaultBackup
454
} else {
455
snapshotUrl, snapshotName, err = wsc.operations.SnapshotIDs(ctx, ws.Name)
456
if err != nil {
457
return ctrl.Result{}, fmt.Errorf("failed to get snapshot name and URL: %w", err)
458
}
459
460
// todo(ft): remove this and only set the snapshot url after the actual backup is done (see L320-322) ENT-319
461
// ws-manager-bridge expects to receive the snapshot url while the workspace
462
// is in STOPPING so instead of breaking the assumptions of ws-manager-bridge
463
// we set the url here and not after the snapshot has been taken as otherwise
464
// the workspace would already be in STOPPED and ws-manager-bridge would not
465
// receive the url
466
err = retry.RetryOnConflict(retryParams, func() error {
467
if err := wsc.Get(ctx, req.NamespacedName, ws); err != nil {
468
return err
469
}
470
471
ws.Status.Snapshot = snapshotUrl
472
return wsc.Client.Status().Update(ctx, ws)
473
})
474
475
if err != nil {
476
return ctrl.Result{}, fmt.Errorf("failed to set snapshot URL: %w", err)
477
}
478
}
479
480
gitStatus, disposeErr := wsc.operations.BackupWorkspace(ctx, BackupOptions{
481
Meta: WorkspaceMeta{
482
Owner: ws.Spec.Ownership.Owner,
483
WorkspaceID: ws.Spec.Ownership.WorkspaceID,
484
InstanceID: ws.Name,
485
},
486
SnapshotName: snapshotName,
487
BackupLogs: ws.Spec.Type == workspacev1.WorkspaceTypePrebuild,
488
UpdateGitStatus: ws.Spec.Type == workspacev1.WorkspaceTypeRegular,
489
SkipBackupContent: false,
490
})
491
492
err = retry.RetryOnConflict(retryParams, func() error {
493
if err := wsc.Get(ctx, req.NamespacedName, ws); err != nil {
494
return err
495
}
496
497
ws.Status.GitStatus = toWorkspaceGitStatus(gitStatus)
498
499
if disposeErr != nil {
500
log.Error(disposeErr, "failed to backup workspace", "name", ws.Name)
501
ws.Status.SetCondition(workspacev1.NewWorkspaceConditionBackupFailure(disposeErr.Error()))
502
} else {
503
ws.Status.SetCondition(workspacev1.NewWorkspaceConditionBackupComplete())
504
if ws.Spec.Type != workspacev1.WorkspaceTypeRegular {
505
ws.Status.Snapshot = snapshotUrl
506
}
507
}
508
509
return wsc.Status().Update(ctx, ws)
510
})
511
512
if err == nil {
513
wsc.metrics.recordFinalizeTime(time.Since(disposeStart).Seconds(), ws)
514
} else {
515
log.Error(err, "failed to set backup condition", "disposeErr", disposeErr)
516
}
517
518
if disposeErr != nil {
519
wsc.emitEvent(ws, "Backup", fmt.Errorf("failed to backup workspace: %w", disposeErr))
520
}
521
522
err = wsc.operations.DeleteWorkspace(ctx, ws.Name)
523
if err != nil {
524
wsc.emitEvent(ws, "Backup", fmt.Errorf("failed to clean up workspace: %w", err))
525
return ctrl.Result{}, fmt.Errorf("failed to clean up workspace: %w", err)
526
}
527
528
return ctrl.Result{}, nil
529
}
530
531
func (wsc *WorkspaceController) dumpWorkspaceContainerInfo(ctx context.Context, ws *workspacev1.Workspace) error {
532
id, err := wsc.runtime.WaitForContainer(ctx, ws.Name)
533
if err != nil {
534
return fmt.Errorf("failed to wait for container: %w", err)
535
}
536
task, err := wsc.runtime.GetContainerTaskInfo(ctx, id)
537
if err != nil {
538
return fmt.Errorf("failed to get container task info: %w", err)
539
}
540
glog.WithFields(ws.OWI()).WithFields(logrus.Fields{
541
"containerID": id,
542
"exitStatus": task.ExitStatus,
543
"pid": task.Pid,
544
"exitedAt": task.ExitedAt.String(),
545
"status": task.Status.String(),
546
}).Info("container task info")
547
return nil
548
}
549
550
func (wsc *WorkspaceController) forceKillContainerTask(ctx context.Context, ws *workspacev1.Workspace) error {
551
id, err := wsc.runtime.WaitForContainer(ctx, ws.Name)
552
if err != nil {
553
return fmt.Errorf("failed to wait for container: %w", err)
554
}
555
return wsc.runtime.ForceKillContainerTask(ctx, id)
556
}
557
558
func (wsc *WorkspaceController) prepareInitializer(ctx context.Context, ws *workspacev1.Workspace) (*csapi.WorkspaceInitializer, error) {
559
var init csapi.WorkspaceInitializer
560
err := proto.Unmarshal(ws.Spec.Initializer, &init)
561
if err != nil {
562
err = fmt.Errorf("cannot unmarshal initializer config: %w", err)
563
return nil, err
564
}
565
566
var tokenSecret corev1.Secret
567
err = wsc.Get(ctx, types.NamespacedName{Name: fmt.Sprintf("%s-tokens", ws.Name), Namespace: wsc.secretNamespace}, &tokenSecret)
568
if err != nil {
569
return nil, fmt.Errorf("could not get token secret for workspace: %w", err)
570
}
571
572
if err = csapi.InjectSecretsToInitializer(&init, tokenSecret.Data); err != nil {
573
return nil, fmt.Errorf("failed to inject secrets into initializer: %w", err)
574
}
575
576
return &init, nil
577
}
578
579
func (wsc *WorkspaceController) emitEvent(ws *workspacev1.Workspace, operation string, failure error) {
580
if failure != nil {
581
wsc.recorder.Eventf(ws, corev1.EventTypeWarning, "Failed", "%s failed: %s", operation, failure.Error())
582
}
583
}
584
585
func toWorkspaceGitStatus(status *csapi.GitStatus) *workspacev1.GitStatus {
586
if status == nil {
587
return nil
588
}
589
590
return &workspacev1.GitStatus{
591
Branch: status.Branch,
592
LatestCommit: status.LatestCommit,
593
UncommitedFiles: status.UncommitedFiles,
594
TotalUncommitedFiles: status.TotalUncommitedFiles,
595
UntrackedFiles: status.UntrackedFiles,
596
TotalUntrackedFiles: status.TotalUntrackedFiles,
597
UnpushedCommits: status.UnpushedCommits,
598
TotalUnpushedCommits: status.TotalUnpushedCommits,
599
}
600
}
601
602
type workspaceMetrics struct {
603
initializeTimeHistVec *prometheus.HistogramVec
604
finalizeTimeHistVec *prometheus.HistogramVec
605
}
606
607
func newWorkspaceMetrics() *workspaceMetrics {
608
return &workspaceMetrics{
609
initializeTimeHistVec: prometheus.NewHistogramVec(prometheus.HistogramOpts{
610
Namespace: "gitpod",
611
Subsystem: "ws_daemon",
612
Name: "workspace_initialize_seconds",
613
Help: "time it took to initialize workspace",
614
Buckets: prometheus.ExponentialBuckets(2, 2, 10),
615
}, []string{"type", "class"}),
616
finalizeTimeHistVec: prometheus.NewHistogramVec(prometheus.HistogramOpts{
617
Namespace: "gitpod",
618
Subsystem: "ws_daemon",
619
Name: "workspace_finalize_seconds",
620
Help: "time it took to finalize workspace",
621
Buckets: prometheus.ExponentialBuckets(2, 2, 10),
622
}, []string{"type", "class"}),
623
}
624
}
625
626
func (m *workspaceMetrics) recordInitializeTime(duration float64, ws *workspacev1.Workspace) {
627
tpe := string(ws.Spec.Type)
628
class := ws.Spec.Class
629
630
hist, err := m.initializeTimeHistVec.GetMetricWithLabelValues(tpe, class)
631
if err != nil {
632
glog.WithError(err).WithFields(ws.OWI()).WithField("type", tpe).WithField("class", class).Infof("could not retrieve initialize metric")
633
}
634
635
hist.Observe(duration)
636
}
637
638
func (m *workspaceMetrics) recordFinalizeTime(duration float64, ws *workspacev1.Workspace) {
639
tpe := string(ws.Spec.Type)
640
class := ws.Spec.Class
641
642
hist, err := m.finalizeTimeHistVec.GetMetricWithLabelValues(tpe, class)
643
if err != nil {
644
glog.WithError(err).WithFields(ws.OWI()).WithField("type", tpe).WithField("class", class).Infof("could not retrieve finalize metric")
645
}
646
647
hist.Observe(duration)
648
}
649
650
// Describe implements Collector. It will send exactly one Desc to the provided channel.
651
func (m *workspaceMetrics) Describe(ch chan<- *prometheus.Desc) {
652
m.initializeTimeHistVec.Describe(ch)
653
m.finalizeTimeHistVec.Describe(ch)
654
}
655
656
// Collect implements Collector.
657
func (m *workspaceMetrics) Collect(ch chan<- prometheus.Metric) {
658
m.initializeTimeHistVec.Collect(ch)
659
m.finalizeTimeHistVec.Collect(ch)
660
}
661
662