Path: blob/main/components/ws-daemon/pkg/daemon/markunmount.go
2501 views
// Copyright (c) 2021 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 daemon56import (7"bufio"8"bytes"9"context"10"errors"11"io/ioutil"12"path/filepath"13"strings"14"sync"15"time"1617"golang.org/x/sync/errgroup"18"golang.org/x/sys/unix"19"golang.org/x/xerrors"20"k8s.io/apimachinery/pkg/util/wait"21"k8s.io/client-go/util/retry"2223"github.com/gitpod-io/gitpod/common-go/log"24"github.com/gitpod-io/gitpod/ws-daemon/pkg/dispatch"25"github.com/prometheus/client_golang/prometheus"26)2728const (29// propagationGracePeriod is the time we allow on top of a container's deletionGracePeriod30// to make sure the changes propagate on the data plane.31propagationGracePeriod = 10 * time.Second32)3334// NewMarkUnmountFallback produces a new MarkUnmountFallback. reg can be nil35func NewMarkUnmountFallback(reg prometheus.Registerer) (*MarkUnmountFallback, error) {36counter := prometheus.NewCounterVec(prometheus.CounterOpts{37Name: "markunmountfallback_active_total",38Help: "counts how often the mark unmount fallback was active",39}, []string{"successful"})40if reg != nil {41err := reg.Register(counter)42if err != nil {43return nil, err44}45}4647return &MarkUnmountFallback{48activityCounter: counter,49}, nil50}5152// MarkUnmountFallback works around the mount propagation of the ring1 FS mark mount.53// When ws-daemon restarts runc propagates all rootfs mounts to ws-daemon's mount namespace.54// This prevents proper unmounting of the mark mount, hence the rootfs of the workspace container.55//56// To work around this issue we wait pod.terminationGracePeriod + propagationGracePeriod and,57// after which we attempt to unmount the mark mount.58//59// Some clusters might run an older version of containerd, for which we build this workaround.60type MarkUnmountFallback struct {61mu sync.Mutex62handled map[string]struct{}6364activityCounter *prometheus.CounterVec65}6667// WorkspaceAdded does nothing but implemented the dispatch.Listener interface68func (c *MarkUnmountFallback) WorkspaceAdded(ctx context.Context, ws *dispatch.Workspace) error {69return nil70}7172// WorkspaceUpdated gets called when a workspace pod is updated. For containers being deleted, we'll check73// if they're still running after their terminationGracePeriod and if Kubernetes still knows about them.74func (c *MarkUnmountFallback) WorkspaceUpdated(ctx context.Context, ws *dispatch.Workspace) error {75if ws.Pod.DeletionTimestamp == nil {76return nil77}7879err := func() error {80c.mu.Lock()81defer c.mu.Unlock()8283if c.handled == nil {84c.handled = make(map[string]struct{})85}86if _, exists := c.handled[ws.InstanceID]; exists {87return nil88}89c.handled[ws.InstanceID] = struct{}{}90return nil91}()92if err != nil {93return err94}9596var gracePeriod int6497if ws.Pod.DeletionGracePeriodSeconds != nil {98gracePeriod = *ws.Pod.DeletionGracePeriodSeconds99} else {100gracePeriod = 30101}102ttl := time.Duration(gracePeriod)*time.Second + propagationGracePeriod103104dispatch.GetDispatchWaitGroup(ctx).Add(1)105go func() {106defer dispatch.GetDispatchWaitGroup(ctx).Done()107108defer func() {109// We expect the container to be gone now. Don't keep its referenec in memory.110c.mu.Lock()111delete(c.handled, ws.InstanceID)112c.mu.Unlock()113}()114115wait := time.NewTicker(ttl)116defer wait.Stop()117select {118case <-ctx.Done():119return120case <-wait.C:121}122123dsp := dispatch.GetFromContext(ctx)124if !dsp.WorkspaceExistsOnNode(ws.InstanceID) {125// container is already gone - all is well126return127}128129err := unmountMark(ws.InstanceID)130if err != nil && errors.Is(err, context.Canceled) {131log.WithFields(ws.OWI()).WithError(err).Error("cannot unmount mark mount from within ws-daemon")132c.activityCounter.WithLabelValues("false").Inc()133} else {134c.activityCounter.WithLabelValues("true").Inc()135}136}()137138return nil139}140141// if the mark mount still exists in /proc/mounts it means we failed to unmount it and142// we cannot remove the content. As a side effect the pod will stay in Terminating state143func unmountMark(instanceID string) error {144mounts, err := ioutil.ReadFile("/proc/mounts")145if err != nil {146return xerrors.Errorf("cannot read /proc/mounts: %w", err)147}148149dir := instanceID + "-daemon"150path := fromPartialMount(filepath.Join(dir, "mark"), mounts)151// empty path means no mount found152if len(path) == 0 {153return nil154}155156// in some scenarios we need to wait for the unmount157var canRetryFn = func(err error) bool {158if !strings.Contains(err.Error(), "device or resource busy") {159log.WithError(err).WithFields(log.OWI("", "", instanceID)).Info("Will not retry unmount mark")160}161return strings.Contains(err.Error(), "device or resource busy")162}163164var eg errgroup.Group165for _, p := range path {166// add p as closure so that we can use it inside the Go routine.167p := p168eg.Go(func() error {169return retry.OnError(wait.Backoff{170Steps: 5,171Duration: 1 * time.Second,172Factor: 5.0,173Jitter: 0.1,174}, canRetryFn, func() error {175return unix.Unmount(p, 0)176})177})178}179return eg.Wait()180}181182func fromPartialMount(path string, info []byte) (res []string) {183scanner := bufio.NewScanner(bytes.NewReader(info))184for scanner.Scan() {185mount := strings.Split(scanner.Text(), " ")186if len(mount) < 2 {187continue188}189190if strings.Contains(mount[1], path) {191res = append(res, mount[1])192}193}194195return res196}197198199