Skip to content

Commit d065a49

Browse files
perdasilvaPer Goncalves da Silva
andauthored
🌱 Rename util packages and add missing unit tests (#1677)
* Rename util package name and file Signed-off-by: Per Goncalves da Silva <[email protected]> * Refactor and add missing unit tests Signed-off-by: Per Goncalves da Silva <[email protected]> --------- Signed-off-by: Per Goncalves da Silva <[email protected]> Co-authored-by: Per Goncalves da Silva <[email protected]>
1 parent c929975 commit d065a49

File tree

10 files changed

+282
-97
lines changed

10 files changed

+282
-97
lines changed

catalogd/cmd/catalogd/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ import (
6363
"github.com/operator-framework/operator-controller/catalogd/internal/storage"
6464
"github.com/operator-framework/operator-controller/catalogd/internal/version"
6565
"github.com/operator-framework/operator-controller/catalogd/internal/webhook"
66-
"github.com/operator-framework/operator-controller/internal/util"
66+
"github.com/operator-framework/operator-controller/internal/fsutil"
6767
)
6868

6969
var (
@@ -258,7 +258,7 @@ func main() {
258258
systemNamespace = podNamespace()
259259
}
260260

261-
if err := util.EnsureEmptyDirectory(cacheDir, 0700); err != nil {
261+
if err := fsutil.EnsureEmptyDirectory(cacheDir, 0700); err != nil {
262262
setupLog.Error(err, "unable to ensure empty cache directory")
263263
os.Exit(1)
264264
}

catalogd/internal/source/containers_image.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ import (
3030
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3131

3232
catalogdv1 "github.com/operator-framework/operator-controller/catalogd/api/v1"
33+
"github.com/operator-framework/operator-controller/internal/fsutil"
3334
"github.com/operator-framework/operator-controller/internal/rukpak/source"
34-
"github.com/operator-framework/operator-controller/internal/util"
3535
)
3636

3737
const ConfigDirLabel = "operators.operatorframework.io.index.configs.v1"
@@ -297,7 +297,7 @@ func (i *ContainersImageRegistry) unpackImage(ctx context.Context, unpackPath st
297297
return wrapTerminal(fmt.Errorf("catalog image is missing the required label %q", ConfigDirLabel), specIsCanonical)
298298
}
299299

300-
if err := util.EnsureEmptyDirectory(unpackPath, 0700); err != nil {
300+
if err := fsutil.EnsureEmptyDirectory(unpackPath, 0700); err != nil {
301301
return fmt.Errorf("error ensuring empty unpack directory: %w", err)
302302
}
303303
l := log.FromContext(ctx)

cmd/operator-controller/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,12 @@ import (
6363
"github.com/operator-framework/operator-controller/internal/controllers"
6464
"github.com/operator-framework/operator-controller/internal/features"
6565
"github.com/operator-framework/operator-controller/internal/finalizers"
66+
"github.com/operator-framework/operator-controller/internal/fsutil"
6667
"github.com/operator-framework/operator-controller/internal/httputil"
6768
"github.com/operator-framework/operator-controller/internal/resolve"
6869
"github.com/operator-framework/operator-controller/internal/rukpak/preflights/crdupgradesafety"
6970
"github.com/operator-framework/operator-controller/internal/rukpak/source"
7071
"github.com/operator-framework/operator-controller/internal/scheme"
71-
"github.com/operator-framework/operator-controller/internal/util"
7272
"github.com/operator-framework/operator-controller/internal/version"
7373
)
7474

@@ -300,7 +300,7 @@ func main() {
300300
}
301301
}
302302

303-
if err := util.EnsureEmptyDirectory(cachePath, 0700); err != nil {
303+
if err := fsutil.EnsureEmptyDirectory(cachePath, 0700); err != nil {
304304
setupLog.Error(err, "unable to ensure empty cache directory")
305305
os.Exit(1)
306306
}

internal/util/fs.go renamed to internal/fsutil/helpers.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package util
1+
package fsutil
22

33
import (
44
"io/fs"
@@ -8,7 +8,9 @@ import (
88

99
// EnsureEmptyDirectory ensures the directory given by `path` is empty.
1010
// If the directory does not exist, it will be created with permission bits
11-
// given by `perm`.
11+
// given by `perm`. If the directory exists, it will not simply rm -rf && mkdir -p
12+
// as the calling process may not have permissions to delete the directory. E.g.
13+
// in the case of a pod mount. Rather, it will delete the contents of the directory.
1214
func EnsureEmptyDirectory(path string, perm fs.FileMode) error {
1315
entries, err := os.ReadDir(path)
1416
if err != nil && !os.IsNotExist(err) {

internal/fsutil/helpers_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package fsutil_test
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/operator-framework/operator-controller/internal/fsutil"
11+
)
12+
13+
func TestEnsureEmptyDirectory(t *testing.T) {
14+
tempDir := t.TempDir()
15+
dirPath := filepath.Join(tempDir, "testdir")
16+
dirPerms := os.FileMode(0755)
17+
18+
t.Log("Ensure directory is created with the correct perms if it does not already exist")
19+
require.NoError(t, fsutil.EnsureEmptyDirectory(dirPath, dirPerms))
20+
21+
stat, err := os.Stat(dirPath)
22+
require.NoError(t, err)
23+
require.True(t, stat.IsDir())
24+
require.Equal(t, dirPerms, stat.Mode().Perm())
25+
26+
t.Log("Create a file inside directory")
27+
file := filepath.Join(dirPath, "file1")
28+
// nolint:gosec
29+
require.NoError(t, os.WriteFile(file, []byte("test"), 0640))
30+
31+
t.Log("Create a sub-directory inside directory")
32+
subDir := filepath.Join(dirPath, "subdir")
33+
require.NoError(t, os.Mkdir(subDir, dirPerms))
34+
35+
t.Log("Call EnsureEmptyDirectory against directory with different permissions")
36+
require.NoError(t, fsutil.EnsureEmptyDirectory(dirPath, 0640))
37+
38+
t.Log("Ensure directory is now empty")
39+
entries, err := os.ReadDir(dirPath)
40+
require.NoError(t, err)
41+
require.Empty(t, entries)
42+
43+
t.Log("Ensure original directory permissions are unchanged")
44+
stat, err = os.Stat(dirPath)
45+
require.NoError(t, err)
46+
require.Equal(t, dirPerms, stat.Mode().Perm())
47+
}

internal/rukpak/source/containers_image.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import (
2525
"sigs.k8s.io/controller-runtime/pkg/log"
2626
"sigs.k8s.io/controller-runtime/pkg/reconcile"
2727

28-
"github.com/operator-framework/operator-controller/internal/util"
28+
"github.com/operator-framework/operator-controller/internal/fsutil"
2929
)
3030

3131
var insecurePolicy = []byte(`{"default":[{"type":"insecureAcceptAnything"}]}`)
@@ -266,7 +266,7 @@ func (i *ContainersImageRegistry) unpackImage(ctx context.Context, unpackPath st
266266
}
267267
}()
268268

269-
if err := util.EnsureEmptyDirectory(unpackPath, 0700); err != nil {
269+
if err := fsutil.EnsureEmptyDirectory(unpackPath, 0700); err != nil {
270270
return fmt.Errorf("error ensuring empty unpack directory: %w", err)
271271
}
272272
l := log.FromContext(ctx)

internal/rukpak/source/containers_image_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ func TestUnpackUnexpectedFile(t *testing.T) {
286286
require.True(t, stat.IsDir())
287287

288288
// Unset read-only to allow cleanup
289-
require.NoError(t, source.UnsetReadOnlyRecursive(unpackPath))
289+
require.NoError(t, source.SetWritableRecursive(unpackPath))
290290
}
291291

292292
func TestUnpackCopySucceedsMountFails(t *testing.T) {

internal/rukpak/source/helpers.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package source
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"time"
9+
)
10+
11+
const (
12+
OwnerWritableFileMode os.FileMode = 0700
13+
OwnerWritableDirMode os.FileMode = 0700
14+
OwnerReadOnlyFileMode os.FileMode = 0400
15+
OwnerReadOnlyDirMode os.FileMode = 0500
16+
)
17+
18+
// SetReadOnlyRecursive recursively sets files and directories under the path given by `root` as read-only
19+
func SetReadOnlyRecursive(root string) error {
20+
return setModeRecursive(root, OwnerReadOnlyFileMode, OwnerReadOnlyDirMode)
21+
}
22+
23+
// SetWritableRecursive recursively sets files and directories under the path given by `root` as writable
24+
func SetWritableRecursive(root string) error {
25+
return setModeRecursive(root, OwnerWritableFileMode, OwnerWritableDirMode)
26+
}
27+
28+
// DeleteReadOnlyRecursive deletes read-only directory with path given by `root`
29+
func DeleteReadOnlyRecursive(root string) error {
30+
if err := SetWritableRecursive(root); err != nil {
31+
return fmt.Errorf("error making directory writable for deletion: %w", err)
32+
}
33+
return os.RemoveAll(root)
34+
}
35+
36+
// IsImageUnpacked checks whether an image has been unpacked in `unpackPath`.
37+
// If true, time of unpack will also be returned. If false unpack time is gibberish (zero/epoch time).
38+
// If `unpackPath` is a file, it will be deleted and false will be returned without an error.
39+
func IsImageUnpacked(unpackPath string) (bool, time.Time, error) {
40+
unpackStat, err := os.Stat(unpackPath)
41+
if errors.Is(err, os.ErrNotExist) {
42+
return false, time.Time{}, nil
43+
}
44+
if err != nil {
45+
return false, time.Time{}, err
46+
}
47+
if !unpackStat.IsDir() {
48+
return false, time.Time{}, os.Remove(unpackPath)
49+
}
50+
return true, unpackStat.ModTime(), nil
51+
}
52+
53+
func setModeRecursive(path string, fileMode os.FileMode, dirMode os.FileMode) error {
54+
return filepath.WalkDir(path, func(path string, d os.DirEntry, err error) error {
55+
if os.IsNotExist(err) {
56+
return nil
57+
}
58+
if err != nil {
59+
return err
60+
}
61+
fi, err := d.Info()
62+
if err != nil {
63+
return err
64+
}
65+
66+
switch typ := fi.Mode().Type(); typ {
67+
case os.ModeSymlink:
68+
// do not follow symlinks
69+
// 1. if they resolve to other locations in the root, we'll find them anyway
70+
// 2. if they resolve to other locations outside the root, we don't want to change their permissions
71+
return nil
72+
case os.ModeDir:
73+
return os.Chmod(path, dirMode)
74+
case 0: // regular file
75+
return os.Chmod(path, fileMode)
76+
default:
77+
return fmt.Errorf("refusing to change ownership of file %q with type %v", path, typ.String())
78+
}
79+
})
80+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package source_test
2+
3+
import (
4+
"io/fs"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/operator-framework/operator-controller/internal/rukpak/source"
12+
)
13+
14+
func TestSetReadOnlyRecursive(t *testing.T) {
15+
tempDir := t.TempDir()
16+
targetFilePath := filepath.Join(tempDir, "target")
17+
nestedDir := filepath.Join(tempDir, "nested")
18+
filePath := filepath.Join(nestedDir, "testfile")
19+
symlinkPath := filepath.Join(nestedDir, "symlink")
20+
21+
t.Log("Create symlink target file outside directory with its own permissions")
22+
// nolint:gosec
23+
require.NoError(t, os.WriteFile(targetFilePath, []byte("something"), 0644))
24+
25+
t.Log("Create a nested directory structure that contains a file and sym. link")
26+
require.NoError(t, os.Mkdir(nestedDir, source.OwnerWritableDirMode))
27+
require.NoError(t, os.WriteFile(filePath, []byte("test"), source.OwnerWritableFileMode))
28+
require.NoError(t, os.Symlink(targetFilePath, symlinkPath))
29+
30+
t.Log("Set directory structure as read-only")
31+
require.NoError(t, source.SetReadOnlyRecursive(nestedDir))
32+
33+
t.Log("Check file permissions")
34+
stat, err := os.Stat(filePath)
35+
require.NoError(t, err)
36+
require.Equal(t, source.OwnerReadOnlyFileMode, stat.Mode().Perm())
37+
38+
t.Log("Check directory permissions")
39+
nestedStat, err := os.Stat(nestedDir)
40+
require.NoError(t, err)
41+
require.Equal(t, source.OwnerReadOnlyDirMode, nestedStat.Mode().Perm())
42+
43+
t.Log("Check symlink target file permissions - should not be affected")
44+
stat, err = os.Stat(targetFilePath)
45+
require.NoError(t, err)
46+
require.Equal(t, fs.FileMode(0644), stat.Mode().Perm())
47+
48+
t.Log("Make directory writable to enable test clean-up")
49+
require.NoError(t, source.SetWritableRecursive(tempDir))
50+
}
51+
52+
func TestSetWritableRecursive(t *testing.T) {
53+
tempDir := t.TempDir()
54+
targetFilePath := filepath.Join(tempDir, "target")
55+
nestedDir := filepath.Join(tempDir, "nested")
56+
filePath := filepath.Join(nestedDir, "testfile")
57+
symlinkPath := filepath.Join(nestedDir, "symlink")
58+
59+
t.Log("Create symlink target file outside directory with its own permissions")
60+
// nolint:gosec
61+
require.NoError(t, os.WriteFile(targetFilePath, []byte("something"), 0644))
62+
63+
t.Log("Create a nested directory (writable) structure that contains a file (read-only) and sym. link")
64+
require.NoError(t, os.Mkdir(nestedDir, source.OwnerWritableDirMode))
65+
require.NoError(t, os.WriteFile(filePath, []byte("test"), source.OwnerReadOnlyFileMode))
66+
require.NoError(t, os.Symlink(targetFilePath, symlinkPath))
67+
68+
t.Log("Make directory read-only")
69+
require.NoError(t, os.Chmod(nestedDir, source.OwnerReadOnlyDirMode))
70+
71+
t.Log("Call SetWritableRecursive")
72+
require.NoError(t, source.SetWritableRecursive(nestedDir))
73+
74+
t.Log("Check file is writable")
75+
stat, err := os.Stat(filePath)
76+
require.NoError(t, err)
77+
require.Equal(t, source.OwnerWritableFileMode, stat.Mode().Perm())
78+
79+
t.Log("Check directory is writable")
80+
nestedStat, err := os.Stat(nestedDir)
81+
require.NoError(t, err)
82+
require.Equal(t, source.OwnerWritableDirMode, nestedStat.Mode().Perm())
83+
84+
t.Log("Check symlink target file permissions - should not be affected")
85+
stat, err = os.Stat(targetFilePath)
86+
require.NoError(t, err)
87+
require.Equal(t, fs.FileMode(0644), stat.Mode().Perm())
88+
}
89+
90+
func TestDeleteReadOnlyRecursive(t *testing.T) {
91+
tempDir := t.TempDir()
92+
nestedDir := filepath.Join(tempDir, "nested")
93+
filePath := filepath.Join(nestedDir, "testfile")
94+
95+
t.Log("Create a nested read-only directory structure that contains a file and sym. link")
96+
require.NoError(t, os.Mkdir(nestedDir, source.OwnerWritableDirMode))
97+
require.NoError(t, os.WriteFile(filePath, []byte("test"), source.OwnerReadOnlyFileMode))
98+
require.NoError(t, os.Chmod(nestedDir, source.OwnerReadOnlyDirMode))
99+
100+
t.Log("Set directory structure as read-only")
101+
require.NoError(t, source.DeleteReadOnlyRecursive(nestedDir))
102+
103+
t.Log("Ensure directory was deleted")
104+
_, err := os.Stat(nestedDir)
105+
require.ErrorIs(t, err, os.ErrNotExist)
106+
}
107+
108+
func TestIsImageUnpacked(t *testing.T) {
109+
tempDir := t.TempDir()
110+
unpackPath := filepath.Join(tempDir, "myimage")
111+
112+
t.Log("Test case: unpack path does not exist")
113+
unpacked, modTime, err := source.IsImageUnpacked(unpackPath)
114+
require.NoError(t, err)
115+
require.False(t, unpacked)
116+
require.True(t, modTime.IsZero())
117+
118+
t.Log("Test case: unpack path points to file")
119+
require.NoError(t, os.WriteFile(unpackPath, []byte("test"), source.OwnerWritableFileMode))
120+
121+
unpacked, modTime, err = source.IsImageUnpacked(filepath.Join(tempDir, "myimage"))
122+
require.NoError(t, err)
123+
require.False(t, unpacked)
124+
require.True(t, modTime.IsZero())
125+
126+
t.Log("Expect file to be deleted")
127+
_, err = os.Stat(unpackPath)
128+
require.ErrorIs(t, err, os.ErrNotExist)
129+
130+
t.Log("Test case: unpack path points to directory (happy path)")
131+
require.NoError(t, os.Mkdir(unpackPath, source.OwnerWritableDirMode))
132+
133+
unpacked, modTime, err = source.IsImageUnpacked(unpackPath)
134+
require.NoError(t, err)
135+
require.True(t, unpacked)
136+
require.False(t, modTime.IsZero())
137+
138+
t.Log("Expect unpack time to match directory mod time")
139+
stat, err := os.Stat(unpackPath)
140+
require.NoError(t, err)
141+
require.Equal(t, stat.ModTime(), modTime)
142+
}

0 commit comments

Comments
 (0)