diff --git a/docs/reference/cluster_manifest.md b/docs/reference/cluster_manifest.md index fd0660f57..212dfa86e 100644 --- a/docs/reference/cluster_manifest.md +++ b/docs/reference/cluster_manifest.md @@ -324,6 +324,12 @@ explanation of `ttl` and `loop_wait` parameters. custom `pg_hba` should include the pam line to avoid breaking pam authentication. Optional. +* **pg_ident** + list of custom `pg_ident` lines defining user name maps for external + authentication methods (e.g. cert, gss, peer). Each line is of the form + `mapname system-username pg-username`. Patroni manages `pg_ident.conf` + and reloads PostgreSQL when this list changes. Optional. + * **ttl** Patroni `ttl` parameter value, optional. The default is set by the Spilo Docker image. Optional. diff --git a/manifests/complete-postgres-manifest.yaml b/manifests/complete-postgres-manifest.yaml index 7b347a9c8..e7b465fc2 100644 --- a/manifests/complete-postgres-manifest.yaml +++ b/manifests/complete-postgres-manifest.yaml @@ -137,6 +137,9 @@ spec: # pg_hba: # - hostssl all all 0.0.0.0/0 md5 # - host all all 0.0.0.0/0 md5 +# pg_ident: +# - mymap /^(.*)@example\.com$ \1 +# - mymap admin@example.com postgres # slots: # permanent_physical_1: # type: physical diff --git a/manifests/postgresql.crd.yaml b/manifests/postgresql.crd.yaml index 39811824e..4cc1cd6c3 100644 --- a/manifests/postgresql.crd.yaml +++ b/manifests/postgresql.crd.yaml @@ -3484,6 +3484,10 @@ spec: items: type: string type: array + pg_ident: + items: + type: string + type: array retry_timeout: format: int32 type: integer diff --git a/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml b/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml index 39811824e..4cc1cd6c3 100644 --- a/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml +++ b/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml @@ -3484,6 +3484,10 @@ spec: items: type: string type: array + pg_ident: + items: + type: string + type: array retry_timeout: format: int32 type: integer diff --git a/pkg/apis/acid.zalan.do/v1/postgresql_type.go b/pkg/apis/acid.zalan.do/v1/postgresql_type.go index 1dadfd06c..9fd73325c 100644 --- a/pkg/apis/acid.zalan.do/v1/postgresql_type.go +++ b/pkg/apis/acid.zalan.do/v1/postgresql_type.go @@ -235,6 +235,7 @@ type Resources struct { type Patroni struct { InitDB map[string]string `json:"initdb,omitempty"` PgHba []string `json:"pg_hba,omitempty"` + PgIdent []string `json:"pg_ident,omitempty"` TTL uint32 `json:"ttl,omitempty"` LoopWait uint32 `json:"loop_wait,omitempty"` RetryTimeout uint32 `json:"retry_timeout,omitempty"` diff --git a/pkg/apis/acid.zalan.do/v1/util_test.go b/pkg/apis/acid.zalan.do/v1/util_test.go index fcc5ae5fd..3e855da4d 100644 --- a/pkg/apis/acid.zalan.do/v1/util_test.go +++ b/pkg/apis/acid.zalan.do/v1/util_test.go @@ -257,6 +257,10 @@ var unmarshalCluster = []struct { "hostssl all all 0.0.0.0/0 md5", "host all all 0.0.0.0/0 md5" ], + "pg_ident": [ + "mymap user1 dbuser1", + "mymap user2 dbuser2" + ], "ttl": 30, "loop_wait": 10, "retry_timeout": 10, @@ -307,6 +311,7 @@ var unmarshalCluster = []struct { "data-checksums": "true", }, PgHba: []string{"hostssl all all 0.0.0.0/0 md5", "host all all 0.0.0.0/0 md5"}, + PgIdent: []string{"mymap user1 dbuser1", "mymap user2 dbuser2"}, TTL: 30, LoopWait: 10, RetryTimeout: 10, @@ -346,7 +351,7 @@ var unmarshalCluster = []struct { }, Error: "", }, - marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"18","parameters":{"log_statement":"all","max_connections":"10","shared_buffers":"32MB"}},"pod_priority_class_name":"spilo-pod-priority","volume":{"size":"5Gi","storageClass":"SSD", "subPath": "subdir"},"enableShmVolume":false,"patroni":{"initdb":{"data-checksums":"true","encoding":"UTF8","locale":"en_US.UTF-8"},"pg_hba":["hostssl all all 0.0.0.0/0 md5","host all all 0.0.0.0/0 md5"],"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"slots":{"permanent_logical_1":{"database":"foo","plugin":"pgoutput","type":"logical"}}},"resources":{"requests":{"cpu":"10m","memory":"50Mi"},"limits":{"cpu":"300m","memory":"3000Mi"}},"teamId":"acid","allowedSourceRanges":["127.0.0.1/32"],"numberOfInstances":2,"users":{"zalando":["superuser","createdb"]},"maintenanceWindows":["Mon:01:00-06:00","Sat:00:00-04:00","05:00-05:15"],"clone":{"cluster":"acid-batman"}},"status":{"PostgresClusterStatus":""}}`), + marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"18","parameters":{"log_statement":"all","max_connections":"10","shared_buffers":"32MB"}},"pod_priority_class_name":"spilo-pod-priority","volume":{"size":"5Gi","storageClass":"SSD", "subPath": "subdir"},"enableShmVolume":false,"patroni":{"initdb":{"data-checksums":"true","encoding":"UTF8","locale":"en_US.UTF-8"},"pg_hba":["hostssl all all 0.0.0.0/0 md5","host all all 0.0.0.0/0 md5"],"pg_ident":["mymap user1 dbuser1","mymap user2 dbuser2"],"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"slots":{"permanent_logical_1":{"database":"foo","plugin":"pgoutput","type":"logical"}}},"resources":{"requests":{"cpu":"10m","memory":"50Mi"},"limits":{"cpu":"300m","memory":"3000Mi"}},"teamId":"acid","allowedSourceRanges":["127.0.0.1/32"],"numberOfInstances":2,"users":{"zalando":["superuser","createdb"]},"maintenanceWindows":["Mon:01:00-06:00","Sat:00:00-04:00","05:00-05:15"],"clone":{"cluster":"acid-batman"}},"status":{"PostgresClusterStatus":""}}`), err: nil}, { about: "example with clone", diff --git a/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go b/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go index 0fa4b1037..572ac07ea 100644 --- a/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go +++ b/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go @@ -587,6 +587,11 @@ func (in *Patroni) DeepCopyInto(out *Patroni) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.PgIdent != nil { + in, out := &in.PgIdent, &out.PgIdent + *out = make([]string, len(*in)) + copy(*out, *in) + } if in.Slots != nil { in, out := &in.Slots, &out.Slots *out = make(map[string]map[string]string, len(*in)) diff --git a/pkg/cluster/k8sres.go b/pkg/cluster/k8sres.go index 2eb867f06..965fc7527 100644 --- a/pkg/cluster/k8sres.go +++ b/pkg/cluster/k8sres.go @@ -35,15 +35,16 @@ import ( ) const ( - pgBinariesLocationTemplate = "/usr/lib/postgresql/%v/bin" - patroniPGBinariesParameterName = "bin_dir" - patroniPGHBAConfParameterName = "pg_hba" - localHost = "127.0.0.1/32" - scalyrSidecarName = "scalyr-sidecar" - logicalBackupContainerName = "logical-backup" - connectionPoolerContainer = "connection-pooler" - pgPort = 5432 - operatorPort = 8080 + pgBinariesLocationTemplate = "/usr/lib/postgresql/%v/bin" + patroniPGBinariesParameterName = "bin_dir" + patroniPGHBAConfParameterName = "pg_hba" + patroniPGIdentConfParameterName = "pg_ident" + localHost = "127.0.0.1/32" + scalyrSidecarName = "scalyr-sidecar" + logicalBackupContainerName = "logical-backup" + connectionPoolerContainer = "connection-pooler" + pgPort = 5432 + operatorPort = 8080 ) type patroniDCS struct { @@ -469,12 +470,16 @@ PatroniInitDBParams: config.Bootstrap.DCS.PGBootstrapConfiguration[constants.PatroniPGParametersParameterName] = bootstrap } } - // Patroni gives us a choice of writing pg_hba.conf to either the bootstrap section or to the local postgresql one. - // We choose the local one, because we need Patroni to change pg_hba.conf in PostgreSQL after the user changes the + + // Patroni gives us a choice of writing pg_hba.conf and pg_ident.conf to either the bootstrap section or to the local postgresql one. + // We choose the local one, because we need Patroni to change them in PostgreSQL after the user changes the // relevant section in the manifest. if len(patroni.PgHba) > 0 { config.PgLocalConfiguration[patroniPGHBAConfParameterName] = patroni.PgHba } + if len(patroni.PgIdent) > 0 { + config.PgLocalConfiguration[patroniPGIdentConfParameterName] = patroni.PgIdent + } res, err := json.Marshal(config) return string(res), err diff --git a/pkg/cluster/k8sres_test.go b/pkg/cluster/k8sres_test.go index 62481c7e3..3b1fcfa66 100644 --- a/pkg/cluster/k8sres_test.go +++ b/pkg/cluster/k8sres_test.go @@ -91,6 +91,7 @@ func TestGenerateSpiloJSONConfiguration(t *testing.T) { "data-checksums": "true", }, PgHba: []string{"hostssl all all 0.0.0.0/0 md5", "host all all 0.0.0.0/0 md5"}, + PgIdent: []string{"mymap user1 dbuser1", "mymap user2 dbuser2"}, TTL: 30, LoopWait: 10, RetryTimeout: 10, @@ -102,7 +103,7 @@ func TestGenerateSpiloJSONConfiguration(t *testing.T) { FailsafeMode: util.True(), }, opConfig: &config.Config{}, - result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/18/bin","pg_hba":["hostssl all all 0.0.0.0/0 md5","host all all 0.0.0.0/0 md5"]},"bootstrap":{"initdb":[{"auth-host":"md5"},{"auth-local":"trust"},"data-checksums",{"encoding":"UTF8"},{"locale":"en_US.UTF-8"}],"dcs":{"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"synchronous_mode":true,"synchronous_mode_strict":true,"synchronous_node_count":1,"slots":{"permanent_logical_1":{"database":"foo","plugin":"pgoutput","type":"logical"}},"failsafe_mode":true}}}`, + result: `{"postgresql":{"bin_dir":"/usr/lib/postgresql/18/bin","pg_hba":["hostssl all all 0.0.0.0/0 md5","host all all 0.0.0.0/0 md5"],"pg_ident":["mymap user1 dbuser1","mymap user2 dbuser2"]},"bootstrap":{"initdb":[{"auth-host":"md5"},{"auth-local":"trust"},"data-checksums",{"encoding":"UTF8"},{"locale":"en_US.UTF-8"}],"dcs":{"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"synchronous_mode":true,"synchronous_mode_strict":true,"synchronous_node_count":1,"slots":{"permanent_logical_1":{"database":"foo","plugin":"pgoutput","type":"logical"}},"failsafe_mode":true}}}`, }, { subtest: "Patroni failsafe_mode configured globally", diff --git a/pkg/cluster/sync.go b/pkg/cluster/sync.go index 7c478477a..ae01b0235 100644 --- a/pkg/cluster/sync.go +++ b/pkg/cluster/sync.go @@ -917,6 +917,9 @@ func (c *Cluster) checkAndSetGlobalPostgreSQLConfiguration(pod *v1.Pod, effectiv if desiredPatroniConfig.PgHba != nil && !reflect.DeepEqual(desiredPatroniConfig.PgHba, effectivePatroniConfig.PgHba) { configToSet["pg_hba"] = desiredPatroniConfig.PgHba } + if desiredPatroniConfig.PgIdent != nil && !reflect.DeepEqual(desiredPatroniConfig.PgIdent, effectivePatroniConfig.PgIdent) { + configToSet["pg_ident"] = desiredPatroniConfig.PgIdent + } if desiredPatroniConfig.RetryTimeout > 0 && desiredPatroniConfig.RetryTimeout != effectivePatroniConfig.RetryTimeout { configToSet["retry_timeout"] = desiredPatroniConfig.RetryTimeout }