Skip to content

Commit 032d04a

Browse files
committed
Add atomicdir package
pkg/operator/staticpod/internal/atomicdir contains internal helpers for performing atomic operations on directories. This patch contains only a standalone swap function, which will be subsequenty used for synchronizing directory with the given state.
1 parent 42e91dd commit 032d04a

File tree

4 files changed

+285
-1
lines changed

4 files changed

+285
-1
lines changed

pkg/operator/staticpod/installerpod/cmd.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ package installerpod
33
import (
44
"context"
55
"fmt"
6-
"k8s.io/utils/clock"
76
"os"
87
"path"
98
"sort"
109
"strconv"
1110
"strings"
1211
"time"
1312

13+
"k8s.io/utils/clock"
14+
1415
"k8s.io/apimachinery/pkg/util/wait"
1516

1617
"github.com/blang/semver/v4"
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//go:build linux
2+
3+
package atomicdir
4+
5+
import (
6+
"golang.org/x/sys/unix"
7+
)
8+
9+
// swap can be used to exchange two directories atomically.
10+
func swap(firstDir, secondDir string) error {
11+
// Renameat2 can be used to exchange two directories atomically when RENAME_EXCHANGE flag is specified.
12+
// The paths to be exchanged can be specified in multiple ways:
13+
//
14+
// * You can specify a file descriptor and a relative path to that descriptor.
15+
// * You can specify an absolute path, in which case the file descriptor is ignored.
16+
//
17+
// We use AT_FDCWD special file descriptor so that when any of the paths is relative,
18+
// it's relative to the current working directory.
19+
//
20+
// For more details, see `man renameat2` as that is the associated C library function.
21+
return unix.Renameat2(unix.AT_FDCWD, firstDir, unix.AT_FDCWD, secondDir, unix.RENAME_EXCHANGE)
22+
}
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
//go:build linux
2+
3+
package atomicdir
4+
5+
import (
6+
"bytes"
7+
"os"
8+
"path/filepath"
9+
"testing"
10+
11+
"github.com/google/go-cmp/cmp"
12+
13+
"k8s.io/apimachinery/pkg/util/sets"
14+
)
15+
16+
func TestSwap(t *testing.T) {
17+
stateFirst := directoryState{
18+
"1.txt": {
19+
Content: []byte("hello 1 world"),
20+
Perm: 0600,
21+
},
22+
"2.txt": {
23+
Content: []byte("hello 2 world"),
24+
Perm: 0400,
25+
},
26+
}
27+
stateSecond := directoryState{
28+
"a.txt": {
29+
Content: []byte("hello a world"),
30+
Perm: 0600,
31+
},
32+
}
33+
stateEmpty := directoryState{}
34+
35+
expectNoError := func(t *testing.T, err error) {
36+
t.Helper()
37+
if err != nil {
38+
t.Fatalf("Expected no error, got %v", err)
39+
}
40+
}
41+
42+
checkSuccess := func(t *testing.T, pathFirst string, stateFirst directoryState, pathSecond string, stateSecond directoryState, err error) {
43+
t.Helper()
44+
expectNoError(t, err)
45+
46+
// Make sure the contents are swapped.
47+
stateFirst.CheckDirectoryMatches(t, pathSecond)
48+
stateSecond.CheckDirectoryMatches(t, pathFirst)
49+
}
50+
51+
testCases := []struct {
52+
name string
53+
setup func(t *testing.T, tmpDir string) (pathFirst string, stateFirst directoryState, pathSecond string, stateSecond directoryState)
54+
checkResult func(t *testing.T, pathFirst string, stateFirst directoryState, pathSecond string, stateSecond directoryState, err error)
55+
}{
56+
{
57+
name: "success with absolute paths",
58+
setup: func(t *testing.T, tmpDir string) (string, directoryState, string, directoryState) {
59+
pathFirst := filepath.Join(tmpDir, "first")
60+
pathSecond := filepath.Join(tmpDir, "second")
61+
62+
stateFirst.Write(t, pathFirst)
63+
stateSecond.Write(t, pathSecond)
64+
65+
return pathFirst, stateFirst, pathSecond, stateSecond
66+
},
67+
checkResult: checkSuccess,
68+
},
69+
{
70+
name: "success with the first path relative",
71+
setup: func(t *testing.T, tmpDir string) (string, directoryState, string, directoryState) {
72+
pathFirst := filepath.Join(tmpDir, "first")
73+
pathSecond := filepath.Join(tmpDir, "second")
74+
75+
stateFirst.Write(t, pathFirst)
76+
stateSecond.Write(t, pathSecond)
77+
78+
cwd, err := os.Getwd()
79+
expectNoError(t, err)
80+
81+
relFirst, err := filepath.Rel(cwd, pathFirst)
82+
expectNoError(t, err)
83+
84+
return relFirst, stateFirst, pathSecond, stateSecond
85+
},
86+
checkResult: checkSuccess,
87+
},
88+
{
89+
name: "success with the second path relative",
90+
setup: func(t *testing.T, tmpDir string) (string, directoryState, string, directoryState) {
91+
pathFirst := filepath.Join(tmpDir, "first")
92+
pathSecond := filepath.Join(tmpDir, "second")
93+
94+
stateFirst.Write(t, pathFirst)
95+
stateSecond.Write(t, pathSecond)
96+
97+
cwd, err := os.Getwd()
98+
expectNoError(t, err)
99+
100+
relSecond, err := filepath.Rel(cwd, pathSecond)
101+
expectNoError(t, err)
102+
103+
return pathFirst, stateFirst, relSecond, stateSecond
104+
},
105+
checkResult: checkSuccess,
106+
},
107+
{
108+
name: "success with an empty directory",
109+
setup: func(t *testing.T, tmpDir string) (string, directoryState, string, directoryState) {
110+
pathFirst := filepath.Join(tmpDir, "first")
111+
pathSecond := filepath.Join(tmpDir, "second")
112+
113+
stateFirst.Write(t, pathFirst)
114+
stateEmpty.Write(t, pathSecond)
115+
116+
return pathFirst, stateFirst, pathSecond, stateEmpty
117+
},
118+
checkResult: checkSuccess,
119+
},
120+
{
121+
name: "success with both directories empty",
122+
setup: func(t *testing.T, tmpDir string) (string, directoryState, string, directoryState) {
123+
pathFirst := filepath.Join(tmpDir, "first")
124+
pathSecond := filepath.Join(tmpDir, "second")
125+
126+
stateEmpty.Write(t, pathFirst)
127+
stateEmpty.Write(t, pathSecond)
128+
129+
return pathFirst, stateEmpty, pathSecond, stateEmpty
130+
},
131+
checkResult: checkSuccess,
132+
},
133+
{
134+
name: "error with the first directory not existing",
135+
setup: func(t *testing.T, tmpDir string) (string, directoryState, string, directoryState) {
136+
pathFirst := filepath.Join(tmpDir, "first")
137+
pathSecond := filepath.Join(tmpDir, "second")
138+
139+
expectNoError(t, os.Mkdir(pathSecond, 0755))
140+
141+
return pathFirst, stateEmpty, pathSecond, stateEmpty
142+
},
143+
checkResult: func(t *testing.T, pathFirst string, stateFirst directoryState, pathSecond string, stateSecond directoryState, err error) {
144+
if !os.IsNotExist(err) {
145+
t.Errorf("Expected a directory not exists error, got %v", err)
146+
}
147+
},
148+
},
149+
{
150+
name: "error with the second directory not existing",
151+
setup: func(t *testing.T, tmpDir string) (string, directoryState, string, directoryState) {
152+
pathFirst := filepath.Join(tmpDir, "first")
153+
pathSecond := filepath.Join(tmpDir, "second")
154+
155+
expectNoError(t, os.Mkdir(pathFirst, 0755))
156+
157+
return pathFirst, stateEmpty, pathSecond, stateEmpty
158+
},
159+
checkResult: func(t *testing.T, pathFirst string, stateFirst directoryState, pathSecond string, stateSecond directoryState, err error) {
160+
if !os.IsNotExist(err) {
161+
t.Errorf("Expected a directory not exists error, got %v", err)
162+
}
163+
},
164+
},
165+
{
166+
name: "error with no directory existing",
167+
setup: func(t *testing.T, tmpDir string) (string, directoryState, string, directoryState) {
168+
pathFirst := filepath.Join(tmpDir, "first")
169+
pathSecond := filepath.Join(tmpDir, "second")
170+
171+
return pathFirst, stateEmpty, pathSecond, stateEmpty
172+
},
173+
checkResult: func(t *testing.T, pathFirst string, stateFirst directoryState, pathSecond string, stateSecond directoryState, err error) {
174+
if !os.IsNotExist(err) {
175+
t.Errorf("Expected a directory not exists error, got %v", err)
176+
}
177+
},
178+
},
179+
}
180+
181+
for _, tc := range testCases {
182+
t.Run(tc.name, func(t *testing.T) {
183+
pathFirst, stateFirst, pathSecond, stateSecond := tc.setup(t, t.TempDir())
184+
tc.checkResult(t, pathFirst, stateFirst, pathSecond, stateSecond, swap(pathFirst, pathSecond))
185+
})
186+
}
187+
}
188+
189+
type fileState struct {
190+
Content []byte
191+
Perm os.FileMode
192+
}
193+
194+
type directoryState map[string]fileState
195+
196+
func (dir directoryState) Write(t *testing.T, path string) {
197+
if err := os.MkdirAll(path, 0755); err != nil && !os.IsExist(err) {
198+
t.Fatalf("Failed to create directory %q: %v", path, err)
199+
}
200+
201+
for filename, state := range dir {
202+
fullFilename := filepath.Join(path, filename)
203+
if err := os.WriteFile(fullFilename, state.Content, state.Perm); err != nil {
204+
t.Fatalf("Failed to write file %q: %v", fullFilename, err)
205+
}
206+
}
207+
}
208+
209+
func (dir directoryState) CheckDirectoryMatches(t *testing.T, path string) {
210+
entries, err := os.ReadDir(path)
211+
if err != nil {
212+
t.Fatalf("Failed to read directory %q: %v", path, err)
213+
}
214+
215+
expectedFiles := sets.KeySet(dir)
216+
for _, entry := range entries {
217+
// Mark the file as visited.
218+
expectedFiles.Delete(entry.Name())
219+
220+
// Get the expected state.
221+
state, ok := dir[entry.Name()]
222+
if !ok {
223+
t.Errorf("Directory %q contains unexpected file %q", path, entry.Name())
224+
continue
225+
}
226+
227+
// Check permissions.
228+
info, err := entry.Info()
229+
if err != nil {
230+
t.Errorf("Failed to stat file %q: %v", entry.Name(), err)
231+
continue
232+
}
233+
234+
if info.Mode() != state.Perm {
235+
t.Errorf("Unexpected permissions on file %q: expected %v, got %v", entry.Name(), state.Perm, info.Mode())
236+
}
237+
238+
// Check file content.
239+
content, err := os.ReadFile(filepath.Join(path, entry.Name()))
240+
if err != nil {
241+
t.Errorf("Failed to read file %q: %v", entry.Name(), err)
242+
continue
243+
}
244+
if !bytes.Equal(state.Content, content) {
245+
t.Errorf("Unexpected content in file %q:\n%v", entry.Name(), cmp.Diff(string(state.Content), string(content)))
246+
}
247+
}
248+
if expectedFiles.Len() != 0 {
249+
t.Errorf("Some expected files were not found in directory %q: %s", path, expectedFiles.UnsortedList())
250+
}
251+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//go:build !linux
2+
3+
package atomicdir
4+
5+
// swap can be used to exchange two directories atomically.
6+
//
7+
// This function is only implemented for Linux and returns an error on other platforms.
8+
func swap(firstDir, secondDir string) error {
9+
return errors.New("swap is not supported on this platform")
10+
}

0 commit comments

Comments
 (0)