Skip to content

feat: add orchestrator/common package#293

Open
jason-lynch wants to merge 1 commit intofeat/PLAT-417/postgres-database-resourcefrom
feat/PLAT-417/orchestrator-common-resources
Open

feat: add orchestrator/common package#293
jason-lynch wants to merge 1 commit intofeat/PLAT-417/postgres-database-resourcefrom
feat/PLAT-417/orchestrator-common-resources

Conversation

@jason-lynch
Copy link
Member

Summary

Adds a package with common resources that can be shared between more than one orchestrator implementation. This is conceptually distinct from the common resources in the database package, which are shared across all current and future orchestrator implementations.

Testing

There are no user-facing functionality changes in this PR.

PLAT-417

@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: edbb3ce2-a27f-41bb-a3a2-97a5ecccc121

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This pull request introduces a comprehensive orchestration framework for managing Patroni/PostgreSQL clusters with etcd integration. It adds resource types for credentials, certificates, and configuration management, implements Patroni config generation with multiple deployment strategies, and registers these resources globally via a new common module.

Changes

Cohort / File(s) Summary
Registry & Command Setup
server/cmd/root.go, server/internal/database/orchestrator.go
Registers new common orchestrator resource types via common.RegisterResourceTypes() during initialization, and adds ExecuteInstanceCommand method to the Orchestrator interface for executing arbitrary instance commands.
Etcd Credentials Resource
server/internal/orchestrator/common/etcd_creds.go
Introduces EtcdCreds resource type with lifecycle methods to manage etcd user credentials, client certificates, and CA certificates on disk with proper ownership and permissions.
PostgreSQL Certificates Resource
server/internal/orchestrator/common/postgres_certs.go
Adds PostgresCerts resource for managing PostgreSQL server and role certificates (superuser, replication) with certificate generation via injected service and cleanup on deletion.
Backup & Restore Configuration Resources
server/internal/orchestrator/common/pgbackrest_config.go, server/internal/orchestrator/common/pgbackrest_stanza.go
Implements PgBackRestConfig and PgBackRestStanza resources for managing pgBackRest configuration files and stanza initialization/validation with backup/restore type variants.
Patroni Cluster & Member Resources
server/internal/orchestrator/common/patroni_cluster.go, server/internal/orchestrator/common/patroni_member.go
Defines PatroniCluster and PatroniMember resources with etcd-backed lifecycle methods for cluster namespace and member registration/cleanup in the distributed configuration store.
Patroni Configuration Generation
server/internal/orchestrator/common/patroni_config_generator.go, server/internal/orchestrator/common/patroni_config.go
Provides PatroniConfigGenerator for building comprehensive Patroni configs with support for fast basebackup, restore workflows, and custom PostgreSQL parameters; PatroniConfig resource manages config file lifecycle.
Patroni Configuration Golden Tests
server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/*.yaml, server/internal/orchestrator/common/patroni_config_generator_test.go, server/internal/orchestrator/common/main_test.go
Adds extensive golden-file test suite covering minimal swarm/systemd configurations, backup/restore workflows, fast basebackup, and in-place restore scenarios with golden YAML outputs for verification.
Service Configuration Resources
server/internal/orchestrator/common/pg_service_conf.go
Introduces PgServiceConf resource for generating and managing pg\_service.conf file with DSN entries for all cluster nodes.
Path & Command Utilities
server/internal/orchestrator/common/paths.go, server/internal/orchestrator/common/paths_test.go
Adds InstancePaths and Paths types for managing directory structure and generating pgBackRest/data movement commands with intelligent target-action injection for restore workflows.
Resource Registration
server/internal/orchestrator/common/resources.go
Central registration function that wires all common resource types (EtcdCreds, PostgresCerts, PgBackRestConfig/Stanza, PatroniCluster/Member, PgServiceConf) into the resource framework.
Etcd Host Discovery
server/internal/patroni/utils.go
Adds EtcdHosts utility function to extract client host addresses from etcd cluster member list with URL parsing and error wrapping.
PgBackRest Port Configuration
server/internal/pgbackrest/config.go
Extends ConfigOptions with Port field and writes pg1\-port to pgBackRest config when Port is specified.
PostgreSQL GUC & Service Utilities
server/internal/postgres/gucs.go, server/internal/postgres/pg_service_conf.go
Simplifies SnowflakeLolorGUCs signature (removes error return), adds PgServiceConf type for INI-style service configuration generation with sorted section output.
Swarm Orchestrator Implementation
server/internal/orchestrator/swarm/orchestrator.go, server/internal/orchestrator/swarm/patroni_config.go
Implements ExecuteInstanceCommand wrapper for container execution; updates patroni config to use non-error SnowflakeLolorGUCs signature.

Poem

🐰 Hop, hop, resources hop!
Etcd creds and certs won't stop,
Patroni configs bloom so fine,
With TLS, backups, all align!
From swarm to systemd we dance with glee,
Building clusters wild and free! 🌿

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is incomplete. It lacks required sections including Changes, Testing (beyond a statement about no user-facing changes), and Checklist items. The description does not follow the provided template structure. Complete the PR description by adding missing sections: expand Changes with a bulleted list of key additions, provide explicit testing instructions (e.g., 'go test ./...'), and mark all applicable Checklist items.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add orchestrator/common package' is clear, concise, and directly summarizes the main change: adding a new package for shared orchestrator resources.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/PLAT-417/orchestrator-common-resources
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@jason-lynch jason-lynch force-pushed the feat/PLAT-417/postgres-database-resource branch from 298c58f to 887b1a8 Compare March 9, 2026 22:37
@jason-lynch jason-lynch force-pushed the feat/PLAT-417/orchestrator-common-resources branch 2 times, most recently from 9bfda29 to 3bd613f Compare March 10, 2026 13:10
@jason-lynch jason-lynch force-pushed the feat/PLAT-417/postgres-database-resource branch from 887b1a8 to daeb238 Compare March 10, 2026 13:10
@jason-lynch jason-lynch force-pushed the feat/PLAT-417/orchestrator-common-resources branch 2 times, most recently from 2000707 to 321ebff Compare March 10, 2026 14:33
@jason-lynch jason-lynch force-pushed the feat/PLAT-417/postgres-database-resource branch from 20b19ea to b768409 Compare March 10, 2026 14:33
@jason-lynch jason-lynch force-pushed the feat/PLAT-417/orchestrator-common-resources branch 3 times, most recently from 7213a0b to fbd7177 Compare March 10, 2026 21:01
@jason-lynch jason-lynch force-pushed the feat/PLAT-417/postgres-database-resource branch from e47f053 to 26aa05d Compare March 11, 2026 12:58
@jason-lynch jason-lynch force-pushed the feat/PLAT-417/orchestrator-common-resources branch from fbd7177 to 818b6cd Compare March 11, 2026 12:58
@jason-lynch jason-lynch force-pushed the feat/PLAT-417/postgres-database-resource branch from 26aa05d to a856583 Compare March 11, 2026 13:25
@jason-lynch jason-lynch force-pushed the feat/PLAT-417/orchestrator-common-resources branch from 818b6cd to db06d7c Compare March 11, 2026 13:25
@jason-lynch jason-lynch force-pushed the feat/PLAT-417/postgres-database-resource branch from a856583 to 57a6663 Compare March 11, 2026 13:43
@jason-lynch jason-lynch force-pushed the feat/PLAT-417/orchestrator-common-resources branch from db06d7c to 94c2dbe Compare March 11, 2026 13:43
@jason-lynch jason-lynch force-pushed the feat/PLAT-417/postgres-database-resource branch from 57a6663 to e656f49 Compare March 11, 2026 13:49
@jason-lynch jason-lynch force-pushed the feat/PLAT-417/orchestrator-common-resources branch from 94c2dbe to 1de4ace Compare March 11, 2026 13:49
@jason-lynch jason-lynch force-pushed the feat/PLAT-417/postgres-database-resource branch from e656f49 to 4e0f475 Compare March 12, 2026 19:28
@jason-lynch jason-lynch force-pushed the feat/PLAT-417/orchestrator-common-resources branch from 1de4ace to 187c000 Compare March 12, 2026 19:28
@jason-lynch jason-lynch force-pushed the feat/PLAT-417/postgres-database-resource branch from 4e0f475 to 3b3cfbb Compare March 12, 2026 21:17
@jason-lynch jason-lynch force-pushed the feat/PLAT-417/orchestrator-common-resources branch from 187c000 to a6f44f6 Compare March 12, 2026 21:17
@jason-lynch
Copy link
Member Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Mar 12, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (6)
server/internal/orchestrator/common/paths.go (1)

45-77: Well-designed auto-injection of --target-action.

The logic correctly identifies restore types (immediate, lsn, name, time, xid) that require --target-action and defaults to promote when not explicitly provided.

Minor nit: The comment at line 51 ("skip the next arg since it's the value") is slightly misleading since continue doesn't actually increment i. The code still works correctly because the value arg won't match any prefix checks and will hit continue at line 60. Consider updating the comment to clarify this:

 		if strings.HasPrefix(arg, "--target-action") {
 			hasTargetAction = true
-			continue // skip the next arg since it's the value of --target-action no further checks needed
+			continue // no further checks needed for this flag
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/internal/orchestrator/common/paths.go` around lines 45 - 77, The
comment inside InstancePaths.PgBackRestRestoreCmd's arg-parsing loop is
misleading: when detecting "--target-action" the code sets hasTargetAction and
uses continue but does not increment i, so the following value token is not
explicitly skipped by i++; update the comment to accurately state that continue
leaves i unchanged and that the next token will be seen but won't match any
"--type" checks (so it is effectively ignored), and similarly clarify the other
comment where i++ is used for "--type" to avoid confusion about which branch
advances the index.
server/internal/orchestrator/common/pg_service_conf.go (1)

22-25: Rename nodeName parameter to match actual identifier semantics.

This helper currently receives instance IDs in Identifier(). Renaming the parameter (e.g., id or instanceID) will reduce confusion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/internal/orchestrator/common/pg_service_conf.go` around lines 22 - 25,
Rename the PgServiceConfResourceIdentifier function parameter from nodeName to a
name that reflects it holds the instance identifier (e.g., id or instanceID) so
the signature reads PgServiceConfResourceIdentifier(instanceID string) and
update all references inside the function (resource.Identifier{ ID: instanceID,
Type: ResourceTypePgServiceConf }) and any callers to use the new parameter name
to avoid semantic confusion.
server/internal/patroni/utils.go (1)

16-27: Return a deterministic unique host list to avoid config churn.

Consider deduping and sorting before return, so callers get stable output across runs.

♻️ Proposed refactor
 import (
 	"context"
 	"fmt"
 	"net/url"
+	"sort"
 
 	clientv3 "go.etcd.io/etcd/client/v3"
 )
@@
-	var hosts []string
+	hostSet := map[string]struct{}{}
 	for _, member := range members.Members {
 		for _, endpoint := range member.GetClientURLs() {
 			u, err := url.Parse(endpoint)
 			if err != nil {
 				return nil, fmt.Errorf("failed to parse etcd client url '%s': %w", endpoint, err)
 			}
-			hosts = append(hosts, u.Host)
+			hostSet[u.Host] = struct{}{}
 		}
 	}
+	hosts := make([]string, 0, len(hostSet))
+	for host := range hostSet {
+		hosts = append(hosts, host)
+	}
+	sort.Strings(hosts)
 
 	return hosts, nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/internal/patroni/utils.go` around lines 16 - 27, The returned hosts
slice built from members.Members and member.GetClientURLs() should be
deduplicated and sorted to ensure deterministic output; update the code that
constructs the hosts slice (the loop over members.Members and
member.GetClientURLs()) to collect unique host strings (e.g., via a
map[string]struct{}), convert the map keys back to a slice, sort it (e.g.,
sort.Strings), and then return that sorted, deduped hosts slice so callers
receive stable output across runs.
server/internal/orchestrator/common/patroni_cluster.go (1)

3-12: Derive the Patroni prefix from DatabaseID/NodeName instead of storing it.

server/internal/patroni/config.go already defines ClusterPrefix(databaseID, nodeName) as the canonical DCS namespace. Keeping PatroniClusterPrefix as separate serialized state makes the delete path depend on a derived value that can drift from the struct’s real identity.

♻️ Suggested simplification
 import (
 	"context"
 	"fmt"
 
 	"github.com/samber/do"
 	clientv3 "go.etcd.io/etcd/client/v3"
 
+	"github.com/pgEdge/control-plane/server/internal/patroni"
 	"github.com/pgEdge/control-plane/server/internal/resource"
 	"github.com/pgEdge/control-plane/server/internal/storage"
 )
 
 type PatroniCluster struct {
 	DatabaseID string `json:"database_id"`
 	NodeName   string `json:"node_name"`
-	PatroniClusterPrefix string `json:"patroni_namespace"`
 }
 
 ...
 
-	_, err = storage.NewDeletePrefixOp(client, p.PatroniClusterPrefix).Exec(ctx)
+	_, err = storage.NewDeletePrefixOp(client, patroni.ClusterPrefix(p.DatabaseID, p.NodeName)).Exec(ctx)

Also applies to: 25-29, 67-76

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/internal/orchestrator/common/patroni_cluster.go` around lines 3 - 12,
The struct currently persists PatroniClusterPrefix separately causing drift;
instead compute the DCS namespace on the fly using the existing
ClusterPrefix(databaseID, nodeName) helper from
server/internal/patroni/config.go. Remove the PatroniClusterPrefix field usage
in the struct and replace all reads/writes (including delete paths) to call
ClusterPrefix(p.DatabaseID, p.NodeName) (or equivalent variables) where
PatroniClusterPrefix is referenced; update any constructors/serializers to stop
setting PatroniClusterPrefix and ensure code paths that previously relied on
PatroniClusterPrefix use the derived value so stored state no longer diverges
from the canonical identity.
server/internal/orchestrator/common/patroni_config_generator.go (2)

70-140: Note the TODO for PatroniAllowlist.

The TODO at line 137 indicates that PatroniAllowlist is not yet wired to the instance/database types. The field exists on PatroniConfigGenerator but will always be nil/empty until this is addressed.

Would you like me to open an issue to track adding the allowlist field to instance and database types?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/internal/orchestrator/common/patroni_config_generator.go` around lines
70 - 140, The PatroniAllowlist is currently not propagated from the
instance/database types into PatroniConfigGenerator, so add a PatroniAllowlist
field to the InstanceSpec (and/or Database type if global) and to
PatroniConfigGeneratorOptions, then wire it through in NewPatroniConfigGenerator
by setting PatroniAllowlist: opts.Instance.PatroniAllowlist (or
opts.<Database>.PatroniAllowlist) when constructing the PatroniConfigGenerator;
update any constructor signatures, serialization/parsing of instance/database
specs, and relevant tests to ensure the allowlist is persisted and populated
instead of remaining nil as the TODO indicates.

400-419: Consider the ordering of extraEntries relative to reject rules.

Extra HBA entries are appended after the reject rules for system users (lines 400-415). If any extraEntries need to allow connections for pgedge or patroni_replicator from additional addresses, they will be ineffective because the reject rules are evaluated first.

If this is intentional (extra entries should only apply to non-system users), consider documenting this constraint in the GenerateOptions.ExtraHbaEntries field comment.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/internal/orchestrator/common/patroni_config_generator.go` around lines
400 - 419, The current code appends the system reject hba.Entry rules before
appending GenerateOptions.ExtraHbaEntries (extraEntries), which makes any extra
entries ineffective for pgedge/patroni_replicator because rejects are evaluated
first; to fix, either move the loop that appends extraEntries to before the
block that appends the two reject hba.Entry objects so extraEntries can allow
connections for those users, or if the intended behavior is to keep rejects
taking precedence, document this in the GenerateOptions.ExtraHbaEntries comment
stating that extra entries are applied after system reject rules and cannot
override them; refer to the extraEntries variable, entries slice, the hba.Entry
reject entries, and GenerateOptions.ExtraHbaEntries when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/internal/orchestrator/common/paths_test.go`:
- Line 89: The test currently only verifies the pgbackrest binary path, so
update the assertion to compare the entire pgbackrest.Cmd value (including
args/target/action) instead of a single field: replace the one-field check with
an equality check between tc.expected.PgBackrestCmd and result.PgBackrestCmd
(e.g., use assert.Equal or require.Equal on the whole pgbackrest.Cmd) so the
test validates args and target-action as well.

In `@server/internal/orchestrator/common/paths.go`:
- Around line 79-157: Add a samber/do provider function for this package so the
injector can construct the Paths utility; implement a Provide() function (e.g.,
Provide) in the same package that returns *Paths (or a constructor like
NewPaths) and registers it with do.New or do.Provide so callers can inject Paths
via the samber/do container; ensure the function creates/returns &Paths{BaseDir:
...} or accepts configuration params if needed and export the constructor name
(Provide or NewPaths) to satisfy the package-level DI requirement referenced in
the review.

In `@server/internal/orchestrator/common/patroni_config_generator.go`:
- Around line 421-431: The MD5 host rule only adds an IPv4 catch-all
("0.0.0.0/0") so IPv6 clients are not covered; update the logic that appends the
non-system-user rule (the entries slice where you create hba.Entry with Type
hba.EntryTypeHost, Database "all", User "all", AuthMethod hba.AuthMethodMD5) to
also append a second hba.Entry with Address "::/0" (same
Database/User/AuthMethod) before returning &entries so both IPv4 and IPv6 are
allowed.

In `@server/internal/orchestrator/common/pg_service_conf.go`:
- Line 147: Update the wrapped error message to reference the correct filename:
replace the incorrect "patroni.yaml" text in the fmt.Errorf call with
"pg_service.conf" so the error returned when failing to remove the
pg_service.conf file accurately reflects the operation (i.e., change the message
in the fmt.Errorf that currently reads "failed to remove patroni.yaml: %w" to
"failed to remove pg_service.conf: %w").

---

Nitpick comments:
In `@server/internal/orchestrator/common/paths.go`:
- Around line 45-77: The comment inside InstancePaths.PgBackRestRestoreCmd's
arg-parsing loop is misleading: when detecting "--target-action" the code sets
hasTargetAction and uses continue but does not increment i, so the following
value token is not explicitly skipped by i++; update the comment to accurately
state that continue leaves i unchanged and that the next token will be seen but
won't match any "--type" checks (so it is effectively ignored), and similarly
clarify the other comment where i++ is used for "--type" to avoid confusion
about which branch advances the index.

In `@server/internal/orchestrator/common/patroni_cluster.go`:
- Around line 3-12: The struct currently persists PatroniClusterPrefix
separately causing drift; instead compute the DCS namespace on the fly using the
existing ClusterPrefix(databaseID, nodeName) helper from
server/internal/patroni/config.go. Remove the PatroniClusterPrefix field usage
in the struct and replace all reads/writes (including delete paths) to call
ClusterPrefix(p.DatabaseID, p.NodeName) (or equivalent variables) where
PatroniClusterPrefix is referenced; update any constructors/serializers to stop
setting PatroniClusterPrefix and ensure code paths that previously relied on
PatroniClusterPrefix use the derived value so stored state no longer diverges
from the canonical identity.

In `@server/internal/orchestrator/common/patroni_config_generator.go`:
- Around line 70-140: The PatroniAllowlist is currently not propagated from the
instance/database types into PatroniConfigGenerator, so add a PatroniAllowlist
field to the InstanceSpec (and/or Database type if global) and to
PatroniConfigGeneratorOptions, then wire it through in NewPatroniConfigGenerator
by setting PatroniAllowlist: opts.Instance.PatroniAllowlist (or
opts.<Database>.PatroniAllowlist) when constructing the PatroniConfigGenerator;
update any constructor signatures, serialization/parsing of instance/database
specs, and relevant tests to ensure the allowlist is persisted and populated
instead of remaining nil as the TODO indicates.
- Around line 400-419: The current code appends the system reject hba.Entry
rules before appending GenerateOptions.ExtraHbaEntries (extraEntries), which
makes any extra entries ineffective for pgedge/patroni_replicator because
rejects are evaluated first; to fix, either move the loop that appends
extraEntries to before the block that appends the two reject hba.Entry objects
so extraEntries can allow connections for those users, or if the intended
behavior is to keep rejects taking precedence, document this in the
GenerateOptions.ExtraHbaEntries comment stating that extra entries are applied
after system reject rules and cannot override them; refer to the extraEntries
variable, entries slice, the hba.Entry reject entries, and
GenerateOptions.ExtraHbaEntries when making the change.

In `@server/internal/orchestrator/common/pg_service_conf.go`:
- Around line 22-25: Rename the PgServiceConfResourceIdentifier function
parameter from nodeName to a name that reflects it holds the instance identifier
(e.g., id or instanceID) so the signature reads
PgServiceConfResourceIdentifier(instanceID string) and update all references
inside the function (resource.Identifier{ ID: instanceID, Type:
ResourceTypePgServiceConf }) and any callers to use the new parameter name to
avoid semantic confusion.

In `@server/internal/patroni/utils.go`:
- Around line 16-27: The returned hosts slice built from members.Members and
member.GetClientURLs() should be deduplicated and sorted to ensure deterministic
output; update the code that constructs the hosts slice (the loop over
members.Members and member.GetClientURLs()) to collect unique host strings
(e.g., via a map[string]struct{}), convert the map keys back to a slice, sort it
(e.g., sort.Strings), and then return that sorted, deduped hosts slice so
callers receive stable output across runs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 08e9173a-f36f-4419-abbc-fcb188374101

📥 Commits

Reviewing files that changed from the base of the PR and between 3b3cfbb and a6f44f6.

📒 Files selected for processing (28)
  • server/cmd/root.go
  • server/internal/database/orchestrator.go
  • server/internal/orchestrator/common/etcd_creds.go
  • server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/enable_fast_basebackup.yaml
  • server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/in-place_restore.yaml
  • server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/minimal_swarm.yaml
  • server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/minimal_systemd.yaml
  • server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/with_backup_config.yaml
  • server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/with_restore_config.yaml
  • server/internal/orchestrator/common/main_test.go
  • server/internal/orchestrator/common/paths.go
  • server/internal/orchestrator/common/paths_test.go
  • server/internal/orchestrator/common/patroni_cluster.go
  • server/internal/orchestrator/common/patroni_config.go
  • server/internal/orchestrator/common/patroni_config_generator.go
  • server/internal/orchestrator/common/patroni_config_generator_test.go
  • server/internal/orchestrator/common/patroni_member.go
  • server/internal/orchestrator/common/pg_service_conf.go
  • server/internal/orchestrator/common/pgbackrest_config.go
  • server/internal/orchestrator/common/pgbackrest_stanza.go
  • server/internal/orchestrator/common/postgres_certs.go
  • server/internal/orchestrator/common/resources.go
  • server/internal/orchestrator/swarm/orchestrator.go
  • server/internal/orchestrator/swarm/patroni_config.go
  • server/internal/patroni/utils.go
  • server/internal/pgbackrest/config.go
  • server/internal/postgres/gucs.go
  • server/internal/postgres/pg_service_conf.go

PatroniPath: "/usr/bin/patroni",
}
result := paths.PgBackRestRestoreCmd(tc.command, tc.args...)
assert.Equal(t, tc.expected.PgBackrestCmd, result.PgBackrestCmd)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Assert the full pgbackrest.Cmd, not just binary path.

This assertion only checks one field, so cases intended to validate args/target-action behavior can pass incorrectly.

✅ Proposed fix
-				assert.Equal(t, tc.expected.PgBackrestCmd, result.PgBackrestCmd)
+				assert.Equal(t, tc.expected, result.PgBackrestCmd)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/internal/orchestrator/common/paths_test.go` at line 89, The test
currently only verifies the pgbackrest binary path, so update the assertion to
compare the entire pgbackrest.Cmd value (including args/target/action) instead
of a single field: replace the one-field check with an equality check between
tc.expected.PgBackrestCmd and result.PgBackrestCmd (e.g., use assert.Equal or
require.Equal on the whole pgbackrest.Cmd) so the test validates args and
target-action as well.

Comment on lines +79 to +157
type Paths struct {
BaseDir string `json:"base_dir"`
}

func (p *Paths) Data() string {
return filepath.Join(p.BaseDir, "data")
}

func (p *Paths) Configs() string {
return filepath.Join(p.BaseDir, "configs")
}

func (p *Paths) Certificates() string {
return filepath.Join(p.BaseDir, "certificates")
}

func (p *Paths) PgData() string {
return filepath.Join(p.Data(), "pgdata")
}

func (p *Paths) PgDataRestore() string {
return filepath.Join(p.Data(), "pgdata-restore")
}

func (p *Paths) PatroniConfig() string {
return filepath.Join(p.Configs(), "patroni.yaml")
}

func (p *Paths) PgBackRestConfig(confType PgBackRestConfigType) string {
return filepath.Join(p.Configs(), fmt.Sprintf("pgbackrest.%s.conf", confType))
}

func (p *Paths) EtcdCertificates() string {
return filepath.Join(p.Certificates(), "etcd")
}

func (p *Paths) EtcdCaCert() string {
return filepath.Join(p.EtcdCertificates(), etcdCaCertName)
}

func (p *Paths) EtcdClientCert() string {
return filepath.Join(p.EtcdCertificates(), etcdClientCertName)
}

func (p *Paths) EtcdClientKey() string {
return filepath.Join(p.EtcdCertificates(), etcdClientKeyName)
}

func (p *Paths) PostgresCertificates() string {
return filepath.Join(p.Certificates(), "postgres")
}

func (p *Paths) PostgresCaCert() string {
return filepath.Join(p.PostgresCertificates(), postgresCaCertName)
}

func (p *Paths) PostgresServerCert() string {
return filepath.Join(p.PostgresCertificates(), postgresServerCertName)
}

func (p *Paths) PostgresServerKey() string {
return filepath.Join(p.PostgresCertificates(), postgresServerKeyName)
}

func (p *Paths) PostgresSuperuserCert() string {
return filepath.Join(p.PostgresCertificates(), postgresSuperuserCertName)
}

func (p *Paths) PostgresSuperuserKey() string {
return filepath.Join(p.PostgresCertificates(), postgresSuperuserKeyName)
}

func (p *Paths) PostgresReplicatorCert() string {
return filepath.Join(p.PostgresCertificates(), postgresReplicatorCertName)
}

func (p *Paths) PostgresReplicatorKey() string {
return filepath.Join(p.PostgresCertificates(), postgresReplicatorKeyName)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the certificate name constants are defined in the common package
rg -n 'etcdCaCertName|etcdClientCertName|etcdClientKeyName|postgresCaCertName|postgresServerCertName|postgresServerKeyName|postgresSuperuserCertName|postgresSuperuserKeyName|postgresReplicatorCertName|postgresReplicatorKeyName' server/internal/orchestrator/common/

Repository: pgEdge/control-plane

Length of output: 6508


🏁 Script executed:

#!/bin/bash
# Read the complete paths.go file to check for Provide() function and other guidelines compliance
cat -n server/internal/orchestrator/common/paths.go

Repository: pgEdge/control-plane

Length of output: 5354


Verify dependency injection setup for this utility module.

The certificate name constants are properly defined in the common package, and the path construction methods use filepath.Join correctly for cross-platform compatibility. However, the coding guidelines require each package under server/**/*.go to have a Provide() function for dependency injection using the samber/do injector. While this utility module may not require external dependencies, ensure this requirement is addressed either here or at the package level in server/internal/orchestrator/common/.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/internal/orchestrator/common/paths.go` around lines 79 - 157, Add a
samber/do provider function for this package so the injector can construct the
Paths utility; implement a Provide() function (e.g., Provide) in the same
package that returns *Paths (or a constructor like NewPaths) and registers it
with do.New or do.Provide so callers can inject Paths via the samber/do
container; ensure the function creates/returns &Paths{BaseDir: ...} or accepts
configuration params if needed and export the constructor name (Provide or
NewPaths) to satisfy the package-level DI requirement referenced in the review.

Comment on lines +421 to +431
// Use MD5 for non-system users from all other connections
// TODO: Can we safely upgrade this to scram-sha-256?
entries = append(entries, hba.Entry{
Type: hba.EntryTypeHost,
Database: "all",
User: "all",
Address: "0.0.0.0/0",
AuthMethod: hba.AuthMethodMD5,
}.String())

return &entries
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing IPv6 catch-all for non-system user authentication.

The MD5 authentication rule only covers IPv4 (0.0.0.0/0) but not IPv6 (::0/0). Non-system users connecting via IPv6 would not match any rule and could be rejected. The reject rules for system users correctly include both address families.

🐛 Proposed fix to add IPv6 support
 	// Use MD5 for non-system users from all other connections
 	// TODO: Can we safely upgrade this to scram-sha-256?
 	entries = append(entries, hba.Entry{
 		Type:       hba.EntryTypeHost,
 		Database:   "all",
 		User:       "all",
 		Address:    "0.0.0.0/0",
 		AuthMethod: hba.AuthMethodMD5,
-	}.String())
+	}.String(),
+	hba.Entry{
+		Type:       hba.EntryTypeHost,
+		Database:   "all",
+		User:       "all",
+		Address:    "::0/0",
+		AuthMethod: hba.AuthMethodMD5,
+	}.String())
 
 	return &entries
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Use MD5 for non-system users from all other connections
// TODO: Can we safely upgrade this to scram-sha-256?
entries = append(entries, hba.Entry{
Type: hba.EntryTypeHost,
Database: "all",
User: "all",
Address: "0.0.0.0/0",
AuthMethod: hba.AuthMethodMD5,
}.String())
return &entries
// Use MD5 for non-system users from all other connections
// TODO: Can we safely upgrade this to scram-sha-256?
entries = append(entries, hba.Entry{
Type: hba.EntryTypeHost,
Database: "all",
User: "all",
Address: "0.0.0.0/0",
AuthMethod: hba.AuthMethodMD5,
}.String(),
hba.Entry{
Type: hba.EntryTypeHost,
Database: "all",
User: "all",
Address: "::0/0",
AuthMethod: hba.AuthMethodMD5,
}.String())
return &entries
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/internal/orchestrator/common/patroni_config_generator.go` around lines
421 - 431, The MD5 host rule only adds an IPv4 catch-all ("0.0.0.0/0") so IPv6
clients are not covered; update the logic that appends the non-system-user rule
(the entries slice where you create hba.Entry with Type hba.EntryTypeHost,
Database "all", User "all", AuthMethod hba.AuthMethodMD5) to also append a
second hba.Entry with Address "::/0" (same Database/User/AuthMethod) before
returning &entries so both IPv4 and IPv6 are allowed.

