Skip to content

Commit f12253c

Browse files
committed
Add check for content directory unchanged
The content directory is hashed at the beginning and then right before switching. When there is any change detected, an event and log entry is emitted. This is to minimize possible interactions with other controllers or actors. The controller will retry and eventually replace the directory anyway.
1 parent 65cbd04 commit f12253c

File tree

4 files changed

+140
-3
lines changed

4 files changed

+140
-3
lines changed

pkg/operator/staticpod/certsyncpod/certsync_controller.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package certsyncpod
22

33
import (
4+
"bytes"
45
"context"
6+
"fmt"
57
"os"
68
"path/filepath"
79
"reflect"
@@ -239,7 +241,6 @@ func (c *CertSyncController) sync(ctx context.Context, syncCtx factory.SyncConte
239241

240242
errors = append(errors, writeFiles(&realFS, c.eventRecorder, "secret", secret.ObjectMeta, contentDir, data, 0600))
241243
}
242-
243244
return utilerrors.NewAggregate(errors)
244245
}
245246

@@ -249,6 +250,7 @@ type fileSystem struct {
249250
RemoveAll func(path string) error
250251
WriteFile func(name string, data []byte, perm os.FileMode) error
251252
SwapDirectoriesAtomic func(dirA, dirB string) error
253+
HashDirectory func(path string) ([]byte, error)
252254
}
253255

