|
| 1 | +import { LOCALSTACK_BASE_URL } from "../../core/config"; |
1 | 2 | import { httpClient, HttpError } from "../../core/http-client"; |
2 | 3 |
|
3 | 4 | export type ApiResult<T> = |
@@ -58,6 +59,13 @@ export interface AppInspectorSetStatusResponse { |
58 | 59 | export type AppInspectorStatus = "enabled" | "disabled"; |
59 | 60 | export type AppInspectorQuery = Record<string, string | number | undefined>; |
60 | 61 |
|
| 62 | +export interface StateExportResult { |
| 63 | + content: Buffer; |
| 64 | + services: string[]; |
| 65 | + size: number; |
| 66 | + contentLength?: number; |
| 67 | +} |
| 68 | + |
61 | 69 | // Chaos API Client |
62 | 70 | export class ChaosApiClient { |
63 | 71 | private async makeRequest( |
@@ -184,8 +192,116 @@ export class CloudPodsApiClient { |
184 | 192 | deletePod(podName: string) { |
185 | 193 | return this.makeRequest(`/_localstack/pods/${encodeURIComponent(podName)}`, "DELETE", true, {}); |
186 | 194 | } |
187 | | - resetState() { |
188 | | - return this.makeRequest("/_localstack/state/reset", "POST", false, {}); |
| 195 | +} |
| 196 | + |
| 197 | +// Local file-based State Management API Client |
| 198 | +export class StateManagementApiClient { |
| 199 | + private async requestResponse( |
| 200 | + endpoint: string, |
| 201 | + options: RequestInit = {} |
| 202 | + ): Promise<ApiResult<Response>> { |
| 203 | + try { |
| 204 | + const response = await fetch(`${LOCALSTACK_BASE_URL}${endpoint}`, options); |
| 205 | + if (!response.ok) { |
| 206 | + return { |
| 207 | + success: false, |
| 208 | + message: await response.text(), |
| 209 | + statusCode: response.status, |
| 210 | + }; |
| 211 | + } |
| 212 | + return { success: true, data: response }; |
| 213 | + } catch (error) { |
| 214 | + return { |
| 215 | + success: false, |
| 216 | + message: `Failed to communicate with LocalStack State Management API: ${error instanceof Error ? error.message : "Unknown error"}`, |
| 217 | + }; |
| 218 | + } |
| 219 | + } |
| 220 | + |
| 221 | + private serviceQuery(services?: string[]) { |
| 222 | + if (!services || services.length === 0) return ""; |
| 223 | + return `?services=${encodeURIComponent(services.join(","))}`; |
| 224 | + } |
| 225 | + |
| 226 | + async exportState(services?: string[]): Promise<ApiResult<StateExportResult>> { |
| 227 | + const result = await this.requestResponse( |
| 228 | + `/_localstack/pods/state${this.serviceQuery(services)}`, |
| 229 | + { |
| 230 | + method: "GET", |
| 231 | + } |
| 232 | + ); |
| 233 | + if (!result.success) return result; |
| 234 | + |
| 235 | + const response = result.data; |
| 236 | + const content = Buffer.from(await response.arrayBuffer()); |
| 237 | + const exportedServices = (response.headers.get("x-localstack-pod-services") ?? "") |
| 238 | + .split(",") |
| 239 | + .map((service) => service.trim()) |
| 240 | + .filter(Boolean); |
| 241 | + const size = Number(response.headers.get("x-localstack-pod-size") ?? content.length); |
| 242 | + const contentLength = Number(response.headers.get("content-length") ?? content.length); |
| 243 | + |
| 244 | + return { |
| 245 | + success: true, |
| 246 | + data: { |
| 247 | + content, |
| 248 | + services: exportedServices, |
| 249 | + size: Number.isNaN(size) ? content.length : size, |
| 250 | + contentLength: Number.isNaN(contentLength) ? undefined : contentLength, |
| 251 | + }, |
| 252 | + }; |
| 253 | + } |
| 254 | + |
| 255 | + async importState(content: Buffer): Promise<ApiResult<string>> { |
| 256 | + const body = content.buffer.slice( |
| 257 | + content.byteOffset, |
| 258 | + content.byteOffset + content.byteLength |
| 259 | + ) as ArrayBuffer; |
| 260 | + const result = await this.requestResponse("/_localstack/pods", { |
| 261 | + method: "POST", |
| 262 | + headers: { "Content-Type": "application/octet-stream" }, |
| 263 | + body, |
| 264 | + }); |
| 265 | + if (!result.success) return result; |
| 266 | + return { success: true, data: await result.data.text() }; |
| 267 | + } |
| 268 | + |
| 269 | + async resetState(services?: string[]): Promise<ApiResult<void>> { |
| 270 | + if (!services || services.length === 0) { |
| 271 | + const result = await this.requestResponse("/_localstack/state/reset", { method: "POST" }); |
| 272 | + return result.success ? { success: true, data: undefined } : result; |
| 273 | + } |
| 274 | + |
| 275 | + for (const service of services) { |
| 276 | + const result = await this.requestResponse( |
| 277 | + `/_localstack/state/${encodeURIComponent(service)}/reset`, |
| 278 | + { method: "POST" } |
| 279 | + ); |
| 280 | + if (!result.success) return result; |
| 281 | + } |
| 282 | + |
| 283 | + return { success: true, data: undefined }; |
| 284 | + } |
| 285 | + |
| 286 | + async inspectState(): Promise<ApiResult<unknown>> { |
| 287 | + try { |
| 288 | + const data = await httpClient.request<unknown>("/_localstack/pods/state/metamodel", { |
| 289 | + method: "GET", |
| 290 | + }); |
| 291 | + return { success: true, data }; |
| 292 | + } catch (error) { |
| 293 | + if (error instanceof HttpError) { |
| 294 | + return { |
| 295 | + success: false, |
| 296 | + message: error.body || error.message, |
| 297 | + statusCode: error.status, |
| 298 | + }; |
| 299 | + } |
| 300 | + return { |
| 301 | + success: false, |
| 302 | + message: `Failed to communicate with LocalStack State Management API: ${error instanceof Error ? error.message : "Unknown error"}`, |
| 303 | + }; |
| 304 | + } |
189 | 305 | } |
190 | 306 | } |
191 | 307 |
|
|
0 commit comments