Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/ws-daemon/pkg/content/hooks.go
2500 views
1
// Copyright (c) 2021 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 content
6
7
import (
8
"context"
9
"errors"
10
"io/fs"
11
"os"
12
"path/filepath"
13
14
"github.com/gitpod-io/gitpod/common-go/log"
15
"github.com/gitpod-io/gitpod/common-go/tracing"
16
"github.com/gitpod-io/gitpod/content-service/pkg/initializer"
17
"github.com/gitpod-io/gitpod/content-service/pkg/storage"
18
"github.com/gitpod-io/gitpod/ws-daemon/api"
19
daemonapi "github.com/gitpod-io/gitpod/ws-daemon/api"
20
"github.com/gitpod-io/gitpod/ws-daemon/pkg/internal/session"
21
"github.com/gitpod-io/gitpod/ws-daemon/pkg/iws"
22
"github.com/gitpod-io/gitpod/ws-daemon/pkg/quota"
23
"github.com/opentracing/opentracing-go"
24
"golang.org/x/xerrors"
25
"google.golang.org/grpc"
26
"google.golang.org/grpc/credentials/insecure"
27
)
28
29
// WorkspaceLifecycleHooks configures the lifecycle hooks for all workspaces
30
func WorkspaceLifecycleHooks(cfg Config, workspaceCIDR string, uidmapper *iws.Uidmapper, xfs *quota.XFS, cgroupMountPoint string) map[session.WorkspaceState][]session.WorkspaceLivecycleHook {
31
// startIWS starts the in-workspace service for a workspace. This lifecycle hook is idempotent, hence can - and must -
32
// be called on initialization and ready. The on-ready hook exists only to support ws-daemon restarts.
33
startIWS := iws.ServeWorkspace(uidmapper, api.FSShiftMethod(cfg.UserNamespaces.FSShift), cgroupMountPoint, workspaceCIDR)
34
35
return map[session.WorkspaceState][]session.WorkspaceLivecycleHook{
36
session.WorkspaceInitializing: {
37
hookSetupWorkspaceLocation,
38
startIWS, // workspacekit is waiting for starting IWS, so it needs to start as soon as possible.
39
hookSetupRemoteStorage(cfg),
40
// When starting a workspace, use soft limit for the following reason to ensure content is restored
41
// - workspacekit needs to generate some temporary file when starting a workspace
42
// - when extracting tar file, tar command create some symlinks following a original content
43
hookInstallQuota(xfs, false),
44
},
45
session.WorkspaceReady: {
46
startIWS,
47
hookSetupRemoteStorage(cfg),
48
hookInstallQuota(xfs, true),
49
},
50
session.WorkspaceDisposed: {
51
hookWipingTeardown(), // if ws.DoWipe == true: make sure we 100% tear down the workspace
52
iws.StopServingWorkspace,
53
hookRemoveQuota(xfs),
54
},
55
}
56
}
57
58
// hookSetupRemoteStorage configures the remote storage for a workspace
59
func hookSetupRemoteStorage(cfg Config) session.WorkspaceLivecycleHook {
60
return func(ctx context.Context, ws *session.Workspace) (err error) {
61
span, ctx := opentracing.StartSpanFromContext(ctx, "hook.SetupRemoteStorage")
62
defer tracing.FinishSpan(span, &err)
63
64
if _, ok := ws.NonPersistentAttrs[session.AttrRemoteStorage]; !ws.RemoteStorageDisabled && !ok {
65
remoteStorage, err := storage.NewDirectAccess(&cfg.Storage)
66
if err != nil {
67
return xerrors.Errorf("cannot use configured storage: %w", err)
68
}
69
70
err = remoteStorage.Init(ctx, ws.Owner, ws.WorkspaceID, ws.InstanceID)
71
if err != nil {
72
return xerrors.Errorf("cannot use configured storage: %w", err)
73
}
74
75
err = remoteStorage.EnsureExists(ctx)
76
if err != nil {
77
return xerrors.Errorf("cannot use configured storage: %w", err)
78
}
79
80
ws.NonPersistentAttrs[session.AttrRemoteStorage] = remoteStorage
81
}
82
83
return nil
84
}
85
}
86
87
// hookSetupWorkspaceLocation recreates the workspace location
88
func hookSetupWorkspaceLocation(ctx context.Context, ws *session.Workspace) (err error) {
89
//nolint:ineffassign
90
span, _ := opentracing.StartSpanFromContext(ctx, "hook.SetupWorkspaceLocation")
91
defer tracing.FinishSpan(span, &err)
92
location := ws.Location
93
94
// 1. Clean out the workspace directory
95
if _, err := os.Stat(location); errors.Is(err, fs.ErrNotExist) {
96
// in the very unlikely event that the workspace Pod did not mount (and thus create) the workspace directory, create it
97
err = os.Mkdir(location, 0755)
98
if os.IsExist(err) {
99
log.WithError(err).WithFields(ws.OWI()).WithField("location", location).Debug("ran into non-atomic workspace location existence check")
100
} else if err != nil {
101
return xerrors.Errorf("cannot create workspace: %w", err)
102
}
103
}
104
105
// Chown the workspace directory
106
err = os.Chown(location, initializer.GitpodUID, initializer.GitpodGID)
107
if err != nil {
108
return xerrors.Errorf("cannot create workspace: %w", err)
109
}
110
return nil
111
}
112
113
// hookInstallQuota enforces filesystem quota on the workspace location (if the filesystem supports it)
114
func hookInstallQuota(xfs *quota.XFS, isHard bool) session.WorkspaceLivecycleHook {
115
return func(ctx context.Context, ws *session.Workspace) (err error) {
116
span, _ := opentracing.StartSpanFromContext(ctx, "hook.InstallQuota")
117
defer tracing.FinishSpan(span, &err)
118
119
if xfs == nil {
120
log.WithFields(ws.OWI()).Warn("no xfs definition")
121
return nil
122
}
123
124
if ws.StorageQuota == 0 {
125
log.WithFields(ws.OWI()).Warn("no storage quota defined")
126
return nil
127
}
128
129
size := quota.Size(ws.StorageQuota)
130
131
log.WithFields(ws.OWI()).WithField("isHard", isHard).WithField("size", size).WithField("directory", ws.Location).Debug("setting disk quota")
132
133
var (
134
prj int
135
)
136
if ws.XFSProjectID != 0 {
137
xfs.RegisterProject(ws.XFSProjectID)
138
prj, err = xfs.SetQuotaWithPrjId(ws.Location, size, ws.XFSProjectID, isHard)
139
} else {
140
prj, err = xfs.SetQuota(ws.Location, size, isHard)
141
}
142
143
if err != nil {
144
log.WithFields(ws.OWI()).WithError(err).Warn("cannot enforce workspace size limit")
145
}
146
ws.XFSProjectID = int(prj)
147
148
return nil
149
}
150
}
151
152
// hookRemoveQuota removes the filesystem quota, freeing up resources if need be
153
func hookRemoveQuota(xfs *quota.XFS) session.WorkspaceLivecycleHook {
154
return func(ctx context.Context, ws *session.Workspace) (err error) {
155
span, _ := opentracing.StartSpanFromContext(ctx, "hook.RemoveQuota")
156
defer tracing.FinishSpan(span, &err)
157
158
if xfs == nil {
159
return nil
160
}
161
162
if xfs == nil {
163
return nil
164
}
165
if ws.XFSProjectID == 0 {
166
return nil
167
}
168
169
return xfs.RemoveQuota(ws.XFSProjectID)
170
}
171
}
172
173
func hookWipingTeardown() session.WorkspaceLivecycleHook {
174
return func(ctx context.Context, ws *session.Workspace) error {
175
log := log.WithFields(ws.OWI())
176
177
if !ws.DoWipe {
178
// this is the "default" case for 99% of all workspaces
179
// TODO(gpl): We should probably make this the default for all workspaces - but not with this PR
180
return nil
181
}
182
183
socketFN := filepath.Join(ws.ServiceLocDaemon, "daemon.sock")
184
conn, err := grpc.DialContext(ctx, "unix://"+socketFN, grpc.WithTransportCredentials(insecure.NewCredentials()))
185
if err != nil {
186
log.WithError(err).Error("error connecting to IWS for WipingTeardown")
187
return nil
188
}
189
client := daemonapi.NewInWorkspaceServiceClient(conn)
190
191
res, err := client.WipingTeardown(ctx, &daemonapi.WipingTeardownRequest{
192
DoWipe: ws.DoWipe,
193
})
194
if err != nil {
195
return err
196
}
197
log.WithField("success", res.Success).Debug("wiping teardown done")
198
199
return nil
200
}
201
}
202
203