Coverage for src/fastapi_cloud_cli/commands/domains/_setup.py: 100%
97 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-11-09 00:25 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-11-09 00:25 +0000
1from dataclasses import dataclass 1CDEFGHI
2from typing import Literal 1CDEFGHI
4from fastapi_cloud_cli.api import ( 1CDEFGHI
5 CustomDomain,
6 CustomDomainRecord,
7 CustomDomainStatus,
8)
10StepStatus = Literal["verified", "in_progress", "attention", "locked", "failed"] 1CDEFGHI
11StepID = Literal["ownership", "certificate", "traffic", "combined"] 1CDEFGHI
12PhasedStepID = Literal["ownership", "certificate", "traffic"] 1CDEFGHI
13RecordGroup = PhasedStepID 1CDEFGHI
14Phase = Literal["internal", "external", "origin"] 1CDEFGHI
17@dataclass(frozen=True) 1CDEFGHI
18class SetupStep: 1CDEFGHI
19 id: StepID 1CDEFGHI
20 title: str 1CDEFGHI
21 description: str 1CDEFGHI
22 status: StepStatus 1CDEFGHI
23 records: list[CustomDomainRecord] 1CDEFGHI
26ATTENTION_STATUSES = frozenset( 1CDEFGHI
27 {
28 CustomDomainStatus.internal_dcv_invalid,
29 CustomDomainStatus.origin_setup_invalid,
30 }
31)
33FAILED_STATUSES = frozenset( 1CDEFGHI
34 {
35 CustomDomainStatus.internal_dcv_timeout,
36 CustomDomainStatus.internal_dcv_revoked,
37 CustomDomainStatus.external_dcv_blocked,
38 CustomDomainStatus.external_dcv_timeout,
39 CustomDomainStatus.origin_setup_timeout,
40 CustomDomainStatus.origin_setup_removed,
41 }
42)
44PHASE_ORDER: dict[Phase, int] = { 1CDEFGHI
45 "internal": 0,
46 "external": 1,
47 "origin": 2,
48}
51def _phase_from_status(status: CustomDomainStatus) -> Phase: 1CDEFGHI
52 if status.value.startswith("internal_dcv"): 1ohpaqirbsjtcukvdwlxeymzfAnBg
53 return "internal" 1ohpaqirbsjtcukvdwlxeymzfAnBg
54 if status.value.startswith("external_dcv"): 1haibjckdlemfng
55 return "external" 1haibjckdlemfng
56 return "origin" 1haibjckdlemfng
59def _record_group(record: CustomDomainRecord) -> RecordGroup: 1CDEFGHI
60 if record.type == "A": 1ohpaqirbsjtcukvdwlxeymzfAnBg
61 return "traffic" 1abcdefg
62 if record.type == "TXT": 1ohpaqirbsjtcukvdwlxeymzfAnBg
63 return "ownership" if "_fc-dcv" in (record.name or "") else "certificate" 1ohpaqirbsjtcukvdwlxeymzfAnBg
64 return "certificate" if "_acme-challenge" in (record.name or "") else "traffic" 1ohpaqirbsjtcukvdwlxeymzfAnBg
67def _step_status(status: CustomDomainStatus, phase: Phase) -> StepStatus: 1CDEFGHI
68 current_phase = PHASE_ORDER[_phase_from_status(status)] 1ohpaqirbsjtcukvdwlxeymzfAnBg
69 step_phase = PHASE_ORDER[phase] 1ohpaqirbsjtcukvdwlxeymzfAnBg
70 if current_phase > step_phase: 1ohpaqirbsjtcukvdwlxeymzfAnBg
71 return "verified" 1haibjckdlemfng
72 if current_phase < step_phase: 1ohpaqirbsjtcukvdwlxeymzfAnBg
73 return "locked" 1ohpaqirbsjtcukvdwlxeymzfAnBg
74 if status == CustomDomainStatus.origin_setup_success: 1ohpaqirbsjtcukvdwlxeymzfAnBg
75 return "verified" 1abcdefg
76 if status in ATTENTION_STATUSES: 1ohpaqirbsjtcukvdwlxeymzfAnBg
77 return "attention" 1abcdefg
78 if status in FAILED_STATUSES: 1ohpaqirbsjtcukvdwlxeymzfAnBg
79 return "failed" 1abcdefg
80 return "in_progress" 1ohpaqirbsjtcukvdwlxeymzfAnBg
83def _describe_step( 1CDEFGHI
84 step_id: StepID,
85 status: StepStatus,
86 *,
87 is_apex: bool = False,
88) -> str:
89 if step_id == "ownership": 2Q R o h S T = ? p U a J V W q i X Y @ [ r Z b K 0 1 s j 2 3 ] ^ t 4 c L 5 6 u k 7 8 _ ` v 9 d M ! # w l $ % { | x ' e N ( ) y m * + } ~ z , f O - . A n / : abbbB ; g P
90 if status == "attention": 1ohpaqirbsjtcukvdwlxeymzfAnBg
91 return ( 1abcdefg
92 "We found this record, but its value doesn't match. Update it to "
93 "the value below. We re-check every minute, no restart needed."
94 )
95 if status == "failed": 1ohpaqirbsjtcukvdwlxeymzfAnBg
96 return ( 1abcdefg
97 "We couldn't confirm ownership. Re-check the value below, then "
98 "restart verification."
99 )
100 if status == "verified": 1ohpaqirbsjtcukvdwlxeymzfAnBg
101 return "Ownership confirmed." 1haibjckdlemfng
102 return ( 1ohpaqirbsjtcukvdwlxeymzfAnBg
103 "Add this TXT record. We check every minute and unlock the next step "
104 "automatically."
105 )
107 if step_id == "certificate": 2Q R o h S T = ? p U a J V W q i X Y @ [ r Z b K 0 1 s j 2 3 ] ^ t 4 c L 5 6 u k 7 8 _ ` v 9 d M ! # w l $ % { | x ' e N ( ) y m * + } ~ z , f O - . A n / : abbbB ; g P
108 if status == "locked": 1ohpaqirbsjtcukvdwlxeymzfAnBg
109 return "Unlocks once ownership is verified." 1ohpaqirbsjtcukvdwlxeymzfAnBg
110 if status == "failed": 1haibjckdlemfng
111 return ( 1abcdefg
112 "We couldn't secure your domain. Re-check the records below, then "
113 "restart verification."
114 )
115 if status == "verified": 1haibjckdlemfng
116 return "Domain verified and TLS certificate issued." 1haibjckdlemfng
117 return ( 1haibjckdlemfng
118 "Add both records so we can issue your TLS certificate. Your live site "
119 "doesn't change yet."
120 )
122 if step_id == "traffic": 2Q R o h S T = ? p U a J V W q i X Y @ [ r Z b K 0 1 s j 2 3 ] ^ t 4 c L 5 6 u k 7 8 _ ` v 9 d M ! # w l $ % { | x ' e N ( ) y m * + } ~ z , f O - . A n / : abbbB ; g P
123 if status == "locked": 1ohpaqirbsjtcukvdwlxeymzfAnBg
124 return ( 1ohpaqirbsjtcukvdwlxeymzfAnBg
125 "Unlocks once your domain is secured. Your live site won't change "
126 "until you add the records here."
127 )
128 if status == "attention": 1haibjckdlemfng
129 return ( 1abcdefg
130 "We found these records, but they don't match. Update them to the "
131 "values below. We re-check automatically."
132 )
133 if status == "failed": 1haibjckdlemfng
134 return ( 1abcdefg
135 "We couldn't confirm your traffic records. Re-check them below, then "
136 "restart verification."
137 )
138 if status == "verified": 1haibjckdlemfng
139 return "Your domain is live on FastAPI Cloud." 1abcdefg
140 return "Add these records to move traffic to FastAPI Cloud with no downtime." 1haibjckdlemfng
142 if status == "attention": 2Q R S T = ? U J V W X Y @ [ Z K 0 1 2 3 ] ^ 4 L 5 6 7 8 _ ` 9 M ! # $ % { | ' N ( ) * + } ~ , O - . / : abbb; P
143 return ( 2= J @ K ] L _ M { N } O abP
144 "We found your records, but some values don't match. Update them to the "
145 "values below. We re-check automatically."
146 )
147 if status == "failed": 2Q R S T ? U J V W X Y [ Z K 0 1 2 3 ^ 4 L 5 6 7 8 ` 9 M ! # $ % | ' N ( ) * + ~ , O - . / : bb; P
148 return ( 2? J [ K ^ L ` M | N ~ O bbP
149 "We couldn't complete setup. Re-check the records below, then restart "
150 "verification."
151 )
152 if status == "verified": 1QRSTUJVWXYZK01234L56789M!#$%'N()*+,O-./:;P
153 return "Your domain is live on FastAPI Cloud." 1JKLMNOP
154 if is_apex: 1QRSTUJVWXYZK01234L56789M!#$%'N()*+,O-./:;P
155 return ( 1JKLMNOP
156 "Add the records below. We'll verify ownership, issue your TLS "
157 "certificate, and route traffic automatically."
158 )
159 return ( 1QRSTUJVWXYZK01234L56789M!#$%'N()*+,O-./:;P
160 "Add the record below. We'll verify ownership, issue your TLS certificate, "
161 "and route traffic automatically."
162 )
165def get_setup_steps(domain: CustomDomain) -> list[SetupStep]: 1CDEFGHI
166 is_apex = any(record.type == "A" for record in domain.dns_records) 2Q R o h S T = ? p U a J V W q i X Y @ [ r Z b K 0 1 s j 2 3 ] ^ t 4 c L 5 6 u k 7 8 _ ` v 9 d M ! # w l $ % { | x ' e N ( ) y m * + } ~ z , f O - . A n / : abbbB ; g P
168 if not domain.is_using_pre_validation: 2Q R o h S T = ? p U a J V W q i X Y @ [ r Z b K 0 1 s j 2 3 ] ^ t 4 c L 5 6 u k 7 8 _ ` v 9 d M ! # w l $ % { | x ' e N ( ) y m * + } ~ z , f O - . A n / : abbbB ; g P
169 if domain.setup_successful: 2Q R S T = ? U J V W X Y @ [ Z K 0 1 2 3 ] ^ 4 L 5 6 7 8 _ ` 9 M ! # $ % { | ' N ( ) * + } ~ , O - . / : abbb; P
170 status: StepStatus = "verified" 1JKLMNOP
171 elif domain.setup_failed: 2Q R S T = ? U J V W X Y @ [ Z K 0 1 2 3 ] ^ 4 L 5 6 7 8 _ ` 9 M ! # $ % { | ' N ( ) * + } ~ , O - . / : abbb; P
172 status = "failed" 2? J [ K ^ L ` M | N ~ O bbP
173 elif domain.status in ATTENTION_STATUSES: 2Q R S T = U J V W X Y @ Z K 0 1 2 3 ] 4 L 5 6 7 8 _ 9 M ! # $ % { ' N ( ) * + } , O - . / : ab; P
174 status = "attention" 2= J @ K ] L _ M { N } O abP
175 else:
176 status = "in_progress" 1QRSTUJVWXYZK01234L56789M!#$%'N()*+,O-./:;P
178 return [ 2Q R S T = ? U J V W X Y @ [ Z K 0 1 2 3 ] ^ 4 L 5 6 7 8 _ ` 9 M ! # $ % { | ' N ( ) * + } ~ , O - . / : abbb; P
179 SetupStep(
180 id="combined",
181 title="Verify ownership and route traffic",
182 description=_describe_step("combined", status, is_apex=is_apex),
183 status=status,
184 records=domain.dns_records,
185 )
186 ]
188 def records_for(group: RecordGroup) -> list[CustomDomainRecord]: 1ohpaqirbsjtcukvdwlxeymzfAnBg
189 return [ 1ohpaqirbsjtcukvdwlxeymzfAnBg
190 record for record in domain.dns_records if _record_group(record) == group
191 ]
193 def build_step( 1ohpaqirbsjtcukvdwlxeymzfAnBg
194 step_id: PhasedStepID,
195 title: str,
196 phase: Phase,
197 ) -> SetupStep:
198 status = _step_status(domain.status, phase) 1ohpaqirbsjtcukvdwlxeymzfAnBg
199 return SetupStep( 1ohpaqirbsjtcukvdwlxeymzfAnBg
200 id=step_id,
201 title=title,
202 description=_describe_step(step_id, status),
203 status=status,
204 records=records_for(step_id),
205 )
207 return [ 1ohpaqirbsjtcukvdwlxeymzfAnBg
208 build_step("ownership", "Prove ownership", "internal"),
209 build_step("certificate", "Secure your domain", "external"),
210 build_step("traffic", "Switch traffic", "origin"),
211 ]