Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 99 additions & 1 deletion .github/workflows/xtest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,102 @@ jobs:
root-key: ${{ steps.km-check.outputs.root_key }}
dpop-challenge-enabled: ${{ inputs.dpop-challenge || false }}

# ####### START ERS-POSTGRES + MULTI-STRATEGY PLATFORM ##########
# Mirrors the multi-KAS pattern: a second `platform` process on port
# 8090 running in multi-strategy ERS mode (SQL provider against the
# ers-postgres container). Tests that don't reference the ers-ms
# fixtures are unaffected. See test_ers_multistrategy.py for the
# regression test that consumes this instance.
# These three steps are best-effort: while opentdf/platform#3645 and
# #3543 are still open, they will fail on any platform tag that lacks
# both. `continue-on-error: true` keeps the rest of the xct matrix
# (test_abac, test_pqc, test_dpop, ...) running. The multi-strategy
# test itself skips at fixture setup when localhost:8090 isn't
# reachable, so a failed startup here does not fail the pytest run.
- name: Start ers-postgres for multi-strategy ERS
id: ers-postgres
if: ${{ steps.multikas.outputs.supported == 'true' }}
continue-on-error: true
working-directory: ${{ steps.run-platform.outputs.platform-working-dir }}
run: |-
docker compose --profile ers-test up -d ers-postgres
# Wait for the container to become healthy so the seed step below
# doesn't race the container's init scripts.
for _ in $(seq 1 30); do
if docker exec ers_test_postgres pg_isready -U ers_test_user -d ers_test >/dev/null 2>&1; then
echo "ers-postgres ready"
exit 0
fi
sleep 1
done
echo "ers-postgres failed to become ready"
docker compose --profile ers-test logs ers-postgres
exit 1

- name: Seed ers-postgres
if: ${{ steps.multikas.outputs.supported == 'true' }}
continue-on-error: true
run: |-
docker exec -i ers_test_postgres psql \
-U ers_test_user -d ers_test -v ON_ERROR_STOP=1 <<'SQL'
CREATE TABLE IF NOT EXISTS ers_attributes (
username TEXT PRIMARY KEY,
department TEXT NOT NULL,
active BOOLEAN NOT NULL DEFAULT true
);
INSERT INTO ers_attributes (username, department)
VALUES ('opentdf', 'finance')
ON CONFLICT (username) DO NOTHING;
SQL

- name: Start multi-strategy ERS platform (port 8090)
id: platform-ers-ms
if: ${{ steps.multikas.outputs.supported == 'true' }}
continue-on-error: true
run: |-
set -euo pipefail
config_src="$GITHUB_WORKSPACE/otdftests/xtest/platform-configs/opentdf-multistrategy.yaml"
platform_dir="$GITHUB_WORKSPACE/${PLATFORM_WORKING_DIR}"
cp "$config_src" "$platform_dir/opentdf-ers-ms.yaml"

# Sync root_key with the primary platform's opentdf.yaml so both
# KAS instances can unwrap each other's wrapped DEKs. The template
# ships with a placeholder that yq overwrites here.
primary_root_key="$(yq e '.services.kas.root_key' "$platform_dir/opentdf.yaml")"
if [ -z "$primary_root_key" ] || [ "$primary_root_key" = "null" ]; then
echo "could not read services.kas.root_key from $platform_dir/opentdf.yaml"
exit 1
fi
yq e -i ".services.kas.root_key = \"$primary_root_key\"" \
"$platform_dir/opentdf-ers-ms.yaml"

log_file="$GITHUB_WORKSPACE/platform-ers-ms.log"
echo "log-file=platform-ers-ms.log" >> "$GITHUB_OUTPUT"

# Run the second platform in the background. Uses the same source
# tree as the default platform so both share the policy DB, KAS
# keys, and cryptoProvider config -- only entityresolution differs.
cd "$platform_dir"
nohup go run ./service start --config-file opentdf-ers-ms.yaml \
>"$log_file" 2>&1 &
echo $! > "$GITHUB_WORKSPACE/platform-ers-ms.pid"

# Wait for /healthz. Expected to fail on any platform tag that
# doesn't include both opentdf/platform#3645 and #3543 -- the
# SQL provider panics at startup on #3543-less platforms.
for _ in $(seq 1 60); do
if curl -sf http://localhost:8090/healthz >/dev/null; then
echo "platform-ers-ms ready"
exit 0
fi
sleep 2
done
echo "platform-ers-ms failed to become ready; last 200 log lines:"
tail -n 200 "$log_file" || true
exit 1
env:
PLATFORM_WORKING_DIR: ${{ steps.run-platform.outputs.platform-working-dir }}

