import * as deadline from "@distilled.cloud/aws/deadline"; import * as Effect from "effect/Effect"; import * as EffectStream from "effect/Stream"; import { isResolved } from "../../Diff.ts"; import { createPhysicalName } from "../../PhysicalName.ts"; import * as Provider from "../../Provider.ts"; import { Resource } from "../../Resource.ts"; import type { Providers } from "../Providers.ts"; import { retryWhileFarmSettling } from "./internal.ts"; export type StorageProfileOperatingSystemFamily = deadline.StorageProfileOperatingSystemFamily; export type FileSystemLocation = deadline.FileSystemLocation; export interface StorageProfileProps { /** * The identifier of the farm the storage profile belongs to. Changing it * replaces the storage profile. */ farmId: string; /** * Display name of the storage profile. * @default ${app}-${stage}-${id} */ displayName?: string; /** * Operating system family of the hosts the profile describes * (`WINDOWS`, `LINUX`, `MACOS`). */ osFamily: StorageProfileOperatingSystemFamily; /** * Shared or local file system locations available on hosts using this * profile. */ fileSystemLocations?: FileSystemLocation[]; } export interface StorageProfile extends Resource< "AWS.Deadline.StorageProfile", StorageProfileProps, { /** * The identifier of the farm the storage profile belongs to. */ farmId: string; /** * Service-assigned unique identifier of the storage profile (`sp-...`). */ storageProfileId: string; /** * The storage profile's display name. */ displayName: string; /** * The configured operating system family. */ osFamily: StorageProfileOperatingSystemFamily; /** * The configured file system locations. */ fileSystemLocations: FileSystemLocation[]; }, never, Providers > {} /** * An AWS Deadline Cloud storage profile — describes the operating system * and file system locations of the hosts in a farm so path mapping works * across mixed environments. * * ### Creating Storage Profiles * **Example:** Linux Storage Profile * ```typescript * import * as AWS from "alchemy/AWS"; * * const profile = yield* AWS.Deadline.StorageProfile("LinuxHosts", { * farmId: farm.farmId, * osFamily: "LINUX", * fileSystemLocations: [ * { name: "Assets", path: "/mnt/assets", type: "SHARED" }, * ], * }); * ``` * * **Example:** Cross-Platform Path Mapping * ```typescript * // A second profile in the same farm maps the same shared location to its * // Windows drive path, so jobs submitted from either OS resolve `Assets`. * const windows = yield* AWS.Deadline.StorageProfile("WindowsHosts", { * farmId: farm.farmId, * osFamily: "WINDOWS", * fileSystemLocations: [ * { name: "Assets", path: "Z:\\assets", type: "SHARED" }, * ], * }); * ``` * * @resource */ export const StorageProfile = Resource( "AWS.Deadline.StorageProfile", ); const createStorageProfileName = ( id: string, props: { displayName?: string | undefined }, ) => props.displayName ? Effect.succeed(props.displayName) : createPhysicalName({ id, maxLength: 64 }); interface StorageProfileState { attrs: StorageProfile["Attributes"]; described: deadline.GetStorageProfileResponse; } const readStorageProfileById = Effect.fn(function* ( farmId: string, storageProfileId: string, ) { const described = yield* deadline .getStorageProfile({ farmId, storageProfileId }) .pipe( Effect.catchTag("ResourceNotFoundException", () => Effect.succeed(undefined), ), ); if (!described) return undefined; const state: StorageProfileState = { described, attrs: { farmId, storageProfileId: described.storageProfileId, displayName: described.displayName, osFamily: described.osFamily, fileSystemLocations: [...(described.fileSystemLocations ?? [])], }, }; return state; }); const findStorageProfileByDisplayName = Effect.fn(function* ( farmId: string, displayName: string, ) { const summaries = yield* deadline.listStorageProfiles.items({ farmId }).pipe( EffectStream.runCollect, Effect.map((chunk) => Array.from(chunk)), // The parent farm may itself be gone. Effect.catchTag("ResourceNotFoundException", () => Effect.succeed([] as deadline.StorageProfileSummary[]), ), ); const match = summaries.find( (summary) => summary.displayName === displayName, ); if (!match) return undefined; return yield* readStorageProfileById(farmId, match.storageProfileId); }); const locationKey = (location: FileSystemLocation) => `${location.name}${location.path}${location.type}`; export const StorageProfileProvider = () => Provider.effect( StorageProfile, Effect.gen(function* () { return { stables: ["farmId", "storageProfileId"], // Keyed by a parent farm — sub-resource list() convention. list: () => Effect.succeed([]), // Storage profiles have no tag support, so ownership cannot be // verified out of band — a display-name match is treated as ours. read: Effect.fn(function* ({ id, olds, output }) { const farmId = output?.farmId ?? olds?.farmId; if (farmId === undefined) return undefined; const state = output?.storageProfileId ? yield* readStorageProfileById(farmId, output.storageProfileId) : yield* findStorageProfileByDisplayName( farmId, yield* createStorageProfileName(id, olds ?? {}), ); return state?.attrs; }), diff: Effect.fn(function* ({ news, olds }) { if (!isResolved(news)) return; if (olds === undefined) return; // The parent farm is fixed at creation. if (olds.farmId !== news.farmId) { return { action: "replace" } as const; } }), reconcile: Effect.fn(function* ({ id, news, output, session }) { if (news === undefined) { return yield* Effect.fail( new Error("AWS.Deadline.StorageProfile requires props"), ); } const farmId = news.farmId; const displayName = yield* createStorageProfileName(id, news); // Observe. let state = output?.storageProfileId ? yield* readStorageProfileById(farmId, output.storageProfileId) : yield* findStorageProfileByDisplayName(farmId, displayName); // Ensure. if (state === undefined) { const created = yield* retryWhileFarmSettling( deadline.createStorageProfile({ farmId, displayName, osFamily: news.osFamily, fileSystemLocations: news.fileSystemLocations, }), ); yield* session.note( `Created storage profile ${displayName} (${created.storageProfileId})`, ); state = yield* readStorageProfileById( farmId, created.storageProfileId, ); if (state === undefined) { return yield* Effect.fail( new Error( `failed to read created storage profile ${displayName}`, ), ); } } // Sync — compute the file-system-location delta from OBSERVED state. const observedLocations = state.described.fileSystemLocations ?? []; const desiredLocations = news.fileSystemLocations ?? []; const observedKeys = new Set(observedLocations.map(locationKey)); const desiredKeys = new Set(desiredLocations.map(locationKey)); const toAdd = desiredLocations.filter( (location) => !observedKeys.has(locationKey(location)), ); const toRemove = observedLocations.filter( (location) => !desiredKeys.has(locationKey(location)), ); const needsUpdate = displayName !== state.described.displayName || news.osFamily !== state.described.osFamily || toAdd.length > 0 || toRemove.length > 0; if (needsUpdate) { yield* retryWhileFarmSettling( deadline.updateStorageProfile({ farmId, storageProfileId: state.attrs.storageProfileId, displayName, osFamily: news.osFamily, fileSystemLocationsToAdd: toAdd.length > 0 ? toAdd : undefined, fileSystemLocationsToRemove: toRemove.length > 0 ? toRemove : undefined, }), ); yield* session.note(`Updated storage profile ${displayName}`); } yield* session.note(state.attrs.storageProfileId); const final = yield* readStorageProfileById( farmId, state.attrs.storageProfileId, ); if (!final) { return yield* Effect.fail( new Error( `failed to read reconciled storage profile ${displayName}`, ), ); } return final.attrs; }), delete: Effect.fn(function* ({ output }) { yield* deadline .deleteStorageProfile({ farmId: output.farmId, storageProfileId: output.storageProfileId, }) .pipe( Effect.catchTag("ResourceNotFoundException", () => Effect.void), ); }), }; }), );