if errors.Is(err, afero.ErrFileNotFound) {
return nil
} else if err != nil {
return fmt.Errorf("failed to remove patroni.yaml: %w", err)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix stale filename in delete error message.

The delete path is pg_service.conf, but the wrapped error references patroni.yaml.

🛠️ Proposed fix
-		return fmt.Errorf("failed to remove patroni.yaml: %w", err)
+		return fmt.Errorf("failed to remove pg_service.conf: %w", err)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return fmt.Errorf("failed to remove patroni.yaml: %w", err)
return fmt.Errorf("failed to remove pg_service.conf: %w", err)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/internal/orchestrator/common/pg_service_conf.go` at line 147, Update
the wrapped error message to reference the correct filename: replace the
incorrect "patroni.yaml" text in the fmt.Errorf call with "pg_service.conf" so
the error returned when failing to remove the pg_service.conf file accurately
reflects the operation (i.e., change the message in the fmt.Errorf that
currently reads "failed to remove patroni.yaml: %w" to "failed to remove
pg_service.conf: %w").

Adds a package with common resources that can be shared between more
than one orchestrator implementation. This is conceptually distinct from
the common resources in the `database` package, which are shared across
all current and future orchestrator implementations.

PLAT-417
@jason-lynch jason-lynch force-pushed the feat/PLAT-417/postgres-database-resource branch from 3b3cfbb to 42e1c4c Compare March 13, 2026 13:12
@jason-lynch jason-lynch force-pushed the feat/PLAT-417/orchestrator-common-resources branch from a6f44f6 to 2f99484 Compare March 13, 2026 13:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant