Skip to content

Commit f280184

Browse files
committed
Warn when "log_directory" is outside the v1 range
Issue: PGO-2558
1 parent e743b4d commit f280184

File tree

5 files changed

+104
-19
lines changed

5 files changed

+104
-19
lines changed

internal/controller/postgrescluster/postgres.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ func (r *Reconciler) generatePostgresHBAs(
126126
// 2. parameters in cluster.spec.config.parameters
127127
// 3. parameters in cluster.spec.patroni.dynamicConfiguration
128128
// 4. default values determined by contollers
129-
func (*Reconciler) generatePostgresParameters(
129+
func (r *Reconciler) generatePostgresParameters(
130130
ctx context.Context, cluster *v1beta1.PostgresCluster, backupsSpecFound bool,
131131
) *postgres.ParameterSet {
132132
builtin := postgres.NewParameters()
@@ -180,7 +180,7 @@ func (*Reconciler) generatePostgresParameters(
180180
}
181181

182182
// Finally, remove or replace harmful values.
183-
postgres.SanitizeParameters(cluster, result)
183+
postgres.SanitizeParameters(cluster, result, r.Recorder)
184184

185185
return result
186186
}

internal/postgres/validation.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@ import (
99
"regexp"
1010
"strings"
1111

12+
corev1 "k8s.io/api/core/v1"
13+
"k8s.io/client-go/tools/record"
14+
15+
"github.com/crunchydata/postgres-operator/internal/text"
1216
"github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1"
1317
)
1418

1519
// SanitizeParameters transforms parameters so they are safe for Postgres in cluster.
16-
func SanitizeParameters(cluster *v1beta1.PostgresCluster, parameters *ParameterSet) {
20+
func SanitizeParameters(cluster *v1beta1.PostgresCluster, parameters *ParameterSet, recorder record.EventRecorder) {
1721
if v, ok := parameters.Get("log_directory"); ok {
18-
parameters.Add("log_directory", sanitizeLogDirectory(cluster, v))
22+
parameters.Add("log_directory", sanitizeLogDirectory(cluster, v, recorder))
1923
}
2024
}
2125

@@ -52,7 +56,7 @@ var sensitiveRelativePath = regexp.MustCompile(
5256
// Otherwise, it returns the absolute path to a good "log_directory" value.
5357
//
5458
// https://www.postgresql.org/docs/current/runtime-config-logging.html#GUC-LOG-DIRECTORY
55-
func sanitizeLogDirectory(cluster *v1beta1.PostgresCluster, input string) string {
59+
func sanitizeLogDirectory(cluster *v1beta1.PostgresCluster, input string, recorder record.EventRecorder) string {
5660
directory := path.Clean(input)
5761

5862
// [path.Clean] leaves leading parent directories. Eliminate these as a security measure.
@@ -69,6 +73,11 @@ func sanitizeLogDirectory(cluster *v1beta1.PostgresCluster, input string) string
6973
case directory == "", directory == ".", directory == "/",
7074
sensitiveAbsolutePath.MatchString(directory),
7175
sensitiveRelativePath.MatchString(directory):
76+
if recorder != nil {
77+
recorder.Eventf(cluster, corev1.EventTypeWarning, "InvalidParameter",
78+
"Ignoring unsafe Postgres parameter value %q = %q", "log_directory", text.TruncateAt(input, 128))
79+
}
80+
7281
// When the value is empty after cleaning or disallowed, choose one instead.
7382
// Keep it on the same volume, if possible.
7483
if strings.HasPrefix(directory, tmpMountPath) {
@@ -82,6 +91,11 @@ func sanitizeLogDirectory(cluster *v1beta1.PostgresCluster, input string) string
8291
return path.Join(dataMountPath, "logs/postgres")
8392

8493
case !path.IsAbs(directory):
94+
if recorder != nil {
95+
recorder.Eventf(cluster, corev1.EventTypeWarning, "InvalidParameter",
96+
"Postgres parameter %q should be %q or an absolute path", "log_directory", "log")
97+
}
98+
8599
// Directory is relative. This is disallowed since v1 of PostgresCluster.
86100
// Expand it relative to the data directory like Postgres does.
87101
return path.Join(DataDirectory(cluster), directory)

internal/postgres/validation_test.go

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import (
1010

1111
"gotest.tools/v3/assert"
1212

13+
"github.com/crunchydata/postgres-operator/internal/controller/runtime"
14+
"github.com/crunchydata/postgres-operator/internal/testing/cmp"
15+
"github.com/crunchydata/postgres-operator/internal/testing/events"
1316
"github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1"
1417
)
1518

@@ -18,31 +21,61 @@ func TestSanitizeLogDirectory(t *testing.T) {
1821

1922
cluster := new(v1beta1.PostgresCluster)
2023
cluster.Spec.PostgresVersion = 18
24+
cluster.UID = "doot"
2125

22-
for _, tt := range []struct{ expected, from string }{
26+
recorder := events.NewRecorder(t, runtime.Scheme)
27+
28+
for _, tt := range []struct{ expected, from, event string }{
2329
// User wants logs inside the data directory.
2430
{expected: "/pgdata/pg18/log", from: "log"},
2531

32+
// Other relative paths warn.
33+
{
34+
expected: "/pgdata/pg18/some/directory", from: "some/directory",
35+
event: `"log_directory" should be "log" or an absolute path`,
36+
},
37+
2638
// Postgres interprets blank to mean root.
2739
// That's no good, so we choose better.
28-
{expected: "/pgdata/logs/postgres", from: ""},
29-
{expected: "/pgdata/logs/postgres", from: "/"},
40+
{expected: "/pgdata/logs/postgres", from: "", event: `"log_directory" = ""`},
41+
{expected: "/pgdata/logs/postgres", from: "/", event: `"log_directory" = "/"`},
3042

3143
// Paths into Postgres directories are replaced on the same volume.
32-
{expected: "/pgdata/logs/postgres", from: "."},
33-
{expected: "/pgdata/logs/postgres", from: "global"},
34-
{expected: "/pgdata/logs/postgres", from: "postgresql.conf"},
35-
{expected: "/pgdata/logs/postgres", from: "postgresql.auto.conf"},
36-
{expected: "/pgdata/logs/postgres", from: "pg_wal"},
37-
{expected: "/pgdata/logs/postgres", from: "/pgdata/pg99/any"},
38-
{expected: "/pgdata/logs/postgres", from: "/pgdata/pg18_wal"},
39-
{expected: "/pgdata/logs/postgres", from: "/pgdata/pgsql_tmp/1/2"},
40-
{expected: "/pgwal/logs/postgres", from: "/pgwal/pg18_wal"},
41-
{expected: "/pgtmp/logs/postgres", from: "/pgtmp/pg18_wal"},
44+
{
45+
expected: "/pgdata/logs/postgres", from: ".", event: `"log_directory" = "."`,
46+
}, {
47+
expected: "/pgdata/logs/postgres", from: "global", event: `"log_directory" = "global"`,
48+
}, {
49+
expected: "/pgdata/logs/postgres", from: "postgresql.conf", event: `"log_directory" = "postgresql.conf"`,
50+
}, {
51+
expected: "/pgdata/logs/postgres", from: "postgresql.auto.conf", event: `"log_directory" = "postgresql.auto.conf"`,
52+
}, {
53+
expected: "/pgdata/logs/postgres", from: "pg_wal", event: `"log_directory" = "pg_wal"`,
54+
}, {
55+
expected: "/pgdata/logs/postgres", from: "/pgdata/pg99/any", event: `"log_directory" = "/pgdata/pg99/any"`,
56+
}, {
57+
expected: "/pgdata/logs/postgres", from: "/pgdata/pg18_wal", event: `"log_directory" = "/pgdata/pg18_wal"`,
58+
}, {
59+
expected: "/pgdata/logs/postgres", from: "/pgdata/pgsql_tmp/ludicrous/speed", event: `"log_directory" = "/pgdata/pgsql_tmp/ludicrous/speed"`,
60+
}, {
61+
expected: "/pgwal/logs/postgres", from: "/pgwal/pg18_wal", event: `"log_directory" = "/pgwal/pg18_wal"`,
62+
}, {
63+
expected: "/pgtmp/logs/postgres", from: "/pgtmp/pg18_wal", event: `"log_directory" = "/pgtmp/pg18_wal"`,
64+
},
4265
} {
43-
result := sanitizeLogDirectory(cluster, tt.from)
66+
recorder.Events = nil
67+
result := sanitizeLogDirectory(cluster, tt.from, recorder)
4468

4569
assert.Equal(t, tt.expected, result, "from: %s", tt.from)
4670
assert.Assert(t, path.IsAbs(result))
71+
72+
if len(tt.event) > 0 {
73+
assert.Assert(t, cmp.Len(recorder.Events, 1))
74+
assert.Equal(t, recorder.Events[0].Type, "Warning")
75+
assert.Equal(t, recorder.Events[0].Reason, "InvalidParameter")
76+
assert.Equal(t, recorder.Events[0].Regarding.Kind, "PostgresCluster")
77+
assert.Assert(t, cmp.Equal(recorder.Events[0].Regarding.UID, "doot"))
78+
assert.Assert(t, cmp.Contains(recorder.Events[0].Note, tt.event))
79+
}
4780
}
4881
}

internal/text/text.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright 2025 Crunchy Data Solutions, Inc.
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package text
6+
7+
// TruncateAt returns the first n bytes of s.
8+
// It returns empty string when s is empty or n is less than one.
9+
func TruncateAt(s string, n int) string {
10+
return s[:max(0, min(n, len(s)))]
11+
}

internal/text/text_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright 2025 Crunchy Data Solutions, Inc.
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package text_test
6+
7+
import (
8+
"testing"
9+
10+
"gotest.tools/v3/assert"
11+
12+
"github.com/crunchydata/postgres-operator/internal/text"
13+
)
14+
15+
func TestTruncateAt(t *testing.T) {
16+
assert.Equal(t, text.TruncateAt("", -5), "")
17+
assert.Equal(t, text.TruncateAt("", -0), "")
18+
assert.Equal(t, text.TruncateAt("", +0), "")
19+
assert.Equal(t, text.TruncateAt("", +5), "")
20+
21+
assert.Equal(t, text.TruncateAt("lorem ipsum dolor sit amet", -5), "")
22+
assert.Equal(t, text.TruncateAt("lorem ipsum dolor sit amet", -0), "")
23+
assert.Equal(t, text.TruncateAt("lorem ipsum dolor sit amet", +0), "")
24+
assert.Equal(t, text.TruncateAt("lorem ipsum dolor sit amet", +5), "lorem")
25+
assert.Equal(t, text.TruncateAt("lorem ipsum dolor sit amet", 15), "lorem ipsum dol")
26+
assert.Equal(t, text.TruncateAt("lorem ipsum dolor sit amet", 50), "lorem ipsum dolor sit amet")
27+
}

0 commit comments

Comments
 (0)