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
13 changes: 11 additions & 2 deletions control_plane/drivers/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -532,14 +532,14 @@ def _route_alias(
"Create, refresh, inventory, and destroy Odoo PR previews through the "
"generic-web preview lifecycle."
),
actions=("preview_apply", "preview_verification"),
actions=("preview_apply_inputs", "preview_apply", "preview_verification"),
panels=("preview_inventory", "deployment_evidence", "audit"),
),
DriverCapabilityDescriptor(
capability_id="preview_inventory_managed",
label="Preview inventory managed",
description="Read provider inventory and reconcile current Odoo preview state.",
actions=("preview_apply",),
actions=("preview_apply_inputs", "preview_apply"),
panels=("preview_inventory", "deployment_evidence", "audit"),
),
),
Expand Down Expand Up @@ -603,6 +603,15 @@ def _route_alias(
authz_action="odoo_stable_bootstrap.execute",
writes_records=("deployment", "inventory"),
),
_action(
"preview_apply_inputs",
"Resolve preview apply inputs",
"Resolve Launchplane-owned Odoo preview runtime and provider dry-run inputs.",
safety="read",
scope="preview",
route_path="/v1/drivers/odoo/preview-apply-inputs",
authz_action="odoo_preview_apply_inputs.read",
),
_action(
"preview_apply",
"Apply isolated preview",
Expand Down
80 changes: 78 additions & 2 deletions control_plane/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from socketserver import ThreadingMixIn
import uuid
from pathlib import Path
from typing import BinaryIO, Callable, Generic, Iterable, Literal, Protocol, TypeVar, cast
from typing import Any, BinaryIO, Callable, Generic, Iterable, Literal, Protocol, TypeVar, cast
from urllib.parse import parse_qs
from wsgiref.simple_server import WSGIServer, make_server
from wsgiref.types import WSGIApplication
Expand Down Expand Up @@ -314,7 +314,9 @@
)
from control_plane.workflows.odoo_preview_runtime import (
ODOO_PREVIEW_REQUIRED_ENV_KEYS,
OdooPreviewApplyInputsRequest,
OdooPreviewDokployApplyRequest,
build_odoo_preview_apply_inputs,
execute_odoo_preview_dokploy_apply,
)
from control_plane.workflows.product_onboarding import apply_product_onboarding_manifest
Expand Down Expand Up @@ -1233,6 +1235,18 @@ def _validate_alignment(self) -> "OdooPreviewApplyEnvelope":
return self


class OdooPreviewApplyInputsEnvelope(_ProductRouteEnvelope):
schema_version: int = Field(default=1, ge=1)
inputs: OdooPreviewApplyInputsRequest

@model_validator(mode="after")
def _validate_alignment(self) -> "OdooPreviewApplyInputsEnvelope":
_validate_driver_envelope_product(self.product, label="Odoo preview apply inputs")
if self.product.strip() != self.inputs.product.strip():
raise ValueError("Odoo preview apply inputs require matching product values.")
return self


class OdooPreviewApplyConfigError(click.ClickException):
def __init__(self, *, context: str, instance: str, missing_keys: tuple[str, ...]) -> None:
super().__init__("Odoo preview apply runtime environment is incomplete.")
Expand Down Expand Up @@ -1300,13 +1314,38 @@ def _odoo_preview_identifier(value: str, *, suffix: str) -> str:
return f"{normalized}_{suffix_identifier}" if suffix_identifier else normalized


def _odoo_preview_apply_inputs_response_result(
*,
control_plane_root: Path,
record_store: object,
profile: LaunchplaneProductProfileRecord,
request: OdooPreviewApplyInputsRequest,
database_url: str | None,
) -> dict[str, object]:
driver_result = build_odoo_preview_apply_inputs(
control_plane_root=control_plane_root,
record_store=cast(Any, record_store),
profile=profile,
request=request,
database_url=database_url,
)
return driver_result.model_dump(mode="json")


_ODOO_PREVIEW_APPLY_ROUTE = _DriverRouteExecutionMetadata(
route_path="/v1/drivers/odoo/preview-apply",
envelope_model=OdooPreviewApplyEnvelope,
denial_message="Workflow cannot apply Odoo preview provider state for the requested product/context.",
)


_ODOO_PREVIEW_APPLY_INPUTS_ROUTE = _DriverRouteExecutionMetadata(
route_path="/v1/drivers/odoo/preview-apply-inputs",
envelope_model=OdooPreviewApplyInputsEnvelope,
denial_message="Workflow cannot read Odoo preview apply inputs for the requested product/context.",
)


