Skip to content

Commit 9642615

Browse files
committed
Set correct permissions on the specified log directory
Controllers ignored the specified "log_directory" when preparing directories for Postgres. This parameter can be specified when the OpenTelemetryLogs gate is disabled. A relative path of more than one directory continues to fail during bootstrap. This does not change what happens when the OpenTelemetryLogs gate is enabled. In that case, controllers override the spec with their own value and prepare that directory. Issue: PGO-2558
1 parent d4a953a commit 9642615

File tree

13 files changed

+168
-75
lines changed

13 files changed

+168
-75
lines changed

internal/collector/postgres.go

Lines changed: 57 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,60 @@ import (
1919
"github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1"
2020
)
2121

22+
func PostgreSQLParameters(ctx context.Context,
23+
inCluster *v1beta1.PostgresCluster,
24+
outParameters *postgres.Parameters,
25+
) {
26+
version := inCluster.Spec.PostgresVersion
27+
28+
if OpenTelemetryLogsEnabled(ctx, inCluster) {
29+
var spec *v1beta1.InstrumentationLogsSpec
30+
if inCluster != nil && inCluster.Spec.Instrumentation != nil {
31+
spec = inCluster.Spec.Instrumentation.Logs
32+
}
33+
34+
// Retain logs for a short time unless specified.
35+
retention := metav1.Duration{Duration: 24 * time.Hour}
36+
if spec != nil && spec.RetentionPeriod != nil {
37+
retention = spec.RetentionPeriod.AsDuration()
38+
}
39+
40+
// Rotate log files according to retention and name them for the OpenTelemetry Collector.
41+
//
42+
// The ".log" suffix is replaced by ".csv" for CSV log files, and
43+
// the ".log" suffix is replaced by ".json" for JSON log files.
44+
//
45+
// https://www.postgresql.org/docs/current/runtime-config-logging.html
46+
for k, v := range postgres.LogRotation(retention, "postgresql-", ".log") {
47+
outParameters.Mandatory.Add(k, v)
48+
}
49+
50+
// Enable logging to file. Postgres uses a "logging collector" to safely write concurrent messages.
51+
// NOTE: That collector is designed to not lose messages. When it is overloaded, other Postgres processes block.
52+
//
53+
// https://www.postgresql.org/docs/current/runtime-config-logging.html
54+
outParameters.Mandatory.Add("logging_collector", "on")
55+
56+
// PostgreSQL v8.3 adds support for CSV logging, and
57+
// PostgreSQL v15 adds support for JSON logging.
58+
// The latter is preferred because newlines are escaped as "\n", U+005C + U+006E.
59+
if version >= 15 {
60+
outParameters.Mandatory.Add("log_destination", "jsonlog")
61+
} else {
62+
outParameters.Mandatory.Add("log_destination", "csvlog")
63+
}
64+
65+
// Log in a timezone the OpenTelemetry Collector understands.
66+
outParameters.Mandatory.Add("log_timezone", "UTC")
67+
68+
// TODO(logs): Remove this call and do it in [postgres.NewParameters] regardless of the gate.
69+
outParameters.Mandatory.Add("log_directory", fmt.Sprintf("%s/logs/postgres", postgres.DataStorage(inCluster)))
70+
}
71+
}
72+
2273
func NewConfigForPostgresPod(ctx context.Context,
2374
inCluster *v1beta1.PostgresCluster,
24-
outParameters *postgres.ParameterSet,
75+
inParameters *postgres.ParameterSet,
2576
) *Config {
2677
config := NewConfig(inCluster.Spec.Instrumentation)
2778

@@ -30,7 +81,7 @@ func NewConfigForPostgresPod(ctx context.Context,
3081
EnablePatroniMetrics(ctx, inCluster, config)
3182

3283
// Logging
33-
EnablePostgresLogging(ctx, inCluster, config, outParameters)
84+
EnablePostgresLogging(ctx, inCluster, inParameters, config)
3485
EnablePatroniLogging(ctx, inCluster, config)
3586

3687
return config
@@ -76,51 +127,18 @@ func postgresCSVNames(version int) string {
76127
func EnablePostgresLogging(
77128
ctx context.Context,
78129
inCluster *v1beta1.PostgresCluster,
130+
inParameters *postgres.ParameterSet,
79131
outConfig *Config,
80-
outParameters *postgres.ParameterSet,
81132
) {
82133
var spec *v1beta1.InstrumentationLogsSpec
83134
if inCluster != nil && inCluster.Spec.Instrumentation != nil {
84135
spec = inCluster.Spec.Instrumentation.Logs
85136
}
86137

87138
if OpenTelemetryLogsEnabled(ctx, inCluster) {
88-
directory := postgres.LogDirectory()
139+
directory := inParameters.Value("log_directory")
89140
version := inCluster.Spec.PostgresVersion
90141

91-
// https://www.postgresql.org/docs/current/runtime-config-logging.html
92-
outParameters.Add("logging_collector", "on")
93-
outParameters.Add("log_directory", directory)
94-
95-
// PostgreSQL v8.3 adds support for CSV logging, and
96-
// PostgreSQL v15 adds support for JSON logging. The latter is preferred
97-
// because newlines are escaped as "\n", U+005C + U+006E.
98-
if version < 15 {
99-
outParameters.Add("log_destination", "csvlog")
100-
} else {
101-
outParameters.Add("log_destination", "jsonlog")
102-
}
103-
104-
// If retentionPeriod is set in the spec, use that value; otherwise, we want
105-
// to use a reasonably short duration. Defaulting to 1 day.
106-
retentionPeriod := metav1.Duration{Duration: 24 * time.Hour}
107-
if spec != nil && spec.RetentionPeriod != nil {
108-
retentionPeriod = spec.RetentionPeriod.AsDuration()
109-
}
110-
111-
// Rotate log files according to retention.
112-
//
113-
// The ".log" suffix is replaced by ".csv" for CSV log files, and
114-
// the ".log" suffix is replaced by ".json" for JSON log files.
115-
//
116-
// https://www.postgresql.org/docs/current/runtime-config-logging.html
117-
for k, v := range postgres.LogRotation(retentionPeriod, "postgresql-", ".log") {
118-
outParameters.Add(k, v)
119-
}
120-
121-
// Log in a timezone that the OpenTelemetry Collector will understand.
122-
outParameters.Add("log_timezone", "UTC")
123-
124142
// Keep track of what log records and files have been processed.
125143
// Use a subdirectory of the logs directory to stay within the same failure domain.
126144
// TODO(log-rotation): Create this directory during Collector startup.
@@ -145,8 +163,8 @@ func EnablePostgresLogging(
145163
// The 2nd through 5th fields are optional, so match through to the 7th field.
146164
// This should do a decent job of not matching the middle of some SQL statement.
147165
//
148-
// The number of fields has changed over the years, but the first few
149-
// are always formatted the same way.
166+
// The number of fields has changed over the years, but the first few are always formatted the same way.
167+
// [PostgreSQLParameters] ensures the timezone is UTC.
150168
//
151169
// NOTE: This regexp is invoked in multi-line mode. https://go.dev/s/re2syntax
152170
"multiline": map[string]string{

internal/collector/postgres_test.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@ func TestEnablePostgresLogging(t *testing.T) {
3131
}`)
3232

3333
config := NewConfig(nil)
34-
params := postgres.NewParameterSet()
34+
params := postgres.NewParameters()
3535

36-
EnablePostgresLogging(ctx, cluster, config, params)
36+
// NOTE: This call is necessary only because the default "log_directory" is not absolute.
37+
PostgreSQLParameters(ctx, cluster, &params)
38+
EnablePostgresLogging(ctx, cluster, params.Mandatory, config)
3739

3840
result, err := config.ToYAML()
3941
assert.NilError(t, err)
@@ -293,9 +295,11 @@ service:
293295
cluster.Spec.Instrumentation = testInstrumentationSpec()
294296

295297
config := NewConfig(cluster.Spec.Instrumentation)
296-
params := postgres.NewParameterSet()
298+
params := postgres.NewParameters()
297299

298-
EnablePostgresLogging(ctx, cluster, config, params)
300+
// NOTE: This call is necessary only because the default "log_directory" is not absolute.
301+
PostgreSQLParameters(ctx, cluster, &params)
302+
EnablePostgresLogging(ctx, cluster, params.Mandatory, config)
299303

300304
result, err := config.ToYAML()
301305
assert.NilError(t, err)

internal/controller/postgrescluster/controller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ func (r *Reconciler) Reconcile(
339339
ctx, cluster, clusterConfigMap, clusterReplicationSecret, rootCA,
340340
clusterPodService, instanceServiceAccount, instances, patroniLeaderService,
341341
primaryCertificate, clusterVolumes, exporterQueriesConfig, exporterWebConfig,
342-
backupsSpecFound, otelConfig,
342+
backupsSpecFound, otelConfig, pgParameters,
343343
)
344344
}
345345

internal/controller/postgrescluster/instance.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,7 @@ func (r *Reconciler) reconcileInstanceSets(
534534
exporterQueriesConfig, exporterWebConfig *corev1.ConfigMap,
535535
backupsSpecFound bool,
536536
otelConfig *collector.Config,
537+
pgParameters *postgres.ParameterSet,
537538
) error {
538539

539540
// Go through the observed instances and check if a primary has been determined.
@@ -571,7 +572,7 @@ func (r *Reconciler) reconcileInstanceSets(
571572
patroniLeaderService, primaryCertificate,
572573
findAvailableInstanceNames(*set, instances, clusterVolumes),
573574
numInstancePods, clusterVolumes, exporterQueriesConfig, exporterWebConfig,
574-
backupsSpecFound, otelConfig,
575+
backupsSpecFound, otelConfig, pgParameters,
575576
)
576577

577578
if err == nil {
@@ -1007,6 +1008,7 @@ func (r *Reconciler) scaleUpInstances(
10071008
exporterQueriesConfig, exporterWebConfig *corev1.ConfigMap,
10081009
backupsSpecFound bool,
10091010
otelConfig *collector.Config,
1011+
pgParameters *postgres.ParameterSet,
10101012
) ([]*appsv1.StatefulSet, error) {
10111013
log := logging.FromContext(ctx)
10121014

@@ -1053,7 +1055,7 @@ func (r *Reconciler) scaleUpInstances(
10531055
rootCA, clusterPodService, instanceServiceAccount,
10541056
patroniLeaderService, primaryCertificate, instances[i],
10551057
numInstancePods, clusterVolumes, exporterQueriesConfig, exporterWebConfig,
1056-
backupsSpecFound, otelConfig,
1058+
backupsSpecFound, otelConfig, pgParameters,
10571059
)
10581060
}
10591061
if err == nil {
@@ -1085,6 +1087,7 @@ func (r *Reconciler) reconcileInstance(
10851087
exporterQueriesConfig, exporterWebConfig *corev1.ConfigMap,
10861088
backupsSpecFound bool,
10871089
otelConfig *collector.Config,
1090+
pgParameters *postgres.ParameterSet,
10881091
) error {
10891092
log := logging.FromContext(ctx).WithValues("instance", instance.Name)
10901093
ctx = logging.NewContext(ctx, log)
@@ -1128,7 +1131,7 @@ func (r *Reconciler) reconcileInstance(
11281131
postgres.InstancePod(
11291132
ctx, cluster, spec,
11301133
primaryCertificate, replicationCertSecretProjection(clusterReplicationSecret),
1131-
postgresDataVolume, postgresWALVolume, tablespaceVolumes,
1134+
postgresDataVolume, postgresWALVolume, tablespaceVolumes, pgParameters,
11321135
&instance.Spec.Template)
11331136

11341137
if backupsSpecFound {

internal/controller/postgrescluster/postgres.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"k8s.io/apimachinery/pkg/util/validation/field"
2626
"sigs.k8s.io/controller-runtime/pkg/client"
2727

28+
"github.com/crunchydata/postgres-operator/internal/collector"
2829
"github.com/crunchydata/postgres-operator/internal/feature"
2930
"github.com/crunchydata/postgres-operator/internal/initialize"
3031
"github.com/crunchydata/postgres-operator/internal/logging"
@@ -129,6 +130,7 @@ func (*Reconciler) generatePostgresParameters(
129130
ctx context.Context, cluster *v1beta1.PostgresCluster, backupsSpecFound bool,
130131
) *postgres.ParameterSet {
131132
builtin := postgres.NewParameters()
133+
collector.PostgreSQLParameters(ctx, cluster, &builtin)
132134
pgaudit.PostgreSQLParameters(&builtin)
133135
pgbackrest.PostgreSQLParameters(cluster, &builtin, backupsSpecFound)
134136
pgmonitor.PostgreSQLParameters(ctx, cluster, &builtin)

internal/postgres/config.go

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"context"
99
"fmt"
1010
"math"
11+
"path"
1112
"strings"
1213

1314
corev1 "k8s.io/api/core/v1"
@@ -121,13 +122,13 @@ func ConfigDirectory(cluster *v1beta1.PostgresCluster) string {
121122
// DataDirectory returns the absolute path to the "data_directory" of cluster.
122123
// - https://www.postgresql.org/docs/current/runtime-config-file-locations.html
123124
func DataDirectory(cluster *v1beta1.PostgresCluster) string {
124-
return fmt.Sprintf("%s/pg%d", dataMountPath, cluster.Spec.PostgresVersion)
125+
return fmt.Sprintf("%s/pg%d", DataStorage(cluster), cluster.Spec.PostgresVersion)
125126
}
126127

127-
// LogDirectory returns the absolute path to the "log_directory" of cluster.
128-
// - https://www.postgresql.org/docs/current/runtime-config-logging.html
129-
func LogDirectory() string {
130-
return fmt.Sprintf("%s/logs/postgres", dataMountPath)
128+
// DataStorage returns the absolute path to the disk where cluster stores its data.
129+
// Use [DataDirectory] for the exact directory that Postgres uses.
130+
func DataStorage(cluster *v1beta1.PostgresCluster) string {
131+
return dataMountPath
131132
}
132133

133134
// LogRotation returns parameters that rotate log files while keeping a minimum amount.
@@ -354,12 +355,16 @@ done
354355
// PostgreSQL.
355356
func startupCommand(
356357
ctx context.Context,
357-
cluster *v1beta1.PostgresCluster, instance *v1beta1.PostgresInstanceSetSpec,
358+
cluster *v1beta1.PostgresCluster,
359+
instance *v1beta1.PostgresInstanceSetSpec,
360+
parameters *ParameterSet,
358361
) []string {
359362
version := fmt.Sprint(cluster.Spec.PostgresVersion)
363+
dataDir := DataDirectory(cluster)
364+
logDir := parameters.Value("log_directory")
360365
walDir := WALDirectory(cluster, instance)
361366

362-
mkdirs := make([]string, 0, 7+len(instance.TablespaceVolumes))
367+
mkdirs := make([]string, 0, 9+len(instance.TablespaceVolumes))
363368
mkdirs = append(mkdirs, `dataDirectory "${postgres_data_directory}" || halt "$(permissions "${postgres_data_directory}" ||:)"`)
364369

365370
// If the user requests tablespaces, we want to make sure the directories exist with the correct owner and permissions.
@@ -372,11 +377,24 @@ func startupCommand(
372377
}
373378
}
374379

380+
// Postgres creates "log_directory" but does *not* create any of its parent directories.
381+
// Postgres omits the group-write S_IWGRP permission on the directory. Do both here while being
382+
// careful to *not* touch "data_directory" contents until after `initdb` or Patroni bootstrap.
383+
if path.IsAbs(logDir) && !strings.HasPrefix(logDir, dataDir) {
384+
mkdirs = append(mkdirs,
385+
`(`+shell.MakeDirectories(dataMountPath, logDir)+`) ||`,
386+
`halt "$(permissions `+shell.QuoteWord(logDir)+` ||:)"`,
387+
)
388+
} else {
389+
mkdirs = append(mkdirs,
390+
`[[ ! -f `+shell.QuoteWord(path.Join(dataDir, "PG_VERSION"))+` ]] ||`,
391+
`(`+shell.MakeDirectories(dataDir, logDir)+`) ||`,
392+
`halt "$(permissions `+shell.QuoteWord(path.Join(dataDir, logDir))+` ||:)"`,
393+
)
394+
}
395+
375396
// These directories are outside "data_directory" and can be created.
376397
mkdirs = append(mkdirs,
377-
`(`+shell.MakeDirectories(dataMountPath, LogDirectory())+`) ||`,
378-
`halt "$(permissions `+shell.QuoteWord(LogDirectory())+` ||:)"`,
379-
380398
`(`+shell.MakeDirectories(dataMountPath, naming.PatroniPGDataLogPath)+`) ||`,
381399
`halt "$(permissions `+shell.QuoteWord(naming.PatroniPGDataLogPath)+` ||:)"`,
382400

internal/postgres/config_test.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ func TestDataDirectory(t *testing.T) {
4040
assert.Equal(t, DataDirectory(cluster), "/pgdata/pg12")
4141
}
4242

43+
func TestDataStorage(t *testing.T) {
44+
cluster := new(v1beta1.PostgresCluster)
45+
cluster.Spec.PostgresVersion = rand.IntN(20)
46+
47+
assert.Equal(t, DataStorage(cluster), "/pgdata")
48+
}
49+
4350
func TestLogRotation(t *testing.T) {
4451
t.Parallel()
4552

@@ -543,8 +550,10 @@ func TestStartupCommand(t *testing.T) {
543550
cluster.Spec.PostgresVersion = 13
544551
instance := new(v1beta1.PostgresInstanceSetSpec)
545552

553+
parameters := NewParameters().Default
554+
546555
ctx := context.Background()
547-
command := startupCommand(ctx, cluster, instance)
556+
command := startupCommand(ctx, cluster, instance, parameters)
548557

549558
// Expect a bash command with an inline script.
550559
assert.DeepEqual(t, command[:3], []string{"bash", "-ceu", "--"})
@@ -579,7 +588,7 @@ func TestStartupCommand(t *testing.T) {
579588
},
580589
},
581590
}
582-
command := startupCommand(ctx, cluster, instance)
591+
command := startupCommand(ctx, cluster, instance, parameters)
583592
assert.Assert(t, len(command) > 3)
584593
assert.Assert(t, strings.Contains(command[3], `cat << "EOF" > /tmp/pg_rewind_tde.sh
585594
#!/bin/sh

internal/postgres/parameters.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,17 @@ func NewParameters() Parameters {
4848
// - https://www.postgresql.org/docs/current/auth-password.html
4949
parameters.Default.Add("password_encryption", "scram-sha-256")
5050

51+
// Explicitly use Postgres' default log directory. This value is relative to the "data_directory" parameter.
52+
//
53+
// Controllers look at this parameter to grant group-write S_IWGRP on the directory.
54+
// Postgres does not grant this permission on the directories it creates.
55+
//
56+
// TODO(logs): A better default would be *outside* the data directory.
57+
// This parameter needs to be configurable and documented before the default can change.
58+
//
59+
// PostgreSQL must be reloaded when changing this parameter.
60+
parameters.Default.Add("log_directory", "log")
61+
5162
// Pod "securityContext.fsGroup" ensures processes and filesystems agree on a GID;
5263
// use the same permissions for group and owner.
5364
// This allows every process in the pod to read Postgres log files.

internal/postgres/parameters_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ func TestNewParameters(t *testing.T) {
2828
assert.DeepEqual(t, parameters.Default.AsMap(), map[string]string{
2929
"jit": "off",
3030

31+
"log_directory": "log",
32+
3133
"password_encryption": "scram-sha-256",
3234
})
3335
}

internal/postgres/reconcile.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ func InstancePod(ctx context.Context,
6262
inClusterCertificates, inClientCertificates *corev1.SecretProjection,
6363
inDataVolume, inWALVolume *corev1.PersistentVolumeClaim,
6464
inTablespaceVolumes []*corev1.PersistentVolumeClaim,
65+
inParameters *ParameterSet,
6566
outInstancePod *corev1.PodTemplateSpec,
6667
) {
6768
certVolumeMount := corev1.VolumeMount{
@@ -191,7 +192,7 @@ func InstancePod(ctx context.Context,
191192
startup := corev1.Container{
192193
Name: naming.ContainerPostgresStartup,
193194

194-
Command: startupCommand(ctx, inCluster, inInstanceSpec),
195+
Command: startupCommand(ctx, inCluster, inInstanceSpec, inParameters),
195196
Env: Environment(inCluster),
196197

197198
Image: container.Image,

0 commit comments

Comments
 (0)