From 57a3843351a8d87bed819f869669bdf45be7a78a Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Thu, 3 Sep 2026 11:39:48 -0400 Subject: [PATCH 01/15] feat(api): support managed custom-domain TLS --- features/contract/managed_tls.feature | 8 ++ features/steps/sdk_contract_steps.py | 75 +++++++++++ openapi/openapi.yaml | 125 ++++++++++++++++-- src/volcano_sdk/_generated/models/__init__.py | 18 +++ .../byoc_frontend_custom_domain_tls_config.py | 91 +++++++++++++ ..._frontend_custom_domain_tls_config_mode.py | 10 ++ ...onfig_frontend_custom_domain_tls_config.py | 92 +++++++++++++ ..._frontend_custom_domain_tls_config_mode.py | 10 ++ .../create_frontend_custom_domain_request.py | 59 +++++---- ...tom_domain_response_verification_status.py | 4 +- .../frontend_custom_domain_tls_config.py | 11 +- .../models/frontend_domain_routing_record.py | 37 +++--- ...in_routing_record_zone_apex_record_type.py | 10 ++ .../frontend_domain_verification_record.py | 20 +-- ...naged_frontend_custom_domain_tls_config.py | 65 +++++++++ ..._frontend_custom_domain_tls_config_mode.py | 10 ++ ...onfig_frontend_custom_domain_tls_config.py | 64 +++++++++ ..._frontend_custom_domain_tls_config_mode.py | 10 ++ .../models/project_config_custom_domain.py | 57 +++++--- .../models/project_config_frontend.py | 10 +- tests/unit/test_contract_bindings.py | 7 + tests/unit/test_managed_tls_contract.py | 95 +++++++++++++ 22 files changed, 782 insertions(+), 106 deletions(-) create mode 100644 features/contract/managed_tls.feature create mode 100644 src/volcano_sdk/_generated/models/byoc_frontend_custom_domain_tls_config.py create mode 100644 src/volcano_sdk/_generated/models/byoc_frontend_custom_domain_tls_config_mode.py create mode 100644 src/volcano_sdk/_generated/models/byoc_project_config_frontend_custom_domain_tls_config.py create mode 100644 src/volcano_sdk/_generated/models/byoc_project_config_frontend_custom_domain_tls_config_mode.py create mode 100644 src/volcano_sdk/_generated/models/frontend_domain_routing_record_zone_apex_record_type.py create mode 100644 src/volcano_sdk/_generated/models/managed_frontend_custom_domain_tls_config.py create mode 100644 src/volcano_sdk/_generated/models/managed_frontend_custom_domain_tls_config_mode.py create mode 100644 src/volcano_sdk/_generated/models/managed_project_config_frontend_custom_domain_tls_config.py create mode 100644 src/volcano_sdk/_generated/models/managed_project_config_frontend_custom_domain_tls_config_mode.py create mode 100644 tests/unit/test_managed_tls_contract.py diff --git a/features/contract/managed_tls.feature b/features/contract/managed_tls.feature new file mode 100644 index 00000000..35e7a05d --- /dev/null +++ b/features/contract/managed_tls.feature @@ -0,0 +1,8 @@ +Feature: Managed custom-domain TLS contract + + @managed-tls @TLS-MANAGED-001 + Scenario: Managed TLS uses an explicit secret-free request and DNS lifecycle response + Given a managed custom-domain TLS request + When the client encodes the request and decodes a pending verification response + Then the request selects managed TLS without certificate material + And the response exposes the managed lifecycle and DNS records diff --git a/features/steps/sdk_contract_steps.py b/features/steps/sdk_contract_steps.py index 5b041874..d9c34b54 100644 --- a/features/steps/sdk_contract_steps.py +++ b/features/steps/sdk_contract_steps.py @@ -13,6 +13,15 @@ ) from volcano_sdk import VolcanoClient +from volcano_sdk._generated.models.create_frontend_custom_domain_request import ( + CreateFrontendCustomDomainRequest, +) +from volcano_sdk._generated.models.frontend_custom_domain_response import ( + FrontendCustomDomainResponse, +) +from volcano_sdk._generated.models.managed_frontend_custom_domain_tls_config import ( + ManagedFrontendCustomDomainTLSConfig, +) ACCESS_TOKEN_CLOCK_TICK_SECONDS = 1.1 @@ -395,3 +404,69 @@ def subscriber_received_message(context: Any) -> None: world = _world(context) assert world.last_outcome is not None assert world.last_outcome.value == world.realtime_message + + +@given("a managed custom-domain TLS request") +def managed_tls_request(context: Any) -> None: + _world(context).managed_tls_request = CreateFrontendCustomDomainRequest( + domain="app.example.com", + tls=ManagedFrontendCustomDomainTLSConfig(mode="managed"), + ) + + +@when("the client encodes the request and decodes a pending verification response") +def encode_managed_tls_request(context: Any) -> None: + world = _world(context) + world.managed_tls_wire_request = world.managed_tls_request.to_dict() + world.managed_tls_response = FrontendCustomDomainResponse.from_dict( + { + "domain": "app.example.com", + "tls_mode": "managed", + "domain_status": "pending_verification", + "verification_status": "pending", + "verification_records": [ + { + "name": "_token.app.example.com", + "type": "CNAME", + "value": "_validation.volcano.dev", + } + ], + "required_routing_record": { + "record_type": "CNAME", + "zone_apex_record_type": "ALIAS", + "name": "app.example.com", + "value": "frontend.frontends.volcano.dev", + }, + "effective_urls": ["https://frontend.frontends.volcano.dev/"], + "created_at": "2026-09-02T12:00:00Z", + "updated_at": "2026-09-02T12:00:00Z", + } + ) + + +@then("the request selects managed TLS without certificate material") +def managed_tls_request_has_no_certificate(context: Any) -> None: + assert _world(context).managed_tls_wire_request == { + "domain": "app.example.com", + "tls": {"mode": "managed"}, + } + + +@then("the response exposes the managed lifecycle and DNS records") +def managed_tls_response_has_lifecycle(context: Any) -> None: + response = _world(context).managed_tls_response + assert response.domain == "app.example.com" + assert response.tls_mode == "managed" + assert response.domain_status == "pending_verification" + assert response.verification_status == "pending" + assert response.verification_records[0].to_dict() == { + "name": "_token.app.example.com", + "type": "CNAME", + "value": "_validation.volcano.dev", + } + assert response.required_routing_record.to_dict() == { + "record_type": "CNAME", + "zone_apex_record_type": "ALIAS", + "name": "app.example.com", + "value": "frontend.frontends.volcano.dev", + } diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 0ce8792e..57fc70e6 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -10993,13 +10993,14 @@ components: - text_body CreateFrontendCustomDomainRequest: type: object + additionalProperties: false properties: domain: type: string description: Fully-qualified domain name (hostname only, no scheme/path) example: app.example.com tls: - $ref: '#/components/schemas/FrontendCustomDomainTLSConfig' + $ref: '#/components/schemas/CreateFrontendCustomDomainTLSConfig' required: - domain - tls @@ -12492,6 +12493,7 @@ components: enum: - pending - verified + - failed verification_records: type: array items: @@ -12516,15 +12518,35 @@ components: - effective_urls - created_at - updated_at - FrontendCustomDomainTLSConfig: + CreateFrontendCustomDomainTLSConfig: + oneOf: + - $ref: '#/components/schemas/ManagedFrontendCustomDomainTLSConfig' + - $ref: '#/components/schemas/BYOCFrontendCustomDomainTLSConfig' + discriminator: + propertyName: mode + mapping: + managed: '#/components/schemas/ManagedFrontendCustomDomainTLSConfig' + byoc: '#/components/schemas/BYOCFrontendCustomDomainTLSConfig' + ManagedFrontendCustomDomainTLSConfig: + type: object + description: Volcano issues and renews the certificate. Do not send certificate material. + additionalProperties: false + properties: + mode: + type: string + enum: + - managed + required: + - mode + BYOCFrontendCustomDomainTLSConfig: type: object + description: Use certificate material that you manage. + additionalProperties: false properties: mode: type: string enum: - byoc - default: byoc - description: BYOC is mandatory for custom domain creation. certificate_pem: type: string description: Required. PEM-encoded certificate. @@ -12538,6 +12560,82 @@ components: - mode - certificate_pem - private_key_pem + FrontendCustomDomainTLSConfig: + type: object + deprecated: true + description: Deprecated compatibility model. Use BYOCFrontendCustomDomainTLSConfig. + properties: + mode: + type: string + enum: + - byoc + default: byoc + certificate_pem: + type: string + private_key_pem: + type: string + certificate_chain_pem: + type: string + required: + - mode + - certificate_pem + - private_key_pem + ProjectConfigFrontendCustomDomainTLSConfig: + oneOf: + - $ref: '#/components/schemas/ManagedProjectConfigFrontendCustomDomainTLSConfig' + - $ref: '#/components/schemas/BYOCProjectConfigFrontendCustomDomainTLSConfig' + discriminator: + propertyName: mode + mapping: + managed: '#/components/schemas/ManagedProjectConfigFrontendCustomDomainTLSConfig' + byoc: '#/components/schemas/BYOCProjectConfigFrontendCustomDomainTLSConfig' + ManagedProjectConfigFrontendCustomDomainTLSConfig: + type: object + additionalProperties: false + properties: + mode: + type: string + enum: + - managed + required: + - mode + BYOCProjectConfigFrontendCustomDomainTLSConfig: + type: object + additionalProperties: false + not: + anyOf: + - required: + - certificate_pem + not: + required: + - private_key_pem + - required: + - private_key_pem + not: + required: + - certificate_pem + - required: + - certificate_chain_pem + not: + required: + - certificate_pem + - private_key_pem + properties: + mode: + type: string + enum: + - byoc + certificate_pem: + type: string + description: PEM-encoded certificate for BYOC create or rotation. Omitted from exports. + private_key_pem: + type: string + description: PEM-encoded private key for BYOC create or rotation. Omitted from exports. + certificate_chain_pem: + type: string + description: Optional PEM-encoded certificate chain for BYOC. Omitted from exports. + required: + - mode FrontendDeployment: type: object properties: @@ -12627,21 +12725,30 @@ components: - updated_at FrontendDomainRoutingRecord: type: object + additionalProperties: false properties: record_type: type: string + description: Use this record type when the hostname is not the apex of your DNS zone. enum: - CNAME + zone_apex_record_type: + type: string + description: At the apex of your DNS zone, use your provider's ALIAS, ANAME, or CNAME-flattening equivalent instead of a literal CNAME. + enum: + - ALIAS name: type: string value: type: string required: - record_type + - zone_apex_record_type - name - value FrontendDomainVerificationRecord: type: object + additionalProperties: false properties: name: type: string @@ -14274,17 +14381,15 @@ components: type: object additionalProperties: false description: | - Custom domain with BYOC TLS (PRO plan). `tls` is required when the - domain is first created and optional afterwards: providing new TLS - material for the same domain rotates the certificate in place (zero - downtime); omitting `tls` keeps the stored certificate. TLS material is - write-only and omitted from config export. + Custom domain with managed or BYOC TLS (PRO plan). `tls` is required + when the domain is first created and optional afterwards. BYOC TLS + material is write-only and omitted from config export. properties: domain: type: string description: Fully-qualified domain name (hostname only, no scheme/path) tls: - $ref: '#/components/schemas/FrontendCustomDomainTLSConfig' + $ref: '#/components/schemas/ProjectConfigFrontendCustomDomainTLSConfig' required: - domain ProjectConfigDatabase: diff --git a/src/volcano_sdk/_generated/models/__init__.py b/src/volcano_sdk/_generated/models/__init__.py index d8c93208..409f2413 100644 --- a/src/volcano_sdk/_generated/models/__init__.py +++ b/src/volcano_sdk/_generated/models/__init__.py @@ -83,6 +83,10 @@ from .batch_function_deploy_failure import BatchFunctionDeployFailure from .batch_function_deploy_failure_operation import BatchFunctionDeployFailureOperation from .batch_function_deploy_response import BatchFunctionDeployResponse +from .byoc_frontend_custom_domain_tls_config import BYOCFrontendCustomDomainTLSConfig +from .byoc_frontend_custom_domain_tls_config_mode import BYOCFrontendCustomDomainTLSConfigMode +from .byoc_project_config_frontend_custom_domain_tls_config import BYOCProjectConfigFrontendCustomDomainTLSConfig +from .byoc_project_config_frontend_custom_domain_tls_config_mode import BYOCProjectConfigFrontendCustomDomainTLSConfigMode from .call_o_auth_provider_api_body import CallOAuthProviderAPIBody from .call_o_auth_provider_api_body_body import CallOAuthProviderAPIBodyBody from .call_o_auth_provider_api_body_method import CallOAuthProviderAPIBodyMethod @@ -171,6 +175,7 @@ from .frontend_deployment_status import FrontendDeploymentStatus from .frontend_domain_routing_record import FrontendDomainRoutingRecord from .frontend_domain_routing_record_record_type import FrontendDomainRoutingRecordRecordType +from .frontend_domain_routing_record_zone_apex_record_type import FrontendDomainRoutingRecordZoneApexRecordType from .frontend_domain_verification_record import FrontendDomainVerificationRecord from .frontend_framework import FrontendFramework from .frontend_status import FrontendStatus @@ -266,6 +271,10 @@ from .log_search_request import LogSearchRequest from .log_search_response import LogSearchResponse from .log_stream_request import LogStreamRequest +from .managed_frontend_custom_domain_tls_config import ManagedFrontendCustomDomainTLSConfig +from .managed_frontend_custom_domain_tls_config_mode import ManagedFrontendCustomDomainTLSConfigMode +from .managed_project_config_frontend_custom_domain_tls_config import ManagedProjectConfigFrontendCustomDomainTLSConfig +from .managed_project_config_frontend_custom_domain_tls_config_mode import ManagedProjectConfigFrontendCustomDomainTLSConfigMode from .metric_usage_data import MetricUsageData from .o_auth_config import OAuthConfig from .o_auth_config_provider import OAuthConfigProvider @@ -536,6 +545,10 @@ "BatchFunctionDeployFailure", "BatchFunctionDeployFailureOperation", "BatchFunctionDeployResponse", + "BYOCFrontendCustomDomainTLSConfig", + "BYOCFrontendCustomDomainTLSConfigMode", + "BYOCProjectConfigFrontendCustomDomainTLSConfig", + "BYOCProjectConfigFrontendCustomDomainTLSConfigMode", "CallOAuthProviderAPIBody", "CallOAuthProviderAPIBodyBody", "CallOAuthProviderAPIBodyMethod", @@ -624,6 +637,7 @@ "FrontendDeploymentStatus", "FrontendDomainRoutingRecord", "FrontendDomainRoutingRecordRecordType", + "FrontendDomainRoutingRecordZoneApexRecordType", "FrontendDomainVerificationRecord", "FrontendFramework", "FrontendStatus", @@ -719,6 +733,10 @@ "LogSearchRequest", "LogSearchResponse", "LogStreamRequest", + "ManagedFrontendCustomDomainTLSConfig", + "ManagedFrontendCustomDomainTLSConfigMode", + "ManagedProjectConfigFrontendCustomDomainTLSConfig", + "ManagedProjectConfigFrontendCustomDomainTLSConfigMode", "MetricUsageData", "OAuthConfig", "OAuthConfigProvider", diff --git a/src/volcano_sdk/_generated/models/byoc_frontend_custom_domain_tls_config.py b/src/volcano_sdk/_generated/models/byoc_frontend_custom_domain_tls_config.py new file mode 100644 index 00000000..46ac28ae --- /dev/null +++ b/src/volcano_sdk/_generated/models/byoc_frontend_custom_domain_tls_config.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +from ..models.byoc_frontend_custom_domain_tls_config_mode import BYOCFrontendCustomDomainTLSConfigMode +from ..models.byoc_frontend_custom_domain_tls_config_mode import check_byoc_frontend_custom_domain_tls_config_mode +from ..types import UNSET, Unset +from typing import cast + + + + + + +T = TypeVar("T", bound="BYOCFrontendCustomDomainTLSConfig") + + + +@_attrs_define +class BYOCFrontendCustomDomainTLSConfig: + """ Use certificate material that you manage. + + Attributes: + mode (BYOCFrontendCustomDomainTLSConfigMode): + certificate_pem (str): Required. PEM-encoded certificate. + private_key_pem (str): Required. PEM-encoded private key. + certificate_chain_pem (str | Unset): Optional PEM-encoded certificate chain. + """ + + mode: BYOCFrontendCustomDomainTLSConfigMode + certificate_pem: str + private_key_pem: str + certificate_chain_pem: str | Unset = UNSET + + + + + + def to_dict(self) -> dict[str, Any]: + mode: str = self.mode + + certificate_pem = self.certificate_pem + + private_key_pem = self.private_key_pem + + certificate_chain_pem = self.certificate_chain_pem + + + field_dict: dict[str, Any] = {} + + field_dict.update({ + "mode": mode, + "certificate_pem": certificate_pem, + "private_key_pem": private_key_pem, + }) + if certificate_chain_pem is not UNSET: + field_dict["certificate_chain_pem"] = certificate_chain_pem + + return field_dict + + + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + mode = check_byoc_frontend_custom_domain_tls_config_mode(d.pop("mode")) + + + + + certificate_pem = d.pop("certificate_pem") + + private_key_pem = d.pop("private_key_pem") + + certificate_chain_pem = d.pop("certificate_chain_pem", UNSET) + + byoc_frontend_custom_domain_tls_config = cls( + mode=mode, + certificate_pem=certificate_pem, + private_key_pem=private_key_pem, + certificate_chain_pem=certificate_chain_pem, + ) + + return byoc_frontend_custom_domain_tls_config + diff --git a/src/volcano_sdk/_generated/models/byoc_frontend_custom_domain_tls_config_mode.py b/src/volcano_sdk/_generated/models/byoc_frontend_custom_domain_tls_config_mode.py new file mode 100644 index 00000000..b4b1c68c --- /dev/null +++ b/src/volcano_sdk/_generated/models/byoc_frontend_custom_domain_tls_config_mode.py @@ -0,0 +1,10 @@ +from typing import Literal + +BYOCFrontendCustomDomainTLSConfigMode = Literal['byoc'] + +BYOC_FRONTEND_CUSTOM_DOMAIN_TLS_CONFIG_MODE_VALUES: set[BYOCFrontendCustomDomainTLSConfigMode] = { 'byoc', } + +def check_byoc_frontend_custom_domain_tls_config_mode(value: str) -> BYOCFrontendCustomDomainTLSConfigMode: + if value in BYOC_FRONTEND_CUSTOM_DOMAIN_TLS_CONFIG_MODE_VALUES: + return value + raise TypeError(f"Unexpected value {value!r}. Expected one of {BYOC_FRONTEND_CUSTOM_DOMAIN_TLS_CONFIG_MODE_VALUES!r}") diff --git a/src/volcano_sdk/_generated/models/byoc_project_config_frontend_custom_domain_tls_config.py b/src/volcano_sdk/_generated/models/byoc_project_config_frontend_custom_domain_tls_config.py new file mode 100644 index 00000000..f496af38 --- /dev/null +++ b/src/volcano_sdk/_generated/models/byoc_project_config_frontend_custom_domain_tls_config.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +from ..models.byoc_project_config_frontend_custom_domain_tls_config_mode import BYOCProjectConfigFrontendCustomDomainTLSConfigMode +from ..models.byoc_project_config_frontend_custom_domain_tls_config_mode import check_byoc_project_config_frontend_custom_domain_tls_config_mode +from ..types import UNSET, Unset +from typing import cast + + + + + + +T = TypeVar("T", bound="BYOCProjectConfigFrontendCustomDomainTLSConfig") + + + +@_attrs_define +class BYOCProjectConfigFrontendCustomDomainTLSConfig: + """ + Attributes: + mode (BYOCProjectConfigFrontendCustomDomainTLSConfigMode): + certificate_pem (str | Unset): PEM-encoded certificate for BYOC create or rotation. Omitted from exports. + private_key_pem (str | Unset): PEM-encoded private key for BYOC create or rotation. Omitted from exports. + certificate_chain_pem (str | Unset): Optional PEM-encoded certificate chain for BYOC. Omitted from exports. + """ + + mode: BYOCProjectConfigFrontendCustomDomainTLSConfigMode + certificate_pem: str | Unset = UNSET + private_key_pem: str | Unset = UNSET + certificate_chain_pem: str | Unset = UNSET + + + + + + def to_dict(self) -> dict[str, Any]: + mode: str = self.mode + + certificate_pem = self.certificate_pem + + private_key_pem = self.private_key_pem + + certificate_chain_pem = self.certificate_chain_pem + + + field_dict: dict[str, Any] = {} + + field_dict.update({ + "mode": mode, + }) + if certificate_pem is not UNSET: + field_dict["certificate_pem"] = certificate_pem + if private_key_pem is not UNSET: + field_dict["private_key_pem"] = private_key_pem + if certificate_chain_pem is not UNSET: + field_dict["certificate_chain_pem"] = certificate_chain_pem + + return field_dict + + + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + mode = check_byoc_project_config_frontend_custom_domain_tls_config_mode(d.pop("mode")) + + + + + certificate_pem = d.pop("certificate_pem", UNSET) + + private_key_pem = d.pop("private_key_pem", UNSET) + + certificate_chain_pem = d.pop("certificate_chain_pem", UNSET) + + byoc_project_config_frontend_custom_domain_tls_config = cls( + mode=mode, + certificate_pem=certificate_pem, + private_key_pem=private_key_pem, + certificate_chain_pem=certificate_chain_pem, + ) + + return byoc_project_config_frontend_custom_domain_tls_config + diff --git a/src/volcano_sdk/_generated/models/byoc_project_config_frontend_custom_domain_tls_config_mode.py b/src/volcano_sdk/_generated/models/byoc_project_config_frontend_custom_domain_tls_config_mode.py new file mode 100644 index 00000000..cb24de2d --- /dev/null +++ b/src/volcano_sdk/_generated/models/byoc_project_config_frontend_custom_domain_tls_config_mode.py @@ -0,0 +1,10 @@ +from typing import Literal + +BYOCProjectConfigFrontendCustomDomainTLSConfigMode = Literal['byoc'] + +BYOC_PROJECT_CONFIG_FRONTEND_CUSTOM_DOMAIN_TLS_CONFIG_MODE_VALUES: set[BYOCProjectConfigFrontendCustomDomainTLSConfigMode] = { 'byoc', } + +def check_byoc_project_config_frontend_custom_domain_tls_config_mode(value: str) -> BYOCProjectConfigFrontendCustomDomainTLSConfigMode: + if value in BYOC_PROJECT_CONFIG_FRONTEND_CUSTOM_DOMAIN_TLS_CONFIG_MODE_VALUES: + return value + raise TypeError(f"Unexpected value {value!r}. Expected one of {BYOC_PROJECT_CONFIG_FRONTEND_CUSTOM_DOMAIN_TLS_CONFIG_MODE_VALUES!r}") diff --git a/src/volcano_sdk/_generated/models/create_frontend_custom_domain_request.py b/src/volcano_sdk/_generated/models/create_frontend_custom_domain_request.py index 12637293..c7c9b462 100644 --- a/src/volcano_sdk/_generated/models/create_frontend_custom_domain_request.py +++ b/src/volcano_sdk/_generated/models/create_frontend_custom_domain_request.py @@ -11,7 +11,8 @@ from typing import cast if TYPE_CHECKING: - from ..models.frontend_custom_domain_tls_config import FrontendCustomDomainTLSConfig + from ..models.byoc_frontend_custom_domain_tls_config import BYOCFrontendCustomDomainTLSConfig + from ..models.managed_frontend_custom_domain_tls_config import ManagedFrontendCustomDomainTLSConfig @@ -26,26 +27,31 @@ class CreateFrontendCustomDomainRequest: """ Attributes: domain (str): Fully-qualified domain name (hostname only, no scheme/path) Example: app.example.com. - tls (FrontendCustomDomainTLSConfig): + tls (BYOCFrontendCustomDomainTLSConfig | ManagedFrontendCustomDomainTLSConfig): """ domain: str - tls: FrontendCustomDomainTLSConfig - additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + tls: BYOCFrontendCustomDomainTLSConfig | ManagedFrontendCustomDomainTLSConfig def to_dict(self) -> dict[str, Any]: - from ..models.frontend_custom_domain_tls_config import FrontendCustomDomainTLSConfig + from ..models.byoc_frontend_custom_domain_tls_config import BYOCFrontendCustomDomainTLSConfig + from ..models.managed_frontend_custom_domain_tls_config import ManagedFrontendCustomDomainTLSConfig domain = self.domain - tls = self.tls.to_dict() + tls: dict[str, Any] + if isinstance(self.tls, ManagedFrontendCustomDomainTLSConfig): + tls = self.tls.to_dict() + else: + tls = self.tls.to_dict() + field_dict: dict[str, Any] = {} - field_dict.update(self.additional_properties) + field_dict.update({ "domain": domain, "tls": tls, @@ -57,36 +63,37 @@ def to_dict(self) -> dict[str, Any]: @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: - from ..models.frontend_custom_domain_tls_config import FrontendCustomDomainTLSConfig + from ..models.byoc_frontend_custom_domain_tls_config import BYOCFrontendCustomDomainTLSConfig + from ..models.managed_frontend_custom_domain_tls_config import ManagedFrontendCustomDomainTLSConfig d = dict(src_dict) domain = d.pop("domain") - tls = FrontendCustomDomainTLSConfig.from_dict(d.pop("tls")) + def _parse_tls(data: object) -> BYOCFrontendCustomDomainTLSConfig | ManagedFrontendCustomDomainTLSConfig: + try: + if not isinstance(data, dict): + raise TypeError() + componentsschemas_create_frontend_custom_domain_tls_config_type_0 = ManagedFrontendCustomDomainTLSConfig.from_dict(data) + return componentsschemas_create_frontend_custom_domain_tls_config_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + componentsschemas_create_frontend_custom_domain_tls_config_type_1 = BYOCFrontendCustomDomainTLSConfig.from_dict(data) - create_frontend_custom_domain_request = cls( - domain=domain, - tls=tls, - ) - create_frontend_custom_domain_request.additional_properties = d - return create_frontend_custom_domain_request + return componentsschemas_create_frontend_custom_domain_tls_config_type_1 - @property - def additional_keys(self) -> list[str]: - return list(self.additional_properties.keys()) + tls = _parse_tls(d.pop("tls")) - def __getitem__(self, key: str) -> Any: - return self.additional_properties[key] - def __setitem__(self, key: str, value: Any) -> None: - self.additional_properties[key] = value + create_frontend_custom_domain_request = cls( + domain=domain, + tls=tls, + ) - def __delitem__(self, key: str) -> None: - del self.additional_properties[key] + return create_frontend_custom_domain_request - def __contains__(self, key: str) -> bool: - return key in self.additional_properties diff --git a/src/volcano_sdk/_generated/models/frontend_custom_domain_response_verification_status.py b/src/volcano_sdk/_generated/models/frontend_custom_domain_response_verification_status.py index 2eee519a..716cba53 100644 --- a/src/volcano_sdk/_generated/models/frontend_custom_domain_response_verification_status.py +++ b/src/volcano_sdk/_generated/models/frontend_custom_domain_response_verification_status.py @@ -1,8 +1,8 @@ from typing import Literal -FrontendCustomDomainResponseVerificationStatus = Literal['pending', 'verified'] +FrontendCustomDomainResponseVerificationStatus = Literal['failed', 'pending', 'verified'] -FRONTEND_CUSTOM_DOMAIN_RESPONSE_VERIFICATION_STATUS_VALUES: set[FrontendCustomDomainResponseVerificationStatus] = { 'pending', 'verified', } +FRONTEND_CUSTOM_DOMAIN_RESPONSE_VERIFICATION_STATUS_VALUES: set[FrontendCustomDomainResponseVerificationStatus] = { 'failed', 'pending', 'verified', } def check_frontend_custom_domain_response_verification_status(value: str) -> FrontendCustomDomainResponseVerificationStatus: if value in FRONTEND_CUSTOM_DOMAIN_RESPONSE_VERIFICATION_STATUS_VALUES: diff --git a/src/volcano_sdk/_generated/models/frontend_custom_domain_tls_config.py b/src/volcano_sdk/_generated/models/frontend_custom_domain_tls_config.py index 73a39187..2537ec53 100644 --- a/src/volcano_sdk/_generated/models/frontend_custom_domain_tls_config.py +++ b/src/volcano_sdk/_generated/models/frontend_custom_domain_tls_config.py @@ -24,12 +24,13 @@ @_attrs_define class FrontendCustomDomainTLSConfig: - """ + """ Deprecated compatibility model. Use BYOCFrontendCustomDomainTLSConfig. + Attributes: - mode (FrontendCustomDomainTLSConfigMode): BYOC is mandatory for custom domain creation. Default: 'byoc'. - certificate_pem (str): Required. PEM-encoded certificate. - private_key_pem (str): Required. PEM-encoded private key. - certificate_chain_pem (str | Unset): Optional PEM-encoded certificate chain. + mode (FrontendCustomDomainTLSConfigMode): Default: 'byoc'. + certificate_pem (str): + private_key_pem (str): + certificate_chain_pem (str | Unset): """ certificate_pem: str diff --git a/src/volcano_sdk/_generated/models/frontend_domain_routing_record.py b/src/volcano_sdk/_generated/models/frontend_domain_routing_record.py index d4cc2326..4a5b20be 100644 --- a/src/volcano_sdk/_generated/models/frontend_domain_routing_record.py +++ b/src/volcano_sdk/_generated/models/frontend_domain_routing_record.py @@ -10,6 +10,8 @@ from ..models.frontend_domain_routing_record_record_type import check_frontend_domain_routing_record_record_type from ..models.frontend_domain_routing_record_record_type import FrontendDomainRoutingRecordRecordType +from ..models.frontend_domain_routing_record_zone_apex_record_type import check_frontend_domain_routing_record_zone_apex_record_type +from ..models.frontend_domain_routing_record_zone_apex_record_type import FrontendDomainRoutingRecordZoneApexRecordType from typing import cast @@ -25,15 +27,18 @@ class FrontendDomainRoutingRecord: """ Attributes: - record_type (FrontendDomainRoutingRecordRecordType): + record_type (FrontendDomainRoutingRecordRecordType): Use this record type when the hostname is not the apex of + your DNS zone. + zone_apex_record_type (FrontendDomainRoutingRecordZoneApexRecordType): At the apex of your DNS zone, use your + provider's ALIAS, ANAME, or CNAME-flattening equivalent instead of a literal CNAME. name (str): value (str): """ record_type: FrontendDomainRoutingRecordRecordType + zone_apex_record_type: FrontendDomainRoutingRecordZoneApexRecordType name: str value: str - additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) @@ -42,15 +47,18 @@ class FrontendDomainRoutingRecord: def to_dict(self) -> dict[str, Any]: record_type: str = self.record_type + zone_apex_record_type: str = self.zone_apex_record_type + name = self.name value = self.value field_dict: dict[str, Any] = {} - field_dict.update(self.additional_properties) + field_dict.update({ "record_type": record_type, + "zone_apex_record_type": zone_apex_record_type, "name": name, "value": value, }) @@ -67,32 +75,21 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + zone_apex_record_type = check_frontend_domain_routing_record_zone_apex_record_type(d.pop("zone_apex_record_type")) + + + + name = d.pop("name") value = d.pop("value") frontend_domain_routing_record = cls( record_type=record_type, + zone_apex_record_type=zone_apex_record_type, name=name, value=value, ) - - frontend_domain_routing_record.additional_properties = d return frontend_domain_routing_record - @property - def additional_keys(self) -> list[str]: - return list(self.additional_properties.keys()) - - def __getitem__(self, key: str) -> Any: - return self.additional_properties[key] - - def __setitem__(self, key: str, value: Any) -> None: - self.additional_properties[key] = value - - def __delitem__(self, key: str) -> None: - del self.additional_properties[key] - - def __contains__(self, key: str) -> bool: - return key in self.additional_properties diff --git a/src/volcano_sdk/_generated/models/frontend_domain_routing_record_zone_apex_record_type.py b/src/volcano_sdk/_generated/models/frontend_domain_routing_record_zone_apex_record_type.py new file mode 100644 index 00000000..7d33e8ee --- /dev/null +++ b/src/volcano_sdk/_generated/models/frontend_domain_routing_record_zone_apex_record_type.py @@ -0,0 +1,10 @@ +from typing import Literal + +FrontendDomainRoutingRecordZoneApexRecordType = Literal['ALIAS'] + +FRONTEND_DOMAIN_ROUTING_RECORD_ZONE_APEX_RECORD_TYPE_VALUES: set[FrontendDomainRoutingRecordZoneApexRecordType] = { 'ALIAS', } + +def check_frontend_domain_routing_record_zone_apex_record_type(value: str) -> FrontendDomainRoutingRecordZoneApexRecordType: + if value in FRONTEND_DOMAIN_ROUTING_RECORD_ZONE_APEX_RECORD_TYPE_VALUES: + return value + raise TypeError(f"Unexpected value {value!r}. Expected one of {FRONTEND_DOMAIN_ROUTING_RECORD_ZONE_APEX_RECORD_TYPE_VALUES!r}") diff --git a/src/volcano_sdk/_generated/models/frontend_domain_verification_record.py b/src/volcano_sdk/_generated/models/frontend_domain_verification_record.py index 688bce99..1fd6c1c5 100644 --- a/src/volcano_sdk/_generated/models/frontend_domain_verification_record.py +++ b/src/volcano_sdk/_generated/models/frontend_domain_verification_record.py @@ -30,7 +30,6 @@ class FrontendDomainVerificationRecord: name: str type_: str value: str - additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) @@ -45,7 +44,7 @@ def to_dict(self) -> dict[str, Any]: field_dict: dict[str, Any] = {} - field_dict.update(self.additional_properties) + field_dict.update({ "name": name, "type": type_, @@ -71,22 +70,5 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: value=value, ) - - frontend_domain_verification_record.additional_properties = d return frontend_domain_verification_record - @property - def additional_keys(self) -> list[str]: - return list(self.additional_properties.keys()) - - def __getitem__(self, key: str) -> Any: - return self.additional_properties[key] - - def __setitem__(self, key: str, value: Any) -> None: - self.additional_properties[key] = value - - def __delitem__(self, key: str) -> None: - del self.additional_properties[key] - - def __contains__(self, key: str) -> bool: - return key in self.additional_properties diff --git a/src/volcano_sdk/_generated/models/managed_frontend_custom_domain_tls_config.py b/src/volcano_sdk/_generated/models/managed_frontend_custom_domain_tls_config.py new file mode 100644 index 00000000..0877cfc7 --- /dev/null +++ b/src/volcano_sdk/_generated/models/managed_frontend_custom_domain_tls_config.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +from ..models.managed_frontend_custom_domain_tls_config_mode import check_managed_frontend_custom_domain_tls_config_mode +from ..models.managed_frontend_custom_domain_tls_config_mode import ManagedFrontendCustomDomainTLSConfigMode +from typing import cast + + + + + + +T = TypeVar("T", bound="ManagedFrontendCustomDomainTLSConfig") + + + +@_attrs_define +class ManagedFrontendCustomDomainTLSConfig: + """ Volcano issues and renews the certificate. Do not send certificate material. + + Attributes: + mode (ManagedFrontendCustomDomainTLSConfigMode): + """ + + mode: ManagedFrontendCustomDomainTLSConfigMode + + + + + + def to_dict(self) -> dict[str, Any]: + mode: str = self.mode + + + field_dict: dict[str, Any] = {} + + field_dict.update({ + "mode": mode, + }) + + return field_dict + + + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + mode = check_managed_frontend_custom_domain_tls_config_mode(d.pop("mode")) + + + + + managed_frontend_custom_domain_tls_config = cls( + mode=mode, + ) + + return managed_frontend_custom_domain_tls_config + diff --git a/src/volcano_sdk/_generated/models/managed_frontend_custom_domain_tls_config_mode.py b/src/volcano_sdk/_generated/models/managed_frontend_custom_domain_tls_config_mode.py new file mode 100644 index 00000000..8b5c2d4c --- /dev/null +++ b/src/volcano_sdk/_generated/models/managed_frontend_custom_domain_tls_config_mode.py @@ -0,0 +1,10 @@ +from typing import Literal + +ManagedFrontendCustomDomainTLSConfigMode = Literal['managed'] + +MANAGED_FRONTEND_CUSTOM_DOMAIN_TLS_CONFIG_MODE_VALUES: set[ManagedFrontendCustomDomainTLSConfigMode] = { 'managed', } + +def check_managed_frontend_custom_domain_tls_config_mode(value: str) -> ManagedFrontendCustomDomainTLSConfigMode: + if value in MANAGED_FRONTEND_CUSTOM_DOMAIN_TLS_CONFIG_MODE_VALUES: + return value + raise TypeError(f"Unexpected value {value!r}. Expected one of {MANAGED_FRONTEND_CUSTOM_DOMAIN_TLS_CONFIG_MODE_VALUES!r}") diff --git a/src/volcano_sdk/_generated/models/managed_project_config_frontend_custom_domain_tls_config.py b/src/volcano_sdk/_generated/models/managed_project_config_frontend_custom_domain_tls_config.py new file mode 100644 index 00000000..9ed9b2fe --- /dev/null +++ b/src/volcano_sdk/_generated/models/managed_project_config_frontend_custom_domain_tls_config.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +from ..models.managed_project_config_frontend_custom_domain_tls_config_mode import check_managed_project_config_frontend_custom_domain_tls_config_mode +from ..models.managed_project_config_frontend_custom_domain_tls_config_mode import ManagedProjectConfigFrontendCustomDomainTLSConfigMode +from typing import cast + + + + + + +T = TypeVar("T", bound="ManagedProjectConfigFrontendCustomDomainTLSConfig") + + + +@_attrs_define +class ManagedProjectConfigFrontendCustomDomainTLSConfig: + """ + Attributes: + mode (ManagedProjectConfigFrontendCustomDomainTLSConfigMode): + """ + + mode: ManagedProjectConfigFrontendCustomDomainTLSConfigMode + + + + + + def to_dict(self) -> dict[str, Any]: + mode: str = self.mode + + + field_dict: dict[str, Any] = {} + + field_dict.update({ + "mode": mode, + }) + + return field_dict + + + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + mode = check_managed_project_config_frontend_custom_domain_tls_config_mode(d.pop("mode")) + + + + + managed_project_config_frontend_custom_domain_tls_config = cls( + mode=mode, + ) + + return managed_project_config_frontend_custom_domain_tls_config + diff --git a/src/volcano_sdk/_generated/models/managed_project_config_frontend_custom_domain_tls_config_mode.py b/src/volcano_sdk/_generated/models/managed_project_config_frontend_custom_domain_tls_config_mode.py new file mode 100644 index 00000000..0e87a285 --- /dev/null +++ b/src/volcano_sdk/_generated/models/managed_project_config_frontend_custom_domain_tls_config_mode.py @@ -0,0 +1,10 @@ +from typing import Literal + +ManagedProjectConfigFrontendCustomDomainTLSConfigMode = Literal['managed'] + +MANAGED_PROJECT_CONFIG_FRONTEND_CUSTOM_DOMAIN_TLS_CONFIG_MODE_VALUES: set[ManagedProjectConfigFrontendCustomDomainTLSConfigMode] = { 'managed', } + +def check_managed_project_config_frontend_custom_domain_tls_config_mode(value: str) -> ManagedProjectConfigFrontendCustomDomainTLSConfigMode: + if value in MANAGED_PROJECT_CONFIG_FRONTEND_CUSTOM_DOMAIN_TLS_CONFIG_MODE_VALUES: + return value + raise TypeError(f"Unexpected value {value!r}. Expected one of {MANAGED_PROJECT_CONFIG_FRONTEND_CUSTOM_DOMAIN_TLS_CONFIG_MODE_VALUES!r}") diff --git a/src/volcano_sdk/_generated/models/project_config_custom_domain.py b/src/volcano_sdk/_generated/models/project_config_custom_domain.py index 8220362f..286afa95 100644 --- a/src/volcano_sdk/_generated/models/project_config_custom_domain.py +++ b/src/volcano_sdk/_generated/models/project_config_custom_domain.py @@ -12,7 +12,8 @@ from typing import cast if TYPE_CHECKING: - from ..models.frontend_custom_domain_tls_config import FrontendCustomDomainTLSConfig + from ..models.byoc_project_config_frontend_custom_domain_tls_config import BYOCProjectConfigFrontendCustomDomainTLSConfig + from ..models.managed_project_config_frontend_custom_domain_tls_config import ManagedProjectConfigFrontendCustomDomainTLSConfig @@ -24,33 +25,38 @@ @_attrs_define class ProjectConfigCustomDomain: - """ Custom domain with BYOC TLS (PRO plan). `tls` is required when the - domain is first created and optional afterwards: providing new TLS - material for the same domain rotates the certificate in place (zero - downtime); omitting `tls` keeps the stored certificate. TLS material is - write-only and omitted from config export. + """ Custom domain with managed or BYOC TLS (PRO plan). `tls` is required + when the domain is first created and optional afterwards. BYOC TLS + material is write-only and omitted from config export. Attributes: domain (str): Fully-qualified domain name (hostname only, no scheme/path) - tls (FrontendCustomDomainTLSConfig | Unset): + tls (BYOCProjectConfigFrontendCustomDomainTLSConfig | ManagedProjectConfigFrontendCustomDomainTLSConfig | + Unset): """ domain: str - tls: FrontendCustomDomainTLSConfig | Unset = UNSET + tls: BYOCProjectConfigFrontendCustomDomainTLSConfig | ManagedProjectConfigFrontendCustomDomainTLSConfig | Unset = UNSET def to_dict(self) -> dict[str, Any]: - from ..models.frontend_custom_domain_tls_config import FrontendCustomDomainTLSConfig + from ..models.byoc_project_config_frontend_custom_domain_tls_config import BYOCProjectConfigFrontendCustomDomainTLSConfig + from ..models.managed_project_config_frontend_custom_domain_tls_config import ManagedProjectConfigFrontendCustomDomainTLSConfig domain = self.domain - tls: dict[str, Any] | Unset = UNSET - if not isinstance(self.tls, Unset): + tls: dict[str, Any] | Unset + if isinstance(self.tls, Unset): + tls = UNSET + elif isinstance(self.tls, ManagedProjectConfigFrontendCustomDomainTLSConfig): + tls = self.tls.to_dict() + else: tls = self.tls.to_dict() + field_dict: dict[str, Any] = {} field_dict.update({ @@ -65,18 +71,33 @@ def to_dict(self) -> dict[str, Any]: @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: - from ..models.frontend_custom_domain_tls_config import FrontendCustomDomainTLSConfig + from ..models.byoc_project_config_frontend_custom_domain_tls_config import BYOCProjectConfigFrontendCustomDomainTLSConfig + from ..models.managed_project_config_frontend_custom_domain_tls_config import ManagedProjectConfigFrontendCustomDomainTLSConfig d = dict(src_dict) domain = d.pop("domain") - _tls = d.pop("tls", UNSET) - tls: FrontendCustomDomainTLSConfig | Unset - if isinstance(_tls, Unset): - tls = UNSET - else: - tls = FrontendCustomDomainTLSConfig.from_dict(_tls) + def _parse_tls(data: object) -> BYOCProjectConfigFrontendCustomDomainTLSConfig | ManagedProjectConfigFrontendCustomDomainTLSConfig | Unset: + if isinstance(data, Unset): + return data + try: + if not isinstance(data, dict): + raise TypeError() + componentsschemas_project_config_frontend_custom_domain_tls_config_type_0 = ManagedProjectConfigFrontendCustomDomainTLSConfig.from_dict(data) + + + + return componentsschemas_project_config_frontend_custom_domain_tls_config_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + if not isinstance(data, dict): + raise TypeError() + componentsschemas_project_config_frontend_custom_domain_tls_config_type_1 = BYOCProjectConfigFrontendCustomDomainTLSConfig.from_dict(data) + + + return componentsschemas_project_config_frontend_custom_domain_tls_config_type_1 + tls = _parse_tls(d.pop("tls", UNSET)) project_config_custom_domain = cls( diff --git a/src/volcano_sdk/_generated/models/project_config_frontend.py b/src/volcano_sdk/_generated/models/project_config_frontend.py index f4ae1e83..fac91b54 100644 --- a/src/volcano_sdk/_generated/models/project_config_frontend.py +++ b/src/volcano_sdk/_generated/models/project_config_frontend.py @@ -30,12 +30,10 @@ class ProjectConfigFrontend: Attributes: name (str): - custom_domain (ProjectConfigCustomDomain | Unset): Custom domain with BYOC TLS (PRO plan). `tls` is required - when the - domain is first created and optional afterwards: providing new TLS - material for the same domain rotates the certificate in place (zero - downtime); omitting `tls` keeps the stored certificate. TLS material is - write-only and omitted from config export. + custom_domain (ProjectConfigCustomDomain | Unset): Custom domain with managed or BYOC TLS (PRO plan). `tls` is + required + when the domain is first created and optional afterwards. BYOC TLS + material is write-only and omitted from config export. """ name: str diff --git a/tests/unit/test_contract_bindings.py b/tests/unit/test_contract_bindings.py index 551b8146..82cad390 100644 --- a/tests/unit/test_contract_bindings.py +++ b/tests/unit/test_contract_bindings.py @@ -29,6 +29,9 @@ "4685b29357a621068b25984ff0de29cd4c504eebe5cfb597f0b999e29878a668" ), "locks.feature": "76fa31f9a7c203e33b367e5ca1467b2334e7c85c960de8d5cab8638920137411", + "managed_tls.feature": ( + "eca7f071f387c0f4e017bbce06c9fbe948a19b0ef3828521032faabcc7c063af" + ), "realtime.feature": ( "e65862e27656cdd0afa8e552cb5a628d9831568e3299711e572ccd4f6b750696" ), @@ -72,6 +75,7 @@ def test_every_contract_phrase_is_bound_verbatim() -> None: "a fresh client adopts the current session", "a fresh client tries to refresh the signed-out session", "an authenticated client", + "a managed custom-domain TLS request", "exactly the deleted contract row is returned", "exactly the fixture row is returned", "exactly the inserted contract row is returned", @@ -86,6 +90,7 @@ def test_every_contract_phrase_is_bound_verbatim() -> None: "the client reads the current session", "the client refreshes the current session", "the client listens for auth state changes", + "the client encodes the request and decodes a pending verification response", "the client signs out", "the client signs in with the contract user's credentials", "the client uploads and downloads the contract object", @@ -97,6 +102,8 @@ def test_every_contract_phrase_is_bound_verbatim() -> None: "the downloaded bytes equal the uploaded bytes", "the refreshed session becomes current", "the released lease is no longer held", + "the request selects managed TLS without certificate material", + "the response exposes the managed lifecycle and DNS records", "the SDK operation fails with an authentication error", "the stored object path equals the contract path", "the subscriber receives the contract message within 10 seconds", diff --git a/tests/unit/test_managed_tls_contract.py b/tests/unit/test_managed_tls_contract.py new file mode 100644 index 00000000..9aa42bb8 --- /dev/null +++ b/tests/unit/test_managed_tls_contract.py @@ -0,0 +1,95 @@ +from attrs import fields + +from volcano_sdk._generated import models as generated_models +from volcano_sdk._generated.models.create_frontend_custom_domain_request import ( + CreateFrontendCustomDomainRequest, +) +from volcano_sdk._generated.models.frontend_custom_domain_response import ( + FrontendCustomDomainResponse, +) +from volcano_sdk._generated.models.frontend_custom_domain_tls_config import ( + FrontendCustomDomainTLSConfig, +) +from volcano_sdk._generated.models.managed_frontend_custom_domain_tls_config import ( + ManagedFrontendCustomDomainTLSConfig, +) + + +def test_managed_tls_models_keep_the_public_contract_provider_neutral() -> None: + request = CreateFrontendCustomDomainRequest( + domain="app.example.com", + tls=ManagedFrontendCustomDomainTLSConfig(mode="managed"), + ) + + assert request.to_dict() == { + "domain": "app.example.com", + "tls": {"mode": "managed"}, + } + assert "managed_tls_certificate" not in { + attribute.name for attribute in fields(FrontendCustomDomainResponse) + } + + response = FrontendCustomDomainResponse.from_dict( + { + "domain": "app.example.com", + "tls_mode": "managed", + "domain_status": "pending_verification", + "verification_status": "pending", + "verification_records": [ + { + "name": "_token.app.example.com", + "type": "CNAME", + "value": "_validation.volcano.dev", + } + ], + "required_routing_record": { + "record_type": "CNAME", + "zone_apex_record_type": "ALIAS", + "name": "app.example.com", + "value": "frontend.frontends.volcano.dev", + }, + "effective_urls": ["https://frontend.frontends.volcano.dev/"], + "created_at": "2026-09-02T12:00:00Z", + "updated_at": "2026-09-02T12:00:00Z", + } + ) + assert response.domain_status == "pending_verification" + assert response.verification_status == "pending" + assert isinstance(response.verification_records, list) + assert response.verification_records[0].to_dict() == { + "name": "_token.app.example.com", + "type": "CNAME", + "value": "_validation.volcano.dev", + } + failed = FrontendCustomDomainResponse.from_dict( + {**response.to_dict(), "verification_status": "failed"} + ) + assert failed.verification_status == "failed" + + +def test_managed_project_config_model_cannot_serialize_certificate_material() -> None: + managed_type = getattr( + generated_models, + "ManagedProjectConfigFrontendCustomDomainTLSConfig", + None, + ) + assert managed_type is not None + assert { + "certificate_pem", + "private_key_pem", + "certificate_chain_pem", + }.isdisjoint(attribute.name for attribute in fields(managed_type)) + assert managed_type.from_dict({"mode": "managed"}).to_dict() == {"mode": "managed"} + + +def test_legacy_byoc_model_keeps_its_default_mode() -> None: + legacy = FrontendCustomDomainTLSConfig( + certificate_pem="certificate", + private_key_pem="private-key", + ) + + assert legacy.to_dict() == { + "mode": "byoc", + "certificate_pem": "certificate", + "private_key_pem": "private-key", + } From a91b354969213c5a3848481042e06b800c3e1e4a Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:07:33 -0400 Subject: [PATCH 02/15] fix(api): align managed TLS types with the hosting contract Vendor the managed custom-domain TLS schemas from the current Hosting bundle: the create request uses the flat FrontendCustomDomainTLSConfig with managed and byoc modes, responses expose failure_reason, and the domain and PEM fields carry length limits. Drop the discriminated create request types that Hosting no longer defines. --- openapi/openapi.yaml | 204 +++++++++--------- .../create_frontend_custom_domain.py | 16 ++ src/volcano_sdk/_generated/models/__init__.py | 8 - .../byoc_frontend_custom_domain_tls_config.py | 91 -------- ..._frontend_custom_domain_tls_config_mode.py | 10 - .../create_frontend_custom_domain_request.py | 40 +--- .../models/frontend_custom_domain_response.py | 11 + .../frontend_custom_domain_tls_config.py | 46 ++-- .../frontend_custom_domain_tls_config_mode.py | 4 +- .../frontend_domain_verification_record.py | 5 +- ...naged_frontend_custom_domain_tls_config.py | 65 ------ ..._frontend_custom_domain_tls_config_mode.py | 10 - .../models/project_frontend_custom_domain.py | 11 + .../_tests/test_managed_tls_contract.py | 68 ++++-- 14 files changed, 222 insertions(+), 367 deletions(-) delete mode 100644 src/volcano_sdk/_generated/models/byoc_frontend_custom_domain_tls_config.py delete mode 100644 src/volcano_sdk/_generated/models/byoc_frontend_custom_domain_tls_config_mode.py delete mode 100644 src/volcano_sdk/_generated/models/managed_frontend_custom_domain_tls_config.py delete mode 100644 src/volcano_sdk/_generated/models/managed_frontend_custom_domain_tls_config_mode.py diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index b465a518..f78844f8 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -4800,6 +4800,7 @@ paths: Configures one custom domain for a frontend. The default Volcano-generated frontend URL remains active. Wildcard Volcano frontend TLS remains valid and isolated from custom-domain certificate changes. + Managed TLS returns the DNS records currently required for setup. Volcano may require a tenant-specific TXT ownership challenge before returning the certificate authority's validation record. After ownership verification succeeds, Volcano permanently assigns the hostname to the account. A required but unverified ownership reservation expires after 72 hours. operationId: createFrontendCustomDomain security: - UserToken: [] @@ -13421,10 +13422,11 @@ components: properties: domain: type: string + maxLength: 253 description: Fully-qualified domain name (hostname only, no scheme/path) example: app.example.com tls: - $ref: '#/components/schemas/CreateFrontendCustomDomainTLSConfig' + $ref: '#/components/schemas/FrontendCustomDomainTLSConfig' required: - domain - tls @@ -15255,6 +15257,9 @@ components: - pending - verified - failed + failure_reason: + type: string + description: Safe failure category returned for failed managed TLS provisioning. One of provider, certificate, ownership, or internal. Ownership means another account has already verified the hostname. verification_records: type: array items: @@ -15279,124 +15284,62 @@ components: - effective_urls - created_at - updated_at - CreateFrontendCustomDomainTLSConfig: - oneOf: - - $ref: '#/components/schemas/ManagedFrontendCustomDomainTLSConfig' - - $ref: '#/components/schemas/BYOCFrontendCustomDomainTLSConfig' - discriminator: - propertyName: mode - mapping: - managed: '#/components/schemas/ManagedFrontendCustomDomainTLSConfig' - byoc: '#/components/schemas/BYOCFrontendCustomDomainTLSConfig' - ManagedFrontendCustomDomainTLSConfig: + FrontendCustomDomainTLSConfig: type: object - description: Volcano issues and renews the certificate. Do not send certificate material. + description: Set mode to managed for Volcano-issued TLS, or byoc with certificate_pem and private_key_pem. additionalProperties: false + not: + anyOf: + - allOf: + - properties: + mode: + enum: + - managed + required: + - mode + - anyOf: + - required: + - certificate_pem + - required: + - private_key_pem + - required: + - certificate_chain_pem + - allOf: + - properties: + mode: + enum: + - byoc + required: + - mode + - anyOf: + - not: + required: + - certificate_pem + - not: + required: + - private_key_pem properties: mode: type: string enum: - managed - required: - - mode - BYOCFrontendCustomDomainTLSConfig: - type: object - description: Use certificate material that you manage. - additionalProperties: false - properties: - mode: - type: string - enum: - byoc certificate_pem: type: string + maxLength: 65536 + x-go-type-skip-optional-pointer: true description: Required. PEM-encoded certificate. private_key_pem: type: string + maxLength: 65536 + x-go-type-skip-optional-pointer: true description: Required. PEM-encoded private key. certificate_chain_pem: type: string + maxLength: 65536 description: Optional PEM-encoded certificate chain. required: - mode - - certificate_pem - - private_key_pem - FrontendCustomDomainTLSConfig: - type: object - deprecated: true - description: Deprecated compatibility model. Use BYOCFrontendCustomDomainTLSConfig. - properties: - mode: - type: string - enum: - - byoc - default: byoc - certificate_pem: - type: string - private_key_pem: - type: string - certificate_chain_pem: - type: string - required: - - mode - - certificate_pem - - private_key_pem - ProjectConfigFrontendCustomDomainTLSConfig: - oneOf: - - $ref: '#/components/schemas/ManagedProjectConfigFrontendCustomDomainTLSConfig' - - $ref: '#/components/schemas/BYOCProjectConfigFrontendCustomDomainTLSConfig' - discriminator: - propertyName: mode - mapping: - managed: '#/components/schemas/ManagedProjectConfigFrontendCustomDomainTLSConfig' - byoc: '#/components/schemas/BYOCProjectConfigFrontendCustomDomainTLSConfig' - ManagedProjectConfigFrontendCustomDomainTLSConfig: - type: object - additionalProperties: false - properties: - mode: - type: string - enum: - - managed - required: - - mode - BYOCProjectConfigFrontendCustomDomainTLSConfig: - type: object - additionalProperties: false - not: - anyOf: - - required: - - certificate_pem - not: - required: - - private_key_pem - - required: - - private_key_pem - not: - required: - - certificate_pem - - required: - - certificate_chain_pem - not: - required: - - certificate_pem - - private_key_pem - properties: - mode: - type: string - enum: - - byoc - certificate_pem: - type: string - description: PEM-encoded certificate for BYOC create or rotation. Omitted from exports. - private_key_pem: - type: string - description: PEM-encoded private key for BYOC create or rotation. Omitted from exports. - certificate_chain_pem: - type: string - description: Optional PEM-encoded certificate chain for BYOC. Omitted from exports. - required: - - mode FrontendDeployment: type: object properties: @@ -15509,6 +15452,7 @@ components: - value FrontendDomainVerificationRecord: type: object + description: The DNS records currently required for managed TLS. Volcano may require a tenant-specific TXT ownership record before returning a CNAME that authorizes certificate issuance and renewal. Clients must follow the records returned for the current lifecycle state instead of assuming a fixed sequence. additionalProperties: false properties: name: @@ -17481,6 +17425,7 @@ components: properties: domain: type: string + maxLength: 253 description: Fully-qualified domain name (hostname only, no scheme/path) tls: $ref: '#/components/schemas/ProjectConfigFrontendCustomDomainTLSConfig' @@ -19326,6 +19271,65 @@ components: $ref: '#/components/schemas/AuthPageTheme' layouts: $ref: '#/components/schemas/ProjectConfigAuthPageLayouts' + ManagedProjectConfigFrontendCustomDomainTLSConfig: + type: object + additionalProperties: false + properties: + mode: + type: string + enum: + - managed + required: + - mode + BYOCProjectConfigFrontendCustomDomainTLSConfig: + type: object + additionalProperties: false + not: + anyOf: + - required: + - certificate_pem + not: + required: + - private_key_pem + - required: + - private_key_pem + not: + required: + - certificate_pem + - required: + - certificate_chain_pem + not: + required: + - certificate_pem + - private_key_pem + properties: + mode: + type: string + enum: + - byoc + certificate_pem: + type: string + maxLength: 65536 + description: PEM-encoded certificate for BYOC create or rotation. Omitted from exports. + private_key_pem: + type: string + maxLength: 65536 + description: PEM-encoded private key for BYOC create or rotation. Omitted from exports. + certificate_chain_pem: + type: string + maxLength: 65536 + description: Optional PEM-encoded certificate chain for BYOC. Omitted from exports. + required: + - mode + ProjectConfigFrontendCustomDomainTLSConfig: + oneOf: + - $ref: '#/components/schemas/ManagedProjectConfigFrontendCustomDomainTLSConfig' + - $ref: '#/components/schemas/BYOCProjectConfigFrontendCustomDomainTLSConfig' + discriminator: + propertyName: mode + mapping: + managed: '#/components/schemas/ManagedProjectConfigFrontendCustomDomainTLSConfig' + byoc: '#/components/schemas/BYOCProjectConfigFrontendCustomDomainTLSConfig' DatabaseQueryPerformanceDatabase: type: object properties: diff --git a/src/volcano_sdk/_generated/api/frontends/create_frontend_custom_domain.py b/src/volcano_sdk/_generated/api/frontends/create_frontend_custom_domain.py index 54bb086c..b8848a56 100644 --- a/src/volcano_sdk/_generated/api/frontends/create_frontend_custom_domain.py +++ b/src/volcano_sdk/_generated/api/frontends/create_frontend_custom_domain.py @@ -136,6 +136,10 @@ def sync_detailed( Configures one custom domain for a frontend. The default Volcano-generated frontend URL remains active. Wildcard Volcano frontend TLS remains valid and isolated from custom-domain certificate changes. + Managed TLS returns the DNS records currently required for setup. Volcano may require a tenant- + specific TXT ownership challenge before returning the certificate authority's validation record. + After ownership verification succeeds, Volcano permanently assigns the hostname to the account. A + required but unverified ownership reservation expires after 72 hours. Args: id (UUID): @@ -177,6 +181,10 @@ def sync( Configures one custom domain for a frontend. The default Volcano-generated frontend URL remains active. Wildcard Volcano frontend TLS remains valid and isolated from custom-domain certificate changes. + Managed TLS returns the DNS records currently required for setup. Volcano may require a tenant- + specific TXT ownership challenge before returning the certificate authority's validation record. + After ownership verification succeeds, Volcano permanently assigns the hostname to the account. A + required but unverified ownership reservation expires after 72 hours. Args: id (UUID): @@ -213,6 +221,10 @@ async def asyncio_detailed( Configures one custom domain for a frontend. The default Volcano-generated frontend URL remains active. Wildcard Volcano frontend TLS remains valid and isolated from custom-domain certificate changes. + Managed TLS returns the DNS records currently required for setup. Volcano may require a tenant- + specific TXT ownership challenge before returning the certificate authority's validation record. + After ownership verification succeeds, Volcano permanently assigns the hostname to the account. A + required but unverified ownership reservation expires after 72 hours. Args: id (UUID): @@ -254,6 +266,10 @@ async def asyncio( Configures one custom domain for a frontend. The default Volcano-generated frontend URL remains active. Wildcard Volcano frontend TLS remains valid and isolated from custom-domain certificate changes. + Managed TLS returns the DNS records currently required for setup. Volcano may require a tenant- + specific TXT ownership challenge before returning the certificate authority's validation record. + After ownership verification succeeds, Volcano permanently assigns the hostname to the account. A + required but unverified ownership reservation expires after 72 hours. Args: id (UUID): diff --git a/src/volcano_sdk/_generated/models/__init__.py b/src/volcano_sdk/_generated/models/__init__.py index 7c4c490f..25ae1f20 100644 --- a/src/volcano_sdk/_generated/models/__init__.py +++ b/src/volcano_sdk/_generated/models/__init__.py @@ -96,8 +96,6 @@ from .batch_function_deploy_failure import BatchFunctionDeployFailure from .batch_function_deploy_failure_operation import BatchFunctionDeployFailureOperation from .batch_function_deploy_response import BatchFunctionDeployResponse -from .byoc_frontend_custom_domain_tls_config import BYOCFrontendCustomDomainTLSConfig -from .byoc_frontend_custom_domain_tls_config_mode import BYOCFrontendCustomDomainTLSConfigMode from .byoc_project_config_frontend_custom_domain_tls_config import BYOCProjectConfigFrontendCustomDomainTLSConfig from .byoc_project_config_frontend_custom_domain_tls_config_mode import BYOCProjectConfigFrontendCustomDomainTLSConfigMode from .call_o_auth_provider_api_body import CallOAuthProviderAPIBody @@ -314,8 +312,6 @@ from .log_search_request import LogSearchRequest from .log_search_response import LogSearchResponse from .log_stream_request import LogStreamRequest -from .managed_frontend_custom_domain_tls_config import ManagedFrontendCustomDomainTLSConfig -from .managed_frontend_custom_domain_tls_config_mode import ManagedFrontendCustomDomainTLSConfigMode from .managed_project_config_frontend_custom_domain_tls_config import ManagedProjectConfigFrontendCustomDomainTLSConfig from .managed_project_config_frontend_custom_domain_tls_config_mode import ManagedProjectConfigFrontendCustomDomainTLSConfigMode from .metric_usage_data import MetricUsageData @@ -620,8 +616,6 @@ "BatchFunctionDeployFailure", "BatchFunctionDeployFailureOperation", "BatchFunctionDeployResponse", - "BYOCFrontendCustomDomainTLSConfig", - "BYOCFrontendCustomDomainTLSConfigMode", "BYOCProjectConfigFrontendCustomDomainTLSConfig", "BYOCProjectConfigFrontendCustomDomainTLSConfigMode", "CallOAuthProviderAPIBody", @@ -838,8 +832,6 @@ "LogSearchRequest", "LogSearchResponse", "LogStreamRequest", - "ManagedFrontendCustomDomainTLSConfig", - "ManagedFrontendCustomDomainTLSConfigMode", "ManagedProjectConfigFrontendCustomDomainTLSConfig", "ManagedProjectConfigFrontendCustomDomainTLSConfigMode", "MetricUsageData", diff --git a/src/volcano_sdk/_generated/models/byoc_frontend_custom_domain_tls_config.py b/src/volcano_sdk/_generated/models/byoc_frontend_custom_domain_tls_config.py deleted file mode 100644 index 46ac28ae..00000000 --- a/src/volcano_sdk/_generated/models/byoc_frontend_custom_domain_tls_config.py +++ /dev/null @@ -1,91 +0,0 @@ -from __future__ import annotations - -from collections.abc import Mapping -from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator - -from attrs import define as _attrs_define -from attrs import field as _attrs_field - -from ..types import UNSET, Unset - -from ..models.byoc_frontend_custom_domain_tls_config_mode import BYOCFrontendCustomDomainTLSConfigMode -from ..models.byoc_frontend_custom_domain_tls_config_mode import check_byoc_frontend_custom_domain_tls_config_mode -from ..types import UNSET, Unset -from typing import cast - - - - - - -T = TypeVar("T", bound="BYOCFrontendCustomDomainTLSConfig") - - - -@_attrs_define -class BYOCFrontendCustomDomainTLSConfig: - """ Use certificate material that you manage. - - Attributes: - mode (BYOCFrontendCustomDomainTLSConfigMode): - certificate_pem (str): Required. PEM-encoded certificate. - private_key_pem (str): Required. PEM-encoded private key. - certificate_chain_pem (str | Unset): Optional PEM-encoded certificate chain. - """ - - mode: BYOCFrontendCustomDomainTLSConfigMode - certificate_pem: str - private_key_pem: str - certificate_chain_pem: str | Unset = UNSET - - - - - - def to_dict(self) -> dict[str, Any]: - mode: str = self.mode - - certificate_pem = self.certificate_pem - - private_key_pem = self.private_key_pem - - certificate_chain_pem = self.certificate_chain_pem - - - field_dict: dict[str, Any] = {} - - field_dict.update({ - "mode": mode, - "certificate_pem": certificate_pem, - "private_key_pem": private_key_pem, - }) - if certificate_chain_pem is not UNSET: - field_dict["certificate_chain_pem"] = certificate_chain_pem - - return field_dict - - - - @classmethod - def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: - d = dict(src_dict) - mode = check_byoc_frontend_custom_domain_tls_config_mode(d.pop("mode")) - - - - - certificate_pem = d.pop("certificate_pem") - - private_key_pem = d.pop("private_key_pem") - - certificate_chain_pem = d.pop("certificate_chain_pem", UNSET) - - byoc_frontend_custom_domain_tls_config = cls( - mode=mode, - certificate_pem=certificate_pem, - private_key_pem=private_key_pem, - certificate_chain_pem=certificate_chain_pem, - ) - - return byoc_frontend_custom_domain_tls_config - diff --git a/src/volcano_sdk/_generated/models/byoc_frontend_custom_domain_tls_config_mode.py b/src/volcano_sdk/_generated/models/byoc_frontend_custom_domain_tls_config_mode.py deleted file mode 100644 index b4b1c68c..00000000 --- a/src/volcano_sdk/_generated/models/byoc_frontend_custom_domain_tls_config_mode.py +++ /dev/null @@ -1,10 +0,0 @@ -from typing import Literal - -BYOCFrontendCustomDomainTLSConfigMode = Literal['byoc'] - -BYOC_FRONTEND_CUSTOM_DOMAIN_TLS_CONFIG_MODE_VALUES: set[BYOCFrontendCustomDomainTLSConfigMode] = { 'byoc', } - -def check_byoc_frontend_custom_domain_tls_config_mode(value: str) -> BYOCFrontendCustomDomainTLSConfigMode: - if value in BYOC_FRONTEND_CUSTOM_DOMAIN_TLS_CONFIG_MODE_VALUES: - return value - raise TypeError(f"Unexpected value {value!r}. Expected one of {BYOC_FRONTEND_CUSTOM_DOMAIN_TLS_CONFIG_MODE_VALUES!r}") diff --git a/src/volcano_sdk/_generated/models/create_frontend_custom_domain_request.py b/src/volcano_sdk/_generated/models/create_frontend_custom_domain_request.py index c7c9b462..19b6eb3d 100644 --- a/src/volcano_sdk/_generated/models/create_frontend_custom_domain_request.py +++ b/src/volcano_sdk/_generated/models/create_frontend_custom_domain_request.py @@ -11,8 +11,7 @@ from typing import cast if TYPE_CHECKING: - from ..models.byoc_frontend_custom_domain_tls_config import BYOCFrontendCustomDomainTLSConfig - from ..models.managed_frontend_custom_domain_tls_config import ManagedFrontendCustomDomainTLSConfig + from ..models.frontend_custom_domain_tls_config import FrontendCustomDomainTLSConfig @@ -27,27 +26,22 @@ class CreateFrontendCustomDomainRequest: """ Attributes: domain (str): Fully-qualified domain name (hostname only, no scheme/path) Example: app.example.com. - tls (BYOCFrontendCustomDomainTLSConfig | ManagedFrontendCustomDomainTLSConfig): + tls (FrontendCustomDomainTLSConfig): Set mode to managed for Volcano-issued TLS, or byoc with certificate_pem + and private_key_pem. """ domain: str - tls: BYOCFrontendCustomDomainTLSConfig | ManagedFrontendCustomDomainTLSConfig + tls: FrontendCustomDomainTLSConfig def to_dict(self) -> dict[str, Any]: - from ..models.byoc_frontend_custom_domain_tls_config import BYOCFrontendCustomDomainTLSConfig - from ..models.managed_frontend_custom_domain_tls_config import ManagedFrontendCustomDomainTLSConfig + from ..models.frontend_custom_domain_tls_config import FrontendCustomDomainTLSConfig domain = self.domain - tls: dict[str, Any] - if isinstance(self.tls, ManagedFrontendCustomDomainTLSConfig): - tls = self.tls.to_dict() - else: - tls = self.tls.to_dict() - + tls = self.tls.to_dict() field_dict: dict[str, Any] = {} @@ -63,31 +57,13 @@ def to_dict(self) -> dict[str, Any]: @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: - from ..models.byoc_frontend_custom_domain_tls_config import BYOCFrontendCustomDomainTLSConfig - from ..models.managed_frontend_custom_domain_tls_config import ManagedFrontendCustomDomainTLSConfig + from ..models.frontend_custom_domain_tls_config import FrontendCustomDomainTLSConfig d = dict(src_dict) domain = d.pop("domain") - def _parse_tls(data: object) -> BYOCFrontendCustomDomainTLSConfig | ManagedFrontendCustomDomainTLSConfig: - try: - if not isinstance(data, dict): - raise TypeError() - componentsschemas_create_frontend_custom_domain_tls_config_type_0 = ManagedFrontendCustomDomainTLSConfig.from_dict(data) - - - - return componentsschemas_create_frontend_custom_domain_tls_config_type_0 - except (TypeError, ValueError, AttributeError, KeyError): - pass - if not isinstance(data, dict): - raise TypeError() - componentsschemas_create_frontend_custom_domain_tls_config_type_1 = BYOCFrontendCustomDomainTLSConfig.from_dict(data) - - + tls = FrontendCustomDomainTLSConfig.from_dict(d.pop("tls")) - return componentsschemas_create_frontend_custom_domain_tls_config_type_1 - tls = _parse_tls(d.pop("tls")) create_frontend_custom_domain_request = cls( diff --git a/src/volcano_sdk/_generated/models/frontend_custom_domain_response.py b/src/volcano_sdk/_generated/models/frontend_custom_domain_response.py index 3c8ebdf1..34ba7396 100644 --- a/src/volcano_sdk/_generated/models/frontend_custom_domain_response.py +++ b/src/volcano_sdk/_generated/models/frontend_custom_domain_response.py @@ -41,6 +41,9 @@ class FrontendCustomDomainResponse: effective_urls (list[str]): created_at (datetime.datetime): updated_at (datetime.datetime): + failure_reason (str | Unset): Safe failure category returned for failed managed TLS provisioning. One of + provider, certificate, ownership, or internal. Ownership means another account has already verified the + hostname. verification_records (list[FrontendDomainVerificationRecord] | Unset): required_routing_record (FrontendDomainRoutingRecord | Unset): """ @@ -52,6 +55,7 @@ class FrontendCustomDomainResponse: effective_urls: list[str] created_at: datetime.datetime updated_at: datetime.datetime + failure_reason: str | Unset = UNSET verification_records: list[FrontendDomainVerificationRecord] | Unset = UNSET required_routing_record: FrontendDomainRoutingRecord | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) @@ -79,6 +83,8 @@ def to_dict(self) -> dict[str, Any]: updated_at = self.updated_at.isoformat() + failure_reason = self.failure_reason + verification_records: list[dict[str, Any]] | Unset = UNSET if not isinstance(self.verification_records, Unset): verification_records = [] @@ -104,6 +110,8 @@ def to_dict(self) -> dict[str, Any]: "created_at": created_at, "updated_at": updated_at, }) + if failure_reason is not UNSET: + field_dict["failure_reason"] = failure_reason if verification_records is not UNSET: field_dict["verification_records"] = verification_records if required_routing_record is not UNSET: @@ -148,6 +156,8 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + failure_reason = d.pop("failure_reason", UNSET) + _verification_records = d.pop("verification_records", UNSET) verification_records: list[FrontendDomainVerificationRecord] | Unset = UNSET if _verification_records is not UNSET: @@ -178,6 +188,7 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: effective_urls=effective_urls, created_at=created_at, updated_at=updated_at, + failure_reason=failure_reason, verification_records=verification_records, required_routing_record=required_routing_record, ) diff --git a/src/volcano_sdk/_generated/models/frontend_custom_domain_tls_config.py b/src/volcano_sdk/_generated/models/frontend_custom_domain_tls_config.py index 2537ec53..eb36cfb7 100644 --- a/src/volcano_sdk/_generated/models/frontend_custom_domain_tls_config.py +++ b/src/volcano_sdk/_generated/models/frontend_custom_domain_tls_config.py @@ -24,20 +24,19 @@ @_attrs_define class FrontendCustomDomainTLSConfig: - """ Deprecated compatibility model. Use BYOCFrontendCustomDomainTLSConfig. + """ Set mode to managed for Volcano-issued TLS, or byoc with certificate_pem and private_key_pem. Attributes: - mode (FrontendCustomDomainTLSConfigMode): Default: 'byoc'. - certificate_pem (str): - private_key_pem (str): - certificate_chain_pem (str | Unset): + mode (FrontendCustomDomainTLSConfigMode): + certificate_pem (str | Unset): Required. PEM-encoded certificate. + private_key_pem (str | Unset): Required. PEM-encoded private key. + certificate_chain_pem (str | Unset): Optional PEM-encoded certificate chain. """ - certificate_pem: str - private_key_pem: str - mode: FrontendCustomDomainTLSConfigMode = 'byoc' + mode: FrontendCustomDomainTLSConfigMode + certificate_pem: str | Unset = UNSET + private_key_pem: str | Unset = UNSET certificate_chain_pem: str | Unset = UNSET - additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) @@ -54,12 +53,14 @@ def to_dict(self) -> dict[str, Any]: field_dict: dict[str, Any] = {} - field_dict.update(self.additional_properties) + field_dict.update({ "mode": mode, - "certificate_pem": certificate_pem, - "private_key_pem": private_key_pem, }) + if certificate_pem is not UNSET: + field_dict["certificate_pem"] = certificate_pem + if private_key_pem is not UNSET: + field_dict["private_key_pem"] = private_key_pem if certificate_chain_pem is not UNSET: field_dict["certificate_chain_pem"] = certificate_chain_pem @@ -75,9 +76,9 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: - certificate_pem = d.pop("certificate_pem") + certificate_pem = d.pop("certificate_pem", UNSET) - private_key_pem = d.pop("private_key_pem") + private_key_pem = d.pop("private_key_pem", UNSET) certificate_chain_pem = d.pop("certificate_chain_pem", UNSET) @@ -88,22 +89,5 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: certificate_chain_pem=certificate_chain_pem, ) - - frontend_custom_domain_tls_config.additional_properties = d return frontend_custom_domain_tls_config - @property - def additional_keys(self) -> list[str]: - return list(self.additional_properties.keys()) - - def __getitem__(self, key: str) -> Any: - return self.additional_properties[key] - - def __setitem__(self, key: str, value: Any) -> None: - self.additional_properties[key] = value - - def __delitem__(self, key: str) -> None: - del self.additional_properties[key] - - def __contains__(self, key: str) -> bool: - return key in self.additional_properties diff --git a/src/volcano_sdk/_generated/models/frontend_custom_domain_tls_config_mode.py b/src/volcano_sdk/_generated/models/frontend_custom_domain_tls_config_mode.py index 32360945..bdfceb82 100644 --- a/src/volcano_sdk/_generated/models/frontend_custom_domain_tls_config_mode.py +++ b/src/volcano_sdk/_generated/models/frontend_custom_domain_tls_config_mode.py @@ -1,8 +1,8 @@ from typing import Literal -FrontendCustomDomainTLSConfigMode = Literal['byoc'] +FrontendCustomDomainTLSConfigMode = Literal['byoc', 'managed'] -FRONTEND_CUSTOM_DOMAIN_TLS_CONFIG_MODE_VALUES: set[FrontendCustomDomainTLSConfigMode] = { 'byoc', } +FRONTEND_CUSTOM_DOMAIN_TLS_CONFIG_MODE_VALUES: set[FrontendCustomDomainTLSConfigMode] = { 'byoc', 'managed', } def check_frontend_custom_domain_tls_config_mode(value: str) -> FrontendCustomDomainTLSConfigMode: if value in FRONTEND_CUSTOM_DOMAIN_TLS_CONFIG_MODE_VALUES: diff --git a/src/volcano_sdk/_generated/models/frontend_domain_verification_record.py b/src/volcano_sdk/_generated/models/frontend_domain_verification_record.py index 1fd6c1c5..93b5dacd 100644 --- a/src/volcano_sdk/_generated/models/frontend_domain_verification_record.py +++ b/src/volcano_sdk/_generated/models/frontend_domain_verification_record.py @@ -20,7 +20,10 @@ @_attrs_define class FrontendDomainVerificationRecord: - """ + """ The DNS records currently required for managed TLS. Volcano may require a tenant-specific TXT ownership record + before returning a CNAME that authorizes certificate issuance and renewal. Clients must follow the records returned + for the current lifecycle state instead of assuming a fixed sequence. + Attributes: name (str): type_ (str): diff --git a/src/volcano_sdk/_generated/models/managed_frontend_custom_domain_tls_config.py b/src/volcano_sdk/_generated/models/managed_frontend_custom_domain_tls_config.py deleted file mode 100644 index 0877cfc7..00000000 --- a/src/volcano_sdk/_generated/models/managed_frontend_custom_domain_tls_config.py +++ /dev/null @@ -1,65 +0,0 @@ -from __future__ import annotations - -from collections.abc import Mapping -from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator - -from attrs import define as _attrs_define -from attrs import field as _attrs_field - -from ..types import UNSET, Unset - -from ..models.managed_frontend_custom_domain_tls_config_mode import check_managed_frontend_custom_domain_tls_config_mode -from ..models.managed_frontend_custom_domain_tls_config_mode import ManagedFrontendCustomDomainTLSConfigMode -from typing import cast - - - - - - -T = TypeVar("T", bound="ManagedFrontendCustomDomainTLSConfig") - - - -@_attrs_define -class ManagedFrontendCustomDomainTLSConfig: - """ Volcano issues and renews the certificate. Do not send certificate material. - - Attributes: - mode (ManagedFrontendCustomDomainTLSConfigMode): - """ - - mode: ManagedFrontendCustomDomainTLSConfigMode - - - - - - def to_dict(self) -> dict[str, Any]: - mode: str = self.mode - - - field_dict: dict[str, Any] = {} - - field_dict.update({ - "mode": mode, - }) - - return field_dict - - - - @classmethod - def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: - d = dict(src_dict) - mode = check_managed_frontend_custom_domain_tls_config_mode(d.pop("mode")) - - - - - managed_frontend_custom_domain_tls_config = cls( - mode=mode, - ) - - return managed_frontend_custom_domain_tls_config - diff --git a/src/volcano_sdk/_generated/models/managed_frontend_custom_domain_tls_config_mode.py b/src/volcano_sdk/_generated/models/managed_frontend_custom_domain_tls_config_mode.py deleted file mode 100644 index 8b5c2d4c..00000000 --- a/src/volcano_sdk/_generated/models/managed_frontend_custom_domain_tls_config_mode.py +++ /dev/null @@ -1,10 +0,0 @@ -from typing import Literal - -ManagedFrontendCustomDomainTLSConfigMode = Literal['managed'] - -MANAGED_FRONTEND_CUSTOM_DOMAIN_TLS_CONFIG_MODE_VALUES: set[ManagedFrontendCustomDomainTLSConfigMode] = { 'managed', } - -def check_managed_frontend_custom_domain_tls_config_mode(value: str) -> ManagedFrontendCustomDomainTLSConfigMode: - if value in MANAGED_FRONTEND_CUSTOM_DOMAIN_TLS_CONFIG_MODE_VALUES: - return value - raise TypeError(f"Unexpected value {value!r}. Expected one of {MANAGED_FRONTEND_CUSTOM_DOMAIN_TLS_CONFIG_MODE_VALUES!r}") diff --git a/src/volcano_sdk/_generated/models/project_frontend_custom_domain.py b/src/volcano_sdk/_generated/models/project_frontend_custom_domain.py index ccf6585f..fc549bb0 100644 --- a/src/volcano_sdk/_generated/models/project_frontend_custom_domain.py +++ b/src/volcano_sdk/_generated/models/project_frontend_custom_domain.py @@ -44,6 +44,9 @@ class ProjectFrontendCustomDomain: updated_at (datetime.datetime): frontend (ProjectFrontendCustomDomainFrontend): The frontend this custom domain is attached to. Inlined to avoid a second fetch from the project-scoped feed. + failure_reason (str | Unset): Safe failure category returned for failed managed TLS provisioning. One of + provider, certificate, ownership, or internal. Ownership means another account has already verified the + hostname. verification_records (list[FrontendDomainVerificationRecord] | Unset): required_routing_record (FrontendDomainRoutingRecord | Unset): """ @@ -56,6 +59,7 @@ class ProjectFrontendCustomDomain: created_at: datetime.datetime updated_at: datetime.datetime frontend: ProjectFrontendCustomDomainFrontend + failure_reason: str | Unset = UNSET verification_records: list[FrontendDomainVerificationRecord] | Unset = UNSET required_routing_record: FrontendDomainRoutingRecord | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) @@ -86,6 +90,8 @@ def to_dict(self) -> dict[str, Any]: frontend = self.frontend.to_dict() + failure_reason = self.failure_reason + verification_records: list[dict[str, Any]] | Unset = UNSET if not isinstance(self.verification_records, Unset): verification_records = [] @@ -112,6 +118,8 @@ def to_dict(self) -> dict[str, Any]: "updated_at": updated_at, "frontend": frontend, }) + if failure_reason is not UNSET: + field_dict["failure_reason"] = failure_reason if verification_records is not UNSET: field_dict["verification_records"] = verification_records if required_routing_record is not UNSET: @@ -162,6 +170,8 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + failure_reason = d.pop("failure_reason", UNSET) + _verification_records = d.pop("verification_records", UNSET) verification_records: list[FrontendDomainVerificationRecord] | Unset = UNSET if _verification_records is not UNSET: @@ -193,6 +203,7 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: created_at=created_at, updated_at=updated_at, frontend=frontend, + failure_reason=failure_reason, verification_records=verification_records, required_routing_record=required_routing_record, ) diff --git a/src/volcano_sdk/_tests/test_managed_tls_contract.py b/src/volcano_sdk/_tests/test_managed_tls_contract.py index 71744ff0..ee0baee9 100644 --- a/src/volcano_sdk/_tests/test_managed_tls_contract.py +++ b/src/volcano_sdk/_tests/test_managed_tls_contract.py @@ -1,25 +1,49 @@ from attrs import fields_dict from volcano_sdk._generated.models import ( + BYOCProjectConfigFrontendCustomDomainTLSConfig, CreateFrontendCustomDomainRequest, FrontendCustomDomainResponse, FrontendCustomDomainTLSConfig, FrontendDomainRoutingRecord, - ManagedFrontendCustomDomainTLSConfig, ManagedProjectConfigFrontendCustomDomainTLSConfig, + ProjectConfigCustomDomain, ) -def test_managed_tls_models_keep_the_public_contract_provider_neutral() -> None: +def test_managed_tls_request_omits_certificate_material() -> None: request = CreateFrontendCustomDomainRequest( domain="app.example.com", - tls=ManagedFrontendCustomDomainTLSConfig(mode="managed"), + tls=FrontendCustomDomainTLSConfig(mode="managed"), ) assert request.to_dict() == { "domain": "app.example.com", "tls": {"mode": "managed"}, } + + +def test_byoc_tls_request_carries_certificate_material() -> None: + request = CreateFrontendCustomDomainRequest( + domain="app.example.com", + tls=FrontendCustomDomainTLSConfig( + mode="byoc", + certificate_pem="certificate", + private_key_pem="private-key", + ), + ) + + assert request.to_dict() == { + "domain": "app.example.com", + "tls": { + "mode": "byoc", + "certificate_pem": "certificate", + "private_key_pem": "private-key", + }, + } + + +def test_managed_tls_response_exposes_provider_neutral_lifecycle() -> None: assert "managed_tls_certificate" not in fields_dict(FrontendCustomDomainResponse) response = FrontendCustomDomainResponse.from_dict( @@ -62,31 +86,41 @@ def test_managed_tls_models_keep_the_public_contract_provider_neutral() -> None: "name": "app.example.com", "value": "frontend.frontends.volcano.dev", } + failed = FrontendCustomDomainResponse.from_dict( - {**response.to_dict(), "verification_status": "failed"} + { + **response.to_dict(), + "domain_status": "failed", + "verification_status": "failed", + "failure_reason": "ownership", + } ) + assert failed.domain_status == "failed" assert failed.verification_status == "failed" + assert failed.failure_reason == "ownership" -def test_managed_project_config_model_cannot_serialize_certificate_material() -> None: +def test_project_config_tls_decodes_each_mode_without_certificate_material() -> None: assert { "certificate_pem", "private_key_pem", "certificate_chain_pem", }.isdisjoint(fields_dict(ManagedProjectConfigFrontendCustomDomainTLSConfig)) - assert ManagedProjectConfigFrontendCustomDomainTLSConfig.from_dict( - {"mode": "managed"} - ).to_dict() == {"mode": "managed"} - -def test_legacy_byoc_model_keeps_its_default_mode() -> None: - legacy = FrontendCustomDomainTLSConfig( - certificate_pem="certificate", - private_key_pem="private-key", + managed = ProjectConfigCustomDomain.from_dict( + {"domain": "app.example.com", "tls": {"mode": "managed"}} + ) + exported_byoc = ProjectConfigCustomDomain.from_dict( + {"domain": "app.example.com", "tls": {"mode": "byoc"}} ) - assert legacy.to_dict() == { - "mode": "byoc", - "certificate_pem": "certificate", - "private_key_pem": "private-key", + assert isinstance(managed.tls, ManagedProjectConfigFrontendCustomDomainTLSConfig) + assert managed.to_dict() == { + "domain": "app.example.com", + "tls": {"mode": "managed"}, + } + assert isinstance(exported_byoc.tls, BYOCProjectConfigFrontendCustomDomainTLSConfig) + assert exported_byoc.to_dict() == { + "domain": "app.example.com", + "tls": {"mode": "byoc"}, } From 86b13fe7db0db33e9033ab5aa7f3fd12ec46942f Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:19:30 -0400 Subject: [PATCH 03/15] test(api): guard custom-domain responses against certificate fields --- src/volcano_sdk/_tests/test_managed_tls_contract.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/volcano_sdk/_tests/test_managed_tls_contract.py b/src/volcano_sdk/_tests/test_managed_tls_contract.py index ee0baee9..4d4562ad 100644 --- a/src/volcano_sdk/_tests/test_managed_tls_contract.py +++ b/src/volcano_sdk/_tests/test_managed_tls_contract.py @@ -1,3 +1,4 @@ +import pytest from attrs import fields_dict from volcano_sdk._generated.models import ( @@ -8,6 +9,7 @@ FrontendDomainRoutingRecord, ManagedProjectConfigFrontendCustomDomainTLSConfig, ProjectConfigCustomDomain, + ProjectFrontendCustomDomain, ) @@ -43,9 +45,16 @@ def test_byoc_tls_request_carries_certificate_material() -> None: } -def test_managed_tls_response_exposes_provider_neutral_lifecycle() -> None: - assert "managed_tls_certificate" not in fields_dict(FrontendCustomDomainResponse) +@pytest.mark.parametrize( + "model", [FrontendCustomDomainResponse, ProjectFrontendCustomDomain] +) +def test_domain_responses_do_not_expose_certificate_internals( + model: type[FrontendCustomDomainResponse | ProjectFrontendCustomDomain], +) -> None: + assert not [name for name in fields_dict(model) if "certificate" in name] + +def test_managed_tls_response_exposes_provider_neutral_lifecycle() -> None: response = FrontendCustomDomainResponse.from_dict( { "domain": "app.example.com", From 793e0137f6c2b9923b64908dfd6933ca3371af81 Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:02:21 -0400 Subject: [PATCH 04/15] test(api): cover both TLS modes and key fields in custom-domain guards --- .../_tests/test_managed_tls_contract.py | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/src/volcano_sdk/_tests/test_managed_tls_contract.py b/src/volcano_sdk/_tests/test_managed_tls_contract.py index 4d4562ad..e2e84fce 100644 --- a/src/volcano_sdk/_tests/test_managed_tls_contract.py +++ b/src/volcano_sdk/_tests/test_managed_tls_contract.py @@ -13,45 +13,45 @@ ) -def test_managed_tls_request_omits_certificate_material() -> None: - request = CreateFrontendCustomDomainRequest( - domain="app.example.com", - tls=FrontendCustomDomainTLSConfig(mode="managed"), - ) - - assert request.to_dict() == { - "domain": "app.example.com", - "tls": {"mode": "managed"}, - } - - -def test_byoc_tls_request_carries_certificate_material() -> None: - request = CreateFrontendCustomDomainRequest( - domain="app.example.com", - tls=FrontendCustomDomainTLSConfig( - mode="byoc", - certificate_pem="certificate", - private_key_pem="private-key", +@pytest.mark.parametrize( + ("tls", "wire_tls"), + [ + (FrontendCustomDomainTLSConfig(mode="managed"), {"mode": "managed"}), + ( + FrontendCustomDomainTLSConfig( + mode="byoc", + certificate_pem="certificate", + private_key_pem="private-key", + ), + { + "mode": "byoc", + "certificate_pem": "certificate", + "private_key_pem": "private-key", + }, ), - ) + ], + ids=["managed", "byoc"], +) +def test_create_request_encodes_the_selected_tls_mode( + tls: FrontendCustomDomainTLSConfig, + wire_tls: dict[str, str], +) -> None: + request = CreateFrontendCustomDomainRequest(domain="app.example.com", tls=tls) - assert request.to_dict() == { - "domain": "app.example.com", - "tls": { - "mode": "byoc", - "certificate_pem": "certificate", - "private_key_pem": "private-key", - }, - } + assert request.to_dict() == {"domain": "app.example.com", "tls": wire_tls} @pytest.mark.parametrize( "model", [FrontendCustomDomainResponse, ProjectFrontendCustomDomain] ) -def test_domain_responses_do_not_expose_certificate_internals( +def test_domain_responses_do_not_declare_certificate_or_key_fields( model: type[FrontendCustomDomainResponse | ProjectFrontendCustomDomain], ) -> None: - assert not [name for name in fields_dict(model) if "certificate" in name] + assert not [ + name + for name in fields_dict(model) + if any(marker in name for marker in ("certificate", "private_key", "pem")) + ] def test_managed_tls_response_exposes_provider_neutral_lifecycle() -> None: From 4d547e001e4fb2447458f17061961e7a0fc9afc0 Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:26:53 -0400 Subject: [PATCH 05/15] test(api): round-trip managed TLS wire shapes through generated models --- .../_tests/test_managed_tls_contract.py | 98 ++++++++++--------- 1 file changed, 52 insertions(+), 46 deletions(-) diff --git a/src/volcano_sdk/_tests/test_managed_tls_contract.py b/src/volcano_sdk/_tests/test_managed_tls_contract.py index e2e84fce..3f99d427 100644 --- a/src/volcano_sdk/_tests/test_managed_tls_contract.py +++ b/src/volcano_sdk/_tests/test_managed_tls_contract.py @@ -11,41 +11,45 @@ ProjectConfigCustomDomain, ProjectFrontendCustomDomain, ) +from volcano_sdk._generated.types import UNSET + +BYOC_MATERIAL = { + "certificate_pem": "certificate", + "private_key_pem": "private-key", +} @pytest.mark.parametrize( - ("tls", "wire_tls"), - [ - (FrontendCustomDomainTLSConfig(mode="managed"), {"mode": "managed"}), - ( - FrontendCustomDomainTLSConfig( - mode="byoc", - certificate_pem="certificate", - private_key_pem="private-key", - ), - { - "mode": "byoc", - "certificate_pem": "certificate", - "private_key_pem": "private-key", - }, - ), - ], + "wire_tls", + [{"mode": "managed"}, {"mode": "byoc", **BYOC_MATERIAL}], ids=["managed", "byoc"], ) -def test_create_request_encodes_the_selected_tls_mode( - tls: FrontendCustomDomainTLSConfig, +def test_create_request_round_trips_the_selected_tls_mode( wire_tls: dict[str, str], ) -> None: - request = CreateFrontendCustomDomainRequest(domain="app.example.com", tls=tls) + wire_request = {"domain": "app.example.com", "tls": wire_tls} - assert request.to_dict() == {"domain": "app.example.com", "tls": wire_tls} + request = CreateFrontendCustomDomainRequest.from_dict(wire_request) + + assert isinstance(request.tls, FrontendCustomDomainTLSConfig) + assert request.tls.mode == wire_tls["mode"] + assert request.to_dict() == wire_request @pytest.mark.parametrize( - "model", [FrontendCustomDomainResponse, ProjectFrontendCustomDomain] + "model", + [ + FrontendCustomDomainResponse, + ProjectFrontendCustomDomain, + ManagedProjectConfigFrontendCustomDomainTLSConfig, + ], ) -def test_domain_responses_do_not_declare_certificate_or_key_fields( - model: type[FrontendCustomDomainResponse | ProjectFrontendCustomDomain], +def test_material_free_models_do_not_declare_certificate_or_key_fields( + model: type[ + FrontendCustomDomainResponse + | ProjectFrontendCustomDomain + | ManagedProjectConfigFrontendCustomDomainTLSConfig + ], ) -> None: assert not [ name @@ -54,7 +58,7 @@ def test_domain_responses_do_not_declare_certificate_or_key_fields( ] -def test_managed_tls_response_exposes_provider_neutral_lifecycle() -> None: +def test_managed_tls_response_decodes_lifecycle_and_dns_records() -> None: response = FrontendCustomDomainResponse.from_dict( { "domain": "app.example.com", @@ -82,6 +86,7 @@ def test_managed_tls_response_exposes_provider_neutral_lifecycle() -> None: assert response.tls_mode == "managed" assert response.domain_status == "pending_verification" assert response.verification_status == "pending" + assert response.failure_reason is UNSET assert isinstance(response.verification_records, list) assert response.verification_records[0].to_dict() == { "name": "_token.app.example.com", @@ -109,27 +114,28 @@ def test_managed_tls_response_exposes_provider_neutral_lifecycle() -> None: assert failed.failure_reason == "ownership" -def test_project_config_tls_decodes_each_mode_without_certificate_material() -> None: - assert { - "certificate_pem", - "private_key_pem", - "certificate_chain_pem", - }.isdisjoint(fields_dict(ManagedProjectConfigFrontendCustomDomainTLSConfig)) +@pytest.mark.parametrize( + ("wire_tls", "variant"), + [ + ({"mode": "managed"}, ManagedProjectConfigFrontendCustomDomainTLSConfig), + ({"mode": "byoc"}, BYOCProjectConfigFrontendCustomDomainTLSConfig), + ( + {"mode": "byoc", **BYOC_MATERIAL}, + BYOCProjectConfigFrontendCustomDomainTLSConfig, + ), + ], + ids=["managed", "byoc-export", "byoc-rotation"], +) +def test_project_config_tls_decodes_each_mode( + wire_tls: dict[str, str], + variant: type[ + ManagedProjectConfigFrontendCustomDomainTLSConfig + | BYOCProjectConfigFrontendCustomDomainTLSConfig + ], +) -> None: + wire_domain = {"domain": "app.example.com", "tls": wire_tls} - managed = ProjectConfigCustomDomain.from_dict( - {"domain": "app.example.com", "tls": {"mode": "managed"}} - ) - exported_byoc = ProjectConfigCustomDomain.from_dict( - {"domain": "app.example.com", "tls": {"mode": "byoc"}} - ) + domain = ProjectConfigCustomDomain.from_dict(wire_domain) - assert isinstance(managed.tls, ManagedProjectConfigFrontendCustomDomainTLSConfig) - assert managed.to_dict() == { - "domain": "app.example.com", - "tls": {"mode": "managed"}, - } - assert isinstance(exported_byoc.tls, BYOCProjectConfigFrontendCustomDomainTLSConfig) - assert exported_byoc.to_dict() == { - "domain": "app.example.com", - "tls": {"mode": "byoc"}, - } + assert isinstance(domain.tls, variant) + assert domain.to_dict() == wire_domain From d91fa696c5205ba927547558ce63e9d32fb9af3d Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:48:28 -0400 Subject: [PATCH 06/15] test(api): type the certificate guard against any attrs model --- src/volcano_sdk/_tests/test_managed_tls_contract.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/volcano_sdk/_tests/test_managed_tls_contract.py b/src/volcano_sdk/_tests/test_managed_tls_contract.py index 3f99d427..0417c539 100644 --- a/src/volcano_sdk/_tests/test_managed_tls_contract.py +++ b/src/volcano_sdk/_tests/test_managed_tls_contract.py @@ -1,5 +1,5 @@ import pytest -from attrs import fields_dict +from attrs import AttrsInstance, fields_dict from volcano_sdk._generated.models import ( BYOCProjectConfigFrontendCustomDomainTLSConfig, @@ -45,11 +45,7 @@ def test_create_request_round_trips_the_selected_tls_mode( ], ) def test_material_free_models_do_not_declare_certificate_or_key_fields( - model: type[ - FrontendCustomDomainResponse - | ProjectFrontendCustomDomain - | ManagedProjectConfigFrontendCustomDomainTLSConfig - ], + model: type[AttrsInstance], ) -> None: assert not [ name From 7e20324d3a97ce7932a85d13443f7ecf8e22d372 Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:01:34 -0400 Subject: [PATCH 07/15] test(api): encode managed TLS requests through constructors and cover the domain feed --- .../_tests/test_managed_tls_contract.py | 123 +++++++++++------- 1 file changed, 77 insertions(+), 46 deletions(-) diff --git a/src/volcano_sdk/_tests/test_managed_tls_contract.py b/src/volcano_sdk/_tests/test_managed_tls_contract.py index 0417c539..a0633dd1 100644 --- a/src/volcano_sdk/_tests/test_managed_tls_contract.py +++ b/src/volcano_sdk/_tests/test_managed_tls_contract.py @@ -7,6 +7,7 @@ FrontendCustomDomainResponse, FrontendCustomDomainTLSConfig, FrontendDomainRoutingRecord, + FrontendDomainVerificationRecord, ManagedProjectConfigFrontendCustomDomainTLSConfig, ProjectConfigCustomDomain, ProjectFrontendCustomDomain, @@ -17,23 +18,55 @@ "certificate_pem": "certificate", "private_key_pem": "private-key", } +ROUTING_RECORD = { + "record_type": "CNAME", + "zone_apex_record_type": "ALIAS", + "name": "app.example.com", + "value": "frontend.frontends.volcano.dev", +} +VALIDATION_RECORD = { + "name": "_token.app.example.com", + "type": "CNAME", + "value": "_validation.volcano.dev", +} +MANAGED_PENDING = { + "domain": "app.example.com", + "tls_mode": "managed", + "domain_status": "pending_verification", + "verification_status": "pending", + "verification_records": [VALIDATION_RECORD], + "required_routing_record": ROUTING_RECORD, + "effective_urls": ["https://frontend.frontends.volcano.dev/"], + "created_at": "2026-09-02T12:00:00Z", + "updated_at": "2026-09-02T12:00:00Z", +} @pytest.mark.parametrize( - "wire_tls", - [{"mode": "managed"}, {"mode": "byoc", **BYOC_MATERIAL}], + ("tls", "wire_tls"), + [ + (FrontendCustomDomainTLSConfig(mode="managed"), {"mode": "managed"}), + ( + FrontendCustomDomainTLSConfig( + mode="byoc", + certificate_pem="certificate", + private_key_pem="private-key", + ), + {"mode": "byoc", **BYOC_MATERIAL}, + ), + ], ids=["managed", "byoc"], ) -def test_create_request_round_trips_the_selected_tls_mode( +def test_create_request_encodes_the_selected_tls_mode( + tls: FrontendCustomDomainTLSConfig, wire_tls: dict[str, str], ) -> None: wire_request = {"domain": "app.example.com", "tls": wire_tls} - request = CreateFrontendCustomDomainRequest.from_dict(wire_request) + request = CreateFrontendCustomDomainRequest(domain="app.example.com", tls=tls) - assert isinstance(request.tls, FrontendCustomDomainTLSConfig) - assert request.tls.mode == wire_tls["mode"] assert request.to_dict() == wire_request + assert CreateFrontendCustomDomainRequest.from_dict(wire_request) == request @pytest.mark.parametrize( @@ -41,6 +74,8 @@ def test_create_request_round_trips_the_selected_tls_mode( [ FrontendCustomDomainResponse, ProjectFrontendCustomDomain, + FrontendDomainVerificationRecord, + FrontendDomainRoutingRecord, ManagedProjectConfigFrontendCustomDomainTLSConfig, ], ) @@ -50,64 +85,60 @@ def test_material_free_models_do_not_declare_certificate_or_key_fields( assert not [ name for name in fields_dict(model) - if any(marker in name for marker in ("certificate", "private_key", "pem")) + if any(marker in name for marker in ("cert", "key", "pem")) ] def test_managed_tls_response_decodes_lifecycle_and_dns_records() -> None: - response = FrontendCustomDomainResponse.from_dict( - { - "domain": "app.example.com", - "tls_mode": "managed", - "domain_status": "pending_verification", - "verification_status": "pending", - "verification_records": [ - { - "name": "_token.app.example.com", - "type": "CNAME", - "value": "_validation.volcano.dev", - } - ], - "required_routing_record": { - "record_type": "CNAME", - "zone_apex_record_type": "ALIAS", - "name": "app.example.com", - "value": "frontend.frontends.volcano.dev", - }, - "effective_urls": ["https://frontend.frontends.volcano.dev/"], - "created_at": "2026-09-02T12:00:00Z", - "updated_at": "2026-09-02T12:00:00Z", - } - ) + response = FrontendCustomDomainResponse.from_dict(MANAGED_PENDING) + assert response.tls_mode == "managed" assert response.domain_status == "pending_verification" assert response.verification_status == "pending" assert response.failure_reason is UNSET assert isinstance(response.verification_records, list) - assert response.verification_records[0].to_dict() == { - "name": "_token.app.example.com", - "type": "CNAME", - "value": "_validation.volcano.dev", - } + assert [record.to_dict() for record in response.verification_records] == [ + VALIDATION_RECORD + ] assert isinstance(response.required_routing_record, FrontendDomainRoutingRecord) - assert response.required_routing_record.to_dict() == { - "record_type": "CNAME", - "zone_apex_record_type": "ALIAS", - "name": "app.example.com", - "value": "frontend.frontends.volcano.dev", - } - - failed = FrontendCustomDomainResponse.from_dict( + assert response.required_routing_record.to_dict() == ROUTING_RECORD + + +@pytest.mark.parametrize( + ("model", "scope"), + [ + (FrontendCustomDomainResponse, {}), + ( + ProjectFrontendCustomDomain, + { + "frontend": { + "id": "00000000-0000-4000-8000-000000000001", + "name": "web", + } + }, + ), + ], + ids=["frontend", "project-feed"], +) +def test_failed_managed_domain_reports_its_failure_reason( + model: type[FrontendCustomDomainResponse | ProjectFrontendCustomDomain], + scope: dict[str, dict[str, str]], +) -> None: + failed = model.from_dict( { - **response.to_dict(), + **MANAGED_PENDING, + **scope, "domain_status": "failed", "verification_status": "failed", "failure_reason": "ownership", } ) + assert failed.domain_status == "failed" assert failed.verification_status == "failed" assert failed.failure_reason == "ownership" + assert isinstance(failed.required_routing_record, FrontendDomainRoutingRecord) + assert failed.required_routing_record.to_dict() == ROUTING_RECORD @pytest.mark.parametrize( @@ -120,7 +151,7 @@ def test_managed_tls_response_decodes_lifecycle_and_dns_records() -> None: BYOCProjectConfigFrontendCustomDomainTLSConfig, ), ], - ids=["managed", "byoc-export", "byoc-rotation"], + ids=["managed", "byoc-export", "byoc-apply"], ) def test_project_config_tls_decodes_each_mode( wire_tls: dict[str, str], From a7e5ca1c7e7952bc7225ca253030aa335ecfafbc Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:15:20 -0400 Subject: [PATCH 08/15] test(api): clarify managed TLS fixture names and sources --- src/volcano_sdk/_tests/test_managed_tls_contract.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/volcano_sdk/_tests/test_managed_tls_contract.py b/src/volcano_sdk/_tests/test_managed_tls_contract.py index a0633dd1..ec62ed68 100644 --- a/src/volcano_sdk/_tests/test_managed_tls_contract.py +++ b/src/volcano_sdk/_tests/test_managed_tls_contract.py @@ -49,8 +49,8 @@ ( FrontendCustomDomainTLSConfig( mode="byoc", - certificate_pem="certificate", - private_key_pem="private-key", + certificate_pem=BYOC_MATERIAL["certificate_pem"], + private_key_pem=BYOC_MATERIAL["private_key_pem"], ), {"mode": "byoc", **BYOC_MATERIAL}, ), @@ -105,7 +105,7 @@ def test_managed_tls_response_decodes_lifecycle_and_dns_records() -> None: @pytest.mark.parametrize( - ("model", "scope"), + ("model", "feed_fields"), [ (FrontendCustomDomainResponse, {}), ( @@ -118,16 +118,16 @@ def test_managed_tls_response_decodes_lifecycle_and_dns_records() -> None: }, ), ], - ids=["frontend", "project-feed"], + ids=["frontend-domain", "project-feed"], ) def test_failed_managed_domain_reports_its_failure_reason( model: type[FrontendCustomDomainResponse | ProjectFrontendCustomDomain], - scope: dict[str, dict[str, str]], + feed_fields: dict[str, dict[str, str]], ) -> None: failed = model.from_dict( { **MANAGED_PENDING, - **scope, + **feed_fields, "domain_status": "failed", "verification_status": "failed", "failure_reason": "ownership", From f54b532da5d6a7991196f0d7bb6596091c0d91b7 Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Mon, 5 Oct 2026 18:34:20 -0400 Subject: [PATCH 09/15] fix(api): vendor the final managed TLS contract Take the custom-domain TLS area from the final Hosting bundle: responses expose routing_target_hostname and deprecate required_routing_record, the routing record drops zone_apex_record_type, verification_status includes failed, and createFrontendCustomDomain returns a typed 409 with the caller's ownership record. Pin the new shapes in native tests. --- openapi/openapi.yaml | 66 +++++--- .../create_frontend_custom_domain.py | 63 ++++--- src/volcano_sdk/_generated/models/__init__.py | 4 +- ...onfig_frontend_custom_domain_tls_config.py | 14 +- .../create_frontend_custom_domain_request.py | 8 +- .../frontend_custom_domain_conflict_error.py | 116 +++++++++++++ .../models/frontend_custom_domain_response.py | 16 +- .../frontend_custom_domain_tls_config.py | 14 +- .../models/frontend_domain_routing_record.py | 37 +++-- ...in_routing_record_zone_apex_record_type.py | 10 -- .../frontend_domain_verification_record.py | 20 ++- ...onfig_frontend_custom_domain_tls_config.py | 3 +- .../models/project_config_custom_domain.py | 12 +- .../models/project_config_frontend.py | 9 +- .../models/project_frontend_custom_domain.py | 16 +- .../_tests/test_managed_tls_contract.py | 156 +++++++++++++----- 16 files changed, 427 insertions(+), 137 deletions(-) create mode 100644 src/volcano_sdk/_generated/models/frontend_custom_domain_conflict_error.py delete mode 100644 src/volcano_sdk/_generated/models/frontend_domain_routing_record_zone_apex_record_type.py diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 36f63aef..d2fcee27 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -4800,7 +4800,8 @@ paths: Configures one custom domain for a frontend. The default Volcano-generated frontend URL remains active. Wildcard Volcano frontend TLS remains valid and isolated from custom-domain certificate changes. - Managed TLS returns the DNS records currently required for setup. Volcano may require a tenant-specific TXT ownership challenge before returning the certificate authority's validation record. After ownership verification succeeds, Volcano permanently assigns the hostname to the account. A required but unverified ownership reservation expires after 72 hours. + Managed TLS returns the DNS records currently required for setup. Volcano may require a tenant-specific TXT ownership challenge before returning the certificate authority's validation record. After ownership verification succeeds, Volcano permanently assigns the hostname to the account, including after the domain is deleted. A required but unverified ownership reservation expires after 72 hours. + An unverified reservation does not block an account that proves ownership. When another account holds one, a managed TLS request gets `409` with `code: ownership_verification_required` and the caller's own `required_record`; after publishing it, the same request takes over the reservation. A BYOC request with a publicly trusted certificate and key for the hostname also takes it over; other BYOC requests get a `409` without `code`. Verified hostnames and BYOC domains are never taken over. operationId: createFrontendCustomDomain security: - UserToken: [] @@ -4851,11 +4852,11 @@ paths: schema: $ref: '#/components/schemas/Error' '409': - description: Conflict - custom domain already in use, still detaching, or frontend already has a custom domain + description: Conflict - custom domain already in use, reserved by another account until ownership is proven, still detaching, or frontend already has a custom domain content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: '#/components/schemas/FrontendCustomDomainConflictError' '500': description: Internal server error content: @@ -14606,7 +14607,7 @@ components: domain: type: string maxLength: 253 - description: Fully-qualified domain name (hostname only, no scheme/path) + description: 'Fully-qualified domain name (hostname only, no scheme/path). Managed TLS (`tls.mode: managed`) accepts at most 219 characters; BYOC accepts 253.' example: app.example.com tls: $ref: '#/components/schemas/FrontendCustomDomainTLSConfig' @@ -16442,13 +16443,19 @@ components: - failed failure_reason: type: string - description: Safe failure category returned for failed managed TLS provisioning. One of provider, certificate, ownership, or internal. Ownership means another account has already verified the hostname. + description: Failure category, present only when managed TLS setup has failed. Current values are provider, certificate, ownership, and internal; ownership means another account has already verified the hostname. Treat unrecognized values as internal. verification_records: type: array items: $ref: '#/components/schemas/FrontendDomainVerificationRecord' required_routing_record: - $ref: '#/components/schemas/FrontendDomainRoutingRecord' + allOf: + - $ref: '#/components/schemas/FrontendDomainRoutingRecord' + deprecated: true + description: Deprecated and no longer returned. Use routing_target_hostname as the DNS routing target. + routing_target_hostname: + type: string + description: DNS routing target hostname for this frontend. The DNS record type depends on whether the custom domain is a zone apex. effective_urls: type: array items: @@ -16469,7 +16476,7 @@ components: - updated_at FrontendCustomDomainTLSConfig: type: object - description: Set mode to managed for Volcano-issued TLS, or byoc with certificate_pem and private_key_pem. + description: 'TLS for a new custom domain. With `mode: managed`, Volcano issues and renews the certificate; omit every PEM field. With `mode: byoc`, send both `certificate_pem` and `private_key_pem`, plus an optional `certificate_chain_pem`.' additionalProperties: false not: anyOf: @@ -16507,20 +16514,19 @@ components: enum: - managed - byoc + description: managed for a Volcano-issued certificate; byoc to supply your own. certificate_pem: type: string maxLength: 65536 - x-go-type-skip-optional-pointer: true - description: Required. PEM-encoded certificate. + description: PEM-encoded certificate. Required when mode is byoc; not allowed when mode is managed. private_key_pem: type: string maxLength: 65536 - x-go-type-skip-optional-pointer: true - description: Required. PEM-encoded private key. + description: PEM-encoded private key. Required when mode is byoc; not allowed when mode is managed. certificate_chain_pem: type: string maxLength: 65536 - description: Optional PEM-encoded certificate chain. + description: Optional PEM-encoded certificate chain when mode is byoc; not allowed when mode is managed. required: - mode FrontendDeployment: @@ -16612,31 +16618,22 @@ components: - updated_at FrontendDomainRoutingRecord: type: object - additionalProperties: false properties: record_type: type: string - description: Use this record type when the hostname is not the apex of your DNS zone. enum: - CNAME - zone_apex_record_type: - type: string - description: At the apex of your DNS zone, use your provider's ALIAS, ANAME, or CNAME-flattening equivalent instead of a literal CNAME. - enum: - - ALIAS name: type: string value: type: string required: - record_type - - zone_apex_record_type - name - value FrontendDomainVerificationRecord: type: object description: The DNS records currently required for managed TLS. Volcano may require a tenant-specific TXT ownership record before returning a CNAME that authorizes certificate issuance and renewal. Clients must follow the records returned for the current lifecycle state instead of assuming a fixed sequence. - additionalProperties: false properties: name: type: string @@ -16648,6 +16645,14 @@ components: - name - type - value + FrontendCustomDomainConflictError: + description: 'Custom domain create conflict. With `code: ownership_verification_required`, another account holds an unverified managed TLS reservation for the hostname: publish `required_record` in DNS and send the same request again. The retry succeeds once Volcano can see the record. Other conflicts omit both fields.' + allOf: + - $ref: '#/components/schemas/Error' + - type: object + properties: + required_record: + $ref: '#/components/schemas/FrontendDomainVerificationRecord' FrontendUsageDailyEntry: type: object description: One day of request and error counts for a single frontend. @@ -18603,13 +18608,18 @@ components: additionalProperties: false description: | Custom domain with managed or BYOC TLS (SUPERAGENT plan). `tls` is required - when the domain is first created and optional afterwards. BYOC TLS - material is write-only and omitted from config export. + when the domain is first created and optional afterwards. For an existing + domain, omitting `tls` or sending only `tls.mode` keeps the stored + certificate; new BYOC material for the same domain rotates the + certificate in place (zero downtime). Changing `tls.mode` for the same + hostname, or the hostname of a managed domain, requires deleting the + domain first. BYOC TLS material is write-only; exports render only + `tls.mode`. properties: domain: type: string maxLength: 253 - description: Fully-qualified domain name (hostname only, no scheme/path) + description: 'Fully-qualified domain name (hostname only, no scheme/path). Managed TLS (`tls.mode: managed`) accepts at most 219 characters; BYOC accepts 253.' tls: $ref: '#/components/schemas/ProjectConfigFrontendCustomDomainTLSConfig' required: @@ -20456,6 +20466,7 @@ components: $ref: '#/components/schemas/ProjectConfigAuthPageLayouts' ManagedProjectConfigFrontendCustomDomainTLSConfig: type: object + description: Volcano issues and renews the certificate. Certificate fields are not allowed. additionalProperties: false properties: mode: @@ -20466,6 +20477,7 @@ components: - mode BYOCProjectConfigFrontendCustomDomainTLSConfig: type: object + description: 'Your own certificate. Send `certificate_pem` and `private_key_pem` together, with an optional `certificate_chain_pem`, to create the domain or rotate its certificate. For an existing BYOC domain, `mode: byoc` without certificate fields keeps the stored certificate; exports render only the mode.' additionalProperties: false not: anyOf: @@ -20493,15 +20505,15 @@ components: certificate_pem: type: string maxLength: 65536 - description: PEM-encoded certificate for BYOC create or rotation. Omitted from exports. + description: PEM-encoded certificate for create or rotation. Requires private_key_pem. Omitted from exports. private_key_pem: type: string maxLength: 65536 - description: PEM-encoded private key for BYOC create or rotation. Omitted from exports. + description: PEM-encoded private key for create or rotation. Requires certificate_pem. Omitted from exports. certificate_chain_pem: type: string maxLength: 65536 - description: Optional PEM-encoded certificate chain for BYOC. Omitted from exports. + description: Optional PEM-encoded certificate chain. Requires certificate_pem and private_key_pem. Omitted from exports. required: - mode ProjectConfigFrontendCustomDomainTLSConfig: diff --git a/src/volcano_sdk/_generated/api/frontends/create_frontend_custom_domain.py b/src/volcano_sdk/_generated/api/frontends/create_frontend_custom_domain.py index b8848a56..c85f10e7 100644 --- a/src/volcano_sdk/_generated/api/frontends/create_frontend_custom_domain.py +++ b/src/volcano_sdk/_generated/api/frontends/create_frontend_custom_domain.py @@ -10,6 +10,7 @@ from ...models.create_frontend_custom_domain_request import CreateFrontendCustomDomainRequest from ...models.error import Error +from ...models.frontend_custom_domain_conflict_error import FrontendCustomDomainConflictError from ...models.frontend_custom_domain_response import FrontendCustomDomainResponse from typing import cast from uuid import UUID @@ -44,7 +45,7 @@ def request_kwargs( -def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Error | FrontendCustomDomainResponse | None: +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Error | FrontendCustomDomainConflictError | FrontendCustomDomainResponse | None: if response.status_code == 200: response_200 = FrontendCustomDomainResponse.from_dict(response.json()) @@ -88,7 +89,7 @@ def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Res return response_404 if response.status_code == 409: - response_409 = Error.from_dict(response.json()) + response_409 = FrontendCustomDomainConflictError.from_dict(response.json()) @@ -114,7 +115,7 @@ def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Res return None -def build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Error | FrontendCustomDomainResponse]: +def build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Error | FrontendCustomDomainConflictError | FrontendCustomDomainResponse]: return Response( status_code=HTTPStatus(response.status_code), content=response.content, @@ -130,7 +131,7 @@ def sync_detailed( client: AuthenticatedClient, body: CreateFrontendCustomDomainRequest, -) -> Response[Error | FrontendCustomDomainResponse]: +) -> Response[Error | FrontendCustomDomainConflictError | FrontendCustomDomainResponse]: """ Configure frontend custom domain (SUPERAGENT) Configures one custom domain for a frontend. @@ -138,8 +139,14 @@ def sync_detailed( Wildcard Volcano frontend TLS remains valid and isolated from custom-domain certificate changes. Managed TLS returns the DNS records currently required for setup. Volcano may require a tenant- specific TXT ownership challenge before returning the certificate authority's validation record. - After ownership verification succeeds, Volcano permanently assigns the hostname to the account. A - required but unverified ownership reservation expires after 72 hours. + After ownership verification succeeds, Volcano permanently assigns the hostname to the account, + including after the domain is deleted. A required but unverified ownership reservation expires after + 72 hours. + An unverified reservation does not block an account that proves ownership. When another account + holds one, a managed TLS request gets `409` with `code: ownership_verification_required` and the + caller's own `required_record`; after publishing it, the same request takes over the reservation. A + BYOC request with a publicly trusted certificate and key for the hostname also takes it over; other + BYOC requests get a `409` without `code`. Verified hostnames and BYOC domains are never taken over. Args: id (UUID): @@ -151,7 +158,7 @@ def sync_detailed( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Error | FrontendCustomDomainResponse] + Response[Error | FrontendCustomDomainConflictError | FrontendCustomDomainResponse] """ @@ -175,7 +182,7 @@ def sync( client: AuthenticatedClient, body: CreateFrontendCustomDomainRequest, -) -> Error | FrontendCustomDomainResponse | None: +) -> Error | FrontendCustomDomainConflictError | FrontendCustomDomainResponse | None: """ Configure frontend custom domain (SUPERAGENT) Configures one custom domain for a frontend. @@ -183,8 +190,14 @@ def sync( Wildcard Volcano frontend TLS remains valid and isolated from custom-domain certificate changes. Managed TLS returns the DNS records currently required for setup. Volcano may require a tenant- specific TXT ownership challenge before returning the certificate authority's validation record. - After ownership verification succeeds, Volcano permanently assigns the hostname to the account. A - required but unverified ownership reservation expires after 72 hours. + After ownership verification succeeds, Volcano permanently assigns the hostname to the account, + including after the domain is deleted. A required but unverified ownership reservation expires after + 72 hours. + An unverified reservation does not block an account that proves ownership. When another account + holds one, a managed TLS request gets `409` with `code: ownership_verification_required` and the + caller's own `required_record`; after publishing it, the same request takes over the reservation. A + BYOC request with a publicly trusted certificate and key for the hostname also takes it over; other + BYOC requests get a `409` without `code`. Verified hostnames and BYOC domains are never taken over. Args: id (UUID): @@ -196,7 +209,7 @@ def sync( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Error | FrontendCustomDomainResponse + Error | FrontendCustomDomainConflictError | FrontendCustomDomainResponse """ @@ -215,7 +228,7 @@ async def asyncio_detailed( client: AuthenticatedClient, body: CreateFrontendCustomDomainRequest, -) -> Response[Error | FrontendCustomDomainResponse]: +) -> Response[Error | FrontendCustomDomainConflictError | FrontendCustomDomainResponse]: """ Configure frontend custom domain (SUPERAGENT) Configures one custom domain for a frontend. @@ -223,8 +236,14 @@ async def asyncio_detailed( Wildcard Volcano frontend TLS remains valid and isolated from custom-domain certificate changes. Managed TLS returns the DNS records currently required for setup. Volcano may require a tenant- specific TXT ownership challenge before returning the certificate authority's validation record. - After ownership verification succeeds, Volcano permanently assigns the hostname to the account. A - required but unverified ownership reservation expires after 72 hours. + After ownership verification succeeds, Volcano permanently assigns the hostname to the account, + including after the domain is deleted. A required but unverified ownership reservation expires after + 72 hours. + An unverified reservation does not block an account that proves ownership. When another account + holds one, a managed TLS request gets `409` with `code: ownership_verification_required` and the + caller's own `required_record`; after publishing it, the same request takes over the reservation. A + BYOC request with a publicly trusted certificate and key for the hostname also takes it over; other + BYOC requests get a `409` without `code`. Verified hostnames and BYOC domains are never taken over. Args: id (UUID): @@ -236,7 +255,7 @@ async def asyncio_detailed( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Error | FrontendCustomDomainResponse] + Response[Error | FrontendCustomDomainConflictError | FrontendCustomDomainResponse] """ @@ -260,7 +279,7 @@ async def asyncio( client: AuthenticatedClient, body: CreateFrontendCustomDomainRequest, -) -> Error | FrontendCustomDomainResponse | None: +) -> Error | FrontendCustomDomainConflictError | FrontendCustomDomainResponse | None: """ Configure frontend custom domain (SUPERAGENT) Configures one custom domain for a frontend. @@ -268,8 +287,14 @@ async def asyncio( Wildcard Volcano frontend TLS remains valid and isolated from custom-domain certificate changes. Managed TLS returns the DNS records currently required for setup. Volcano may require a tenant- specific TXT ownership challenge before returning the certificate authority's validation record. - After ownership verification succeeds, Volcano permanently assigns the hostname to the account. A - required but unverified ownership reservation expires after 72 hours. + After ownership verification succeeds, Volcano permanently assigns the hostname to the account, + including after the domain is deleted. A required but unverified ownership reservation expires after + 72 hours. + An unverified reservation does not block an account that proves ownership. When another account + holds one, a managed TLS request gets `409` with `code: ownership_verification_required` and the + caller's own `required_record`; after publishing it, the same request takes over the reservation. A + BYOC request with a publicly trusted certificate and key for the hostname also takes it over; other + BYOC requests get a `409` without `code`. Verified hostnames and BYOC domains are never taken over. Args: id (UUID): @@ -281,7 +306,7 @@ async def asyncio( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Error | FrontendCustomDomainResponse + Error | FrontendCustomDomainConflictError | FrontendCustomDomainResponse """ diff --git a/src/volcano_sdk/_generated/models/__init__.py b/src/volcano_sdk/_generated/models/__init__.py index 7076db7f..5da5c801 100644 --- a/src/volcano_sdk/_generated/models/__init__.py +++ b/src/volcano_sdk/_generated/models/__init__.py @@ -199,6 +199,7 @@ from .error import Error from .export_project_source_request import ExportProjectSourceRequest from .frontend import Frontend +from .frontend_custom_domain_conflict_error import FrontendCustomDomainConflictError from .frontend_custom_domain_response import FrontendCustomDomainResponse from .frontend_custom_domain_response_domain_status import FrontendCustomDomainResponseDomainStatus from .frontend_custom_domain_response_tls_mode import FrontendCustomDomainResponseTlsMode @@ -212,7 +213,6 @@ from .frontend_deployment_status import FrontendDeploymentStatus from .frontend_domain_routing_record import FrontendDomainRoutingRecord from .frontend_domain_routing_record_record_type import FrontendDomainRoutingRecordRecordType -from .frontend_domain_routing_record_zone_apex_record_type import FrontendDomainRoutingRecordZoneApexRecordType from .frontend_domain_verification_record import FrontendDomainVerificationRecord from .frontend_framework import FrontendFramework from .frontend_status import FrontendStatus @@ -750,6 +750,7 @@ "Error", "ExportProjectSourceRequest", "Frontend", + "FrontendCustomDomainConflictError", "FrontendCustomDomainResponse", "FrontendCustomDomainResponseDomainStatus", "FrontendCustomDomainResponseTlsMode", @@ -763,7 +764,6 @@ "FrontendDeploymentStatus", "FrontendDomainRoutingRecord", "FrontendDomainRoutingRecordRecordType", - "FrontendDomainRoutingRecordZoneApexRecordType", "FrontendDomainVerificationRecord", "FrontendFramework", "FrontendStatus", diff --git a/src/volcano_sdk/_generated/models/byoc_project_config_frontend_custom_domain_tls_config.py b/src/volcano_sdk/_generated/models/byoc_project_config_frontend_custom_domain_tls_config.py index f496af38..c9e70070 100644 --- a/src/volcano_sdk/_generated/models/byoc_project_config_frontend_custom_domain_tls_config.py +++ b/src/volcano_sdk/_generated/models/byoc_project_config_frontend_custom_domain_tls_config.py @@ -24,12 +24,18 @@ @_attrs_define class BYOCProjectConfigFrontendCustomDomainTLSConfig: - """ + """ Your own certificate. Send `certificate_pem` and `private_key_pem` together, with an optional + `certificate_chain_pem`, to create the domain or rotate its certificate. For an existing BYOC domain, `mode: byoc` + without certificate fields keeps the stored certificate; exports render only the mode. + Attributes: mode (BYOCProjectConfigFrontendCustomDomainTLSConfigMode): - certificate_pem (str | Unset): PEM-encoded certificate for BYOC create or rotation. Omitted from exports. - private_key_pem (str | Unset): PEM-encoded private key for BYOC create or rotation. Omitted from exports. - certificate_chain_pem (str | Unset): Optional PEM-encoded certificate chain for BYOC. Omitted from exports. + certificate_pem (str | Unset): PEM-encoded certificate for create or rotation. Requires private_key_pem. Omitted + from exports. + private_key_pem (str | Unset): PEM-encoded private key for create or rotation. Requires certificate_pem. Omitted + from exports. + certificate_chain_pem (str | Unset): Optional PEM-encoded certificate chain. Requires certificate_pem and + private_key_pem. Omitted from exports. """ mode: BYOCProjectConfigFrontendCustomDomainTLSConfigMode diff --git a/src/volcano_sdk/_generated/models/create_frontend_custom_domain_request.py b/src/volcano_sdk/_generated/models/create_frontend_custom_domain_request.py index 19b6eb3d..6d656bd4 100644 --- a/src/volcano_sdk/_generated/models/create_frontend_custom_domain_request.py +++ b/src/volcano_sdk/_generated/models/create_frontend_custom_domain_request.py @@ -25,9 +25,11 @@ class CreateFrontendCustomDomainRequest: """ Attributes: - domain (str): Fully-qualified domain name (hostname only, no scheme/path) Example: app.example.com. - tls (FrontendCustomDomainTLSConfig): Set mode to managed for Volcano-issued TLS, or byoc with certificate_pem - and private_key_pem. + domain (str): Fully-qualified domain name (hostname only, no scheme/path). Managed TLS (`tls.mode: managed`) + accepts at most 219 characters; BYOC accepts 253. Example: app.example.com. + tls (FrontendCustomDomainTLSConfig): TLS for a new custom domain. With `mode: managed`, Volcano issues and + renews the certificate; omit every PEM field. With `mode: byoc`, send both `certificate_pem` and + `private_key_pem`, plus an optional `certificate_chain_pem`. """ domain: str diff --git a/src/volcano_sdk/_generated/models/frontend_custom_domain_conflict_error.py b/src/volcano_sdk/_generated/models/frontend_custom_domain_conflict_error.py new file mode 100644 index 00000000..c271cd5c --- /dev/null +++ b/src/volcano_sdk/_generated/models/frontend_custom_domain_conflict_error.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +from ..types import UNSET, Unset +from typing import cast + +if TYPE_CHECKING: + from ..models.frontend_domain_verification_record import FrontendDomainVerificationRecord + + + + + +T = TypeVar("T", bound="FrontendCustomDomainConflictError") + + + +@_attrs_define +class FrontendCustomDomainConflictError: + """ Custom domain create conflict. With `code: ownership_verification_required`, another account holds an unverified + managed TLS reservation for the hostname: publish `required_record` in DNS and send the same request again. The + retry succeeds once Volcano can see the record. Other conflicts omit both fields. + + Attributes: + error (str): + code (str | Unset): Stable machine-readable error code when a specific recovery path is available. + required_record (FrontendDomainVerificationRecord | Unset): The DNS records currently required for managed TLS. + Volcano may require a tenant-specific TXT ownership record before returning a CNAME that authorizes certificate + issuance and renewal. Clients must follow the records returned for the current lifecycle state instead of + assuming a fixed sequence. + """ + + error: str + code: str | Unset = UNSET + required_record: FrontendDomainVerificationRecord | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + + + + + def to_dict(self) -> dict[str, Any]: + from ..models.frontend_domain_verification_record import FrontendDomainVerificationRecord + error = self.error + + code = self.code + + required_record: dict[str, Any] | Unset = UNSET + if not isinstance(self.required_record, Unset): + required_record = self.required_record.to_dict() + + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "error": error, + }) + if code is not UNSET: + field_dict["code"] = code + if required_record is not UNSET: + field_dict["required_record"] = required_record + + return field_dict + + + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.frontend_domain_verification_record import FrontendDomainVerificationRecord + d = dict(src_dict) + error = d.pop("error") + + code = d.pop("code", UNSET) + + _required_record = d.pop("required_record", UNSET) + required_record: FrontendDomainVerificationRecord | Unset + if isinstance(_required_record, Unset): + required_record = UNSET + else: + required_record = FrontendDomainVerificationRecord.from_dict(_required_record) + + + + + frontend_custom_domain_conflict_error = cls( + error=error, + code=code, + required_record=required_record, + ) + + + frontend_custom_domain_conflict_error.additional_properties = d + return frontend_custom_domain_conflict_error + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/volcano_sdk/_generated/models/frontend_custom_domain_response.py b/src/volcano_sdk/_generated/models/frontend_custom_domain_response.py index 34ba7396..76a056a1 100644 --- a/src/volcano_sdk/_generated/models/frontend_custom_domain_response.py +++ b/src/volcano_sdk/_generated/models/frontend_custom_domain_response.py @@ -41,11 +41,13 @@ class FrontendCustomDomainResponse: effective_urls (list[str]): created_at (datetime.datetime): updated_at (datetime.datetime): - failure_reason (str | Unset): Safe failure category returned for failed managed TLS provisioning. One of - provider, certificate, ownership, or internal. Ownership means another account has already verified the - hostname. + failure_reason (str | Unset): Failure category, present only when managed TLS setup has failed. Current values + are provider, certificate, ownership, and internal; ownership means another account has already verified the + hostname. Treat unrecognized values as internal. verification_records (list[FrontendDomainVerificationRecord] | Unset): required_routing_record (FrontendDomainRoutingRecord | Unset): + routing_target_hostname (str | Unset): DNS routing target hostname for this frontend. The DNS record type + depends on whether the custom domain is a zone apex. """ domain: str @@ -58,6 +60,7 @@ class FrontendCustomDomainResponse: failure_reason: str | Unset = UNSET verification_records: list[FrontendDomainVerificationRecord] | Unset = UNSET required_routing_record: FrontendDomainRoutingRecord | Unset = UNSET + routing_target_hostname: str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) @@ -98,6 +101,8 @@ def to_dict(self) -> dict[str, Any]: if not isinstance(self.required_routing_record, Unset): required_routing_record = self.required_routing_record.to_dict() + routing_target_hostname = self.routing_target_hostname + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) @@ -116,6 +121,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["verification_records"] = verification_records if required_routing_record is not UNSET: field_dict["required_routing_record"] = required_routing_record + if routing_target_hostname is not UNSET: + field_dict["routing_target_hostname"] = routing_target_hostname return field_dict @@ -180,6 +187,8 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + routing_target_hostname = d.pop("routing_target_hostname", UNSET) + frontend_custom_domain_response = cls( domain=domain, tls_mode=tls_mode, @@ -191,6 +200,7 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: failure_reason=failure_reason, verification_records=verification_records, required_routing_record=required_routing_record, + routing_target_hostname=routing_target_hostname, ) diff --git a/src/volcano_sdk/_generated/models/frontend_custom_domain_tls_config.py b/src/volcano_sdk/_generated/models/frontend_custom_domain_tls_config.py index eb36cfb7..44a2ca77 100644 --- a/src/volcano_sdk/_generated/models/frontend_custom_domain_tls_config.py +++ b/src/volcano_sdk/_generated/models/frontend_custom_domain_tls_config.py @@ -24,13 +24,17 @@ @_attrs_define class FrontendCustomDomainTLSConfig: - """ Set mode to managed for Volcano-issued TLS, or byoc with certificate_pem and private_key_pem. + """ TLS for a new custom domain. With `mode: managed`, Volcano issues and renews the certificate; omit every PEM field. + With `mode: byoc`, send both `certificate_pem` and `private_key_pem`, plus an optional `certificate_chain_pem`. Attributes: - mode (FrontendCustomDomainTLSConfigMode): - certificate_pem (str | Unset): Required. PEM-encoded certificate. - private_key_pem (str | Unset): Required. PEM-encoded private key. - certificate_chain_pem (str | Unset): Optional PEM-encoded certificate chain. + mode (FrontendCustomDomainTLSConfigMode): managed for a Volcano-issued certificate; byoc to supply your own. + certificate_pem (str | Unset): PEM-encoded certificate. Required when mode is byoc; not allowed when mode is + managed. + private_key_pem (str | Unset): PEM-encoded private key. Required when mode is byoc; not allowed when mode is + managed. + certificate_chain_pem (str | Unset): Optional PEM-encoded certificate chain when mode is byoc; not allowed when + mode is managed. """ mode: FrontendCustomDomainTLSConfigMode diff --git a/src/volcano_sdk/_generated/models/frontend_domain_routing_record.py b/src/volcano_sdk/_generated/models/frontend_domain_routing_record.py index 4a5b20be..d4cc2326 100644 --- a/src/volcano_sdk/_generated/models/frontend_domain_routing_record.py +++ b/src/volcano_sdk/_generated/models/frontend_domain_routing_record.py @@ -10,8 +10,6 @@ from ..models.frontend_domain_routing_record_record_type import check_frontend_domain_routing_record_record_type from ..models.frontend_domain_routing_record_record_type import FrontendDomainRoutingRecordRecordType -from ..models.frontend_domain_routing_record_zone_apex_record_type import check_frontend_domain_routing_record_zone_apex_record_type -from ..models.frontend_domain_routing_record_zone_apex_record_type import FrontendDomainRoutingRecordZoneApexRecordType from typing import cast @@ -27,18 +25,15 @@ class FrontendDomainRoutingRecord: """ Attributes: - record_type (FrontendDomainRoutingRecordRecordType): Use this record type when the hostname is not the apex of - your DNS zone. - zone_apex_record_type (FrontendDomainRoutingRecordZoneApexRecordType): At the apex of your DNS zone, use your - provider's ALIAS, ANAME, or CNAME-flattening equivalent instead of a literal CNAME. + record_type (FrontendDomainRoutingRecordRecordType): name (str): value (str): """ record_type: FrontendDomainRoutingRecordRecordType - zone_apex_record_type: FrontendDomainRoutingRecordZoneApexRecordType name: str value: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) @@ -47,18 +42,15 @@ class FrontendDomainRoutingRecord: def to_dict(self) -> dict[str, Any]: record_type: str = self.record_type - zone_apex_record_type: str = self.zone_apex_record_type - name = self.name value = self.value field_dict: dict[str, Any] = {} - + field_dict.update(self.additional_properties) field_dict.update({ "record_type": record_type, - "zone_apex_record_type": zone_apex_record_type, "name": name, "value": value, }) @@ -75,21 +67,32 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: - zone_apex_record_type = check_frontend_domain_routing_record_zone_apex_record_type(d.pop("zone_apex_record_type")) - - - - name = d.pop("name") value = d.pop("value") frontend_domain_routing_record = cls( record_type=record_type, - zone_apex_record_type=zone_apex_record_type, name=name, value=value, ) + + frontend_domain_routing_record.additional_properties = d return frontend_domain_routing_record + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/volcano_sdk/_generated/models/frontend_domain_routing_record_zone_apex_record_type.py b/src/volcano_sdk/_generated/models/frontend_domain_routing_record_zone_apex_record_type.py deleted file mode 100644 index 7d33e8ee..00000000 --- a/src/volcano_sdk/_generated/models/frontend_domain_routing_record_zone_apex_record_type.py +++ /dev/null @@ -1,10 +0,0 @@ -from typing import Literal - -FrontendDomainRoutingRecordZoneApexRecordType = Literal['ALIAS'] - -FRONTEND_DOMAIN_ROUTING_RECORD_ZONE_APEX_RECORD_TYPE_VALUES: set[FrontendDomainRoutingRecordZoneApexRecordType] = { 'ALIAS', } - -def check_frontend_domain_routing_record_zone_apex_record_type(value: str) -> FrontendDomainRoutingRecordZoneApexRecordType: - if value in FRONTEND_DOMAIN_ROUTING_RECORD_ZONE_APEX_RECORD_TYPE_VALUES: - return value - raise TypeError(f"Unexpected value {value!r}. Expected one of {FRONTEND_DOMAIN_ROUTING_RECORD_ZONE_APEX_RECORD_TYPE_VALUES!r}") diff --git a/src/volcano_sdk/_generated/models/frontend_domain_verification_record.py b/src/volcano_sdk/_generated/models/frontend_domain_verification_record.py index 93b5dacd..462c72ae 100644 --- a/src/volcano_sdk/_generated/models/frontend_domain_verification_record.py +++ b/src/volcano_sdk/_generated/models/frontend_domain_verification_record.py @@ -33,6 +33,7 @@ class FrontendDomainVerificationRecord: name: str type_: str value: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) @@ -47,7 +48,7 @@ def to_dict(self) -> dict[str, Any]: field_dict: dict[str, Any] = {} - + field_dict.update(self.additional_properties) field_dict.update({ "name": name, "type": type_, @@ -73,5 +74,22 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: value=value, ) + + frontend_domain_verification_record.additional_properties = d return frontend_domain_verification_record + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/volcano_sdk/_generated/models/managed_project_config_frontend_custom_domain_tls_config.py b/src/volcano_sdk/_generated/models/managed_project_config_frontend_custom_domain_tls_config.py index 9ed9b2fe..2c07eb0b 100644 --- a/src/volcano_sdk/_generated/models/managed_project_config_frontend_custom_domain_tls_config.py +++ b/src/volcano_sdk/_generated/models/managed_project_config_frontend_custom_domain_tls_config.py @@ -23,7 +23,8 @@ @_attrs_define class ManagedProjectConfigFrontendCustomDomainTLSConfig: - """ + """ Volcano issues and renews the certificate. Certificate fields are not allowed. + Attributes: mode (ManagedProjectConfigFrontendCustomDomainTLSConfigMode): """ diff --git a/src/volcano_sdk/_generated/models/project_config_custom_domain.py b/src/volcano_sdk/_generated/models/project_config_custom_domain.py index fccea446..31e703ad 100644 --- a/src/volcano_sdk/_generated/models/project_config_custom_domain.py +++ b/src/volcano_sdk/_generated/models/project_config_custom_domain.py @@ -26,11 +26,17 @@ @_attrs_define class ProjectConfigCustomDomain: """ Custom domain with managed or BYOC TLS (SUPERAGENT plan). `tls` is required - when the domain is first created and optional afterwards. BYOC TLS - material is write-only and omitted from config export. + when the domain is first created and optional afterwards. For an existing + domain, omitting `tls` or sending only `tls.mode` keeps the stored + certificate; new BYOC material for the same domain rotates the + certificate in place (zero downtime). Changing `tls.mode` for the same + hostname, or the hostname of a managed domain, requires deleting the + domain first. BYOC TLS material is write-only; exports render only + `tls.mode`. Attributes: - domain (str): Fully-qualified domain name (hostname only, no scheme/path) + domain (str): Fully-qualified domain name (hostname only, no scheme/path). Managed TLS (`tls.mode: managed`) + accepts at most 219 characters; BYOC accepts 253. tls (BYOCProjectConfigFrontendCustomDomainTLSConfig | ManagedProjectConfigFrontendCustomDomainTLSConfig | Unset): """ diff --git a/src/volcano_sdk/_generated/models/project_config_frontend.py b/src/volcano_sdk/_generated/models/project_config_frontend.py index cf521d3f..f0c3b15f 100644 --- a/src/volcano_sdk/_generated/models/project_config_frontend.py +++ b/src/volcano_sdk/_generated/models/project_config_frontend.py @@ -32,8 +32,13 @@ class ProjectConfigFrontend: name (str): custom_domain (ProjectConfigCustomDomain | Unset): Custom domain with managed or BYOC TLS (SUPERAGENT plan). `tls` is required - when the domain is first created and optional afterwards. BYOC TLS - material is write-only and omitted from config export. + when the domain is first created and optional afterwards. For an existing + domain, omitting `tls` or sending only `tls.mode` keeps the stored + certificate; new BYOC material for the same domain rotates the + certificate in place (zero downtime). Changing `tls.mode` for the same + hostname, or the hostname of a managed domain, requires deleting the + domain first. BYOC TLS material is write-only; exports render only + `tls.mode`. """ name: str diff --git a/src/volcano_sdk/_generated/models/project_frontend_custom_domain.py b/src/volcano_sdk/_generated/models/project_frontend_custom_domain.py index fc549bb0..e472000a 100644 --- a/src/volcano_sdk/_generated/models/project_frontend_custom_domain.py +++ b/src/volcano_sdk/_generated/models/project_frontend_custom_domain.py @@ -44,11 +44,13 @@ class ProjectFrontendCustomDomain: updated_at (datetime.datetime): frontend (ProjectFrontendCustomDomainFrontend): The frontend this custom domain is attached to. Inlined to avoid a second fetch from the project-scoped feed. - failure_reason (str | Unset): Safe failure category returned for failed managed TLS provisioning. One of - provider, certificate, ownership, or internal. Ownership means another account has already verified the - hostname. + failure_reason (str | Unset): Failure category, present only when managed TLS setup has failed. Current values + are provider, certificate, ownership, and internal; ownership means another account has already verified the + hostname. Treat unrecognized values as internal. verification_records (list[FrontendDomainVerificationRecord] | Unset): required_routing_record (FrontendDomainRoutingRecord | Unset): + routing_target_hostname (str | Unset): DNS routing target hostname for this frontend. The DNS record type + depends on whether the custom domain is a zone apex. """ domain: str @@ -62,6 +64,7 @@ class ProjectFrontendCustomDomain: failure_reason: str | Unset = UNSET verification_records: list[FrontendDomainVerificationRecord] | Unset = UNSET required_routing_record: FrontendDomainRoutingRecord | Unset = UNSET + routing_target_hostname: str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) @@ -105,6 +108,8 @@ def to_dict(self) -> dict[str, Any]: if not isinstance(self.required_routing_record, Unset): required_routing_record = self.required_routing_record.to_dict() + routing_target_hostname = self.routing_target_hostname + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) @@ -124,6 +129,8 @@ def to_dict(self) -> dict[str, Any]: field_dict["verification_records"] = verification_records if required_routing_record is not UNSET: field_dict["required_routing_record"] = required_routing_record + if routing_target_hostname is not UNSET: + field_dict["routing_target_hostname"] = routing_target_hostname return field_dict @@ -194,6 +201,8 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + routing_target_hostname = d.pop("routing_target_hostname", UNSET) + project_frontend_custom_domain = cls( domain=domain, tls_mode=tls_mode, @@ -206,6 +215,7 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: failure_reason=failure_reason, verification_records=verification_records, required_routing_record=required_routing_record, + routing_target_hostname=routing_target_hostname, ) diff --git a/src/volcano_sdk/_tests/test_managed_tls_contract.py b/src/volcano_sdk/_tests/test_managed_tls_contract.py index ec62ed68..3e8a3e3d 100644 --- a/src/volcano_sdk/_tests/test_managed_tls_contract.py +++ b/src/volcano_sdk/_tests/test_managed_tls_contract.py @@ -1,9 +1,17 @@ +import json +from uuid import UUID + +import httpx import pytest from attrs import AttrsInstance, fields_dict +from volcano_sdk._generated.api.frontends import create_frontend_custom_domain +from volcano_sdk._generated.client import AuthenticatedClient from volcano_sdk._generated.models import ( BYOCProjectConfigFrontendCustomDomainTLSConfig, CreateFrontendCustomDomainRequest, + Error, + FrontendCustomDomainConflictError, FrontendCustomDomainResponse, FrontendCustomDomainTLSConfig, FrontendDomainRoutingRecord, @@ -14,32 +22,67 @@ ) from volcano_sdk._generated.types import UNSET +PROJECT_ID = UUID("00000000-0000-4000-8000-000000000001") +FRONTEND_ID = UUID("00000000-0000-4000-8000-000000000002") BYOC_MATERIAL = { "certificate_pem": "certificate", "private_key_pem": "private-key", } -ROUTING_RECORD = { - "record_type": "CNAME", - "zone_apex_record_type": "ALIAS", - "name": "app.example.com", - "value": "frontend.frontends.volcano.dev", +OWNERSHIP_RECORD = { + "name": "_volcano-ownership.app.example.com", + "type": "TXT", + "value": "volcano-ownership=token", } VALIDATION_RECORD = { "name": "_token.app.example.com", "type": "CNAME", "value": "_validation.volcano.dev", } -MANAGED_PENDING = { - "domain": "app.example.com", - "tls_mode": "managed", - "domain_status": "pending_verification", - "verification_status": "pending", - "verification_records": [VALIDATION_RECORD], - "required_routing_record": ROUTING_RECORD, - "effective_urls": ["https://frontend.frontends.volcano.dev/"], - "created_at": "2026-09-02T12:00:00Z", - "updated_at": "2026-09-02T12:00:00Z", -} +ROUTING_TARGET = "frontend.frontends.volcano.dev" + + +def managed_pending() -> dict[str, object]: + return { + "domain": "app.example.com", + "tls_mode": "managed", + "domain_status": "pending_verification", + "verification_status": "pending", + "verification_records": [dict(VALIDATION_RECORD)], + "routing_target_hostname": ROUTING_TARGET, + "effective_urls": [f"https://{ROUTING_TARGET}/"], + "created_at": "2026-09-02T12:00:00+00:00", + "updated_at": "2026-09-02T12:00:00+00:00", + } + + +def create_managed_domain( + status: int, payload: dict[str, object] +) -> Error | FrontendCustomDomainConflictError | FrontendCustomDomainResponse | None: + sent: list[httpx.Request] = [] + + def respond(request: httpx.Request) -> httpx.Response: + sent.append(request) + return httpx.Response(status, json=payload) + + with AuthenticatedClient( + base_url="https://api.example.com", + token="access-token", + httpx_args={"transport": httpx.MockTransport(respond)}, + ) as client: + result = create_frontend_custom_domain.sync_detailed( + PROJECT_ID, + FRONTEND_ID, + client=client, + body=CreateFrontendCustomDomainRequest( + domain="app.example.com", + tls=FrontendCustomDomainTLSConfig(mode="managed"), + ), + ) + + assert [json.loads(request.content) for request in sent] == [ + {"domain": "app.example.com", "tls": {"mode": "managed"}} + ] + return result.parsed @pytest.mark.parametrize( @@ -74,6 +117,7 @@ def test_create_request_encodes_the_selected_tls_mode( [ FrontendCustomDomainResponse, ProjectFrontendCustomDomain, + FrontendCustomDomainConflictError, FrontendDomainVerificationRecord, FrontendDomainRoutingRecord, ManagedProjectConfigFrontendCustomDomainTLSConfig, @@ -89,19 +133,42 @@ def test_material_free_models_do_not_declare_certificate_or_key_fields( ] -def test_managed_tls_response_decodes_lifecycle_and_dns_records() -> None: - response = FrontendCustomDomainResponse.from_dict(MANAGED_PENDING) +def test_create_operation_decodes_the_managed_lifecycle_and_routing_target() -> None: + created = create_managed_domain(201, managed_pending()) - assert response.tls_mode == "managed" - assert response.domain_status == "pending_verification" - assert response.verification_status == "pending" - assert response.failure_reason is UNSET - assert isinstance(response.verification_records, list) - assert [record.to_dict() for record in response.verification_records] == [ - VALIDATION_RECORD - ] - assert isinstance(response.required_routing_record, FrontendDomainRoutingRecord) - assert response.required_routing_record.to_dict() == ROUTING_RECORD + assert isinstance(created, FrontendCustomDomainResponse) + assert created.tls_mode == "managed" + assert created.domain_status == "pending_verification" + assert created.verification_status == "pending" + assert created.failure_reason is UNSET + assert created.required_routing_record is UNSET + assert created.routing_target_hostname == ROUTING_TARGET + assert created.to_dict() == managed_pending() + + +def test_create_conflict_carries_the_callers_ownership_record() -> None: + conflict = create_managed_domain( + 409, + { + "error": "hostname is reserved by another account", + "code": "ownership_verification_required", + "required_record": dict(OWNERSHIP_RECORD), + }, + ) + + assert isinstance(conflict, FrontendCustomDomainConflictError) + assert conflict.code == "ownership_verification_required" + assert isinstance(conflict.required_record, FrontendDomainVerificationRecord) + assert conflict.required_record.to_dict() == OWNERSHIP_RECORD + + +def test_other_create_conflicts_omit_the_ownership_fields() -> None: + conflict = create_managed_domain(409, {"error": "custom domain already in use"}) + + assert isinstance(conflict, FrontendCustomDomainConflictError) + assert conflict.error == "custom domain already in use" + assert conflict.code is UNSET + assert conflict.required_record is UNSET @pytest.mark.parametrize( @@ -110,12 +177,7 @@ def test_managed_tls_response_decodes_lifecycle_and_dns_records() -> None: (FrontendCustomDomainResponse, {}), ( ProjectFrontendCustomDomain, - { - "frontend": { - "id": "00000000-0000-4000-8000-000000000001", - "name": "web", - } - }, + {"frontend": {"id": str(FRONTEND_ID), "name": "web"}}, ), ], ids=["frontend-domain", "project-feed"], @@ -126,7 +188,7 @@ def test_failed_managed_domain_reports_its_failure_reason( ) -> None: failed = model.from_dict( { - **MANAGED_PENDING, + **managed_pending(), **feed_fields, "domain_status": "failed", "verification_status": "failed", @@ -137,8 +199,28 @@ def test_failed_managed_domain_reports_its_failure_reason( assert failed.domain_status == "failed" assert failed.verification_status == "failed" assert failed.failure_reason == "ownership" - assert isinstance(failed.required_routing_record, FrontendDomainRoutingRecord) - assert failed.required_routing_record.to_dict() == ROUTING_RECORD + assert failed.routing_target_hostname == ROUTING_TARGET + + +def test_deprecated_routing_record_from_older_servers_still_decodes() -> None: + legacy_record = { + "record_type": "CNAME", + "name": "app.example.com", + "value": ROUTING_TARGET, + } + legacy = { + key: value + for key, value in managed_pending().items() + if key != "routing_target_hostname" + } + + response = FrontendCustomDomainResponse.from_dict( + {**legacy, "required_routing_record": legacy_record} + ) + + assert response.routing_target_hostname is UNSET + assert isinstance(response.required_routing_record, FrontendDomainRoutingRecord) + assert response.required_routing_record.to_dict() == legacy_record @pytest.mark.parametrize( From 28e6b71c66f586bc310a9c9d2e4c24db80ce4746 Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Mon, 5 Oct 2026 18:56:46 -0400 Subject: [PATCH 10/15] test(api): cover BYOC chains, repeat creates, and failed or legacy domain feeds --- .../_tests/test_managed_tls_contract.py | 51 ++++++++++++++----- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/src/volcano_sdk/_tests/test_managed_tls_contract.py b/src/volcano_sdk/_tests/test_managed_tls_contract.py index 3e8a3e3d..7bb734ec 100644 --- a/src/volcano_sdk/_tests/test_managed_tls_contract.py +++ b/src/volcano_sdk/_tests/test_managed_tls_contract.py @@ -20,14 +20,16 @@ ProjectConfigCustomDomain, ProjectFrontendCustomDomain, ) -from volcano_sdk._generated.types import UNSET +from volcano_sdk._generated.types import UNSET, Unset PROJECT_ID = UUID("00000000-0000-4000-8000-000000000001") FRONTEND_ID = UUID("00000000-0000-4000-8000-000000000002") BYOC_MATERIAL = { "certificate_pem": "certificate", "private_key_pem": "private-key", + "certificate_chain_pem": "chain", } +PROJECT_FEED_FIELDS = {"frontend": {"id": str(FRONTEND_ID), "name": "web"}} OWNERSHIP_RECORD = { "name": "_volcano-ownership.app.example.com", "type": "TXT", @@ -94,6 +96,7 @@ def respond(request: httpx.Request) -> httpx.Response: mode="byoc", certificate_pem=BYOC_MATERIAL["certificate_pem"], private_key_pem=BYOC_MATERIAL["private_key_pem"], + certificate_chain_pem=BYOC_MATERIAL["certificate_chain_pem"], ), {"mode": "byoc", **BYOC_MATERIAL}, ), @@ -133,8 +136,9 @@ def test_material_free_models_do_not_declare_certificate_or_key_fields( ] -def test_create_operation_decodes_the_managed_lifecycle_and_routing_target() -> None: - created = create_managed_domain(201, managed_pending()) +@pytest.mark.parametrize("status", [200, 201], ids=["already-configured", "created"]) +def test_create_operation_decodes_a_pending_managed_domain(status: int) -> None: + created = create_managed_domain(status, managed_pending()) assert isinstance(created, FrontendCustomDomainResponse) assert created.tls_mode == "managed" @@ -171,38 +175,59 @@ def test_other_create_conflicts_omit_the_ownership_fields() -> None: assert conflict.required_record is UNSET +@pytest.mark.parametrize( + ("tls_mode", "failure_fields", "failure_reason"), + [ + ("managed", {"failure_reason": "ownership"}, "ownership"), + ("byoc", {}, UNSET), + ], + ids=["managed", "byoc"], +) @pytest.mark.parametrize( ("model", "feed_fields"), [ (FrontendCustomDomainResponse, {}), - ( - ProjectFrontendCustomDomain, - {"frontend": {"id": str(FRONTEND_ID), "name": "web"}}, - ), + (ProjectFrontendCustomDomain, PROJECT_FEED_FIELDS), ], ids=["frontend-domain", "project-feed"], ) -def test_failed_managed_domain_reports_its_failure_reason( +def test_failed_domain_decodes_with_and_without_failure_reason( model: type[FrontendCustomDomainResponse | ProjectFrontendCustomDomain], feed_fields: dict[str, dict[str, str]], + tls_mode: str, + failure_fields: dict[str, str], + failure_reason: str | Unset, ) -> None: failed = model.from_dict( { **managed_pending(), **feed_fields, + **failure_fields, + "tls_mode": tls_mode, "domain_status": "failed", "verification_status": "failed", - "failure_reason": "ownership", } ) + assert failed.tls_mode == tls_mode assert failed.domain_status == "failed" assert failed.verification_status == "failed" - assert failed.failure_reason == "ownership" + assert failed.failure_reason == failure_reason assert failed.routing_target_hostname == ROUTING_TARGET -def test_deprecated_routing_record_from_older_servers_still_decodes() -> None: +@pytest.mark.parametrize( + ("model", "feed_fields"), + [ + (FrontendCustomDomainResponse, {}), + (ProjectFrontendCustomDomain, PROJECT_FEED_FIELDS), + ], + ids=["frontend-domain", "project-feed"], +) +def test_deprecated_routing_record_from_older_servers_still_decodes( + model: type[FrontendCustomDomainResponse | ProjectFrontendCustomDomain], + feed_fields: dict[str, dict[str, str]], +) -> None: legacy_record = { "record_type": "CNAME", "name": "app.example.com", @@ -214,8 +239,8 @@ def test_deprecated_routing_record_from_older_servers_still_decodes() -> None: if key != "routing_target_hostname" } - response = FrontendCustomDomainResponse.from_dict( - {**legacy, "required_routing_record": legacy_record} + response = model.from_dict( + {**legacy, **feed_fields, "required_routing_record": legacy_record} ) assert response.routing_target_hostname is UNSET From 588b565fa612a23994fa43eec0a39811b0dab1de Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Mon, 5 Oct 2026 19:11:54 -0400 Subject: [PATCH 11/15] test(api): pin managed TLS fixtures to the documented Hosting responses --- .../_tests/test_managed_tls_contract.py | 191 ++++++++++++------ 1 file changed, 133 insertions(+), 58 deletions(-) diff --git a/src/volcano_sdk/_tests/test_managed_tls_contract.py b/src/volcano_sdk/_tests/test_managed_tls_contract.py index 7bb734ec..5f07fe94 100644 --- a/src/volcano_sdk/_tests/test_managed_tls_contract.py +++ b/src/volcano_sdk/_tests/test_managed_tls_contract.py @@ -1,4 +1,5 @@ import json +from datetime import UTC, datetime from uuid import UUID import httpx @@ -29,31 +30,41 @@ "private_key_pem": "private-key", "certificate_chain_pem": "chain", } -PROJECT_FEED_FIELDS = {"frontend": {"id": str(FRONTEND_ID), "name": "web"}} -OWNERSHIP_RECORD = { - "name": "_volcano-ownership.app.example.com", +OWNERSHIP_CHALLENGE = { + "name": "_volcano.app.example.com", "type": "TXT", - "value": "volcano-ownership=token", + "value": "volcano-domain-verification=0123456789abcdef0123456789abcdef", } -VALIDATION_RECORD = { - "name": "_token.app.example.com", +CERTIFICATE_VALIDATION = { + "name": "_acme-challenge.app.example.com", "type": "CNAME", - "value": "_validation.volcano.dev", + "value": "7d7b8a4e-7418-4a86-8e16-670870f00fa0.acme.frontends.volcano.run", } -ROUTING_TARGET = "frontend.frontends.volcano.dev" +ROUTING_TARGET = "my-frontend.frontends.volcano.run" +CREATED_AT = datetime(2026, 9, 2, 12, tzinfo=UTC) +RESPONSE_MODELS = pytest.mark.parametrize( + ("model", "feed_fields"), + [ + (FrontendCustomDomainResponse, {}), + ( + ProjectFrontendCustomDomain, + {"frontend": {"id": str(FRONTEND_ID), "name": "my-frontend"}}, + ), + ], + ids=["frontend-domain", "project-feed"], +) -def managed_pending() -> dict[str, object]: +def domain_payload() -> dict[str, object]: return { "domain": "app.example.com", "tls_mode": "managed", "domain_status": "pending_verification", "verification_status": "pending", - "verification_records": [dict(VALIDATION_RECORD)], "routing_target_hostname": ROUTING_TARGET, "effective_urls": [f"https://{ROUTING_TARGET}/"], - "created_at": "2026-09-02T12:00:00+00:00", - "updated_at": "2026-09-02T12:00:00+00:00", + "created_at": "2026-09-02T12:00:00Z", + "updated_at": "2026-09-02T12:00:00Z", } @@ -81,9 +92,21 @@ def respond(request: httpx.Request) -> httpx.Response: ), ) - assert [json.loads(request.content) for request in sent] == [ - {"domain": "app.example.com", "tls": {"mode": "managed"}} + assert [ + (request.method, request.url.path, request.headers["Authorization"]) + for request in sent + ] == [ + ( + "POST", + f"/projects/{PROJECT_ID}/frontends/{FRONTEND_ID}/domain", + "Bearer access-token", + ) ] + assert json.loads(sent[0].content) == { + "domain": "app.example.com", + "tls": {"mode": "managed"}, + } + assert result.status_code == status return result.parsed @@ -126,44 +149,75 @@ def test_create_request_encodes_the_selected_tls_mode( ManagedProjectConfigFrontendCustomDomainTLSConfig, ], ) -def test_material_free_models_do_not_declare_certificate_or_key_fields( +def test_models_without_tls_material_declare_no_pem_or_private_key_fields( model: type[AttrsInstance], ) -> None: assert not [ name for name in fields_dict(model) - if any(marker in name for marker in ("cert", "key", "pem")) + if name.endswith("_pem") or "private_key" in name ] @pytest.mark.parametrize("status", [200, 201], ids=["already-configured", "created"]) -def test_create_operation_decodes_a_pending_managed_domain(status: int) -> None: - created = create_managed_domain(status, managed_pending()) +def test_create_operation_decodes_the_ownership_challenge(status: int) -> None: + created = create_managed_domain( + status, + {**domain_payload(), "verification_records": [OWNERSHIP_CHALLENGE]}, + ) assert isinstance(created, FrontendCustomDomainResponse) assert created.tls_mode == "managed" assert created.domain_status == "pending_verification" assert created.verification_status == "pending" - assert created.failure_reason is UNSET - assert created.required_routing_record is UNSET + assert isinstance(created.verification_records, list) + assert [record.to_dict() for record in created.verification_records] == [ + OWNERSHIP_CHALLENGE + ] assert created.routing_target_hostname == ROUTING_TARGET - assert created.to_dict() == managed_pending() + assert created.required_routing_record is UNSET + assert created.failure_reason is UNSET + assert created.created_at == CREATED_AT -def test_create_conflict_carries_the_callers_ownership_record() -> None: - conflict = create_managed_domain( - 409, +@RESPONSE_MODELS +def test_pending_domain_decodes_the_certificate_validation_record( + model: type[FrontendCustomDomainResponse | ProjectFrontendCustomDomain], + feed_fields: dict[str, dict[str, str]], +) -> None: + pending = model.from_dict( { - "error": "hostname is reserved by another account", - "code": "ownership_verification_required", - "required_record": dict(OWNERSHIP_RECORD), - }, + **domain_payload(), + **feed_fields, + "verification_records": [CERTIFICATE_VALIDATION], + } ) + assert isinstance(pending.verification_records, list) + assert [record.to_dict() for record in pending.verification_records] == [ + CERTIFICATE_VALIDATION + ] + assert pending.routing_target_hostname == ROUTING_TARGET + + +def test_create_conflict_carries_the_callers_ownership_record() -> None: + payload: dict[str, object] = { + "error": ( + "custom domain is reserved by another account until its ownership " + "is verified" + ), + "code": "ownership_verification_required", + "required_record": OWNERSHIP_CHALLENGE, + } + + conflict = create_managed_domain(409, payload) + assert isinstance(conflict, FrontendCustomDomainConflictError) + assert conflict.error == payload["error"] assert conflict.code == "ownership_verification_required" assert isinstance(conflict.required_record, FrontendDomainVerificationRecord) - assert conflict.required_record.to_dict() == OWNERSHIP_RECORD + assert conflict.required_record.to_dict() == OWNERSHIP_CHALLENGE + assert conflict.to_dict() == payload def test_other_create_conflicts_omit_the_ownership_fields() -> None: @@ -175,56 +229,76 @@ def test_other_create_conflicts_omit_the_ownership_fields() -> None: assert conflict.required_record is UNSET +@pytest.mark.parametrize("status", [400, 503], ids=["bad-request", "unavailable"]) +def test_other_create_errors_keep_the_plain_error_shape(status: int) -> None: + error = create_managed_domain(status, {"error": "custom domain rejected"}) + + assert type(error) is Error + assert error.error == "custom domain rejected" + + @pytest.mark.parametrize( - ("tls_mode", "failure_fields", "failure_reason"), + ("failure_fields", "failure_reason"), [ - ("managed", {"failure_reason": "ownership"}, "ownership"), - ("byoc", {}, UNSET), + ( + { + "tls_mode": "managed", + "verification_status": "failed", + "failure_reason": "ownership", + }, + "ownership", + ), + ( + { + "tls_mode": "managed", + "verification_status": "verified", + "failure_reason": "certificate", + }, + "certificate", + ), + ( + { + "tls_mode": "managed", + "verification_status": "failed", + "failure_reason": "quota", + }, + "quota", + ), + ({"tls_mode": "managed", "verification_status": "failed"}, UNSET), + ({"tls_mode": "byoc", "verification_status": "failed"}, UNSET), ], - ids=["managed", "byoc"], -) -@pytest.mark.parametrize( - ("model", "feed_fields"), - [ - (FrontendCustomDomainResponse, {}), - (ProjectFrontendCustomDomain, PROJECT_FEED_FIELDS), + ids=[ + "managed", + "managed-after-verification", + "unrecognized-reason", + "managed-uncategorized", + "byoc", ], - ids=["frontend-domain", "project-feed"], ) -def test_failed_domain_decodes_with_and_without_failure_reason( +@RESPONSE_MODELS +def test_failed_domain_decodes_its_failure_reason( model: type[FrontendCustomDomainResponse | ProjectFrontendCustomDomain], feed_fields: dict[str, dict[str, str]], - tls_mode: str, failure_fields: dict[str, str], failure_reason: str | Unset, ) -> None: failed = model.from_dict( { - **managed_pending(), + **domain_payload(), **feed_fields, **failure_fields, - "tls_mode": tls_mode, "domain_status": "failed", - "verification_status": "failed", } ) - assert failed.tls_mode == tls_mode + assert failed.tls_mode == failure_fields["tls_mode"] assert failed.domain_status == "failed" - assert failed.verification_status == "failed" + assert failed.verification_status == failure_fields["verification_status"] assert failed.failure_reason == failure_reason - assert failed.routing_target_hostname == ROUTING_TARGET -@pytest.mark.parametrize( - ("model", "feed_fields"), - [ - (FrontendCustomDomainResponse, {}), - (ProjectFrontendCustomDomain, PROJECT_FEED_FIELDS), - ], - ids=["frontend-domain", "project-feed"], -) -def test_deprecated_routing_record_from_older_servers_still_decodes( +@RESPONSE_MODELS +def test_deprecated_routing_record_from_older_servers_round_trips( model: type[FrontendCustomDomainResponse | ProjectFrontendCustomDomain], feed_fields: dict[str, dict[str, str]], ) -> None: @@ -235,7 +309,7 @@ def test_deprecated_routing_record_from_older_servers_still_decodes( } legacy = { key: value - for key, value in managed_pending().items() + for key, value in domain_payload().items() if key != "routing_target_hostname" } @@ -246,6 +320,7 @@ def test_deprecated_routing_record_from_older_servers_still_decodes( assert response.routing_target_hostname is UNSET assert isinstance(response.required_routing_record, FrontendDomainRoutingRecord) assert response.required_routing_record.to_dict() == legacy_record + assert response.to_dict()["required_routing_record"] == legacy_record @pytest.mark.parametrize( From 93cdf0981051bfcaedbd8d3cf59338b7011eaef4 Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Mon, 5 Oct 2026 19:25:14 -0400 Subject: [PATCH 12/15] test(api): decode domain status through the generated GET operation --- .../_tests/test_managed_tls_contract.py | 113 ++++++++++-------- 1 file changed, 64 insertions(+), 49 deletions(-) diff --git a/src/volcano_sdk/_tests/test_managed_tls_contract.py b/src/volcano_sdk/_tests/test_managed_tls_contract.py index 5f07fe94..5c270c24 100644 --- a/src/volcano_sdk/_tests/test_managed_tls_contract.py +++ b/src/volcano_sdk/_tests/test_managed_tls_contract.py @@ -1,4 +1,5 @@ import json +from collections.abc import Callable from datetime import UTC, datetime from uuid import UUID @@ -6,7 +7,10 @@ import pytest from attrs import AttrsInstance, fields_dict -from volcano_sdk._generated.api.frontends import create_frontend_custom_domain +from volcano_sdk._generated.api.frontends import ( + create_frontend_custom_domain, + get_frontend_custom_domain, +) from volcano_sdk._generated.client import AuthenticatedClient from volcano_sdk._generated.models import ( BYOCProjectConfigFrontendCustomDomainTLSConfig, @@ -42,17 +46,8 @@ } ROUTING_TARGET = "my-frontend.frontends.volcano.run" CREATED_AT = datetime(2026, 9, 2, 12, tzinfo=UTC) -RESPONSE_MODELS = pytest.mark.parametrize( - ("model", "feed_fields"), - [ - (FrontendCustomDomainResponse, {}), - ( - ProjectFrontendCustomDomain, - {"frontend": {"id": str(FRONTEND_ID), "name": "my-frontend"}}, - ), - ], - ids=["frontend-domain", "project-feed"], -) +DOMAIN_PATH = f"/projects/{PROJECT_ID}/frontends/{FRONTEND_ID}/domain" +DomainEntry = FrontendCustomDomainResponse | ProjectFrontendCustomDomain def domain_payload() -> dict[str, object]: @@ -68,20 +63,28 @@ def domain_payload() -> dict[str, object]: } -def create_managed_domain( +def mock_client( status: int, payload: dict[str, object] -) -> Error | FrontendCustomDomainConflictError | FrontendCustomDomainResponse | None: +) -> tuple[AuthenticatedClient, list[httpx.Request]]: sent: list[httpx.Request] = [] def respond(request: httpx.Request) -> httpx.Response: sent.append(request) return httpx.Response(status, json=payload) - with AuthenticatedClient( + client = AuthenticatedClient( base_url="https://api.example.com", token="access-token", httpx_args={"transport": httpx.MockTransport(respond)}, - ) as client: + ) + return client, sent + + +def create_managed_domain( + status: int, payload: dict[str, object] +) -> Error | FrontendCustomDomainConflictError | FrontendCustomDomainResponse | None: + client, sent = mock_client(status, payload) + with client: result = create_frontend_custom_domain.sync_detailed( PROJECT_ID, FRONTEND_ID, @@ -95,13 +98,7 @@ def respond(request: httpx.Request) -> httpx.Response: assert [ (request.method, request.url.path, request.headers["Authorization"]) for request in sent - ] == [ - ( - "POST", - f"/projects/{PROJECT_ID}/frontends/{FRONTEND_ID}/domain", - "Bearer access-token", - ) - ] + ] == [("POST", DOMAIN_PATH, "Bearer access-token")] assert json.loads(sent[0].content) == { "domain": "app.example.com", "tls": {"mode": "managed"}, @@ -110,6 +107,33 @@ def respond(request: httpx.Request) -> httpx.Response: return result.parsed +def poll_domain(payload: dict[str, object]) -> DomainEntry: + client, sent = mock_client(200, payload) + with client: + result = get_frontend_custom_domain.sync_detailed( + PROJECT_ID, FRONTEND_ID, client=client + ) + + assert [(request.method, request.url.path) for request in sent] == [ + ("GET", DOMAIN_PATH) + ] + assert isinstance(result.parsed, FrontendCustomDomainResponse) + return result.parsed + + +def decode_project_feed_entry(payload: dict[str, object]) -> DomainEntry: + return ProjectFrontendCustomDomain.from_dict( + {**payload, "frontend": {"id": str(FRONTEND_ID), "name": "my-frontend"}} + ) + + +DOMAIN_DECODERS = pytest.mark.parametrize( + "decode", + [poll_domain, decode_project_feed_entry], + ids=["domain-status", "project-feed"], +) + + @pytest.mark.parametrize( ("tls", "wire_tls"), [ @@ -180,17 +204,12 @@ def test_create_operation_decodes_the_ownership_challenge(status: int) -> None: assert created.created_at == CREATED_AT -@RESPONSE_MODELS +@DOMAIN_DECODERS def test_pending_domain_decodes_the_certificate_validation_record( - model: type[FrontendCustomDomainResponse | ProjectFrontendCustomDomain], - feed_fields: dict[str, dict[str, str]], + decode: Callable[[dict[str, object]], DomainEntry], ) -> None: - pending = model.from_dict( - { - **domain_payload(), - **feed_fields, - "verification_records": [CERTIFICATE_VALIDATION], - } + pending = decode( + {**domain_payload(), "verification_records": [CERTIFICATE_VALIDATION]} ) assert isinstance(pending.verification_records, list) @@ -275,21 +294,13 @@ def test_other_create_errors_keep_the_plain_error_shape(status: int) -> None: "byoc", ], ) -@RESPONSE_MODELS +@DOMAIN_DECODERS def test_failed_domain_decodes_its_failure_reason( - model: type[FrontendCustomDomainResponse | ProjectFrontendCustomDomain], - feed_fields: dict[str, dict[str, str]], + decode: Callable[[dict[str, object]], DomainEntry], failure_fields: dict[str, str], failure_reason: str | Unset, ) -> None: - failed = model.from_dict( - { - **domain_payload(), - **feed_fields, - **failure_fields, - "domain_status": "failed", - } - ) + failed = decode({**domain_payload(), **failure_fields, "domain_status": "failed"}) assert failed.tls_mode == failure_fields["tls_mode"] assert failed.domain_status == "failed" @@ -297,10 +308,9 @@ def test_failed_domain_decodes_its_failure_reason( assert failed.failure_reason == failure_reason -@RESPONSE_MODELS +@DOMAIN_DECODERS def test_deprecated_routing_record_from_older_servers_round_trips( - model: type[FrontendCustomDomainResponse | ProjectFrontendCustomDomain], - feed_fields: dict[str, dict[str, str]], + decode: Callable[[dict[str, object]], DomainEntry], ) -> None: legacy_record = { "record_type": "CNAME", @@ -313,9 +323,7 @@ def test_deprecated_routing_record_from_older_servers_round_trips( if key != "routing_target_hostname" } - response = model.from_dict( - {**legacy, **feed_fields, "required_routing_record": legacy_record} - ) + response = decode({**legacy, "required_routing_record": legacy_record}) assert response.routing_target_hostname is UNSET assert isinstance(response.required_routing_record, FrontendDomainRoutingRecord) @@ -348,3 +356,10 @@ def test_project_config_tls_decodes_each_mode( assert isinstance(domain.tls, variant) assert domain.to_dict() == wire_domain + + +def test_project_config_domain_without_tls_keeps_the_stored_certificate() -> None: + domain = ProjectConfigCustomDomain.from_dict({"domain": "app.example.com"}) + + assert domain.tls is UNSET + assert domain.to_dict() == {"domain": "app.example.com"} From 077de0f5faa24e1a65f681ff34258325f4a36d26 Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Mon, 5 Oct 2026 19:40:32 -0400 Subject: [PATCH 13/15] test(api): pin re-encoding of routing target and failure reason --- src/volcano_sdk/_tests/test_managed_tls_contract.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/volcano_sdk/_tests/test_managed_tls_contract.py b/src/volcano_sdk/_tests/test_managed_tls_contract.py index 5c270c24..c4d1fcd2 100644 --- a/src/volcano_sdk/_tests/test_managed_tls_contract.py +++ b/src/volcano_sdk/_tests/test_managed_tls_contract.py @@ -217,6 +217,7 @@ def test_pending_domain_decodes_the_certificate_validation_record( CERTIFICATE_VALIDATION ] assert pending.routing_target_hostname == ROUTING_TARGET + assert pending.to_dict()["routing_target_hostname"] == ROUTING_TARGET def test_create_conflict_carries_the_callers_ownership_record() -> None: @@ -306,6 +307,7 @@ def test_failed_domain_decodes_its_failure_reason( assert failed.domain_status == "failed" assert failed.verification_status == failure_fields["verification_status"] assert failed.failure_reason == failure_reason + assert failed.to_dict().get("failure_reason", UNSET) == failure_reason @DOMAIN_DECODERS @@ -358,7 +360,7 @@ def test_project_config_tls_decodes_each_mode( assert domain.to_dict() == wire_domain -def test_project_config_domain_without_tls_keeps_the_stored_certificate() -> None: +def test_project_config_domain_without_tls_omits_tls_on_the_wire() -> None: domain = ProjectConfigCustomDomain.from_dict({"domain": "app.example.com"}) assert domain.tls is UNSET From b0fb688e1fc3d8a80ab1a9a5363def8d06f9c6fb Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Tue, 6 Oct 2026 00:05:36 -0400 Subject: [PATCH 14/15] fix(api): vendor final managed TLS contract text Restore the BYOC default on the custom-domain TLS mode and take the final Hosting wording for verification_status, the ownership failure reason, and createFrontendCustomDomain takeover rules. Regenerate the client: FrontendCustomDomainTLSConfig now defaults mode to byoc on construction and always sends it, while decoding still requires it. --- openapi/openapi.yaml | 6 ++++-- .../frontends/create_frontend_custom_domain.py | 12 ++++++++---- .../models/frontend_custom_domain_response.py | 9 ++++++--- .../models/frontend_custom_domain_tls_config.py | 3 ++- .../models/project_frontend_custom_domain.py | 9 ++++++--- .../_tests/test_managed_tls_contract.py | 17 +++++++++++++++++ 6 files changed, 43 insertions(+), 13 deletions(-) diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index d2fcee27..88769789 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -4801,7 +4801,7 @@ paths: The default Volcano-generated frontend URL remains active. Wildcard Volcano frontend TLS remains valid and isolated from custom-domain certificate changes. Managed TLS returns the DNS records currently required for setup. Volcano may require a tenant-specific TXT ownership challenge before returning the certificate authority's validation record. After ownership verification succeeds, Volcano permanently assigns the hostname to the account, including after the domain is deleted. A required but unverified ownership reservation expires after 72 hours. - An unverified reservation does not block an account that proves ownership. When another account holds one, a managed TLS request gets `409` with `code: ownership_verification_required` and the caller's own `required_record`; after publishing it, the same request takes over the reservation. A BYOC request with a publicly trusted certificate and key for the hostname also takes it over; other BYOC requests get a `409` without `code`. Verified hostnames and BYOC domains are never taken over. + An unverified reservation does not block an account that proves ownership. When another account holds one, a managed TLS request gets `409` with `code: ownership_verification_required` and the caller's own `required_record`; after publishing it, the same request takes over the reservation. A BYOC request with a publicly trusted certificate and key for the hostname also takes it over; other BYOC requests get a `409` without `code`. Hostnames claimed through ownership verification and BYOC domains are never taken over. operationId: createFrontendCustomDomain security: - UserToken: [] @@ -16441,9 +16441,10 @@ components: - pending - verified - failed + description: '`verified`: the domain is served by a validated certificate. `pending`: it is not served yet, is being re-validated after its certificate material was withdrawn, or Volcano is retrying after a failure. `failed`: a failure left the domain unserved, alongside `domain_status: failed`; managed domains report the cause in `failure_reason`.' failure_reason: type: string - description: Failure category, present only when managed TLS setup has failed. Current values are provider, certificate, ownership, and internal; ownership means another account has already verified the hostname. Treat unrecognized values as internal. + description: Failure category, present only when managed TLS setup has failed. Current values are provider, certificate, ownership, and internal; ownership means another account has already claimed the hostname through ownership verification. Treat unrecognized values as internal. verification_records: type: array items: @@ -16514,6 +16515,7 @@ components: enum: - managed - byoc + default: byoc description: managed for a Volcano-issued certificate; byoc to supply your own. certificate_pem: type: string diff --git a/src/volcano_sdk/_generated/api/frontends/create_frontend_custom_domain.py b/src/volcano_sdk/_generated/api/frontends/create_frontend_custom_domain.py index c85f10e7..d2f6eea0 100644 --- a/src/volcano_sdk/_generated/api/frontends/create_frontend_custom_domain.py +++ b/src/volcano_sdk/_generated/api/frontends/create_frontend_custom_domain.py @@ -146,7 +146,8 @@ def sync_detailed( holds one, a managed TLS request gets `409` with `code: ownership_verification_required` and the caller's own `required_record`; after publishing it, the same request takes over the reservation. A BYOC request with a publicly trusted certificate and key for the hostname also takes it over; other - BYOC requests get a `409` without `code`. Verified hostnames and BYOC domains are never taken over. + BYOC requests get a `409` without `code`. Hostnames claimed through ownership verification and BYOC + domains are never taken over. Args: id (UUID): @@ -197,7 +198,8 @@ def sync( holds one, a managed TLS request gets `409` with `code: ownership_verification_required` and the caller's own `required_record`; after publishing it, the same request takes over the reservation. A BYOC request with a publicly trusted certificate and key for the hostname also takes it over; other - BYOC requests get a `409` without `code`. Verified hostnames and BYOC domains are never taken over. + BYOC requests get a `409` without `code`. Hostnames claimed through ownership verification and BYOC + domains are never taken over. Args: id (UUID): @@ -243,7 +245,8 @@ async def asyncio_detailed( holds one, a managed TLS request gets `409` with `code: ownership_verification_required` and the caller's own `required_record`; after publishing it, the same request takes over the reservation. A BYOC request with a publicly trusted certificate and key for the hostname also takes it over; other - BYOC requests get a `409` without `code`. Verified hostnames and BYOC domains are never taken over. + BYOC requests get a `409` without `code`. Hostnames claimed through ownership verification and BYOC + domains are never taken over. Args: id (UUID): @@ -294,7 +297,8 @@ async def asyncio( holds one, a managed TLS request gets `409` with `code: ownership_verification_required` and the caller's own `required_record`; after publishing it, the same request takes over the reservation. A BYOC request with a publicly trusted certificate and key for the hostname also takes it over; other - BYOC requests get a `409` without `code`. Verified hostnames and BYOC domains are never taken over. + BYOC requests get a `409` without `code`. Hostnames claimed through ownership verification and BYOC + domains are never taken over. Args: id (UUID): diff --git a/src/volcano_sdk/_generated/models/frontend_custom_domain_response.py b/src/volcano_sdk/_generated/models/frontend_custom_domain_response.py index 76a056a1..c4c000da 100644 --- a/src/volcano_sdk/_generated/models/frontend_custom_domain_response.py +++ b/src/volcano_sdk/_generated/models/frontend_custom_domain_response.py @@ -37,13 +37,16 @@ class FrontendCustomDomainResponse: domain (str): tls_mode (FrontendCustomDomainResponseTlsMode): domain_status (FrontendCustomDomainResponseDomainStatus): - verification_status (FrontendCustomDomainResponseVerificationStatus): + verification_status (FrontendCustomDomainResponseVerificationStatus): `verified`: the domain is served by a + validated certificate. `pending`: it is not served yet, is being re-validated after its certificate material was + withdrawn, or Volcano is retrying after a failure. `failed`: a failure left the domain unserved, alongside + `domain_status: failed`; managed domains report the cause in `failure_reason`. effective_urls (list[str]): created_at (datetime.datetime): updated_at (datetime.datetime): failure_reason (str | Unset): Failure category, present only when managed TLS setup has failed. Current values - are provider, certificate, ownership, and internal; ownership means another account has already verified the - hostname. Treat unrecognized values as internal. + are provider, certificate, ownership, and internal; ownership means another account has already claimed the + hostname through ownership verification. Treat unrecognized values as internal. verification_records (list[FrontendDomainVerificationRecord] | Unset): required_routing_record (FrontendDomainRoutingRecord | Unset): routing_target_hostname (str | Unset): DNS routing target hostname for this frontend. The DNS record type diff --git a/src/volcano_sdk/_generated/models/frontend_custom_domain_tls_config.py b/src/volcano_sdk/_generated/models/frontend_custom_domain_tls_config.py index 44a2ca77..866372eb 100644 --- a/src/volcano_sdk/_generated/models/frontend_custom_domain_tls_config.py +++ b/src/volcano_sdk/_generated/models/frontend_custom_domain_tls_config.py @@ -29,6 +29,7 @@ class FrontendCustomDomainTLSConfig: Attributes: mode (FrontendCustomDomainTLSConfigMode): managed for a Volcano-issued certificate; byoc to supply your own. + Default: 'byoc'. certificate_pem (str | Unset): PEM-encoded certificate. Required when mode is byoc; not allowed when mode is managed. private_key_pem (str | Unset): PEM-encoded private key. Required when mode is byoc; not allowed when mode is @@ -37,7 +38,7 @@ class FrontendCustomDomainTLSConfig: mode is managed. """ - mode: FrontendCustomDomainTLSConfigMode + mode: FrontendCustomDomainTLSConfigMode = 'byoc' certificate_pem: str | Unset = UNSET private_key_pem: str | Unset = UNSET certificate_chain_pem: str | Unset = UNSET diff --git a/src/volcano_sdk/_generated/models/project_frontend_custom_domain.py b/src/volcano_sdk/_generated/models/project_frontend_custom_domain.py index e472000a..9a4316f3 100644 --- a/src/volcano_sdk/_generated/models/project_frontend_custom_domain.py +++ b/src/volcano_sdk/_generated/models/project_frontend_custom_domain.py @@ -38,15 +38,18 @@ class ProjectFrontendCustomDomain: domain (str): tls_mode (FrontendCustomDomainResponseTlsMode): domain_status (FrontendCustomDomainResponseDomainStatus): - verification_status (FrontendCustomDomainResponseVerificationStatus): + verification_status (FrontendCustomDomainResponseVerificationStatus): `verified`: the domain is served by a + validated certificate. `pending`: it is not served yet, is being re-validated after its certificate material was + withdrawn, or Volcano is retrying after a failure. `failed`: a failure left the domain unserved, alongside + `domain_status: failed`; managed domains report the cause in `failure_reason`. effective_urls (list[str]): created_at (datetime.datetime): updated_at (datetime.datetime): frontend (ProjectFrontendCustomDomainFrontend): The frontend this custom domain is attached to. Inlined to avoid a second fetch from the project-scoped feed. failure_reason (str | Unset): Failure category, present only when managed TLS setup has failed. Current values - are provider, certificate, ownership, and internal; ownership means another account has already verified the - hostname. Treat unrecognized values as internal. + are provider, certificate, ownership, and internal; ownership means another account has already claimed the + hostname through ownership verification. Treat unrecognized values as internal. verification_records (list[FrontendDomainVerificationRecord] | Unset): required_routing_record (FrontendDomainRoutingRecord | Unset): routing_target_hostname (str | Unset): DNS routing target hostname for this frontend. The DNS record type diff --git a/src/volcano_sdk/_tests/test_managed_tls_contract.py b/src/volcano_sdk/_tests/test_managed_tls_contract.py index c4d1fcd2..24021c3a 100644 --- a/src/volcano_sdk/_tests/test_managed_tls_contract.py +++ b/src/volcano_sdk/_tests/test_managed_tls_contract.py @@ -162,6 +162,23 @@ def test_create_request_encodes_the_selected_tls_mode( assert CreateFrontendCustomDomainRequest.from_dict(wire_request) == request +def test_tls_mode_defaults_to_byoc_and_is_always_sent() -> None: + byoc_material = { + "certificate_pem": BYOC_MATERIAL["certificate_pem"], + "private_key_pem": BYOC_MATERIAL["private_key_pem"], + } + + tls = FrontendCustomDomainTLSConfig( + certificate_pem=byoc_material["certificate_pem"], + private_key_pem=byoc_material["private_key_pem"], + ) + + assert tls.mode == "byoc" + assert tls.to_dict() == {"mode": "byoc", **byoc_material} + with pytest.raises(KeyError, match="mode"): + _ = FrontendCustomDomainTLSConfig.from_dict(byoc_material) + + @pytest.mark.parametrize( "model", [ From e082f74a51231cda986cc556504b7f40a2382163 Mon Sep 17 00:00:00 2001 From: Sean Keever <33592180+swkeever@users.noreply.github.com> Date: Tue, 6 Oct 2026 12:12:01 -0400 Subject: [PATCH 15/15] fix(api): accept manifest TLS blocks without a mode Take Hosting's manifest TLS contract: BYOCProjectConfigFrontendCustomDomainTLSConfig no longer requires mode, and ProjectConfigFrontendCustomDomainTLSConfig drops its mode discriminator and documents the BYOC default. Regenerate the client so a manifest TLS block without mode decodes as BYOC and round-trips without adding one, while mode: managed still selects the managed variant. --- openapi/openapi.yaml | 9 ++------- ...onfig_frontend_custom_domain_tls_config.py | 19 ++++++++++++++----- .../models/project_config_custom_domain.py | 2 +- .../_tests/test_managed_tls_contract.py | 17 +++++++++++++++++ 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 88769789..86b51d5a 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -20504,6 +20504,7 @@ components: type: string enum: - byoc + description: Optional; a TLS block without `mode` is BYOC. certificate_pem: type: string maxLength: 65536 @@ -20516,17 +20517,11 @@ components: type: string maxLength: 65536 description: Optional PEM-encoded certificate chain. Requires certificate_pem and private_key_pem. Omitted from exports. - required: - - mode ProjectConfigFrontendCustomDomainTLSConfig: + description: TLS for the custom domain. `mode` defaults to `byoc` when omitted. oneOf: - $ref: '#/components/schemas/ManagedProjectConfigFrontendCustomDomainTLSConfig' - $ref: '#/components/schemas/BYOCProjectConfigFrontendCustomDomainTLSConfig' - discriminator: - propertyName: mode - mapping: - managed: '#/components/schemas/ManagedProjectConfigFrontendCustomDomainTLSConfig' - byoc: '#/components/schemas/BYOCProjectConfigFrontendCustomDomainTLSConfig' DatabaseQueryPerformanceDatabase: type: object properties: diff --git a/src/volcano_sdk/_generated/models/byoc_project_config_frontend_custom_domain_tls_config.py b/src/volcano_sdk/_generated/models/byoc_project_config_frontend_custom_domain_tls_config.py index c9e70070..42f6c2c1 100644 --- a/src/volcano_sdk/_generated/models/byoc_project_config_frontend_custom_domain_tls_config.py +++ b/src/volcano_sdk/_generated/models/byoc_project_config_frontend_custom_domain_tls_config.py @@ -29,7 +29,7 @@ class BYOCProjectConfigFrontendCustomDomainTLSConfig: without certificate fields keeps the stored certificate; exports render only the mode. Attributes: - mode (BYOCProjectConfigFrontendCustomDomainTLSConfigMode): + mode (BYOCProjectConfigFrontendCustomDomainTLSConfigMode | Unset): Optional; a TLS block without `mode` is BYOC. certificate_pem (str | Unset): PEM-encoded certificate for create or rotation. Requires private_key_pem. Omitted from exports. private_key_pem (str | Unset): PEM-encoded private key for create or rotation. Requires certificate_pem. Omitted @@ -38,7 +38,7 @@ class BYOCProjectConfigFrontendCustomDomainTLSConfig: private_key_pem. Omitted from exports. """ - mode: BYOCProjectConfigFrontendCustomDomainTLSConfigMode + mode: BYOCProjectConfigFrontendCustomDomainTLSConfigMode | Unset = UNSET certificate_pem: str | Unset = UNSET private_key_pem: str | Unset = UNSET certificate_chain_pem: str | Unset = UNSET @@ -48,7 +48,10 @@ class BYOCProjectConfigFrontendCustomDomainTLSConfig: def to_dict(self) -> dict[str, Any]: - mode: str = self.mode + mode: str | Unset = UNSET + if not isinstance(self.mode, Unset): + mode = self.mode + certificate_pem = self.certificate_pem @@ -60,8 +63,9 @@ def to_dict(self) -> dict[str, Any]: field_dict: dict[str, Any] = {} field_dict.update({ - "mode": mode, }) + if mode is not UNSET: + field_dict["mode"] = mode if certificate_pem is not UNSET: field_dict["certificate_pem"] = certificate_pem if private_key_pem is not UNSET: @@ -76,7 +80,12 @@ def to_dict(self) -> dict[str, Any]: @classmethod def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: d = dict(src_dict) - mode = check_byoc_project_config_frontend_custom_domain_tls_config_mode(d.pop("mode")) + _mode = d.pop("mode", UNSET) + mode: BYOCProjectConfigFrontendCustomDomainTLSConfigMode | Unset + if isinstance(_mode, Unset): + mode = UNSET + else: + mode = check_byoc_project_config_frontend_custom_domain_tls_config_mode(_mode) diff --git a/src/volcano_sdk/_generated/models/project_config_custom_domain.py b/src/volcano_sdk/_generated/models/project_config_custom_domain.py index 31e703ad..f826ef6a 100644 --- a/src/volcano_sdk/_generated/models/project_config_custom_domain.py +++ b/src/volcano_sdk/_generated/models/project_config_custom_domain.py @@ -38,7 +38,7 @@ class ProjectConfigCustomDomain: domain (str): Fully-qualified domain name (hostname only, no scheme/path). Managed TLS (`tls.mode: managed`) accepts at most 219 characters; BYOC accepts 253. tls (BYOCProjectConfigFrontendCustomDomainTLSConfig | ManagedProjectConfigFrontendCustomDomainTLSConfig | - Unset): + Unset): TLS for the custom domain. `mode` defaults to `byoc` when omitted. """ domain: str diff --git a/src/volcano_sdk/_tests/test_managed_tls_contract.py b/src/volcano_sdk/_tests/test_managed_tls_contract.py index 24021c3a..51317c61 100644 --- a/src/volcano_sdk/_tests/test_managed_tls_contract.py +++ b/src/volcano_sdk/_tests/test_managed_tls_contract.py @@ -377,6 +377,23 @@ def test_project_config_tls_decodes_each_mode( assert domain.to_dict() == wire_domain +@pytest.mark.parametrize( + "wire_tls", + [{}, dict(BYOC_MATERIAL)], + ids=["empty-block", "byoc-material"], +) +def test_project_config_tls_without_mode_decodes_as_byoc( + wire_tls: dict[str, str], +) -> None: + wire_domain = {"domain": "app.example.com", "tls": wire_tls} + + domain = ProjectConfigCustomDomain.from_dict(wire_domain) + + assert isinstance(domain.tls, BYOCProjectConfigFrontendCustomDomainTLSConfig) + assert domain.tls.mode is UNSET + assert domain.to_dict() == wire_domain + + def test_project_config_domain_without_tls_omits_tls_on_the_wire() -> None: domain = ProjectConfigCustomDomain.from_dict({"domain": "app.example.com"})