Path: blob/main/components/ws-manager-mk2/controllers/create.go
2498 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 controllers56import (7"context"8"crypto/rand"9"fmt"10"io"11"path/filepath"12"reflect"13"strconv"14"strings"15"time"1617"github.com/imdario/mergo"18"golang.org/x/xerrors"19"google.golang.org/protobuf/proto"20corev1 "k8s.io/api/core/v1"21metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"22"k8s.io/apimachinery/pkg/util/intstr"23"k8s.io/apimachinery/pkg/version"24"k8s.io/utils/pointer"2526wsk8s "github.com/gitpod-io/gitpod/common-go/kubernetes"27"github.com/gitpod-io/gitpod/common-go/tracing"28csapi "github.com/gitpod-io/gitpod/content-service/api"29regapi "github.com/gitpod-io/gitpod/registry-facade/api"30"github.com/gitpod-io/gitpod/ws-manager-mk2/pkg/constants"31config "github.com/gitpod-io/gitpod/ws-manager/api/config"32workspacev1 "github.com/gitpod-io/gitpod/ws-manager/api/crd/v1"33)3435const (36// workspaceVolume is the name of the workspace volume37workspaceVolumeName = "vol-this-workspace"38// workspaceDir is the path within all containers where workspaceVolume is mounted to39workspaceDir = "/workspace"4041// headlessLabel marks a workspace as headless42headlessLabel = "gitpod.io/headless"4344// instanceIDLabel is added for the container dispatch mechanism in ws-daemon to work45// TODO(furisto): remove this label once we have moved ws-daemon to a controller setup46instanceIDLabel = "gitpod.io/instanceID"4748// Grace time until the process in the workspace is properly completed49// e.g. dockerd in the workspace may take some time to clean up the overlay directory.50//51// 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.52// This could be related to creation of the backup or the upload to the object storage53// https://github.com/kubernetes/autoscaler/blob/ee59c74cc0d61165c633d3e5b42caccc614be542/cluster-autoscaler/utils/drain/drain.go#L33054// https://github.com/kubernetes/autoscaler/blob/ee59c74cc0d61165c633d3e5b42caccc614be542/cluster-autoscaler/utils/drain/drain.go#L10755gracePeriod = 30 * time.Minute56)5758type startWorkspaceContext struct {59Config *config.Configuration60Workspace *workspacev1.Workspace61Labels map[string]string `json:"labels"`62IDEPort int32 `json:"idePort"`63SupervisorPort int32 `json:"supervisorPort"`64Headless bool `json:"headless"`65ServerVersion *version.Info `json:"serverVersion"`66}6768// createWorkspacePod creates the actual workspace pod based on the definite workspace pod and appropriate69// templates. The result of this function is not expected to be modified prior to being passed to Kubernetes.70func (r *WorkspaceReconciler) createWorkspacePod(sctx *startWorkspaceContext) (*corev1.Pod, error) {71class, ok := sctx.Config.WorkspaceClasses[sctx.Workspace.Spec.Class]72if !ok {73return nil, xerrors.Errorf("unknown workspace class: %s", sctx.Workspace.Spec.Class)74}7576podTemplate, err := config.GetWorkspacePodTemplate(class.Templates.DefaultPath)77if err != nil {78return nil, xerrors.Errorf("cannot read pod template - this is a configuration problem: %w", err)79}80var typeSpecificTpl *corev1.Pod81switch sctx.Workspace.Spec.Type {82case workspacev1.WorkspaceTypeRegular:83typeSpecificTpl, err = config.GetWorkspacePodTemplate(class.Templates.RegularPath)84case workspacev1.WorkspaceTypePrebuild:85typeSpecificTpl, err = config.GetWorkspacePodTemplate(class.Templates.PrebuildPath)86case workspacev1.WorkspaceTypeImageBuild:87typeSpecificTpl, err = config.GetWorkspacePodTemplate(class.Templates.ImagebuildPath)88}89if err != nil {90return nil, xerrors.Errorf("cannot read type-specific pod template - this is a configuration problem: %w", err)91}92if typeSpecificTpl != nil {93err = combineDefiniteWorkspacePodWithTemplate(podTemplate, typeSpecificTpl)94if err != nil {95return nil, xerrors.Errorf("cannot apply type-specific pod template: %w", err)96}97}9899pod, err := createDefiniteWorkspacePod(sctx)100if err != nil {101return nil, xerrors.Errorf("cannot create definite workspace pod: %w", err)102}103104err = combineDefiniteWorkspacePodWithTemplate(pod, podTemplate)105if err != nil {106return nil, xerrors.Errorf("cannot create workspace pod: %w", err)107}108return pod, nil109}110111// combineDefiniteWorkspacePodWithTemplate merges a definite workspace pod with a user-provided template.112// 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).113func combineDefiniteWorkspacePodWithTemplate(pod *corev1.Pod, template *corev1.Pod) error {114if template == nil {115return nil116}117if pod == nil {118return xerrors.Errorf("definite pod cannot be nil")119}120121err := mergo.Merge(pod, template, mergo.WithAppendSlice, mergo.WithTransformers(&mergePodTransformer{}))122if err != nil {123return xerrors.Errorf("cannot merge workspace pod with template: %w", err)124}125126return nil127}128129// mergePodTransformer is a mergo transformer which facilitates merging of NodeAffinity and containers130type mergePodTransformer struct{}131132func (*mergePodTransformer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error {133switch typ {134case reflect.TypeOf([]corev1.NodeSelectorTerm{}):135return mergeNodeAffinityMatchExpressions136case reflect.TypeOf([]corev1.Container{}):137return mergeContainer138case reflect.TypeOf(&corev1.Probe{}):139return mergeProbe140}141142return nil143}144145// mergeContainer merges cnotainers by name146func mergeContainer(dst, src reflect.Value) (err error) {147// working with reflection is tricky business - add a safety net here and recover if things go sideways148defer func() {149r := recover()150if er, ok := r.(error); r != nil && ok {151err = er152}153}()154155if !dst.CanSet() || !src.CanSet() {156return nil157}158159srcs := src.Interface().([]corev1.Container)160dsts := dst.Interface().([]corev1.Container)161162for _, s := range srcs {163di := -1164for i, d := range dsts {165if d.Name == s.Name {166di = i167break168}169}170if di < 0 {171// We don't have a matching destination container to merge this src one into172continue173}174175err = mergo.Merge(&dsts[di], s, mergo.WithAppendSlice, mergo.WithOverride, mergo.WithTransformers(&mergePodTransformer{}))176if err != nil {177return err178}179}180181dst.Set(reflect.ValueOf(dsts))182return nil183}184185// mergeNodeAffinityMatchExpressions ensures that NodeAffinityare AND'ed186func mergeNodeAffinityMatchExpressions(dst, src reflect.Value) (err error) {187// working with reflection is tricky business - add a safety net here and recover if things go sideways188defer func() {189r := recover()190if er, ok := r.(error); r != nil && ok {191err = er192}193}()194195if !dst.CanSet() || !src.CanSet() {196return nil197}198199srcs := src.Interface().([]corev1.NodeSelectorTerm)200dsts := dst.Interface().([]corev1.NodeSelectorTerm)201202if len(dsts) > 1 {203// we only run this mechanism if it's clear where we merge into204return nil205}206if len(dsts) == 0 {207dsts = srcs208} else {209for _, term := range srcs {210dsts[0].MatchExpressions = append(dsts[0].MatchExpressions, term.MatchExpressions...)211}212}213dst.Set(reflect.ValueOf(dsts))214215return nil216}217218func mergeProbe(dst, src reflect.Value) (err error) {219// working with reflection is tricky business - add a safety net here and recover if things go sideways220defer func() {221r := recover()222if er, ok := r.(error); r != nil && ok {223err = er224}225}()226227srcs := src.Interface().(*corev1.Probe)228dsts := dst.Interface().(*corev1.Probe)229230if dsts != nil && srcs == nil {231// don't overwrite with nil232} else if dsts == nil && srcs != nil {233// we don't have anything at dst yet - take the whole src234*dsts = *srcs235} else {236dsts.HTTPGet = srcs.HTTPGet237dsts.Exec = srcs.Exec238dsts.TCPSocket = srcs.TCPSocket239}240241// *srcs = *dsts242return nil243}244245// createDefiniteWorkspacePod creates a workspace pod without regard for any template.246// The result of this function can be deployed and it would work.247func createDefiniteWorkspacePod(sctx *startWorkspaceContext) (*corev1.Pod, error) {248workspaceContainer, err := createWorkspaceContainer(sctx)249if err != nil {250return nil, xerrors.Errorf("cannot create workspace container: %w", err)251}252253// Beware: this allows setuid binaries in the workspace - supervisor needs to set no_new_privs now.254// However: the whole user workload now runs in a user namespace, which makes this acceptable.255workspaceContainer.SecurityContext.AllowPrivilegeEscalation = pointer.Bool(true)256257workspaceVolume, err := createWorkspaceVolumes(sctx)258if err != nil {259return nil, xerrors.Errorf("cannot create workspace volumes: %w", err)260}261262labels := make(map[string]string)263labels["gitpod.io/networkpolicy"] = "default"264for k, v := range sctx.Labels {265labels[k] = v266}267268var prefix string269switch sctx.Workspace.Spec.Type {270case workspacev1.WorkspaceTypePrebuild:271prefix = "prebuild"272case workspacev1.WorkspaceTypeImageBuild:273prefix = "imagebuild"274default:275prefix = "ws"276}277278annotations := map[string]string{279"prometheus.io/scrape": "true",280"prometheus.io/path": "/metrics",281"prometheus.io/port": strconv.Itoa(int(sctx.IDEPort)),282// prevent cluster-autoscaler from removing a node283// https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/FAQ.md#what-types-of-pods-can-prevent-ca-from-removing-a-node284"cluster-autoscaler.kubernetes.io/safe-to-evict": "false",285}286287configureAppamor(sctx, annotations, workspaceContainer)288289for k, v := range sctx.Workspace.Annotations {290annotations[k] = v291}292293// By default we embue our workspace pods with some tolerance towards pressure taints,294// see https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/#taint-based-evictions295// for more details. As hope/assume that the pressure might go away in this time.296// Memory and Disk pressure are no reason to stop a workspace - instead of stopping a workspace297// we'd rather wait things out or gracefully fail the workspace ourselves.298var perssureToleranceSeconds int64 = 30299300// Mounting /dev/net/tun should be fine security-wise, because:301// - the TAP driver documentation says so (see https://www.kernel.org/doc/Documentation/networking/tuntap.txt)302// - systemd's nspawn does the same thing (if it's good enough for them, it's good enough for us)303var (304hostPathOrCreate = corev1.HostPathDirectoryOrCreate305daemonVolumeName = "daemon-mount"306)307volumes := []corev1.Volume{308workspaceVolume,309{310Name: daemonVolumeName,311VolumeSource: corev1.VolumeSource{312HostPath: &corev1.HostPathVolumeSource{313Path: filepath.Join(sctx.Config.WorkspaceHostPath, sctx.Workspace.Name+"-daemon"),314Type: &hostPathOrCreate,315},316},317},318}319320if sctx.Config.EnableCustomSSLCertificate {321volumes = append(volumes, corev1.Volume{322Name: "gitpod-ca-crt",323VolumeSource: corev1.VolumeSource{324ConfigMap: &corev1.ConfigMapVolumeSource{325LocalObjectReference: corev1.LocalObjectReference{Name: "gitpod-customer-certificate-bundle"},326},327},328})329}330331workloadType := "regular"332if sctx.Headless {333workloadType = "headless"334}335336matchExpressions := []corev1.NodeSelectorRequirement{337{338Key: "gitpod.io/workload_workspace_" + workloadType,339Operator: corev1.NodeSelectorOpExists,340},341{342Key: "gitpod.io/ws-daemon_ready_ns_" + sctx.Config.Namespace,343Operator: corev1.NodeSelectorOpExists,344},345{346Key: "gitpod.io/registry-facade_ready_ns_" + sctx.Config.Namespace,347Operator: corev1.NodeSelectorOpExists,348},349}350351affinity := &corev1.Affinity{352NodeAffinity: &corev1.NodeAffinity{353RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{354NodeSelectorTerms: []corev1.NodeSelectorTerm{355{356MatchExpressions: matchExpressions,357},358},359},360},361}362363graceSec := int64(gracePeriod.Seconds())364pod := corev1.Pod{365ObjectMeta: metav1.ObjectMeta{366Name: fmt.Sprintf("%s-%s", prefix, sctx.Workspace.Name),367Namespace: sctx.Config.Namespace,368Labels: labels,369Annotations: annotations,370Finalizers: []string{workspacev1.GitpodFinalizerName},371},372Spec: corev1.PodSpec{373Hostname: sctx.Workspace.Spec.Ownership.WorkspaceID,374AutomountServiceAccountToken: pointer.Bool(false),375ServiceAccountName: "workspace",376SchedulerName: sctx.Config.SchedulerName,377EnableServiceLinks: pointer.Bool(false),378Affinity: affinity,379SecurityContext: &corev1.PodSecurityContext{380// We're using a custom seccomp profile for user namespaces to allow clone, mount and chroot.381SeccompProfile: &corev1.SeccompProfile{382Type: corev1.SeccompProfileTypeLocalhost,383LocalhostProfile: pointer.String(sctx.Config.SeccompProfile),384},385},386Containers: []corev1.Container{387*workspaceContainer,388},389RestartPolicy: corev1.RestartPolicyNever,390Volumes: volumes,391TerminationGracePeriodSeconds: &graceSec,392Tolerations: []corev1.Toleration{393{394Key: "node.kubernetes.io/disk-pressure",395Operator: "Exists",396Effect: "NoExecute",397// Tolarate Indefinitely398},399{400Key: "node.kubernetes.io/memory-pressure",401Operator: "Exists",402Effect: "NoExecute",403// Tolarate Indefinitely404},405{406Key: "node.kubernetes.io/network-unavailable",407Operator: "Exists",408Effect: "NoExecute",409TolerationSeconds: &perssureToleranceSeconds,410},411},412},413}414415return &pod, nil416}417418func createWorkspaceContainer(sctx *startWorkspaceContext) (*corev1.Container, error) {419class, ok := sctx.Config.WorkspaceClasses[sctx.Workspace.Spec.Class]420if !ok {421return nil, xerrors.Errorf("unknown workspace class: %s", sctx.Workspace.Spec.Class)422}423424limits, err := class.Container.Limits.ResourceList()425if err != nil {426return nil, xerrors.Errorf("cannot parse workspace container limits: %w", err)427}428requests, err := class.Container.Requests.ResourceList()429if err != nil {430return nil, xerrors.Errorf("cannot parse workspace container requests: %w", err)431}432env, err := createWorkspaceEnvironment(sctx)433if err != nil {434return nil, xerrors.Errorf("cannot create workspace env: %w", err)435}436sec, err := createDefaultSecurityContext()437if err != nil {438return nil, xerrors.Errorf("cannot create Theia env: %w", err)439}440mountPropagation := corev1.MountPropagationHostToContainer441442var (443command = []string{"/.supervisor/workspacekit", "ring0"}444readinessProbe = &corev1.Probe{445ProbeHandler: corev1.ProbeHandler{446HTTPGet: &corev1.HTTPGetAction{447Path: "/_supervisor/v1/status/ide/wait/true",448Port: intstr.FromInt((int)(sctx.SupervisorPort)),449Scheme: corev1.URISchemeHTTP,450},451},452// We make the readiness probe more difficult to fail than the liveness probe.453// This way, if the workspace really has a problem it will be shut down by Kubernetes rather than end up in454// some undefined state.455FailureThreshold: 600,456PeriodSeconds: 1,457SuccessThreshold: 1,458TimeoutSeconds: 1,459InitialDelaySeconds: 1,460}461)462463image := fmt.Sprintf("%s/%s/%s", sctx.Config.RegistryFacadeHost, regapi.ProviderPrefixRemote, sctx.Workspace.Name)464465volumeMounts := []corev1.VolumeMount{466{467Name: workspaceVolumeName,468MountPath: workspaceDir,469ReadOnly: false,470MountPropagation: &mountPropagation,471},472{473MountPath: "/.workspace",474Name: "daemon-mount",475MountPropagation: &mountPropagation,476},477}478479if sctx.Config.EnableCustomSSLCertificate {480volumeMounts = append(volumeMounts, corev1.VolumeMount{481Name: "gitpod-ca-crt",482MountPath: "/etc/ssl/certs/gitpod-ca.crt",483SubPath: "ca-certificates.crt",484ReadOnly: true,485})486}487488return &corev1.Container{489Name: "workspace",490Image: image,491SecurityContext: sec,492ImagePullPolicy: corev1.PullIfNotPresent,493Ports: []corev1.ContainerPort{494{ContainerPort: sctx.IDEPort},495},496Resources: corev1.ResourceRequirements{497Limits: limits,498Requests: requests,499},500VolumeMounts: volumeMounts,501ReadinessProbe: readinessProbe,502Env: env,503Command: command,504TerminationMessagePolicy: corev1.TerminationMessageReadFile,505}, nil506}507508func createWorkspaceEnvironment(sctx *startWorkspaceContext) ([]corev1.EnvVar, error) {509class, ok := sctx.Config.WorkspaceClasses[sctx.Workspace.Spec.Class]510if !ok {511return nil, xerrors.Errorf("unknown workspace class: %s", sctx.Workspace.Spec.Class)512}513514getWorkspaceRelativePath := func(segment string) string {515// ensure we do not produce nested paths for the default workspace location516return filepath.Join("/workspace", strings.TrimPrefix(segment, "/workspace"))517}518519var init csapi.WorkspaceInitializer520err := proto.Unmarshal(sctx.Workspace.Spec.Initializer, &init)521if err != nil {522err = fmt.Errorf("cannot unmarshal initializer config: %w", err)523return nil, err524}525526allRepoRoots := csapi.GetCheckoutLocationsFromInitializer(&init)527if len(allRepoRoots) == 0 {528allRepoRoots = []string{""} // for backward compatibility, we are adding a single empty location (translates to /workspace/)529}530for i, root := range allRepoRoots {531allRepoRoots[i] = getWorkspaceRelativePath(root)532}533534// Can't read the workspace URL from status yet, as the status likely hasn't535// been set by the controller yet at this point. Therefore, manually construct536// the URL to pass to the container env.537wsUrl, err := config.RenderWorkspaceURL(sctx.Config.WorkspaceURLTemplate, sctx.Workspace.Name, sctx.Workspace.Spec.Ownership.WorkspaceID, sctx.Config.GitpodHostURL)538if err != nil {539return nil, fmt.Errorf("cannot render workspace URL: %w", err)540}541542// Envs that start with GITPOD_ are appended to the Terminal environments543result := []corev1.EnvVar{}544result = append(result, corev1.EnvVar{Name: "GITPOD_REPO_ROOT", Value: allRepoRoots[0]})545result = append(result, corev1.EnvVar{Name: "GITPOD_REPO_ROOTS", Value: strings.Join(allRepoRoots, ",")})546result = append(result, corev1.EnvVar{Name: "GITPOD_OWNER_ID", Value: sctx.Workspace.Spec.Ownership.Owner})547result = append(result, corev1.EnvVar{Name: "GITPOD_WORKSPACE_ID", Value: sctx.Workspace.Spec.Ownership.WorkspaceID})548result = append(result, corev1.EnvVar{Name: "GITPOD_INSTANCE_ID", Value: sctx.Workspace.Name})549result = append(result, corev1.EnvVar{Name: "GITPOD_THEIA_PORT", Value: strconv.Itoa(int(sctx.IDEPort))})550result = append(result, corev1.EnvVar{Name: "THEIA_WORKSPACE_ROOT", Value: getWorkspaceRelativePath(sctx.Workspace.Spec.WorkspaceLocation)})551result = append(result, corev1.EnvVar{Name: "GITPOD_HOST", Value: sctx.Config.GitpodHostURL})552result = append(result, corev1.EnvVar{Name: "GITPOD_WORKSPACE_URL", Value: wsUrl})553result = append(result, corev1.EnvVar{Name: "GITPOD_WORKSPACE_CLUSTER_HOST", Value: sctx.Config.WorkspaceClusterHost})554result = append(result, corev1.EnvVar{Name: "GITPOD_WORKSPACE_CLASS", Value: sctx.Workspace.Spec.Class})555result = append(result, corev1.EnvVar{Name: "THEIA_SUPERVISOR_ENDPOINT", Value: fmt.Sprintf(":%d", sctx.SupervisorPort)})556// TODO(ak) remove THEIA_WEBVIEW_EXTERNAL_ENDPOINT and THEIA_MINI_BROWSER_HOST_PATTERN when Theia is removed557result = append(result, corev1.EnvVar{Name: "THEIA_WEBVIEW_EXTERNAL_ENDPOINT", Value: "webview-{{hostname}}"})558result = append(result, corev1.EnvVar{Name: "THEIA_MINI_BROWSER_HOST_PATTERN", Value: "browser-{{hostname}}"})559560result = append(result, corev1.EnvVar{Name: "GITPOD_SSH_CA_PUBLIC_KEY", Value: sctx.Workspace.Spec.SSHGatewayCAPublicKey})561562// We don't require that Git be configured for workspaces563if sctx.Workspace.Spec.Git != nil {564result = append(result, corev1.EnvVar{Name: "GITPOD_GIT_USER_NAME", Value: sctx.Workspace.Spec.Git.Username})565result = append(result, corev1.EnvVar{Name: "GITPOD_GIT_USER_EMAIL", Value: sctx.Workspace.Spec.Git.Email})566}567568if sctx.Config.EnableCustomSSLCertificate {569const (570customCAMountPath = "/etc/ssl/certs/gitpod-ca.crt"571certsMountPath = "/etc/ssl/certs/"572)573574result = append(result, corev1.EnvVar{Name: "NODE_EXTRA_CA_CERTS", Value: customCAMountPath})575result = append(result, corev1.EnvVar{Name: "GIT_SSL_CAPATH", Value: certsMountPath})576result = append(result, corev1.EnvVar{Name: "GIT_SSL_CAINFO", Value: customCAMountPath})577}578579// System level env vars580for _, e := range sctx.Workspace.Spec.SysEnvVars {581env := corev1.EnvVar{582Name: e.Name,583Value: e.Value,584}585result = append(result, env)586}587588// User-defined env vars (i.e. those coming from the request)589for _, e := range sctx.Workspace.Spec.UserEnvVars {590switch e.Name {591case "GITPOD_WORKSPACE_CONTEXT",592"GITPOD_WORKSPACE_CONTEXT_URL",593"GITPOD_TASKS",594"GITPOD_RESOLVED_EXTENSIONS",595"GITPOD_EXTERNAL_EXTENSIONS",596"GITPOD_WORKSPACE_CLASS_INFO",597"GITPOD_IDE_ALIAS",598"GITPOD_RLIMIT_CORE",599"GITPOD_IMAGE_AUTH":600// these variables are allowed - don't skip them601default:602if strings.HasPrefix(e.Name, "GITPOD_") {603// we don't allow env vars starting with GITPOD_ and those that we do allow we've listed above604continue605}606}607608result = append(result, e)609}610611heartbeatInterval := time.Duration(sctx.Config.HeartbeatInterval)612result = append(result, corev1.EnvVar{Name: "GITPOD_INTERVAL", Value: fmt.Sprintf("%d", int64(heartbeatInterval/time.Millisecond))})613614res, err := class.Container.Requests.ResourceList()615if err != nil {616return nil, xerrors.Errorf("cannot create environment: %w", err)617}618memoryInMegabyte := res.Memory().Value() / (1024 * 1024)619result = append(result, corev1.EnvVar{Name: "GITPOD_MEMORY", Value: strconv.FormatInt(memoryInMegabyte, 10)})620621cpuCount := res.Cpu().Value()622result = append(result, corev1.EnvVar{Name: "GITPOD_CPU_COUNT", Value: strconv.FormatInt(int64(cpuCount), 10)})623624if sctx.Headless {625result = append(result, corev1.EnvVar{Name: "GITPOD_HEADLESS", Value: "true"})626}627628// remove empty env vars629cleanResult := make([]corev1.EnvVar, 0)630for _, v := range result {631if v.Name == "" || (v.Value == "" && v.ValueFrom == nil) {632continue633}634635cleanResult = append(cleanResult, v)636}637638return cleanResult, nil639}640641func createWorkspaceVolumes(sctx *startWorkspaceContext) (workspace corev1.Volume, err error) {642// silly protobuf structure design - this needs to be a reference to a string,643// so we have to assign it to a variable first to take the address644hostPathOrCreate := corev1.HostPathDirectoryOrCreate645646workspace = corev1.Volume{647Name: workspaceVolumeName,648VolumeSource: corev1.VolumeSource{649HostPath: &corev1.HostPathVolumeSource{650Path: filepath.Join(sctx.Config.WorkspaceHostPath, sctx.Workspace.Name),651Type: &hostPathOrCreate,652},653},654}655656err = nil657return658}659660func createDefaultSecurityContext() (*corev1.SecurityContext, error) {661gitpodGUID := int64(33333)662663res := &corev1.SecurityContext{664AllowPrivilegeEscalation: pointer.Bool(false),665Capabilities: &corev1.Capabilities{666Add: []corev1.Capability{667"AUDIT_WRITE", // Write records to kernel auditing log.668"FSETID", // Don’t clear set-user-ID and set-group-ID permission bits when a file is modified.669"KILL", // Bypass permission checks for sending signals.670"NET_BIND_SERVICE", // Bind a socket to internet domain privileged ports (port numbers less than 1024).671"SYS_PTRACE", // Trace arbitrary processes using ptrace(2).672},673Drop: []corev1.Capability{674"SETPCAP", // Modify process capabilities.675"CHOWN", // Make arbitrary changes to file UIDs and GIDs (see chown(2)).676"NET_RAW", // Use RAW and PACKET sockets.677"DAC_OVERRIDE", // Bypass file read, write, and execute permission checks.678"FOWNER", // Bypass permission checks on operations that normally require the file system UID of the process to match the UID of the file.679"SYS_CHROOT", // Use chroot(2), change root directory.680"SETFCAP", // Set file capabilities.681"SETUID", // Make arbitrary manipulations of process UIDs.682"SETGID", // Make arbitrary manipulations of process GIDs and supplementary GID list.683},684},685Privileged: pointer.Bool(false),686ReadOnlyRootFilesystem: pointer.Bool(false),687RunAsGroup: &gitpodGUID,688RunAsNonRoot: pointer.Bool(true),689RunAsUser: &gitpodGUID,690}691692return res, nil693}694695func newStartWorkspaceContext(ctx context.Context, cfg *config.Configuration, ws *workspacev1.Workspace, serverVersion *version.Info) (res *startWorkspaceContext, err error) {696// we deliberately do not shadow ctx here as we need the original context later to extract the TraceID697span, _ := tracing.FromContext(ctx, "newStartWorkspaceContext")698defer tracing.FinishSpan(span, &err)699700return &startWorkspaceContext{701Labels: map[string]string{702"app": "gitpod",703"component": "workspace",704wsk8s.MetaIDLabel: ws.Spec.Ownership.WorkspaceID,705wsk8s.WorkspaceIDLabel: ws.Name,706wsk8s.OwnerLabel: ws.Spec.Ownership.Owner,707wsk8s.TypeLabel: strings.ToLower(string(ws.Spec.Type)),708wsk8s.WorkspaceManagedByLabel: constants.ManagedBy,709instanceIDLabel: ws.Name,710headlessLabel: strconv.FormatBool(ws.IsHeadless()),711},712Config: cfg,713Workspace: ws,714IDEPort: 23000,715SupervisorPort: 22999,716Headless: ws.IsHeadless(),717ServerVersion: serverVersion,718}, nil719}720721func configureAppamor(sctx *startWorkspaceContext, annotations map[string]string, workspaceContainer *corev1.Container) {722// pre K8s 1.30 we need to set the apparmor profile to unconfined as an annotation723if sctx.ServerVersion.Major <= "1" && sctx.ServerVersion.Minor < "30" {724annotations["container.apparmor.security.beta.kubernetes.io/workspace"] = "unconfined"725} else {726workspaceContainer.SecurityContext.AppArmorProfile = &corev1.AppArmorProfile{727Type: corev1.AppArmorProfileTypeUnconfined,728}729}730}731732// validCookieChars contains all characters which may occur in an HTTP Cookie value (unicode \u0021 through \u007E),733// without the characters , ; and / ... I did not find more details about permissible characters in RFC2965, so I took734// this list of permissible chars from Wikipedia.735//736// The tokens we produce here (e.g. owner token or CLI API token) are likely placed in cookies or transmitted via HTTP.737// To make the lifes of downstream users easier we'll try and play nice here w.r.t. to the characters used.738var validCookieChars = []byte("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_-.")739740func getRandomString(length int) (string, error) {741b := make([]byte, length)742n, err := rand.Read(b)743if err != nil {744return "", err745}746if n != length {747return "", io.ErrShortWrite748}749750lrsc := len(validCookieChars)751for i, c := range b {752b[i] = validCookieChars[int(c)%lrsc]753}754return string(b), nil755}756757758