diff --git a/envbuilder.go b/envbuilder.go index 3ecbf88a..0c2bd2a0 100644 --- a/envbuilder.go +++ b/envbuilder.go @@ -36,6 +36,7 @@ import ( "github.com/GoogleContainerTools/kaniko/pkg/util" "github.com/coder/coder/v2/codersdk" "github.com/coder/envbuilder/devcontainer" + "github.com/coder/envbuilder/internal/ebutil" "github.com/containerd/containerd/platforms" "github.com/distribution/distribution/v3/configuration" "github.com/distribution/distribution/v3/registry/handlers" @@ -401,6 +402,19 @@ func Run(ctx context.Context, options Options) error { }) } + // temp move of all ro mounts + tempRemountDest := filepath.Join("/", MagicDir, "mnt") + ignorePrefixes := []string{tempRemountDest, "/proc", "/sys"} + restoreMounts, err := ebutil.TempRemount(options.Logger, tempRemountDest, ignorePrefixes...) + defer func() { // restoreMounts should never be nil + if err := restoreMounts(); err != nil { + options.Logger(codersdk.LogLevelError, "restore mounts: %s", err.Error()) + } + }() + if err != nil { + return fmt.Errorf("temp remount: %w", err) + } + skippedRebuild := false build := func() (v1.Image, error) { _, err := options.Filesystem.Stat(MagicFile) @@ -547,6 +561,10 @@ func Run(ctx context.Context, options Options) error { closeAfterBuild() } + if err := restoreMounts(); err != nil { + return fmt.Errorf("restore mounts: %w", err) + } + // Create the magic file to indicate that this build // has already been ran before! file, err := options.Filesystem.Create(MagicFile) diff --git a/go.mod b/go.mod index 4a61ec6c..703ee109 100644 --- a/go.mod +++ b/go.mod @@ -30,14 +30,17 @@ require ( github.com/go-git/go-billy/v5 v5.5.0 github.com/go-git/go-git/v5 v5.12.0 github.com/google/go-containerregistry v0.15.2 + github.com/hashicorp/go-multierror v1.1.1 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/mattn/go-isatty v0.0.20 github.com/moby/buildkit v0.11.6 github.com/otiai10/copy v1.14.0 + github.com/prometheus/procfs v0.12.0 github.com/sirupsen/logrus v1.9.3 github.com/skeema/knownhosts v1.2.2 github.com/stretchr/testify v1.9.0 github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a + go.uber.org/mock v0.4.0 golang.org/x/crypto v0.22.0 golang.org/x/sync v0.7.0 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 @@ -155,7 +158,6 @@ require ( github.com/hashicorp/go-hclog v1.5.0 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-memdb v1.3.2 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/go-version v1.6.0 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect @@ -221,7 +223,6 @@ require ( github.com/prometheus/client_golang v1.18.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.46.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052 // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 9fecac8b..d957a9b4 100644 --- a/go.sum +++ b/go.sum @@ -910,6 +910,8 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA= diff --git a/internal/ebutil/mock_mounter_test.go b/internal/ebutil/mock_mounter_test.go new file mode 100644 index 00000000..3386d56e --- /dev/null +++ b/internal/ebutil/mock_mounter_test.go @@ -0,0 +1,100 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: remount.go +// +// Generated by this command: +// +// mockgen -source=remount.go -package=ebutil -destination=mock_mounter_test.go -write_generate_directive +// + +// Package ebutil is a generated GoMock package. +package ebutil + +import ( + os "os" + reflect "reflect" + + procfs "github.com/prometheus/procfs" + gomock "go.uber.org/mock/gomock" +) + +//go:generate mockgen -source=remount.go -package=ebutil -destination=mock_mounter_test.go -write_generate_directive + +// Mockmounter is a mock of mounter interface. +type Mockmounter struct { + ctrl *gomock.Controller + recorder *MockmounterMockRecorder +} + +// MockmounterMockRecorder is the mock recorder for Mockmounter. +type MockmounterMockRecorder struct { + mock *Mockmounter +} + +// NewMockmounter creates a new mock instance. +func NewMockmounter(ctrl *gomock.Controller) *Mockmounter { + mock := &Mockmounter{ctrl: ctrl} + mock.recorder = &MockmounterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *Mockmounter) EXPECT() *MockmounterMockRecorder { + return m.recorder +} + +// GetMounts mocks base method. +func (m *Mockmounter) GetMounts() ([]*procfs.MountInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMounts") + ret0, _ := ret[0].([]*procfs.MountInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMounts indicates an expected call of GetMounts. +func (mr *MockmounterMockRecorder) GetMounts() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMounts", reflect.TypeOf((*Mockmounter)(nil).GetMounts)) +} + +// MkdirAll mocks base method. +func (m *Mockmounter) MkdirAll(arg0 string, arg1 os.FileMode) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MkdirAll", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// MkdirAll indicates an expected call of MkdirAll. +func (mr *MockmounterMockRecorder) MkdirAll(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MkdirAll", reflect.TypeOf((*Mockmounter)(nil).MkdirAll), arg0, arg1) +} + +// Mount mocks base method. +func (m *Mockmounter) Mount(arg0, arg1, arg2 string, arg3 uintptr, arg4 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Mount", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].(error) + return ret0 +} + +// Mount indicates an expected call of Mount. +func (mr *MockmounterMockRecorder) Mount(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Mount", reflect.TypeOf((*Mockmounter)(nil).Mount), arg0, arg1, arg2, arg3, arg4) +} + +// Unmount mocks base method. +func (m *Mockmounter) Unmount(arg0 string, arg1 int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Unmount", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Unmount indicates an expected call of Unmount. +func (mr *MockmounterMockRecorder) Unmount(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unmount", reflect.TypeOf((*Mockmounter)(nil).Unmount), arg0, arg1) +} diff --git a/internal/ebutil/remount.go b/internal/ebutil/remount.go new file mode 100644 index 00000000..056a1057 --- /dev/null +++ b/internal/ebutil/remount.go @@ -0,0 +1,134 @@ +package ebutil + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "sync" + "syscall" + + "github.com/coder/coder/v2/codersdk" + "github.com/hashicorp/go-multierror" + "github.com/prometheus/procfs" +) + +// TempRemount iterates through all read-only mounted filesystems, bind-mounts them at dest, +// and unmounts them from their original source. All mount points underneath ignorePrefixes +// will not be touched. +// +// Some container runtimes such as sysbox-runc will mount in `/lib/modules` read-only. +// See https://github.com/nestybox/sysbox/issues/564 +// This trips us up because: +// 1. We call a Kaniko library function `util.DeleteFilesystem` that does exactly what it says +// on the tin. If this hits a read-only volume mounted in, unhappiness is the result. +// 2. After deleting the filesystem and building the image, we extract it to the filesystem. +// If some paths mounted in via volume are present at that time, unhappiness is also likely +// to result -- especially in case of read-only mounts. +// +// To work around this we move the mounts out of the way temporarily by bind-mounting them +// while we do our thing, and move them back when we're done. +// +// It is the responsibility of the caller to call the returned function +// to restore the original mount points. If an error is encountered while attempting to perform +// the operation, calling the returned function will make a best-effort attempt to restore +// the original state. +func TempRemount(logf func(codersdk.LogLevel, string, ...any), dest string, ignorePrefixes ...string) (restore func() error, err error, +) { + return tempRemount(&realMounter{}, logf, dest, ignorePrefixes...) +} + +func tempRemount(m mounter, logf func(codersdk.LogLevel, string, ...any), base string, ignorePrefixes ...string) (restore func() error, err error) { + mountInfos, err := m.GetMounts() + if err != nil { + return func() error { return nil }, fmt.Errorf("get mounts: %w", err) + } + + // temp move of all ro mounts + mounts := map[string]string{} + var restoreOnce sync.Once + var merr error + // closer to attempt to restore original mount points + restore = func() error { + restoreOnce.Do(func() { + for orig, moved := range mounts { + if err := remount(m, moved, orig); err != nil { + merr = multierror.Append(merr, fmt.Errorf("restore mount: %w", err)) + } + } + }) + return merr + } + +outer: + for _, mountInfo := range mountInfos { + // TODO: do this for all mounts + if _, ok := mountInfo.Options["ro"]; !ok { + logf(codersdk.LogLevelTrace, "skip rw mount %s", mountInfo.MountPoint) + continue + } + + for _, prefix := range ignorePrefixes { + if strings.HasPrefix(mountInfo.MountPoint, prefix) { + logf(codersdk.LogLevelTrace, "skip mount %s under ignored prefix %s", mountInfo.MountPoint, prefix) + continue outer + } + } + + src := mountInfo.MountPoint + dest := filepath.Join(base, src) + if err := remount(m, src, dest); err != nil { + return restore, fmt.Errorf("temp remount: %w", err) + } + + mounts[src] = dest + } + + return restore, nil +} + +func remount(m mounter, src, dest string) error { + if err := m.MkdirAll(dest, 0o750); err != nil { + return fmt.Errorf("ensure path: %w", err) + } + if err := m.Mount(src, dest, "bind", syscall.MS_BIND, ""); err != nil { + return fmt.Errorf("bind mount %s => %s: %w", src, dest, err) + } + if err := m.Unmount(src, 0); err != nil { + return fmt.Errorf("unmount orig src %s: %w", src, err) + } + return nil +} + +// mounter is an interface to system-level calls used by TempRemount. +type mounter interface { + // GetMounts wraps procfs.GetMounts + GetMounts() ([]*procfs.MountInfo, error) + // MkdirAll wraps os.MkdirAll + MkdirAll(string, os.FileMode) error + // Mount wraps syscall.Mount + Mount(string, string, string, uintptr, string) error + // Unmount wraps syscall.Unmount + Unmount(string, int) error +} + +// realMounter implements mounter and actually does the thing. +type realMounter struct{} + +var _ mounter = &realMounter{} + +func (m *realMounter) Mount(src string, dest string, fstype string, flags uintptr, data string) error { + return syscall.Mount(src, dest, fstype, flags, data) +} + +func (m *realMounter) Unmount(tgt string, flags int) error { + return syscall.Unmount(tgt, flags) +} + +func (m *realMounter) GetMounts() ([]*procfs.MountInfo, error) { + return procfs.GetMounts() +} + +func (m *realMounter) MkdirAll(path string, perm os.FileMode) error { + return os.MkdirAll(path, perm) +} diff --git a/internal/ebutil/remount_internal_test.go b/internal/ebutil/remount_internal_test.go new file mode 100644 index 00000000..911aabee --- /dev/null +++ b/internal/ebutil/remount_internal_test.go @@ -0,0 +1,202 @@ +package ebutil + +import ( + "os" + "strings" + "syscall" + "testing" + + "github.com/coder/coder/v2/codersdk" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/prometheus/procfs" +) + +func Test_tempRemount(t *testing.T) { + t.Parallel() + + t.Run("OK", func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mm := NewMockmounter(ctrl) + mounts := fakeMounts("/home", "/var/lib/modules:ro", "/proc", "/sys") + + mm.EXPECT().GetMounts().Return(mounts, nil) + mm.EXPECT().MkdirAll("/.test/var/lib/modules", os.FileMode(0o750)).Times(1).Return(nil) + mm.EXPECT().Mount("/var/lib/modules", "/.test/var/lib/modules", "bind", uintptr(syscall.MS_BIND), "").Times(1).Return(nil) + mm.EXPECT().Unmount("/var/lib/modules", 0).Times(1).Return(nil) + mm.EXPECT().MkdirAll("/var/lib/modules", os.FileMode(0o750)).Times(1).Return(nil) + mm.EXPECT().Mount("/.test/var/lib/modules", "/var/lib/modules", "bind", uintptr(syscall.MS_BIND), "").Times(1).Return(nil) + mm.EXPECT().Unmount("/.test/var/lib/modules", 0).Times(1).Return(nil) + + remount, err := tempRemount(mm, fakeLog(t), "/.test") + require.NoError(t, err) + err = remount() + require.NoError(t, err) + // sync.Once should handle multiple remount calls + _ = remount() + }) + + t.Run("IgnorePrefixes", func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mm := NewMockmounter(ctrl) + mounts := fakeMounts("/home", "/var/lib/modules:ro", "/proc", "/sys") + + mm.EXPECT().GetMounts().Return(mounts, nil) + + remount, err := tempRemount(mm, fakeLog(t), "/.test", "/var/lib") + require.NoError(t, err) + err = remount() + require.NoError(t, err) + }) + + t.Run("ErrGetMounts", func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mm := NewMockmounter(ctrl) + mm.EXPECT().GetMounts().Return(nil, assert.AnError) + remount, err := tempRemount(mm, fakeLog(t), "/.test", "/var/lib") + require.ErrorContains(t, err, assert.AnError.Error()) + err = remount() + require.NoError(t, err) + }) + + t.Run("ErrMkdirAll", func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mm := NewMockmounter(ctrl) + mounts := fakeMounts("/home", "/var/lib/modules:ro", "/proc", "/sys") + + mm.EXPECT().GetMounts().Return(mounts, nil) + mm.EXPECT().MkdirAll("/.test/var/lib/modules", os.FileMode(0o750)).Times(1).Return(assert.AnError) + + remount, err := tempRemount(mm, fakeLog(t), "/.test") + require.ErrorContains(t, err, assert.AnError.Error()) + err = remount() + require.NoError(t, err) + }) + + t.Run("ErrMountBind", func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mm := NewMockmounter(ctrl) + mounts := fakeMounts("/home", "/var/lib/modules:ro", "/proc", "/sys") + + mm.EXPECT().GetMounts().Return(mounts, nil) + mm.EXPECT().MkdirAll("/.test/var/lib/modules", os.FileMode(0o750)).Times(1).Return(nil) + mm.EXPECT().Mount("/var/lib/modules", "/.test/var/lib/modules", "bind", uintptr(syscall.MS_BIND), "").Times(1).Return(assert.AnError) + + remount, err := tempRemount(mm, fakeLog(t), "/.test") + require.ErrorContains(t, err, assert.AnError.Error()) + err = remount() + require.NoError(t, err) + }) + + t.Run("ErrUnmount", func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mm := NewMockmounter(ctrl) + mounts := fakeMounts("/home", "/var/lib/modules:ro", "/proc", "/sys") + + mm.EXPECT().GetMounts().Return(mounts, nil) + mm.EXPECT().MkdirAll("/.test/var/lib/modules", os.FileMode(0o750)).Times(1).Return(nil) + mm.EXPECT().Mount("/var/lib/modules", "/.test/var/lib/modules", "bind", uintptr(syscall.MS_BIND), "").Times(1).Return(nil) + mm.EXPECT().Unmount("/var/lib/modules", 0).Times(1).Return(assert.AnError) + + remount, err := tempRemount(mm, fakeLog(t), "/.test") + require.ErrorContains(t, err, assert.AnError.Error()) + err = remount() + require.NoError(t, err) + }) + + t.Run("ErrRemountMkdirAll", func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mm := NewMockmounter(ctrl) + mounts := fakeMounts("/home", "/var/lib/modules:ro", "/proc", "/sys") + + mm.EXPECT().GetMounts().Return(mounts, nil) + mm.EXPECT().MkdirAll("/.test/var/lib/modules", os.FileMode(0o750)).Times(1).Return(nil) + mm.EXPECT().Mount("/var/lib/modules", "/.test/var/lib/modules", "bind", uintptr(syscall.MS_BIND), "").Times(1).Return(nil) + mm.EXPECT().Unmount("/var/lib/modules", 0).Times(1).Return(nil) + mm.EXPECT().MkdirAll("/var/lib/modules", os.FileMode(0o750)).Times(1).Return(assert.AnError) + + remount, err := tempRemount(mm, fakeLog(t), "/.test") + require.NoError(t, err) + err = remount() + require.ErrorContains(t, err, assert.AnError.Error()) + }) + + t.Run("ErrRemountMountBind", func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mm := NewMockmounter(ctrl) + mounts := fakeMounts("/home", "/var/lib/modules:ro", "/proc", "/sys") + + mm.EXPECT().GetMounts().Return(mounts, nil) + mm.EXPECT().MkdirAll("/.test/var/lib/modules", os.FileMode(0o750)).Times(1).Return(nil) + mm.EXPECT().Mount("/var/lib/modules", "/.test/var/lib/modules", "bind", uintptr(syscall.MS_BIND), "").Times(1).Return(nil) + mm.EXPECT().Unmount("/var/lib/modules", 0).Times(1).Return(nil) + mm.EXPECT().MkdirAll("/var/lib/modules", os.FileMode(0o750)).Times(1).Return(nil) + mm.EXPECT().Mount("/.test/var/lib/modules", "/var/lib/modules", "bind", uintptr(syscall.MS_BIND), "").Times(1).Return(assert.AnError) + + remount, err := tempRemount(mm, fakeLog(t), "/.test") + require.NoError(t, err) + err = remount() + require.ErrorContains(t, err, assert.AnError.Error()) + }) + + t.Run("ErrRemountUnmount", func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mm := NewMockmounter(ctrl) + mounts := fakeMounts("/home", "/var/lib/modules:ro", "/proc", "/sys") + + mm.EXPECT().GetMounts().Return(mounts, nil) + mm.EXPECT().MkdirAll("/.test/var/lib/modules", os.FileMode(0o750)).Times(1).Return(nil) + mm.EXPECT().Mount("/var/lib/modules", "/.test/var/lib/modules", "bind", uintptr(syscall.MS_BIND), "").Times(1).Return(nil) + mm.EXPECT().Unmount("/var/lib/modules", 0).Times(1).Return(nil) + mm.EXPECT().MkdirAll("/var/lib/modules", os.FileMode(0o750)).Times(1).Return(nil) + mm.EXPECT().Mount("/.test/var/lib/modules", "/var/lib/modules", "bind", uintptr(syscall.MS_BIND), "").Times(1).Return(nil) + mm.EXPECT().Unmount("/.test/var/lib/modules", 0).Times(1).Return(assert.AnError) + + remount, err := tempRemount(mm, fakeLog(t), "/.test") + require.NoError(t, err) + err = remount() + require.ErrorContains(t, err, assert.AnError.Error()) + }) +} + +// convenience function for generating a slice of *procfs.MountInfo +func fakeMounts(mounts ...string) []*procfs.MountInfo { + m := make([]*procfs.MountInfo, 0) + for _, s := range mounts { + mp := s + o := make(map[string]string) + if strings.HasSuffix(mp, ":ro") { + mp = strings.TrimSuffix(mp, ":ro") + o["ro"] = "true" + } + m = append(m, &procfs.MountInfo{MountPoint: mp, Options: o}) + } + return m +} + +func fakeLog(t *testing.T) func(codersdk.LogLevel, string, ...any) { + t.Helper() + return func(_ codersdk.LogLevel, s string, a ...any) { + t.Logf(s, a...) + } +}