ReviewOS

also looking at this

stacks/ts-cloud

fix(operations): the inventory read a location Hetzner no longer sends

#193
Merged glennmichael123 wants to merge fix/inventory-provider-payload-type into main
2 files +69 -2
packages/ts-cloud/src/operations/inventory.tsmodified+49-2
Changes to packages/ts-cloud/src/operations/inventory.ts
@@ -79,6 +79,53 @@ function text(value: unknown): string | undefined {
7979 return typeof value === 'string' && value.trim() ? value.trim() : undefined
8080}
8181
82/**
83 * One provider server record, as a listing returns it.
84 *
85 * Every field is optional and `unknown` because this is JSON from an external
86 * API: `text()` is what turns each one into a string or nothing. The nested
87 * fields are Hetzner's spelling; the flat ones beside them are the fallback a
88 * driver with a simpler listing can answer with, which is what lets one type
89 * cover more than one provider.
90 */
91export interface ProviderServerPayload {
92 id?: unknown
93 name?: unknown
94 status?: unknown
95 labels?: Record<string, unknown>
96 public_net?: { ipv4?: { ip?: unknown } | null, ipv6?: { ip?: unknown } | null } | null
97 server_type?: { name?: unknown } | null
98 /**
99 * Legacy shape. Hetzner has stopped returning this and sends an explicit
100 * `null` in its place, which is why the null belongs in the type: it is what
101 * a live listing contains today. See `location`.
102 */
103 datacenter?: { name?: unknown, location?: { name?: unknown } | null } | null
104 ipv4?: unknown
105 ipv6?: unknown
106 type?: unknown
107 /**
108 * Current Hetzner shape is an object; a simpler driver may answer a bare
109 * string. {@link placeName} reads both.
110 */
111 location?: unknown
112}
113
114/**
115 * A place name that arrives either as `{ name }` or as a bare string.
116 *
117 * Hetzner's current listing nests it and its retired `datacenter` field did
118 * too, so reading only the string form reported no location at all for every
119 * live box. `resize.ts` and `role-swap.ts` already carry the same fallback.
120 */
121function placeName(value: unknown): string | undefined {
122 if (typeof value === 'string')
123 return text(value)
124 if (value && typeof value === 'object')
125 return text((value as { name?: unknown }).name)
126 return undefined
127}
128
82129/**
83130 * Shape one provider server record into {@link InventoryServer}.
84131 *
@@ -90,7 +137,7 @@ function text(value: unknown): string | undefined {
90137 * or by a ts-cloud old enough not to have labelled it, is exactly the kind a
91138 * consolidation needs to see; requiring the labels would hide it.
92139 */
93export function toInventoryServer(raw: any): InventoryServer {
140export function toInventoryServer(raw: ProviderServerPayload | null | undefined): InventoryServer {
94141 const labels: Record<string, string> = {}
95142 for (const [key, value] of Object.entries(raw?.labels ?? {})) {
96143 if (typeof value === 'string') labels[key] = value
@@ -103,7 +150,7 @@ export function toInventoryServer(raw: any): InventoryServer {
103150 ipv4: text(raw?.public_net?.ipv4?.ip) ?? text(raw?.ipv4),
104151 ipv6: text(raw?.public_net?.ipv6?.ip) ?? text(raw?.ipv6),
105152 type: text(raw?.server_type?.name) ?? text(raw?.type),
106 location: text(raw?.datacenter?.location?.name) ?? text(raw?.datacenter?.name) ?? text(raw?.location),
153 location: placeName(raw?.location) ?? placeName(raw?.datacenter?.location) ?? placeName(raw?.datacenter),
107154 labels,
108155 project: text(labels[PROJECT_LABEL]),
109156 environment: text(labels[ENVIRONMENT_LABEL]),