diff --git a/e2e/patroni_global_params_test.go b/e2e/patroni_global_params_test.go new file mode 100644 index 00000000..9f5fb17f --- /dev/null +++ b/e2e/patroni_global_params_test.go @@ -0,0 +1,101 @@ +//go:build e2e_test + +package e2e + +import ( + "testing" + + "github.com/jackc/pgx/v5" + api "github.com/pgEdge/control-plane/api/apiv1/gen/control_plane" + "github.com/stretchr/testify/require" +) + +func TestUpdatePatroniGlobalParams(t *testing.T) { + t.Parallel() + + // I'm intentionally not adding a helper function for this check because + // this condition will be very temporary. + if fixture.orchestrator != "systemd" { + t.Skip("patroni global parameter update is currently unsupported in non-systemd clusters") + } + + host1 := fixture.HostIDs()[0] + + username := "admin" + password := "password" + + tLog(t, "creating database") + + ctx := t.Context() + db := fixture.NewDatabaseFixture(ctx, t, &api.CreateDatabaseRequest{ + Spec: &api.DatabaseSpec{ + DatabaseName: "test_global_params", + DatabaseUsers: []*api.DatabaseUserSpec{ + { + Username: username, + Password: pointerTo(password), + DbOwner: pointerTo(true), + Attributes: []string{"LOGIN", "SUPERUSER"}, + }, + }, + Port: pointerTo(0), + PatroniPort: pointerTo(0), + Nodes: []*api.DatabaseNodeSpec{ + { + Name: "n1", + HostIds: []api.Identifier{api.Identifier(host1)}, + }, + }, + }, + }) + + tLog(t, "querying original max connections") + + opts := ConnectionOptions{ + Username: username, + Password: password, + } + var originalMaxConnections int + db.WithConnection(ctx, opts, t, func(conn *pgx.Conn) { + require.NoError(t, conn.QueryRow(ctx, `SELECT setting::integer FROM pg_settings WHERE name = 'max_connections';`).Scan(&originalMaxConnections)) + }) + + tLogf(t, "got max_connections=%d", originalMaxConnections) + tLog(t, "updating database to change max connections") + + err := db.Update(ctx, UpdateOptions{ + Spec: &api.DatabaseSpec{ + DatabaseName: "test_global_params", + DatabaseUsers: []*api.DatabaseUserSpec{ + { + Username: username, + DbOwner: pointerTo(true), + Attributes: []string{"LOGIN", "SUPERUSER"}, + }, + }, + Port: pointerTo(0), + PatroniPort: pointerTo(0), + PostgresqlConf: map[string]any{ + "max_connections": originalMaxConnections + 1, + }, + Nodes: []*api.DatabaseNodeSpec{ + { + Name: "n1", + HostIds: []api.Identifier{api.Identifier(host1)}, + }, + }, + }, + }) + require.NoError(t, err) + + tLog(t, "querying new max connections") + + var newMaxConnections int + db.WithConnection(ctx, opts, t, func(conn *pgx.Conn) { + require.NoError(t, conn.QueryRow(ctx, `SELECT setting::integer FROM pg_settings WHERE name = 'max_connections';`).Scan(&newMaxConnections)) + }) + + tLogf(t, "got max_connections=%d", newMaxConnections) + + require.Equal(t, originalMaxConnections+1, newMaxConnections) +} diff --git a/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/enable_fast_basebackup.yaml b/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/enable_fast_basebackup.yaml index 4845166c..dc77d1bb 100644 --- a/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/enable_fast_basebackup.yaml +++ b/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/enable_fast_basebackup.yaml @@ -13,6 +13,7 @@ bootstrap: loop_wait: 10 ttl: 30 retry_timeout: 10 + failsafe_mode: false postgresql: parameters: max_connections: 901 diff --git a/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/in-place_restore.yaml b/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/in-place_restore.yaml index b27294c4..e22be2cc 100644 --- a/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/in-place_restore.yaml +++ b/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/in-place_restore.yaml @@ -13,6 +13,7 @@ bootstrap: loop_wait: 10 ttl: 30 retry_timeout: 10 + failsafe_mode: false postgresql: parameters: max_connections: 901 diff --git a/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/minimal_swarm.yaml b/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/minimal_swarm.yaml index 9e17bd3a..5cbd7337 100644 --- a/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/minimal_swarm.yaml +++ b/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/minimal_swarm.yaml @@ -13,6 +13,7 @@ bootstrap: loop_wait: 10 ttl: 30 retry_timeout: 10 + failsafe_mode: true postgresql: parameters: max_connections: 901 diff --git a/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/minimal_systemd.yaml b/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/minimal_systemd.yaml index 36c7dd75..43e5cd91 100644 --- a/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/minimal_systemd.yaml +++ b/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/minimal_systemd.yaml @@ -13,6 +13,7 @@ bootstrap: loop_wait: 10 ttl: 30 retry_timeout: 10 + failsafe_mode: false postgresql: parameters: max_connections: 901 diff --git a/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/user_pg_hba_pg_ident_and_scram.yaml b/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/user_pg_hba_pg_ident_and_scram.yaml index 76ea7dd4..caf8c437 100644 --- a/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/user_pg_hba_pg_ident_and_scram.yaml +++ b/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/user_pg_hba_pg_ident_and_scram.yaml @@ -13,6 +13,7 @@ bootstrap: loop_wait: 10 ttl: 30 retry_timeout: 10 + failsafe_mode: false postgresql: parameters: max_connections: 901 diff --git a/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/with_backup_config.yaml b/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/with_backup_config.yaml index 5e0a728e..c344407e 100644 --- a/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/with_backup_config.yaml +++ b/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/with_backup_config.yaml @@ -13,6 +13,7 @@ bootstrap: loop_wait: 10 ttl: 30 retry_timeout: 10 + failsafe_mode: false postgresql: parameters: max_connections: 901 diff --git a/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/with_restore_config.yaml b/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/with_restore_config.yaml index aa4b4937..0e7d5b22 100644 --- a/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/with_restore_config.yaml +++ b/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/with_restore_config.yaml @@ -13,6 +13,7 @@ bootstrap: loop_wait: 10 ttl: 30 retry_timeout: 10 + failsafe_mode: false postgresql: parameters: max_connections: 901 diff --git a/server/internal/orchestrator/common/patroni_config.go b/server/internal/orchestrator/common/patroni_config.go index dc267bcb..9af2451b 100644 --- a/server/internal/orchestrator/common/patroni_config.go +++ b/server/internal/orchestrator/common/patroni_config.go @@ -5,8 +5,10 @@ import ( "errors" "fmt" "path/filepath" + "time" "github.com/goccy/go-yaml" + "github.com/rs/zerolog" "github.com/samber/do" "github.com/spf13/afero" clientv3 "go.etcd.io/etcd/client/v3" @@ -74,11 +76,58 @@ func (c *PatroniConfig) Create( systemAddresses []string, extraHbaEntries []hba.Entry, ) error { - fs, err := do.Invoke[afero.Fs](rc.Injector) + _, err := c.create(ctx, rc, systemAddresses, extraHbaEntries) + return err +} + +func (c *PatroniConfig) Update( + ctx context.Context, + rc *resource.Context, + systemAddresses []string, + extraHbaEntries []hba.Entry, + reload func(ctx context.Context, rc *resource.Context, wait time.Duration) error, +) error { + logger, err := do.Invoke[zerolog.Logger](rc.Injector) if err != nil { return err } - etcdClient, err := do.Invoke[*clientv3.Client](rc.Injector) + + cfg, err := c.create(ctx, rc, systemAddresses, extraHbaEntries) + if err != nil { + return err + } + + wait := patroni.DefaultLoopWaitSeconds * time.Second + if client := c.client(rc); client != nil { + w, isPrimary := c.getStatusInfo(ctx, client) + if w > 0 { + wait = w + } + if isPrimary && cfg.Bootstrap != nil && cfg.Bootstrap.DCS != nil { + _, err := client.PatchDynamicConfig(ctx, cfg.Bootstrap.DCS.ToDynamicConfig()) + if err != nil { + logger.Warn(). + Str("database_id", c.Generator.DatabaseID). + Str("instance_id", c.InstanceID). + Err(err). + Msg("failed to patch dynamic config") + } + } + } + + // We intentionally leave the reload implementation up to the caller rather + // than use the Patroni API's reload method. A process signal-based reload + // has the advantage that it will work regardless of whether the Patroni API + // is healthy. + if err := reload(ctx, rc, wait); err != nil { + return fmt.Errorf("failed to trigger patroni reload: %w", err) + } + + return nil +} + +func (c *PatroniConfig) Delete(ctx context.Context, rc *resource.Context) error { + fs, err := do.Invoke[afero.Fs](rc.Injector) if err != nil { return err } @@ -88,62 +137,71 @@ func (c *PatroniConfig) Create( return fmt.Errorf("failed to get parent full path: %w", err) } + err = fs.Remove(filepath.Join(parentFullPath, "patroni.yaml")) + if errors.Is(err, afero.ErrFileNotFound) { + return nil + } else if err != nil { + return fmt.Errorf("failed to remove patroni.yaml: %w", err) + } + + return nil +} + +func (c *PatroniConfig) create( + ctx context.Context, + rc *resource.Context, + systemAddresses []string, + extraHbaEntries []hba.Entry, +) (*patroni.Config, error) { + fs, err := do.Invoke[afero.Fs](rc.Injector) + if err != nil { + return nil, err + } + etcdClient, err := do.Invoke[*clientv3.Client](rc.Injector) + if err != nil { + return nil, err + } + + parentFullPath, err := filesystem.DirResourceFullPath(rc, c.ParentID) + if err != nil { + return nil, fmt.Errorf("failed to get parent full path: %w", err) + } + etcdCreds, err := resource.FromContext[*EtcdCreds](rc, EtcdCredsIdentifier(c.InstanceID)) if err != nil { - return fmt.Errorf("failed to get etcd creds from state: %w", err) + return nil, fmt.Errorf("failed to get etcd creds from state: %w", err) } etcdHosts, err := patroni.EtcdHosts(ctx, etcdClient) if err != nil { - return fmt.Errorf("failed to get etcd hosts: %w", err) + return nil, fmt.Errorf("failed to get etcd hosts: %w", err) } enableFastBasebackup, err := c.isNewNode(rc) if err != nil { - return err + return nil, err } - config := c.Generator.Generate(etcdHosts, etcdCreds, GenerateOptions{ + cfg := c.Generator.Generate(etcdHosts, etcdCreds, GenerateOptions{ EnableFastBasebackup: enableFastBasebackup, SystemAddresses: systemAddresses, ExtraHbaEntries: extraHbaEntries, }) - content, err := yaml.Marshal(config) + content, err := yaml.Marshal(cfg) if err != nil { - return fmt.Errorf("failed to marshal patroni config: %w", err) + return nil, fmt.Errorf("failed to marshal patroni config: %w", err) } configPath := filepath.Join(parentFullPath, "patroni.yaml") if err := afero.WriteFile(fs, configPath, content, 0o600); err != nil { - return fmt.Errorf("failed to write %s: %w", configPath, err) + return nil, fmt.Errorf("failed to write %s: %w", configPath, err) } if err := fs.Chown(configPath, c.OwnerUID, c.OwnerGID); err != nil { - return fmt.Errorf("failed to change ownership for %s: %w", configPath, err) - } - - return nil -} - -func (c *PatroniConfig) Delete(ctx context.Context, rc *resource.Context) error { - fs, err := do.Invoke[afero.Fs](rc.Injector) - if err != nil { - return err - } - - parentFullPath, err := filesystem.DirResourceFullPath(rc, c.ParentID) - if err != nil { - return fmt.Errorf("failed to get parent full path: %w", err) - } - - err = fs.Remove(filepath.Join(parentFullPath, "patroni.yaml")) - if errors.Is(err, afero.ErrFileNotFound) { - return nil - } else if err != nil { - return fmt.Errorf("failed to remove patroni.yaml: %w", err) + return nil, fmt.Errorf("failed to change ownership for %s: %w", configPath, err) } - return nil + return cfg, nil } func (c *PatroniConfig) isNewNode(rc *resource.Context) (bool, error) { @@ -157,3 +215,50 @@ func (c *PatroniConfig) isNewNode(rc *resource.Context) (bool, error) { return false, nil } } + +func (c *PatroniConfig) client(rc *resource.Context) *patroni.Client { + // We're not using FromContext here to handle the case where the instance + // creation failed, but Patroni is still running. + data, ok := rc.State.Get(database.InstanceResourceIdentifier(c.InstanceID)) + if !ok { + return nil + } + instance, err := resource.ToResource[*database.InstanceResource](data) + if err == nil && instance.ConnectionInfo != nil { + return patroni.NewClient(instance.ConnectionInfo.PatroniURL(), nil) + } + + return nil +} + +func (c *PatroniConfig) getStatusInfo(ctx context.Context, client *patroni.Client) (time.Duration, bool) { + cfg, err := client.GetDynamicConfig(ctx) + if err != nil { + return 0, false + } + + var loopWait time.Duration + if cfg.LoopWait == nil { + loopWait = patroni.DefaultLoopWaitSeconds * time.Second + } else { + loopWait = time.Duration(*cfg.LoopWait) * time.Second + } + + wait := loopWait + status, err := client.GetInstanceStatus(ctx) + if err != nil { + return wait, false + } + if status.DCSLastSeen != nil { + lastSeen := time.Unix(*status.DCSLastSeen, 0) + lowerBound := time.Now().Add(-2 * loopWait) + upperBound := time.Now() + // Ignore last seen if clocks are very off in either direction + if lastSeen.After(lowerBound) && lastSeen.Before(upperBound) { + // Compute the time until the next run cycle + wait = time.Until(lastSeen.Add(loopWait)) + } + } + + return wait, status.IsPrimary() +} diff --git a/server/internal/orchestrator/common/patroni_config_generator.go b/server/internal/orchestrator/common/patroni_config_generator.go index 07913346..3f7dd182 100644 --- a/server/internal/orchestrator/common/patroni_config_generator.go +++ b/server/internal/orchestrator/common/patroni_config_generator.go @@ -48,6 +48,8 @@ type PatroniConfigGenerator struct { // this would be '1'. This is used to configure the Snowflake and LOLOR // extensions. NodeOrdinal int `json:"node_ordinal"` + // NodeSize is the number of instances in this Spock node. + NodeSize int `json:"node_size"` // OrchestratorParameters are additional parameters to be provided by the // orchestrator implementation. OrchestratorParameters map[string]any `json:"orchestrator_parameters,omitempty"` @@ -138,6 +140,7 @@ func NewPatroniConfigGenerator(opts PatroniConfigGeneratorOptions) *PatroniConfi MemoryBytes: memoryBytes, NodeName: opts.Instance.NodeName, NodeOrdinal: opts.Instance.NodeOrdinal, + NodeSize: opts.Instance.NodeSize, OrchestratorParameters: opts.OrchestratorParameters, PatroniPort: opts.PatroniPort, PostgresCertsDir: opts.Paths.Instance.PostgresCertificates(), @@ -230,6 +233,7 @@ func (p *PatroniConfigGenerator) bootstrap(dcsParameters map[string]any) *patron TTL: utils.PointerTo(30), LoopWait: utils.PointerTo(int(patroni.DefaultLoopWaitSeconds)), RetryTimeout: utils.PointerTo(10), + FailsafeMode: utils.PointerTo(p.NodeSize == 1), }, InitDB: utils.PointerTo([]string{"data-checksums"}), } diff --git a/server/internal/orchestrator/common/patroni_config_generator_test.go b/server/internal/orchestrator/common/patroni_config_generator_test.go index 2a99737f..7ac448ba 100644 --- a/server/internal/orchestrator/common/patroni_config_generator_test.go +++ b/server/internal/orchestrator/common/patroni_config_generator_test.go @@ -52,6 +52,7 @@ func TestPatroniConfigGenerator(t *testing.T) { DatabaseName: "app", NodeName: "n1", NodeOrdinal: 1, + NodeSize: 1, PgEdgeVersion: ds.MustParsePgEdgeVersion("18.1", "5.0.4"), ClusterSize: 3, }, @@ -109,6 +110,7 @@ func TestPatroniConfigGenerator(t *testing.T) { DatabaseName: "app", NodeName: "n1", NodeOrdinal: 1, + NodeSize: 2, PgEdgeVersion: ds.MustParsePgEdgeVersion("18.1", "5.0.4"), ClusterSize: 3, BackupConfig: &database.BackupConfig{}, @@ -167,6 +169,7 @@ func TestPatroniConfigGenerator(t *testing.T) { DatabaseName: "app", NodeName: "n1", NodeOrdinal: 1, + NodeSize: 2, PgEdgeVersion: ds.MustParsePgEdgeVersion("18.1", "5.0.4"), ClusterSize: 3, RestoreConfig: &database.RestoreConfig{}, @@ -225,6 +228,7 @@ func TestPatroniConfigGenerator(t *testing.T) { DatabaseName: "app", NodeName: "n1", NodeOrdinal: 1, + NodeSize: 2, PgEdgeVersion: ds.MustParsePgEdgeVersion("18.1", "5.0.4"), ClusterSize: 3, RestoreConfig: &database.RestoreConfig{}, @@ -284,6 +288,7 @@ func TestPatroniConfigGenerator(t *testing.T) { DatabaseName: "app", NodeName: "n1", NodeOrdinal: 1, + NodeSize: 2, PgEdgeVersion: ds.MustParsePgEdgeVersion("18.1", "5.0.4"), ClusterSize: 3, }, @@ -325,6 +330,7 @@ func TestPatroniConfigGenerator(t *testing.T) { DatabaseName: "app", NodeName: "n1", NodeOrdinal: 1, + NodeSize: 2, PgEdgeVersion: ds.MustParsePgEdgeVersion("18.1", "5.0.4"), ClusterSize: 3, }, diff --git a/server/internal/orchestrator/systemd/patroni_config.go b/server/internal/orchestrator/systemd/patroni_config.go index 0ac8a1b8..6f4d3ddd 100644 --- a/server/internal/orchestrator/systemd/patroni_config.go +++ b/server/internal/orchestrator/systemd/patroni_config.go @@ -10,7 +10,6 @@ import ( "github.com/pgEdge/control-plane/server/internal/ds" "github.com/pgEdge/control-plane/server/internal/host" "github.com/pgEdge/control-plane/server/internal/orchestrator/common" - "github.com/pgEdge/control-plane/server/internal/patroni" "github.com/pgEdge/control-plane/server/internal/resource" "github.com/pgEdge/control-plane/server/internal/utils" "github.com/samber/do" @@ -62,16 +61,37 @@ func (c *PatroniConfig) Refresh(ctx context.Context, rc *resource.Context) error } func (c *PatroniConfig) Create(ctx context.Context, rc *resource.Context) error { - hostSvc, err := do.Invoke[*host.Service](rc.Injector) + addresses, err := c.getAddresses(ctx, rc) + if err != nil { + return err + } + + return c.Base.Create(ctx, rc, addresses, nil) +} + +func (c *PatroniConfig) Update(ctx context.Context, rc *resource.Context) error { + addresses, err := c.getAddresses(ctx, rc) if err != nil { return err } + return c.Base.Update(ctx, rc, addresses, nil, c.signalReload) +} + +func (c *PatroniConfig) Delete(ctx context.Context, rc *resource.Context) error { + return c.Base.Delete(ctx, rc) +} + +func (c *PatroniConfig) getAddresses(ctx context.Context, rc *resource.Context) ([]string, error) { + hostSvc, err := do.Invoke[*host.Service](rc.Injector) + if err != nil { + return nil, err + } hosts, err := hostSvc.GetHosts(ctx, c.AllHostIDs) if err != nil { - return fmt.Errorf("failed to get hosts: %w", err) + return nil, fmt.Errorf("failed to get hosts: %w", err) } if len(hosts) != len(c.AllHostIDs) { - return fmt.Errorf("wrong number of hosts: expected %d, got %d", len(c.AllHostIDs), len(hosts)) + return nil, fmt.Errorf("wrong number of hosts: expected %d, got %d", len(c.AllHostIDs), len(hosts)) } addresses := ds.NewSet[string]() @@ -79,22 +99,10 @@ func (c *PatroniConfig) Create(ctx context.Context, rc *resource.Context) error addresses.Add(h.PeerAddresses...) } - return c.Base.Create(ctx, rc, addresses.ToSortedSlice(strings.Compare), nil) -} - -func (c *PatroniConfig) Update(ctx context.Context, rc *resource.Context) error { - if err := c.Create(ctx, rc); err != nil { - return err - } - - return c.signalReload(ctx, rc) + return addresses.ToSortedSlice(strings.Compare), nil } -func (c *PatroniConfig) Delete(ctx context.Context, rc *resource.Context) error { - return c.Base.Delete(ctx, rc) -} - -func (c *PatroniConfig) signalReload(ctx context.Context, rc *resource.Context) error { +func (c *PatroniConfig) signalReload(ctx context.Context, rc *resource.Context, wait time.Duration) error { client, err := do.Invoke[*Client](rc.Injector) if err != nil { return err @@ -110,8 +118,6 @@ func (c *PatroniConfig) signalReload(ctx context.Context, rc *resource.Context) if err := client.ReloadUnit(ctx, name); err != nil { return fmt.Errorf("failed to reload patroni: %w", err) } - // It can take up to loop_wait seconds for Patroni to reload the config. - // We'll want to update this code to read loop_wait from c.Base if we make - // loop_wait configurable. - return utils.SleepContext(ctx, patroni.DefaultLoopWaitSeconds*time.Second) + + return utils.SleepContext(ctx, wait) } diff --git a/server/internal/patroni/client.go b/server/internal/patroni/client.go index f8ad26b8..97504499 100644 --- a/server/internal/patroni/client.go +++ b/server/internal/patroni/client.go @@ -302,14 +302,14 @@ type DynamicStandbyClusterConfig struct { // An ordered list of methods that can be used to bootstrap standby leader // from the remote primary, can be different from the list defined in // PostgreSQL - CreateReplicaMethods *string `json:"create_replica_methods,omitempty"` + CreateReplicaMethods *[]string `json:"create_replica_methods,omitempty"` // Command to restore WAL records from the remote primary to nodes in a // standby cluster, can be different from the list defined in PostgreSQL RestoreCommand *string `json:"restore_command,omitempty"` // Cleanup command for standby leader ArchiveCleanupCommand *string `json:"archive_cleanup_command,omitempty"` // How long to wait before actually apply WAL records on a standby leader - RecoveryMinApplyDelay *string `json:"recovery_min_apply_delay,omitempty"` + RecoveryMinApplyDelay *int `json:"recovery_min_apply_delay,omitempty"` } type DynamicConfig struct { @@ -375,7 +375,7 @@ type DynamicConfig struct { // matching properties will cause a slot to be ignored. IgnoreSlots *[]IgnoreSlot `json:"ignore_slots,omitempty"` // Stops Patroni from making changes to the cluster. - Pause bool `json:"pause"` + Pause *bool `json:"pause,omitempty"` } type Switchover struct { diff --git a/server/internal/patroni/config.go b/server/internal/patroni/config.go index 0a63bde3..0c5c1d3e 100644 --- a/server/internal/patroni/config.go +++ b/server/internal/patroni/config.go @@ -70,6 +70,19 @@ type DCSPostgreSQL struct { PgIdent *[]string `json:"pg_ident,omitempty"` } +func (d *DCSPostgreSQL) ToDynamicConfig() *DynamicPostgreSQLConfig { + if d == nil { + return nil + } + return &DynamicPostgreSQLConfig{ + UsePgRewind: d.UsePgRewind, + UseSlots: d.UseSlots, + Parameters: d.Parameters, + PgHba: d.PgHba, + PgIdent: d.PgIdent, + } +} + type DCSStandbyCluster struct { Host *string `json:"host,omitempty"` Port *int `json:"port,omitempty"` @@ -80,6 +93,21 @@ type DCSStandbyCluster struct { RecoveryMinApplyDelay *int `json:"recovery_min_apply_delay,omitempty"` } +func (d *DCSStandbyCluster) ToDynamicConfig() *DynamicStandbyClusterConfig { + if d == nil { + return nil + } + return &DynamicStandbyClusterConfig{ + Host: d.Host, + Port: d.Port, + PrimarySlotName: d.PrimarySlotName, + CreateReplicaMethods: d.CreateReplicaMethods, + RestoreCommand: d.RestoreCommand, + ArchiveCleanupCommand: d.ArchiveCleanupCommand, + RecoveryMinApplyDelay: d.RecoveryMinApplyDelay, + } +} + type SlotType string const ( @@ -111,15 +139,40 @@ type DCS struct { PrimaryStopTimeout *int `json:"primary_stop_timeout,omitempty"` SynchronousMode *string `json:"synchronous_mode,omitempty"` SynchronousModeStrict *bool `json:"synchronous_mode_strict,omitempty"` - SynchronousModeCount *int `json:"synchronous_mode_count,omitempty"` + SynchronousNodeCount *int `json:"synchronous_node_count,omitempty"` FailsafeMode *bool `json:"failsafe_mode,omitempty"` Postgresql *DCSPostgreSQL `json:"postgresql,omitempty"` StandbyCluster *DCSStandbyCluster `json:"standby_cluster,omitempty"` - MemberSlotsTtl *string `json:"member_slots_ttl,omitempty"` + MemberSlotsTtl *int `json:"member_slots_ttl,omitempty"` Slots *map[string]Slot `json:"slots,omitempty"` IgnoreSlots *[]IgnoreSlot `json:"ignore_slots,omitempty"` } +func (d *DCS) ToDynamicConfig() *DynamicConfig { + if d == nil { + return nil + } + return &DynamicConfig{ + LoopWait: d.LoopWait, + TTL: d.TTL, + RetryTimeout: d.RetryTimeout, + MaximumLagOnFailover: d.MaximumLagOnFailover, + MaximumLagOnSyncNode: d.MaximumLagOnSyncNode, + MaxTimelinesHistory: d.MaxTimelinesHistory, + PrimaryStartTimeout: d.PrimaryStartTimeout, + PrimaryStopTimeout: d.PrimaryStopTimeout, + SynchronousMode: (*SynchronousMode)(d.SynchronousMode), + SynchronousModeStrict: d.SynchronousModeStrict, + SynchronousNodeCount: d.SynchronousNodeCount, + FailsafeMode: d.FailsafeMode, + MemberSlotsTtl: d.MemberSlotsTtl, + Slots: d.Slots, + IgnoreSlots: d.IgnoreSlots, + PostgreSQL: d.Postgresql.ToDynamicConfig(), + StandbyCluster: d.StandbyCluster.ToDynamicConfig(), + } +} + type BootstrapMethod string const (