254256
var realFS = fileSystem{
@@ -257,6 +259,7 @@ var realFS = fileSystem{
257259
RemoveAll: os.RemoveAll,
258260
WriteFile: os.WriteFile,
259261
SwapDirectoriesAtomic: staticpod.SwapDirectoriesAtomic,
262+
HashDirectory: hashDirectory,
260263
}
261264

262265
func writeFiles[C string | []byte](
@@ -276,14 +279,22 @@ func writeFiles[C string | []byte](
276279
// This would all just increase complexity and require atomic swap on the OS level anyway.
277280

278281
// In case the target directory does not exist, create it so that the directory not existing is not a special case.
279-
klog.Infof("Ensuring target directory %q exists ...", targetDir)
282+
klog.Infof("Ensuring content directory %q exists ...", targetDir)
280283
if err := fs.MkdirAll(targetDir, 0755); err != nil && !os.IsExist(err) {
281284
eventRecorder.Warningf("CertificateUpdateFailed", "Failed creating content directory for %s: %s/%s: %v", typeName, o.Namespace, o.Name, err)
282285
return err
283286
}
284287

288+
// We make sure the target directory is unchanged while we prepare the switch by computing the directory hash.
289+
klog.Infof("Hashing current content directory %q ...", targetDir)
290+
targetDirHashBefore, err := fs.HashDirectory(targetDir)
291+
if err != nil {
292+
eventRecorder.Warningf("CertificateUpdateFailed", "Failed to hash current content directory for %s: %s/%s: %v", typeName, o.Namespace, o.Name, err)
293+
return err
294+
}
295+
285296
// Create a tmp source directory to be swapped.
286-
klog.Infof("Ensuring source directory %q exists ...", targetDir)
297+
klog.Infof("Creating temporary directory to swap for %q ...", targetDir)
287298
tmpDir, err := fs.MkdirTemp(filepath.Dir(targetDir), filepath.Base(targetDir)+"-*")
288299
if err != nil {
289300
eventRecorder.Warningf("CertificateUpdateFailed", "Failed to create temporary directory for %s: %s/%s: %v", typeName, o.Namespace, o.Name, err)
@@ -306,6 +317,19 @@ func writeFiles[C string | []byte](
306317
}
307318
}
308319

320+
// Make sure the target directory hasn't changed in the meantime.
321+
klog.Infof("Hashing current content directory %q again and ensuring it's unchanged ...", targetDir)
322+
targetDirHashAfter, err := fs.HashDirectory(targetDir)
323+
if err != nil {
324+
eventRecorder.Warningf("CertificateUpdateFailed", "Failed to hash current content directory for %s: %s/%s: %v", typeName, o.Namespace, o.Name, err)
325+
return err
326+
}
327+
if !bytes.Equal(targetDirHashBefore, targetDirHashAfter) {
328+
eventRecorder.Warningf("CertificateUpdateFailed", "Content directory changed while preparing to apply an update for %s: %s/%s", typeName, o.Namespace, o.Name)
329+
klog.Warningf("Content directory changed while preparing to apply an update: %q", targetDir)
330+
return fmt.Errorf("content directory changed while preparing to apply an update: %q", targetDir)
331+
}
332+
309333
// Swap directories atomically.
310334
klog.Infof("Atomically swapping target directory %q with temporary directory %q for %s: %s/%s ...", targetDir, tmpDir, typeName, o.Namespace, o.Name)
311335
if err := fs.SwapDirectoriesAtomic(targetDir, tmpDir); err != nil {

pkg/operator/staticpod/certsyncpod/certsync_controller_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"os"
66
"path/filepath"
7+
"strings"
78
"testing"
89

910
"github.com/google/go-cmp/cmp"
@@ -228,6 +229,21 @@ func TestWriteFiles(t *testing.T) {
228229
}
229230
return fs
230231
}),
232+
errorTestCase("directory unchanged on content directory hash mismatch", func() *fileSystem {
233+
fs := newRealFS()
234+
fs.HashDirectory = hashDirectoryFuncWithReturnValues("hash", "mismatch")
235+
return fs
236+
}),
237+
errorTestCase("directory unchanged on content directory initial hash error", func() *fileSystem {
238+
fs := newRealFS()
239+
fs.HashDirectory = hashDirectoryFuncWithReturnValues("error: nuked")
240+
return fs
241+
}),
242+
errorTestCase("directory unchanged on content directory subsequent hash error", func() *fileSystem {
243+
fs := newRealFS()
244+
fs.HashDirectory = hashDirectoryFuncWithReturnValues("hash", "error: nuked")
245+
return fs
246+
}),
231247
}
232248

233249
for _, tc := range testCases {
@@ -271,3 +287,14 @@ func failToWriteNth(writeFile writeFileFunc, n int) writeFileFunc {
271287
return writeFile(path, data, perm)
272288
}
273289
}
290+
291+
func hashDirectoryFuncWithReturnValues(retVals ...string) func(string) ([]byte, error) {
292+
return func(path string) ([]byte, error) {
293+
v := retVals[0]
294+
retVals = retVals[1:]
295+
if strings.HasPrefix(v, "error:") {
296+
return nil, errors.New(strings.TrimPrefix(v, "error:"))
297+
}
298+
return []byte(v), nil
299+
}
300+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package certsyncpod
2+
3+
import (
4+
"bytes"
5+
"crypto/md5"
6+
"encoding/binary"
7+
"io/fs"
8+
"path/filepath"
9+
)
10+
11+
func hashDirectory(root string) ([]byte, error) {
12+
var b bytes.Buffer
13+
if err := filepath.Walk(root, func(path string, info fs.FileInfo, err error) error {
14+
if err != nil {
15+
return err
16+
}
17+
// We do not hash the root directory entry itself.
18+
if path == root {
19+
return nil
20+
}
21+
22+
b.WriteString(path)
23+
binary.Write(&b, binary.LittleEndian, info.ModTime().UnixNano())
24+
return nil
25+
}); err != nil {
26+
return nil, err
27+
}
28+
29+
return md5.New().Sum(b.Bytes()), nil
30+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package certsyncpod
2+
3+
import (
4+
"bytes"
5+
"crypto/md5"
6+
"encoding/binary"
7+
"os"
8+
"path/filepath"
9+
"testing"
10+
11+
"github.com/google/go-cmp/cmp"
12+
)
13+
14+
func TestHashDirectory(t *testing.T) {
15+
tmpDir := t.TempDir()
16+
17+
// Create two files to be hashed.
18+
pathA := filepath.Join(tmpDir, "1.txt")
19+
if err := os.WriteFile(pathA, []byte("1"), 0644); err != nil {
20+
t.Fatalf("Failed to write file %q: %v", pathA, err)
21+
}
22+
23+
pathB := filepath.Join(tmpDir, "2.txt")
24+
if err := os.WriteFile(pathB, []byte("2"), 0644); err != nil {
25+
t.Fatalf("Failed to write file %q: %v", pathB, err)
26+
}
27+
28+
// Get their mod times.
29+
infoA, err := os.Lstat(pathA)
30+
if err != nil {
31+
t.Fatalf("Failed to stat file %q: %v", pathA, err)
32+
}
33+
infoB, err := os.Lstat(pathB)
34+
if err != nil {
35+
t.Fatalf("Failed to stat file %q: %v", pathB, err)
36+
}
37+
38+
// Compute the expected hash.
39+
var b bytes.Buffer
40+
b.WriteString(pathA)
41+
binary.Write(&b, binary.LittleEndian, infoA.ModTime().UnixNano())
42+
b.WriteString(pathB)
43+
binary.Write(&b, binary.LittleEndian, infoB.ModTime().UnixNano())
44+
expectedHash := md5.New().Sum(b.Bytes())
45+
46+
// Compute the actual hash.
47+
gotHash, err := hashDirectory(tmpDir)
48+
if err != nil {
49+
t.Fatalf("Failed to hash directory %q: %v", tmpDir, err)
50+
}
51+
52+
// Compare both hashes.
53+
if !bytes.Equal(gotHash, expectedHash) {
54+
t.Errorf("Unexpected directory hash received:\n%v", cmp.Diff(expectedHash, gotHash))
55+
}
56+
}

0 commit comments

Comments
 (0)