Skip to content

Commit 5af84ea

Browse files
committed
Add a function for setting permission on directories
1 parent d3fa16c commit 5af84ea

File tree

2 files changed

+123
-0
lines changed

2 files changed

+123
-0
lines changed

internal/shell/paths.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright 2024 - 2025 Crunchy Data Solutions, Inc.
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
// We want the [filepath] package to behave correctly for Linux containers.
6+
//go:build unix
7+
8+
package shell
9+
10+
import (
11+
"fmt"
12+
"io/fs"
13+
"path/filepath"
14+
"strings"
15+
)
16+
17+
// MakeDirectories returns a list of POSIX shell commands that ensure each path
18+
// exists. It creates every directory leading to path from (but not including)
19+
// base and sets their permissions to exactly perms, regardless of umask.
20+
//
21+
// See:
22+
// - https://pubs.opengroup.org/onlinepubs/9799919799/utilities/chmod.html
23+
// - https://pubs.opengroup.org/onlinepubs/9799919799/utilities/mkdir.html
24+
// - https://pubs.opengroup.org/onlinepubs/9799919799/utilities/test.html
25+
// - https://pubs.opengroup.org/onlinepubs/9799919799/utilities/umask.html
26+
func MakeDirectories(perms fs.FileMode, base string, paths ...string) string {
27+
// Without any paths, return a command that succeeds when the base path
28+
// exists.
29+
if len(paths) == 0 {
30+
return `test -d ` + QuoteWord(base)
31+
}
32+
33+
allPaths := append([]string(nil), paths...)
34+
for _, p := range paths {
35+
if r, err := filepath.Rel(base, p); err == nil && filepath.IsLocal(r) {
36+
// The result of [filepath.Rel] is a shorter representation
37+
// of the full path; skip it.
38+
r = filepath.Dir(r)
39+
40+
for r != "." {
41+
allPaths = append(allPaths, filepath.Join(base, r))
42+
r = filepath.Dir(r)
43+
}
44+
}
45+
}
46+
47+
return `` +
48+
// Create all the paths and any missing parents.
49+
`mkdir -p ` + strings.Join(QuoteWords(paths...), " ") +
50+
51+
// Set the permissions of every path and each parent.
52+
// NOTE: FileMode bits other than file permissions are ignored.
53+
fmt.Sprintf(` && chmod %#o %s`,
54+
perms&fs.ModePerm,
55+
strings.Join(QuoteWords(allPaths...), " "),
56+
)
57+
}

internal/shell/paths_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright 2024 - 2025 Crunchy Data Solutions, Inc.
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package shell
6+
7+
import (
8+
"os"
9+
"os/exec"
10+
"path/filepath"
11+
"strings"
12+
"testing"
13+
14+
"gotest.tools/v3/assert"
15+
"sigs.k8s.io/yaml"
16+
17+
"github.com/crunchydata/postgres-operator/internal/testing/require"
18+
)
19+
20+
func TestMakeDirectories(t *testing.T) {
21+
t.Parallel()
22+
23+
t.Run("NoPaths", func(t *testing.T) {
24+
assert.Equal(t,
25+
MakeDirectories(0o755, "/asdf/jklm"),
26+
`test -d '/asdf/jklm'`)
27+
})
28+
29+
t.Run("Children", func(t *testing.T) {
30+
assert.DeepEqual(t,
31+
MakeDirectories(0o775, "/asdf", "/asdf/jklm", "/asdf/qwerty"),
32+
`mkdir -p '/asdf/jklm' '/asdf/qwerty' && chmod 0775 '/asdf/jklm' '/asdf/qwerty'`)
33+
})
34+
35+
t.Run("Grandchild", func(t *testing.T) {
36+
script := MakeDirectories(0o775, "/asdf", "/asdf/qwerty/boots")
37+
assert.DeepEqual(t, script,
38+
`mkdir -p '/asdf/qwerty/boots' && chmod 0775 '/asdf/qwerty/boots' '/asdf/qwerty'`)
39+
40+
t.Run("ShellCheckPOSIX", func(t *testing.T) {
41+
shellcheck := require.ShellCheck(t)
42+
43+
dir := t.TempDir()
44+
file := filepath.Join(dir, "script.sh")
45+
assert.NilError(t, os.WriteFile(file, []byte(script), 0o600))
46+
47+
// Expect ShellCheck for "sh" to be happy.
48+
// - https://www.shellcheck.net/wiki/SC2148
49+
cmd := exec.Command(shellcheck, "--enable=all", "--shell=sh", file)
50+
output, err := cmd.CombinedOutput()
51+
assert.NilError(t, err, "%q\n%s", cmd.Args, output)
52+
})
53+
})
54+
55+
t.Run("Long", func(t *testing.T) {
56+
script := MakeDirectories(0o700, "/", strings.Repeat("/asdf", 20))
57+
58+
t.Run("PrettyYAML", func(t *testing.T) {
59+
b, err := yaml.Marshal(script)
60+
s := string(b)
61+
assert.NilError(t, err)
62+
assert.Assert(t, !strings.HasPrefix(s, `"`) && !strings.HasPrefix(s, `'`),
63+
"expected plain unquoted scalar, got:\n%s", b)
64+
})
65+
})
66+
}

0 commit comments

Comments
 (0)