_PREVIEW_DESIRED_STATE_ROUTE_PATHS = frozenset(
{
_GENERIC_WEB_PREVIEW_DESIRED_STATE_ROUTE.route_path,
Expand Down Expand Up @@ -1899,7 +1938,9 @@ def _validate_alignment(self) -> "OdooPreviewVerificationEnvelope":
}
)
_GENERIC_WEB_BASE_DRIVER_PREVIEW_ROUTE_PATHS = frozenset(
_GENERIC_WEB_BASE_DRIVER_PREVIEW_ROUTE_PATHS | _PREVIEW_VERIFICATION_ROUTE_PATHS
_GENERIC_WEB_BASE_DRIVER_PREVIEW_ROUTE_PATHS
| _PREVIEW_VERIFICATION_ROUTE_PATHS
| {_ODOO_PREVIEW_APPLY_INPUTS_ROUTE.route_path}
)
_GENERIC_WEB_BASE_DRIVER_ROUTE_PATHS = frozenset(
_GENERIC_WEB_BASE_DRIVER_SHARED_ROUTE_PATHS | _GENERIC_WEB_BASE_DRIVER_PREVIEW_ROUTE_PATHS
Expand Down Expand Up @@ -13168,6 +13209,41 @@ def product_action_allowed(
database_url=database_url,
)
result = driver_result.model_dump(mode="json")
elif path == _ODOO_PREVIEW_APPLY_INPUTS_ROUTE.route_path:
odoo_preview_inputs_request = (
_ODOO_PREVIEW_APPLY_INPUTS_ROUTE.envelope_model.model_validate(payload)
)
resolved_driver_context = _resolve_descriptor_product_driver_context(
record_store=record_store,
route_path=path,
product=odoo_preview_inputs_request.product,
require_profile=True,
)
if resolved_driver_context.profile is None:
raise ProductDriverMismatchError(
"Odoo preview apply inputs require a product profile."
)
authorization_context = resolved_driver_context.profile.preview.context
authorization_response = _driver_route_authorization_response(
authz_policy=authz_policy,
identity=identity,
route_path=path,
product=odoo_preview_inputs_request.product,
context=authorization_context,
denial_message=_ODOO_PREVIEW_APPLY_INPUTS_ROUTE.denial_message,
start_response=start_response,
trace_id=request_trace_id,
)
if authorization_response is not None:
return authorization_response
driver_result = _odoo_preview_apply_inputs_response_result(
control_plane_root=resolved_root,
record_store=record_store,
profile=resolved_driver_context.profile,
request=odoo_preview_inputs_request.inputs,
database_url=database_url,
)
result = driver_result
elif path == _ODOO_STABLE_VERIFICATION_ROUTE.route_path:
odoo_stable_verification_request = (
_ODOO_STABLE_VERIFICATION_ROUTE.envelope_model.model_validate(payload)
Expand Down
17 changes: 7 additions & 10 deletions control_plane/workflows/generic_web_preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from pathlib import Path
from typing import Iterator, Literal, Protocol, runtime_checkable
from urllib.error import HTTPError, URLError
from urllib.parse import urlparse, urlunparse
from urllib.parse import urlparse
from urllib.request import Request, urlopen

import click
Expand Down Expand Up @@ -739,29 +739,26 @@ def _preview_url_from_base_url(*, preview_slug: str, preview_base_url: str) -> s
raise click.ClickException(
f"{_PREVIEW_BASE_URL_ENV_KEY} must be a root URL without path, query, or fragment."
)
return urlunparse(
parsed._replace(
netloc=f"{preview_slug.strip()}.{parsed.hostname}",
path="",
params="",
query="",
fragment="",
)
)
host = f"{preview_slug.strip()}.{parsed.hostname}"
if parsed.port:
host = f"{host}:{parsed.port}"
return f"{parsed.scheme}://{host}"


def resolve_generic_web_preview_url(
*,
control_plane_root: Path,
profile: LaunchplaneProductProfileRecord,
request: GenericWebPreviewRefreshRequest,
database_url: str | None = None,
) -> str:
explicit_preview_url = request.preview_url.strip()
if explicit_preview_url:
return explicit_preview_url
context_values = control_plane_runtime_environments.resolve_runtime_context_values(
control_plane_root=control_plane_root,
context_name=profile.preview.context,
database_url=database_url,
)
preview_base_url = str(context_values.get(_PREVIEW_BASE_URL_ENV_KEY) or "").strip()
if not preview_base_url:
Expand Down
Loading