2022-12-08 13:05:19 -03:00

184 lines
5.0 KiB
Go

// Copyright (c) 2021 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License.AGPL.txt in the project root for license information.
package daemon
import (
"bufio"
"bytes"
"context"
"io/ioutil"
"path/filepath"
"strings"
"sync"
"time"
"golang.org/x/sync/errgroup"
"golang.org/x/sys/unix"
"golang.org/x/xerrors"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/util/retry"
"github.com/gitpod-io/gitpod/common-go/log"
"github.com/gitpod-io/gitpod/ws-daemon/pkg/content"
"github.com/gitpod-io/gitpod/ws-daemon/pkg/dispatch"
"github.com/prometheus/client_golang/prometheus"
)
const (
// propagationGracePeriod is the time we allow on top of a container's deletionGracePeriod
// to make sure the changes propagate on the data plane.
propagationGracePeriod = 10 * time.Second
)
// NewMarkUnmountFallback produces a new MarkUnmountFallback. reg can be nil
func NewMarkUnmountFallback(reg prometheus.Registerer) (*MarkUnmountFallback, error) {
counter := prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "markunmountfallback_active_total",
Help: "counts how often the mark unmount fallback was active",
}, []string{"successful"})
if reg != nil {
err := reg.Register(counter)
if err != nil {
return nil, err
}
}
return &MarkUnmountFallback{
activityCounter: counter,
}, nil
}
// MarkUnmountFallback works around the mount propagation of the ring1 FS mark mount.
// When ws-daemon restarts runc propagates all rootfs mounts to ws-daemon's mount namespace.
// This prevents proper unmounting of the mark mount, hence the rootfs of the workspace container.
//
// To work around this issue we wait pod.terminationGracePeriod + propagationGracePeriod and,
// after which we attempt to unmount the mark mount.
//
// Some clusters might run an older version of containerd, for which we build this workaround.
type MarkUnmountFallback struct {
mu sync.Mutex
handled map[string]struct{}
activityCounter *prometheus.CounterVec
}
// WorkspaceAdded does nothing but implemented the dispatch.Listener interface
func (c *MarkUnmountFallback) WorkspaceAdded(ctx context.Context, ws *dispatch.Workspace) error {
return nil
}
// WorkspaceUpdated gets called when a workspace pod is updated. For containers being deleted, we'll check
// if they're still running after their terminationGracePeriod and if Kubernetes still knows about them.
func (c *MarkUnmountFallback) WorkspaceUpdated(ctx context.Context, ws *dispatch.Workspace) error {
if ws.Pod.DeletionTimestamp == nil {
return nil
}
err := func() error {
c.mu.Lock()
defer c.mu.Unlock()
if c.handled == nil {
c.handled = make(map[string]struct{})
}
if _, exists := c.handled[ws.InstanceID]; exists {
return nil
}
c.handled[ws.InstanceID] = struct{}{}
return nil
}()
if err != nil {
return err
}
var gracePeriod int64
if ws.Pod.DeletionGracePeriodSeconds != nil {
gracePeriod = *ws.Pod.DeletionGracePeriodSeconds
} else {
gracePeriod = 30
}
ttl := time.Duration(gracePeriod)*time.Second + propagationGracePeriod
go func() {
time.Sleep(ttl)
dsp := dispatch.GetFromContext(ctx)
if !dsp.WorkspaceExistsOnNode(ws.InstanceID) {
// container is already gone - all is well
return
}
err := unmountMark(ws.InstanceID)
if err != nil {
log.WithFields(ws.OWI()).WithError(err).Error("cannot unmount mark mount from within ws-daemon")
c.activityCounter.WithLabelValues("false").Inc()
} else {
c.activityCounter.WithLabelValues("true").Inc()
}
// We expect the container to be gone now. Don't keep its referenec in memory.
c.mu.Lock()
delete(c.handled, ws.InstanceID)
c.mu.Unlock()
}()
return nil
}
// if the mark mount still exists in /proc/mounts it means we failed to unmount it and
// we cannot remove the content. As a side effect the pod will stay in Terminating state
func unmountMark(instanceID string) error {
mounts, err := ioutil.ReadFile("/proc/mounts")
if err != nil {
return xerrors.Errorf("cannot read /proc/mounts: %w", err)
}
dir := content.ServiceDirName(instanceID)
path := fromPartialMount(filepath.Join(dir, "mark"), mounts)
// empty path means no mount found
if len(path) == 0 {
return nil
}
// in some scenarios we need to wait for the unmount
var errorFn = func(err error) bool {
return strings.Contains(err.Error(), "device or resource busy")
}
var eg errgroup.Group
for _, p := range path {
// add p as closure so that we can use it inside the Go routine.
p := p
eg.Go(func() error {
return retry.OnError(wait.Backoff{
Steps: 5,
Duration: 1 * time.Second,
Factor: 5.0,
Jitter: 0.1,
}, errorFn, func() error {
return unix.Unmount(p, 0)
})
})
}
return eg.Wait()
}
func fromPartialMount(path string, info []byte) (res []string) {
scanner := bufio.NewScanner(bytes.NewReader(info))
for scanner.Scan() {
mount := strings.Split(scanner.Text(), " ")
if len(mount) < 2 {
continue
}
if strings.Contains(mount[1], path) {
res = append(res, mount[1])
}
}
return res
}