|
| 1 | +import { z } from 'zod' |
| 2 | +import { createOciClient, type OciAuthenticatedResponse, type OciClient } from '@/lib/internal/oci/client.server' |
| 3 | +import { createOciStaticEndpointPolicy, type OciPreparedEndpoint } from '@/lib/internal/oci/endpoints' |
| 4 | + |
| 5 | +export const OCI_RESOURCE_MANAGER_SERVICE_ID = 'oci-resource-manager' |
| 6 | +export const OCI_RESOURCE_MANAGER_POLICY = createOciStaticEndpointPolicy({ serviceId: OCI_RESOURCE_MANAGER_SERVICE_ID, serviceName: 'resourcemanager', hostnameTemplate: 'regional' }) |
| 7 | +export const OCI_RESOURCE_MANAGER_JSON_LIMIT = 6_000_000 |
| 8 | +export const OCI_RESOURCE_MANAGER_FILE_LIMIT = 100 * 1024 * 1024 |
| 9 | +export const OCI_RESOURCE_MANAGER_ZIP_LIMIT = 11_000_000 |
| 10 | +export interface PreparedOciResourceManagerClient { client: OciClient; endpoint: OciPreparedEndpoint } |
| 11 | +export class OciResourceManagerError extends Error { |
| 12 | + constructor(message: string, readonly status = 400) { super(message) } |
| 13 | +} |
| 14 | +export async function prepareOciResourceManagerClient(binding: { credentialId: string; workspaceId: string; region?: string }): Promise<PreparedOciResourceManagerClient> { |
| 15 | + const client = await createOciClient({ ...binding, serviceId: OCI_RESOURCE_MANAGER_SERVICE_ID }) |
| 16 | + return { client, endpoint: await client.prepareStaticEndpoint(OCI_RESOURCE_MANAGER_POLICY) } |
| 17 | +} |
| 18 | +export function resourcePath(kind: 'stacks' | 'jobs' | 'workRequests', id?: string, suffix = '') { |
| 19 | + return `/20180917/${kind}${id === undefined ? '' : `/${encodeURIComponent(id)}`}${suffix}` |
| 20 | +} |
| 21 | +export interface ResourceManagerRequest { |
| 22 | + method: 'GET' | 'POST' | 'PUT' | 'DELETE' |
| 23 | + path: string |
| 24 | + body?: unknown |
| 25 | + query?: readonly (readonly [string, string])[] |
| 26 | + ifMatch?: string |
| 27 | + retryToken?: string |
| 28 | + binary?: boolean |
| 29 | + expectedStatus?: number |
| 30 | +} |
| 31 | +export async function requestResourceManager(prepared: PreparedOciResourceManagerClient, request: ResourceManagerRequest, signal?: AbortSignal) { |
| 32 | + const common = { |
| 33 | + endpoint: prepared.endpoint, encodedPath: request.path, queryPairs: request.query, |
| 34 | + headers: request.ifMatch ? { 'if-match': request.ifMatch } : undefined, |
| 35 | + responseHeaders: ['opc-next-page', 'opc-work-request-id'], |
| 36 | + timeoutMs: 60_000, maxResponseBytes: request.binary ? OCI_RESOURCE_MANAGER_FILE_LIMIT : OCI_RESOURCE_MANAGER_JSON_LIMIT, signal, |
| 37 | + } |
| 38 | + const retry = request.retryToken ? { kind: 'tokenized' as const, maxAttempts: 2, retryToken: request.retryToken } : undefined |
| 39 | + const response = await prepared.client.request(request.method === 'POST' || request.method === 'PUT' |
| 40 | + ? { ...common, method: request.method, contentType: 'application/json', body: request.body === undefined ? new Uint8Array() : new TextEncoder().encode(JSON.stringify(request.body)), retry } |
| 41 | + : request.method === 'GET' |
| 42 | + ? { ...common, method: 'GET', retry: { kind: 'safe', maxAttempts: 2 } } |
| 43 | + : { ...common, method: 'DELETE' }) |
| 44 | + if (response.status !== (request.expectedStatus ?? 200)) throw new OciResourceManagerError('Unexpected OCI Resource Manager response status', 502) |
| 45 | + return response |
| 46 | +} |
| 47 | +export function responseJson(response: OciAuthenticatedResponse): unknown { |
| 48 | + try { return JSON.parse(new TextDecoder().decode(response.body)) } catch { throw new OciResourceManagerError('Invalid OCI Resource Manager JSON response', 502) } |
| 49 | +} |
| 50 | +const string = z.string().nullish() |
| 51 | +const stringMap = z.record(z.string(), z.string()).nullish() |
| 52 | +const common = { id: z.string().min(1), compartmentId: string, displayName: string, lifecycleState: string, timeCreated: string } |
| 53 | +export const stackResponseSchema = z.object({ ...common, terraformVersion: string, stackDriftStatus: string, timeDriftLastChecked: string, variables: stringMap, configSource: z.object({ configSourceType: string, workingDirectory: string, compartmentId: string, servicesToDiscover: z.array(z.string()).nullish(), configurationSourceProviderId: string, repositoryUrl: string, branchName: string, region: string, namespace: string, bucketName: string, projectId: string, repositoryId: string, workspaceId: string }).nullish() }) |
| 54 | +export const jobResponseSchema = z.object({ ...common, stackId: z.string().min(1), operation: string, timeFinished: string, failureDetails: z.object({ code: string }).nullish(), variables: stringMap, jobOperationDetails: z.object({ operation: string, executionPlanStrategy: string, executionPlanJobId: string, executionPlanRollbackStrategy: string, executionPlanRollbackJobId: string, targetRollbackJobId: string }).nullish(), cancellationDetails: z.object({ isForced: z.boolean().nullish() }).nullish(), configSource: z.object({ configSourceRecordType: string, commitId: string }).nullish() }) |
| 55 | +export const workResponseSchema = z.object({ id: z.string().min(1), compartmentId: string, operationType: string, status: string, percentComplete: z.number().nullish(), timeAccepted: string, timeStarted: string, timeFinished: string, resources: z.array(z.object({ actionType: string, entityType: string, identifier: string, entityUri: string })).nullish() }) |
| 56 | +export const logResponseSchema = z.object({ type: string, level: string, timestamp: string, message: string, code: string }) |
| 57 | +export const outputResponseSchema = z.object({ outputName: string, outputType: string, isSensitive: z.boolean().nullish(), outputValue: string }) |
| 58 | +export const associatedResponseSchema = z.object({ resourceId: string, resourceName: string, resourceType: string, resourceAddress: string, region: string, timeCreated: string, attributes: stringMap }) |
| 59 | +export const driftResponseSchema = z.object({ stackId: string, compartmentId: string, resourceId: string, resourceName: string, resourceType: string, resourceDriftStatus: string, timeDriftChecked: string, actualProperties: stringMap, expectedProperties: stringMap }) |
| 60 | +export const providerResponseSchema = z.object({ ...common, configSourceProviderType: string }) |
| 61 | +export const templateResponseSchema = z.object({ ...common, isFreeTier: z.boolean().nullish() }) |
| 62 | +export const versionResponseSchema = z.object({ name: z.string(), isDefault: z.boolean().nullish() }) |
| 63 | +export const discoveryResponseSchema = z.object({ name: z.string(), discoveryScope: string }) |
| 64 | +export function parseResponse<T>(schema: z.ZodType<T>, value: unknown): T { |
| 65 | + const result = schema.safeParse(value) |
| 66 | + if (!result.success) throw new OciResourceManagerError('Unexpected OCI Resource Manager response shape', 502) |
| 67 | + return result.data |
| 68 | +} |
0 commit comments