Skip to content
Merged
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
83 changes: 74 additions & 9 deletions control_plane/product_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,11 +167,32 @@ def apply_product_config_bundle(
actor=actor,
source_label=source_label,
)
secret_id = str(result["secret_id"])
binding_key = str(secret["binding_key"])
now = utc_now_timestamp()
_retire_disabled_runtime_secret_placeholders(
record_store=record_store,
configured_binding=SecretBinding(
binding_id=control_plane_secrets.expected_secret_binding_id(
secret_id=secret_id,
binding_key=binding_key,
),
secret_id=secret_id,
integration=str(secret["integration"]),
binding_key=binding_key,
context=str(secret["context"]),
instance=str(secret["instance"]),
status="configured",
created_at=now,
updated_at=now,
),
updated_at=now,
)
secret_summaries.append(
_summarize_product_config_secret_input(
action=str(result["action"]),
secret=secret,
secret_id=str(result["secret_id"]),
secret_id=secret_id,
)
)
continue
Expand Down Expand Up @@ -509,13 +530,11 @@ def _planned_runtime_secret_bindings(
record_store: ProductConfigStore,
secrets: tuple[dict[str, object], ...],
) -> tuple[SecretBinding, ...]:
existing_bindings = {
binding.binding_id: binding
for binding in record_store.list_secret_bindings(
integration=control_plane_secrets.RUNTIME_ENVIRONMENT_SECRET_INTEGRATION,
limit=None,
)
}
existing_runtime_bindings = record_store.list_secret_bindings(
integration=control_plane_secrets.RUNTIME_ENVIRONMENT_SECRET_INTEGRATION,
limit=None,
)
existing_bindings = {binding.binding_id: binding for binding in existing_runtime_bindings}
planned_bindings: list[SecretBinding] = []
for secret in secrets:
existing_record = record_store.find_secret_record(
Expand Down Expand Up @@ -554,7 +573,53 @@ def _planned_runtime_secret_bindings(
updated_at=now,
)
)
return tuple(planned_bindings)
planned_binding_ids = {binding.binding_id for binding in planned_bindings}
planned_routes = {
(binding.integration, binding.binding_key, binding.context, binding.instance)
for binding in planned_bindings
}
configured_existing_bindings = tuple(
binding
for binding in existing_runtime_bindings
if binding.status == "configured"
and binding.binding_id not in planned_binding_ids
and (binding.integration, binding.binding_key, binding.context, binding.instance)
in planned_routes
)
return (*planned_bindings, *configured_existing_bindings)


def _retire_disabled_runtime_secret_placeholders(
*,
record_store: ProductConfigStore,
configured_binding: SecretBinding,
updated_at: str,
) -> None:
if (
configured_binding.integration
!= control_plane_secrets.RUNTIME_ENVIRONMENT_SECRET_INTEGRATION
):
return
for binding in record_store.list_secret_bindings(
integration=configured_binding.integration,
context_name=configured_binding.context,
instance_name=configured_binding.instance,
limit=None,
):
if binding.binding_id == configured_binding.binding_id:
continue
if binding.binding_key != configured_binding.binding_key:
continue
if binding.status != "disabled":
continue
record_store.write_secret_binding(
binding.model_copy(
update={
"integration": f"retired:{binding.integration}",
"updated_at": updated_at,
}
)
)


