From 00022bfae6d4643fd1c421a8f61b11375cf0788b Mon Sep 17 00:00:00 2001 From: Jason Lynch Date: Tue, 16 Jun 2026 16:26:49 -0400 Subject: [PATCH 01/11] build(release): bump version to v0.9.0 --- CHANGELOG.md | 23 +++++++++++++++++++ api/apiv1/gen/control_plane/service.go | 2 +- api/apiv1/gen/http/openapi.json | 2 +- api/apiv1/gen/http/openapi.yaml | 2 +- api/apiv1/gen/http/openapi3.json | 2 +- api/apiv1/gen/http/openapi3.yaml | 2 +- api/version.txt | 2 +- changes/unreleased/Added-20260504-150235.yaml | 3 --- changes/unreleased/Added-20260511-090000.yaml | 3 --- changes/unreleased/Added-20260511-090010.yaml | 3 --- changes/unreleased/Added-20260511-090020.yaml | 3 --- changes/unreleased/Added-20260511-100000.yaml | 3 --- changes/unreleased/Added-20260610-000000.yaml | 3 --- changes/unreleased/Added-20260611-090000.yaml | 3 --- changes/unreleased/Added-20260611-090010.yaml | 3 --- .../unreleased/Changed-20260504-152348.yaml | 3 --- changes/unreleased/Fixed-20260611-090000.yaml | 3 --- changes/unreleased/Fixed-20260611-090010.yaml | 3 --- changes/unreleased/Fixed-20260616-150152.yaml | 3 --- changes/v0.9.0.md | 22 ++++++++++++++++++ docs/api/openapi.md | 8 +++---- docs/api/reference.md | 2 +- docs/changelog.md | 23 +++++++++++++++++++ docs/development/e2e-tests.md | 2 +- docs/installation/installation.md | 10 ++++---- docs/installation/mtls.md | 8 +++---- docs/installation/systemd.md | 2 +- docs/installation/upgrading.md | 4 ++-- 28 files changed, 92 insertions(+), 60 deletions(-) delete mode 100644 changes/unreleased/Added-20260504-150235.yaml delete mode 100644 changes/unreleased/Added-20260511-090000.yaml delete mode 100644 changes/unreleased/Added-20260511-090010.yaml delete mode 100644 changes/unreleased/Added-20260511-090020.yaml delete mode 100644 changes/unreleased/Added-20260511-100000.yaml delete mode 100644 changes/unreleased/Added-20260610-000000.yaml delete mode 100644 changes/unreleased/Added-20260611-090000.yaml delete mode 100644 changes/unreleased/Added-20260611-090010.yaml delete mode 100644 changes/unreleased/Changed-20260504-152348.yaml delete mode 100644 changes/unreleased/Fixed-20260611-090000.yaml delete mode 100644 changes/unreleased/Fixed-20260611-090010.yaml delete mode 100644 changes/unreleased/Fixed-20260616-150152.yaml create mode 100644 changes/v0.9.0.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 4247be08..b32c62f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## v0.9.0 - 2026-06-16 + +### Added + +- Added a feature to enable manual Postgres minor version updates in systemd clusters. The Control Plane will now update its copy of the database spec when it detects changes to an instance's Postgres or Spock version. +- Added post-update and pre-remove scriptlets to our RPM and deb packages to automatically restart the Control Plane service during upgrades and to stop and disable the service during uninstallation. +- Added support for Debian-based distributions to the systemd orchestrator. +- Added deb packages to our releases for Debian-based distributions. +- Added the ability to configure a separate override for the database owner GID to complement the existing UID override. +- Added support for Spock 5.0.9 on Postgres 16.14, 17.10, and 18.4. +- Added `pg_hba_conf` and `pg_ident_conf` fields to the database spec — Operators can now supply custom `pg_hba.conf` and `pg_ident.conf` entries at the database or per-node level, giving full control over client authentication rules and ident mappings. Per-node entries take first-match priority over database-level entries. +- Added Knowledge Base search support to the MCP service — Enable the `search_knowledgebase` tool via the `kb_enabled` option in the MCP service configuration, with support for OpenAI and Voyage AI embedding providers. The Knowledge Base file is loaded from a configurable host path and mounted read-only into the container. + +### Changed + +- Changed the instance monitoring system to query the Postgres and Spock versions for replica instances and report them in the databases API. + +### Fixed + +- Fixed global Patroni parameters not being applied to running database clusters. Dynamic configuration changes are now patched directly via the Patroni API on the primary instance, ensuring parameters that cannot be changed via config file reload take effect immediately. +- Fixed valid instance IDs being incorrectly rejected by the API when the database ID was at its maximum allowed length — Instance IDs are an internal aggregate of the database ID, node name, and host hash, and are no longer subject to the same length limit as user-supplied identifiers. +- Database specs with duplicate allocated ports on a single host are now rejected by the API. + ## v0.8.1 - 2026-05-19 ### Added diff --git a/api/apiv1/gen/control_plane/service.go b/api/apiv1/gen/control_plane/service.go index 9d82a879..54e78b37 100644 --- a/api/apiv1/gen/control_plane/service.go +++ b/api/apiv1/gen/control_plane/service.go @@ -81,7 +81,7 @@ type Service interface { const APIName = "control-plane" // APIVersion is the version of the API as defined in the design. -const APIVersion = "v0.8.1" +const APIVersion = "v0.9.0" // ServiceName is the name of the service as defined in the design. This is the // same value that is set in the endpoint request contexts under the ServiceKey diff --git a/api/apiv1/gen/http/openapi.json b/api/apiv1/gen/http/openapi.json index dbdb3ca6..6ad3ca98 100644 --- a/api/apiv1/gen/http/openapi.json +++ b/api/apiv1/gen/http/openapi.json @@ -3,7 +3,7 @@ "info": { "title": "pgEdge Control Plane v1 API", "description": "A distributed application that provides a declarative API to deploy and manage Postgres databases.", - "version": "v0.8.1" + "version": "v0.9.0" }, "host": "localhost:3000", "consumes": [ diff --git a/api/apiv1/gen/http/openapi.yaml b/api/apiv1/gen/http/openapi.yaml index 11b9e288..1b86fa7b 100644 --- a/api/apiv1/gen/http/openapi.yaml +++ b/api/apiv1/gen/http/openapi.yaml @@ -2,7 +2,7 @@ swagger: "2.0" info: title: pgEdge Control Plane v1 API description: A distributed application that provides a declarative API to deploy and manage Postgres databases. - version: v0.8.1 + version: v0.9.0 host: localhost:3000 consumes: - application/json diff --git a/api/apiv1/gen/http/openapi3.json b/api/apiv1/gen/http/openapi3.json index 871b1691..cd629941 100644 --- a/api/apiv1/gen/http/openapi3.json +++ b/api/apiv1/gen/http/openapi3.json @@ -3,7 +3,7 @@ "info": { "title": "pgEdge Control Plane v1 API", "description": "A distributed application that provides a declarative API to deploy and manage Postgres databases.", - "version": "v0.8.1" + "version": "v0.9.0" }, "servers": [ { diff --git a/api/apiv1/gen/http/openapi3.yaml b/api/apiv1/gen/http/openapi3.yaml index 4873fb0b..98efeaf1 100644 --- a/api/apiv1/gen/http/openapi3.yaml +++ b/api/apiv1/gen/http/openapi3.yaml @@ -2,7 +2,7 @@ openapi: 3.0.3 info: title: pgEdge Control Plane v1 API description: A distributed application that provides a declarative API to deploy and manage Postgres databases. - version: v0.8.1 + version: v0.9.0 servers: - url: http://localhost:3000 paths: diff --git a/api/version.txt b/api/version.txt index 349ca0e1..7965b36d 100644 --- a/api/version.txt +++ b/api/version.txt @@ -1 +1 @@ -v0.8.1 \ No newline at end of file +v0.9.0 \ No newline at end of file diff --git a/changes/unreleased/Added-20260504-150235.yaml b/changes/unreleased/Added-20260504-150235.yaml deleted file mode 100644 index 05fa0862..00000000 --- a/changes/unreleased/Added-20260504-150235.yaml +++ /dev/null @@ -1,3 +0,0 @@ -kind: Added -body: Added a feature to enable manual Postgres minor version updates in systemd clusters. The Control Plane will now update its copy of the database spec when it detects changes to an instance's Postgres or Spock version. -time: 2026-05-04T15:02:35.045407-04:00 diff --git a/changes/unreleased/Added-20260511-090000.yaml b/changes/unreleased/Added-20260511-090000.yaml deleted file mode 100644 index 35d14e98..00000000 --- a/changes/unreleased/Added-20260511-090000.yaml +++ /dev/null @@ -1,3 +0,0 @@ -kind: Added -body: Added support for Debian-based distributions to the systemd orchestrator. -time: 2026-05-11T09:28:06.065156-04:00 diff --git a/changes/unreleased/Added-20260511-090010.yaml b/changes/unreleased/Added-20260511-090010.yaml deleted file mode 100644 index 4704965b..00000000 --- a/changes/unreleased/Added-20260511-090010.yaml +++ /dev/null @@ -1,3 +0,0 @@ -kind: Added -body: Added deb packages to our releases for Debian-based distributions. -time: 2026-05-11T10:07:07.636452-04:00 diff --git a/changes/unreleased/Added-20260511-090020.yaml b/changes/unreleased/Added-20260511-090020.yaml deleted file mode 100644 index 37b27bd9..00000000 --- a/changes/unreleased/Added-20260511-090020.yaml +++ /dev/null @@ -1,3 +0,0 @@ -kind: Added -body: Added post-update and pre-remove scriptlets to our RPM and deb packages to automatically restart the Control Plane service during upgrades and to stop and disable the service during uninstallation. -time: 2026-05-11T09:26:22.435191-04:00 diff --git a/changes/unreleased/Added-20260511-100000.yaml b/changes/unreleased/Added-20260511-100000.yaml deleted file mode 100644 index 08238066..00000000 --- a/changes/unreleased/Added-20260511-100000.yaml +++ /dev/null @@ -1,3 +0,0 @@ -kind: Added -body: Added the ability to configure a separate override for the database owner GID to complement the existing UID override. -time: 2026-05-11T10:09:04.640943-04:00 diff --git a/changes/unreleased/Added-20260610-000000.yaml b/changes/unreleased/Added-20260610-000000.yaml deleted file mode 100644 index 18d7b5a2..00000000 --- a/changes/unreleased/Added-20260610-000000.yaml +++ /dev/null @@ -1,3 +0,0 @@ -kind: Added -body: Added support for Spock 5.0.9 on Postgres 16.14, 17.10, and 18.4. -time: 2026-06-10T00:00:00.000000-00:00 diff --git a/changes/unreleased/Added-20260611-090000.yaml b/changes/unreleased/Added-20260611-090000.yaml deleted file mode 100644 index d4e93ba2..00000000 --- a/changes/unreleased/Added-20260611-090000.yaml +++ /dev/null @@ -1,3 +0,0 @@ -kind: Added -body: Added `pg_hba_conf` and `pg_ident_conf` fields to the database spec — Operators can now supply custom `pg_hba.conf` and `pg_ident.conf` entries at the database or per-node level, giving full control over client authentication rules and ident mappings. Per-node entries take first-match priority over database-level entries. -time: 2026-06-11T09:00:00.000000-04:00 diff --git a/changes/unreleased/Added-20260611-090010.yaml b/changes/unreleased/Added-20260611-090010.yaml deleted file mode 100644 index 8a84e641..00000000 --- a/changes/unreleased/Added-20260611-090010.yaml +++ /dev/null @@ -1,3 +0,0 @@ -kind: Added -body: Added Knowledge Base search support to the MCP service — Enable the `search_knowledgebase` tool via the `kb_enabled` option in the MCP service configuration, with support for OpenAI and Voyage AI embedding providers. The Knowledge Base file is loaded from a configurable host path and mounted read-only into the container. -time: 2026-06-11T09:00:10.000000-04:00 diff --git a/changes/unreleased/Changed-20260504-152348.yaml b/changes/unreleased/Changed-20260504-152348.yaml deleted file mode 100644 index fa9dc8bb..00000000 --- a/changes/unreleased/Changed-20260504-152348.yaml +++ /dev/null @@ -1,3 +0,0 @@ -kind: Changed -body: Changed the instance monitoring system to query the Postgres and Spock versions for replica instances and report them in the databases API. -time: 2026-05-04T15:23:48.324604-04:00 diff --git a/changes/unreleased/Fixed-20260611-090000.yaml b/changes/unreleased/Fixed-20260611-090000.yaml deleted file mode 100644 index e823c70e..00000000 --- a/changes/unreleased/Fixed-20260611-090000.yaml +++ /dev/null @@ -1,3 +0,0 @@ -kind: Fixed -body: Fixed global Patroni parameters not being applied to running database clusters. Dynamic configuration changes are now patched directly via the Patroni API on the primary instance, ensuring parameters that cannot be changed via config file reload take effect immediately. -time: 2026-06-11T09:00:00.000000-04:00 diff --git a/changes/unreleased/Fixed-20260611-090010.yaml b/changes/unreleased/Fixed-20260611-090010.yaml deleted file mode 100644 index bd21c7af..00000000 --- a/changes/unreleased/Fixed-20260611-090010.yaml +++ /dev/null @@ -1,3 +0,0 @@ -kind: Fixed -body: Fixed valid instance IDs being incorrectly rejected by the API when the database ID was at its maximum allowed length — Instance IDs are an internal aggregate of the database ID, node name, and host hash, and are no longer subject to the same length limit as user-supplied identifiers. -time: 2026-06-11T09:00:10.000000-04:00 diff --git a/changes/unreleased/Fixed-20260616-150152.yaml b/changes/unreleased/Fixed-20260616-150152.yaml deleted file mode 100644 index 23098c1e..00000000 --- a/changes/unreleased/Fixed-20260616-150152.yaml +++ /dev/null @@ -1,3 +0,0 @@ -kind: Fixed -body: Database specs with duplicate allocated ports on a single host are now rejected by the API. -time: 2026-06-16T15:01:52.449574-04:00 diff --git a/changes/v0.9.0.md b/changes/v0.9.0.md new file mode 100644 index 00000000..47b405d0 --- /dev/null +++ b/changes/v0.9.0.md @@ -0,0 +1,22 @@ +## v0.9.0 - 2026-06-16 + +### Added + +- Added a feature to enable manual Postgres minor version updates in systemd clusters. The Control Plane will now update its copy of the database spec when it detects changes to an instance's Postgres or Spock version. +- Added post-update and pre-remove scriptlets to our RPM and deb packages to automatically restart the Control Plane service during upgrades and to stop and disable the service during uninstallation. +- Added support for Debian-based distributions to the systemd orchestrator. +- Added deb packages to our releases for Debian-based distributions. +- Added the ability to configure a separate override for the database owner GID to complement the existing UID override. +- Added support for Spock 5.0.9 on Postgres 16.14, 17.10, and 18.4. +- Added `pg_hba_conf` and `pg_ident_conf` fields to the database spec — Operators can now supply custom `pg_hba.conf` and `pg_ident.conf` entries at the database or per-node level, giving full control over client authentication rules and ident mappings. Per-node entries take first-match priority over database-level entries. +- Added Knowledge Base search support to the MCP service — Enable the `search_knowledgebase` tool via the `kb_enabled` option in the MCP service configuration, with support for OpenAI and Voyage AI embedding providers. The Knowledge Base file is loaded from a configurable host path and mounted read-only into the container. + +### Changed + +- Changed the instance monitoring system to query the Postgres and Spock versions for replica instances and report them in the databases API. + +### Fixed + +- Fixed global Patroni parameters not being applied to running database clusters. Dynamic configuration changes are now patched directly via the Patroni API on the primary instance, ensuring parameters that cannot be changed via config file reload take effect immediately. +- Fixed valid instance IDs being incorrectly rejected by the API when the database ID was at its maximum allowed length — Instance IDs are an internal aggregate of the database ID, node name, and host hash, and are no longer subject to the same length limit as user-supplied identifiers. +- Database specs with duplicate allocated ports on a single host are now rejected by the API. diff --git a/docs/api/openapi.md b/docs/api/openapi.md index a3733b23..d2d53f54 100644 --- a/docs/api/openapi.md +++ b/docs/api/openapi.md @@ -7,7 +7,7 @@ The Control Plane server serves a JSON OpenAPI v3 specification from the You can also access offline copies of the OpenAPI specification in the pgEdge Control Plane repository. We generate a few versions of the specification to accommodate different tools and use cases: -- [OpenAPI v3 YAML](https://github.com/pgEdge/control-plane/blob/v0.8.1/api/apiv1/gen/http/openapi3.yaml) -- [OpenAPI v3 JSON](https://github.com/pgEdge/control-plane/blob/v0.8.1/api/apiv1/gen/http/openapi3.json) -- [OpenAPI v2 YAML](https://github.com/pgEdge/control-plane/blob/v0.8.1/api/apiv1/gen/http/openapi.yaml) -- [OpenAPI v2 JSON](https://github.com/pgEdge/control-plane/blob/v0.8.1/api/apiv1/gen/http/openapi.json) \ No newline at end of file +- [OpenAPI v3 YAML](https://github.com/pgEdge/control-plane/blob/v0.9.0/api/apiv1/gen/http/openapi3.yaml) +- [OpenAPI v3 JSON](https://github.com/pgEdge/control-plane/blob/v0.9.0/api/apiv1/gen/http/openapi3.json) +- [OpenAPI v2 YAML](https://github.com/pgEdge/control-plane/blob/v0.9.0/api/apiv1/gen/http/openapi.yaml) +- [OpenAPI v2 JSON](https://github.com/pgEdge/control-plane/blob/v0.9.0/api/apiv1/gen/http/openapi.json) \ No newline at end of file diff --git a/docs/api/reference.md b/docs/api/reference.md index 434100e1..38508d1c 100644 --- a/docs/api/reference.md +++ b/docs/api/reference.md @@ -34,4 +34,4 @@ hide: - \ No newline at end of file + \ No newline at end of file diff --git a/docs/changelog.md b/docs/changelog.md index 4247be08..b32c62f5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,28 @@ # Changelog +## v0.9.0 - 2026-06-16 + +### Added + +- Added a feature to enable manual Postgres minor version updates in systemd clusters. The Control Plane will now update its copy of the database spec when it detects changes to an instance's Postgres or Spock version. +- Added post-update and pre-remove scriptlets to our RPM and deb packages to automatically restart the Control Plane service during upgrades and to stop and disable the service during uninstallation. +- Added support for Debian-based distributions to the systemd orchestrator. +- Added deb packages to our releases for Debian-based distributions. +- Added the ability to configure a separate override for the database owner GID to complement the existing UID override. +- Added support for Spock 5.0.9 on Postgres 16.14, 17.10, and 18.4. +- Added `pg_hba_conf` and `pg_ident_conf` fields to the database spec — Operators can now supply custom `pg_hba.conf` and `pg_ident.conf` entries at the database or per-node level, giving full control over client authentication rules and ident mappings. Per-node entries take first-match priority over database-level entries. +- Added Knowledge Base search support to the MCP service — Enable the `search_knowledgebase` tool via the `kb_enabled` option in the MCP service configuration, with support for OpenAI and Voyage AI embedding providers. The Knowledge Base file is loaded from a configurable host path and mounted read-only into the container. + +### Changed + +- Changed the instance monitoring system to query the Postgres and Spock versions for replica instances and report them in the databases API. + +### Fixed + +- Fixed global Patroni parameters not being applied to running database clusters. Dynamic configuration changes are now patched directly via the Patroni API on the primary instance, ensuring parameters that cannot be changed via config file reload take effect immediately. +- Fixed valid instance IDs being incorrectly rejected by the API when the database ID was at its maximum allowed length — Instance IDs are an internal aggregate of the database ID, node name, and host hash, and are no longer subject to the same length limit as user-supplied identifiers. +- Database specs with duplicate allocated ports on a single host are now rejected by the API. + ## v0.8.1 - 2026-05-19 ### Added diff --git a/docs/development/e2e-tests.md b/docs/development/e2e-tests.md index ce0175a7..2f745cff 100644 --- a/docs/development/e2e-tests.md +++ b/docs/development/e2e-tests.md @@ -395,7 +395,7 @@ specifying `FIXTURE_CONTROL_PLANE_IMAGE` with the `{deploy,update,reset}-{lima,ec2}-fixture` targets. For example: ```sh -make update-lima-fixture FIXTURE_CONTROL_PLANE_IMAGE='ghcr.io/pgedge/control-plane:v0.8.1-rc.1' +make update-lima-fixture FIXTURE_CONTROL_PLANE_IMAGE='ghcr.io/pgedge/control-plane:v0.9.0-rc.1' ``` ### Fixture variants diff --git a/docs/installation/installation.md b/docs/installation/installation.md index 44dfe922..c4a1ed64 100644 --- a/docs/installation/installation.md +++ b/docs/installation/installation.md @@ -98,7 +98,7 @@ Given that output, the following stack definition file will deploy a single Cont ```yaml services: host-1: - image: ghcr.io/pgedge/control-plane:v0.8.1 + image: ghcr.io/pgedge/control-plane:v0.9.0 command: run environment: - PGEDGE_HOST_ID=host-1 @@ -113,7 +113,7 @@ services: constraints: - node.id==vzou89zyd4n3xz6p6jvoohqxx host-2: - image: ghcr.io/pgedge/control-plane:v0.8.1 + image: ghcr.io/pgedge/control-plane:v0.9.0 command: run environment: - PGEDGE_HOST_ID=host-2 @@ -128,7 +128,7 @@ services: constraints: - node.id==5sa7m11ub62t1n22feuhg0mbp host-3: - image: ghcr.io/pgedge/control-plane:v0.8.1 + image: ghcr.io/pgedge/control-plane:v0.9.0 command: run environment: - PGEDGE_HOST_ID=host-3 @@ -191,7 +191,7 @@ For example: ```yaml host-4: - image: ghcr.io/pgedge/control-plane:v0.8.1 + image: ghcr.io/pgedge/control-plane:v0.9.0 command: run environment: - PGEDGE_HOST_ID=host-4 @@ -227,7 +227,7 @@ Paste the output below and click "Generate Stack." This generator is fully local - + ``` yaml {#global-output} # Once submitted, the generated stack will appear here. diff --git a/docs/installation/mtls.md b/docs/installation/mtls.md index 2882abf5..b4eecc70 100644 --- a/docs/installation/mtls.md +++ b/docs/installation/mtls.md @@ -35,7 +35,7 @@ For example, if you've placed the certificates in a `/opt/pgedge/control-plane` ```yaml services: host-1: - image: ghcr.io/pgedge/control-plane:v0.8.1 + image: ghcr.io/pgedge/control-plane:v0.9.0 command: run environment: - PGEDGE_HOST_ID=host-1 @@ -195,7 +195,7 @@ Now that our certificates are available on each machine, we can update our stack ```yaml services: host-1: - image: ghcr.io/pgedge/control-plane:v0.8.1 + image: ghcr.io/pgedge/control-plane:v0.9.0 command: run environment: - PGEDGE_HOST_ID=host-1 @@ -219,7 +219,7 @@ services: constraints: - node.id==81kw1zwmh9y8hk4rd7igylry0 host-2: - image: ghcr.io/pgedge/control-plane:v0.8.1 + image: ghcr.io/pgedge/control-plane:v0.9.0 command: run environment: - PGEDGE_HOST_ID=host-2 @@ -240,7 +240,7 @@ services: constraints: - node.id==xz7069ytbdq7uzd2tvrj2wlf2 host-3: - image: ghcr.io/pgedge/control-plane:v0.8.1 + image: ghcr.io/pgedge/control-plane:v0.9.0 command: run environment: - PGEDGE_HOST_ID=host-3 diff --git a/docs/installation/systemd.md b/docs/installation/systemd.md index e3349c59..b348754e 100644 --- a/docs/installation/systemd.md +++ b/docs/installation/systemd.md @@ -130,7 +130,7 @@ Use the following commands to download and install the RPM: ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') # Set the version to install -VERSION="v0.8.1" +VERSION="v0.9.0" # Download the RPM curl -LO "https://github.com/pgedge/control-plane/releases/download/${VERSION}/pgedge-control-plane_${VERSION#v}_linux_${ARCH}.rpm" diff --git a/docs/installation/upgrading.md b/docs/installation/upgrading.md index a5651607..a7f09265 100644 --- a/docs/installation/upgrading.md +++ b/docs/installation/upgrading.md @@ -6,11 +6,11 @@ Plane. The Control Plane version is specified in the image property of the [stac ```yaml services: host-1: - image: ghcr.io/pgedge/control-plane:v0.8.1 + image: ghcr.io/pgedge/control-plane:v0.9.0 ``` You can *pin* to a specific version by including a version in the `image` -fields in your service specification, such as `ghcr.io/pgedge/control-plane:v0.8.1`. +fields in your service specification, such as `ghcr.io/pgedge/control-plane:v0.9.0`. If you do not include a version, Docker will pull the `ghcr.io/pgedge/control-plane:latest` tag by default. From 841223672a67539c171a953dc835d15aee5697cb Mon Sep 17 00:00:00 2001 From: Jason Lynch Date: Tue, 16 Jun 2026 16:34:54 -0400 Subject: [PATCH 02/11] docs: bump deb package version in systemd.md This version got out of sync because of the merge order between the Debian support PR and the v0.8.1 patch release and, as a result, it didn't get updated automatically by the release target. This one-time manual change will get it back in sync. --- docs/installation/systemd.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/systemd.md b/docs/installation/systemd.md index b348754e..5636746d 100644 --- a/docs/installation/systemd.md +++ b/docs/installation/systemd.md @@ -148,7 +148,7 @@ Use the following commands to download and install the deb package: ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') # Set the version to install -VERSION="v0.8.0" +VERSION="v0.9.0" # Download the deb package curl -LO "https://github.com/pgedge/control-plane/releases/download/${VERSION}/pgedge-control-plane_${VERSION#v}_linux_${ARCH}.deb" From 0f12e9ea580abcc3d6f183872c1d3a62310ebe5d Mon Sep 17 00:00:00 2001 From: Jason Lynch Date: Wed, 17 Jun 2026 15:11:56 -0400 Subject: [PATCH 03/11] fix: switch to server_version_num On Debian, `server_version` contains build information, like: `18.4 (Debian 18.4-1.trixie)`. This breaks our process that reconciles the database spec with the observed Postgres version. This commit fixes that issue by changing our `GetPostgresVersion` query to use the integer representation of the server version, e.g. `180004`, to compute the string representation. This is more reliable and behaves consistently across host OSes. --- server/internal/postgres/info.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/server/internal/postgres/info.go b/server/internal/postgres/info.go index c27c1672..a0c2fb8c 100644 --- a/server/internal/postgres/info.go +++ b/server/internal/postgres/info.go @@ -2,7 +2,14 @@ package postgres func GetPostgresVersion() Query[string] { return Query[string]{ - SQL: "SHOW server_version;", + // We're computing our own string from the numeric server version here + // because the text version from 'SHOW server_version' contains build + // information on Debian. + SQL: `WITH version_num AS ( + SELECT current_setting('server_version_num')::integer AS num + ) + SELECT (num / 10000)::text || '.' || (num % 10000)::text + FROM version_num;`, } } From a7d96c72366cf1efc8657899ebc1a69bebceca84 Mon Sep 17 00:00:00 2001 From: Jason Lynch Date: Wed, 17 Jun 2026 15:16:32 -0400 Subject: [PATCH 04/11] docs: add -y flag to apt install in systemd.md We use `-y` in all the other `apt` and `dnf` commands so that they're easy to copy and paste or translate into a script. --- docs/installation/systemd.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/systemd.md b/docs/installation/systemd.md index 5636746d..83a60d54 100644 --- a/docs/installation/systemd.md +++ b/docs/installation/systemd.md @@ -87,7 +87,7 @@ Run the following commands on each Debian-based host: ```sh # Install prerequisites for the pgEdge Enterprise Postgres packages sudo apt update -sudo apt install curl gnupg2 lsb-release +sudo apt install -y curl gnupg2 lsb-release # Install the pgEdge Enterprise Postgres repository curl -O --output-dir /tmp https://apt.pgedge.com/repodeb/pgedge-release_latest_all.deb From 12d8e82bc201cad44caf0da035f2cb7c3f5221e5 Mon Sep 17 00:00:00 2001 From: Jason Lynch Date: Thu, 18 Jun 2026 08:39:41 -0400 Subject: [PATCH 05/11] docs: download control plane deb file to /tmp Putting the Control Plane deb file in /tmp makes it accessible to the _apt user, so apt can avoid using the root user. --- docs/installation/systemd.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation/systemd.md b/docs/installation/systemd.md index 83a60d54..b40d843c 100644 --- a/docs/installation/systemd.md +++ b/docs/installation/systemd.md @@ -151,10 +151,10 @@ ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') VERSION="v0.9.0" # Download the deb package -curl -LO "https://github.com/pgedge/control-plane/releases/download/${VERSION}/pgedge-control-plane_${VERSION#v}_linux_${ARCH}.deb" +curl -LO --output-dir /tmp "https://github.com/pgedge/control-plane/releases/download/${VERSION}/pgedge-control-plane_${VERSION#v}_linux_${ARCH}.deb" # Install the deb package -sudo apt install ./pgedge-control-plane_${VERSION#v}_linux_${ARCH}.deb +sudo apt install /tmp/pgedge-control-plane_${VERSION#v}_linux_${ARCH}.deb ``` ## Configuration From a375c599761ab2aae4bc48ad3edff294b27eb922 Mon Sep 17 00:00:00 2001 From: Moiz Ibrar Date: Thu, 18 Jun 2026 23:26:25 +0500 Subject: [PATCH 06/11] fix: restart MCP container on KB config change (#414) * fix: restart MCP container on KB config change KB config changes (path, provider, model, key) had no effect after an update: config.yaml is bind-mounted so SIGHUP-only reload kept the old KB path, causing "file not found" / SQLite errors. Embed a PGEDGE_CONFIG_VERSION hash in the MCP container env (like RAG) so a config change forces a restart and re-initializes the KB. Also refresh the service instance's UpdatedAt before an update redeploy so the monitor doesn't briefly mark it failed during the restart. --- .../orchestrator/swarm/service_instance.go | 18 +++++++ .../orchestrator/swarm/service_spec.go | 22 ++++++-- .../orchestrator/swarm/service_spec_test.go | 51 ++++++++++++++++--- 3 files changed, 80 insertions(+), 11 deletions(-) diff --git a/server/internal/orchestrator/swarm/service_instance.go b/server/internal/orchestrator/swarm/service_instance.go index 25786566..b94a978b 100644 --- a/server/internal/orchestrator/swarm/service_instance.go +++ b/server/internal/orchestrator/swarm/service_instance.go @@ -144,6 +144,24 @@ func (s *ServiceInstanceResource) Update(ctx context.Context, rc *resource.Conte } } + // Refresh UpdatedAt before redeploying so the monitor's running-state grace + // period covers the restart window. An update that changes the container + // (e.g. a config change) restarts it, during which the container has no + // status; without bumping UpdatedAt the monitor would briefly mark the + // instance failed mid-restart even though it recovers seconds later. + // Best-effort: a failure here only risks that transient false "failed" + // state, so we log and continue rather than block the redeploy. + logger, logErr := do.Invoke[zerolog.Logger](rc.Injector) + dbSvc, refreshErr := do.Invoke[*database.Service](rc.Injector) + if refreshErr == nil { + refreshErr = dbSvc.SetServiceInstanceState(ctx, s.DatabaseID, s.ServiceInstanceID, database.ServiceInstanceStateRunning) + } + if refreshErr != nil && logErr == nil { + logger.Warn().Err(refreshErr). + Str("service_instance_id", s.ServiceInstanceID). + Msg("failed to refresh service instance UpdatedAt before redeploy; monitor may briefly mark it failed during the restart") + } + return s.deploy(ctx, rc) } diff --git a/server/internal/orchestrator/swarm/service_spec.go b/server/internal/orchestrator/swarm/service_spec.go index 7535d376..0a8ebe38 100644 --- a/server/internal/orchestrator/swarm/service_spec.go +++ b/server/internal/orchestrator/swarm/service_spec.go @@ -42,11 +42,14 @@ func buildPostgRESTEnvVars() []string { } } -// ragConfigHash returns a short hex digest of the RAG service configuration. +// serviceConfigHash returns a short hex digest of a service's configuration. // It is embedded in the container spec as PGEDGE_CONFIG_VERSION so that Docker // Swarm detects a TaskTemplate change and restarts the container whenever the -// pipeline configuration or API keys change. -func ragConfigHash(config map[string]any) string { +// configuration or API keys change. This is required for services whose config +// lives in a bind-mounted file (config.yaml / pipelines): editing the file is +// invisible to Swarm, so without a TaskTemplate change the container is never +// restarted and the new config is never re-read. +func serviceConfigHash(config map[string]any) string { b, _ := json.Marshal(config) sum := sha256.Sum256(b) return fmt.Sprintf("%x", sum[:8]) @@ -182,6 +185,17 @@ func ServiceContainerSpec(opts *ServiceContainerSpecOptions) (swarm.ServiceSpec, // Override the default container entrypoint to specify config path on bind mount. command = []string{"/app/pgedge-postgres-mcp"} args = []string{"-config", "/app/data/config.yaml"} + // Embed a hash of the service config so that Docker Swarm detects a + // TaskTemplate change and restarts the container when the config changes. + // The MCP config (config.yaml) lives on a bind mount, so edits to it are + // invisible to Swarm. SIGHUP reloads the database client connections but + // does NOT re-initialize the knowledgebase, so KB config changes (path, + // provider, model, key) only take effect on a restart. Without this, a + // changed kb_database_host_path silently keeps using the old KB file. + // Connection details (hosts, target_session_attrs) are intentionally not + // part of the config map, so failover-driven reconnects still use SIGHUP + // without forcing a restart. + env = []string{"PGEDGE_CONFIG_VERSION=" + serviceConfigHash(opts.ServiceSpec.Config)} healthcheck = &container.HealthConfig{ Test: []string{"CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"}, StartPeriod: serviceHealthCheckStartPeriod, @@ -202,7 +216,7 @@ func ServiceContainerSpec(opts *ServiceContainerSpecOptions) (swarm.ServiceSpec, // Embed a hash of the service config so that Docker Swarm detects a // TaskTemplate change and restarts the container when pipelines or API // keys change. Without this, bind-mount updates are invisible to Swarm. - env = []string{"PGEDGE_CONFIG_VERSION=" + ragConfigHash(opts.ServiceSpec.Config)} + env = []string{"PGEDGE_CONFIG_VERSION=" + serviceConfigHash(opts.ServiceSpec.Config)} // No curl in the RHEL minimal image — use a TCP probe instead. healthcheck = &container.HealthConfig{ Test: []string{"CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/8080"}, diff --git a/server/internal/orchestrator/swarm/service_spec_test.go b/server/internal/orchestrator/swarm/service_spec_test.go index 4a66e1b0..e7957f93 100644 --- a/server/internal/orchestrator/swarm/service_spec_test.go +++ b/server/internal/orchestrator/swarm/service_spec_test.go @@ -106,9 +106,11 @@ func TestServiceContainerSpec(t *testing.T) { if m.Target != "/app/data" { t.Errorf("mount target = %v, want /app/data", m.Target) } - // No env vars for config (config is via file) - if len(spec.Env) > 0 { - t.Errorf("expected no env vars, got %d: %v", len(spec.Env), spec.Env) + // Config is delivered via a bind-mounted file, but a config-version + // env var is set so Swarm restarts the container when the config + // changes (the file edit alone is invisible to Swarm). + if len(spec.Env) != 1 || !strings.HasPrefix(spec.Env[0], "PGEDGE_CONFIG_VERSION=") { + t.Errorf("expected only PGEDGE_CONFIG_VERSION env, got %d: %v", len(spec.Env), spec.Env) } // Healthcheck should be set if spec.Healthcheck == nil { @@ -361,8 +363,8 @@ func TestRagConfigHash(t *testing.T) { }, } - h1 := ragConfigHash(cfg) - h2 := ragConfigHash(cfg) + h1 := serviceConfigHash(cfg) + h2 := serviceConfigHash(cfg) assert.Equal(t, h1, h2, "same config must produce the same hash") assert.Len(t, h1, 16, "hash should be 16 hex chars (8 bytes)") @@ -371,7 +373,7 @@ func TestRagConfigHash(t *testing.T) { map[string]any{"name": "default", "api_key": "sk-xyz"}, }, } - assert.NotEqual(t, h1, ragConfigHash(changed), "different config must produce a different hash") + assert.NotEqual(t, h1, serviceConfigHash(changed), "different config must produce a different hash") } func TestServiceContainerSpec_RAGHasConfigVersionEnv(t *testing.T) { @@ -412,7 +414,7 @@ func TestServiceContainerSpec_RAGHasConfigVersionEnv(t *testing.T) { } } require.NotEmpty(t, configVersion, "RAG container spec must have PGEDGE_CONFIG_VERSION env var") - assert.Equal(t, ragConfigHash(config), configVersion) + assert.Equal(t, serviceConfigHash(config), configVersion) // Changing the config must produce a different env var value. opts.ServiceSpec.Config = map[string]any{ @@ -633,3 +635,38 @@ func TestServiceContainerSpec_MCP_KBMount(t *testing.T) { t.Error("KB mount must be read-only") } } + +func TestServiceContainerSpec_MCPHasConfigVersionEnv(t *testing.T) { + // The MCP config lives in a bind-mounted config.yaml. Editing it is invisible + // to Swarm, and SIGHUP does not re-initialize the knowledgebase, so a KB config + // change must alter the TaskTemplate to force a restart. We embed a config hash + // in PGEDGE_CONFIG_VERSION exactly as RAG does. + opts := makeMCPSpecOpts() + opts.ServiceSpec.Config = map[string]any{ + "kb_enabled": true, + "kb_embedding_provider": "openai", + "kb_embedding_model": "text-embedding-3-small", + "kb_database_host_path": "/var/lib/pgedge/kb/nla-kb.db", + } + + configVersion := func(o *ServiceContainerSpecOptions) string { + spec, err := ServiceContainerSpec(o) + require.NoError(t, err) + for _, e := range spec.TaskTemplate.ContainerSpec.Env { + if strings.HasPrefix(e, "PGEDGE_CONFIG_VERSION=") { + return strings.TrimPrefix(e, "PGEDGE_CONFIG_VERSION=") + } + } + return "" + } + + v1 := configVersion(opts) + require.NotEmpty(t, v1, "MCP container spec must have PGEDGE_CONFIG_VERSION env var") + assert.Equal(t, serviceConfigHash(opts.ServiceSpec.Config), v1) + + // Renaming the KB file (changing kb_database_host_path) must change the env + // var so Swarm restarts the container and the new KB path takes effect. + opts.ServiceSpec.Config["kb_database_host_path"] = "/var/lib/pgedge/kb/nla-kb-openai.db" + v2 := configVersion(opts) + assert.NotEqual(t, v1, v2, "changing the KB path must change PGEDGE_CONFIG_VERSION so the container restarts") +} From 8cffc46142ae9866dc266ce5b7be4d40857b16de Mon Sep 17 00:00:00 2001 From: Jason Lynch Date: Thu, 18 Jun 2026 16:22:38 -0400 Subject: [PATCH 07/11] fix: only execute preremove scriptlets during removal The preremove scriptlets also get called during package upgrade. Our preremove scriptlets stop and disable the control plane service, so we only want them to run during a removal. This commit resolves the issue by adding a condition that's only true during removal. This condition is different for each package type, so I've split this script into separate files like we do for the postinstall script. --- .goreleaser.yaml | 4 ++-- packaging/deb/preremove.sh | 6 ++++++ packaging/preremove.sh | 4 ---- packaging/rpm/preremove.sh | 6 ++++++ 4 files changed, 14 insertions(+), 6 deletions(-) create mode 100755 packaging/deb/preremove.sh delete mode 100755 packaging/preremove.sh create mode 100755 packaging/rpm/preremove.sh diff --git a/.goreleaser.yaml b/.goreleaser.yaml index cff77a6d..47cb4776 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -37,15 +37,15 @@ nfpms: - rpm - deb bindir: /usr/sbin - scripts: - preremove: packaging/preremove.sh overrides: rpm: scripts: postinstall: packaging/rpm/postinstall.sh + preremove: packaging/rpm/preremove.sh deb: scripts: postinstall: packaging/deb/postinstall.sh + preremove: packaging/deb/preremove.sh contents: - src: packaging/pgedge-control-plane.service dst: /usr/lib/systemd/system/pgedge-control-plane.service diff --git a/packaging/deb/preremove.sh b/packaging/deb/preremove.sh new file mode 100755 index 00000000..86e27398 --- /dev/null +++ b/packaging/deb/preremove.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +if [ "$1" = "remove" ]; then + /bin/systemctl stop pgedge-control-plane.service >/dev/null 2>&1 || : + /bin/systemctl --no-reload disable pgedge-control-plane.service >/dev/null 2>&1 || : +fi diff --git a/packaging/preremove.sh b/packaging/preremove.sh deleted file mode 100755 index 315bf5e4..00000000 --- a/packaging/preremove.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -/bin/systemctl stop pgedge-control-plane.service >/dev/null 2>&1 || : -/bin/systemctl --no-reload disable pgedge-control-plane.service >/dev/null 2>&1 || : diff --git a/packaging/rpm/preremove.sh b/packaging/rpm/preremove.sh new file mode 100755 index 00000000..ce7c67e1 --- /dev/null +++ b/packaging/rpm/preremove.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +if [ "$1" -eq 0 ]; then + /bin/systemctl stop pgedge-control-plane.service >/dev/null 2>&1 || : + /bin/systemctl --no-reload disable pgedge-control-plane.service >/dev/null 2>&1 || : +fi From 3c6b982253eb0464f39169d7305ee19c74ec10c8 Mon Sep 17 00:00:00 2001 From: Jason Lynch Date: Mon, 22 Jun 2026 08:41:42 -0400 Subject: [PATCH 08/11] fix: migrate scheduled job deps Adds a migration for scheduled job dependencies to use the new common pgBackRest stanza resource type instead of the the old swarm one. --- server/internal/resource/migrations/1_2_0.go | 102 +++- .../resource/migrations/1_2_0_test.go | 46 ++ .../with_scheduled_backups.json | 494 ++++++++++++++++++ .../migrations/schemas/v1_1_0/scheduler.go | 26 + .../migrations/schemas/v1_2_0/scheduler.go | 26 + 5 files changed, 676 insertions(+), 18 deletions(-) create mode 100644 server/internal/resource/migrations/golden_test/TestVersion_1_2_0/with_scheduled_backups.json create mode 100644 server/internal/resource/migrations/schemas/v1_1_0/scheduler.go create mode 100644 server/internal/resource/migrations/schemas/v1_2_0/scheduler.go diff --git a/server/internal/resource/migrations/1_2_0.go b/server/internal/resource/migrations/1_2_0.go index 5398c177..77cb04a7 100644 --- a/server/internal/resource/migrations/1_2_0.go +++ b/server/internal/resource/migrations/1_2_0.go @@ -38,6 +38,7 @@ func (v *Version_1_2_0) Run(databaseID string, state *resource.State) error { v.migratePgBackRestStanza(databaseID, state), v.migratePostgresCerts(state), v.migratePatroniConfig(state), + v.migrateScheduledJob(state), ) v.updateDependencies(state) return err @@ -49,26 +50,9 @@ func (v *Version_1_2_0) updateDependencies(state *resource.State) { for _, byID := range state.Resources { for _, data := range byID { for i, dep := range data.Dependencies { - var newType resource.Type - switch dep.Type { - case v1_1_0.ResourceTypeEtcdCreds: - newType = v1_2_0.ResourceTypeEtcdCreds - case v1_1_0.ResourceTypePatroniCluster: - newType = v1_2_0.ResourceTypePatroniCluster - case v1_1_0.ResourceTypePatroniMember: - newType = v1_2_0.ResourceTypePatroniMember - case v1_1_0.ResourceTypePgBackRestConfig: - newType = v1_2_0.ResourceTypePgBackRestConfig - case v1_1_0.ResourceTypePgBackRestStanza: - newType = v1_2_0.ResourceTypePgBackRestStanza - case v1_1_0.ResourceTypePostgresCerts: - newType = v1_2_0.ResourceTypePostgresCerts - default: - continue - } data.Dependencies[i] = resource.Identifier{ ID: dep.ID, - Type: newType, + Type: v.transformType(dep.Type), } } } @@ -293,6 +277,63 @@ func (v *Version_1_2_0) migratePgBackRestConfig(state *resource.State) error { return nil } +func (v *Version_1_2_0) migrateScheduledJob(state *resource.State) error { + resources, ok := state.Resources[v1_1_0.ResourceTypeScheduledJob] + if !ok { + return nil + } + adds := make([]*resource.ResourceData, 0, len(resources)) + for oldID, data := range resources { + var old v1_1_0.ScheduledJobResource + if err := json.Unmarshal(data.Attributes, &old); err != nil { + return fmt.Errorf("failed to unmarshal %s resource: %w", v1_1_0.ResourceTypeScheduledJob, err) + } + deps := make([]struct { + ID string `json:"id"` + Type string `json:"type"` + }, len(old.DependsOn)) + for i, dep := range old.DependsOn { + deps[i] = struct { + ID string `json:"id"` + Type string `json:"type"` + }{ + ID: dep.ID, + Type: v.transformTypeString(dep.Type), + } + } + new := v1_2_0.ScheduledJobResource{ + ID: old.ID, + CronExpr: old.CronExpr, + Workflow: old.Workflow, + Args: old.Args, + DependsOn: deps, + } + attrs, err := json.Marshal(new) + if err != nil { + return fmt.Errorf("failed to marshal %s resource: %w", v1_2_0.ResourceTypeScheduledJob, err) + } + adds = append(adds, &resource.ResourceData{ + Identifier: v1_2_0.ScheduledJobResourceIdentifier(old.ID), + Attributes: attrs, + Dependencies: data.Dependencies, // these will get updated by updateDependencies + Executor: data.Executor, + ResourceVersion: "1", + NeedsRecreate: data.NeedsRecreate, + DiffIgnore: data.DiffIgnore, + PendingDeletion: data.PendingDeletion, + Error: data.Error, + TypeDependencies: data.TypeDependencies, + }) + state.RemoveByIdentifier(resource.Identifier{ + Type: v1_1_0.ResourceTypeScheduledJob, + ID: oldID, + }) + } + state.Add(adds...) + + return nil +} + func (v *Version_1_2_0) migratePgBackRestStanza(databaseID string, state *resource.State) error { resources, ok := state.Resources[v1_1_0.ResourceTypePgBackRestStanza] if !ok { @@ -596,3 +637,28 @@ func (v *Version_1_2_0) swarmInstancePaths() database.InstancePaths { PatroniPath: "/usr/local/bin/patroni", } } + +func (v *Version_1_2_0) transformType(old resource.Type) resource.Type { + var new resource.Type + switch old { + case v1_1_0.ResourceTypeEtcdCreds: + new = v1_2_0.ResourceTypeEtcdCreds + case v1_1_0.ResourceTypePatroniCluster: + new = v1_2_0.ResourceTypePatroniCluster + case v1_1_0.ResourceTypePatroniMember: + new = v1_2_0.ResourceTypePatroniMember + case v1_1_0.ResourceTypePgBackRestConfig: + new = v1_2_0.ResourceTypePgBackRestConfig + case v1_1_0.ResourceTypePgBackRestStanza: + new = v1_2_0.ResourceTypePgBackRestStanza + case v1_1_0.ResourceTypePostgresCerts: + new = v1_2_0.ResourceTypePostgresCerts + default: + new = old + } + return new +} + +func (v *Version_1_2_0) transformTypeString(old string) string { + return string(v.transformType(resource.Type(old))) +} diff --git a/server/internal/resource/migrations/1_2_0_test.go b/server/internal/resource/migrations/1_2_0_test.go index 239a699b..f4fea312 100644 --- a/server/internal/resource/migrations/1_2_0_test.go +++ b/server/internal/resource/migrations/1_2_0_test.go @@ -99,6 +99,23 @@ func TestVersion_1_2_0(t *testing.T) { v1_1_0_network(t, databaseID), }, }, + { + name: "with scheduled backups", + in: []*resource.ResourceData{ + v1_1_0_node(t, "n1", "instance-1"), + v1_1_0_instance(t, databaseID, "instance-1", "host-1", "n1"), + v1_1_0_etcdCreds(t, databaseID, "instance-1", "host-1", "n1"), + v1_1_0_patroniCluster(t, databaseID, "n1"), + v1_1_0_patroniMember(t, databaseID, "instance-1", "n1"), + v1_1_0_postgresCerts(t, "instance-1", "host-1"), + v1_1_0_patroniConfig(t, databaseID, "instance-1", "host-1", "n1", true, false, false), + v1_1_0_pgBackRestConfig(t, databaseID, "instance-1", "host-1", "n1", pgbackrest.ConfigTypeBackup), + v1_1_0_pgBackRestStanza(t, "n1"), + v1_1_0_configsDir(t, "host-1"), + v1_1_0_network(t, databaseID), + v1_1_0_scheduledJob(t, databaseID, "n1"), + }, + }, } { t.Run(tc.name, func(t *testing.T) { state := &resource.State{ @@ -695,6 +712,35 @@ func v1_1_0_configsDir(t testing.TB, hostID string) *resource.ResourceData { } } +func v1_1_0_scheduledJob(t testing.TB, databaseID, nodeName string) *resource.ResourceData { + stanzaID := v1_1_0.PgBackRestStanzaIdentifier(nodeName) + return &resource.ResourceData{ + Executor: resource.AnyExecutor(), + Identifier: v1_1_0.ScheduledJobResourceIdentifier("backups"), + ResourceVersion: "1", + Attributes: mustJSON(t, v1_1_0.ScheduledJobResource{ + ID: "backups", + CronExpr: "0 * * * *", + Workflow: "CreatePgBackRestBackup", + Args: map[string]any{ + "database_id": databaseID, + "node_name": nodeName, + "type": pgbackrest.BackupTypeFull.String(), + }, + DependsOn: []struct { + ID string `json:"id"` + Type string `json:"type"` + }{ + { + ID: stanzaID.ID, + Type: stanzaID.Type.String(), + }, + }, + }), + Dependencies: []resource.Identifier{stanzaID}, + } +} + func v1_1_0_configsDirID(hostID string) string { return hostID + "-configs" } diff --git a/server/internal/resource/migrations/golden_test/TestVersion_1_2_0/with_scheduled_backups.json b/server/internal/resource/migrations/golden_test/TestVersion_1_2_0/with_scheduled_backups.json new file mode 100644 index 00000000..acec8932 --- /dev/null +++ b/server/internal/resource/migrations/golden_test/TestVersion_1_2_0/with_scheduled_backups.json @@ -0,0 +1,494 @@ +{ + "version": "1.2.0", + "resources": { + "common.etcd_creds": { + "instance-1": { + "needs_recreate": false, + "executor": { + "type": "host", + "id": "host-1" + }, + "identifier": { + "id": "instance-1", + "type": "common.etcd_creds" + }, + "attributes": { + "instance_id": "instance-1", + "database_id": "database-1", + "host_id": "host-1", + "node_name": "n1", + "parent_id": "host-1-configs", + "owner_uid": 123, + "owner_gid": 124, + "username": "username", + "password": "password", + "ca_cert": "Y2FfY2VydA==", + "client_cert": "Y2xpZW50X2NlcnQ=", + "client_key": "Y2xpZW50X2tleQ==" + }, + "dependencies": [ + { + "id": "host-1-configs", + "type": "filesystem.dir" + } + ], + "type_dependencies": null, + "diff_ignore": [ + "/username", + "/password", + "/ca_cert", + "/client_cert", + "/client_key" + ], + "resource_version": "1", + "pending_deletion": false, + "error": "" + } + }, + "common.patroni_cluster": { + "n1": { + "needs_recreate": false, + "executor": { + "type": "any", + "id": "" + }, + "identifier": { + "id": "n1", + "type": "common.patroni_cluster" + }, + "attributes": { + "database_id": "database-1", + "node_name": "n1" + }, + "dependencies": null, + "type_dependencies": null, + "diff_ignore": null, + "resource_version": "1", + "pending_deletion": false, + "error": "" + } + }, + "common.patroni_member": { + "instance-1": { + "needs_recreate": false, + "executor": { + "type": "any", + "id": "" + }, + "identifier": { + "id": "instance-1", + "type": "common.patroni_member" + }, + "attributes": { + "database_id": "database-1", + "node_name": "n1", + "instance_id": "instance-1" + }, + "dependencies": [ + { + "id": "n1", + "type": "common.patroni_cluster" + } + ], + "type_dependencies": null, + "diff_ignore": null, + "resource_version": "1", + "pending_deletion": false, + "error": "" + } + }, + "common.pgbackrest_config": { + "instance-1-backup": { + "needs_recreate": false, + "executor": { + "type": "host", + "id": "host-1" + }, + "identifier": { + "id": "instance-1-backup", + "type": "common.pgbackrest_config" + }, + "attributes": { + "instance_id": "instance-1", + "host_id": "host-1", + "database_id": "database-1", + "node_name": "n1", + "repositories": [ + { + "id": "default", + "type": "s3", + "s3_bucket": "bucket", + "s3_region": "us-east-1", + "s3_endpoint": "s3.us-east-1.amazonaws.com", + "retention_full": 0, + "retention_full_type": "" + } + ], + "parent_id": "host-1-configs", + "type": "backup", + "owner_uid": 123, + "owner_gid": 124, + "paths": { + "instance": { + "base_dir": "/opt/pgedge" + }, + "host": { + "base_dir": "" + }, + "pg_backrest_path": "/usr/bin/pgbackrest", + "patroni_path": "/usr/local/bin/patroni" + }, + "port": 5432 + }, + "dependencies": [ + { + "id": "host-1-configs", + "type": "filesystem.dir" + } + ], + "type_dependencies": null, + "diff_ignore": null, + "resource_version": "1", + "pending_deletion": false, + "error": "" + } + }, + "common.pgbackrest_stanza": { + "n1": { + "needs_recreate": false, + "executor": { + "type": "primary", + "id": "n1" + }, + "identifier": { + "id": "n1", + "type": "common.pgbackrest_stanza" + }, + "attributes": { + "database_id": "database-1", + "node_name": "n1" + }, + "dependencies": [ + { + "id": "n1", + "type": "database.node" + } + ], + "type_dependencies": null, + "diff_ignore": null, + "resource_version": "1", + "pending_deletion": false, + "error": "" + } + }, + "common.postgres_certs": { + "instance-1": { + "needs_recreate": false, + "executor": { + "type": "host", + "id": "host-1" + }, + "identifier": { + "id": "instance-1", + "type": "common.postgres_certs" + }, + "attributes": { + "instance_id": "instance-1", + "host_id": "host-1", + "instance_addresses": [ + "127.0.0.1", + "localhost" + ], + "parent_id": "host-1-configs", + "owner_uid": 123, + "owner_gid": 124, + "ca_cert": "Y2FfY2VydA==", + "server_cert": "c2VydmVyX2NlcnQ=", + "server_key": "c2VydmVyX2tleQ==", + "superuser_cert": "c3VwZXJ1c2VyX2NlcnQ=", + "superuser_key": "c3VwZXJ1c2VyX2tleQ==", + "replication_cert": "cmVwbGljYXRpb25fY2VydA==", + "replication_key": "cmVwbGljYXRpb25fa2V5" + }, + "dependencies": [ + { + "id": "host-1-configs", + "type": "filesystem.dir" + } + ], + "type_dependencies": null, + "diff_ignore": [ + "/ca_cert", + "/server_cert", + "/server_key", + "/superuser_cert", + "/superuser_key", + "/replication_cert", + "/replication_key" + ], + "resource_version": "1", + "pending_deletion": false, + "error": "" + } + }, + "database.instance": { + "instance-1": { + "needs_recreate": false, + "executor": { + "type": "host", + "id": "host-1" + }, + "identifier": { + "id": "instance-1", + "type": "database.instance" + }, + "attributes": { + "spec": { + "database_id": "database-1", + "database_name": "test", + "database_users": [ + { + "db_owner": true, + "username": "admin" + } + ], + "host_id": "host-1", + "instance_id": "instance-1", + "node_name": "n1" + } + }, + "dependencies": null, + "type_dependencies": null, + "diff_ignore": [ + "/primary_instance_id", + "/connection_info" + ], + "resource_version": "1", + "pending_deletion": false, + "error": "" + } + }, + "database.node": { + "n1": { + "needs_recreate": false, + "executor": { + "type": "any", + "id": "" + }, + "identifier": { + "id": "n1", + "type": "database.node" + }, + "attributes": { + "name": "n1", + "instance_ids": [ + "instance-1" + ], + "primary_instance_id": "instance-1" + }, + "dependencies": [ + { + "id": "instance-1", + "type": "database.instance" + } + ], + "type_dependencies": null, + "diff_ignore": null, + "resource_version": "1", + "pending_deletion": false, + "error": "" + } + }, + "filesystem.dir": { + "host-1-configs": { + "needs_recreate": false, + "executor": { + "type": "host", + "id": "host-1" + }, + "identifier": { + "id": "host-1-configs", + "type": "filesystem.dir" + }, + "attributes": { + "id": "host-1-configs", + "parent_id": "", + "host_id": "", + "path": "/configs", + "owner_uid": 0, + "owner_gid": 0, + "perm": 0, + "full_path": "" + }, + "dependencies": null, + "type_dependencies": null, + "diff_ignore": null, + "resource_version": "1", + "pending_deletion": false, + "error": "" + } + }, + "scheduler.job": { + "backups": { + "needs_recreate": false, + "executor": { + "type": "any", + "id": "" + }, + "identifier": { + "id": "backups", + "type": "scheduler.job" + }, + "attributes": { + "id": "backups", + "cron_expr": "0 * * * *", + "workflow": "CreatePgBackRestBackup", + "args": { + "database_id": "database-1", + "node_name": "n1", + "type": "full" + }, + "depends_on": [ + { + "id": "n1", + "type": "common.pgbackrest_stanza" + } + ] + }, + "dependencies": [ + { + "id": "n1", + "type": "common.pgbackrest_stanza" + } + ], + "type_dependencies": null, + "diff_ignore": null, + "resource_version": "1", + "pending_deletion": false, + "error": "" + } + }, + "swarm.network": { + "database-1": { + "needs_recreate": false, + "executor": { + "type": "manager", + "id": "" + }, + "identifier": { + "id": "database-1", + "type": "swarm.network" + }, + "attributes": { + "scope": "swarm", + "driver": "overlay", + "allocator": { + "prefix": "", + "bits": 0 + }, + "name": "database-1", + "network_id": "", + "subnet": "", + "gateway": "" + }, + "dependencies": null, + "type_dependencies": null, + "diff_ignore": null, + "resource_version": "1", + "pending_deletion": false, + "error": "" + } + }, + "swarm.patroni_config": { + "instance-1": { + "needs_recreate": false, + "executor": { + "type": "host", + "id": "host-1" + }, + "identifier": { + "id": "instance-1", + "type": "swarm.patroni_config" + }, + "attributes": { + "database_id": "database-1", + "base": { + "instance_id": "instance-1", + "host_id": "host-1", + "node_name": "n1", + "generator": { + "archive_command": "/usr/bin/pgbackrest --config /opt/pgedge/configs/pgbackrest.backup.conf --stanza db archive-push \"%p\"", + "cluster_size": 3, + "cpus": 4, + "database_id": "database-1", + "data_dir": "/opt/pgedge/data/pgdata", + "etcd_certs_dir": "/opt/pgedge/certificates/etcd", + "fqdn": "database-1-n1", + "instance_id": "instance-1", + "log_type": "json", + "memory_bytes": 1073741824, + "node_name": "n1", + "node_ordinal": 1, + "node_size": 1, + "patroni_allowlist": null, + "patroni_port": 8888, + "pg_hba_conf": [ + "hostssl all myapp_user 203.0.113.0/24 scram-sha-256" + ], + "pg_ident_conf": [ + "ssl_users CN=alice,O=example alice" + ], + "postgres_certs_dir": "/opt/pgedge/certificates/postgres", + "postgres_port": 5432, + "restore_command": "", + "spec_parameters": { + "max_connections": 1000 + } + }, + "parent_id": "host-1-configs", + "owner_uid": 123, + "owner_gid": 124 + }, + "host_network_info": { + "name": "bridge", + "id": "bridge_id", + "subnet": "172.16.0.0/12", + "gateway": "172.16.0.1" + }, + "database_network_name": "database-1" + }, + "dependencies": [ + { + "id": "host-1-configs", + "type": "filesystem.dir" + }, + { + "id": "instance-1", + "type": "common.etcd_creds" + }, + { + "id": "instance-1", + "type": "common.patroni_member" + }, + { + "id": "n1", + "type": "common.patroni_cluster" + }, + { + "id": "database-1", + "type": "swarm.network" + }, + { + "id": "instance-1-backup", + "type": "common.pgbackrest_config" + } + ], + "type_dependencies": null, + "diff_ignore": null, + "resource_version": "1", + "pending_deletion": false, + "error": "" + } + } + } +} \ No newline at end of file diff --git a/server/internal/resource/migrations/schemas/v1_1_0/scheduler.go b/server/internal/resource/migrations/schemas/v1_1_0/scheduler.go new file mode 100644 index 00000000..efd4ea81 --- /dev/null +++ b/server/internal/resource/migrations/schemas/v1_1_0/scheduler.go @@ -0,0 +1,26 @@ +// produced by schematool 7c318111f3fd9ddfce9b7fdf0bd32865e23d7cca server/internal/scheduler ScheduledJobResource +package v1_1_0 + +import ( + "github.com/pgEdge/control-plane/server/internal/resource" +) + +const ResourceTypeScheduledJob resource.Type = "scheduler.job" + +func ScheduledJobResourceIdentifier(id string) resource.Identifier { + return resource.Identifier{ + ID: id, + Type: ResourceTypeScheduledJob, + } +} + +type ScheduledJobResource struct { + ID string `json:"id"` + CronExpr string `json:"cron_expr"` + Workflow string `json:"workflow"` + Args map[string]any `json:"args"` + DependsOn []struct { + ID string `json:"id"` + Type string `json:"type"` + } `json:"depends_on,omitempty"` +} diff --git a/server/internal/resource/migrations/schemas/v1_2_0/scheduler.go b/server/internal/resource/migrations/schemas/v1_2_0/scheduler.go new file mode 100644 index 00000000..a4269dc1 --- /dev/null +++ b/server/internal/resource/migrations/schemas/v1_2_0/scheduler.go @@ -0,0 +1,26 @@ +// produced by schematool 4d916a246d1ef9e10e2b697e1aaf3b2bc1c2ea21 server/internal/scheduler ScheduledJobResource +package v1_2_0 + +import ( + "github.com/pgEdge/control-plane/server/internal/resource" +) + +const ResourceTypeScheduledJob resource.Type = "scheduler.job" + +func ScheduledJobResourceIdentifier(id string) resource.Identifier { + return resource.Identifier{ + ID: id, + Type: ResourceTypeScheduledJob, + } +} + +type ScheduledJobResource struct { + ID string `json:"id"` + CronExpr string `json:"cron_expr"` + Workflow string `json:"workflow"` + Args map[string]any `json:"args"` + DependsOn []struct { + ID string `json:"id"` + Type string `json:"type"` + } `json:"depends_on,omitempty"` +} From 659a6ac3495a3d44e21695e0afe07553a8a27081 Mon Sep 17 00:00:00 2001 From: Moiz Ibrar Date: Mon, 22 Jun 2026 17:56:32 +0500 Subject: [PATCH 09/11] fix: reject KB path that points to a directory (#417) --- .../orchestrator/swarm/mcp_config_resource.go | 16 ++++++++++--- .../swarm/mcp_config_resource_test.go | 24 +++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/server/internal/orchestrator/swarm/mcp_config_resource.go b/server/internal/orchestrator/swarm/mcp_config_resource.go index b0888fc7..7cf3bbfa 100644 --- a/server/internal/orchestrator/swarm/mcp_config_resource.go +++ b/server/internal/orchestrator/swarm/mcp_config_resource.go @@ -99,9 +99,9 @@ func (r *MCPConfigResource) Refresh(ctx context.Context, rc *resource.Context) e } // checkKBFileExists blocks deployment when kb_enabled is set but the host -// KB file is missing. Called from Create and Update — not Refresh, because -// Refresh is only invoked for resources already in state, so a check there -// would not fire on first deploy. +// KB file is missing or is not a regular file. Called from Create and Update — +// not Refresh, because Refresh is only invoked for resources already in state, +// so a check there would not fire on first deploy. func (r *MCPConfigResource) checkKBFileExists(fs afero.Fs) error { if r.KBHostPath == "" { return nil @@ -113,6 +113,16 @@ func (r *MCPConfigResource) checkKBFileExists(fs afero.Fs) error { if !exists { return fmt.Errorf("KB database file not found at %s — stage the file on the host before deploying with kb_enabled: true", r.KBHostPath) } + // A directory passes the existence check above but cannot be opened as a + // SQLite database, which would only surface as a confusing error at query + // time. Reject it here so the failure is clear at deploy time. + isDir, err := afero.IsDir(fs, r.KBHostPath) + if err != nil { + return fmt.Errorf("failed to check KB database file at %s: %w", r.KBHostPath, err) + } + if isDir { + return fmt.Errorf("KB database path %s is a directory, not a file — kb_database_host_path must point to the KB SQLite file", r.KBHostPath) + } return nil } diff --git a/server/internal/orchestrator/swarm/mcp_config_resource_test.go b/server/internal/orchestrator/swarm/mcp_config_resource_test.go index 5f6df823..58a101e0 100644 --- a/server/internal/orchestrator/swarm/mcp_config_resource_test.go +++ b/server/internal/orchestrator/swarm/mcp_config_resource_test.go @@ -141,6 +141,30 @@ func TestMCPConfigResource_Create_KBFileMissing(t *testing.T) { assert.False(t, errors.Is(err, resource.ErrNotFound), "missing KB file must not return ErrNotFound") } +func TestMCPConfigResource_Create_KBPathIsDirectory(t *testing.T) { + // KBHostPath points to a directory → blocked at deploy time, since a + // directory cannot be opened as a SQLite database. Without this check the + // error would only surface as a confusing failure at query time. + dirID := "inst-data" + dirPath := "/var/lib/pgedge/services/inst-kb-dir" + kbPath := "/var/lib/pgedge/kb" // a directory, not a file + rc, fs := mcpRCAndFs(t, dirID, dirPath) + + require.NoError(t, fs.MkdirAll(kbPath, 0o700)) + + r := &MCPConfigResource{ + ServiceInstanceID: "inst-kb-dir", + HostID: "host-1", + DirResourceID: dirID, + Config: &database.MCPServiceConfig{}, + KBHostPath: kbPath, + } + err := r.Create(context.Background(), rc) + require.Error(t, err) + assert.Contains(t, err.Error(), "is a directory") + assert.False(t, errors.Is(err, resource.ErrNotFound), "a directory KB path must not return ErrNotFound") +} + func TestMCPConfigResource_Update_KBFileMissing(t *testing.T) { // KBHostPath set but file does not exist on the update path → blocked. dirID := "inst-data" From 5174d9cf38c145ab3252f3ec33289b3e2ee03f4d Mon Sep 17 00:00:00 2001 From: Jason Lynch Date: Mon, 22 Jun 2026 09:01:01 -0400 Subject: [PATCH 10/11] fix: validate backup config exists in backup node Adds validation to the backup database node endpoint to return a 400 error when the given node does not have a backup configuration. --- server/internal/api/apiv1/post_init_handlers.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/internal/api/apiv1/post_init_handlers.go b/server/internal/api/apiv1/post_init_handlers.go index 38fb740f..56791d1b 100644 --- a/server/internal/api/apiv1/post_init_handlers.go +++ b/server/internal/api/apiv1/post_init_handlers.go @@ -470,6 +470,9 @@ func (s *PostInitHandlers) BackupDatabaseNode(ctx context.Context, req *api.Back if err != nil { return nil, apiErr(err) } + if node.BackupConfig == nil && db.Spec.BackupConfig == nil { + return nil, makeInvalidInputErr(errors.New("backups are not configured for this database node")) + } if err := validateBackupOptions(req.Options); err != nil { return nil, apiErr(err) From 12fcadf21a163e82d0375d3a157c8a058fc6512b Mon Sep 17 00:00:00 2001 From: Jason Lynch Date: Wed, 24 Jun 2026 08:38:33 -0400 Subject: [PATCH 11/11] docs: mirror systemd install commands in update section Updates the `Updating the Control Plane` section to use the same commands as the installation section. --- docs/installation/systemd.md | 57 +++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/docs/installation/systemd.md b/docs/installation/systemd.md index b40d843c..ab5babb1 100644 --- a/docs/installation/systemd.md +++ b/docs/installation/systemd.md @@ -343,21 +343,58 @@ new versions to be reflected in the database spec in the Control Plane API. Updating the Control Plane just involves installing the new package. This will automatically restart the Control Plane service after the update is complete. -Download the new RPM or deb from the -[GitHub releases page](https://github.com/pgedge/control-plane/releases) and run -one of the following commands: + +> [!NOTE] +> The package upgrade will preserve any modifications to the configuration file +> at `/etc/pgedge-control-plane/config.json`. + +### RPM Package + +Use the following commands to download and install the updated RPM: ```sh -# If you're using the RPM package, run: -sudo rpm -U pgedge-control-plane__linux_.rpm +# (Optional) print the current version via the API +curl http://localhost:3000/v1/version + +# Detect architecture +ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') -# If you're using the deb package, run: -sudo apt install pgedge-control-plane__linux_.deb +# Set the new version to install +VERSION="v0.9.0" + +# Download the RPM +curl -LO "https://github.com/pgedge/control-plane/releases/download/${VERSION}/pgedge-control-plane_${VERSION#v}_linux_${ARCH}.rpm" + +# Install the RPM with the 'upgrade' flag +sudo rpm -U pgedge-control-plane_${VERSION#v}_linux_${ARCH}.rpm + +# (Optional) print the updated version via the API +curl http://localhost:3000/v1/version ``` -> [!NOTE] -> The package upgrade will preserve any modifications to the configuration file -> at `/etc/pgedge-control-plane/config.json`. +### Deb Package + +Use the following commands to download and install the deb package: + +```sh +# (Optional) print the current version via the API +curl http://localhost:3000/v1/version + +# Detect architecture +ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') + +# Set the new version to install +VERSION="v0.9.0" + +# Download the deb package +curl -LO --output-dir /tmp "https://github.com/pgedge/control-plane/releases/download/${VERSION}/pgedge-control-plane_${VERSION#v}_linux_${ARCH}.deb" + +# Install the deb package +sudo apt install /tmp/pgedge-control-plane_${VERSION#v}_linux_${ARCH}.deb + +# (Optional) print the updated version via the API +curl http://localhost:3000/v1/version +``` ## Uninstalling the Control Plane