Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14284,7 +14284,12 @@ spec:
description: |-
Settings that apply to the entire PgBouncer process.
More info: https://www.pgbouncer.org/config.html
maxProperties: 50
type: object
x-kubernetes-map-type: granular
x-kubernetes-validations:
- message: logfile config must end with '.log'
rule: '!has(self.logfile) || self.logfile.endsWith(''.log'')'
users:
additionalProperties:
type: string
Expand Down Expand Up @@ -16258,6 +16263,12 @@ spec:
x-kubernetes-list-type: map
type: object
type: object
x-kubernetes-validations:
- message: config.global.logfile destination is restricted to
'/tmp/logs/pgbouncer/' or an existing additional volume
rule: self.?config.global.logfile.optMap(f, f.startsWith("/tmp/logs/pgbouncer/")
|| (self.?volumes.additional.hasValue() && self.volumes.additional.exists(v,
f.startsWith("/volumes/" + v.name)))).orValue(true)
required:
- pgBouncer
type: object
Expand Down Expand Up @@ -33063,7 +33074,12 @@ spec:
description: |-
Settings that apply to the entire PgBouncer process.
More info: https://www.pgbouncer.org/config.html
maxProperties: 50
type: object
x-kubernetes-map-type: granular
x-kubernetes-validations:
- message: logfile config must end with '.log'
rule: '!has(self.logfile) || self.logfile.endsWith(''.log'')'
users:
additionalProperties:
type: string
Expand Down
2 changes: 1 addition & 1 deletion internal/pgbouncer/reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ func Pod(
if collector.OpenTelemetryLogsOrMetricsEnabled(ctx, inCluster) {
collector.AddToPod(ctx, inCluster.Spec.Instrumentation, inCluster.Spec.ImagePullPolicy, inConfigMap,
template, []corev1.VolumeMount{configVolumeMount}, string(inSecret.Data["pgbouncer-password"]),
[]string{naming.PGBouncerLogPath}, true, true)
[]string{logPath}, true, true)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed this in an earlier PR re: OTEL handling user-set logfiles.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch.

}
}

Expand Down
73 changes: 73 additions & 0 deletions internal/pgbouncer/reconcile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ func TestPod(t *testing.T) {
t.Parallel()

features := feature.NewGate()
assert.NilError(t, features.SetFromMap(map[string]bool{
feature.OpenTelemetryLogs: true,
feature.OpenTelemetryMetrics: true,
}))
Comment on lines +145 to +148
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 to setting it here since nothing happens until instrumentation is in the spec

ctx := feature.NewContext(context.Background(), features)

cluster := new(v1beta1.PostgresCluster)
Expand Down Expand Up @@ -463,6 +467,59 @@ containers:
- mountPath: /etc/pgbouncer
name: pgbouncer-config
readOnly: true
- command:
- bash
- -ceu
- --
- "monitor() {\n\nmkdir -p '/tmp/receiver' && { chmod 0775 '/tmp/receiver' || :;
}\nOTEL_PIDFILE=/tmp/otel.pid\n\nstart_otel_collector() {\n\techo \"Starting OTel
Collector\"\n\t/otelcol-contrib --config /etc/otel-collector/config.yaml &\n\techo
$! > $OTEL_PIDFILE\n}\nstart_otel_collector\n\nexec {fd}<> <(:||:)\nwhile read
-r -t 5 -u \"${fd}\" ||:; do\n\tlogrotate -s /tmp/logrotate.status /etc/logrotate.d/logrotate.conf\n\tif
[[ \"${directory}\" -nt \"/proc/self/fd/${fd}\" ]] && kill -HUP $(head -1 ${OTEL_PIDFILE?});\n\tthen\n\t\techo
\"OTel configuration changed...\"\n\t\texec {fd}>&- && exec {fd}<> <(:||:)\n\t\tstat
--format='Loaded configuration dated %y' \"${directory}\"\n\tfi\n\tif [[ ! -e
/proc/$(head -1 ${OTEL_PIDFILE?}) ]] ; then\n\t\tstart_otel_collector\n\tfi\ndone\n};
export directory=\"$1\"; export -f monitor; exec -a \"$0\" bash -ceu monitor"
- collector
- /etc/otel-collector
env:
- name: K8S_POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: K8S_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: PGPASSWORD
imagePullPolicy: Always
name: collector
ports:
- containerPort: 9187
name: otel-metrics
protocol: TCP
resources: {}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
privileged: false
readOnlyRootFilesystem: true
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
volumeMounts:
- mountPath: /etc/pgbouncer
name: pgbouncer-config
readOnly: true
- mountPath: /etc/otel-collector
name: collector-config
readOnly: true
- mountPath: /etc/logrotate.d
name: logrotate-config
readOnly: true
volumes:
- name: pgbouncer-config
projected:
Expand Down Expand Up @@ -490,6 +547,20 @@ volumes:
items:
- key: ca.crt
path: ~postgres-operator/backend-ca.crt
- name: logrotate-config
projected:
sources:
- configMap:
items:
- key: logrotate.conf
path: logrotate.conf
- name: collector-config
projected:
sources:
- configMap:
items:
- key: collector.yaml
path: config.yaml
`))
})

Expand All @@ -498,6 +569,8 @@ volumes:
"logfile": "/volumes/required/mylog.log",
}
logfile = "/volumes/required/mylog.log"
// Reset the instrumentation from the previous test
cluster.Spec.Instrumentation = nil

call()

Expand Down
168 changes: 168 additions & 0 deletions internal/testing/validation/pgbouncer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// Copyright 2021 - 2025 Crunchy Data Solutions, Inc.
//
// SPDX-License-Identifier: Apache-2.0

package validation

import (
"context"
"testing"

"gotest.tools/v3/assert"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/crunchydata/postgres-operator/internal/testing/require"
v1 "github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1"
)

func TestV1PGBouncerLogging(t *testing.T) {
ctx := context.Background()
cc := require.Kubernetes(t)
t.Parallel()

namespace := require.Namespace(t, cc)

base := v1.NewPostgresCluster()
base.Namespace = namespace.Name
base.Name = "pgbouncer-logging"
// required fields
require.UnmarshalInto(t, &base.Spec, `{
postgresVersion: 16,
instances: [{
dataVolumeClaimSpec: {
accessModes: [ReadWriteOnce],
resources: { requests: { storage: 1Mi } },
},
}],
}`)

assert.NilError(t, cc.Create(ctx, base.DeepCopy(), client.DryRunAll),
"expected this base to be valid")

t.Run("Can set logging on tmp with .log", func(t *testing.T) {
tmp := base.DeepCopy()

require.UnmarshalInto(t, &tmp.Spec.Proxy, `{
pgBouncer: {
config: {
global: {
logfile: "/tmp/logs/pgbouncer/log.log"
}
}
}
}`)
assert.NilError(t, cc.Create(ctx, base.DeepCopy(), client.DryRunAll),
"expected this option to be valid")
})

t.Run("Cannot set logging on tmp without .log", func(t *testing.T) {
tmp := base.DeepCopy()

require.UnmarshalInto(t, &tmp.Spec.Proxy, `{
pgBouncer: {
config: {
global: {
logfile: "/tmp/logs/pgbouncer/log.txt"
}
}
}
}`)

err := cc.Create(ctx, tmp.DeepCopy(), client.DryRunAll)
assert.Assert(t, apierrors.IsInvalid(err))
assert.ErrorContains(t, err, "logfile config must end with '.log'")
})

t.Run("Cannot set logging on tmp without correct subdir", func(t *testing.T) {
tmp := base.DeepCopy()

require.UnmarshalInto(t, &tmp.Spec.Proxy, `{
pgBouncer: {
config: {
global: {
logfile: "/tmp/logs/log.log"
}
}
}
}`)

err := cc.Create(ctx, tmp.DeepCopy(), client.DryRunAll)
assert.Assert(t, apierrors.IsInvalid(err))
assert.ErrorContains(t, err, "logfile destination is restricted to '/tmp/logs/pgbouncer/' or an existing additional volume")

require.UnmarshalInto(t, &tmp.Spec.Proxy, `{
pgBouncer: {
config: {
global: {
logfile: "/tmp/pgbouncer/log.log"
}
}
}
}`)

err = cc.Create(ctx, tmp.DeepCopy(), client.DryRunAll)
assert.Assert(t, apierrors.IsInvalid(err))
assert.ErrorContains(t, err, "logfile destination is restricted to '/tmp/logs/pgbouncer/' or an existing additional volume")
})

t.Run("Cannot set logging on volumes that don't exist", func(t *testing.T) {
vol := base.DeepCopy()

require.UnmarshalInto(t, &vol.Spec.Proxy, `{
pgBouncer: {
config: {
global: {
logfile: "/volumes/logging/log.log"
}
}
}
}`)

err := cc.Create(ctx, vol.DeepCopy(), client.DryRunAll)
assert.Assert(t, apierrors.IsInvalid(err))
assert.ErrorContains(t, err, "logfile destination is restricted to '/tmp/logs/pgbouncer/' or an existing additional volume")
})

t.Run("Cannot set logging elsewhere", func(t *testing.T) {
vol := base.DeepCopy()

require.UnmarshalInto(t, &vol.Spec.Proxy, `{
pgBouncer: {
config: {
global: {
logfile: "/var/log.log"
}
}
}
}`)

err := cc.Create(ctx, vol.DeepCopy(), client.DryRunAll)
assert.Assert(t, apierrors.IsInvalid(err))
assert.ErrorContains(t, err, "logfile destination is restricted to '/tmp/logs/pgbouncer/' or an existing additional volume")
})

t.Run("Can set logging on volumes that exist", func(t *testing.T) {
vol := base.DeepCopy()

require.UnmarshalInto(t, &vol.Spec.Proxy, `{
pgBouncer: {
config: {
global: {
logfile: "/volumes/logging/log.log"
}
},
volumes: {
additional: [
{
name: logging,
claimName: required-1
}]
}
}
}`)

assert.NilError(t, cc.Create(ctx, vol.DeepCopy(), client.DryRunAll),
"expected this option to be valid")
})
}
15 changes: 15 additions & 0 deletions pkg/apis/postgres-operator.crunchydata.com/v1/pgbouncer_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright 2021 - 2025 Crunchy Data Solutions, Inc.
//
// SPDX-License-Identifier: Apache-2.0

package v1

import (
"github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1"
)

// PGBouncerPodSpec defines the desired state of a PgBouncer connection pooler.
// +kubebuilder:validation:XValidation:rule=`self.?config.global.logfile.optMap(f, f.startsWith("/tmp/logs/pgbouncer/") || (self.?volumes.additional.hasValue() && self.volumes.additional.exists(v, f.startsWith("/volumes/" + v.name)))).orValue(true)`,message=`config.global.logfile destination is restricted to '/tmp/logs/pgbouncer/' or an existing additional volume`
type PGBouncerPodSpec struct {
v1beta1.PGBouncerPodSpec `json:",inline"`
}
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,7 @@ type PostgresInstanceSetStatus struct {
type PostgresProxySpec struct {

// Defines a PgBouncer proxy and connection pooler.
PGBouncer *v1beta1.PGBouncerPodSpec `json:"pgBouncer"`
PGBouncer *PGBouncerPodSpec `json:"pgBouncer"`
}

// Default sets the defaults for any proxies that are set.
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,14 @@ type PGBouncerConfiguration struct {

// Settings that apply to the entire PgBouncer process.
// More info: https://www.pgbouncer.org/config.html
// ---
// # Logging
// +kubebuilder:validation:XValidation:rule=`!has(self.logfile) || self.logfile.endsWith('.log')`,message=`logfile config must end with '.log'`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// +kubebuilder:validation:XValidation:rule=`!has(self.logfile) || self.logfile.endsWith('.log')`,message=`logfile config must end with '.log'`
// +kubebuilder:validation:XValidation:rule=`self.?logfile.endsWith('.log')orValue(true)`,message=`logfile config must end with '.log'`

Is something like this more legible?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that doesn't work:

compilation failed: ERROR: <input>:1:23: found no matching overload for 'endsWith' applied to 'optional_type(string).(string)'")

But this works:

self.?logfile.orValue("").endsWith('.log')

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

meh, for just one step, I'll do without the "?"

// +kubebuilder:validation:MaxProperties=50
Comment on lines +31 to +32
Copy link
Contributor Author

@benjaminjb benjaminjb Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we feel about adding these two validations/exclusions here to v1beta1?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻 These new rules/criteria seem fine to me.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed 👍

// See also XValidation rule on v1 PostgresProxySpec
//
// +optional
// +mapType=granular
Global map[string]string `json:"global,omitempty"`

// PgBouncer database definitions. The key is the database requested by a
Expand Down
Loading
Loading