Path: blob/main/components/ws-manager-api/go/config/config.go
2500 views
// Copyright (c) 2020 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package config56import (7"bytes"8"html/template"9iofs "io/fs"10"net/url"11"os"12"path/filepath"13"time"1415ozzo "github.com/go-ozzo/ozzo-validation"16"github.com/go-ozzo/ozzo-validation/is"17"golang.org/x/xerrors"18corev1 "k8s.io/api/core/v1"19"k8s.io/apimachinery/pkg/api/resource"20"k8s.io/apimachinery/pkg/util/validation"21"k8s.io/apimachinery/pkg/util/yaml"2223"github.com/gitpod-io/gitpod/common-go/grpc"24"github.com/gitpod-io/gitpod/common-go/util"25cntntcfg "github.com/gitpod-io/gitpod/content-service/api/config"26)2728// DefaultWorkspaceClass is the name of the default workspace class29const DefaultWorkspaceClass = "g1-standard"3031type osFS struct{}3233func (*osFS) Open(name string) (iofs.File, error) {34return os.Open(name)35}3637// FS is used to load files referred to by this configuration.38// We use a library here to be able to test things properly.39var FS iofs.FS = &osFS{}4041// ServiceConfiguration configures the ws-manager configuration42type ServiceConfiguration struct {43Manager Configuration `json:"manager"`44Content struct {45Storage cntntcfg.StorageConfig `json:"storage"`46} `json:"content"`47RPCServer struct {48Addr string `json:"addr"`49TLS struct {50CA string `json:"ca"`51Certificate string `json:"crt"`52PrivateKey string `json:"key"`53} `json:"tls"`54RateLimits map[string]grpc.RateLimit `json:"ratelimits"`55} `json:"rpcServer"`56ImageBuilderProxy struct {57TargetAddr string `json:"targetAddr"`58TLS struct {59CA string `json:"ca"`60Certificate string `json:"crt"`61PrivateKey string `json:"key"`62} `json:"tls"`63} `json:"imageBuilderProxy"`6465PProf struct {66Addr string `json:"addr"`67} `json:"pprof"`68Prometheus struct {69Addr string `json:"addr"`70} `json:"prometheus"`71Health struct {72Addr string `json:"addr"`73} `json:"health"`74}7576// Configuration is the configuration of the ws-manager77type Configuration struct {78// Namespace is the kubernetes namespace the workspace manager operates in79Namespace string `json:"namespace"`80// SecretsNamespace is the kubernetes namespace which contains workspace secrets81SecretsNamespace string `json:"secretsNamespace"`82// SchedulerName is the name of the workspace scheduler all pods are created with83SchedulerName string `json:"schedulerName"`84// SeccompProfile names the seccomp profile workspaces will use85SeccompProfile string `json:"seccompProfile"`86// Timeouts configures how long workspaces can be without activity before they're shut down.87// All values in here must be valid time.Duration88Timeouts WorkspaceTimeoutConfiguration `json:"timeouts"`89// InitProbe configures the ready-probe of workspaces which signal when the initialization is finished90InitProbe InitProbeConfiguration `json:"initProbe"`91// WorkspaceURLTemplate is a Go template which resolves to the external URL of the92// workspace. Available fields are:93// - `ID` which is the workspace ID,94// - `Prefix` which is the workspace's service prefix95// - `Host` which is the GitpodHostURL96WorkspaceURLTemplate string `json:"urlTemplate"`97// WorkspaceURLTemplate is a Go template which resolves to the external URL of the98// workspace port. Available fields are:99// - `ID` which is the workspace ID,100// - `Prefix` which is the workspace's service prefix101// - `Host` which is the GitpodHostURL102// - `WorkspacePort` which is the workspace port103// - `IngressPort` which is the publicly accessile port104WorkspacePortURLTemplate string `json:"portUrlTemplate"`105// HostPath is the path on the node where workspace data resides (ideally this is an SSD)106WorkspaceHostPath string `json:"workspaceHostPath"`107// HeartbeatInterval is the time in seconds in which Theia sends a heartbeat if the user is active108HeartbeatInterval util.Duration `json:"heartbeatInterval"`109// Is the URL under which Gitpod is installed (e.g. https://gitpod.io)110GitpodHostURL string `json:"hostURL"`111// EventTraceLog is a path to file where we'll write the monitor event trace log to112EventTraceLog string `json:"eventTraceLog,omitempty"`113// ReconnectionInterval configures the time we wait until we reconnect to the various other services114ReconnectionInterval util.Duration `json:"reconnectionInterval"`115// MaintenanceMode prevents start workspace, stop workspace, and take snapshot operations116MaintenanceMode bool `json:"maintenanceMode,omitempty"`117// WorkspaceDaemon configures our connection to the workspace sync daemons runnin on the nodes118WorkspaceDaemon WorkspaceDaemonConfiguration `json:"wsdaemon"`119// RegistryFacadeHost is the host (possibly including port) on which the registry facade resolves120RegistryFacadeHost string `json:"registryFacadeHost"`121// Cluster host under which workspaces are served, e.g. ws-eu11.gitpod.io122WorkspaceClusterHost string `json:"workspaceClusterHost"`123// WorkspaceClasses provide different resource classes for workspaces124WorkspaceClasses map[string]*WorkspaceClass `json:"workspaceClass"`125// PreferredWorkspaceClass is the name of the workspace class that should be used by default126PreferredWorkspaceClass string `json:"preferredWorkspaceClass"`127// DebugWorkspacePod adds extra finalizer to workspace to prevent it from shutting down. Helps to debug.128DebugWorkspacePod bool `json:"debugWorkspacePod,omitempty"`129// WorkspaceMaxConcurrentReconciles configures the max amount of concurrent workspace reconciliations on130// the workspace controller.131WorkspaceMaxConcurrentReconciles int `json:"workspaceMaxConcurrentReconciles,omitempty"`132// TimeoutMaxConcurrentReconciles configures the max amount of concurrent workspace reconciliations on133// the timeout controller.134TimeoutMaxConcurrentReconciles int `json:"timeoutMaxConcurrentReconciles,omitempty"`135// EnableCustomSSLCertificate controls if we need to support custom SSL certificates for git operations136EnableCustomSSLCertificate bool `json:"enableCustomSSLCertificate"`137// WorkspacekitImage points to the default workspacekit image138WorkspacekitImage string `json:"workspacekitImage,omitempty"`139140SSHGatewayCAPublicKeyFile string `json:"sshGatewayCAPublicKeyFile,omitempty"`141142// SSHGatewayCAPublicKey is a CA public key143SSHGatewayCAPublicKey string144145// PodRecreationMaxRetries146PodRecreationMaxRetries int `json:"podRecreationMaxRetries,omitempty"`147// PodRecreationBackoff148PodRecreationBackoff util.Duration `json:"podRecreationBackoff,omitempty"`149}150151type WorkspaceClass struct {152Name string `json:"name"`153Description string `json:"description"`154Container ContainerConfiguration `json:"container"`155Templates WorkspacePodTemplateConfiguration `json:"templates"`156157// CreditsPerMinute is the cost per minute for this workspace class in credits158CreditsPerMinute float32 `json:"creditsPerMinute"`159}160161// WorkspaceTimeoutConfiguration configures the timeout behaviour of workspaces162type WorkspaceTimeoutConfiguration struct {163// TotalStartup is the total time a workspace can take until we expect the first activity164TotalStartup util.Duration `json:"startup"`165// Initialization is the time the initialization phase alone can take166Initialization util.Duration `json:"initialization"`167// RegularWorkspace is the time a regular workspace can be without activity before it's shutdown168RegularWorkspace util.Duration `json:"regularWorkspace"`169// MaxLifetime is the maximum lifetime of a regular workspace170MaxLifetime util.Duration `json:"maxLifetime"`171// HeadlessWorkspace is the maximum runtime a headless workspace can have (including startup)172HeadlessWorkspace util.Duration `json:"headlessWorkspace"`173// AfterClose is the time a workspace lives after it has been marked closed174AfterClose util.Duration `json:"afterClose"`175// ContentFinalization is the time in which the workspace's content needs to be backed up and removed from the node176ContentFinalization util.Duration `json:"contentFinalization"`177// Stopping is the time a workspace has until it has to be stopped. This time includes finalization, hence must be greater than178// the ContentFinalization timeout.179Stopping util.Duration `json:"stopping"`180// Interrupted is the time a workspace may be interrupted (since it last saw activity or since it was created if it never saw any)181Interrupted util.Duration `json:"interrupted"`182}183184// InitProbeConfiguration configures the behaviour of the workspace ready probe185type InitProbeConfiguration struct {186// Disabled disables the workspace init probe - this is only neccesary during tests and in noDomain environments.187Disabled bool `json:"disabled,omitempty"`188189// Timeout is the HTTP GET timeout during each probe attempt. Defaults to 5 seconds.190Timeout string `json:"timeout,omitempty"`191}192193// WorkspacePodTemplateConfiguration configures the paths to workspace pod templates194type WorkspacePodTemplateConfiguration struct {195// DefaultPath is a path to a workspace pod template YAML file that's used for196// all workspaces irregardles of their type. If a type-specific template is configured197// as well, that template is merged in, too.198DefaultPath string `json:"defaultPath,omitempty"`199// RegularPath is a path to an additional workspace pod template YAML file for regular workspaces200RegularPath string `json:"regularPath,omitempty"`201// PrebuildPath is a path to an additional workspace pod template YAML file for prebuild workspaces202PrebuildPath string `json:"prebuildPath,omitempty"`203// ProbePath is a path to an additional workspace pod template YAML file for probe workspaces204// Deprecated205ProbePath string `json:"probePath,omitempty"`206// ImagebuildPath is a path to an additional workspace pod template YAML file for imagebuild workspaces207ImagebuildPath string `json:"imagebuildPath,omitempty"`208}209210// WorkspaceDaemonConfiguration configures our connection to the workspace sync daemons runnin on the nodes211type WorkspaceDaemonConfiguration struct {212// Port is the port on the node on which the ws-daemon is listening213Port int `json:"port"`214// TLS is the certificate/key config to connect to ws-daemon215TLS struct {216// Authority is the root certificate that was used to sign the certificate itself217Authority string `json:"ca"`218// Certificate is the crt file, the actual certificate219Certificate string `json:"crt"`220// PrivateKey is the private key in order to use the certificate221PrivateKey string `json:"key"`222} `json:"tls"`223}224225// Validate validates the configuration to catch issues during startup and not at runtime226func (c *Configuration) Validate() error {227err := ozzo.ValidateStruct(&c.Timeouts,228ozzo.Field(&c.Timeouts.AfterClose, ozzo.Required),229ozzo.Field(&c.Timeouts.HeadlessWorkspace, ozzo.Required),230ozzo.Field(&c.Timeouts.Initialization, ozzo.Required),231ozzo.Field(&c.Timeouts.RegularWorkspace, ozzo.Required),232ozzo.Field(&c.Timeouts.MaxLifetime, ozzo.Required),233ozzo.Field(&c.Timeouts.TotalStartup, ozzo.Required),234ozzo.Field(&c.Timeouts.ContentFinalization, ozzo.Required),235ozzo.Field(&c.Timeouts.Stopping, ozzo.Required),236)237if err != nil {238return xerrors.Errorf("timeouts: %w", err)239}240if c.Timeouts.Stopping < c.Timeouts.ContentFinalization {241return xerrors.Errorf("stopping timeout must be greater than content finalization timeout")242}243244err = ozzo.ValidateStruct(c,245ozzo.Field(&c.WorkspaceURLTemplate, ozzo.Required, validWorkspaceURLTemplate),246ozzo.Field(&c.WorkspaceHostPath, ozzo.Required),247ozzo.Field(&c.HeartbeatInterval, ozzo.Required),248ozzo.Field(&c.GitpodHostURL, ozzo.Required, is.URL),249ozzo.Field(&c.ReconnectionInterval, ozzo.Required),250)251if err != nil {252return err253}254255if _, ok := c.WorkspaceClasses[DefaultWorkspaceClass]; !ok {256return xerrors.Errorf("missing default workspace class (\"%s\")", DefaultWorkspaceClass)257}258for name, class := range c.WorkspaceClasses {259if errs := validation.IsValidLabelValue(name); len(errs) > 0 {260return xerrors.Errorf("workspace class name \"%s\" is invalid: %v", name, errs)261}262if err := class.Container.Validate(); err != nil {263return xerrors.Errorf("workspace class %s: %w", name, err)264}265266err = ozzo.ValidateStruct(&class.Templates,267ozzo.Field(&class.Templates.DefaultPath, validPodTemplate),268ozzo.Field(&class.Templates.PrebuildPath, validPodTemplate),269ozzo.Field(&class.Templates.ProbePath, validPodTemplate),270ozzo.Field(&class.Templates.RegularPath, validPodTemplate),271)272if err != nil {273return xerrors.Errorf("workspace class %s: %w", name, err)274}275}276277return err278}279280var validPodTemplate = ozzo.By(func(o interface{}) error {281s, ok := o.(string)282if !ok {283return xerrors.Errorf("field should be string")284}285286_, err := GetWorkspacePodTemplate(s)287return err288})289290var validWorkspaceURLTemplate = ozzo.By(func(o interface{}) error {291s, ok := o.(string)292if !ok {293return xerrors.Errorf("field should be string")294}295296wsurl, err := RenderWorkspaceURL(s, "foo", "bar", "gitpod.io")297if err != nil {298return xerrors.Errorf("cannot render URL: %w", err)299}300_, err = url.Parse(wsurl)301if err != nil {302return xerrors.Errorf("not a valid URL: %w", err)303}304305return err306})307308// ContainerConfiguration configures properties of workspace pod container309type ContainerConfiguration struct {310Requests *ResourceRequestConfiguration `json:"requests,omitempty"`311Limits *ResourceLimitConfiguration `json:"limits,omitempty"`312}313314// Validate validates a container configuration315func (c *ContainerConfiguration) Validate() error {316return ozzo.ValidateStruct(c,317ozzo.Field(&c.Requests, validResourceRequestConfig),318ozzo.Field(&c.Limits, validResourceLimitConfig),319)320}321322var validResourceRequestConfig = ozzo.By(func(o interface{}) error {323rc, ok := o.(*ResourceRequestConfiguration)324if !ok {325return xerrors.Errorf("can only validate ResourceRequestConfiguration")326}327if rc == nil {328return nil329}330if rc.CPU != "" {331_, err := resource.ParseQuantity(rc.CPU)332if err != nil {333return xerrors.Errorf("cannot parse CPU quantity: %w", err)334}335}336if rc.Memory != "" {337_, err := resource.ParseQuantity(rc.Memory)338if err != nil {339return xerrors.Errorf("cannot parse Memory quantity: %w", err)340}341}342if rc.EphemeralStorage != "" {343_, err := resource.ParseQuantity(rc.EphemeralStorage)344if err != nil {345return xerrors.Errorf("cannot parse EphemeralStorage quantity: %w", err)346}347}348if rc.Storage != "" {349_, err := resource.ParseQuantity(rc.Storage)350if err != nil {351return xerrors.Errorf("cannot parse Storage quantity: %w", err)352}353}354return nil355})356357var validResourceLimitConfig = ozzo.By(func(o interface{}) error {358rc, ok := o.(*ResourceLimitConfiguration)359if !ok {360return xerrors.Errorf("can only validate ResourceLimitConfiguration")361}362if rc == nil {363return nil364}365if rc.CPU.MinLimit != "" {366_, err := resource.ParseQuantity(rc.CPU.MinLimit)367if err != nil {368return xerrors.Errorf("cannot parse low limit CPU quantity: %w", err)369}370}371if rc.CPU.BurstLimit != "" {372_, err := resource.ParseQuantity(rc.CPU.BurstLimit)373if err != nil {374return xerrors.Errorf("cannot parse burst limit CPU quantity: %w", err)375}376}377if rc.Memory != "" {378_, err := resource.ParseQuantity(rc.Memory)379if err != nil {380return xerrors.Errorf("cannot parse Memory quantity: %w", err)381}382}383if rc.EphemeralStorage != "" {384_, err := resource.ParseQuantity(rc.EphemeralStorage)385if err != nil {386return xerrors.Errorf("cannot parse EphemeralStorage quantity: %w", err)387}388}389if rc.Storage != "" {390_, err := resource.ParseQuantity(rc.Storage)391if err != nil {392return xerrors.Errorf("cannot parse Storage quantity: %w", err)393}394}395return nil396})397398func (r *ResourceRequestConfiguration) StorageQuantity() (resource.Quantity, error) {399if r.Storage == "" {400res := resource.NewQuantity(0, resource.BinarySI)401return *res, nil402}403return resource.ParseQuantity(r.Storage)404}405406// ResourceList parses the quantities in the resource config407func (r *ResourceRequestConfiguration) ResourceList() (corev1.ResourceList, error) {408if r == nil {409return corev1.ResourceList{}, nil410}411res := map[corev1.ResourceName]string{412corev1.ResourceCPU: r.CPU,413corev1.ResourceMemory: r.Memory,414corev1.ResourceEphemeralStorage: r.EphemeralStorage,415}416417var l = make(corev1.ResourceList)418for k, v := range res {419if v == "" {420continue421}422423q, err := resource.ParseQuantity(v)424if err != nil {425return nil, xerrors.Errorf("%s: %w", k, err)426}427if q.Value() == 0 {428continue429}430431l[k] = q432}433return l, nil434}435436// GetWorkspacePodTemplate parses a pod template YAML file. Returns nil if path is empty.437func GetWorkspacePodTemplate(filename string) (*corev1.Pod, error) {438if filename == "" {439return nil, nil440}441442tpr := os.Getenv("TELEPRESENCE_ROOT")443if tpr != "" {444filename = filepath.Join(tpr, filename)445}446447tpl, err := FS.Open(filename)448if err != nil {449return nil, xerrors.Errorf("cannot read pod template: %w", err)450}451defer tpl.Close()452453var res corev1.Pod454decoder := yaml.NewYAMLOrJSONDecoder(tpl, 4096)455err = decoder.Decode(&res)456if err != nil {457return nil, xerrors.Errorf("cannot unmarshal pod template: %w", err)458}459460return &res, nil461}462463// RenderWorkspaceURL takes a workspace URL template and renders it464func RenderWorkspaceURL(urltpl, id, servicePrefix, host string) (string, error) {465tpl, err := template.New("url").Parse(urltpl)466if err != nil {467return "", xerrors.Errorf("cannot compute workspace URL: %w", err)468}469470type data struct {471ID string472Prefix string473Host string474}475d := data{476ID: id,477Prefix: servicePrefix,478Host: host,479}480481var b bytes.Buffer482err = tpl.Execute(&b, d)483if err != nil {484return "", xerrors.Errorf("cannot compute workspace URL: %w", err)485}486487return b.String(), nil488}489490type PortURLContext struct {491ID string492Prefix string493Host string494WorkspacePort string495IngressPort string496}497498// RenderWorkspacePortURL takes a workspace port URL template and renders it499func RenderWorkspacePortURL(urltpl string, ctx PortURLContext) (string, error) {500tpl, err := template.New("url").Parse(urltpl)501if err != nil {502return "", xerrors.Errorf("cannot compute workspace URL: %w", err)503}504505var b bytes.Buffer506err = tpl.Execute(&b, ctx)507if err != nil {508return "", xerrors.Errorf("cannot compute workspace port URL: %w", err)509}510511return b.String(), nil512}513514// ResourceRequestConfiguration configures resources of a pod/container515type ResourceRequestConfiguration struct {516CPU string `json:"cpu"`517Memory string `json:"memory"`518EphemeralStorage string `json:"ephemeral-storage"`519Storage string `json:"storage,omitempty"`520}521522type ResourceLimitConfiguration struct {523CPU *CpuResourceLimit `json:"cpu"`524Memory string `json:"memory"`525EphemeralStorage string `json:"ephemeral-storage"`526Storage string `json:"storage,omitempty"`527}528529func (r *ResourceLimitConfiguration) ResourceList() (corev1.ResourceList, error) {530if r == nil {531return corev1.ResourceList{}, nil532}533res := map[corev1.ResourceName]string{534corev1.ResourceMemory: r.Memory,535corev1.ResourceEphemeralStorage: r.EphemeralStorage,536}537538if r.CPU != nil {539res[corev1.ResourceCPU] = r.CPU.BurstLimit540}541542var l = make(corev1.ResourceList)543for k, v := range res {544if v == "" {545continue546}547548q, err := resource.ParseQuantity(v)549if err != nil {550return nil, xerrors.Errorf("%s: %w", k, err)551}552if q.Value() == 0 {553continue554}555556l[k] = q557}558return l, nil559}560561func (r *ResourceLimitConfiguration) StorageQuantity() (resource.Quantity, error) {562if r.Storage == "" {563res := resource.NewQuantity(0, resource.BinarySI)564return *res, nil565}566return resource.ParseQuantity(r.Storage)567}568569type CpuResourceLimit struct {570MinLimit string `json:"min"`571BurstLimit string `json:"burst"`572}573574type MaintenanceConfig struct {575EnabledUntil *time.Time `json:"enabledUntil"`576}577578579