Skip to content

Commit eb990fd

Browse files
committed
add the possibility to create a standby cluster that streams from a remote primary
1 parent ca0c27a commit eb990fd

File tree

8 files changed

+149
-35
lines changed

8 files changed

+149
-35
lines changed

charts/postgres-operator/crds/postgresqls.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,10 @@ spec:
474474
type: string
475475
gs_wal_path:
476476
type: string
477+
standby_host:
478+
type: string
479+
standby_port:
480+
type: string
477481
streams:
478482
type: array
479483
nullable: true

docs/reference/cluster_manifest.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -385,17 +385,25 @@ under the `clone` top-level key and do not affect the already running cluster.
385385
## Standby cluster
386386

387387
On startup, an existing `standby` top-level key creates a standby Postgres
388-
cluster streaming from a remote location. So far streaming from S3 and GCS WAL
389-
archives is supported.
388+
cluster streaming from a remote location. Either from a S3 or GCS WAL
389+
archive or a remote primary. When both of them are set, `standby_host`
390+
takes precedence.
390391

391392
* **s3_wal_path**
392393
the url to S3 bucket containing the WAL archive of the remote primary.
393-
Optional, but `s3_wal_path` or `gs_wal_path` is required.
394+
Required when the `standby` section is present even when `standby_host` is set.
394395

395396
* **gs_wal_path**
396397
the url to GS bucket containing the WAL archive of the remote primary.
397398
Optional, but `s3_wal_path` or `gs_wal_path` is required.
398399

400+
* **standby_host**
401+
hostname or IP address of the primary to stream from.
402+
When set, `s3_wal_path` is ignored.
403+
404+
* **standby_port**
405+
TCP port on which the primary is listening for connections.
406+
399407
## Volume properties
400408

401409
Those parameters are grouped under the `volume` top-level key and define the

manifests/postgresql.crd.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,10 @@ spec:
472472
type: string
473473
gs_wal_path:
474474
type: string
475+
standby_host:
476+
type: string
477+
standby_port:
478+
type: string
475479
streams:
476480
type: array
477481
nullable: true

manifests/standby-manifest.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ spec:
1010
numberOfInstances: 1
1111
postgresql:
1212
version: "14"
13-
# Make this a standby cluster and provide the s3 bucket path of source cluster for continuous streaming.
13+
# Make this a standby cluster and provide either the s3 bucket path of source cluster or the remote primary host for continuous streaming.
1414
standby:
1515
s3_wal_path: "s3://path/to/bucket/containing/wal/of/source/cluster/"
16+
# standby_host: ""
17+
# standby_port: ""

