Skip to content

Commit 38350c8

Browse files
committed
[ws-daemon] Support rootfs quota
1 parent 38c3d8e commit 38350c8

File tree

5 files changed

+78
-9
lines changed

5 files changed

+78
-9
lines changed

components/ws-daemon/pkg/container/container.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,8 @@ type ID string
6666
// OptsContainerRootfs provides options for the ContainerRootfs function
6767
type OptsContainerRootfs struct {
6868
Unmapped bool
69+
70+
// UpperDir selects the upperdir of the rootfs, rather than the rootfs mountpoint itself.
71+
// If the container's rootfs is not an overlayfs, an error is returned.
72+
UpperDir bool
6973
}

components/ws-daemon/pkg/container/containerd.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -412,9 +412,16 @@ func (s *Containerd) ContainerRootfs(ctx context.Context, id ID, opts OptsContai
412412
// We can't get the rootfs location on the node from containerd somehow.
413413
// As a workaround we'll look at the node's mount table using the snapshotter key.
414414
// This feels brittle and we should keep looking for a better way.
415-
mnt, err := s.Mounts.GetMountpoint(func(mountPoint string) bool {
415+
matcher := func(mountPoint string) bool {
416416
return strings.Contains(mountPoint, info.SnapshotKey)
417-
})
417+
}
418+
419+
var mnt string
420+
if opts.UpperDir {
421+
mnt, err = s.Mounts.GetUpperdir(matcher)
422+
} else {
423+
mnt, err = s.Mounts.GetMountpoint(matcher)
424+
}
418425
if err != nil {
419426
return
420427
}

components/ws-daemon/pkg/daemon/config.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/gitpod-io/gitpod/ws-daemon/pkg/diskguard"
1111
"github.com/gitpod-io/gitpod/ws-daemon/pkg/hosts"
1212
"github.com/gitpod-io/gitpod/ws-daemon/pkg/iws"
13+
"github.com/gitpod-io/gitpod/ws-daemon/pkg/quota"
1314
"github.com/gitpod-io/gitpod/ws-daemon/pkg/resources"
1415
)
1516

@@ -18,11 +19,12 @@ type Config struct {
1819
Runtime RuntimeConfig `json:"runtime"`
1920
ReadinessSignal ReadinessSignalConfig `json:"readiness"`
2021

21-
Content content.Config `json:"content"`
22-
Uidmapper iws.UidmapperConfig `json:"uidmapper"`
23-
Resources resources.Config `json:"resources"`
24-
Hosts hosts.Config `json:"hosts"`
25-
DiskSpaceGuard diskguard.Config `json:"disk"`
22+
Content content.Config `json:"content"`
23+
Uidmapper iws.UidmapperConfig `json:"uidmapper"`
24+
Resources resources.Config `json:"resources"`
25+
Hosts hosts.Config `json:"hosts"`
26+
DiskSpaceGuard diskguard.Config `json:"disk"`
27+
ContainerRootfsQuota quota.Size `json:"containerRootfsQuota"`
2628
}
2729

2830
type RuntimeConfig struct {

components/ws-daemon/pkg/daemon/daemon.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,26 @@ func NewDaemon(config Config, reg prometheus.Registerer) (*Daemon, error) {
4646
if nodename == "" {
4747
return nil, xerrors.Errorf("NODENAME env var isn't set")
4848
}
49+
4950
cgCustomizer := &CgroupCustomizer{}
5051
cgCustomizer.WithCgroupBasePath(config.Resources.CGroupsBasePath)
5152
markUnmountFallback, err := NewMarkUnmountFallback(reg)
5253
if err != nil {
5354
return nil, err
5455
}
55-
dsptch, err := dispatch.NewDispatch(containerRuntime, clientset, config.Runtime.KubernetesNamespace, nodename,
56+
listener := []dispatch.Listener{
5657
resources.NewDispatchListener(&config.Resources, reg),
5758
cgCustomizer,
5859
markUnmountFallback,
59-
)
60+
}
61+
62+
if config.ContainerRootfsQuota != 0 {
63+
listener = append(listener, &ContainerRootFSQuotaEnforcer{
64+
Quota: config.ContainerRootfsQuota,
65+
})
66+
}
67+
68+
dsptch, err := dispatch.NewDispatch(containerRuntime, clientset, config.Runtime.KubernetesNamespace, nodename, listener...)
6069
if err != nil {
6170
return nil, err
6271
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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 daemon
6+
7+
import (
8+
"context"
9+
10+
"github.com/gitpod-io/gitpod/ws-daemon/pkg/container"
11+
"github.com/gitpod-io/gitpod/ws-daemon/pkg/dispatch"
12+
"github.com/gitpod-io/gitpod/ws-daemon/pkg/quota"
13+
"golang.org/x/xerrors"
14+
)
15+
16+
type ContainerRootFSQuotaEnforcer struct {
17+
Quota quota.Size
18+
}
19+
20+
func (c *ContainerRootFSQuotaEnforcer) WorkspaceAdded(ctx context.Context, ws *dispatch.Workspace) error {
21+
disp := dispatch.GetFromContext(ctx)
22+
if disp == nil {
23+
return xerrors.Errorf("no dispatch available")
24+
}
25+
26+
loc, err := disp.Runtime.ContainerRootfs(ctx, ws.ContainerID, container.OptsContainerRootfs{
27+
Unmapped: false,
28+
UpperDir: true,
29+
})
30+
if err != nil {
31+
return xerrors.Errorf("cannot find container rootfs: %w", err)
32+
}
33+
34+
// TODO(cw); create one FS for all of those operations for performance/memory optimisation
35+
fs, err := quota.NewXFS(loc)
36+
if err != nil {
37+
return xerrors.Errorf("XFS is not supported: %w", err)
38+
}
39+
40+
// TODO(cw): we'll need to clean up the used prjquota's - otherwise we'll run out of them on a busy node
41+
_, err = fs.SetQuota(loc, c.Quota)
42+
if err != nil {
43+
return xerrors.Errorf("cannot enforce rootfs quota: %w", err)
44+
}
45+
46+
return nil
47+
}

0 commit comments

Comments
 (0)