- name: Run attribute based configuration tests
if: ${{ steps.multikas.outputs.supported == 'true' }}
run: |-
Expand All @@ -689,13 +785,15 @@ jobs:
--sdks-encrypt "${ENCRYPT_SDK}" \
--focus "$FOCUS_SDK" \
$skip_flag \
test_abac.py test_pqc.py test_dpop.py
test_abac.py test_pqc.py test_dpop.py test_ers_multistrategy.py
working-directory: otdftests/xtest
env:
PLATFORM_DIR: "../../${{ steps.run-platform.outputs.platform-working-dir }}"
PLATFORM_TAG: ${{ matrix.platform-tag }}
OTDFCTL_HEADS: ${{ steps.configure-go.outputs.heads }}
PLATFORM_LOG_FILE: "../../${{ steps.run-platform.outputs.platform-log-file }}"
PLATFORMURL_ERS_MS: "http://localhost:8090"
PLATFORM_ERS_MS_LOG_FILE: "../../platform-ers-ms.log"
KAS_ALPHA_LOG_FILE: "../../${{ steps.kas-alpha.outputs.log-file }}"
KAS_BETA_LOG_FILE: "../../${{ steps.kas-beta.outputs.log-file }}"
KAS_GAMMA_LOG_FILE: "../../${{ steps.kas-gamma.outputs.log-file }}"
Expand Down
22 changes: 22 additions & 0 deletions otdf-local/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,30 @@ uv run pytest --sdks go -v

Auto-configured by otdf-local:
- Keycloak: 8888, Postgres: 5432, Platform: 8080
- Multi-strategy ERS platform: 8090 (backed by ers-postgres on 5433)
- KAS: alpha=8181, beta=8282, gamma=8383, delta=8484, km1=8585, km2=8686

## Multi-strategy ERS platform

`otdf-local up` boots a **second `platform` process** (`platform-ers-ms`)
on port 8090 alongside the default Keycloak-ERS platform. It runs with
`entityresolution: mode: multi-strategy` and a SQL provider pointed at the
`ers-postgres` container (docker compose profile `ers-test`, port 5433).
Comment thread
damonbanks marked this conversation as resolved.
Both platforms share the same policy DB, KAS keys, and cryptoProvider
config — only the entity-resolution block differs. This mirrors the
multi-KAS pattern: extra infrastructure is always up; tests that don't
reference the ers-ms fixtures are unaffected.

- Template config: `xtest/platform-configs/opentdf-multistrategy.yaml`.
- Generated config: `xtest/tmp/config/opentdf-ers-ms.yaml`.
- Log file: `xtest/tmp/logs/platform-ers-ms.log`.
- Env vars exported by `otdf-local env`: `PLATFORMURL_ERS_MS`, `PLATFORM_ERS_MS_LOG_FILE`.
- Restart just this instance: `uv run otdf-local restart platform-ers-ms`.

The seed row for the multi-strategy SQL provider (`INSERT INTO
ers_attributes VALUES ('opentdf', 'finance')`) is applied during the
`provision` step and is idempotent, so `otdf-local restart` is safe.

## Restart Procedures