def _product_config_runtime_environment_class(
Expand Down
73 changes: 56 additions & 17 deletions control_plane/workflows/product_onboarding.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ def list_physical_provider_target_records(self) -> tuple[ProviderTargetRecord, .

def write_runtime_environment_record(self, record: RuntimeEnvironmentRecord) -> None: ...

def list_secret_bindings(
self,
*,
integration: str = "",
context_name: str = "",
instance_name: str = "",
limit: int | None = None,
) -> tuple[SecretBinding, ...]: ...

def write_secret_binding(self, binding: SecretBinding) -> None: ...


Expand Down Expand Up @@ -155,7 +164,11 @@ def build_physical_provider_target_records(
target_id_record=target_id_record,
)
for target_record in provider_targets
if (target_id_record := target_ids_by_route.get((target_record.context, target_record.instance)))
if (
target_id_record := target_ids_by_route.get(
(target_record.context, target_record.instance)
)
)
is not None
)

Expand All @@ -177,22 +190,41 @@ def build_runtime_environment_records(


def build_secret_bindings(
*, manifest: ProductOnboardingManifest, updated_at: str
*,
manifest: ProductOnboardingManifest,
updated_at: str,
existing_bindings: tuple[SecretBinding, ...] = (),
) -> tuple[SecretBinding, ...]:
return tuple(
SecretBinding(
binding_id=_secret_binding_id(product=manifest.product, binding=binding),
secret_id=_secret_id(product=manifest.product, binding=binding),
integration=binding.integration,
binding_key=binding.binding_key,
context=binding.context,
instance=binding.instance,
status=binding.status,
created_at=updated_at,
updated_at=updated_at,
existing_bindings_by_id = {binding.binding_id: binding for binding in existing_bindings}
configured_routes = {
(binding.integration, binding.binding_key, binding.context, binding.instance)
for binding in existing_bindings
if binding.status == "configured"
}
secret_bindings: list[SecretBinding] = []
for binding in manifest.secret_bindings:
if (
binding.status != "configured"
and (binding.integration, binding.binding_key, binding.context, binding.instance)
in configured_routes
):
continue
binding_id = _secret_binding_id(product=manifest.product, binding=binding)
existing_binding = existing_bindings_by_id.get(binding_id)
secret_bindings.append(
SecretBinding(
binding_id=binding_id,
secret_id=_secret_id(product=manifest.product, binding=binding),
integration=binding.integration,
binding_key=binding.binding_key,
context=binding.context,
instance=binding.instance,
status=binding.status,
created_at=existing_binding.created_at if existing_binding else updated_at,
updated_at=updated_at,
)
)
for binding in manifest.secret_bindings
)
return tuple(secret_bindings)


def apply_product_onboarding_manifest(
Expand All @@ -214,15 +246,22 @@ def apply_product_onboarding_manifest(
runtime_environments = build_runtime_environment_records(
manifest=manifest, updated_at=recorded_at
)
secret_bindings = build_secret_bindings(manifest=manifest, updated_at=recorded_at)
secret_bindings = build_secret_bindings(
manifest=manifest,
updated_at=recorded_at,
existing_bindings=record_store.list_secret_bindings(limit=None),
)
target_records_by_route = {
(record.context, record.instance): record for record in provider_targets
}
target_id_records_by_route = {
(record.context, record.instance): record for record in provider_target_ids
}
provider_target_pairs = tuple(
(target_records_by_route[(record.context, record.instance)], target_id_records_by_route[(record.context, record.instance)])
(
target_records_by_route[(record.context, record.instance)],
target_id_records_by_route[(record.context, record.instance)],
)
for record in physical_provider_targets
)
for target_record, target_id_record in provider_target_pairs:
Expand Down
6 changes: 6 additions & 0 deletions docs/secrets.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,12 @@ title: Secrets
previously recorded matching dry-run. They must still send plaintext secret
values only in the request body over the Launchplane service API. Do not copy
those request bodies into logs, GitHub issues, PR bodies, or docs.
- Product onboarding may create disabled managed-secret binding placeholders for
expected runtime secrets. Once product-config writes the configured managed
secret for the same integration, binding key, context, and instance, Launchplane
retires the disabled placeholder from active runtime-secret lookups. Later
onboarding imports preserve the configured binding instead of recreating the
disabled placeholder.
- `uv run launchplane environments unset --scope <scope> --key KEY` removes
stale keys from DB-backed runtime-environment records without reading or
printing plaintext values.
Expand Down
46 changes: 45 additions & 1 deletion tests/test_product_onboarding.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from control_plane.cli import main
from control_plane.contracts.deploy_target import ProviderTargetRecord
from control_plane.contracts.product_onboarding_manifest import ProductOnboardingManifest
from control_plane.contracts.secret_record import SecretBinding
from control_plane.storage.postgres import PostgresRecordStore
from control_plane.workflows.product_onboarding import apply_product_onboarding_manifest

Expand Down Expand Up @@ -1497,6 +1498,7 @@ def test_apply_product_onboarding_manifest_writes_canonical_records(self) -> Non
second_result = apply_product_onboarding_manifest(
record_store=store,
manifest=manifest,
updated_at="2026-05-03T02:30:00Z",
)

profile = store.read_product_profile_record("example-site")
Expand All @@ -1508,7 +1510,7 @@ def test_apply_product_onboarding_manifest_writes_canonical_records(self) -> Non
store.close()

self.assertEqual(first_result.product, "example-site")
self.assertEqual(second_result.product_profile.updated_at, "2026-05-03T01:30:00Z")
self.assertEqual(second_result.product_profile.updated_at, "2026-05-03T02:30:00Z")
self.assertEqual(profile.driver_id, "generic-web")
self.assertEqual(profile.lanes[0].health_url, "https://testing.example.invalid/api/health")
self.assertTrue(profile.lanes[0].odoo_stable_bootstrap.enabled)
Expand Down Expand Up @@ -1561,6 +1563,48 @@ def test_apply_product_onboarding_manifest_writes_canonical_records(self) -> Non
self.assertEqual(len(secret_bindings), 1)
self.assertEqual(secret_bindings[0].binding_key, "SMTP_PASSWORD")
self.assertEqual(secret_bindings[0].status, "disabled")
self.assertEqual(secret_bindings[0].created_at, first_result.secret_bindings[0].created_at)
self.assertEqual(secret_bindings[0].updated_at, second_result.secret_bindings[0].updated_at)

def test_apply_product_onboarding_manifest_preserves_configured_secret_binding(
self,
) -> None:
with TemporaryDirectory() as temporary_directory_name:
store = PostgresRecordStore(
database_url=_sqlite_database_url(Path(temporary_directory_name) / "db.sqlite3")
)
store.ensure_schema()
manifest = ProductOnboardingManifest.model_validate(_manifest_payload())
store.write_secret_binding(
SecretBinding(
binding_id="secret-runtime-environment-smtp-password-example-site-prod-prod-binding-smtp-password",
secret_id="secret-runtime-environment-smtp-password-example-site-prod-prod",
integration="runtime_environment",
binding_key="SMTP_PASSWORD",
context="example-site-prod",
instance="prod",
status="configured",
created_at="2026-05-03T00:30:00Z",
updated_at="2026-05-03T00:30:00Z",
)
)

result = apply_product_onboarding_manifest(
record_store=store,
manifest=manifest,
)

secret_bindings = store.list_secret_bindings(
integration="runtime_environment",
context_name="example-site-prod",
instance_name="prod",
)
store.close()

self.assertEqual(result.secret_bindings, ())
self.assertEqual(len(secret_bindings), 1)
self.assertEqual(secret_bindings[0].binding_key, "SMTP_PASSWORD")
self.assertEqual(secret_bindings[0].status, "configured")

def test_apply_product_onboarding_manifest_blocks_conflicting_provider_target(
self,
Expand Down
17 changes: 17 additions & 0 deletions tests/test_product_onboarding_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,23 @@ def write_provider_target_record(self, record: ProviderTargetRecord) -> None:
def write_runtime_environment_record(self, record: RuntimeEnvironmentRecord) -> None:
self.runtime_environments.append(record)

def list_secret_bindings(
self,
*,
integration: str = "",
context_name: str = "",
instance_name: str = "",
limit: int | None = None,
) -> tuple[SecretBinding, ...]:
bindings = tuple(
binding
for binding in self.secret_bindings
if (not integration or binding.integration == integration)
and (not context_name or binding.context == context_name)
and (not instance_name or binding.instance == instance_name)
)
return bindings[:limit] if limit is not None else bindings

def write_secret_binding(self, binding: SecretBinding) -> None:
self.secret_bindings.append(binding)

Expand Down
Loading