Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/ws-manager-api/go/config/config.go
2500 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 config
6
7
import (
8
"bytes"
9
"html/template"
10
iofs "io/fs"
11
"net/url"
12
"os"
13
"path/filepath"
14
"time"
15
16
ozzo "github.com/go-ozzo/ozzo-validation"
17
"github.com/go-ozzo/ozzo-validation/is"
18
"golang.org/x/xerrors"
19
corev1 "k8s.io/api/core/v1"
20
"k8s.io/apimachinery/pkg/api/resource"
21
"k8s.io/apimachinery/pkg/util/validation"
22
"k8s.io/apimachinery/pkg/util/yaml"
23
24
"github.com/gitpod-io/gitpod/common-go/grpc"
25
"github.com/gitpod-io/gitpod/common-go/util"
26
cntntcfg "github.com/gitpod-io/gitpod/content-service/api/config"
27
)
28
29
// DefaultWorkspaceClass is the name of the default workspace class
30
const DefaultWorkspaceClass = "g1-standard"
31
32
type osFS struct{}
33
34
func (*osFS) Open(name string) (iofs.File, error) {
35
return os.Open(name)
36
}
37
38
// FS is used to load files referred to by this configuration.
39
// We use a library here to be able to test things properly.
40
var FS iofs.FS = &osFS{}
41
42
// ServiceConfiguration configures the ws-manager configuration
43
type ServiceConfiguration struct {
44
Manager Configuration `json:"manager"`
45
Content struct {
46
Storage cntntcfg.StorageConfig `json:"storage"`
47
} `json:"content"`
48
RPCServer struct {
49
Addr string `json:"addr"`
50
TLS struct {
51
CA string `json:"ca"`
52
Certificate string `json:"crt"`
53
PrivateKey string `json:"key"`
54
} `json:"tls"`
55
RateLimits map[string]grpc.RateLimit `json:"ratelimits"`
56
} `json:"rpcServer"`
57
ImageBuilderProxy struct {
58
TargetAddr string `json:"targetAddr"`
59
TLS struct {
60
CA string `json:"ca"`
61
Certificate string `json:"crt"`
62
PrivateKey string `json:"key"`
63
} `json:"tls"`
64
} `json:"imageBuilderProxy"`
65
66
PProf struct {
67
Addr string `json:"addr"`
68
} `json:"pprof"`
69
Prometheus struct {
70
Addr string `json:"addr"`
71
} `json:"prometheus"`
72
Health struct {
73
Addr string `json:"addr"`
74
} `json:"health"`
75
}
76
77
// Configuration is the configuration of the ws-manager
78
type Configuration struct {
79
// Namespace is the kubernetes namespace the workspace manager operates in
80
Namespace string `json:"namespace"`
81
// SecretsNamespace is the kubernetes namespace which contains workspace secrets
82
SecretsNamespace string `json:"secretsNamespace"`
83
// SchedulerName is the name of the workspace scheduler all pods are created with
84
SchedulerName string `json:"schedulerName"`
85
// SeccompProfile names the seccomp profile workspaces will use
86
SeccompProfile string `json:"seccompProfile"`
87
// Timeouts configures how long workspaces can be without activity before they're shut down.
88
// All values in here must be valid time.Duration
89
Timeouts WorkspaceTimeoutConfiguration `json:"timeouts"`
90
// InitProbe configures the ready-probe of workspaces which signal when the initialization is finished
91
InitProbe InitProbeConfiguration `json:"initProbe"`
92
// WorkspaceURLTemplate is a Go template which resolves to the external URL of the
93
// workspace. Available fields are:
94
// - `ID` which is the workspace ID,
95
// - `Prefix` which is the workspace's service prefix
96
// - `Host` which is the GitpodHostURL
97
WorkspaceURLTemplate string `json:"urlTemplate"`
98
// WorkspaceURLTemplate is a Go template which resolves to the external URL of the
99
// workspace port. Available fields are:
100
// - `ID` which is the workspace ID,
101
// - `Prefix` which is the workspace's service prefix
102
// - `Host` which is the GitpodHostURL
103
// - `WorkspacePort` which is the workspace port
104
// - `IngressPort` which is the publicly accessile port
105
WorkspacePortURLTemplate string `json:"portUrlTemplate"`
106
// HostPath is the path on the node where workspace data resides (ideally this is an SSD)
107
WorkspaceHostPath string `json:"workspaceHostPath"`
108
// HeartbeatInterval is the time in seconds in which Theia sends a heartbeat if the user is active
109
HeartbeatInterval util.Duration `json:"heartbeatInterval"`
110
// Is the URL under which Gitpod is installed (e.g. https://gitpod.io)
111
GitpodHostURL string `json:"hostURL"`
112
// EventTraceLog is a path to file where we'll write the monitor event trace log to
113
EventTraceLog string `json:"eventTraceLog,omitempty"`
114
// ReconnectionInterval configures the time we wait until we reconnect to the various other services
115
ReconnectionInterval util.Duration `json:"reconnectionInterval"`
116
// MaintenanceMode prevents start workspace, stop workspace, and take snapshot operations
117
MaintenanceMode bool `json:"maintenanceMode,omitempty"`
118
// WorkspaceDaemon configures our connection to the workspace sync daemons runnin on the nodes
119
WorkspaceDaemon WorkspaceDaemonConfiguration `json:"wsdaemon"`
120
// RegistryFacadeHost is the host (possibly including port) on which the registry facade resolves
121
RegistryFacadeHost string `json:"registryFacadeHost"`
122
// Cluster host under which workspaces are served, e.g. ws-eu11.gitpod.io
123
WorkspaceClusterHost string `json:"workspaceClusterHost"`
124
// WorkspaceClasses provide different resource classes for workspaces
125
WorkspaceClasses map[string]*WorkspaceClass `json:"workspaceClass"`
126
// PreferredWorkspaceClass is the name of the workspace class that should be used by default
127
PreferredWorkspaceClass string `json:"preferredWorkspaceClass"`
128
// DebugWorkspacePod adds extra finalizer to workspace to prevent it from shutting down. Helps to debug.
129
DebugWorkspacePod bool `json:"debugWorkspacePod,omitempty"`
130
// WorkspaceMaxConcurrentReconciles configures the max amount of concurrent workspace reconciliations on
131
// the workspace controller.
132
WorkspaceMaxConcurrentReconciles int `json:"workspaceMaxConcurrentReconciles,omitempty"`
133
// TimeoutMaxConcurrentReconciles configures the max amount of concurrent workspace reconciliations on
134
// the timeout controller.
135
TimeoutMaxConcurrentReconciles int `json:"timeoutMaxConcurrentReconciles,omitempty"`
136
// EnableCustomSSLCertificate controls if we need to support custom SSL certificates for git operations
137
EnableCustomSSLCertificate bool `json:"enableCustomSSLCertificate"`
138
// WorkspacekitImage points to the default workspacekit image
139
WorkspacekitImage string `json:"workspacekitImage,omitempty"`
140
141
SSHGatewayCAPublicKeyFile string `json:"sshGatewayCAPublicKeyFile,omitempty"`
142
143
// SSHGatewayCAPublicKey is a CA public key
144
SSHGatewayCAPublicKey string
145
146
// PodRecreationMaxRetries
147
PodRecreationMaxRetries int `json:"podRecreationMaxRetries,omitempty"`
148
// PodRecreationBackoff
149
PodRecreationBackoff util.Duration `json:"podRecreationBackoff,omitempty"`
150
}
151
152
type WorkspaceClass struct {
153
Name string `json:"name"`
154
Description string `json:"description"`
155
Container ContainerConfiguration `json:"container"`
156
Templates WorkspacePodTemplateConfiguration `json:"templates"`
157
158
// CreditsPerMinute is the cost per minute for this workspace class in credits
159
CreditsPerMinute float32 `json:"creditsPerMinute"`
160
}
161
162
// WorkspaceTimeoutConfiguration configures the timeout behaviour of workspaces
163
type WorkspaceTimeoutConfiguration struct {
164
// TotalStartup is the total time a workspace can take until we expect the first activity
165
TotalStartup util.Duration `json:"startup"`
166
// Initialization is the time the initialization phase alone can take
167
Initialization util.Duration `json:"initialization"`
168
// RegularWorkspace is the time a regular workspace can be without activity before it's shutdown
169
RegularWorkspace util.Duration `json:"regularWorkspace"`
170
// MaxLifetime is the maximum lifetime of a regular workspace
171
MaxLifetime util.Duration `json:"maxLifetime"`
172
// HeadlessWorkspace is the maximum runtime a headless workspace can have (including startup)
173
HeadlessWorkspace util.Duration `json:"headlessWorkspace"`
174
// AfterClose is the time a workspace lives after it has been marked closed
175
AfterClose util.Duration `json:"afterClose"`
176
// ContentFinalization is the time in which the workspace's content needs to be backed up and removed from the node
177
ContentFinalization util.Duration `json:"contentFinalization"`
178
// Stopping is the time a workspace has until it has to be stopped. This time includes finalization, hence must be greater than
179
// the ContentFinalization timeout.
180
Stopping util.Duration `json:"stopping"`
181
// Interrupted is the time a workspace may be interrupted (since it last saw activity or since it was created if it never saw any)
182
Interrupted util.Duration `json:"interrupted"`
183
}
184
185
// InitProbeConfiguration configures the behaviour of the workspace ready probe
186
type InitProbeConfiguration struct {
187
// Disabled disables the workspace init probe - this is only neccesary during tests and in noDomain environments.
188
Disabled bool `json:"disabled,omitempty"`
189
190
// Timeout is the HTTP GET timeout during each probe attempt. Defaults to 5 seconds.
191
Timeout string `json:"timeout,omitempty"`
192
}
193
194
// WorkspacePodTemplateConfiguration configures the paths to workspace pod templates
195
type WorkspacePodTemplateConfiguration struct {
196
// DefaultPath is a path to a workspace pod template YAML file that's used for
197
// all workspaces irregardles of their type. If a type-specific template is configured
198
// as well, that template is merged in, too.
199
DefaultPath string `json:"defaultPath,omitempty"`
200
// RegularPath is a path to an additional workspace pod template YAML file for regular workspaces
201
RegularPath string `json:"regularPath,omitempty"`
202
// PrebuildPath is a path to an additional workspace pod template YAML file for prebuild workspaces
203
PrebuildPath string `json:"prebuildPath,omitempty"`
204
// ProbePath is a path to an additional workspace pod template YAML file for probe workspaces
205
// Deprecated
206
ProbePath string `json:"probePath,omitempty"`
207
// ImagebuildPath is a path to an additional workspace pod template YAML file for imagebuild workspaces
208
ImagebuildPath string `json:"imagebuildPath,omitempty"`
209
}
210
211
// WorkspaceDaemonConfiguration configures our connection to the workspace sync daemons runnin on the nodes
212
type WorkspaceDaemonConfiguration struct {
213
// Port is the port on the node on which the ws-daemon is listening
214
Port int `json:"port"`
215
// TLS is the certificate/key config to connect to ws-daemon
216
TLS struct {
217
// Authority is the root certificate that was used to sign the certificate itself
218
Authority string `json:"ca"`
219
// Certificate is the crt file, the actual certificate
220
Certificate string `json:"crt"`
221
// PrivateKey is the private key in order to use the certificate
222
PrivateKey string `json:"key"`
223
} `json:"tls"`
224
}
225
226
// Validate validates the configuration to catch issues during startup and not at runtime
227
func (c *Configuration) Validate() error {
228
err := ozzo.ValidateStruct(&c.Timeouts,
229
ozzo.Field(&c.Timeouts.AfterClose, ozzo.Required),
230
ozzo.Field(&c.Timeouts.HeadlessWorkspace, ozzo.Required),
231
ozzo.Field(&c.Timeouts.Initialization, ozzo.Required),
232
ozzo.Field(&c.Timeouts.RegularWorkspace, ozzo.Required),
233
ozzo.Field(&c.Timeouts.MaxLifetime, ozzo.Required),
234
ozzo.Field(&c.Timeouts.TotalStartup, ozzo.Required),
235
ozzo.Field(&c.Timeouts.ContentFinalization, ozzo.Required),
236
ozzo.Field(&c.Timeouts.Stopping, ozzo.Required),
237
)
238
if err != nil {
239
return xerrors.Errorf("timeouts: %w", err)
240
}
241
if c.Timeouts.Stopping < c.Timeouts.ContentFinalization {
242
return xerrors.Errorf("stopping timeout must be greater than content finalization timeout")
243
}
244
245
err = ozzo.ValidateStruct(c,
246
ozzo.Field(&c.WorkspaceURLTemplate, ozzo.Required, validWorkspaceURLTemplate),
247
ozzo.Field(&c.WorkspaceHostPath, ozzo.Required),
248
ozzo.Field(&c.HeartbeatInterval, ozzo.Required),
249
ozzo.Field(&c.GitpodHostURL, ozzo.Required, is.URL),
250
ozzo.Field(&c.ReconnectionInterval, ozzo.Required),
251
)
252
if err != nil {
253
return err
254
}
255
256
if _, ok := c.WorkspaceClasses[DefaultWorkspaceClass]; !ok {
257
return xerrors.Errorf("missing default workspace class (\"%s\")", DefaultWorkspaceClass)
258
}
259
for name, class := range c.WorkspaceClasses {
260
if errs := validation.IsValidLabelValue(name); len(errs) > 0 {
261
return xerrors.Errorf("workspace class name \"%s\" is invalid: %v", name, errs)
262
}
263
if err := class.Container.Validate(); err != nil {
264
return xerrors.Errorf("workspace class %s: %w", name, err)
265
}
266
267
err = ozzo.ValidateStruct(&class.Templates,
268
ozzo.Field(&class.Templates.DefaultPath, validPodTemplate),
269
ozzo.Field(&class.Templates.PrebuildPath, validPodTemplate),
270
ozzo.Field(&class.Templates.ProbePath, validPodTemplate),
271
ozzo.Field(&class.Templates.RegularPath, validPodTemplate),
272
)
273
if err != nil {
274
return xerrors.Errorf("workspace class %s: %w", name, err)
275
}
276
}
277
278
return err
279
}
280
281
var validPodTemplate = ozzo.By(func(o interface{}) error {
282
s, ok := o.(string)
283
if !ok {
284
return xerrors.Errorf("field should be string")
285
}
286
287
_, err := GetWorkspacePodTemplate(s)
288
return err
289
})
290
291
var validWorkspaceURLTemplate = ozzo.By(func(o interface{}) error {
292
s, ok := o.(string)
293
if !ok {
294
return xerrors.Errorf("field should be string")
295
}
296
297
wsurl, err := RenderWorkspaceURL(s, "foo", "bar", "gitpod.io")
298
if err != nil {
299
return xerrors.Errorf("cannot render URL: %w", err)
300
}
301
_, err = url.Parse(wsurl)
302
if err != nil {
303
return xerrors.Errorf("not a valid URL: %w", err)
304
}
305
306
return err
307
})
308
309
// ContainerConfiguration configures properties of workspace pod container
310
type ContainerConfiguration struct {
311
Requests *ResourceRequestConfiguration `json:"requests,omitempty"`
312
Limits *ResourceLimitConfiguration `json:"limits,omitempty"`
313
}
314
315
// Validate validates a container configuration
316
func (c *ContainerConfiguration) Validate() error {
317
return ozzo.ValidateStruct(c,
318
ozzo.Field(&c.Requests, validResourceRequestConfig),
319
ozzo.Field(&c.Limits, validResourceLimitConfig),
320
)
321
}
322
323
var validResourceRequestConfig = ozzo.By(func(o interface{}) error {
324
rc, ok := o.(*ResourceRequestConfiguration)
325
if !ok {
326
return xerrors.Errorf("can only validate ResourceRequestConfiguration")
327
}
328
if rc == nil {
329
return nil
330
}
331
if rc.CPU != "" {
332
_, err := resource.ParseQuantity(rc.CPU)
333
if err != nil {
334
return xerrors.Errorf("cannot parse CPU quantity: %w", err)
335
}
336
}
337
if rc.Memory != "" {
338
_, err := resource.ParseQuantity(rc.Memory)
339
if err != nil {
340
return xerrors.Errorf("cannot parse Memory quantity: %w", err)
341
}
342
}
343
if rc.EphemeralStorage != "" {
344
_, err := resource.ParseQuantity(rc.EphemeralStorage)
345
if err != nil {
346
return xerrors.Errorf("cannot parse EphemeralStorage quantity: %w", err)
347
}
348
}
349
if rc.Storage != "" {
350
_, err := resource.ParseQuantity(rc.Storage)
351
if err != nil {
352
return xerrors.Errorf("cannot parse Storage quantity: %w", err)
353
}
354
}
355
return nil
356
})
357
358
var validResourceLimitConfig = ozzo.By(func(o interface{}) error {
359
rc, ok := o.(*ResourceLimitConfiguration)
360
if !ok {
361
return xerrors.Errorf("can only validate ResourceLimitConfiguration")
362
}
363
if rc == nil {
364
return nil
365
}
366
if rc.CPU.MinLimit != "" {
367
_, err := resource.ParseQuantity(rc.CPU.MinLimit)
368
if err != nil {
369
return xerrors.Errorf("cannot parse low limit CPU quantity: %w", err)
370
}
371
}
372
if rc.CPU.BurstLimit != "" {
373
_, err := resource.ParseQuantity(rc.CPU.BurstLimit)
374
if err != nil {
375
return xerrors.Errorf("cannot parse burst limit CPU quantity: %w", err)
376
}
377
}
378
if rc.Memory != "" {
379
_, err := resource.ParseQuantity(rc.Memory)
380
if err != nil {
381
return xerrors.Errorf("cannot parse Memory quantity: %w", err)
382
}
383
}
384
if rc.EphemeralStorage != "" {
385
_, err := resource.ParseQuantity(rc.EphemeralStorage)
386
if err != nil {
387
return xerrors.Errorf("cannot parse EphemeralStorage quantity: %w", err)
388
}
389
}
390
if rc.Storage != "" {
391
_, err := resource.ParseQuantity(rc.Storage)
392
if err != nil {
393
return xerrors.Errorf("cannot parse Storage quantity: %w", err)
394
}
395
}
396
return nil
397
})
398
399
func (r *ResourceRequestConfiguration) StorageQuantity() (resource.Quantity, error) {
400
if r.Storage == "" {
401
res := resource.NewQuantity(0, resource.BinarySI)
402
return *res, nil
403
}
404
return resource.ParseQuantity(r.Storage)
405
}
406
407
// ResourceList parses the quantities in the resource config
408
func (r *ResourceRequestConfiguration) ResourceList() (corev1.ResourceList, error) {
409
if r == nil {
410
return corev1.ResourceList{}, nil
411
}
412
res := map[corev1.ResourceName]string{
413
corev1.ResourceCPU: r.CPU,
414
corev1.ResourceMemory: r.Memory,
415
corev1.ResourceEphemeralStorage: r.EphemeralStorage,
416
}
417
418
var l = make(corev1.ResourceList)
419
for k, v := range res {
420
if v == "" {
421
continue
422
}
423
424
q, err := resource.ParseQuantity(v)
425
if err != nil {
426
return nil, xerrors.Errorf("%s: %w", k, err)
427
}
428
if q.Value() == 0 {
429
continue
430
}
431
432
l[k] = q
433
}
434
return l, nil
435
}
436
437
// GetWorkspacePodTemplate parses a pod template YAML file. Returns nil if path is empty.
438
func GetWorkspacePodTemplate(filename string) (*corev1.Pod, error) {
439
if filename == "" {
440
return nil, nil
441
}
442
443
tpr := os.Getenv("TELEPRESENCE_ROOT")
444
if tpr != "" {
445
filename = filepath.Join(tpr, filename)
446
}
447
448
tpl, err := FS.Open(filename)
449
if err != nil {
450
return nil, xerrors.Errorf("cannot read pod template: %w", err)
451
}
452
defer tpl.Close()
453
454
var res corev1.Pod
455
decoder := yaml.NewYAMLOrJSONDecoder(tpl, 4096)
456
err = decoder.Decode(&res)
457
if err != nil {
458
return nil, xerrors.Errorf("cannot unmarshal pod template: %w", err)
459
}
460
461
return &res, nil
462
}
463
464
// RenderWorkspaceURL takes a workspace URL template and renders it
465
func RenderWorkspaceURL(urltpl, id, servicePrefix, host string) (string, error) {
466
tpl, err := template.New("url").Parse(urltpl)
467
if err != nil {
468
return "", xerrors.Errorf("cannot compute workspace URL: %w", err)
469
}
470
471
type data struct {
472
ID string
473
Prefix string
474
Host string
475
}
476
d := data{
477
ID: id,
478
Prefix: servicePrefix,
479
Host: host,
480
}
481
482
var b bytes.Buffer
483
err = tpl.Execute(&b, d)
484
if err != nil {
485
return "", xerrors.Errorf("cannot compute workspace URL: %w", err)
486
}
487
488
return b.String(), nil
489
}
490
491
type PortURLContext struct {
492
ID string
493
Prefix string
494
Host string
495
WorkspacePort string
496
IngressPort string
497
}
498
499
// RenderWorkspacePortURL takes a workspace port URL template and renders it
500
func RenderWorkspacePortURL(urltpl string, ctx PortURLContext) (string, error) {
501
tpl, err := template.New("url").Parse(urltpl)
502
if err != nil {
503
return "", xerrors.Errorf("cannot compute workspace URL: %w", err)
504
}
505
506
var b bytes.Buffer
507
err = tpl.Execute(&b, ctx)
508
if err != nil {
509
return "", xerrors.Errorf("cannot compute workspace port URL: %w", err)
510
}
511
512
return b.String(), nil
513
}
514
515
// ResourceRequestConfiguration configures resources of a pod/container
516
type ResourceRequestConfiguration struct {
517
CPU string `json:"cpu"`
518
Memory string `json:"memory"`
519
EphemeralStorage string `json:"ephemeral-storage"`
520
Storage string `json:"storage,omitempty"`
521
}
522
523
type ResourceLimitConfiguration struct {
524
CPU *CpuResourceLimit `json:"cpu"`
525
Memory string `json:"memory"`
526
EphemeralStorage string `json:"ephemeral-storage"`
527
Storage string `json:"storage,omitempty"`
528
}
529
530
func (r *ResourceLimitConfiguration) ResourceList() (corev1.ResourceList, error) {
531
if r == nil {
532
return corev1.ResourceList{}, nil
533
}
534
res := map[corev1.ResourceName]string{
535
corev1.ResourceMemory: r.Memory,
536
corev1.ResourceEphemeralStorage: r.EphemeralStorage,
537
}
538
539
if r.CPU != nil {
540
res[corev1.ResourceCPU] = r.CPU.BurstLimit
541
}
542
543
var l = make(corev1.ResourceList)
544
for k, v := range res {
545
if v == "" {
546
continue
547
}
548
549
q, err := resource.ParseQuantity(v)
550
if err != nil {
551
return nil, xerrors.Errorf("%s: %w", k, err)
552
}
553
if q.Value() == 0 {
554
continue
555
}
556
557
l[k] = q
558
}
559
return l, nil
560
}
561
562
func (r *ResourceLimitConfiguration) StorageQuantity() (resource.Quantity, error) {
563
if r.Storage == "" {
564
res := resource.NewQuantity(0, resource.BinarySI)
565
return *res, nil
566
}
567
return resource.ParseQuantity(r.Storage)
568
}
569
570
type CpuResourceLimit struct {
571
MinLimit string `json:"min"`
572
BurstLimit string `json:"burst"`
573
}
574
575
type MaintenanceConfig struct {
576
EnabledUntil *time.Time `json:"enabledUntil"`
577
}
578
579