Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/ws-manager-mk2/controllers/create.go
2498 views
1
// Copyright (c) 2020 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 controllers
6
7
import (
8
"context"
9
"crypto/rand"
10
"fmt"
11
"io"
12
"path/filepath"
13
"reflect"
14
"strconv"
15
"strings"
16
"time"
17
18
"github.com/imdario/mergo"
19
"golang.org/x/xerrors"
20
"google.golang.org/protobuf/proto"
21
corev1 "k8s.io/api/core/v1"
22
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
23
"k8s.io/apimachinery/pkg/util/intstr"
24
"k8s.io/apimachinery/pkg/version"
25
"k8s.io/utils/pointer"
26
27
wsk8s "github.com/gitpod-io/gitpod/common-go/kubernetes"
28
"github.com/gitpod-io/gitpod/common-go/tracing"
29
csapi "github.com/gitpod-io/gitpod/content-service/api"
30
regapi "github.com/gitpod-io/gitpod/registry-facade/api"
31
"github.com/gitpod-io/gitpod/ws-manager-mk2/pkg/constants"
32
config "github.com/gitpod-io/gitpod/ws-manager/api/config"
33
workspacev1 "github.com/gitpod-io/gitpod/ws-manager/api/crd/v1"
34
)
35
36
const (
37
// workspaceVolume is the name of the workspace volume
38
workspaceVolumeName = "vol-this-workspace"
39
// workspaceDir is the path within all containers where workspaceVolume is mounted to
40
workspaceDir = "/workspace"
41
42
// headlessLabel marks a workspace as headless
43
headlessLabel = "gitpod.io/headless"
44
45
// instanceIDLabel is added for the container dispatch mechanism in ws-daemon to work
46
// TODO(furisto): remove this label once we have moved ws-daemon to a controller setup
47
instanceIDLabel = "gitpod.io/instanceID"
48
49
// Grace time until the process in the workspace is properly completed
50
// e.g. dockerd in the workspace may take some time to clean up the overlay directory.
51
//
52
// The high value here tries to avoid issues in nodes under load, we could face the deletion of the node, even if the pod is still in terminating state.
53
// This could be related to creation of the backup or the upload to the object storage
54
// https://github.com/kubernetes/autoscaler/blob/ee59c74cc0d61165c633d3e5b42caccc614be542/cluster-autoscaler/utils/drain/drain.go#L330
55
// https://github.com/kubernetes/autoscaler/blob/ee59c74cc0d61165c633d3e5b42caccc614be542/cluster-autoscaler/utils/drain/drain.go#L107
56
gracePeriod = 30 * time.Minute
57
)
58
59
type startWorkspaceContext struct {
60
Config *config.Configuration
61
Workspace *workspacev1.Workspace
62
Labels map[string]string `json:"labels"`
63
IDEPort int32 `json:"idePort"`
64
SupervisorPort int32 `json:"supervisorPort"`
65
Headless bool `json:"headless"`
66
ServerVersion *version.Info `json:"serverVersion"`
67
}
68
69
// createWorkspacePod creates the actual workspace pod based on the definite workspace pod and appropriate
70
// templates. The result of this function is not expected to be modified prior to being passed to Kubernetes.
71
func (r *WorkspaceReconciler) createWorkspacePod(sctx *startWorkspaceContext) (*corev1.Pod, error) {
72
class, ok := sctx.Config.WorkspaceClasses[sctx.Workspace.Spec.Class]
73
if !ok {
74
return nil, xerrors.Errorf("unknown workspace class: %s", sctx.Workspace.Spec.Class)
75
}
76
77
podTemplate, err := config.GetWorkspacePodTemplate(class.Templates.DefaultPath)
78
if err != nil {
79
return nil, xerrors.Errorf("cannot read pod template - this is a configuration problem: %w", err)
80
}
81
var typeSpecificTpl *corev1.Pod
82
switch sctx.Workspace.Spec.Type {
83
case workspacev1.WorkspaceTypeRegular:
84
typeSpecificTpl, err = config.GetWorkspacePodTemplate(class.Templates.RegularPath)
85
case workspacev1.WorkspaceTypePrebuild:
86
typeSpecificTpl, err = config.GetWorkspacePodTemplate(class.Templates.PrebuildPath)
87
case workspacev1.WorkspaceTypeImageBuild:
88
typeSpecificTpl, err = config.GetWorkspacePodTemplate(class.Templates.ImagebuildPath)
89
}
90
if err != nil {
91
return nil, xerrors.Errorf("cannot read type-specific pod template - this is a configuration problem: %w", err)
92
}
93
if typeSpecificTpl != nil {
94
err = combineDefiniteWorkspacePodWithTemplate(podTemplate, typeSpecificTpl)
95
if err != nil {
96
return nil, xerrors.Errorf("cannot apply type-specific pod template: %w", err)
97
}
98
}
99
100
pod, err := createDefiniteWorkspacePod(sctx)
101
if err != nil {
102
return nil, xerrors.Errorf("cannot create definite workspace pod: %w", err)
103
}
104
105
err = combineDefiniteWorkspacePodWithTemplate(pod, podTemplate)
106
if err != nil {
107
return nil, xerrors.Errorf("cannot create workspace pod: %w", err)
108
}
109
return pod, nil
110
}
111
112
// combineDefiniteWorkspacePodWithTemplate merges a definite workspace pod with a user-provided template.
113
// In essence this function just calls mergo, but we need to make sure we use the right flags (and that we can test the right flags).
114
func combineDefiniteWorkspacePodWithTemplate(pod *corev1.Pod, template *corev1.Pod) error {
115
if template == nil {
116
return nil
117
}
118
if pod == nil {
119
return xerrors.Errorf("definite pod cannot be nil")
120
}
121
122
err := mergo.Merge(pod, template, mergo.WithAppendSlice, mergo.WithTransformers(&mergePodTransformer{}))
123
if err != nil {
124
return xerrors.Errorf("cannot merge workspace pod with template: %w", err)
125
}
126
127
return nil
128
}
129
130
// mergePodTransformer is a mergo transformer which facilitates merging of NodeAffinity and containers
131
type mergePodTransformer struct{}
132
133
func (*mergePodTransformer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error {
134
switch typ {
135
case reflect.TypeOf([]corev1.NodeSelectorTerm{}):
136
return mergeNodeAffinityMatchExpressions
137
case reflect.TypeOf([]corev1.Container{}):
138
return mergeContainer
139
case reflect.TypeOf(&corev1.Probe{}):
140
return mergeProbe
141
}
142
143
return nil
144
}
145
146
// mergeContainer merges cnotainers by name
147
func mergeContainer(dst, src reflect.Value) (err error) {
148
// working with reflection is tricky business - add a safety net here and recover if things go sideways
149
defer func() {
150
r := recover()
151
if er, ok := r.(error); r != nil && ok {
152
err = er
153
}
154
}()
155
156
if !dst.CanSet() || !src.CanSet() {
157
return nil
158
}
159
160
srcs := src.Interface().([]corev1.Container)
161
dsts := dst.Interface().([]corev1.Container)
162
163
for _, s := range srcs {
164
di := -1
165
for i, d := range dsts {
166
if d.Name == s.Name {
167
di = i
168
break
169
}
170
}
171
if di < 0 {
172
// We don't have a matching destination container to merge this src one into
173
continue
174
}
175
176
err = mergo.Merge(&dsts[di], s, mergo.WithAppendSlice, mergo.WithOverride, mergo.WithTransformers(&mergePodTransformer{}))
177
if err != nil {
178
return err
179
}
180
}
181
182
dst.Set(reflect.ValueOf(dsts))
183
return nil
184
}
185
186
// mergeNodeAffinityMatchExpressions ensures that NodeAffinityare AND'ed
187
func mergeNodeAffinityMatchExpressions(dst, src reflect.Value) (err error) {
188
// working with reflection is tricky business - add a safety net here and recover if things go sideways
189
defer func() {
190
r := recover()
191
if er, ok := r.(error); r != nil && ok {
192
err = er
193
}
194
}()
195
196
if !dst.CanSet() || !src.CanSet() {
197
return nil
198
}
199
200
srcs := src.Interface().([]corev1.NodeSelectorTerm)
201
dsts := dst.Interface().([]corev1.NodeSelectorTerm)
202
203
if len(dsts) > 1 {
204
// we only run this mechanism if it's clear where we merge into
205
return nil
206
}
207
if len(dsts) == 0 {
208
dsts = srcs
209
} else {
210
for _, term := range srcs {
211
dsts[0].MatchExpressions = append(dsts[0].MatchExpressions, term.MatchExpressions...)
212
}
213
}
214
dst.Set(reflect.ValueOf(dsts))
215
216
return nil
217
}
218
219
func mergeProbe(dst, src reflect.Value) (err error) {
220
// working with reflection is tricky business - add a safety net here and recover if things go sideways
221
defer func() {
222
r := recover()
223
if er, ok := r.(error); r != nil && ok {
224
err = er
225
}
226
}()
227
228
srcs := src.Interface().(*corev1.Probe)
229
dsts := dst.Interface().(*corev1.Probe)
230
231
if dsts != nil && srcs == nil {
232
// don't overwrite with nil
233
} else if dsts == nil && srcs != nil {
234
// we don't have anything at dst yet - take the whole src
235
*dsts = *srcs
236
} else {
237
dsts.HTTPGet = srcs.HTTPGet
238
dsts.Exec = srcs.Exec
239
dsts.TCPSocket = srcs.TCPSocket
240
}
241
242
// *srcs = *dsts
243
return nil
244
}
245
246
// createDefiniteWorkspacePod creates a workspace pod without regard for any template.
247
// The result of this function can be deployed and it would work.
248
func createDefiniteWorkspacePod(sctx *startWorkspaceContext) (*corev1.Pod, error) {
249
workspaceContainer, err := createWorkspaceContainer(sctx)
250
if err != nil {
251
return nil, xerrors.Errorf("cannot create workspace container: %w", err)
252
}
253
254
// Beware: this allows setuid binaries in the workspace - supervisor needs to set no_new_privs now.
255
// However: the whole user workload now runs in a user namespace, which makes this acceptable.
256
workspaceContainer.SecurityContext.AllowPrivilegeEscalation = pointer.Bool(true)
257
258
workspaceVolume, err := createWorkspaceVolumes(sctx)
259
if err != nil {
260
return nil, xerrors.Errorf("cannot create workspace volumes: %w", err)
261
}
262
263
labels := make(map[string]string)
264
labels["gitpod.io/networkpolicy"] = "default"
265
for k, v := range sctx.Labels {
266
labels[k] = v
267
}
268
269
var prefix string
270
switch sctx.Workspace.Spec.Type {
271
case workspacev1.WorkspaceTypePrebuild:
272
prefix = "prebuild"
273
case workspacev1.WorkspaceTypeImageBuild:
274
prefix = "imagebuild"
275
default:
276
prefix = "ws"
277
}
278
279
annotations := map[string]string{
280
"prometheus.io/scrape": "true",
281
"prometheus.io/path": "/metrics",
282
"prometheus.io/port": strconv.Itoa(int(sctx.IDEPort)),
283
// prevent cluster-autoscaler from removing a node
284
// https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/FAQ.md#what-types-of-pods-can-prevent-ca-from-removing-a-node
285
"cluster-autoscaler.kubernetes.io/safe-to-evict": "false",
286
}
287
288
configureAppamor(sctx, annotations, workspaceContainer)
289
290
for k, v := range sctx.Workspace.Annotations {
291
annotations[k] = v
292
}
293
294
// By default we embue our workspace pods with some tolerance towards pressure taints,
295
// see https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/#taint-based-evictions
296
// for more details. As hope/assume that the pressure might go away in this time.
297
// Memory and Disk pressure are no reason to stop a workspace - instead of stopping a workspace
298
// we'd rather wait things out or gracefully fail the workspace ourselves.
299
var perssureToleranceSeconds int64 = 30
300
301
// Mounting /dev/net/tun should be fine security-wise, because:
302
// - the TAP driver documentation says so (see https://www.kernel.org/doc/Documentation/networking/tuntap.txt)
303
// - systemd's nspawn does the same thing (if it's good enough for them, it's good enough for us)
304
var (
305
hostPathOrCreate = corev1.HostPathDirectoryOrCreate
306
daemonVolumeName = "daemon-mount"
307
)
308
volumes := []corev1.Volume{
309
workspaceVolume,
310
{
311
Name: daemonVolumeName,
312
VolumeSource: corev1.VolumeSource{
313
HostPath: &corev1.HostPathVolumeSource{
314
Path: filepath.Join(sctx.Config.WorkspaceHostPath, sctx.Workspace.Name+"-daemon"),
315
Type: &hostPathOrCreate,
316
},
317
},
318
},
319
}
320
321
if sctx.Config.EnableCustomSSLCertificate {
322
volumes = append(volumes, corev1.Volume{
323
Name: "gitpod-ca-crt",
324
VolumeSource: corev1.VolumeSource{
325
ConfigMap: &corev1.ConfigMapVolumeSource{
326
LocalObjectReference: corev1.LocalObjectReference{Name: "gitpod-customer-certificate-bundle"},
327
},
328
},
329
})
330
}
331
332
workloadType := "regular"
333
if sctx.Headless {
334
workloadType = "headless"
335
}
336
337
matchExpressions := []corev1.NodeSelectorRequirement{
338
{
339
Key: "gitpod.io/workload_workspace_" + workloadType,
340
Operator: corev1.NodeSelectorOpExists,
341
},
342
{
343
Key: "gitpod.io/ws-daemon_ready_ns_" + sctx.Config.Namespace,
344
Operator: corev1.NodeSelectorOpExists,
345
},
346
{
347
Key: "gitpod.io/registry-facade_ready_ns_" + sctx.Config.Namespace,
348
Operator: corev1.NodeSelectorOpExists,
349
},
350
}
351
352
affinity := &corev1.Affinity{
353
NodeAffinity: &corev1.NodeAffinity{
354
RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
355
NodeSelectorTerms: []corev1.NodeSelectorTerm{
356
{
357
MatchExpressions: matchExpressions,
358
},
359
},
360
},
361
},
362
}
363
364
graceSec := int64(gracePeriod.Seconds())
365
pod := corev1.Pod{
366
ObjectMeta: metav1.ObjectMeta{
367
Name: fmt.Sprintf("%s-%s", prefix, sctx.Workspace.Name),
368
Namespace: sctx.Config.Namespace,
369
Labels: labels,
370
Annotations: annotations,
371
Finalizers: []string{workspacev1.GitpodFinalizerName},
372
},
373
Spec: corev1.PodSpec{
374
Hostname: sctx.Workspace.Spec.Ownership.WorkspaceID,
375
AutomountServiceAccountToken: pointer.Bool(false),
376
ServiceAccountName: "workspace",
377
SchedulerName: sctx.Config.SchedulerName,
378
EnableServiceLinks: pointer.Bool(false),
379
Affinity: affinity,
380
SecurityContext: &corev1.PodSecurityContext{
381
// We're using a custom seccomp profile for user namespaces to allow clone, mount and chroot.
382
SeccompProfile: &corev1.SeccompProfile{
383
Type: corev1.SeccompProfileTypeLocalhost,
384
LocalhostProfile: pointer.String(sctx.Config.SeccompProfile),
385
},
386
},
387
Containers: []corev1.Container{
388
*workspaceContainer,
389
},
390
RestartPolicy: corev1.RestartPolicyNever,
391
Volumes: volumes,
392
TerminationGracePeriodSeconds: &graceSec,
393
Tolerations: []corev1.Toleration{
394
{
395
Key: "node.kubernetes.io/disk-pressure",
396
Operator: "Exists",
397
Effect: "NoExecute",
398
// Tolarate Indefinitely
399
},
400
{
401
Key: "node.kubernetes.io/memory-pressure",
402
Operator: "Exists",
403
Effect: "NoExecute",
404
// Tolarate Indefinitely
405
},
406
{
407
Key: "node.kubernetes.io/network-unavailable",
408
Operator: "Exists",
409
Effect: "NoExecute",
410
TolerationSeconds: &perssureToleranceSeconds,
411
},
412
},
413
},
414
}
415
416
return &pod, nil
417
}
418
419
func createWorkspaceContainer(sctx *startWorkspaceContext) (*corev1.Container, error) {
420
class, ok := sctx.Config.WorkspaceClasses[sctx.Workspace.Spec.Class]
421
if !ok {
422
return nil, xerrors.Errorf("unknown workspace class: %s", sctx.Workspace.Spec.Class)
423
}
424
425
limits, err := class.Container.Limits.ResourceList()
426
if err != nil {
427
return nil, xerrors.Errorf("cannot parse workspace container limits: %w", err)
428
}
429
requests, err := class.Container.Requests.ResourceList()
430
if err != nil {
431
return nil, xerrors.Errorf("cannot parse workspace container requests: %w", err)
432
}
433
env, err := createWorkspaceEnvironment(sctx)
434
if err != nil {
435
return nil, xerrors.Errorf("cannot create workspace env: %w", err)
436
}
437
sec, err := createDefaultSecurityContext()
438
if err != nil {
439
return nil, xerrors.Errorf("cannot create Theia env: %w", err)
440
}
441
mountPropagation := corev1.MountPropagationHostToContainer
442
443
var (
444
command = []string{"/.supervisor/workspacekit", "ring0"}
445
readinessProbe = &corev1.Probe{
446
ProbeHandler: corev1.ProbeHandler{
447
HTTPGet: &corev1.HTTPGetAction{
448
Path: "/_supervisor/v1/status/ide/wait/true",
449
Port: intstr.FromInt((int)(sctx.SupervisorPort)),
450
Scheme: corev1.URISchemeHTTP,
451
},
452
},
453
// We make the readiness probe more difficult to fail than the liveness probe.
454
// This way, if the workspace really has a problem it will be shut down by Kubernetes rather than end up in
455
// some undefined state.
456
FailureThreshold: 600,
457
PeriodSeconds: 1,
458
SuccessThreshold: 1,
459
TimeoutSeconds: 1,
460
InitialDelaySeconds: 1,
461
}
462
)
463
464
image := fmt.Sprintf("%s/%s/%s", sctx.Config.RegistryFacadeHost, regapi.ProviderPrefixRemote, sctx.Workspace.Name)
465
466
volumeMounts := []corev1.VolumeMount{
467
{
468
Name: workspaceVolumeName,
469
MountPath: workspaceDir,
470
ReadOnly: false,
471
MountPropagation: &mountPropagation,
472
},
473
{
474
MountPath: "/.workspace",
475
Name: "daemon-mount",
476
MountPropagation: &mountPropagation,
477
},
478
}
479
480
if sctx.Config.EnableCustomSSLCertificate {
481
volumeMounts = append(volumeMounts, corev1.VolumeMount{
482
Name: "gitpod-ca-crt",
483
MountPath: "/etc/ssl/certs/gitpod-ca.crt",
484
SubPath: "ca-certificates.crt",
485
ReadOnly: true,
486
})
487
}
488
489
return &corev1.Container{
490
Name: "workspace",
491
Image: image,
492
SecurityContext: sec,
493
ImagePullPolicy: corev1.PullIfNotPresent,
494
Ports: []corev1.ContainerPort{
495
{ContainerPort: sctx.IDEPort},
496
},
497
Resources: corev1.ResourceRequirements{
498
Limits: limits,
499
Requests: requests,
500
},
501
VolumeMounts: volumeMounts,
502
ReadinessProbe: readinessProbe,
503
Env: env,
504
Command: command,
505
TerminationMessagePolicy: corev1.TerminationMessageReadFile,
506
}, nil
507
}
508
509
func createWorkspaceEnvironment(sctx *startWorkspaceContext) ([]corev1.EnvVar, error) {
510
class, ok := sctx.Config.WorkspaceClasses[sctx.Workspace.Spec.Class]
511
if !ok {
512
return nil, xerrors.Errorf("unknown workspace class: %s", sctx.Workspace.Spec.Class)
513
}
514
515
getWorkspaceRelativePath := func(segment string) string {
516
// ensure we do not produce nested paths for the default workspace location
517
return filepath.Join("/workspace", strings.TrimPrefix(segment, "/workspace"))
518
}
519
520
var init csapi.WorkspaceInitializer
521
err := proto.Unmarshal(sctx.Workspace.Spec.Initializer, &init)
522
if err != nil {
523
err = fmt.Errorf("cannot unmarshal initializer config: %w", err)
524
return nil, err
525
}
526
527
allRepoRoots := csapi.GetCheckoutLocationsFromInitializer(&init)
528
if len(allRepoRoots) == 0 {
529
allRepoRoots = []string{""} // for backward compatibility, we are adding a single empty location (translates to /workspace/)
530
}
531
for i, root := range allRepoRoots {
532
allRepoRoots[i] = getWorkspaceRelativePath(root)
533
}
534
535
// Can't read the workspace URL from status yet, as the status likely hasn't
536
// been set by the controller yet at this point. Therefore, manually construct
537
// the URL to pass to the container env.
538
wsUrl, err := config.RenderWorkspaceURL(sctx.Config.WorkspaceURLTemplate, sctx.Workspace.Name, sctx.Workspace.Spec.Ownership.WorkspaceID, sctx.Config.GitpodHostURL)
539
if err != nil {
540
return nil, fmt.Errorf("cannot render workspace URL: %w", err)
541
}
542
543
// Envs that start with GITPOD_ are appended to the Terminal environments
544
result := []corev1.EnvVar{}
545
result = append(result, corev1.EnvVar{Name: "GITPOD_REPO_ROOT", Value: allRepoRoots[0]})
546
result = append(result, corev1.EnvVar{Name: "GITPOD_REPO_ROOTS", Value: strings.Join(allRepoRoots, ",")})
547
result = append(result, corev1.EnvVar{Name: "GITPOD_OWNER_ID", Value: sctx.Workspace.Spec.Ownership.Owner})
548
result = append(result, corev1.EnvVar{Name: "GITPOD_WORKSPACE_ID", Value: sctx.Workspace.Spec.Ownership.WorkspaceID})
549
result = append(result, corev1.EnvVar{Name: "GITPOD_INSTANCE_ID", Value: sctx.Workspace.Name})
550
result = append(result, corev1.EnvVar{Name: "GITPOD_THEIA_PORT", Value: strconv.Itoa(int(sctx.IDEPort))})
551
result = append(result, corev1.EnvVar{Name: "THEIA_WORKSPACE_ROOT", Value: getWorkspaceRelativePath(sctx.Workspace.Spec.WorkspaceLocation)})
552
result = append(result, corev1.EnvVar{Name: "GITPOD_HOST", Value: sctx.Config.GitpodHostURL})
553
result = append(result, corev1.EnvVar{Name: "GITPOD_WORKSPACE_URL", Value: wsUrl})
554
result = append(result, corev1.EnvVar{Name: "GITPOD_WORKSPACE_CLUSTER_HOST", Value: sctx.Config.WorkspaceClusterHost})
555
result = append(result, corev1.EnvVar{Name: "GITPOD_WORKSPACE_CLASS", Value: sctx.Workspace.Spec.Class})
556
result = append(result, corev1.EnvVar{Name: "THEIA_SUPERVISOR_ENDPOINT", Value: fmt.Sprintf(":%d", sctx.SupervisorPort)})
557
// TODO(ak) remove THEIA_WEBVIEW_EXTERNAL_ENDPOINT and THEIA_MINI_BROWSER_HOST_PATTERN when Theia is removed
558
result = append(result, corev1.EnvVar{Name: "THEIA_WEBVIEW_EXTERNAL_ENDPOINT", Value: "webview-{{hostname}}"})
559
result = append(result, corev1.EnvVar{Name: "THEIA_MINI_BROWSER_HOST_PATTERN", Value: "browser-{{hostname}}"})
560
561
result = append(result, corev1.EnvVar{Name: "GITPOD_SSH_CA_PUBLIC_KEY", Value: sctx.Workspace.Spec.SSHGatewayCAPublicKey})
562
563
// We don't require that Git be configured for workspaces
564
if sctx.Workspace.Spec.Git != nil {
565
result = append(result, corev1.EnvVar{Name: "GITPOD_GIT_USER_NAME", Value: sctx.Workspace.Spec.Git.Username})
566
result = append(result, corev1.EnvVar{Name: "GITPOD_GIT_USER_EMAIL", Value: sctx.Workspace.Spec.Git.Email})
567
}
568
569
if sctx.Config.EnableCustomSSLCertificate {
570
const (
571
customCAMountPath = "/etc/ssl/certs/gitpod-ca.crt"
572
certsMountPath = "/etc/ssl/certs/"
573
)
574
575
result = append(result, corev1.EnvVar{Name: "NODE_EXTRA_CA_CERTS", Value: customCAMountPath})
576
result = append(result, corev1.EnvVar{Name: "GIT_SSL_CAPATH", Value: certsMountPath})
577
result = append(result, corev1.EnvVar{Name: "GIT_SSL_CAINFO", Value: customCAMountPath})
578
}
579
580
// System level env vars
581
for _, e := range sctx.Workspace.Spec.SysEnvVars {
582
env := corev1.EnvVar{
583
Name: e.Name,
584
Value: e.Value,
585
}
586
result = append(result, env)
587
}
588
589
// User-defined env vars (i.e. those coming from the request)
590
for _, e := range sctx.Workspace.Spec.UserEnvVars {
591
switch e.Name {
592
case "GITPOD_WORKSPACE_CONTEXT",
593
"GITPOD_WORKSPACE_CONTEXT_URL",
594
"GITPOD_TASKS",
595
"GITPOD_RESOLVED_EXTENSIONS",
596
"GITPOD_EXTERNAL_EXTENSIONS",
597
"GITPOD_WORKSPACE_CLASS_INFO",
598
"GITPOD_IDE_ALIAS",
599
"GITPOD_RLIMIT_CORE",
600
"GITPOD_IMAGE_AUTH":
601
// these variables are allowed - don't skip them
602
default:
603
if strings.HasPrefix(e.Name, "GITPOD_") {
604
// we don't allow env vars starting with GITPOD_ and those that we do allow we've listed above
605
continue
606
}
607
}
608
609
result = append(result, e)
610
}
611
612
heartbeatInterval := time.Duration(sctx.Config.HeartbeatInterval)
613
result = append(result, corev1.EnvVar{Name: "GITPOD_INTERVAL", Value: fmt.Sprintf("%d", int64(heartbeatInterval/time.Millisecond))})
614
615
res, err := class.Container.Requests.ResourceList()
616
if err != nil {
617
return nil, xerrors.Errorf("cannot create environment: %w", err)
618
}
619
memoryInMegabyte := res.Memory().Value() / (1024 * 1024)
620
result = append(result, corev1.EnvVar{Name: "GITPOD_MEMORY", Value: strconv.FormatInt(memoryInMegabyte, 10)})
621
622
cpuCount := res.Cpu().Value()
623
result = append(result, corev1.EnvVar{Name: "GITPOD_CPU_COUNT", Value: strconv.FormatInt(int64(cpuCount), 10)})
624
625
if sctx.Headless {
626
result = append(result, corev1.EnvVar{Name: "GITPOD_HEADLESS", Value: "true"})
627
}
628
629
// remove empty env vars
630
cleanResult := make([]corev1.EnvVar, 0)
631
for _, v := range result {
632
if v.Name == "" || (v.Value == "" && v.ValueFrom == nil) {
633
continue
634
}
635
636
cleanResult = append(cleanResult, v)
637
}
638
639
return cleanResult, nil
640
}
641
642
func createWorkspaceVolumes(sctx *startWorkspaceContext) (workspace corev1.Volume, err error) {
643
// silly protobuf structure design - this needs to be a reference to a string,
644
// so we have to assign it to a variable first to take the address
645
hostPathOrCreate := corev1.HostPathDirectoryOrCreate
646
647
workspace = corev1.Volume{
648
Name: workspaceVolumeName,
649
VolumeSource: corev1.VolumeSource{
650
HostPath: &corev1.HostPathVolumeSource{
651
Path: filepath.Join(sctx.Config.WorkspaceHostPath, sctx.Workspace.Name),
652
Type: &hostPathOrCreate,
653
},
654
},
655
}
656
657
err = nil
658
return
659
}
660
661
func createDefaultSecurityContext() (*corev1.SecurityContext, error) {
662
gitpodGUID := int64(33333)
663
664
res := &corev1.SecurityContext{
665
AllowPrivilegeEscalation: pointer.Bool(false),
666
Capabilities: &corev1.Capabilities{
667
Add: []corev1.Capability{
668
"AUDIT_WRITE", // Write records to kernel auditing log.
669
"FSETID", // Don’t clear set-user-ID and set-group-ID permission bits when a file is modified.
670
"KILL", // Bypass permission checks for sending signals.
671
"NET_BIND_SERVICE", // Bind a socket to internet domain privileged ports (port numbers less than 1024).
672
"SYS_PTRACE", // Trace arbitrary processes using ptrace(2).
673
},
674
Drop: []corev1.Capability{
675
"SETPCAP", // Modify process capabilities.
676
"CHOWN", // Make arbitrary changes to file UIDs and GIDs (see chown(2)).
677
"NET_RAW", // Use RAW and PACKET sockets.
678
"DAC_OVERRIDE", // Bypass file read, write, and execute permission checks.
679
"FOWNER", // Bypass permission checks on operations that normally require the file system UID of the process to match the UID of the file.
680
"SYS_CHROOT", // Use chroot(2), change root directory.
681
"SETFCAP", // Set file capabilities.
682
"SETUID", // Make arbitrary manipulations of process UIDs.
683
"SETGID", // Make arbitrary manipulations of process GIDs and supplementary GID list.
684
},
685
},
686
Privileged: pointer.Bool(false),
687
ReadOnlyRootFilesystem: pointer.Bool(false),
688
RunAsGroup: &gitpodGUID,
689
RunAsNonRoot: pointer.Bool(true),
690
RunAsUser: &gitpodGUID,
691
}
692
693
return res, nil
694
}
695
696
func newStartWorkspaceContext(ctx context.Context, cfg *config.Configuration, ws *workspacev1.Workspace, serverVersion *version.Info) (res *startWorkspaceContext, err error) {
697
// we deliberately do not shadow ctx here as we need the original context later to extract the TraceID
698
span, _ := tracing.FromContext(ctx, "newStartWorkspaceContext")
699
defer tracing.FinishSpan(span, &err)
700
701
return &startWorkspaceContext{
702
Labels: map[string]string{
703
"app": "gitpod",
704
"component": "workspace",
705
wsk8s.MetaIDLabel: ws.Spec.Ownership.WorkspaceID,
706
wsk8s.WorkspaceIDLabel: ws.Name,
707
wsk8s.OwnerLabel: ws.Spec.Ownership.Owner,
708
wsk8s.TypeLabel: strings.ToLower(string(ws.Spec.Type)),
709
wsk8s.WorkspaceManagedByLabel: constants.ManagedBy,
710
instanceIDLabel: ws.Name,
711
headlessLabel: strconv.FormatBool(ws.IsHeadless()),
712
},
713
Config: cfg,
714
Workspace: ws,
715
IDEPort: 23000,
716
SupervisorPort: 22999,
717
Headless: ws.IsHeadless(),
718
ServerVersion: serverVersion,
719
}, nil
720
}
721
722
func configureAppamor(sctx *startWorkspaceContext, annotations map[string]string, workspaceContainer *corev1.Container) {
723
// pre K8s 1.30 we need to set the apparmor profile to unconfined as an annotation
724
if sctx.ServerVersion.Major <= "1" && sctx.ServerVersion.Minor < "30" {
725
annotations["container.apparmor.security.beta.kubernetes.io/workspace"] = "unconfined"
726
} else {
727
workspaceContainer.SecurityContext.AppArmorProfile = &corev1.AppArmorProfile{
728
Type: corev1.AppArmorProfileTypeUnconfined,
729
}
730
}
731
}
732
733
// validCookieChars contains all characters which may occur in an HTTP Cookie value (unicode \u0021 through \u007E),
734
// without the characters , ; and / ... I did not find more details about permissible characters in RFC2965, so I took
735
// this list of permissible chars from Wikipedia.
736
//
737
// The tokens we produce here (e.g. owner token or CLI API token) are likely placed in cookies or transmitted via HTTP.
738
// To make the lifes of downstream users easier we'll try and play nice here w.r.t. to the characters used.
739
var validCookieChars = []byte("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_-.")
740
741
func getRandomString(length int) (string, error) {
742
b := make([]byte, length)
743
n, err := rand.Read(b)
744
if err != nil {
745
return "", err
746
}
747
if n != length {
748
return "", io.ErrShortWrite
749
}
750
751
lrsc := len(validCookieChars)
752
for i, c := range b {
753
b[i] = validCookieChars[int(c)%lrsc]
754
}
755
return string(b), nil
756
}
757
758