diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index 89b04cb0..86b51d5a 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -4800,6 +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, 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`. Hostnames claimed through ownership verification and BYOC domains are never taken over. operationId: createFrontendCustomDomain security: - UserToken: [] @@ -4850,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: @@ -14600,10 +14602,12 @@ components: - text_body CreateFrontendCustomDomainRequest: type: object + additionalProperties: false properties: domain: type: string - description: Fully-qualified domain name (hostname only, no scheme/path) + maxLength: 253 + 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' @@ -16436,12 +16440,23 @@ components: enum: - 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 claimed the hostname through ownership verification. 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: @@ -16462,26 +16477,60 @@ components: - updated_at FrontendCustomDomainTLSConfig: type: object + 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: + - 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 - byoc default: byoc - description: BYOC is mandatory for custom domain creation. + description: managed for a Volcano-issued certificate; byoc to supply your own. certificate_pem: type: string - description: Required. PEM-encoded certificate. + maxLength: 65536 + description: PEM-encoded certificate. Required when mode is byoc; not allowed when mode is managed. private_key_pem: type: string - description: Required. PEM-encoded private key. + maxLength: 65536 + description: PEM-encoded private key. Required when mode is byoc; not allowed when mode is managed. certificate_chain_pem: type: string - description: Optional PEM-encoded certificate chain. + maxLength: 65536 + description: Optional PEM-encoded certificate chain when mode is byoc; not allowed when mode is managed. required: - mode - - certificate_pem - - private_key_pem FrontendDeployment: type: object properties: @@ -16586,6 +16635,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. properties: name: type: string @@ -16597,6 +16647,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. @@ -18551,17 +18609,21 @@ components: type: object additionalProperties: false description: | - Custom domain with BYOC TLS (SUPERAGENT 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 (SUPERAGENT plan). `tls` is required + 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 - description: Fully-qualified domain name (hostname only, no scheme/path) + maxLength: 253 + 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/FrontendCustomDomainTLSConfig' + $ref: '#/components/schemas/ProjectConfigFrontendCustomDomainTLSConfig' required: - domain ProjectConfigDatabase: @@ -20404,6 +20466,62 @@ components: $ref: '#/components/schemas/AuthPageTheme' layouts: $ref: '#/components/schemas/ProjectConfigAuthPageLayouts' + ManagedProjectConfigFrontendCustomDomainTLSConfig: + type: object + description: Volcano issues and renews the certificate. Certificate fields are not allowed. + additionalProperties: false + properties: + mode: + type: string + enum: + - managed + required: + - 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: + - 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 + description: Optional; a TLS block without `mode` is BYOC. + certificate_pem: + type: string + maxLength: 65536 + 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 create or rotation. Requires certificate_pem. Omitted from exports. + certificate_chain_pem: + type: string + maxLength: 65536 + description: Optional PEM-encoded certificate chain. Requires certificate_pem and private_key_pem. Omitted from exports. + ProjectConfigFrontendCustomDomainTLSConfig: + description: TLS for the custom domain. `mode` defaults to `byoc` when omitted. + oneOf: + - $ref: '#/components/schemas/ManagedProjectConfigFrontendCustomDomainTLSConfig' + - $ref: '#/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..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 @@ -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,12 +131,23 @@ 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. 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`. Hostnames claimed through ownership verification and BYOC + domains are never taken over. Args: id (UUID): @@ -147,7 +159,7 @@ def sync_detailed( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Error | FrontendCustomDomainResponse] + Response[Error | FrontendCustomDomainConflictError | FrontendCustomDomainResponse] """ @@ -171,12 +183,23 @@ 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. 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`. Hostnames claimed through ownership verification and BYOC + domains are never taken over. Args: id (UUID): @@ -188,7 +211,7 @@ def sync( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Error | FrontendCustomDomainResponse + Error | FrontendCustomDomainConflictError | FrontendCustomDomainResponse """ @@ -207,12 +230,23 @@ 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. 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`. Hostnames claimed through ownership verification and BYOC + domains are never taken over. Args: id (UUID): @@ -224,7 +258,7 @@ async def asyncio_detailed( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - Response[Error | FrontendCustomDomainResponse] + Response[Error | FrontendCustomDomainConflictError | FrontendCustomDomainResponse] """ @@ -248,12 +282,23 @@ 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. 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`. Hostnames claimed through ownership verification and BYOC + domains are never taken over. Args: id (UUID): @@ -265,7 +310,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 30cd0ce7..ea2acf53 100644 --- a/src/volcano_sdk/_generated/models/__init__.py +++ b/src/volcano_sdk/_generated/models/__init__.py @@ -96,6 +96,8 @@ from .batch_function_deploy_failure import BatchFunctionDeployFailure from .batch_function_deploy_failure_operation import BatchFunctionDeployFailureOperation from .batch_function_deploy_response import BatchFunctionDeployResponse +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 @@ -197,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 @@ -311,6 +314,8 @@ from .log_search_request import LogSearchRequest from .log_search_response import LogSearchResponse from .log_stream_request import LogStreamRequest +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 @@ -642,6 +647,8 @@ "BatchFunctionDeployFailure", "BatchFunctionDeployFailureOperation", "BatchFunctionDeployResponse", + "BYOCProjectConfigFrontendCustomDomainTLSConfig", + "BYOCProjectConfigFrontendCustomDomainTLSConfigMode", "CallOAuthProviderApiBody", "CallOAuthProviderApiBodyBody", "CallOAuthProviderApiBodyMethod", @@ -743,6 +750,7 @@ "Error", "ExportProjectSourceRequest", "Frontend", + "FrontendCustomDomainConflictError", "FrontendCustomDomainResponse", "FrontendCustomDomainResponseDomainStatus", "FrontendCustomDomainResponseTlsMode", @@ -857,6 +865,8 @@ "LogSearchRequest", "LogSearchResponse", "LogStreamRequest", + "ManagedProjectConfigFrontendCustomDomainTLSConfig", + "ManagedProjectConfigFrontendCustomDomainTLSConfigMode", "MetricUsageData", "OAuthConfig", "OAuthConfigProvider", 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..42f6c2c1 --- /dev/null +++ b/src/volcano_sdk/_generated/models/byoc_project_config_frontend_custom_domain_tls_config.py @@ -0,0 +1,107 @@ +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: + """ 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 | 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 + from exports. + certificate_chain_pem (str | Unset): Optional PEM-encoded certificate chain. Requires certificate_pem and + private_key_pem. Omitted from exports. + """ + + mode: BYOCProjectConfigFrontendCustomDomainTLSConfigMode | Unset = UNSET + 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 | Unset = UNSET + if not isinstance(self.mode, Unset): + mode = 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({ + }) + 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: + 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 = 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) + + + + + 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 2036b75c..b281ffea 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,13 +25,15 @@ class CreateFrontendCustomDomainRequest: """ Attributes: - domain (str): Fully-qualified domain name (hostname only, no scheme/path) Example: app.example.com. - tls (FrontendCustomDomainTLSConfig): + 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 tls: FrontendCustomDomainTLSConfig - additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) @@ -45,7 +47,7 @@ def to_dict(self) -> dict[str, Any]: field_dict: dict[str, Any] = {} - field_dict.update(self.additional_properties) + field_dict.update({ "domain": domain, "tls": tls, @@ -71,22 +73,5 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: tls=tls, ) - - create_frontend_custom_domain_request.additional_properties = d return create_frontend_custom_domain_request - @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_conflict_error.py b/src/volcano_sdk/_generated/models/frontend_custom_domain_conflict_error.py new file mode 100644 index 00000000..009a8918 --- /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 # noqa: PLC0415 + 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 # noqa: PLC0415 + 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 c0e0d5e5..d04b3329 100644 --- a/src/volcano_sdk/_generated/models/frontend_custom_domain_response.py +++ b/src/volcano_sdk/_generated/models/frontend_custom_domain_response.py @@ -37,12 +37,20 @@ 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 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 + depends on whether the custom domain is a zone apex. """ domain: str @@ -52,8 +60,10 @@ 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 + routing_target_hostname: str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) @@ -79,6 +89,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 = [] @@ -92,6 +104,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) @@ -104,10 +118,14 @@ 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: 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 @@ -148,6 +166,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: @@ -170,6 +190,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, @@ -178,8 +200,10 @@ 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, + routing_target_hostname=routing_target_hostname, ) 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..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 @@ -24,19 +24,24 @@ @_attrs_define class 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`. + 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): 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 + managed. + certificate_chain_pem (str | Unset): Optional PEM-encoded certificate chain when mode is byoc; not allowed when + mode is managed. """ - certificate_pem: str - private_key_pem: str mode: FrontendCustomDomainTLSConfigMode = 'byoc' + 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) @@ -53,12 +58,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 @@ -74,9 +81,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) @@ -87,22 +94,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 688bce99..462c72ae 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_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..2c07eb0b --- /dev/null +++ b/src/volcano_sdk/_generated/models/managed_project_config_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_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: + """ Volcano issues and renews the certificate. Certificate fields are not allowed. + + 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 faffc786..cba0e042 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,44 @@ @_attrs_define class ProjectConfigCustomDomain: - """ Custom domain with BYOC TLS (SUPERAGENT 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 (SUPERAGENT plan). `tls` is required + 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) - tls (FrontendCustomDomainTLSConfig | Unset): + 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): TLS for the custom domain. `mode` defaults to `byoc` when omitted. """ 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 # noqa: PLC0415 + from ..models.byoc_project_config_frontend_custom_domain_tls_config import BYOCProjectConfigFrontendCustomDomainTLSConfig # noqa: PLC0415 + from ..models.managed_project_config_frontend_custom_domain_tls_config import ManagedProjectConfigFrontendCustomDomainTLSConfig # noqa: PLC0415 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 +77,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 # noqa: PLC0415 + from ..models.byoc_project_config_frontend_custom_domain_tls_config import BYOCProjectConfigFrontendCustomDomainTLSConfig # noqa: PLC0415 + from ..models.managed_project_config_frontend_custom_domain_tls_config import ManagedProjectConfigFrontendCustomDomainTLSConfig # noqa: PLC0415 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 68300ec6..2547d7c1 100644 --- a/src/volcano_sdk/_generated/models/project_config_frontend.py +++ b/src/volcano_sdk/_generated/models/project_config_frontend.py @@ -30,12 +30,15 @@ class ProjectConfigFrontend: Attributes: name (str): - custom_domain (ProjectConfigCustomDomain | Unset): Custom domain with BYOC TLS (SUPERAGENT 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 (SUPERAGENT plan). + `tls` is required + 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 efefd70e..3eb1e683 100644 --- a/src/volcano_sdk/_generated/models/project_frontend_custom_domain.py +++ b/src/volcano_sdk/_generated/models/project_frontend_custom_domain.py @@ -38,14 +38,22 @@ 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 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 + depends on whether the custom domain is a zone apex. """ domain: str @@ -56,8 +64,10 @@ 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 + routing_target_hostname: str | Unset = UNSET additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) @@ -86,6 +96,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 = [] @@ -99,6 +111,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) @@ -112,10 +126,14 @@ 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: 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 @@ -162,6 +180,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: @@ -184,6 +204,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, @@ -193,8 +215,10 @@ 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, + 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 new file mode 100644 index 00000000..51317c61 --- /dev/null +++ b/src/volcano_sdk/_tests/test_managed_tls_contract.py @@ -0,0 +1,401 @@ +import json +from collections.abc import Callable +from datetime import UTC, datetime +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, + get_frontend_custom_domain, +) +from volcano_sdk._generated.client import AuthenticatedClient +from volcano_sdk._generated.models import ( + BYOCProjectConfigFrontendCustomDomainTLSConfig, + CreateFrontendCustomDomainRequest, + Error, + FrontendCustomDomainConflictError, + FrontendCustomDomainResponse, + FrontendCustomDomainTLSConfig, + FrontendDomainRoutingRecord, + FrontendDomainVerificationRecord, + ManagedProjectConfigFrontendCustomDomainTLSConfig, + ProjectConfigCustomDomain, + ProjectFrontendCustomDomain, +) +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", +} +OWNERSHIP_CHALLENGE = { + "name": "_volcano.app.example.com", + "type": "TXT", + "value": "volcano-domain-verification=0123456789abcdef0123456789abcdef", +} +CERTIFICATE_VALIDATION = { + "name": "_acme-challenge.app.example.com", + "type": "CNAME", + "value": "7d7b8a4e-7418-4a86-8e16-670870f00fa0.acme.frontends.volcano.run", +} +ROUTING_TARGET = "my-frontend.frontends.volcano.run" +CREATED_AT = datetime(2026, 9, 2, 12, tzinfo=UTC) +DOMAIN_PATH = f"/projects/{PROJECT_ID}/frontends/{FRONTEND_ID}/domain" +DomainEntry = FrontendCustomDomainResponse | ProjectFrontendCustomDomain + + +def domain_payload() -> dict[str, object]: + return { + "domain": "app.example.com", + "tls_mode": "managed", + "domain_status": "pending_verification", + "verification_status": "pending", + "routing_target_hostname": ROUTING_TARGET, + "effective_urls": [f"https://{ROUTING_TARGET}/"], + "created_at": "2026-09-02T12:00:00Z", + "updated_at": "2026-09-02T12:00:00Z", + } + + +def mock_client( + status: int, payload: dict[str, object] +) -> 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) + + client = AuthenticatedClient( + base_url="https://api.example.com", + token="access-token", + httpx_args={"transport": httpx.MockTransport(respond)}, + ) + 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, + client=client, + body=CreateFrontendCustomDomainRequest( + domain="app.example.com", + tls=FrontendCustomDomainTLSConfig(mode="managed"), + ), + ) + + assert [ + (request.method, request.url.path, request.headers["Authorization"]) + for request in sent + ] == [("POST", DOMAIN_PATH, "Bearer access-token")] + assert json.loads(sent[0].content) == { + "domain": "app.example.com", + "tls": {"mode": "managed"}, + } + assert result.status_code == status + 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"), + [ + (FrontendCustomDomainTLSConfig(mode="managed"), {"mode": "managed"}), + ( + FrontendCustomDomainTLSConfig( + 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}, + ), + ], + ids=["managed", "byoc"], +) +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(domain="app.example.com", tls=tls) + + assert request.to_dict() == wire_request + 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", + [ + FrontendCustomDomainResponse, + ProjectFrontendCustomDomain, + FrontendCustomDomainConflictError, + FrontendDomainVerificationRecord, + FrontendDomainRoutingRecord, + ManagedProjectConfigFrontendCustomDomainTLSConfig, + ], +) +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 name.endswith("_pem") or "private_key" in name + ] + + +@pytest.mark.parametrize("status", [200, 201], ids=["already-configured", "created"]) +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 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.required_routing_record is UNSET + assert created.failure_reason is UNSET + assert created.created_at == CREATED_AT + + +@DOMAIN_DECODERS +def test_pending_domain_decodes_the_certificate_validation_record( + decode: Callable[[dict[str, object]], DomainEntry], +) -> None: + pending = decode( + {**domain_payload(), "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 + assert pending.to_dict()["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_CHALLENGE + assert conflict.to_dict() == payload + + +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("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( + ("failure_fields", "failure_reason"), + [ + ( + { + "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", + "managed-after-verification", + "unrecognized-reason", + "managed-uncategorized", + "byoc", + ], +) +@DOMAIN_DECODERS +def test_failed_domain_decodes_its_failure_reason( + decode: Callable[[dict[str, object]], DomainEntry], + failure_fields: dict[str, str], + failure_reason: str | Unset, +) -> None: + failed = decode({**domain_payload(), **failure_fields, "domain_status": "failed"}) + + assert failed.tls_mode == failure_fields["tls_mode"] + 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 +def test_deprecated_routing_record_from_older_servers_round_trips( + decode: Callable[[dict[str, object]], DomainEntry], +) -> None: + legacy_record = { + "record_type": "CNAME", + "name": "app.example.com", + "value": ROUTING_TARGET, + } + legacy = { + key: value + for key, value in domain_payload().items() + if key != "routing_target_hostname" + } + + response = decode({**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 + assert response.to_dict()["required_routing_record"] == legacy_record + + +@pytest.mark.parametrize( + ("wire_tls", "variant"), + [ + ({"mode": "managed"}, ManagedProjectConfigFrontendCustomDomainTLSConfig), + ({"mode": "byoc"}, BYOCProjectConfigFrontendCustomDomainTLSConfig), + ( + {"mode": "byoc", **BYOC_MATERIAL}, + BYOCProjectConfigFrontendCustomDomainTLSConfig, + ), + ], + ids=["managed", "byoc-export", "byoc-apply"], +) +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} + + domain = ProjectConfigCustomDomain.from_dict(wire_domain) + + assert isinstance(domain.tls, variant) + 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"}) + + assert domain.tls is UNSET + assert domain.to_dict() == {"domain": "app.example.com"}