Skip to content

Commit 9088f42

Browse files
authored
Merge pull request #5346 from twz123/backport-5300-to-release-1.29
[Backport release-1.29] Fix deletion of persistent data with k0s reset
2 parents e0afa89 + 40e07bf commit 9088f42

File tree

7 files changed

+212
-17
lines changed

7 files changed

+212
-17
lines changed

docs/reset.md

+5-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ following:
1010
* Processes and containers: Terminates all running k0s processes to ensure that
1111
there are no active components left. This includes all container processes
1212
managed by the Container Runtime.
13+
* Mounts under k0s data directory: In order to prevent persistent data to be
14+
deleted, all mount points under k0s' data directory will be unmounted. If an
15+
unmount fails, it will be unmounted lazy.
1316
* Data stored on the node: Deletes the whole k0s data directory, which includes
1417
* all k0s-related configuration files, including those used for cluster setup
1518
and node-specific settings,
@@ -23,8 +26,8 @@ following:
2326
reboot the host after a reset to ensure that there are no k0s remnants in the
2427
host's network configuration.
2528
* Registration with the host's init system: Reverts the registration done by
26-
`k0s install`. After a reset, k0s won't be automatically started when the host
27-
boots.
29+
`k0s install`. After a reset, k0s won't be automatically started when the
30+
host boots.
2831

2932
After a successful reset, the k0s binary itself remains. It can then be used to
3033
join another cluster or create a new one.

inttest/reset/clutter-data-dir.sh

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# SPDX-FileCopyrightText: 2024 k0s authors
3+
#shellcheck shell=ash
4+
5+
set -eu
6+
7+
make_dir() { mkdir -- "$1" && echo "$1"; }
8+
make_file() { echo "$1" >"$1" && echo "$1"; }
9+
10+
make_bind_mounts() {
11+
local real="$1"
12+
local target="$2"
13+
14+
# Directory bind mount
15+
make_dir "$real/real_dir"
16+
make_file "$real/real_dir/real_dir_info.txt"
17+
make_dir "$target/bind_dir"
18+
mount --bind -- "$real/real_dir" "$target/bind_dir"
19+
20+
# File bind mount
21+
make_file "$real/real_file.txt"
22+
make_file "$target/bind_file.txt"
23+
mount --bind -- "$real/real_file.txt" "$target/bind_file.txt"
24+
25+
# Recursive directory bind mount
26+
make_dir "$real/real_recursive_dir"
27+
make_file "$real/real_recursive_dir/real_recursive_dir.txt"
28+
make_dir "$real/real_recursive_dir/bind_dir"
29+
mount --bind -- "$real/real_dir" "$real/real_recursive_dir/bind_dir"
30+
make_file "$real/real_recursive_dir/bind_file.txt"
31+
mount --bind -- "$real/real_file.txt" "$real/real_recursive_dir/bind_file.txt"
32+
make_dir "$target/rbind_dir"
33+
mount --rbind -- "$real/real_recursive_dir" "$target/rbind_dir"
34+
35+
# Directory overmounts
36+
make_dir "$real/overmount_dir"
37+
make_file "$real/overmount_dir/in_overmount_dir.txt"
38+
mount --bind -- "$real/overmount_dir" "$target/bind_dir"
39+
40+
# File overmounts
41+
make_file "$real/overmount_file.txt"
42+
mount --bind -- "$real/overmount_file.txt" "$target/bind_file.txt"
43+
}
44+
45+
clutter() {
46+
local dataDir="$1"
47+
local realDir
48+
49+
realDir="$(mktemp -t -d k0s_reset_inttest.XXXXXX)"
50+
51+
local dir="$dataDir"/cluttered
52+
make_dir "$dir"
53+
54+
# Directories and files with restricted permissions
55+
make_dir "$dir/restricted_dir"
56+
make_file "$dir/restricted_dir/no_read_file.txt"
57+
chmod 000 -- "$dir/restricted_dir/no_read_file.txt" # No permissions on the file
58+
make_dir "$dir/restricted_dir/no_exec_dir"
59+
chmod 000 -- "$dir/restricted_dir/no_exec_dir" # No permissions on the directory
60+
make_dir "$dir/restricted_dir/no_exec_nonempty_dir"
61+
make_file "$dir/restricted_dir/no_exec_nonempty_dir/.hidden_file"
62+
chmod 000 -- "$dir/restricted_dir/no_exec_nonempty_dir" # No permissions on the directory
63+
64+
# Symlinks pointing outside the directory tree
65+
make_dir "$realDir/some_dir"
66+
make_file "$realDir/some_dir/real_file.txt"
67+
ln -s -- "$realDir/some_dir/real_file.txt" "$dir/symlink_to_file" # Symlink to a file
68+
ln -s -- "$realDir/some_dir" "$dir/symlink_to_dir" # Symlink to a directory
69+
70+
# Bind mounts pointing outside the directory tree
71+
make_bind_mounts "$realDir" "$dir"
72+
73+
# Bind mounts outside the directory tree pointing into it
74+
# make_bind_mounts "$dir" "$realDir"
75+
}
76+
77+
clutter "$@"