### Full Environment Restart
Expand Down
62 changes: 61 additions & 1 deletion otdf-local/src/otdf_local/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
ProvisionResult,
get_docker_service,
get_kas_manager,
get_platform_ers_ms_service,
get_platform_service,
get_provisioner,
)
Expand Down Expand Up @@ -185,6 +186,45 @@ def up(
else:
print_success("Provisioning complete")

print_info("Seeding ers-postgres for multi-strategy ERS...")
seed_result = provisioner.provision_ers_ms_seed()
if not seed_result:
_show_provision_error(seed_result, "ers-postgres seed")
print_warning(
"ers-postgres seed failed - multi-strategy ERS tests will fail"
)
else:
print_success("ers-postgres seeded")

# Step 2b: Start multi-strategy ERS platform (second platform instance).
# Mirrors additional-KAS pattern: always up (when its template config is
# available), dedicated port, per-instance fixtures in xtest. Existing
# tests that don't reference the ers-ms fixtures are unaffected.
if start_platform:
template_path = settings.platform_ers_ms_template_config
if not template_path.exists():
print_warning(
f"Multi-strategy platform template not found at {template_path}; "
"skipping ers-ms platform startup."
)
else:
print_info("Starting multi-strategy ERS platform...")
platform_ers_ms = get_platform_ers_ms_service(settings)
if not platform_ers_ms.start():
print_error("Failed to start multi-strategy ERS platform")
raise typer.Exit(1)

with status_spinner("Waiting for multi-strategy ERS platform..."):
try:
wait_for_health(
f"http://localhost:{Ports.PLATFORM_ERS_MS}/healthz",
timeout=120,
service_name="Platform (ERS-MS)",
)
except WaitTimeoutError as e:
print_warning(str(e))
print_success("Multi-strategy ERS platform is ready")

# Step 4: Start KAS instances
if start_kas:
print_info("Starting KAS instances...")
Expand Down Expand Up @@ -227,6 +267,11 @@ def down(
kas_manager = get_kas_manager(settings)
kas_manager.stop_all()

# Stop multi-strategy ERS platform
print_info("Stopping multi-strategy ERS platform...")
platform_ers_ms = get_platform_ers_ms_service(settings)
platform_ers_ms.stop()

# Stop Platform
print_info("Stopping Platform...")
platform = get_platform_service(settings)
Expand Down Expand Up @@ -266,6 +311,7 @@ def list_services(
all_info = []
all_info.extend(docker.get_all_info())
all_info.append(platform.get_info())
all_info.append(get_platform_ers_ms_service(settings).get_info())
all_info.extend(kas_manager.get_all_info())

# Filter if not showing all
Expand Down Expand Up @@ -370,6 +416,7 @@ def logs(

# Add all services to aggregator
aggregator.add_service("platform")
aggregator.add_service("platform-ers-ms")
for kas_name in Ports.all_kas_names():
aggregator.add_service(f"kas-{kas_name}")

Expand Down Expand Up @@ -493,6 +540,13 @@ def restart(
print_success("Platform restarted")
return

if service == "platform-ers-ms":
print_info("Restarting multi-strategy ERS platform...")
platform_ers_ms = get_platform_ers_ms_service(settings)
platform_ers_ms.restart()
print_success("Multi-strategy ERS platform restarted")
return

if service.startswith("kas-"):
kas_name = service[4:] # Remove "kas-" prefix
if kas_name not in Ports.all_kas_names():
Expand Down Expand Up @@ -523,7 +577,8 @@ def restart(

print_error(f"Unknown service: {service}")
print_info(
"Valid services: docker, platform, kas-alpha, kas-beta, kas-gamma, kas-delta, kas-km1, kas-km2"
"Valid services: docker, platform, platform-ers-ms, "
"kas-alpha, kas-beta, kas-gamma, kas-delta, kas-km1, kas-km2"
)
raise typer.Exit(1)

Expand Down Expand Up @@ -558,6 +613,7 @@ def env(

# Platform configuration
env_vars["PLATFORMURL"] = settings.platform_url
env_vars["PLATFORMURL_ERS_MS"] = f"http://localhost:{Ports.PLATFORM_ERS_MS}"
env_vars["PLATFORM_DIR"] = str(settings.platform_dir.resolve())

# Schema file for manifest validation
Expand All @@ -570,6 +626,10 @@ def env(
if platform_log.exists():
env_vars["PLATFORM_LOG_FILE"] = str(platform_log.resolve())

platform_ers_ms_log = settings.platform_ers_ms_log_path
if platform_ers_ms_log.exists():
env_vars["PLATFORM_ERS_MS_LOG_FILE"] = str(platform_ers_ms_log.resolve())

# KAS log files
kas_env_mapping = {
"alpha": "KAS_ALPHA_LOG_FILE",
Expand Down
4 changes: 4 additions & 0 deletions otdf-local/src/otdf_local/config/ports.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ class Ports:

# Platform
PLATFORM: int = 8080
PLATFORM_ERS_MS: int = 8090

# ERS test Postgres (docker profile 'ers-test'), used by multi-strategy platform
ERS_POSTGRES: int = 5433

# KAS instances
KAS_ALPHA: int = 8181
Expand Down
15 changes: 15 additions & 0 deletions otdf-local/src/otdf_local/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,21 @@ def kas_template_config(self) -> Path:
"""KAS config template path."""
return self.platform_dir / "opentdf-kas-mode.yaml"

@property
def platform_ers_ms_template_config(self) -> Path:
"""Multi-strategy ERS platform config template path (in xtest repo)."""
return self.xtest_root / "platform-configs" / "opentdf-multistrategy.yaml"

@property
def platform_ers_ms_config(self) -> Path:
"""Generated multi-strategy ERS platform config path."""
return self.config_dir / "opentdf-ers-ms.yaml"

@property
def platform_ers_ms_log_path(self) -> Path:
"""Log file path for the multi-strategy ERS platform instance."""
return self.logs_dir / "platform-ers-ms.log"

@property
def docker_compose_file(self) -> Path:
"""Docker compose file path."""
Expand Down
6 changes: 6 additions & 0 deletions otdf-local/src/otdf_local/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
from otdf_local.services.docker import DockerService, get_docker_service
from otdf_local.services.kas import KASManager, KASService, get_kas_manager
from otdf_local.services.platform import PlatformService, get_platform_service
from otdf_local.services.platform_ers_ms import (
PlatformERSMultiStrategyService,
get_platform_ers_ms_service,
)
from otdf_local.services.provisioner import (
Provisioner,
ProvisionResult,
Expand All @@ -21,6 +25,8 @@
"get_kas_manager",
"PlatformService",
"get_platform_service",
"PlatformERSMultiStrategyService",
"get_platform_ers_ms_service",
"Provisioner",
"ProvisionResult",
"get_provisioner",
Expand Down
Loading
Loading