pkg/apis/acid.zalan.do/v1/crds.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,12 @@ var PostgresCRDResourceValidation = apiextv1.CustomResourceValidation{
714714
"gs_wal_path": {
715715
Type: "string",
716716
},
717+
"standby_host": {
718+
Type: "string",
719+
},
720+
"standby_port": {
721+
Type: "string",
722+
},
717723
},
718724
},
719725
"streams": {

pkg/apis/acid.zalan.do/v1/postgresql_type.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,10 +168,12 @@ type Patroni struct {
168168
SynchronousNodeCount uint32 `json:"synchronous_node_count,omitempty" defaults:1`
169169
}
170170

171-
// StandbyDescription contains s3 wal path
171+
// StandbyDescription contains remote primary config or s3 wal path
172172
type StandbyDescription struct {
173173
S3WalPath string `json:"s3_wal_path,omitempty"`
174174
GSWalPath string `json:"gs_wal_path,omitempty"`
175+
StandbyHost string `json:"standby_host,omitempty"`
176+
StandbyPort string `json:"standby_port,omitempty"`
175177
}
176178

177179
// TLSDescription specs TLS properties

pkg/cluster/k8sres.go

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1093,9 +1093,10 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
10931093
sort.Slice(customPodEnvVarsList,
10941094
func(i, j int) bool { return customPodEnvVarsList[i].Name < customPodEnvVarsList[j].Name })
10951095

1096-
if spec.StandbyCluster != nil && spec.StandbyCluster.S3WalPath == "" &&
1097-
spec.StandbyCluster.GSWalPath == "" {
1098-
return nil, fmt.Errorf("one of s3_wal_path or gs_wal_path must be set for standby cluster")
1096+
if spec.StandbyCluster != nil {
1097+
if spec.StandbyCluster.S3WalPath == "" && spec.StandbyCluster.GSWalPath == "" && spec.StandbyCluster.StandbyHost == "" {
1098+
return nil, fmt.Errorf("s3_wal_path, gs_wal_path and standby_host are empty for standby cluster")
1099+
}
10991100
}
11001101

11011102
// backward compatible check for InitContainers
@@ -1905,39 +1906,49 @@ func (c *Cluster) generateCloneEnvironment(description *acidv1.CloneDescription)
19051906
func (c *Cluster) generateStandbyEnvironment(description *acidv1.StandbyDescription) []v1.EnvVar {
19061907
result := make([]v1.EnvVar, 0)
19071908

1908-
if description.S3WalPath == "" && description.GSWalPath == "" {
1909-
return nil
1910-
}
1911-
1912-
if description.S3WalPath != "" {
1913-
// standby with S3, find out the bucket to setup standby
1914-
msg := "Standby from S3 bucket using custom parsed S3WalPath from the manifest %s "
1915-
c.logger.Infof(msg, description.S3WalPath)
1916-
1909+
if description.StandbyHost != "" {
1910+
// standby from remote primary
19171911
result = append(result, v1.EnvVar{
1918-
Name: "STANDBY_WALE_S3_PREFIX",
1919-
Value: description.S3WalPath,
1912+
Name: "STANDBY_HOST",
1913+
Value: description.StandbyHost,
19201914
})
1921-
} else if description.GSWalPath != "" {
1922-
msg := "Standby from GS bucket using custom parsed GSWalPath from the manifest %s "
1923-
c.logger.Infof(msg, description.GSWalPath)
1915+
if description.StandbyPort != "" {
1916+
result = append(result, v1.EnvVar{
1917+
Name: "STANDBY_PORT",
1918+
Value: description.StandbyPort,
1919+
})
1920+
}
1921+
} else {
1922+
if description.S3WalPath != "" {
1923+
// standby with S3, find out the bucket to setup standby
1924+
msg := "Standby from S3 bucket using custom parsed S3WalPath from the manifest %s "
1925+
c.logger.Infof(msg, description.S3WalPath)
19241926

1925-
envs := []v1.EnvVar{
1926-
{
1927-
Name: "STANDBY_WALE_GS_PREFIX",
1928-
Value: description.GSWalPath,
1929-
},
1930-
{
1931-
Name: "STANDBY_GOOGLE_APPLICATION_CREDENTIALS",
1932-
Value: c.OpConfig.GCPCredentials,
1933-
},
1927+
result = append(result, v1.EnvVar{
1928+
Name: "STANDBY_WALE_S3_PREFIX",
1929+
Value: description.S3WalPath,
1930+
})
1931+
} else if description.GSWalPath != "" {
1932+
msg := "Standby from GS bucket using custom parsed GSWalPath from the manifest %s "
1933+
c.logger.Infof(msg, description.GSWalPath)
1934+
1935+
envs := []v1.EnvVar{
1936+
{
1937+
Name: "STANDBY_WALE_GS_PREFIX",
1938+
Value: description.GSWalPath,
1939+
},
1940+
{
1941+
Name: "STANDBY_GOOGLE_APPLICATION_CREDENTIALS",
1942+
Value: c.OpConfig.GCPCredentials,
1943+
},
1944+
}
1945+
result = append(result, envs...)
19341946
}
1935-
result = append(result, envs...)
19361947

1937-
}
1948+
result = append(result, v1.EnvVar{Name: "STANDBY_METHOD", Value: "STANDBY_WITH_WALE"})
1949+
result = append(result, v1.EnvVar{Name: "STANDBY_WAL_BUCKET_SCOPE_PREFIX", Value: ""})
19381950

1939-
result = append(result, v1.EnvVar{Name: "STANDBY_METHOD", Value: "STANDBY_WITH_WALE"})
1940-
result = append(result, v1.EnvVar{Name: "STANDBY_WAL_BUCKET_SCOPE_PREFIX", Value: ""})
1951+
}
19411952

19421953
return result
19431954
}

pkg/cluster/k8sres_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,83 @@ func TestCloneEnv(t *testing.T) {
529529
}
530530
}
531531

532+
func TestStandbyEnv(t *testing.T) {
533+
testName := "TestStandbyEnv"
534+
tests := []struct {
535+
subTest string
536+
standbyOpts *acidv1.StandbyDescription
537+
env v1.EnvVar
538+
envPos int
539+
}{
540+
{
541+
subTest: "from custom s3 path",
542+
standbyOpts: &acidv1.StandbyDescription{
543+
S3WalPath: "s3://some/path/",
544+
},
545+
env: v1.EnvVar{
546+
Name: "STANDBY_WALE_S3_PREFIX",
547+
Value: "s3://some/path/",
548+
},
549+
envPos: 0,
550+
},
551+
{
552+
subTest: "from custom gs path",
553+
standbyOpts: &acidv1.StandbyDescription{
554+
GSWalPath: "gs://some/path/",
555+
},
556+
env: v1.EnvVar{
557+
Name: "STANDBY_WALE_GS_PREFIX",
558+
Value: "gs://some/path/",
559+
},
560+
envPos: 0,
561+
},
562+
{
563+
subTest: "from remote primary",
564+
standbyOpts: &acidv1.StandbyDescription{
565+
S3WalPath: "s3://some/path/",
566+
StandbyHost: "remote-primary",
567+
},
568+
env: v1.EnvVar{
569+
Name: "STANDBY_HOST",
570+
Value: "remote-primary",
571+
},
572+
envPos: 0,
573+
},
574+
{
575+
subTest: "from remote primary with port",
576+
standbyOpts: &acidv1.StandbyDescription{
577+
S3WalPath: "s3://some/path/",
578+
StandbyHost: "remote-primary",
579+
StandbyPort: "9876",
580+
},
581+
env: v1.EnvVar{
582+
Name: "STANDBY_PORT",
583+
Value: "9876",
584+
},
585+
envPos: 1,
586+
},
587+
}
588+
589+
var cluster = New(
590+
Config{}, k8sutil.KubernetesClient{}, acidv1.Postgresql{}, logger, eventRecorder)
591+
592+
for _, tt := range tests {
593+
envs := cluster.generateStandbyEnvironment(tt.standbyOpts)
594+
595+
env := envs[tt.envPos]
596+
597+
if env.Name != tt.env.Name {
598+
t.Errorf("%s %s: Expected env name %s, have %s instead",
599+
testName, tt.subTest, tt.env.Name, env.Name)
600+
}
601+
602+
if env.Value != tt.env.Value {
603+
t.Errorf("%s %s: Expected env value %s, have %s instead",
604+
testName, tt.subTest, tt.env.Value, env.Value)
605+
}
606+
}
607+
}
608+
532609
func TestExtractPgVersionFromBinPath(t *testing.T) {
533610
testName := "TestExtractPgVersionFromBinPath"
534611
tests := []struct {

0 commit comments

Comments
 (0)