inttest/reset/reset_test.go

+42-5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ limitations under the License.
1717
package reset
1818

1919
import (
20+
"bytes"
21+
_ "embed"
22+
"fmt"
23+
"io"
24+
"strings"
2025
"testing"
2126

2227
testifysuite "github.com/stretchr/testify/suite"
@@ -28,11 +33,14 @@ type suite struct {
2833
common.BootlooseSuite
2934
}
3035

36+
//go:embed clutter-data-dir.sh
37+
var clutterScript []byte
38+
3139
func (s *suite) TestReset() {
3240
ctx := s.Context()
3341
workerNode := s.WorkerNode(0)
3442

35-
if ok := s.Run("k0s gets up", func() {
43+
if !s.Run("k0s gets up", func() {
3644
s.Require().NoError(s.InitController(0, "--disable-components=konnectivity-server,metrics-server"))
3745
s.Require().NoError(s.RunWorkers())
3846

@@ -44,11 +52,7 @@ func (s *suite) TestReset() {
4452

4553
s.T().Log("waiting to see CNI pods ready")
4654
s.NoError(common.WaitForKubeRouterReady(ctx, kc), "CNI did not start")
47-
}); !ok {
48-
return
49-
}
5055

51-
s.Run("k0s reset", func() {
5256
ssh, err := s.SSH(ctx, workerNode)
5357
s.Require().NoError(err)
5458
defer ssh.Disconnect()
@@ -57,14 +61,47 @@ func (s *suite) TestReset() {
5761
s.NoError(ssh.Exec(ctx, "test -d /run/k0s", common.SSHStreams{}), "/run/k0s is not a directory")
5862

5963
s.NoError(ssh.Exec(ctx, "pidof containerd-shim-runc-v2 >&2", common.SSHStreams{}), "Expected some running containerd shims")
64+
}) {
65+
return
66+
}
6067

68+
var clutteringPaths bytes.Buffer
69+
70+
if !s.Run("prepare k0s reset", func() {
6171
s.NoError(s.StopWorker(workerNode), "Failed to stop k0s")
6272

73+
ssh, err := s.SSH(ctx, workerNode)
74+
s.Require().NoError(err)
75+
defer ssh.Disconnect()
76+
77+
streams, flushStreams := common.TestLogStreams(s.T(), "clutter data dir")
78+
streams.In = bytes.NewReader(clutterScript)
79+
streams.Out = io.MultiWriter(&clutteringPaths, streams.Out)
80+
err = ssh.Exec(ctx, "sh -s -- /var/lib/k0s", streams)
81+
flushStreams()
82+
s.Require().NoError(err)
83+
}) {
84+
return
85+
}
86+
87+
s.Run("k0s reset", func() {
88+
ssh, err := s.SSH(ctx, workerNode)
89+
s.Require().NoError(err)
90+
defer ssh.Disconnect()
91+
6392
streams, flushStreams := common.TestLogStreams(s.T(), "reset")
6493
err = ssh.Exec(ctx, "k0s reset --debug", streams)
6594
flushStreams()
6695
s.NoError(err, "k0s reset didn't exit cleanly")
6796

97+
for _, path := range strings.Split(string(bytes.TrimSpace(clutteringPaths.Bytes())), "\n") {
98+
if strings.HasPrefix(path, "/var/lib/k0s") {
99+
s.NoError(ssh.Exec(ctx, fmt.Sprintf("! test -e %q", path), common.SSHStreams{}), "Failed to verify non-existence of %s", path)
100+
} else {
101+
s.NoError(ssh.Exec(ctx, fmt.Sprintf("test -e %q", path), common.SSHStreams{}), "Failed to verify existence of %s", path)
102+
}
103+
}
104+
68105
// /var/lib/k0s is a mount point in the Docker container and can't be deleted, so it must be empty
69106
s.NoError(ssh.Exec(ctx, `x="$(ls -A /var/lib/k0s)" && echo "$x" >&2 && [ -z "$x" ]`, common.SSHStreams{}), "/var/lib/k0s is not empty")
70107
s.NoError(ssh.Exec(ctx, "! test -e /run/k0s", common.SSHStreams{}), "/run/k0s still exists")

pkg/cleanup/containers.go

-3
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,6 @@ func (c *containers) stopAllContainers() error {
153153
return fmt.Errorf("failed at listing pods %w", err)
154154
}
155155
if len(pods) > 0 {
156-
if err := removeMount("kubelet/pods"); err != nil {
157-
msg = append(msg, err)
158-
}
159156
if err := removeMount("run/netns"); err != nil {
160157
msg = append(msg, err)
161158
}

pkg/cleanup/directories.go

+38-7
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"fmt"
2222
"os"
2323
"path/filepath"
24+
"strings"
2425

2526
"github.com/sirupsen/logrus"
2627
"k8s.io/mount-utils"
@@ -46,15 +47,39 @@ func (d *directories) Run() error {
4647

4748
var dataDirMounted bool
4849

49-
// search and unmount kubelet volume mounts
50-
for _, v := range procMounts {
51-
if v.Path == filepath.Join(d.Config.dataDir, "kubelet") {
50+
// ensure that we don't delete any persistent data volumes that may be
51+
// mounted by kubernetes by unmount every mount point under DataDir.
52+
//
53+
// Unmount in the reverse order it was mounted so we handle recursive
54+
// bind mounts and over mounts properly. If we for any reason are not
55+
// able to unmount, fall back to lazy unmount and if that also fails
56+
// bail out and don't delete anything.
57+
//
58+
// Note that if there are any shared bind mounts under k0s data
59+
// directory, we may end up unmounting stuff outside the k0s DataDir.
60+
// If someone has set a bind mount to be shared, we assume that is the
61+
// desired behavior. See MS_SHARED and NOTES:
62+
// - https://man7.org/linux/man-pages/man2/mount.2.html
63+
// - https://man7.org/linux/man-pages/man2/umount.2.html#NOTES
64+
for i := len(procMounts) - 1; i >= 0; i-- {
65+
v := procMounts[i]
66+
// avoid unmount datadir if its mounted on separate partition
67+
// k0s didn't mount it so leave it alone
68+
if v.Path == d.Config.k0sVars.DataDir {
69+
dataDirMounted = true
70+
continue
71+
}
72+
if isUnderPath(v.Path, filepath.Join(d.Config.dataDir, "kubelet")) || isUnderPath(v.Path, d.Config.k0sVars.DataDir) {
5273
logrus.Debugf("%v is mounted! attempting to unmount...", v.Path)
5374
if err = mounter.Unmount(v.Path); err != nil {
54-
logrus.Warningf("failed to unmount %v", v.Path)
75+
// if we fail to unmount, try lazy unmount so
76+
// we don't end up deleting stuff that we
77+
// shouldn't
78+
logrus.Warningf("lazy unmounting %v", v.Path)
79+
if err = UnmountLazy(v.Path); err != nil {
80+
return fmt.Errorf("failed unmount %v", v.Path)
81+
}
5582
}
56-
} else if v.Path == d.Config.dataDir {
57-
dataDirMounted = true
5883
}
5984
}
6085

@@ -81,7 +106,13 @@ func (d *directories) Run() error {
81106
return nil
82107
}
83108

84-
// this is for checking if the error retrned by os.RemoveAll is due to
109+
// test if the path is a directory equal to or under base
110+
func isUnderPath(path, base string) bool {
111+
rel, err := filepath.Rel(base, path)
112+
return err == nil && !strings.HasPrefix(rel, "..") && !filepath.IsAbs(rel)
113+
}
114+
115+
// this is for checking if the error returned by os.RemoveAll is due to
85116
// it being a mount point. if it is, we can ignore the error. this way
86117
// we can't rely on os.RemoveAll instead of recursively deleting the
87118
// contents of the directory

pkg/cleanup/unmount_unix.go

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//go:build unix
2+
3+
/*
4+
Copyright 2024 k0s authors
5+
6+
Licensed under the Apache License, Version 2.0 (the "License");
7+
you may not use this file except in compliance with the License.
8+
You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
*/
18+
19+
package cleanup
20+
21+
import (
22+
"golang.org/x/sys/unix"
23+
)
24+
25+
func UnmountLazy(path string) error {
26+
return unix.Unmount(path, unix.MNT_DETACH)
27+
}

pkg/cleanup/unmount_windows.go

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
Copyright 2024 k0s authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package cleanup
18+
19+
import "fmt"
20+
21+
func UnmountLazy(path string) error {
22+
return fmt.Errorf("lazy unmount is not supported on Windows for path: %s", path)
23+
}

0 commit comments

Comments
 (0)